diff --git a/README.md b/README.md index 7497455d..eaaaa061 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ And in the Zebar config (if using the default generated one), replace the GlazeW height: 30px; color: #ffffffe6; border: none; - border-radius: 2px; + border-radius: 2px; } .workspace.active { @@ -452,6 +452,20 @@ Self provider doesn't take any config options. | `parsedConfig` | Parsed config for this element. | `WindowConfig \| GroupConfig \| TemplateConfig` | microsoft iconapple iconlinux icon | | `globalConfig` | Global user config. | `GlobalConfig` | microsoft iconapple iconlinux icon | +### Language (Windows only) + +### Provider config + +| Option | Description | Option type | Default value | +| ------------------ | -------------------------------------------------- | ----------- | ------------- | +| `refresh_interval` | How often this provider refreshes in milliseconds. | `number` | `5000` | + +### Variables + +| Variable | Description | Return type | Supported OS | +| ------------- | ------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `language` | Current language, for example 'en-US' | `string` | microsoft icon + ## Util ## Functions diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 5b6ec097..286112ce 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -13,6 +13,7 @@ import { createNetworkProvider } from './network/create-network-provider'; import { createSelfProvider } from './self/create-self-provider'; import { createUtilProvider } from './util/create-util-provider'; import { createWeatherProvider } from './weather/create-weather-provider'; +import { createLanguageProvider } from './language/create-language-provider'; import { ProviderType, type ProviderConfig } from '~/user-config'; import type { ElementContext } from '~/element-context.model'; import type { PickPartial } from '~/utils'; @@ -52,6 +53,8 @@ export async function createProvider( return createUtilProvider(config, owner); case ProviderType.WEATHER: return createWeatherProvider(config, owner); + case ProviderType.LANGUAGE: + return createLanguageProvider(config, owner); default: throw new Error('Not a supported provider type.'); } diff --git a/packages/client-api/src/providers/index.ts b/packages/client-api/src/providers/index.ts index d0106b1c..5aede37b 100644 --- a/packages/client-api/src/providers/index.ts +++ b/packages/client-api/src/providers/index.ts @@ -11,3 +11,4 @@ export * from './weather/create-weather-provider'; export * from './create-provider-listener'; export * from './create-provider'; export * from './get-element-providers'; +export * from './language/create-language-provider'; diff --git a/packages/client-api/src/providers/language/create-language-provider.ts b/packages/client-api/src/providers/language/create-language-provider.ts new file mode 100644 index 00000000..5727dbed --- /dev/null +++ b/packages/client-api/src/providers/language/create-language-provider.ts @@ -0,0 +1,38 @@ +import { runWithOwner, type Owner, createEffect } from 'solid-js'; + +import type { LanguageProviderConfig } from '~/user-config'; +import { createProviderListener } from '../create-provider-listener'; +import { createStore } from 'solid-js/store'; + +export interface LanguageVariables { + language: string; +} + +export async function createLanguageProvider( + config: LanguageProviderConfig, + owner: Owner, +) { + const providerListener = await createProviderListener< + LanguageProviderConfig, + LanguageVariables + >(config, owner); + + const [languageVariables, setLanguageVariables] = createStore( + await getVariables(), + ); + + runWithOwner(owner, () => { + createEffect(async () => setLanguageVariables(await getVariables())); + }); + + async function getVariables() { + const state = providerListener(); + return { language: state.language }; + } + + return { + get language() { + return languageVariables.language; + }, + }; +} diff --git a/packages/client-api/src/user-config/window/provider-config.model.ts b/packages/client-api/src/user-config/window/provider-config.model.ts index 7adfbaa0..e1ec90e0 100644 --- a/packages/client-api/src/user-config/window/provider-config.model.ts +++ b/packages/client-api/src/user-config/window/provider-config.model.ts @@ -15,6 +15,7 @@ import { SelfProviderConfigSchema, UtilProviderConfigSchema, WeatherProviderConfigSchema, + LanguageProviderConfigSchema, } from './providers'; export const ProviderConfigSchema = z.union([ @@ -31,6 +32,7 @@ export const ProviderConfigSchema = z.union([ SelfProviderConfigSchema, UtilProviderConfigSchema, WeatherProviderConfigSchema, + LanguageProviderConfigSchema, ]); export type ProviderConfig = Prettify< diff --git a/packages/client-api/src/user-config/window/provider-type.model.ts b/packages/client-api/src/user-config/window/provider-type.model.ts index 6ea9c221..6539d69b 100644 --- a/packages/client-api/src/user-config/window/provider-type.model.ts +++ b/packages/client-api/src/user-config/window/provider-type.model.ts @@ -14,6 +14,7 @@ export enum ProviderType { SELF = 'self', UTIL = 'util', WEATHER = 'weather', + LANGUAGE = 'language', } export const ProviderTypeSchema = z.nativeEnum(ProviderType); diff --git a/packages/client-api/src/user-config/window/providers/index.ts b/packages/client-api/src/user-config/window/providers/index.ts index caca0198..25806b71 100644 --- a/packages/client-api/src/user-config/window/providers/index.ts +++ b/packages/client-api/src/user-config/window/providers/index.ts @@ -11,3 +11,4 @@ export * from './network-provider-config.model'; export * from './self-provider-config.model'; export * from './util-provider-config.model'; export * from './weather-provider-config.model'; +export * from './language-provider-config.model'; diff --git a/packages/client-api/src/user-config/window/providers/language-provider-config.model.ts b/packages/client-api/src/user-config/window/providers/language-provider-config.model.ts new file mode 100644 index 00000000..dd11e6c8 --- /dev/null +++ b/packages/client-api/src/user-config/window/providers/language-provider-config.model.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { ProviderType } from '../provider-type.model'; + +export const LanguageProviderConfigSchema = z.object({ + type: z.literal(ProviderType.LANGUAGE), + + refresh_interval: z.coerce.number().default(5 * 1000), +}); + +export type LanguageProviderConfig = z.infer< + typeof LanguageProviderConfigSchema +>; diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 668c3ab9..dfce37ec 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -39,7 +39,7 @@ regex = "1" [target.'cfg(target_os = "windows")'.dependencies] komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" } -windows = { version = "0.57", features = [] } +windows = { version = "0.57", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_Globalization", "Win32_System_SystemServices"] } [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.25" diff --git a/packages/desktop/src/providers/config.rs b/packages/desktop/src/providers/config.rs index 40da9c9a..1bae29aa 100644 --- a/packages/desktop/src/providers/config.rs +++ b/packages/desktop/src/providers/config.rs @@ -2,6 +2,8 @@ use serde::Deserialize; #[cfg(windows)] use super::komorebi::KomorebiProviderConfig; +#[cfg(windows)] +use super::language::LanguageProviderConfig; use super::{ battery::BatteryProviderConfig, cpu::CpuProviderConfig, host::HostProviderConfig, ip::IpProviderConfig, @@ -21,4 +23,6 @@ pub enum ProviderConfig { Memory(MemoryProviderConfig), Network(NetworkProviderConfig), Weather(WeatherProviderConfig), + #[cfg(windows)] + Language(LanguageProviderConfig), } diff --git a/packages/desktop/src/providers/language/config.rs b/packages/desktop/src/providers/language/config.rs new file mode 100644 index 00000000..ee7c78ad --- /dev/null +++ b/packages/desktop/src/providers/language/config.rs @@ -0,0 +1,5 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +#[serde(tag = "type", rename = "inputlanguage")] +pub struct LanguageProviderConfig {} diff --git a/packages/desktop/src/providers/language/mod.rs b/packages/desktop/src/providers/language/mod.rs new file mode 100644 index 00000000..8434e838 --- /dev/null +++ b/packages/desktop/src/providers/language/mod.rs @@ -0,0 +1,7 @@ +mod config; +mod provider; +mod variables; + +pub use config::*; +pub use provider::*; +pub use variables::*; diff --git a/packages/desktop/src/providers/language/provider.rs b/packages/desktop/src/providers/language/provider.rs new file mode 100644 index 00000000..77c22da5 --- /dev/null +++ b/packages/desktop/src/providers/language/provider.rs @@ -0,0 +1,108 @@ +use std::{sync::Arc, time::Duration}; + +use async_trait::async_trait; +use tokio::{ + sync::mpsc::Sender, + task::{self, AbortHandle}, + time::sleep, +}; +use windows::Win32::{ + Globalization::{LCIDToLocaleName, LOCALE_ALLOW_NEUTRAL_NAMES}, + System::SystemServices::LOCALE_NAME_MAX_LENGTH, + UI::{ + Input::KeyboardAndMouse::GetKeyboardLayout, + WindowsAndMessaging::{GetForegroundWindow, GetWindowThreadProcessId}, + }, +}; + +use super::{LanguageProviderConfig, LanguageVariables}; +use crate::providers::{ + provider::Provider, + provider_ref::{ProviderOutput, VariablesResult}, + variables::ProviderVariables, +}; + +pub struct LanguageProvider { + pub config: Arc, + abort_handle: Option, +} + +impl LanguageProvider { + pub fn new(config: LanguageProviderConfig) -> LanguageProvider { + LanguageProvider { + config: Arc::new(config), + abort_handle: None, + } + } +} + +#[async_trait] +impl Provider for LanguageProvider { + async fn on_start( + &mut self, + config_hash: &str, + emit_output_tx: Sender, + ) { + let config_hash = config_hash.to_string(); + let task_handle = task::spawn(async move { + let mut previous = 0; + loop { + sleep(Duration::from_millis(150)).await; + + let keyboard_layout = unsafe { + GetKeyboardLayout(GetWindowThreadProcessId( + GetForegroundWindow(), + None, + )) + }; + let lang_id = (keyboard_layout.0 as u32) & 0xffff; + + if lang_id == previous { + continue; + } + previous = lang_id; + + let mut locale_name = [0; LOCALE_NAME_MAX_LENGTH as usize]; + + let result = unsafe { + LCIDToLocaleName( + lang_id, + Some(&mut locale_name), + LOCALE_ALLOW_NEUTRAL_NAMES, + ) + }; + + if result > 0 { + let language_name = + String::from_utf16_lossy(&locale_name[..result as usize]); + _ = emit_output_tx + .send(ProviderOutput { + config_hash: config_hash.clone(), + variables: VariablesResult::Data( + ProviderVariables::Language(LanguageVariables { + language: language_name, + }), + ), + }) + .await; + } + } + }); + + self.abort_handle = Some(task_handle.abort_handle()); + _ = task_handle.await; + } + + async fn on_refresh( + &mut self, + _config_hash: &str, + _emit_output_tx: Sender, + ) { + } + + async fn on_stop(&mut self) {} + + fn min_refresh_interval(&self) -> Option { + None + } +} diff --git a/packages/desktop/src/providers/language/variables.rs b/packages/desktop/src/providers/language/variables.rs new file mode 100644 index 00000000..c79f91ba --- /dev/null +++ b/packages/desktop/src/providers/language/variables.rs @@ -0,0 +1,7 @@ +use serde::Serialize; + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LanguageVariables { + pub language: String, +} diff --git a/packages/desktop/src/providers/mod.rs b/packages/desktop/src/providers/mod.rs index b902a359..c43dc8fd 100644 --- a/packages/desktop/src/providers/mod.rs +++ b/packages/desktop/src/providers/mod.rs @@ -5,6 +5,8 @@ pub mod host; pub mod ip; #[cfg(windows)] pub mod komorebi; +#[cfg(windows)] +pub mod language; pub mod memory; pub mod network; pub mod provider; diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index ec4c68c0..cc58ab6d 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -7,6 +7,8 @@ use tracing::info; #[cfg(windows)] use super::komorebi::KomorebiProvider; +#[cfg(windows)] +use super::language::LanguageProvider; use super::{ battery::BatteryProvider, config::ProviderConfig, cpu::CpuProvider, host::HostProvider, ip::IpProvider, memory::MemoryProvider, @@ -169,6 +171,10 @@ impl ProviderRef { ProviderConfig::Weather(config) => { Box::new(WeatherProvider::new(config)) } + #[cfg(windows)] + ProviderConfig::Language(config) => { + Box::new(LanguageProvider::new(config)) + } #[allow(unreachable_patterns)] _ => bail!("Provider not supported on this operating system."), }; diff --git a/packages/desktop/src/providers/variables.rs b/packages/desktop/src/providers/variables.rs index e6337f92..65d3d31f 100644 --- a/packages/desktop/src/providers/variables.rs +++ b/packages/desktop/src/providers/variables.rs @@ -2,6 +2,8 @@ use serde::Serialize; #[cfg(windows)] use super::komorebi::KomorebiVariables; +#[cfg(windows)] +use super::language::LanguageVariables; use super::{ battery::BatteryVariables, cpu::CpuVariables, host::HostVariables, ip::IpVariables, memory::MemoryVariables, network::NetworkVariables, @@ -20,4 +22,6 @@ pub enum ProviderVariables { Memory(MemoryVariables), Network(NetworkVariables), Weather(WeatherVariables), + #[cfg(windows)] + Language(LanguageVariables), }