diff --git a/Cargo.toml b/Cargo.toml index b02e0cfd..22aa6d79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,10 +45,10 @@ version = "0.58" features = [ "implement", "Foundation_Numerics", - "Globalization", "Win32_System_Com", "Win32_UI_Shell_Common", # for IObjectArray "Win32_Foundation", + "Win32_Globalization", "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", "Win32_Graphics_Direct2D", diff --git a/komorebi-bar/src/keyboard.rs b/komorebi-bar/src/keyboard.rs index 5364d14a..c215ddb2 100755 --- a/komorebi-bar/src/keyboard.rs +++ b/komorebi-bar/src/keyboard.rs @@ -1,6 +1,5 @@ use crate::config::LabelPrefix; use crate::render::RenderConfig; -use crate::selected_frame::SelectableFrame; use crate::widget::BarWidget; use eframe::egui::text::LayoutJob; use eframe::egui::Align; @@ -12,38 +11,126 @@ use eframe::egui::WidgetText; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use windows::Globalization::Language; +use std::time::Duration; +use std::time::Instant; +use windows::Win32::Globalization::LCIDToLocaleName; +use windows::Win32::Globalization::LOCALE_ALLOW_NEUTRAL_NAMES; +use windows::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH; +use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout; +use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow; +use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; + +const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1; +const ERROR_TEXT: &str = "Error"; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct KeyboardConfig { /// Enable the Input widget pub enable: bool, + /// Data refresh interval (default: 1 second) + pub data_refresh_interval: Option, /// Display label prefix pub label_prefix: Option, } impl From for Keyboard { fn from(value: KeyboardConfig) -> Self { + let data_refresh_interval = value.data_refresh_interval.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL); Self { enable: value.enable, + data_refresh_interval, label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), + last_updated: Instant::now(), + lang_name: get_lang(), } } } pub struct Keyboard { pub enable: bool, - pub label_prefix: LabelPrefix, + data_refresh_interval: u64, + label_prefix: LabelPrefix, + last_updated: Instant, + lang_name: String, +} + +/// Retrieves the name of the active keyboard layout for the current foreground window. +/// +/// This function determines the active keyboard layout by querying the system for the +/// foreground window's thread ID and its associated keyboard layout. It then attempts +/// to retrieve the locale name corresponding to the keyboard layout. +/// +/// # Failure Cases +/// +/// This function can fail in two distinct scenarios: +/// +/// 1. **Failure to Retrieve the Locale Name**: +/// If the system fails to retrieve the locale name (e.g., due to an invalid or unsupported +/// language identifier), the function will return `Err(())`. +/// +/// 2. **Invalid UTF-16 Characters in the Locale Name**: +/// If the retrieved locale name contains invalid UTF-16 sequences, the conversion to a Rust +/// `String` will fail, and the function will return `Err(())`. +/// +/// # Returns +/// +/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string. +/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered +/// invalid UTF-16 characters during conversion. +fn get_active_keyboard_layout() -> Result { + let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) }; + let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) }; + + // Extract the low word (language identifier) from the keyboard layout handle. + let lang_id = (lcid.0 as u32) & 0xFFFF; + let mut locale_name_buffer = [0; LOCALE_NAME_MAX_LENGTH as usize]; + let char_count = unsafe { + LCIDToLocaleName( + lang_id, + Some(&mut locale_name_buffer), + LOCALE_ALLOW_NEUTRAL_NAMES, + ) + }; + + match char_count { + 0 => Err(()), + _ => String::from_utf16(&locale_name_buffer[..char_count as usize]).map_err(|_| ()), + } +} + +/// Retrieves the name of the active keyboard layout or a fallback error message. +/// +/// # Behavior +/// +/// - **Success Case**: +/// If [`get_active_keyboard_layout`] succeeds, this function returns the retrieved keyboard +/// layout name as a `String`. +/// +/// - **Failure Case**: +/// If [`get_active_keyboard_layout`] fails, this function returns the value of `ERROR_TEXT` +/// as a fallback message. This ensures that the function always returns a valid `String`, +/// even in error scenarios. +/// +/// # Returns +/// +/// A `String` representing either: +/// - The name of the active keyboard layout, or +/// - The fallback error message (`ERROR_TEXT`) if the layout name cannot be retrieved. +fn get_lang() -> String { + get_active_keyboard_layout().unwrap_or_else(|_| ERROR_TEXT.to_string()) } impl Keyboard { fn output(&mut self) -> String { - let lang = Language::CurrentInputMethodLanguageTag() - .map(|lang| lang.to_string()) - .unwrap_or_else(|_| "error".to_string()); + let now = Instant::now(); + if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { + self.last_updated = now; + self.lang_name = get_lang(); + } + match self.label_prefix { - LabelPrefix::Text | LabelPrefix::IconAndText => format!("KB: {}", lang), - LabelPrefix::None | LabelPrefix::Icon => lang, + LabelPrefix::Text | LabelPrefix::IconAndText => format!("KB: {}", self.lang_name), + LabelPrefix::None | LabelPrefix::Icon => self.lang_name.clone(), } } } @@ -77,15 +164,7 @@ impl BarWidget for Keyboard { ); config.apply_on_widget(false, ui, |ui| { - if SelectableFrame::new(false) - .show(ui, |ui| { - ui.add( - Label::new(WidgetText::LayoutJob(layout_job.clone())) - .selectable(false), - ) - }) - .clicked() - {} + ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false)) }); } } diff --git a/schema.asc.json b/schema.asc.json index d39fec39..5aedbe2e 100644 Binary files a/schema.asc.json and b/schema.asc.json differ diff --git a/schema.bar.json b/schema.bar.json index b8092534..8440827e 100644 Binary files a/schema.bar.json and b/schema.bar.json differ diff --git a/schema.json b/schema.json index 54ee1d81..6d6678e4 100644 Binary files a/schema.json and b/schema.json differ