mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-07-10 23:22:43 +02:00
feat(bar): add keyboard language widget
This commit is a squashed commit containing the below commits from PR #1266, which introduces a new "Keyboard" widget, which is used to display information about the user's currently selected keyboard input language. This new widget has a data refresh interval of 1 second if not specified by the user.721d2ef40858373cd26cce27a76b36fb9054a18b55cc2fd889461a73833e781b8d0bd0fa6bf6ff76
This commit is contained in:
@@ -48,6 +48,7 @@ features = [
|
|||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_UI_Shell_Common", # for IObjectArray
|
"Win32_UI_Shell_Common", # for IObjectArray
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
"Win32_Globalization",
|
||||||
"Win32_Graphics_Dwm",
|
"Win32_Graphics_Dwm",
|
||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_Graphics_Direct2D",
|
"Win32_Graphics_Direct2D",
|
||||||
|
|||||||
Executable
+177
@@ -0,0 +1,177 @@
|
|||||||
|
use crate::config::LabelPrefix;
|
||||||
|
use crate::render::RenderConfig;
|
||||||
|
use crate::widget::BarWidget;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
|
use eframe::egui::Align;
|
||||||
|
use eframe::egui::Context;
|
||||||
|
use eframe::egui::Label;
|
||||||
|
use eframe::egui::TextFormat;
|
||||||
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::WidgetText;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
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<u64>,
|
||||||
|
/// Display label prefix
|
||||||
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KeyboardConfig> 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,
|
||||||
|
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<String, ()> {
|
||||||
|
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()
|
||||||
|
.map(|l| l.trim_end_matches('\0').to_string())
|
||||||
|
.unwrap_or_else(|_| ERROR_TEXT.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyboard {
|
||||||
|
fn output(&mut self) -> 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: {}", self.lang_name),
|
||||||
|
LabelPrefix::None | LabelPrefix::Icon => self.lang_name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BarWidget for Keyboard {
|
||||||
|
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||||
|
if self.enable {
|
||||||
|
let output = self.output();
|
||||||
|
if !output.is_empty() {
|
||||||
|
let mut layout_job = LayoutJob::simple(
|
||||||
|
match self.label_prefix {
|
||||||
|
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||||
|
egui_phosphor::regular::KEYBOARD.to_string()
|
||||||
|
}
|
||||||
|
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||||
|
},
|
||||||
|
config.icon_font_id.clone(),
|
||||||
|
ctx.style().visuals.selection.stroke.color,
|
||||||
|
100.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
layout_job.append(
|
||||||
|
&output,
|
||||||
|
10.0,
|
||||||
|
TextFormat {
|
||||||
|
font_id: config.text_font_id.clone(),
|
||||||
|
color: ctx.style().visuals.text_color(),
|
||||||
|
valign: Align::Center,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
config.apply_on_widget(true, ui, |ui| {
|
||||||
|
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ mod battery;
|
|||||||
mod config;
|
mod config;
|
||||||
mod cpu;
|
mod cpu;
|
||||||
mod date;
|
mod date;
|
||||||
|
mod keyboard;
|
||||||
mod komorebi;
|
mod komorebi;
|
||||||
mod komorebi_layout;
|
mod komorebi_layout;
|
||||||
mod media;
|
mod media;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use crate::cpu::Cpu;
|
|||||||
use crate::cpu::CpuConfig;
|
use crate::cpu::CpuConfig;
|
||||||
use crate::date::Date;
|
use crate::date::Date;
|
||||||
use crate::date::DateConfig;
|
use crate::date::DateConfig;
|
||||||
|
use crate::keyboard::Keyboard;
|
||||||
|
use crate::keyboard::KeyboardConfig;
|
||||||
use crate::komorebi::Komorebi;
|
use crate::komorebi::Komorebi;
|
||||||
use crate::komorebi::KomorebiConfig;
|
use crate::komorebi::KomorebiConfig;
|
||||||
use crate::media::Media;
|
use crate::media::Media;
|
||||||
@@ -34,6 +36,7 @@ pub enum WidgetConfig {
|
|||||||
Battery(BatteryConfig),
|
Battery(BatteryConfig),
|
||||||
Cpu(CpuConfig),
|
Cpu(CpuConfig),
|
||||||
Date(DateConfig),
|
Date(DateConfig),
|
||||||
|
Keyboard(KeyboardConfig),
|
||||||
Komorebi(KomorebiConfig),
|
Komorebi(KomorebiConfig),
|
||||||
Media(MediaConfig),
|
Media(MediaConfig),
|
||||||
Memory(MemoryConfig),
|
Memory(MemoryConfig),
|
||||||
@@ -49,6 +52,7 @@ impl WidgetConfig {
|
|||||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||||
|
WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
|
||||||
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||||
@@ -64,6 +68,7 @@ impl WidgetConfig {
|
|||||||
WidgetConfig::Battery(config) => config.enable,
|
WidgetConfig::Battery(config) => config.enable,
|
||||||
WidgetConfig::Cpu(config) => config.enable,
|
WidgetConfig::Cpu(config) => config.enable,
|
||||||
WidgetConfig::Date(config) => config.enable,
|
WidgetConfig::Date(config) => config.enable,
|
||||||
|
WidgetConfig::Keyboard(config) => config.enable,
|
||||||
WidgetConfig::Komorebi(config) => {
|
WidgetConfig::Komorebi(config) => {
|
||||||
config.workspaces.as_ref().is_some_and(|w| w.enable)
|
config.workspaces.as_ref().is_some_and(|w| w.enable)
|
||||||
|| config.layout.as_ref().is_some_and(|w| w.enable)
|
|| config.layout.as_ref().is_some_and(|w| w.enable)
|
||||||
|
|||||||
+180
@@ -271,6 +271,66 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Keyboard"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Keyboard": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"enable"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"data_refresh_interval": {
|
||||||
|
"description": "Data refresh interval (default: 1 second)",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"description": "Enable the Input widget",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"label_prefix": {
|
||||||
|
"description": "Display label prefix",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Show no prefix",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show an icon",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Icon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show text",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show an icon and text",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"IconAndText"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1518,6 +1578,66 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Keyboard"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Keyboard": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"enable"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"data_refresh_interval": {
|
||||||
|
"description": "Data refresh interval (default: 1 second)",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"description": "Enable the Input widget",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"label_prefix": {
|
||||||
|
"description": "Display label prefix",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Show no prefix",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show an icon",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Icon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show text",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show an icon and text",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"IconAndText"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2698,6 +2818,66 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Keyboard"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Keyboard": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"enable"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"data_refresh_interval": {
|
||||||
|
"description": "Data refresh interval (default: 1 second)",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint64",
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
"enable": {
|
||||||
|
"description": "Enable the Input widget",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"label_prefix": {
|
||||||
|
"description": "Display label prefix",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Show no prefix",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show an icon",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Icon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show text",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show an icon and text",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"IconAndText"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
Reference in New Issue
Block a user