diff --git a/data/locale/ca-ES/keyb.toml b/data/locale/ca-ES/keyb.toml deleted file mode 100644 index d5843fd2..00000000 --- a/data/locale/ca-ES/keyb.toml +++ /dev/null @@ -1,23 +0,0 @@ -[meta] -app = "keyb" -language = "ca-ES" - -[ids] -IDS_USAGE = 1001 -IDS_USAGE_LINE2 = 1002 -IDS_USAGE_LINE3 = 1003 -IDS_USAGE_LINE4 = 1004 -IDS_LAYOUT = 1005 -IDS_ERR_READING = 1006 -IDS_ERR_INVALID = 1007 -IDS_ERR_CHANGE = 1008 - -[strings] -IDS_USAGE = "KEYB [distribució]" -IDS_USAGE_LINE2 = " Mostra o canvia la distribució del teclat." -IDS_USAGE_LINE3 = " KEYB mostra la distribució actual" -IDS_USAGE_LINE4 = " KEYB es canvia a distribució espanyola" -IDS_LAYOUT = "Distribució del teclat: " -IDS_ERR_READING = "Error en llegir la distribució del teclat." -IDS_ERR_INVALID = "Distribució no vàlida." -IDS_ERR_CHANGE = "Error: no s'ha pogut canviar la distribució del teclat." diff --git a/data/locale/ca-ES/neokey.toml b/data/locale/ca-ES/neokey.toml index f1ca52f0..0bdb5de1 100644 --- a/data/locale/ca-ES/neokey.toml +++ b/data/locale/ca-ES/neokey.toml @@ -13,15 +13,47 @@ IDS_LEDS = 1007 IDS_ACTIVE_LAYOUT = 1008 IDS_AVAIL_LAYOUTS = 1009 IDS_UNKNOWN_CMD = 1010 +IDS_LAYOUT_SET = 1011 +IDS_ERR_SET_LAYOUT = 1012 +IDS_MISSING_LAYOUT = 1013 +IDS_USAGE_LINE5 = 1014 +IDS_USAGE_LINE6 = 1015 +IDS_USAGE_LINE7 = 1016 +IDS_USAGE_LINE8 = 1017 +IDS_REPEAT = 1018 +IDS_DELAY = 1019 +IDS_REPEAT_SET = 1020 +IDS_DELAY_SET = 1021 +IDS_LEDS_SET = 1022 +IDS_ERR_SET = 1023 +IDS_INVALID_VALUE = 1024 +IDS_CPS = 1025 +IDS_MS = 1026 [strings] -IDS_USAGE = "NEOKEY [opció]" -IDS_USAGE_LINE2 = " Mostra l'estat del teclat i les distribucions disponibles." -IDS_USAGE_LINE3 = " NEOKEY mostra l'estat del teclat" -IDS_USAGE_LINE4 = " NEOKEY --list llista les distribucions disponibles" +IDS_USAGE = "NEOKEY [comanda]" +IDS_USAGE_LINE2 = " Mostra estat, distribució, repetició i LEDs." +IDS_USAGE_LINE3 = " NEOKEY mostra l'estat i la configuració" +IDS_USAGE_LINE4 = " NEOKEY layouts llista les distribucions disponibles" +IDS_USAGE_LINE5 = " NEOKEY layout estableix la distribució del teclat" +IDS_USAGE_LINE6 = " NEOKEY repeat estableix la velocitat de repetició (2-60)" +IDS_USAGE_LINE7 = " NEOKEY delay estableix el retard de repetició (100-2000)" +IDS_USAGE_LINE8 = " NEOKEY leds estableix els LEDs: num,caps,scroll (o none)" IDS_KBD_STATE = "Estat del teclat:" IDS_MODIFIERS = "Modificadors:" IDS_LEDS = "LEDs:" IDS_ACTIVE_LAYOUT = "Distribució activa: " IDS_AVAIL_LAYOUTS = "Distribucions de teclat disponibles:" IDS_UNKNOWN_CMD = "Ordre desconeguda." +IDS_LAYOUT_SET = "Distribució establerta: " +IDS_ERR_SET_LAYOUT = "Error: no s'ha pogut establir la distribució del teclat." +IDS_MISSING_LAYOUT = "Falta el nom de la distribució." +IDS_REPEAT = "Repetició:" +IDS_DELAY = "Retard:" +IDS_REPEAT_SET = "Velocitat de repetició establerta: " +IDS_DELAY_SET = "Retard de repetició establert: " +IDS_LEDS_SET = "LEDs establerts: " +IDS_ERR_SET = "Error: no s'ha pogut aplicar la configuració del teclat." +IDS_INVALID_VALUE = "Valor no vàlid." +IDS_CPS = " cps" +IDS_MS = " ms" diff --git a/data/locale/en-US/keyb.toml b/data/locale/en-US/keyb.toml deleted file mode 100644 index 78ebcc1c..00000000 --- a/data/locale/en-US/keyb.toml +++ /dev/null @@ -1,23 +0,0 @@ -[meta] -app = "keyb" -language = "en-US" - -[ids] -IDS_USAGE = 1001 -IDS_USAGE_LINE2 = 1002 -IDS_USAGE_LINE3 = 1003 -IDS_USAGE_LINE4 = 1004 -IDS_LAYOUT = 1005 -IDS_ERR_READING = 1006 -IDS_ERR_INVALID = 1007 -IDS_ERR_CHANGE = 1008 - -[strings] -IDS_USAGE = "KEYB [layout]" -IDS_USAGE_LINE2 = " Display or set the keyboard layout." -IDS_USAGE_LINE3 = " KEYB shows current layout" -IDS_USAGE_LINE4 = " KEYB es sets layout to Spanish" -IDS_LAYOUT = "Keyboard layout: " -IDS_ERR_READING = "Error reading keyboard layout." -IDS_ERR_INVALID = "Invalid layout." -IDS_ERR_CHANGE = "Error: failed to change keyboard layout." diff --git a/data/locale/en-US/neokey.toml b/data/locale/en-US/neokey.toml index a25e89db..b0a41784 100644 --- a/data/locale/en-US/neokey.toml +++ b/data/locale/en-US/neokey.toml @@ -13,15 +13,47 @@ IDS_LEDS = 1007 IDS_ACTIVE_LAYOUT = 1008 IDS_AVAIL_LAYOUTS = 1009 IDS_UNKNOWN_CMD = 1010 +IDS_LAYOUT_SET = 1011 +IDS_ERR_SET_LAYOUT = 1012 +IDS_MISSING_LAYOUT = 1013 +IDS_USAGE_LINE5 = 1014 +IDS_USAGE_LINE6 = 1015 +IDS_USAGE_LINE7 = 1016 +IDS_USAGE_LINE8 = 1017 +IDS_REPEAT = 1018 +IDS_DELAY = 1019 +IDS_REPEAT_SET = 1020 +IDS_DELAY_SET = 1021 +IDS_LEDS_SET = 1022 +IDS_ERR_SET = 1023 +IDS_INVALID_VALUE = 1024 +IDS_CPS = 1025 +IDS_MS = 1026 [strings] -IDS_USAGE = "NEOKEY [option]" -IDS_USAGE_LINE2 = " Display keyboard state and available layouts." -IDS_USAGE_LINE3 = " NEOKEY shows keyboard state" -IDS_USAGE_LINE4 = " NEOKEY --list lists available layouts" +IDS_USAGE = "NEOKEY [command]" +IDS_USAGE_LINE2 = " Show state, layout, repeat and LEDs." +IDS_USAGE_LINE3 = " NEOKEY shows the state and configuration" +IDS_USAGE_LINE4 = " NEOKEY layouts lists available layouts" +IDS_USAGE_LINE5 = " NEOKEY layout sets the keyboard layout" +IDS_USAGE_LINE6 = " NEOKEY repeat sets the repeat rate (2-60)" +IDS_USAGE_LINE7 = " NEOKEY delay sets the repeat delay (100-2000)" +IDS_USAGE_LINE8 = " NEOKEY leds sets LEDs: num,caps,scroll (or none)" IDS_KBD_STATE = "Keyboard state:" IDS_MODIFIERS = "Modifiers:" IDS_LEDS = "LEDs:" IDS_ACTIVE_LAYOUT = "Active layout: " IDS_AVAIL_LAYOUTS = "Available keyboard layouts:" IDS_UNKNOWN_CMD = "Unknown command." +IDS_LAYOUT_SET = "Layout set to: " +IDS_ERR_SET_LAYOUT = "Error: failed to set keyboard layout." +IDS_MISSING_LAYOUT = "Missing layout name." +IDS_REPEAT = "Repeat:" +IDS_DELAY = "Delay:" +IDS_REPEAT_SET = "Repeat rate set to: " +IDS_DELAY_SET = "Repeat delay set to: " +IDS_LEDS_SET = "LEDs set to: " +IDS_ERR_SET = "Error: failed to apply keyboard setting." +IDS_INVALID_VALUE = "Invalid value." +IDS_CPS = " cps" +IDS_MS = " ms" diff --git a/data/locale/es-ES/keyb.toml b/data/locale/es-ES/keyb.toml deleted file mode 100644 index 5cc4b7b4..00000000 --- a/data/locale/es-ES/keyb.toml +++ /dev/null @@ -1,23 +0,0 @@ -[meta] -app = "keyb" -language = "es-ES" - -[ids] -IDS_USAGE = 1001 -IDS_USAGE_LINE2 = 1002 -IDS_USAGE_LINE3 = 1003 -IDS_USAGE_LINE4 = 1004 -IDS_LAYOUT = 1005 -IDS_ERR_READING = 1006 -IDS_ERR_INVALID = 1007 -IDS_ERR_CHANGE = 1008 - -[strings] -IDS_USAGE = "KEYB [distribución]" -IDS_USAGE_LINE2 = " Muestra o cambia la distribución del teclado." -IDS_USAGE_LINE3 = " KEYB muestra la distribución actual" -IDS_USAGE_LINE4 = " KEYB es cambia a distribución española" -IDS_LAYOUT = "Distribución del teclado: " -IDS_ERR_READING = "Error al leer la distribución del teclado." -IDS_ERR_INVALID = "Distribución no válida." -IDS_ERR_CHANGE = "Error: no se pudo cambiar la distribución del teclado." diff --git a/data/locale/es-ES/neokey.toml b/data/locale/es-ES/neokey.toml index c78d4232..45e46580 100644 --- a/data/locale/es-ES/neokey.toml +++ b/data/locale/es-ES/neokey.toml @@ -13,15 +13,47 @@ IDS_LEDS = 1007 IDS_ACTIVE_LAYOUT = 1008 IDS_AVAIL_LAYOUTS = 1009 IDS_UNKNOWN_CMD = 1010 +IDS_LAYOUT_SET = 1011 +IDS_ERR_SET_LAYOUT = 1012 +IDS_MISSING_LAYOUT = 1013 +IDS_USAGE_LINE5 = 1014 +IDS_USAGE_LINE6 = 1015 +IDS_USAGE_LINE7 = 1016 +IDS_USAGE_LINE8 = 1017 +IDS_REPEAT = 1018 +IDS_DELAY = 1019 +IDS_REPEAT_SET = 1020 +IDS_DELAY_SET = 1021 +IDS_LEDS_SET = 1022 +IDS_ERR_SET = 1023 +IDS_INVALID_VALUE = 1024 +IDS_CPS = 1025 +IDS_MS = 1026 [strings] -IDS_USAGE = "NEOKEY [opción]" -IDS_USAGE_LINE2 = " Muestra el estado del teclado y las distribuciones disponibles." -IDS_USAGE_LINE3 = " NEOKEY muestra el estado del teclado" -IDS_USAGE_LINE4 = " NEOKEY --list lista las distribuciones disponibles" +IDS_USAGE = "NEOKEY [comando]" +IDS_USAGE_LINE2 = " Muestra estado, distribución, repetición y LEDs." +IDS_USAGE_LINE3 = " NEOKEY muestra el estado y la configuración" +IDS_USAGE_LINE4 = " NEOKEY layouts lista las distribuciones disponibles" +IDS_USAGE_LINE5 = " NEOKEY layout establece la distribución del teclado" +IDS_USAGE_LINE6 = " NEOKEY repeat establece la velocidad de repetición (2-60)" +IDS_USAGE_LINE7 = " NEOKEY delay establece el retardo de repetición (100-2000)" +IDS_USAGE_LINE8 = " NEOKEY leds establece los LEDs: num,caps,scroll (o none)" IDS_KBD_STATE = "Estado del teclado:" IDS_MODIFIERS = "Modificadores:" IDS_LEDS = "LEDs:" IDS_ACTIVE_LAYOUT = "Distribución activa: " IDS_AVAIL_LAYOUTS = "Distribuciones de teclado disponibles:" IDS_UNKNOWN_CMD = "Comando desconocido." +IDS_LAYOUT_SET = "Distribución establecida: " +IDS_ERR_SET_LAYOUT = "Error: no se pudo establecer la distribución del teclado." +IDS_MISSING_LAYOUT = "Falta el nombre de la distribución." +IDS_REPEAT = "Repetición:" +IDS_DELAY = "Retardo:" +IDS_REPEAT_SET = "Velocidad de repetición establecida: " +IDS_DELAY_SET = "Retardo de repetición establecido: " +IDS_LEDS_SET = "LEDs establecidos: " +IDS_ERR_SET = "Error: no se pudo aplicar la configuración del teclado." +IDS_INVALID_VALUE = "Valor no válido." +IDS_CPS = " cps" +IDS_MS = " ms" diff --git a/docs/architecture/disk-layout.md b/docs/architecture/disk-layout.md index adde2127..3c7721ab 100644 --- a/docs/architecture/disk-layout.md +++ b/docs/architecture/disk-layout.md @@ -17,11 +17,11 @@ El contenido de `C:` se genera íntegramente en `neodev/src/image.rs::collect_fi C:\ ├── README.TXT ├── Temp/.empty -├── Programs\ # 28 .NXE del OS (mezcla shell + utilidades core) +├── Programs\ # 27 .NXE del OS (mezcla shell + utilidades core) │ ├── neoshell.nxe neoinit.nxe cmdtest.nxe │ ├── cd.nxe corehelp.nxe coredir.nxe coretype.nxe │ ├── corecls.nxe corecopy.nxe coredel.nxe coreren.nxe -│ ├── coremd.nxe corerd.nxe drives.nxe ps.nxe keyb.nxe +│ ├── coremd.nxe corerd.nxe drives.nxe ps.nxe │ └── datetime neomem echo label poweroff colors neokey │ nxres nxlocale nxverify ping hostname └── System\ @@ -31,7 +31,7 @@ C:\ │ ├── nslookup │ └── reboot shtest stresscmd tree ver vol # movidos aquí por capacidad ├── Libraries\ # fs.nxl math.nxl console.nxl net.nxl - ├── Keyboard\ # US.kbd Spanish.kbd + ├── Keyboard\ # en-US.kbd es-ES.kbd ├── Locale\\*.nlt ├── Drivers\ # *.nem BOOT y SYSTEM mezclados └── Registry\SYSTEM.hiv @@ -92,7 +92,7 @@ C:\ │ │ ├── config\ # hives: SYSTEM.hiv (+ SOFTWARE/SAM/SECURITY/DEFAULT) │ │ ├── drivers\ # .NEM SYSTEM │ │ │ └── BOOT\ # .NEM boot-critical (ps2kbd, serial, rtc…) -│ │ ├── Keyboard\ # US.kbd, Spanish.kbd +│ │ ├── Keyboard\ # en-US.kbd, es-ES.kbd │ │ ├── Globalization\\ # *.nlt │ │ ├── Fonts\ │ │ ├── INF\ diff --git a/docs/design/neocfg-design.md b/docs/design/neocfg-design.md index eaad65ad..f1773dcc 100644 --- a/docs/design/neocfg-design.md +++ b/docs/design/neocfg-design.md @@ -293,8 +293,8 @@ Sin dependencias externas. Implementable inmediatamente con la API Ob existente. tr!("keyboard.current"): tr!("keyboard.layout.sp") tr!("keyboard.available"): - 1. tr!("keyboard.layout.us") "us" — US - 2. tr!("keyboard.layout.sp") "es" — Spanish [*] + 1. tr!("keyboard.layout.us") "en-US" — English (US) + 2. tr!("keyboard.layout.sp") "es-ES" — Spanish (Spain) [*] tr!("keyboard.actions"): 1. tr!("keyboard.change") @@ -302,7 +302,7 @@ tr!("keyboard.actions"): [Esc] tr!("neocfg.back") ``` -**Implementación** (vía Ob API existente, mismo patrón que `userbin/keyb/`). +**Implementación** (vía Ob API existente, mismo patrón que `userbin/neokey/`). El layout se identifica por **nombre** (string NUL-terminado, p. ej. `"us"`, `"es"`), no por índice numérico: @@ -514,7 +514,7 @@ NeoCfg no extiende el kernel. Es una aplicación Ring 3 que **consume** APIs exi ### Alternative B: Herramientas independientes por subsistema (ej. `powercfg.nxe`, `langcfg.nxe`, `kbdcfg.nxe`) -**Descripción**: Crear un binario .NXE separado para cada área de configuración, como ya se hace con `keyb.nxe`, `ipconfig.nxe`. +**Descripción**: Crear un binario .NXE separado para cada área de configuración, como ya se hace con `neokey.nxe`, `ipconfig.nxe`. **Rechazada porque**: @@ -522,7 +522,7 @@ NeoCfg no extiende el kernel. Es una aplicación Ring 3 que **consume** APIs exi 2. **Duplicación de UI**: Cada herramienta implementa su propia interfaz de usuario. NeoCfg centraliza la UI en `ui/menu.rs` y `ui/dialog.rs`, reutilizables. 3. **Dificultad de descubrimiento**: Un usuario nuevo no sabe qué herramientas existen ni qué hace cada una. NeoCfg ofrece un listado completo. 4. **Alineación con la visión**: El roadmap (`ADM-5+6: neocfg + neofs`) ya prevé una herramienta de administración unificada. -5. **No excluye herramientas independientes**: `keyb.nxe`, `ipconfig.nxe` pueden coexistir con NeoCfg, que las invoca o emula su funcionalidad. +5. **No excluye herramientas independientes**: `neokey.nxe`, `ipconfig.nxe` pueden coexistir con NeoCfg, que las invoca o emula su funcionalidad. ### Alternative C: Aplicación GUI desde el inicio @@ -667,8 +667,8 @@ pub fn i18n_reload_all(); | # | Test | Expected | | --- | --- | --- | | 8 | Keyboard muestra layout actual | Coincide con `ob_query_info(KeyboardLayout)` | -| 9 | Seleccionar "US" → layout cambia a US | `ob_query_info(KeyboardLayout)` retorna `"us"` | -| 10 | Seleccionar "Spanish" → layout cambia a Spanish | `ob_query_info(KeyboardLayout)` retorna `"es"` | +| 9 | Seleccionar "en-US" → layout cambia a en-US | `ob_query_info(KeyboardLayout)` retorna `"en-US"` | +| 10 | Seleccionar "es-ES" → layout cambia a es-ES | `ob_query_info(KeyboardLayout)` retorna `"es-ES"` | | 11 | Keyboard no modifica nada excepto `ob_set_info(KeyboardLayout)` | Solo se invoca la clase 5 | ### 7.4 Tests del módulo About (invariante: los datos de versión son consistentes) @@ -793,7 +793,7 @@ userbin/neocfg/ **Archivos:** `src/modules/keyboard.rs` 1. `ob_open("\Global\Info\Keyboard")` → `ob_query_info(KeyboardLayout)` → get current -2. Mostrar menú con layouts disponibles (US=0, Spanish=1) +2. Mostrar menú con layouts disponibles (en-US=0, es-ES=1) 3. `ob_set_info(KeyboardLayout, &[layout])` si el usuario selecciona cambio 4. Esperar tecla, retornar @@ -913,8 +913,8 @@ keyboard.title = "===== Keyboard Layout =====" keyboard.current = "Current layout" keyboard.available = "Available layouts" keyboard.change = "Change layout" -keyboard.layout.us = "US" -keyboard.layout.sp = "Spanish" +keyboard.layout.us = "en-US" +keyboard.layout.sp = "es-ES" # About module about.title = "===== About NeoDOS =====" diff --git a/docs/design/neokbd-design.md b/docs/design/neokbd-design.md index 3b074efb..75aca57b 100644 --- a/docs/design/neokbd-design.md +++ b/docs/design/neokbd-design.md @@ -4,6 +4,13 @@ > **Estado:** Diseño completo > **Versión de NeoDOS:** v0.50+ > **Dependencias:** Event Bus (existente), Registry/Cm (existente), Input Manager (existente), Ob (existente) +> +> **Nota (post-implementación):** El binario `keyb.nxe` fue retirado; su +> funcionalidad vive ahora en `userbin/neokey/` +> (`NEOKEY show`, `NEOKEY layouts`, `NEOKEY layout `). Los layouts se +> identifican por locale tag — `en-US` y `es-ES` (ficheros `en-US.kbd`/`es-ES.kbd`), +> ya no `US`/`SP`. Las referencias a `userbin/keyb` y a los nombres `US`/`Spanish` +> más abajo son históricas (plan de diseño original). --- diff --git a/docs/userland/shell.md b/docs/userland/shell.md index 1efe16be..f5acadc5 100644 --- a/docs/userland/shell.md +++ b/docs/userland/shell.md @@ -99,7 +99,6 @@ Built-in commands (CWD, SET, EXIT, CALL) are not pipeable and produce an error i | PS | `ob_enum(\Ob\Process)` then `ob_query_info` per process for name/pid/state | | KILL `` | `ob_set_info(Process, ProcessTerminate)` on target process object | | PRI ` ` | `ob_set_info(Process, ProcessPriority, &level)` | -| KEYB `` | Legacy — use `NEOKEY layout ` instead. `ob_set_info(KeyboardLayout)` on keyboard device object. | | POWEROFF | `ob_open(\\System\\PowerManager)` + `ob_set_info(PowerShutdown)` — via Object Manager | | REBOOT | `ob_open(\\System\\PowerManager)` + `ob_set_info(PowerReboot)` — via Object Manager | | VER | `ob_open(\Global\Info\Version)` → `ob_query_info` → print version string | @@ -113,7 +112,7 @@ Built-in commands (CWD, SET, EXIT, CALL) are not pipeable and produce an error i ## userbin/.NXE Binaries -All 43 user-mode binaries, each a standalone `.NXE` ELF file in `userbin//`: +All 42 user-mode binaries, each a standalone `.NXE` ELF file in `userbin//`: | Binary | Category | Description | | -------- | ---------- | ------------- | @@ -151,8 +150,7 @@ All 43 user-mode binaries, each a standalone `.NXE` ELF file in `userbin// | tree | utility | Recursive directory listing | | type | utility | Display file contents | | drives | utility | List mounted drives | -| keyb | utility | Keyboard layout control (legacy — use neokey) | -| neokey | utility | Keyboard management: show state, switch layout, list layouts, set repeat rate/delay, show LEDs | +| neokey | utility | Keyboard management: show state (modifiers/LEDs) and active layout, list layouts, switch layout (`layout `) | | kill | system | Terminate processes | | pri | system | Set process priority | | label | utility | Volume label management | diff --git a/libneodos/src/i18n_keymap.rs b/libneodos/src/i18n_keymap.rs index 0ee8cbd8..8c062665 100644 --- a/libneodos/src/i18n_keymap.rs +++ b/libneodos/src/i18n_keymap.rs @@ -207,14 +207,6 @@ pub static NLT_KEYMAP: &[(&str, u32)] = &[ ("ipconfig.IDS_USAGE", 1037), ("ipconfig.IDS_USAGE_OPTS", 1038), ("ipconfig.IDS_YES", 1021), - ("keyb.IDS_ERR_CHANGE", 1008), - ("keyb.IDS_ERR_INVALID", 1007), - ("keyb.IDS_ERR_READING", 1006), - ("keyb.IDS_LAYOUT", 1005), - ("keyb.IDS_USAGE", 1001), - ("keyb.IDS_USAGE_LINE2", 1002), - ("keyb.IDS_USAGE_LINE3", 1003), - ("keyb.IDS_USAGE_LINE4", 1004), ("kill.IDS_ERR_CANNOT_KILL_IDLE", 1005), ("kill.IDS_ERR_FAILED", 1008), ("kill.IDS_ERR_INVALID_PID", 1004), @@ -266,14 +258,30 @@ pub static NLT_KEYMAP: &[(&str, u32)] = &[ ("neoinit.IDS_SHELL_SPAWN", 1001), ("neokey.IDS_ACTIVE_LAYOUT", 1008), ("neokey.IDS_AVAIL_LAYOUTS", 1009), + ("neokey.IDS_CPS", 1025), + ("neokey.IDS_DELAY", 1019), + ("neokey.IDS_DELAY_SET", 1021), + ("neokey.IDS_ERR_SET", 1023), + ("neokey.IDS_ERR_SET_LAYOUT", 1012), + ("neokey.IDS_INVALID_VALUE", 1024), ("neokey.IDS_KBD_STATE", 1005), + ("neokey.IDS_LAYOUT_SET", 1011), ("neokey.IDS_LEDS", 1007), + ("neokey.IDS_LEDS_SET", 1022), + ("neokey.IDS_MISSING_LAYOUT", 1013), ("neokey.IDS_MODIFIERS", 1006), + ("neokey.IDS_MS", 1026), + ("neokey.IDS_REPEAT", 1018), + ("neokey.IDS_REPEAT_SET", 1020), ("neokey.IDS_UNKNOWN_CMD", 1010), ("neokey.IDS_USAGE", 1001), ("neokey.IDS_USAGE_LINE2", 1002), ("neokey.IDS_USAGE_LINE3", 1003), ("neokey.IDS_USAGE_LINE4", 1004), + ("neokey.IDS_USAGE_LINE5", 1014), + ("neokey.IDS_USAGE_LINE6", 1015), + ("neokey.IDS_USAGE_LINE7", 1016), + ("neokey.IDS_USAGE_LINE8", 1017), ("neolocale.IDS_ERROR_CANNOT_OPEN", 1009), ("neolocale.IDS_ERROR_UNKNOWN_CMD", 1010), ("neolocale.IDS_STATUS_INVALID", 1008), diff --git a/libnlt/src/lang.rs b/libnlt/src/lang.rs index f1d4220a..52612f02 100644 --- a/libnlt/src/lang.rs +++ b/libnlt/src/lang.rs @@ -128,7 +128,6 @@ pub fn app_to_id(app: &str) -> u32 { b"nxres" => 42, b"nxverify" => 43, b"hostname" => 44, - b"keyb" => 45, b"nslookup" => 46, b"ping" => 47, b"dhcptest" => 48, @@ -276,7 +275,7 @@ mod tests { "pri", "cd", "colors", "progress", "vol", "corerd", "coremd", "coreren", "coredel", "corecls", "tree", "dhcpd", "netcfg", "ipconfig", "cpuinfo", "stresscmd", "cmdtest", "shtest", "nxlocale", - "nxres", "nxverify", "hostname", "keyb", "nslookup", "ping", + "nxres", "nxverify", "hostname", "nslookup", "ping", "dhcptest", "ntpd", "netd", "netapplier", "neocfg", ]; let mut ids: Vec = KNOWN.iter().map(|a| app_to_id(a)).collect(); diff --git a/userbin/keyb/.cargo/config.toml b/userbin/keyb/.cargo/config.toml deleted file mode 100644 index 0465985e..00000000 --- a/userbin/keyb/.cargo/config.toml +++ /dev/null @@ -1,9 +0,0 @@ -[build] -target = "x86_64-unknown-none" - -[target.x86_64-unknown-none] -rustflags = [ - "-C", "link-arg=-T../../libneodos/user.ld", - "-C", "link-arg=-melf_x86_64", - "-C", "relocation-model=pie", -] diff --git a/userbin/keyb/Cargo.lock b/userbin/keyb/Cargo.lock deleted file mode 100644 index 7012a66f..00000000 --- a/userbin/keyb/Cargo.lock +++ /dev/null @@ -1,21 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "keyb" -version = "0.1.1" -dependencies = [ - "libneodos", -] - -[[package]] -name = "libneodos" -version = "0.2.1" -dependencies = [ - "libnlt", -] - -[[package]] -name = "libnlt" -version = "0.1.0" diff --git a/userbin/keyb/Cargo.toml b/userbin/keyb/Cargo.toml deleted file mode 100644 index 5af74c03..00000000 --- a/userbin/keyb/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "keyb" -version = "0.1.1" -edition = "2021" - -[[bin]] -name = "keyb" -path = "src/main.rs" -test = false - -[dependencies] -libneodos = { path = "../../libneodos" } - -[profile.release] -panic = "abort" -opt-level = 3 -lto = true -debug = false - -[profile.dev] -panic = "abort" diff --git a/userbin/keyb/rust-toolchain.toml b/userbin/keyb/rust-toolchain.toml deleted file mode 100644 index 5d56faf9..00000000 --- a/userbin/keyb/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "nightly" diff --git a/userbin/keyb/src/main.rs b/userbin/keyb/src/main.rs deleted file mode 100644 index 8add8807..00000000 --- a/userbin/keyb/src/main.rs +++ /dev/null @@ -1,96 +0,0 @@ -#![no_std] -#![no_main] -#![cfg_attr(test, feature(custom_test_frameworks))] -#![cfg_attr(test, test_runner(noop_test_runner))] -#![cfg_attr(test, reexport_test_harness_main = "test_main")] - -#[cfg(test)] -fn noop_test_runner(_tests: &[&dyn Fn()]) { - loop {} -} - -use libneodos::i18n; -use libneodos::syscall; -use libneodos::tr_id; - -const APP_NAME: &str = "keyb"; -const IDS_LAYOUT: u32 = 1005; -const IDS_ERR_READING: u32 = 1006; -const IDS_ERR_INVALID: u32 = 1007; -const IDS_ERR_CHANGE: u32 = 1008; - -fn write_str(s: &[u8]) { - let _ = syscall::sys_write(1, s); -} - -fn write_err(s: &[u8]) { - let _ = syscall::sys_write(2, s); -} - -fn print_help() { - write_str(b"\r\nKEYB [layout]\r\n Display or set the keyboard layout.\r\n KEYB shows current layout\r\n KEYB es sets layout to Spanish\r\n\r\n"); -} - -#[no_mangle] -pub extern "C" fn _start() -> ! { - i18n::i18n_init(); - let _ = i18n::i18n_load(APP_NAME); - let raw = libneodos::args::read_args(); - if libneodos::args::is_help_flag(&raw) { - print_help(); - syscall::sys_exit(0); - } - - let args = libneodos::args::trim_ascii(&raw); - if args.is_empty() { - write_str(tr_id!(IDS_LAYOUT).as_bytes()); - let fd = match syscall::sys_ob_open("\\Global\\Info\\Keyboard", libneodos::syscall::ob_access::READ) { - Ok(f) => f, - Err(_) => { - write_str(b"?\r\n"); - syscall::sys_exit(1); - } - }; - let mut buf = [0u8; 64]; - let n = match syscall::sys_ob_query_info(fd, libneodos::syscall::ObInfoClass::KeyboardLayout, &mut buf) { - Ok(n) => n, - Err(_) => { - let _ = syscall::sys_close(fd); - write_str(b"?\r\n"); - syscall::sys_exit(1); - } - }; - let _ = syscall::sys_close(fd); - let end = buf[..n].iter().position(|&b| b == 0).unwrap_or(n); - write_str(&buf[..end]); - write_str(b"\r\n"); - syscall::sys_exit(0); - } - - let fd = match syscall::sys_ob_open("\\Global\\Info\\Keyboard", libneodos::syscall::ob_access::READ) { - Ok(f) => f, - Err(_) => { - write_err(b"\r\n"); - write_err(tr_id!(IDS_ERR_READING).as_bytes()); - write_err(b"\r\n"); - syscall::sys_exit(1); - } - }; - - let mut buf = [0u8; 12]; - let layout_bytes = args; - buf[..layout_bytes.len().min(11)].copy_from_slice(&layout_bytes[..layout_bytes.len().min(11)]); - match syscall::sys_ob_set_info(fd, syscall::ObSetInfoClass::KeyboardLayout, &buf) { - Ok(_) => { - let _ = syscall::sys_close(fd); - write_str(b"\r\nOK\r\n"); - } - Err(_) => { - let _ = syscall::sys_close(fd); - write_err(b"\r\n"); - write_err(tr_id!(IDS_ERR_CHANGE).as_bytes()); - write_err(b"\r\n"); - } - } - syscall::sys_exit(0) -} diff --git a/userbin/neokey/src/commands.rs b/userbin/neokey/src/commands.rs new file mode 100644 index 00000000..35c6dccb --- /dev/null +++ b/userbin/neokey/src/commands.rs @@ -0,0 +1,214 @@ +//! NEOKEY commands: read/print state and apply keyboard configuration. + +use libneodos::keyboard; +use libneodos::tr_id; + +use crate::output::{format_bits, write_err, write_str, write_u32}; +use crate::{ + IDS_ACTIVE_LAYOUT, IDS_AVAIL_LAYOUTS, IDS_CPS, IDS_DELAY, IDS_DELAY_SET, IDS_ERR_SET, + IDS_ERR_SET_LAYOUT, IDS_INVALID_VALUE, IDS_KBD_STATE, IDS_LAYOUT_SET, IDS_LEDS, IDS_LEDS_SET, + IDS_MISSING_LAYOUT, IDS_MODIFIERS, IDS_MS, IDS_REPEAT, IDS_REPEAT_SET, IDS_UNKNOWN_CMD, + IDS_USAGE, IDS_USAGE_LINE2, IDS_USAGE_LINE3, IDS_USAGE_LINE4, IDS_USAGE_LINE5, IDS_USAGE_LINE6, + IDS_USAGE_LINE7, IDS_USAGE_LINE8, +}; + +const MODIFIERS: &[(&[u8], u8)] = &[ + (b"LCTRL", 1), (b"LSHIFT", 2), (b"LALT", 4), (b"LGUI", 8), + (b"RCTRL", 16), (b"RSHIFT", 32), (b"RALT", 64), (b"RGUI", 128), +]; + +const LEDS: &[(&[u8], u8)] = &[ + (b"NumLock", 1), (b"CapsLock", 2), (b"ScrollLock", 4), +]; + +pub fn print_help() { + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE2).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE3).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE4).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE5).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE6).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE7).as_bytes()); + write_str(b"\r\n"); + write_str(tr_id!(IDS_USAGE_LINE8).as_bytes()); + write_str(b"\r\n\r\n"); +} + +/// Print modifier/LED state, active layout and repeat config. The repeat values +/// come from NeoKBD, which persists them in the Registry +/// (`\Registry\Machine\System\Keyboard`). +pub fn show_state() -> u32 { + let mut code = 0; + + match keyboard::kbd_get_state() { + Ok(state) => { + write_str(b"\r\n"); + write_str(tr_id!(IDS_KBD_STATE).as_bytes()); + write_str(b"\r\n"); + write_str(b" "); + write_str(tr_id!(IDS_MODIFIERS).as_bytes()); + write_str(b" "); + format_bits(state.modifiers, MODIFIERS); + write_str(b"\r\n"); + + write_str(b" "); + write_str(tr_id!(IDS_LEDS).as_bytes()); + write_str(b" "); + format_bits(state.leds, LEDS); + write_str(b"\r\n"); + } + Err(_) => { + write_err(b"\r\nCannot read keyboard state\r\n"); + code = 1; + } + } + + match keyboard::kbd_get_layout() { + Ok(layout) => { + let end = layout.iter().position(|&b| b == 0).unwrap_or(32); + write_str(b" "); + write_str(tr_id!(IDS_ACTIVE_LAYOUT).as_bytes()); + write_str(&layout[..end]); + write_str(b"\r\n"); + } + Err(_) => { + write_err(b"\r\nError: cannot retrieve keyboard layout\r\n"); + code = 1; + } + } + + if let Ok((delay, rate)) = keyboard::kbd_get_repeat() { + write_str(b" "); + write_str(tr_id!(IDS_REPEAT).as_bytes()); + write_str(b" "); + write_u32(rate); + write_str(tr_id!(IDS_CPS).as_bytes()); + write_str(b"\r\n"); + + write_str(b" "); + write_str(tr_id!(IDS_DELAY).as_bytes()); + write_str(b" "); + write_u32(delay); + write_str(tr_id!(IDS_MS).as_bytes()); + write_str(b"\r\n"); + } + + write_str(b"\r\n"); + code +} + +/// List the layouts NeoKBD has loaded, with their language tags. +pub fn list_layouts() -> u32 { + match keyboard::kbd_list_layouts() { + Ok(list) => { + write_str(b"\r\n"); + write_str(tr_id!(IDS_AVAIL_LAYOUTS).as_bytes()); + write_str(b"\r\n"); + for info in &list { + write_str(b" "); + write_str(info.name_str().as_bytes()); + write_str(b" ("); + write_str(info.lang_tag_str().as_bytes()); + write_str(b")\r\n"); + } + write_str(b"\r\n"); + 0 + } + Err(_) => { + write_err(b"\r\nError reading layout list\r\n"); + 1 + } + } +} + +/// Switch the active layout by name (e.g. `es-ES`, `en-US`). +pub fn set_layout(name: &str) -> u32 { + match keyboard::kbd_set_layout(name) { + Ok(()) => { + write_str(b"\r\n"); + write_str(tr_id!(IDS_LAYOUT_SET).as_bytes()); + write_str(name.as_bytes()); + write_str(b"\r\n\r\n"); + 0 + } + Err(_) => { + write_err(b"\r\n"); + write_err(tr_id!(IDS_ERR_SET_LAYOUT).as_bytes()); + write_err(b"\r\n\r\n"); + 1 + } + } +} + +/// Set the repeat rate, preserving the stored delay (Registry). +pub fn set_repeat_rate(cps: u32) -> u32 { + let (delay, _) = match keyboard::kbd_get_repeat() { + Ok(v) => v, + Err(_) => return err_set(), + }; + match keyboard::kbd_set_repeat(delay, cps) { + Ok(()) => { + write_str(b"\r\n"); + write_str(tr_id!(IDS_REPEAT_SET).as_bytes()); + write_u32(cps); + write_str(tr_id!(IDS_CPS).as_bytes()); + write_str(b"\r\n\r\n"); + 0 + } + Err(_) => err_set(), + } +} + +/// Set the repeat delay, preserving the stored rate (Registry). +pub fn set_repeat_delay(ms: u32) -> u32 { + let (_, rate) = match keyboard::kbd_get_repeat() { + Ok(v) => v, + Err(_) => return err_set(), + }; + match keyboard::kbd_set_repeat(ms, rate) { + Ok(()) => { + write_str(b"\r\n"); + write_str(tr_id!(IDS_DELAY_SET).as_bytes()); + write_u32(ms); + write_str(tr_id!(IDS_MS).as_bytes()); + write_str(b"\r\n\r\n"); + 0 + } + Err(_) => err_set(), + } +} + +/// Set the keyboard LEDs to exactly the given mask. +pub fn set_leds(bits: u8) -> u32 { + match keyboard::kbd_set_leds(bits) { + Ok(()) => { + write_str(b"\r\n"); + write_str(tr_id!(IDS_LEDS_SET).as_bytes()); + format_bits(bits, LEDS); + write_str(b"\r\n\r\n"); + 0 + } + Err(_) => err_set(), + } +} + +pub fn missing_layout() -> u32 { err_line(IDS_MISSING_LAYOUT) } +pub fn invalid_value() -> u32 { err_line(IDS_INVALID_VALUE) } +pub fn unknown_command() -> u32 { err_line(IDS_UNKNOWN_CMD) } + +fn err_set() -> u32 { err_line(IDS_ERR_SET) } + +/// Print a translated error line on stderr and return exit code 1. +fn err_line(id: u32) -> u32 { + write_err(b"\r\n"); + write_err(tr_id!(id).as_bytes()); + write_err(b"\r\n\r\n"); + 1 +} diff --git a/userbin/neokey/src/main.rs b/userbin/neokey/src/main.rs index 2b5b2133..a095c631 100644 --- a/userbin/neokey/src/main.rs +++ b/userbin/neokey/src/main.rs @@ -9,125 +9,96 @@ fn noop_test_runner(_tests: &[&dyn Fn()]) { loop {} } +mod commands; +mod output; +mod parse; + use libneodos::i18n; -use libneodos::keyboard; use libneodos::syscall; -use libneodos::tr_id; const APP_NAME: &str = "neokey"; + +// ── Locale IDs (keep in sync with data/locale/*/neokey.toml) ── +// NOTE: kept in `main.rs` because `scripts/check-i18n.sh` verifies the +// code<->catalog bindings against this file only. +const IDS_USAGE: u32 = 1001; +const IDS_USAGE_LINE2: u32 = 1002; +const IDS_USAGE_LINE3: u32 = 1003; +const IDS_USAGE_LINE4: u32 = 1004; const IDS_KBD_STATE: u32 = 1005; const IDS_MODIFIERS: u32 = 1006; const IDS_LEDS: u32 = 1007; const IDS_ACTIVE_LAYOUT: u32 = 1008; const IDS_AVAIL_LAYOUTS: u32 = 1009; const IDS_UNKNOWN_CMD: u32 = 1010; - -fn write_str(s: &[u8]) { - let _ = syscall::sys_write(1, s); -} - -fn write_err(s: &[u8]) { - let _ = syscall::sys_write(2, s); -} - -fn print_help() { - write_str(b"\r\nNEOKEY [option]\r\n Display keyboard state and available layouts.\r\n NEOKEY shows keyboard state\r\n NEOKEY --list lists available layouts\r\n\r\n"); -} - -fn format_bits(val: u8, names: &[(&[u8], u8)]) { - let mut first = true; - for (name, bit) in names { - if val & bit != 0 { - if !first { write_str(b" "); } - write_str(name); - first = false; - } - } - if first { write_str(b"(none)"); } -} +const IDS_LAYOUT_SET: u32 = 1011; +const IDS_ERR_SET_LAYOUT: u32 = 1012; +const IDS_MISSING_LAYOUT: u32 = 1013; +const IDS_USAGE_LINE5: u32 = 1014; +const IDS_USAGE_LINE6: u32 = 1015; +const IDS_USAGE_LINE7: u32 = 1016; +const IDS_USAGE_LINE8: u32 = 1017; +const IDS_REPEAT: u32 = 1018; +const IDS_DELAY: u32 = 1019; +const IDS_REPEAT_SET: u32 = 1020; +const IDS_DELAY_SET: u32 = 1021; +const IDS_LEDS_SET: u32 = 1022; +const IDS_ERR_SET: u32 = 1023; +const IDS_INVALID_VALUE: u32 = 1024; +const IDS_CPS: u32 = 1025; +const IDS_MS: u32 = 1026; #[no_mangle] pub extern "C" fn _start() -> ! { i18n::i18n_init(); let _ = i18n::i18n_load(APP_NAME); + let raw = libneodos::args::read_args(); if libneodos::args::is_help_flag(&raw) { - print_help(); + commands::print_help(); syscall::sys_exit(0); } let args = libneodos::args::trim_ascii(&raw); - - if !args.is_empty() { - let arg_str = core::str::from_utf8(args).unwrap_or(""); - if arg_str.eq_ignore_ascii_case("--list") || arg_str.eq_ignore_ascii_case("-l") { - match keyboard::kbd_list_layouts() { - Ok(list) => { - write_str(b"\r\n"); - write_str(tr_id!(IDS_AVAIL_LAYOUTS).as_bytes()); - write_str(b"\r\n"); - for info in &list { - write_str(b" "); - write_str(info.name_str().as_bytes()); - write_str(b" ("); - write_str(info.lang_tag_str().as_bytes()); - write_str(b")\r\n"); - } - write_str(b"\r\n"); - } - Err(_) => { - write_err(b"\r\nError reading layout list\r\n"); - } - } - syscall::sys_exit(0); - } - - write_err(b"\r\n"); - write_err(tr_id!(IDS_UNKNOWN_CMD).as_bytes()); - write_err(b"\r\n"); - syscall::sys_exit(1); + if args.is_empty() { + syscall::sys_exit(commands::show_state()); } - match keyboard::kbd_get_state() { - Ok(state) => { - write_str(b"\r\n"); - write_str(tr_id!(IDS_KBD_STATE).as_bytes()); - write_str(b"\r\n"); - write_str(b" "); - write_str(tr_id!(IDS_MODIFIERS).as_bytes()); - write_str(b" "); - format_bits(state.modifiers, &[ - (b"LCTRL", 1), (b"LSHIFT", 2), (b"LALT", 4), (b"LGUI", 8), - (b"RCTRL", 16), (b"RSHIFT", 32), (b"RALT", 64), (b"RGUI", 128), - ]); - write_str(b"\r\n"); + let arg_str = core::str::from_utf8(args).unwrap_or(""); + let mut words = arg_str.split_whitespace(); + let cmd = words.next().unwrap_or(""); + let value = words.next(); - write_str(b" "); - write_str(tr_id!(IDS_LEDS).as_bytes()); - write_str(b" "); - format_bits(state.leds, &[ - (b"NumLock", 1), (b"CapsLock", 2), (b"ScrollLock", 4), - ]); - write_str(b"\r\n"); + let code = if cmd.eq_ignore_ascii_case("show") { + commands::show_state() + } else if cmd.eq_ignore_ascii_case("layouts") + || cmd.eq_ignore_ascii_case("--list") + || cmd.eq_ignore_ascii_case("-l") + { + commands::list_layouts() + } else if cmd.eq_ignore_ascii_case("layout") { + match value { + Some(name) if !name.is_empty() => commands::set_layout(name), + _ => commands::missing_layout(), } - Err(_) => { - write_err(b"\r\nCannot read keyboard state\r\n"); + } else if cmd.eq_ignore_ascii_case("repeat") { + match value.and_then(parse::u32) { + Some(cps) => commands::set_repeat_rate(cps), + None => commands::invalid_value(), } - } - - match keyboard::kbd_get_layout() { - Ok(layout) => { - let end = layout.iter().position(|&b| b == 0).unwrap_or(32); - write_str(b" "); - write_str(tr_id!(IDS_ACTIVE_LAYOUT).as_bytes()); - write_str(&layout[..end]); - write_str(b"\r\n"); + } else if cmd.eq_ignore_ascii_case("delay") { + match value.and_then(parse::u32) { + Some(ms) => commands::set_repeat_delay(ms), + None => commands::invalid_value(), } - Err(_) => { - write_err(b"\r\nError: cannot retrieve keyboard layout\r\n"); + } else if cmd.eq_ignore_ascii_case("leds") { + match value.and_then(parse::led_mask) { + Some(bits) => commands::set_leds(bits), + None => commands::invalid_value(), } - } + } else { + commands::unknown_command() + }; - write_str(b"\r\n"); - syscall::sys_exit(0) + syscall::sys_exit(code) } diff --git a/userbin/neokey/src/output.rs b/userbin/neokey/src/output.rs new file mode 100644 index 00000000..12d5ee4d --- /dev/null +++ b/userbin/neokey/src/output.rs @@ -0,0 +1,37 @@ +//! Low-level console output helpers for NEOKEY. + +use libneodos::syscall; + +pub fn write_str(s: &[u8]) { + let _ = syscall::sys_write(1, s); +} + +pub fn write_err(s: &[u8]) { + let _ = syscall::sys_write(2, s); +} + +pub fn write_u32(mut v: u32) { + if v == 0 { write_str(b"0"); return; } + let mut buf = [0u8; 10]; + let mut i = 9; + while v > 0 { + buf[i] = b'0' + (v % 10) as u8; + v /= 10; + i -= 1; + } + write_str(&buf[i + 1..]); +} + +/// Print the set bits of `val` as space-separated `(name, bit)` labels, +/// or `(none)` when no bit is set. +pub fn format_bits(val: u8, names: &[(&[u8], u8)]) { + let mut first = true; + for (name, bit) in names { + if val & bit != 0 { + if !first { write_str(b" "); } + write_str(name); + first = false; + } + } + if first { write_str(b"(none)"); } +} diff --git a/userbin/neokey/src/parse.rs b/userbin/neokey/src/parse.rs new file mode 100644 index 00000000..a3530e36 --- /dev/null +++ b/userbin/neokey/src/parse.rs @@ -0,0 +1,34 @@ +//! Argument parsing helpers for NEOKEY. + +/// Parse a non-empty decimal string into a `u32`. +pub fn u32(s: &str) -> Option { + if s.is_empty() { return None; } + let mut v: u32 = 0; + for &b in s.as_bytes() { + if !b.is_ascii_digit() { return None; } + v = v.checked_mul(10)?.checked_add((b - b'0') as u32)?; + } + Some(v) +} + +/// Parse a comma-separated LED list (`num`, `caps`, `scroll`, `none`). +pub fn led_mask(s: &str) -> Option { + let mut bits = 0u8; + let mut any = false; + for tok in s.split(',') { + if tok.is_empty() { continue; } + any = true; + if tok.eq_ignore_ascii_case("num") || tok.eq_ignore_ascii_case("numlock") { + bits |= 1; + } else if tok.eq_ignore_ascii_case("caps") || tok.eq_ignore_ascii_case("capslock") { + bits |= 2; + } else if tok.eq_ignore_ascii_case("scroll") || tok.eq_ignore_ascii_case("scrolllock") { + bits |= 4; + } else if tok.eq_ignore_ascii_case("none") || tok.eq_ignore_ascii_case("off") { + // explicitly no LED + } else { + return None; + } + } + if any { Some(bits) } else { None } +}