Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/openlogi-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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<String>) {
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).
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions crates/openlogi-core/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
Expand All @@ -155,6 +167,8 @@ impl Capabilities {
haptic_feedback: ids.contains(&0x19b0),
haptic_panel: false,
dpi_gestures: false,
host_switch_target: has(&CHANGE_HOST),
Comment on lines 158 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 HostsInfo falsely enables switching

A device is marked as a host-switch target when it exposes either 0x1814 or 0x1815, but the runtime requires 0x1814 (ChangeHost) to perform the switch. A pointing device that exposes only 0x1815 (HostsInfo) will therefore appear in the GUI and can be saved as a target, but every attempted host change will fail as unsupported.

Knowledge Base Used: Device integration stack

Fix in Codex Fix in Claude Code

host_switch_source: false,
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -482,6 +498,8 @@ mod tests {
haptic_feedback: false,
haptic_panel: false,
dpi_gestures: false,
host_switch_target: false,
host_switch_source: false,
}),
}],
}
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-core/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
50 changes: 48 additions & 2 deletions crates/openlogi-desktop/src/app/detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -635,13 +635,59 @@ fn light_tab(
/// Device tab: device details and configuration cards stacked.
fn device_tab(cx: &mut Context<AppView>) -> 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<HostSwitchCandidate>) -> 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,
)
}

Expand Down
6 changes: 6 additions & 0 deletions crates/openlogi-desktop/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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!(
Expand All @@ -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));
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-desktop/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -64,6 +65,7 @@ mod device_runtime;
mod device_store;
mod devices;
mod dpi;
mod host_switch;
mod inventory;
mod light;
mod lighting;
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-desktop/src/state/devices/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading