mirror of
https://github.com/project-gauntlet/gauntlet.git
synced 2025-12-23 10:35:53 +00:00
Localized application names on macOS (#50)
Co-authored-by: Exidex <16986685+exidex@users.noreply.github.com>
This commit is contained in:
parent
0c6b0282c2
commit
652af98544
10 changed files with 159 additions and 45 deletions
|
|
@ -9,6 +9,12 @@ For changes in `@project-gauntlet/tools` see [separate CHANGELOG.md](https://git
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Added localization support for macOS application names
|
||||
- Added plugin preference `Bundle Name Lang` of enum type
|
||||
- `localized` option - use localized name of bundle if available - this is the default
|
||||
- `default` option - use default name of bundle (usually english)
|
||||
- On macOS use app stem name as a fallback if the bundle name is empty
|
||||
- Fixes empty names of some apps, like "Creality Print" which have an empty bundle name
|
||||
- Added --version flag to CLI to display Gauntlet version
|
||||
- Slightly improved close-on-unfocus behaviour of main window on X11
|
||||
- Global shortcut is now executed on key press, instead of key release
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4109,6 +4109,7 @@ dependencies = [
|
|||
"resvg",
|
||||
"serde",
|
||||
"smithay-client-toolkit",
|
||||
"sys-locale",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,17 @@ type = 'bool'
|
|||
default = true
|
||||
description = "Enables experimental window tracking"
|
||||
|
||||
[[entrypoint.preferences]]
|
||||
id = "bundleNameLang"
|
||||
name = "Bundle Name Lang"
|
||||
type = "enum"
|
||||
default = "localized"
|
||||
description = "Language of the bundle name"
|
||||
enum_values = [
|
||||
{ label = 'Default', value = 'default' },
|
||||
{ label = 'Localized', value = 'localized' },
|
||||
]
|
||||
|
||||
[[entrypoint]]
|
||||
id = 'windows'
|
||||
name = 'All Open Windows'
|
||||
|
|
@ -46,7 +57,7 @@ read = [
|
|||
# technically only uses locations defined by XDG Desktop Entry Specification, but
|
||||
# the spec allows for customization via XDG_DATA_DIRS and XDG_DATA_HOME env vars so it can be any path
|
||||
"/",
|
||||
"C:\\"
|
||||
"C:\\",
|
||||
]
|
||||
|
||||
[[supported_system]]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
macos_app_from_arbitrary_path,
|
||||
macos_app_from_path,
|
||||
macos_application_dirs,
|
||||
macos_get_localized_language,
|
||||
macos_major_version,
|
||||
macos_open_application,
|
||||
macos_open_setting_13_and_post,
|
||||
|
|
@ -20,10 +21,25 @@ import { applicationEventLoopX11, focusX11Window } from "./window/x11";
|
|||
import { applicationEventLoopWayland, focusWaylandWindow } from "./window/wayland";
|
||||
import { windows_app_from_path, windows_application_dirs, windows_open_application } from "gauntlet:bridge/internal-windows";
|
||||
|
||||
type EntrypointPreferences = { experimentalWindowTracking: boolean };
|
||||
type EntrypointPreferences = { experimentalWindowTracking: boolean, bundleNameLang: "default" | "localized" };
|
||||
|
||||
export default async function Applications(context: GeneratorContext<object, EntrypointPreferences>): Promise<void | (() => void)> {
|
||||
const { add, remove, get, getAll, entrypointPreferences: { experimentalWindowTracking } } = context;
|
||||
const { add, remove, get, getAll, entrypointPreferences: { experimentalWindowTracking, bundleNameLang } } = context;
|
||||
let lang: string | undefined;
|
||||
|
||||
switch (bundleNameLang) {
|
||||
case "default": {
|
||||
lang = undefined;
|
||||
break;
|
||||
}
|
||||
case "localized": {
|
||||
lang = macos_get_localized_language();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unknown bundle name type")
|
||||
}
|
||||
}
|
||||
|
||||
switch (current_os()) {
|
||||
case "linux": {
|
||||
|
|
@ -105,7 +121,7 @@ export default async function Applications(context: GeneratorContext<object, Ent
|
|||
const majorVersion = macos_major_version();
|
||||
|
||||
if (majorVersion >= 13) {
|
||||
for (const setting of macos_settings_13_and_post()) {
|
||||
for (const setting of macos_settings_13_and_post(lang)) {
|
||||
add(`settings:${setting.preferences_id}`, {
|
||||
name: setting.name,
|
||||
actions: [
|
||||
|
|
@ -137,7 +153,7 @@ export default async function Applications(context: GeneratorContext<object, Ent
|
|||
}
|
||||
|
||||
for (const path of macos_system_applications()) {
|
||||
const app = await macos_app_from_path(path)
|
||||
const app = await macos_app_from_path(path, lang)
|
||||
if (app) {
|
||||
switch (app.type) {
|
||||
case "add": {
|
||||
|
|
@ -164,7 +180,7 @@ export default async function Applications(context: GeneratorContext<object, Ent
|
|||
|
||||
return await genericGenerator<MacOSDesktopApplicationData>(
|
||||
macos_application_dirs(),
|
||||
path => macos_app_from_arbitrary_path(path),
|
||||
path => macos_app_from_arbitrary_path(path, lang),
|
||||
(_id, data) => ({
|
||||
name: data.name,
|
||||
actions: [
|
||||
|
|
@ -279,5 +295,3 @@ async function genericGenerator<DATA>(
|
|||
watcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export {
|
|||
macos_application_dirs,
|
||||
macos_major_version,
|
||||
macos_open_application,
|
||||
macos_get_localized_language,
|
||||
macos_open_setting_13_and_post,
|
||||
macos_open_setting_pre_13,
|
||||
macos_settings_13_and_post,
|
||||
|
|
|
|||
16
js/typings/index.d.ts
vendored
16
js/typings/index.d.ts
vendored
|
|
@ -162,15 +162,16 @@ declare module "gauntlet:bridge/internal-linux" {
|
|||
declare module "gauntlet:bridge/internal-macos" {
|
||||
function macos_major_version(): number
|
||||
function macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[]
|
||||
function macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[]
|
||||
function macos_settings_13_and_post(lang: string | undefined): MacOSDesktopSettings13AndPostData[]
|
||||
function macos_open_setting_13_and_post(preferences_id: String): void
|
||||
function macos_open_setting_pre_13(setting_path: String): void
|
||||
|
||||
function macos_system_applications(): string[]
|
||||
function macos_application_dirs(): string[]
|
||||
function macos_app_from_path(path: string): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_app_from_arbitrary_path(path: string): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_app_from_path(path: string, lang: string | undefined): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_app_from_arbitrary_path(path: string, lang: string | undefined): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_open_application(app_path: String): void
|
||||
function macos_get_localized_language(): string | undefined
|
||||
}
|
||||
|
||||
declare module "gauntlet:bridge/internal-windows" {
|
||||
|
|
@ -196,15 +197,16 @@ declare module "ext:core/ops" {
|
|||
|
||||
function macos_major_version(): number
|
||||
function macos_settings_pre_13(): MacOSDesktopSettingsPre13Data[]
|
||||
function macos_settings_13_and_post(): MacOSDesktopSettings13AndPostData[]
|
||||
function macos_settings_13_and_post(lang: string | undefined): MacOSDesktopSettings13AndPostData[]
|
||||
function macos_open_setting_13_and_post(preferences_id: String): void
|
||||
function macos_open_setting_pre_13(setting_path: String): void
|
||||
|
||||
function macos_system_applications(): string[]
|
||||
function macos_application_dirs(): string[]
|
||||
function macos_app_from_path(path: string): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_app_from_arbitrary_path(path: string): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_app_from_path(path: string, lang: string | undefined): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_app_from_arbitrary_path(path: string, lang: string | undefined): Promise<undefined | DesktopPathAction<MacOSDesktopApplicationData>>
|
||||
function macos_open_application(app_path: String): void
|
||||
function macos_get_localized_language(): string | undefined
|
||||
|
||||
function windows_application_dirs(): string[]
|
||||
function windows_open_application(path: string): void
|
||||
|
|
@ -479,4 +481,4 @@ type X11ApplicationEventDesktopFileNamePropertyNotify = {
|
|||
type: "DesktopFileNamePropertyNotify",
|
||||
id: X11WindowId,
|
||||
desktop_file_name: string
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ numbat = "1.14.0"
|
|||
which = "7.0.1"
|
||||
uuid = "1.11.0"
|
||||
open = "5"
|
||||
sys-locale = "0.3.2"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ deno_core::extension!(
|
|||
crate::plugins::applications::macos_app_from_arbitrary_path,
|
||||
crate::plugins::applications::macos_app_from_path,
|
||||
crate::plugins::applications::macos_open_application,
|
||||
crate::plugins::applications::macos_get_localized_language,
|
||||
],
|
||||
esm_entry_point = "ext:gauntlet/internal-macos/bootstrap.js",
|
||||
esm = [
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use image::imageops::FilterType;
|
|||
use image::ImageFormat;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sys_locale::get_locale;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
|
@ -140,15 +141,21 @@ pub fn macos_major_version() -> u8 {
|
|||
#[cfg(target_os = "macos")]
|
||||
#[op2(async)]
|
||||
#[serde]
|
||||
pub async fn macos_app_from_path(#[string] path: String) -> anyhow::Result<Option<DesktopPathAction>> {
|
||||
Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path))).await?)
|
||||
pub async fn macos_app_from_path(
|
||||
#[string] path: String,
|
||||
#[string] lang: Option<String>,
|
||||
) -> anyhow::Result<Option<DesktopPathAction>> {
|
||||
Ok(spawn_blocking(|| macos::macos_app_from_path(&PathBuf::from(path), lang)).await?)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[op2(async)]
|
||||
#[serde]
|
||||
pub async fn macos_app_from_arbitrary_path(#[string] path: String) -> anyhow::Result<Option<DesktopPathAction>> {
|
||||
Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path))).await?)
|
||||
pub async fn macos_app_from_arbitrary_path(
|
||||
#[string] path: String,
|
||||
#[string] lang: Option<String>,
|
||||
) -> anyhow::Result<Option<DesktopPathAction>> {
|
||||
Ok(spawn_blocking(|| macos::macos_app_from_arbitrary_path(PathBuf::from(path), lang)).await?)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -189,8 +196,8 @@ pub fn macos_settings_pre_13() -> Vec<DesktopSettingsPre13Data> {
|
|||
#[cfg(target_os = "macos")]
|
||||
#[op2]
|
||||
#[serde]
|
||||
pub fn macos_settings_13_and_post() -> Vec<DesktopSettings13AndPostData> {
|
||||
macos::macos_settings_13_and_post()
|
||||
pub fn macos_settings_13_and_post(#[string] lang: Option<String>) -> Vec<DesktopSettings13AndPostData> {
|
||||
macos::macos_settings_13_and_post(lang)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -209,6 +216,17 @@ pub fn macos_open_setting_pre_13(#[string] setting_path: String) -> anyhow::Resu
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[op2]
|
||||
#[string]
|
||||
pub fn macos_get_localized_language() -> Option<String> {
|
||||
get_locale()?
|
||||
.split("-")
|
||||
.collect::<Vec<&str>>()
|
||||
.get(0)
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn spawn_detached<I, S>(path: &str, args: I) -> std::io::Result<()>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ use objc2_foundation::NSSize;
|
|||
use objc2_foundation::NSString;
|
||||
use objc2_foundation::NSZeroRect;
|
||||
use plist::Dictionary;
|
||||
use plist::Value;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ pub fn macos_application_dirs() -> Vec<PathBuf> {
|
|||
all_applications
|
||||
}
|
||||
|
||||
pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option<DesktopPathAction> {
|
||||
pub fn macos_app_from_arbitrary_path(path: PathBuf, lang: Option<String>) -> Option<DesktopPathAction> {
|
||||
let path = path
|
||||
.ancestors()
|
||||
.into_iter()
|
||||
|
|
@ -123,29 +124,68 @@ pub fn macos_app_from_arbitrary_path(path: PathBuf) -> Option<DesktopPathAction>
|
|||
return None;
|
||||
};
|
||||
|
||||
macos_app_from_path(path)
|
||||
macos_app_from_path(path, lang)
|
||||
}
|
||||
|
||||
pub fn macos_app_from_path(path: &Path) -> Option<DesktopPathAction> {
|
||||
if !path.is_dir() {
|
||||
return None;
|
||||
}
|
||||
fn get_bundle_name(app_path: &Path) -> String {
|
||||
let info_path = app_path.join("Contents").join("Info.plist");
|
||||
|
||||
let name = path
|
||||
let info: Option<Info> = plist::from_file(info_path).ok();
|
||||
|
||||
let fallback_name = app_path
|
||||
.file_stem()
|
||||
.expect(&format!("invalid path: {:?}", path))
|
||||
.expect(&format!("invalid path: {:?}", app_path))
|
||||
.to_str()
|
||||
.expect("non-uft8 paths are not supported")
|
||||
.to_string();
|
||||
|
||||
let info_path = path.join("Contents").join("Info.plist");
|
||||
|
||||
let info: Option<Info> = plist::from_file(info_path).ok();
|
||||
|
||||
let name = info
|
||||
let mut bundle_name = info
|
||||
.as_ref()
|
||||
.and_then(|info| info.bundle_display_name.clone().or_else(|| info.bundle_name.clone()))
|
||||
.unwrap_or(name);
|
||||
.unwrap_or(fallback_name.clone());
|
||||
|
||||
if bundle_name.is_empty() {
|
||||
bundle_name = fallback_name;
|
||||
}
|
||||
|
||||
bundle_name
|
||||
}
|
||||
|
||||
fn get_localized_name(path: &Path, preferred_language: &str) -> Option<String> {
|
||||
let localized_info: Option<InfoPlist> = plist::from_file(path).ok();
|
||||
|
||||
if let Some(localized_info) = localized_info {
|
||||
// get language info. first try to use display name, if not available use name
|
||||
localized_info
|
||||
.languages
|
||||
.get(preferred_language)
|
||||
.and_then(|localized_info| {
|
||||
localized_info
|
||||
.bundle_display_name
|
||||
.clone()
|
||||
.or_else(|| localized_info.bundle_name.clone())
|
||||
})
|
||||
} else {
|
||||
eprintln!("Error: Could not load plist from path '{}'.", path.display());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn macos_app_from_path(path: &Path, lang: Option<String>) -> Option<DesktopPathAction> {
|
||||
if !path.is_dir() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = lang
|
||||
.and_then(|l| {
|
||||
let info_plist_path = path.join("Contents/Resources/InfoPlist.loctable");
|
||||
if info_plist_path.is_file() {
|
||||
get_localized_name(info_plist_path.as_path(), &l)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(get_bundle_name(path));
|
||||
|
||||
let icon = get_application_icon(&path)
|
||||
.inspect_err(|err| tracing::error!("error while reading application icon for {:?}: {:?}", path, err))
|
||||
|
|
@ -198,7 +238,7 @@ pub fn macos_settings_pre_13() -> Vec<DesktopSettingsPre13Data> {
|
|||
all_settings
|
||||
}
|
||||
|
||||
pub fn macos_settings_13_and_post() -> Vec<DesktopSettings13AndPostData> {
|
||||
pub fn macos_settings_13_and_post(lang: Option<String>) -> Vec<DesktopSettings13AndPostData> {
|
||||
let sidebar: Vec<SidebarSection> =
|
||||
plist::from_file("/System/Applications/System Settings.app/Contents/Resources/Sidebar.plist")
|
||||
.expect("Sidebar.plist doesn't follow expected format");
|
||||
|
|
@ -218,13 +258,24 @@ pub fn macos_settings_13_and_post() -> Vec<DesktopSettings13AndPostData> {
|
|||
let extensions: HashMap<_, _> = get_extensions_in_dir(PathBuf::from("/System/Library/ExtensionKit/Extensions"))
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
fn read_plist(path: &Path) -> anyhow::Result<(String, (String, PathBuf))> {
|
||||
let name = path
|
||||
fn read_plist(path: &Path, lang: &Option<String>) -> anyhow::Result<(String, (String, PathBuf))> {
|
||||
let mut name = path
|
||||
.file_stem()
|
||||
.expect(&format!("invalid path: {:?}", path))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let localized_info_path = path.join("Contents/Resources/InfoPlist.loctable");
|
||||
if !localized_info_path.is_file() {
|
||||
return Ok((name.clone(), (name, path.to_path_buf())));
|
||||
}
|
||||
|
||||
if let Some(lang) = lang {
|
||||
name = get_localized_name(localized_info_path.as_path(), lang).unwrap_or(name);
|
||||
} else {
|
||||
name = get_localized_name(localized_info_path.as_path(), "en").unwrap_or(name);
|
||||
}
|
||||
|
||||
let info_path = path.join("Contents").join("Info.plist");
|
||||
|
||||
let info = plist::from_file::<_, Info>(info_path.as_path()).context(format!(
|
||||
|
|
@ -232,16 +283,10 @@ pub fn macos_settings_13_and_post() -> Vec<DesktopSettings13AndPostData> {
|
|||
&info_path.display()
|
||||
))?;
|
||||
|
||||
let name = info
|
||||
.bundle_display_name
|
||||
.clone()
|
||||
.or_else(|| info.bundle_name.clone())
|
||||
.unwrap_or(name);
|
||||
|
||||
Ok((info.bundle_id, (name, path.to_path_buf())))
|
||||
}
|
||||
|
||||
read_plist(&path)
|
||||
read_plist(&path, &lang)
|
||||
.inspect_err(|err| {
|
||||
tracing::error!("error while reading system extension Info.plist {:?}: {:?}", path, err)
|
||||
})
|
||||
|
|
@ -454,6 +499,20 @@ struct Info {
|
|||
bundle_icon_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LocalizedInfo {
|
||||
#[serde(rename = "CFBundleDisplayName")]
|
||||
bundle_display_name: Option<String>,
|
||||
#[serde(rename = "CFBundleName")]
|
||||
bundle_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(flatten)]
|
||||
languages: HashMap<String, LocalizedInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SystemVersion {
|
||||
#[serde(rename = "ProductVersion")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue