diff --git a/crates/openlogi-core/src/config.rs b/crates/openlogi-core/src/config.rs index d4ba4ac55..598e2faba 100644 --- a/crates/openlogi-core/src/config.rs +++ b/crates/openlogi-core/src/config.rs @@ -767,6 +767,26 @@ impl Config { .dpi_presets = presets; } + /// The pointing-device config keys that follow `device_key`'s (a + /// keyboard's) host-switch channel, or an empty `Vec` if none are + /// configured yet. + #[must_use] + pub fn host_switch_targets(&self, device_key: &str) -> Vec { + self.devices + .get(device_key) + .map(|d| d.host_switch_targets.clone()) + .unwrap_or_default() + } + + /// Replace the set of pointing devices that follow `device_key`'s host + /// switch. Pass an empty `Vec` to clear. + pub fn set_host_switch_targets(&mut self, device_key: &str, targets: Vec) { + self.devices + .entry(device_key.to_string()) + .or_default() + .host_switch_targets = targets; + } + /// The last-known [`DeviceIdentity`] for `device_key`, or `None` if the /// device has never been seen online (or was configured before identities /// were recorded). diff --git a/crates/openlogi-core/src/config/tests.rs b/crates/openlogi-core/src/config/tests.rs index f8d7b87c4..6cb0c2c26 100644 --- a/crates/openlogi-core/src/config/tests.rs +++ b/crates/openlogi-core/src/config/tests.rs @@ -586,6 +586,8 @@ fn device_identity_roundtrips_and_is_iterable() { haptic_feedback: false, haptic_panel: false, dpi_gestures: true, + host_switch_target: false, + host_switch_source: false, }, light_capabilities: None, driver_id: None, diff --git a/crates/openlogi-core/src/device.rs b/crates/openlogi-core/src/device.rs index 510673597..278847b6e 100644 --- a/crates/openlogi-core/src/device.rs +++ b/crates/openlogi-core/src/device.rs @@ -132,6 +132,17 @@ pub struct Capabilities { /// both diversion and raw-XY reporting for hold-and-swipe gestures. #[serde(default)] pub dpi_gestures: bool, + /// This device can be switched to by another device's Easy-Switch host + /// change — HID++ `0x1814`/`0x1815 ChangeHost`. A pointing device needs + /// this to be a host-switch link's *target*. + #[serde(default)] + pub host_switch_target: bool, + /// A host-switch-channel control in the device's `0x1b04` control table + /// is divertable or reports analytics key events, so pressing it can be + /// detected and used to trigger a linked device's host change. A + /// keyboard needs this to be a host-switch link's *source*. + #[serde(default)] + pub host_switch_source: bool, } impl Capabilities { @@ -144,6 +155,7 @@ impl Capabilities { // Every family here is driven by `set_keyboard_color`, which tries // effect engines before per-zone paths. Backlight (0x198x) stays out. const LIGHTING: [u16; 4] = [0x8070, 0x8071, 0x8081, 0x8080]; + const CHANGE_HOST: [u16; 2] = [0x1814, 0x1815]; let has = |family: &[u16]| ids.iter().any(|id| family.contains(id)); Self { buttons: has(&BUTTONS), @@ -155,6 +167,8 @@ impl Capabilities { haptic_feedback: ids.contains(&0x19b0), haptic_panel: false, dpi_gestures: false, + host_switch_target: has(&CHANGE_HOST), + host_switch_source: false, } } @@ -176,6 +190,8 @@ impl Capabilities { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }, DeviceKind::Keyboard => Self { lighting: true, @@ -482,6 +498,8 @@ mod tests { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }), }], } @@ -553,6 +571,8 @@ mod tests { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, } ); assert!(!Capabilities::from_feature_ids(&[0x0003, 0x1b04]).thumbwheel); @@ -570,6 +590,8 @@ mod tests { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, } ); // No driving features → nothing offered. diff --git a/crates/openlogi-core/src/diagnostics.rs b/crates/openlogi-core/src/diagnostics.rs index d4cfb98c0..f2db9dfbb 100644 --- a/crates/openlogi-core/src/diagnostics.rs +++ b/crates/openlogi-core/src/diagnostics.rs @@ -598,6 +598,8 @@ mod tests { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }), dpi: Some("1600 dpi (range 200–8000, 5 steps)".to_string()), config_key: "4082d".to_string(), diff --git a/crates/openlogi-desktop/src/app/detail.rs b/crates/openlogi-desktop/src/app/detail.rs index a1e00988e..fe8885d66 100644 --- a/crates/openlogi-desktop/src/app/detail.rs +++ b/crates/openlogi-desktop/src/app/detail.rs @@ -35,7 +35,7 @@ use crate::features::pointer::smartshift::SmartShiftPanel; use crate::features::profiles::{ AppCatalogPicker, ProfileIconCache, action_ring_profile_scope_bar, button_profile_scope_bar, }; -use crate::state::{AppState, DeviceRecord, StateEvent}; +use crate::state::{AppState, DeviceRecord, HostSwitchCandidate, StateEvent}; use crate::ui::battery::BatteryIndicator; use crate::ui::components::{PanelCard, Toggle}; use crate::ui::theme::{ @@ -635,13 +635,59 @@ fn light_tab( /// Device tab: device details and configuration cards stacked. fn device_tab(cx: &mut Context) -> impl IntoElement { let pal = theme::palette(cx); + let host_switch_candidates = AppState::try_read(cx) + .map(AppState::host_switch_candidates) + .unwrap_or_default(); tab_body( ContentWidth::Small, v_flex() .w_full() .gap_3() .child(device_details_card(pal, cx)) - .child(configuration_card(pal, cx)), + .child(configuration_card(pal, cx)) + .when(!host_switch_candidates.is_empty(), |content| { + content.child(host_switch_card(host_switch_candidates)) + }), + ) +} + +/// "Follow host switches from:" — a checkbox per other configured keyboard, +/// letting this pointing device be added to that keyboard's +/// `host_switch_targets` so it follows when the keyboard's Easy-Switch key +/// changes host. Shown only for a persistent mouse/trackball with at least +/// one other configured keyboard to follow. +fn host_switch_card(candidates: Vec) -> impl IntoElement { + let content = v_flex().gap_2().children(candidates.into_iter().map( + |HostSwitchCandidate { + config_key, + display_name, + following, + }| { + h_flex() + .justify_between() + .items_center() + .child(div().text_body().child(display_name)) + .child( + Switch::new(gpui::ElementId::Name( + format!("host-switch-follow-{config_key}").into(), + )) + .checked(following) + .on_click(move |checked, _window, cx| { + let checked = *checked; + let keyboard_key = config_key.clone(); + AppState::update(cx, |state, cx| { + state.set_host_switch_follow(&keyboard_key, checked); + cx.emit(StateEvent::DeviceConfigChanged(keyboard_key.into())); + }); + }), + ) + }, + )); + + PanelCard::new( + tr!("device.host_switch_targets"), + Icon::empty().path("action-icons/keyboard.svg"), + content, ) } diff --git a/crates/openlogi-desktop/src/app/tests.rs b/crates/openlogi-desktop/src/app/tests.rs index b452f5b5d..c17b10a39 100644 --- a/crates/openlogi-desktop/src/app/tests.rs +++ b/crates/openlogi-desktop/src/app/tests.rs @@ -188,6 +188,8 @@ fn tabs_follow_capabilities_not_kind() { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }); // After 0x0005 kind-correction the record has kind=Mouse, not Keyboard. let tabs = DetailTab::tabs_for(&record(DeviceKind::Mouse, caps)); @@ -211,6 +213,8 @@ fn keyboard_without_asset_hides_buttons_tab() { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }); let tabs = DetailTab::tabs_for(&record(DeviceKind::Keyboard, caps)); assert!( @@ -232,6 +236,8 @@ fn keyboard_with_buttons_shows_keys_tab() { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }); let tabs = DetailTab::tabs_for(&record(DeviceKind::Keyboard, caps)); assert!(tabs.contains(&DetailTab::Keys)); diff --git a/crates/openlogi-desktop/src/state.rs b/crates/openlogi-desktop/src/state.rs index ecfc6814a..85d477965 100644 --- a/crates/openlogi-desktop/src/state.rs +++ b/crates/openlogi-desktop/src/state.rs @@ -22,6 +22,7 @@ use tracing::warn; pub use config::ConfigPersistence; pub(crate) use device_key::DeviceKey; pub use devices::DeviceRecord; +pub use host_switch::HostSwitchCandidate; pub use light::LightCommandStatus; pub(crate) use load::Load; pub use load::{DpiStatus, SmartShiftLoad}; @@ -64,6 +65,7 @@ mod device_runtime; mod device_store; mod devices; mod dpi; +mod host_switch; mod inventory; mod light; mod lighting; diff --git a/crates/openlogi-desktop/src/state/devices/tests.rs b/crates/openlogi-desktop/src/state/devices/tests.rs index ba939e814..0a4b75128 100644 --- a/crates/openlogi-desktop/src/state/devices/tests.rs +++ b/crates/openlogi-desktop/src/state/devices/tests.rs @@ -268,6 +268,8 @@ fn mouse_identity(name: &str) -> DeviceIdentity { haptic_feedback: false, haptic_panel: false, dpi_gestures: false, + host_switch_target: false, + host_switch_source: false, }, light_capabilities: None, model_info: None, diff --git a/crates/openlogi-desktop/src/state/host_switch.rs b/crates/openlogi-desktop/src/state/host_switch.rs new file mode 100644 index 000000000..66e7f020c --- /dev/null +++ b/crates/openlogi-desktop/src/state/host_switch.rs @@ -0,0 +1,248 @@ +//! Easy-Switch host-switch links: which keyboards a pointing device follows. +//! +//! `host_switch_targets` is stored on the *keyboard*'s config entry (a list of +//! the pointing devices that follow it — see +//! [`openlogi_core::config::device::DeviceConfig::host_switch_targets`]), but +//! it reads more naturally from the mouse's side ("which keyboard do I follow +//! when it switches hosts?"), so the panel is built here and the write is +//! aimed at the other device's config entry instead of the current one. + +use openlogi_core::device::DeviceKind; + +use super::AppState; + +/// One keyboard the active pointing device could follow, and whether it +/// already does. +pub struct HostSwitchCandidate { + pub config_key: String, + pub display_name: String, + pub following: bool, +} + +impl AppState { + /// Keyboards the active device could follow on host switch, in device + /// gallery order. Empty when the active device isn't a persistent + /// pointing device that measured `ChangeHost` support (HID++ + /// `0x1814`/`0x1815`), and each candidate keyboard is further filtered to + /// ones that measured an armable host-switch control in their `0x1b04` + /// control table — gating on kind alone would let an unsupported + /// pairing be toggled on with no way for the agent to ever act on it. + #[must_use] + pub fn host_switch_candidates(&self) -> Vec { + let Some(target_key) = self + .current_record() + .filter(|record| matches!(record.kind, DeviceKind::Mouse | DeviceKind::Trackball)) + .filter(|record| record.is_persistent()) + .filter(|record| { + record + .capabilities + .is_some_and(|caps| caps.host_switch_target) + }) + .map(|record| record.config_key.clone()) + else { + return Vec::new(); + }; + self.devices() + .iter() + .filter(|record| record.kind == DeviceKind::Keyboard && record.is_persistent()) + .filter(|record| { + record + .capabilities + .is_some_and(|caps| caps.host_switch_source) + }) + .map(|record| HostSwitchCandidate { + config_key: record.config_key.clone(), + display_name: record.display_name.clone(), + following: self + .config + .host_switch_targets(&record.config_key) + .iter() + .any(|key| key == &target_key), + }) + .collect() + } + + /// Add or remove the active pointing device from `keyboard_key`'s + /// host-switch targets. No-op when the active device isn't persistent. + pub fn set_host_switch_follow(&mut self, keyboard_key: &str, follow: bool) { + let Some(target_key) = self + .current_record() + .filter(|record| record.is_persistent()) + .map(|record| record.config_key.clone()) + else { + return; + }; + self.config.edit(|config| { + let mut targets = config.host_switch_targets(keyboard_key); + let already_present = targets.iter().any(|key| key == &target_key); + if follow && !already_present { + targets.push(target_key.clone()); + } else if !follow { + targets.retain(|key| key != &target_key); + } + config.set_host_switch_targets(keyboard_key, targets); + }); + self.persist_and_reload("host-switch target"); + } +} + +#[cfg(test)] +mod tests { + use openlogi_core::config::Config; + use openlogi_core::device::{ + Capabilities, DeviceInventory, DeviceKind, DeviceModelInfo, DeviceTransports, PairedDevice, + ReceiverInfo, + }; + + use super::super::ConfigPersistence; + use super::AppState; + use crate::services::assets::AssetResolver; + + fn direct_device( + unit_id: [u8; 4], + kind: DeviceKind, + name: &str, + supports_host_switch: bool, + ) -> DeviceInventory { + DeviceInventory { + receiver: ReceiverInfo { + name: name.to_string(), + vendor_id: 0x046d, + product_id: 0xb023, + unique_id: None, + }, + paired: vec![PairedDevice { + slot: openlogi_core::hid::DIRECT_DEVICE_INDEX, + codename: Some(name.to_string()), + wpid: None, + kind, + online: true, + battery: None, + model_info: Some(DeviceModelInfo { + entity_count: 1, + serial_number: None, + unit_id, + transports: DeviceTransports::default(), + model_ids: [0xb034, 0, 0], + extended_model_id: 2, + }), + capabilities: Some(Capabilities { + host_switch_target: supports_host_switch, + host_switch_source: supports_host_switch, + ..Capabilities::presumed_from_kind(kind) + }), + }], + } + } + + /// A state with one persistent mouse (the active device) and one + /// persistent keyboard, both measuring host-switch support, so a + /// host-switch link can be formed between them. + fn state_with_a_mouse_and_a_keyboard() -> AppState { + let cache = AssetResolver::new(); + let (commands, _receiver) = tokio::sync::mpsc::unbounded_channel(); + let mouse = direct_device( + [0x01, 0x02, 0x03, 0x04], + DeviceKind::Mouse, + "MX Master 3S", + true, + ); + let keyboard = direct_device( + [0x05, 0x06, 0x07, 0x08], + DeviceKind::Keyboard, + "MX Keys S", + true, + ); + AppState::with_runtime( + Config::ephemeral(), + &[mouse, keyboard], + &[], + &cache, + &[], + ConfigPersistence::MemoryOnly, + commands, + ) + } + + #[test] + fn candidates_list_persistent_keyboards_only_for_the_active_mouse() { + let state = state_with_a_mouse_and_a_keyboard(); + assert!( + state + .devices() + .iter() + .any(|record| record.display_name == "MX Master 3S"), + "the mouse should be the active device by default" + ); + + let candidates = state.host_switch_candidates(); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].display_name, "MX Keys S"); + assert!(!candidates[0].following); + } + + #[test] + fn following_toggles_the_keyboards_host_switch_targets() { + let mut state = state_with_a_mouse_and_a_keyboard(); + let keyboard_key = state.host_switch_candidates()[0].config_key.clone(); + + state.set_host_switch_follow(&keyboard_key, true); + assert!(state.host_switch_candidates()[0].following); + + state.set_host_switch_follow(&keyboard_key, false); + assert!(!state.host_switch_candidates()[0].following); + } + + #[test] + fn a_mouse_without_change_host_support_lists_no_candidates() { + let cache = AssetResolver::new(); + let (commands, _receiver) = tokio::sync::mpsc::unbounded_channel(); + let mouse = direct_device([0x01, 0x02, 0x03, 0x04], DeviceKind::Mouse, "M185", false); + let keyboard = direct_device( + [0x05, 0x06, 0x07, 0x08], + DeviceKind::Keyboard, + "MX Keys S", + true, + ); + let state = AppState::with_runtime( + Config::ephemeral(), + &[mouse, keyboard], + &[], + &cache, + &[], + ConfigPersistence::MemoryOnly, + commands, + ); + + assert!(state.host_switch_candidates().is_empty()); + } + + #[test] + fn a_keyboard_without_an_armable_host_switch_control_is_not_a_candidate() { + let cache = AssetResolver::new(); + let (commands, _receiver) = tokio::sync::mpsc::unbounded_channel(); + let mouse = direct_device( + [0x01, 0x02, 0x03, 0x04], + DeviceKind::Mouse, + "MX Master 3S", + true, + ); + let keyboard = direct_device( + [0x05, 0x06, 0x07, 0x08], + DeviceKind::Keyboard, + "K120", + false, + ); + let state = AppState::with_runtime( + Config::ephemeral(), + &[mouse, keyboard], + &[], + &cache, + &[], + ConfigPersistence::MemoryOnly, + commands, + ); + + assert!(state.host_switch_candidates().is_empty()); + } +} diff --git a/crates/openlogi-device/src/inventory/features.rs b/crates/openlogi-device/src/inventory/features.rs index e5a2d701d..9b074a02c 100644 --- a/crates/openlogi-device/src/inventory/features.rs +++ b/crates/openlogi-device/src/inventory/features.rs @@ -21,7 +21,7 @@ use openlogi_core::device::{ use serde::{Deserialize, Serialize}; use tracing::debug; -use crate::reprog_controls::DPI_MODE_SHIFT_CIDS; +use crate::reprog_controls::{DPI_MODE_SHIFT_CIDS, HOST_SWITCH_CIDS}; use super::events::{EventFeatureIndices, EventSubscriptionHandle}; use super::mappings::{ @@ -341,6 +341,7 @@ async fn probe_extra_capabilities( let count = feature.get_count().await.map_err(|_| ())?; let mut haptic_panel = false; let mut dpi_gestures = false; + let mut host_switch_source = false; for index in 0..count { let info = feature.get_cid_info(index).await.map_err(|_| ())?; haptic_panel |= probe_haptic_controls @@ -349,11 +350,14 @@ async fn probe_extra_capabilities( dpi_gestures |= DPI_MODE_SHIFT_CIDS.contains(&info.cid.0) && info.flags.is_divertable() && info.flags.supports_raw_xy(); + host_switch_source |= HOST_SWITCH_CIDS.contains(&info.cid.0) + && (info.flags.is_divertable() || info.flags.supports_analytics_key_events()); } // Publish only a complete control walk. A lost reply must retain the // cache's last-good capabilities and schedule repair, not hide support. caps.haptic_panel = haptic_panel; caps.dpi_gestures = dpi_gestures; + caps.host_switch_source = host_switch_source; } Ok(()) } diff --git a/crates/openlogi-device/src/reprog_controls.rs b/crates/openlogi-device/src/reprog_controls.rs index 822d0d4cd..97d7d1304 100644 --- a/crates/openlogi-device/src/reprog_controls.rs +++ b/crates/openlogi-device/src/reprog_controls.rs @@ -63,6 +63,16 @@ pub const HAPTIC_PANEL_CID: u16 = control_ids::HAPTIC_PANEL.0; /// cross-checked against Solaar `special_keys.py`. pub const DPI_MODE_SHIFT_CIDS: [u16; 3] = [0x00c4, 0x00ed, 0x00fd]; +/// Control IDs of the host-switch-channel buttons, used both to arm a live +/// host-switch session (see `crate::session::host_switch`) and to derive +/// [`openlogi_core::device::Capabilities::host_switch_source`] from a +/// one-time control-table walk. +pub const HOST_SWITCH_CIDS: [u16; 3] = [ + control_ids::HOST_SWITCH_CHANNEL_1.0, + control_ids::HOST_SWITCH_CHANNEL_2.0, + control_ids::HOST_SWITCH_CHANNEL_3.0, +]; + /// Control IDs of the Back button family. MX Vertical and similar devices /// report Back via HID++ `0x1b04` rather than a standard OS mouse button, /// so macOS never translates them into `OtherMouseDown` events. Whichever a diff --git a/crates/openlogi-ipc/src/ipc.rs b/crates/openlogi-ipc/src/ipc.rs index 65807870d..be412ccab 100644 --- a/crates/openlogi-ipc/src/ipc.rs +++ b/crates/openlogi-ipc/src/ipc.rs @@ -63,7 +63,7 @@ pub use succession::Identity; /// the macOS dormancy gate. /// v30: `Agent::read_wheel` and `Agent::read_backlight` appended. /// v31: `Capabilities::dpi_gestures` appended. -pub const PROTOCOL_VERSION: u32 = 31; +pub const PROTOCOL_VERSION: u32 = 32; /// Environment variable through which the agent hands a supervised helper the /// run token it will serve, so the helper knows which agent it belongs to diff --git a/crates/openlogi-ipc/tests/wire_format.rs b/crates/openlogi-ipc/tests/wire_format.rs index 0d7130cab..ac358af71 100644 --- a/crates/openlogi-ipc/tests/wire_format.rs +++ b/crates/openlogi-ipc/tests/wire_format.rs @@ -102,7 +102,7 @@ fn representative_smartshift_status() -> SmartShiftStatus { /// that makes that visible in the same diff. #[test] fn protocol_version_is_pinned() { - assert_eq!(PROTOCOL_VERSION, 31); + assert_eq!(PROTOCOL_VERSION, 32); } #[test] @@ -439,12 +439,14 @@ fn device_inventory() { haptic_feedback: true, haptic_panel: true, dpi_gestures: true, + host_switch_target: true, + host_switch_source: true, }), }], }]; assert_wire( &inventory, - "010d426f6c74205265636569766572fb6d04fb48c501084630304443414645010101094d58204d535452335301fb34b000010150020001030106323134304c5a0102030400010100fb34b0fb8240000b01010100000101010101", + "010d426f6c74205265636569766572fb6d04fb48c501084630304443414645010101094d58204d535452335301fb34b000010150020001030106323134304c5a0102030400010100fb34b0fb8240000b010101000001010101010101", ); } diff --git a/crates/openlogi-ui/locales/be.toml b/crates/openlogi-ui/locales/be.toml index 7f955b051..cb17f6639 100644 --- a/crates/openlogi-ui/locales/be.toml +++ b/crates/openlogi-ui/locales/be.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Пакіньце поле пустым, к pointer_tuning = "Настройка ўказальніка" device_details = "Звесткі пра прыладу" configuration = "Канфігурацыя" +host_switch_targets = "Follow host switches from" connection = "Падключэнне" slot = "Слот" receiver_slot_number = "Слот %{number}" diff --git a/crates/openlogi-ui/locales/da.toml b/crates/openlogi-ui/locales/da.toml index 5036a25d8..18d921158 100644 --- a/crates/openlogi-ui/locales/da.toml +++ b/crates/openlogi-ui/locales/da.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Lad feltet være tomt for at bruge modelnav pointer_tuning = "Markørjustering" device_details = "Enhedsoplysninger" configuration = "Konfiguration" +host_switch_targets = "Follow host switches from" connection = "Forbindelse" slot = "Plads" receiver_slot_number = "Plads %{number}" diff --git a/crates/openlogi-ui/locales/de.toml b/crates/openlogi-ui/locales/de.toml index 6b7529841..39994193d 100644 --- a/crates/openlogi-ui/locales/de.toml +++ b/crates/openlogi-ui/locales/de.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Leer lassen, um den Modellnamen zu verwende pointer_tuning = "Zeigereinstellung" device_details = "Gerätedetails" configuration = "Konfiguration" +host_switch_targets = "Follow host switches from" connection = "Verbindung" slot = "Steckplatz" receiver_slot_number = "Steckplatz %{number}" diff --git a/crates/openlogi-ui/locales/el.toml b/crates/openlogi-ui/locales/el.toml index fd2ae7524..d04c9feed 100644 --- a/crates/openlogi-ui/locales/el.toml +++ b/crates/openlogi-ui/locales/el.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Αφήστε το κενό για να χρ pointer_tuning = "Ρύθμιση δείκτη" device_details = "Στοιχεία συσκευής" configuration = "Διαμόρφωση" +host_switch_targets = "Follow host switches from" connection = "Σύνδεση" slot = "Θέση" receiver_slot_number = "Θέση %{number}" diff --git a/crates/openlogi-ui/locales/en.toml b/crates/openlogi-ui/locales/en.toml index 90ab3355a..4cec62357 100644 --- a/crates/openlogi-ui/locales/en.toml +++ b/crates/openlogi-ui/locales/en.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Leave blank to use the model name." pointer_tuning = "Pointer tuning" device_details = "Device details" configuration = "Configuration" +host_switch_targets = "Follow host switches from" connection = "Connection" slot = "Slot" receiver_slot_number = "Slot %{number}" diff --git a/crates/openlogi-ui/locales/es.toml b/crates/openlogi-ui/locales/es.toml index 19a724ce1..8e8a61bc8 100644 --- a/crates/openlogi-ui/locales/es.toml +++ b/crates/openlogi-ui/locales/es.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Déjalo en blanco para usar el nombre del m pointer_tuning = "Ajuste del puntero" device_details = "Detalles del dispositivo" configuration = "Configuración" +host_switch_targets = "Follow host switches from" connection = "Conexión" slot = "Ranura" receiver_slot_number = "Ranura %{number}" diff --git a/crates/openlogi-ui/locales/fi.toml b/crates/openlogi-ui/locales/fi.toml index 69eb77e97..bae8eeb1b 100644 --- a/crates/openlogi-ui/locales/fi.toml +++ b/crates/openlogi-ui/locales/fi.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Jätä tyhjäksi käyttääksesi mallin nim pointer_tuning = "Osoittimen säätö" device_details = "Laitteen tiedot" configuration = "Määritykset" +host_switch_targets = "Follow host switches from" connection = "Yhteys" slot = "Paikka" receiver_slot_number = "Paikka %{number}" diff --git a/crates/openlogi-ui/locales/fr.toml b/crates/openlogi-ui/locales/fr.toml index 14cf1be78..e89ebe055 100644 --- a/crates/openlogi-ui/locales/fr.toml +++ b/crates/openlogi-ui/locales/fr.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Laissez ce champ vide pour utiliser le nom pointer_tuning = "Réglage du pointeur" device_details = "Détails de l’appareil" configuration = "Configuration" +host_switch_targets = "Follow host switches from" connection = "Connexion" slot = "Emplacement" receiver_slot_number = "Emplacement %{number}" diff --git a/crates/openlogi-ui/locales/it.toml b/crates/openlogi-ui/locales/it.toml index 072eb24c3..2bb8d34c9 100644 --- a/crates/openlogi-ui/locales/it.toml +++ b/crates/openlogi-ui/locales/it.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Lascia vuoto per usare il nome del modello. pointer_tuning = "Regolazione puntatore" device_details = "Dettagli dispositivo" configuration = "Configurazione" +host_switch_targets = "Follow host switches from" connection = "Connessione" slot = "Slot" receiver_slot_number = "Slot %{number}" diff --git a/crates/openlogi-ui/locales/ja.toml b/crates/openlogi-ui/locales/ja.toml index ad7a389d6..e6f4751f3 100644 --- a/crates/openlogi-ui/locales/ja.toml +++ b/crates/openlogi-ui/locales/ja.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "空欄にするとモデル名が使用さ pointer_tuning = "ポインター調整" device_details = "デバイス詳細" configuration = "設定" +host_switch_targets = "Follow host switches from" connection = "接続" slot = "スロット" receiver_slot_number = "スロット %{number}" diff --git a/crates/openlogi-ui/locales/ko.toml b/crates/openlogi-ui/locales/ko.toml index bdef40e52..1b60ec449 100644 --- a/crates/openlogi-ui/locales/ko.toml +++ b/crates/openlogi-ui/locales/ko.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "비워 두면 모델 이름을 사용합니 pointer_tuning = "포인터 조정" device_details = "기기 정보" configuration = "설정" +host_switch_targets = "Follow host switches from" connection = "연결" slot = "슬롯" receiver_slot_number = "슬롯 %{number}" diff --git a/crates/openlogi-ui/locales/nb.toml b/crates/openlogi-ui/locales/nb.toml index 908dedbac..2b4e46d14 100644 --- a/crates/openlogi-ui/locales/nb.toml +++ b/crates/openlogi-ui/locales/nb.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "La stå tomt for å bruke modellnavnet." pointer_tuning = "Pekerjustering" device_details = "Enhetsdetaljer" configuration = "Konfigurasjon" +host_switch_targets = "Follow host switches from" connection = "Tilkobling" slot = "Plass" receiver_slot_number = "Plass %{number}" diff --git a/crates/openlogi-ui/locales/nl.toml b/crates/openlogi-ui/locales/nl.toml index 9ba5a24f1..315c311ef 100644 --- a/crates/openlogi-ui/locales/nl.toml +++ b/crates/openlogi-ui/locales/nl.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Laat leeg om de modelnaam te gebruiken." pointer_tuning = "Aanwijzer afstemmen" device_details = "Apparaatgegevens" configuration = "Configuratie" +host_switch_targets = "Follow host switches from" connection = "Verbinding" slot = "Sleuf" receiver_slot_number = "Sleuf %{number}" diff --git a/crates/openlogi-ui/locales/pl.toml b/crates/openlogi-ui/locales/pl.toml index 68887f1bd..2d34aa921 100644 --- a/crates/openlogi-ui/locales/pl.toml +++ b/crates/openlogi-ui/locales/pl.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Pozostaw puste, aby użyć nazwy modelu." pointer_tuning = "Dostrajanie wskaźnika" device_details = "Szczegóły urządzenia" configuration = "Konfiguracja" +host_switch_targets = "Follow host switches from" connection = "Połączenie" slot = "Slot" receiver_slot_number = "Slot %{number}" diff --git a/crates/openlogi-ui/locales/pt-BR.toml b/crates/openlogi-ui/locales/pt-BR.toml index 44c6d372d..0ec83c5a0 100644 --- a/crates/openlogi-ui/locales/pt-BR.toml +++ b/crates/openlogi-ui/locales/pt-BR.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Deixe em branco para usar o nome do modelo. pointer_tuning = "Ajuste do ponteiro" device_details = "Detalhes do dispositivo" configuration = "Configuração" +host_switch_targets = "Follow host switches from" connection = "Conexão" slot = "Slot" receiver_slot_number = "Slot %{number}" diff --git a/crates/openlogi-ui/locales/pt-PT.toml b/crates/openlogi-ui/locales/pt-PT.toml index 6142fedda..baa4a4eca 100644 --- a/crates/openlogi-ui/locales/pt-PT.toml +++ b/crates/openlogi-ui/locales/pt-PT.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Deixe em branco para usar o nome do modelo. pointer_tuning = "Ajuste do ponteiro" device_details = "Detalhes do dispositivo" configuration = "Configuração" +host_switch_targets = "Follow host switches from" connection = "Ligação" slot = "Ranhura" receiver_slot_number = "Ranhura %{number}" diff --git a/crates/openlogi-ui/locales/ru.toml b/crates/openlogi-ui/locales/ru.toml index 09098438b..65cb347fd 100644 --- a/crates/openlogi-ui/locales/ru.toml +++ b/crates/openlogi-ui/locales/ru.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Оставьте пустым, чтобы pointer_tuning = "Настройка указателя" device_details = "Сведения об устройстве" configuration = "Конфигурация" +host_switch_targets = "Follow host switches from" connection = "Подключение" slot = "Слот" receiver_slot_number = "Слот %{number}" diff --git a/crates/openlogi-ui/locales/sv.toml b/crates/openlogi-ui/locales/sv.toml index 96ee142aa..da26a7553 100644 --- a/crates/openlogi-ui/locales/sv.toml +++ b/crates/openlogi-ui/locales/sv.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Lämna tomt för att använda modellnamnet. pointer_tuning = "Pekarjustering" device_details = "Enhetsinformation" configuration = "Konfiguration" +host_switch_targets = "Follow host switches from" connection = "Anslutning" slot = "Plats" receiver_slot_number = "Plats %{number}" diff --git a/crates/openlogi-ui/locales/tr.toml b/crates/openlogi-ui/locales/tr.toml index 841613915..21913a97f 100644 --- a/crates/openlogi-ui/locales/tr.toml +++ b/crates/openlogi-ui/locales/tr.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Model adını kullanmak için boş bırakı pointer_tuning = "İmleç ayarı" device_details = "Cihaz ayrıntıları" configuration = "Yapılandırma" +host_switch_targets = "Follow host switches from" connection = "Bağlantı" slot = "Yuva" receiver_slot_number = "Yuva %{number}" diff --git a/crates/openlogi-ui/locales/uk.toml b/crates/openlogi-ui/locales/uk.toml index 948aa8be0..393a194c8 100644 --- a/crates/openlogi-ui/locales/uk.toml +++ b/crates/openlogi-ui/locales/uk.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "Залиште порожнім, щоб в pointer_tuning = "Налаштування вказівника" device_details = "Відомості про пристрій" configuration = "Конфігурація" +host_switch_targets = "Follow host switches from" connection = "Підключення" slot = "Слот" receiver_slot_number = "Слот %{number}" diff --git a/crates/openlogi-ui/locales/zh-CN.toml b/crates/openlogi-ui/locales/zh-CN.toml index 5e09567be..762f59dfa 100644 --- a/crates/openlogi-ui/locales/zh-CN.toml +++ b/crates/openlogi-ui/locales/zh-CN.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "留空以使用型号名称。" pointer_tuning = "指针调校" device_details = "设备详情" configuration = "配置" +host_switch_targets = "Follow host switches from" connection = "连接" slot = "槽位" receiver_slot_number = "槽位 %{number}" diff --git a/crates/openlogi-ui/locales/zh-HK.toml b/crates/openlogi-ui/locales/zh-HK.toml index d7e59914e..1c4ecf7a1 100644 --- a/crates/openlogi-ui/locales/zh-HK.toml +++ b/crates/openlogi-ui/locales/zh-HK.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "留空以使用型號名稱。" pointer_tuning = "指標調校" device_details = "裝置詳情" configuration = "設定" +host_switch_targets = "Follow host switches from" connection = "連線" slot = "插槽" receiver_slot_number = "插槽 %{number}" diff --git a/crates/openlogi-ui/locales/zh-TW.toml b/crates/openlogi-ui/locales/zh-TW.toml index 388b40790..09755892d 100644 --- a/crates/openlogi-ui/locales/zh-TW.toml +++ b/crates/openlogi-ui/locales/zh-TW.toml @@ -98,6 +98,7 @@ leave_blank_to_use_the_model_name = "留白以使用型號名稱。" pointer_tuning = "指標調整" device_details = "裝置詳細資訊" configuration = "設定" +host_switch_targets = "Follow host switches from" connection = "連線" slot = "插槽" receiver_slot_number = "插槽 %{number}"