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` | |
| `globalConfig` | Global user config. | `GlobalConfig` | |
+### 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` |
+
## 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),
}