diff --git a/docs/config.md b/docs/config.md index e318da5e..1173a5f9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -425,6 +425,20 @@ You can use the `configure-connector` action to change this configuration at run See the specification for more details. +### Disabling Connectors of Closed Laptops + +If a laptop has a switch that is signaled when the laptop is closed, you can configure +the built-in connector to be disabled automatically: + +```toml +[[inputs]] +match.name = "" +on-lid-closed = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = false } } +on-lid-opened = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = true } } +``` + +See the specification for more details. + ### Configuring Input Devices You can configure input devices with the top-level `inputs` array. diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 8675d299..d00a274c 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -12,7 +12,7 @@ use { exec::Command, input::{ acceleration::AccelProfile, capability::Capability, FocusFollowsMouseMode, InputDevice, - Seat, + Seat, SwitchEvent, }, keyboard::{ mods::{Modifiers, RELEASE}, @@ -95,6 +95,7 @@ pub(crate) struct Client { on_new_drm_device: RefCell>>, on_del_drm_device: RefCell>>, on_idle: RefCell>, + on_switch_event: RefCell>>, bufs: RefCell>>, reload: Cell, read_interests: RefCell>, @@ -222,6 +223,7 @@ pub unsafe extern "C" fn init( on_new_drm_device: Default::default(), on_del_drm_device: Default::default(), on_idle: Default::default(), + on_switch_event: Default::default(), bufs: Default::default(), reload: Cell::new(false), read_interests: Default::default(), @@ -603,6 +605,16 @@ impl Client { *self.on_new_input_device.borrow_mut() = Some(cb(f)); } + pub fn on_switch_event( + &self, + input_device: InputDevice, + f: F, + ) { + self.on_switch_event + .borrow_mut() + .insert(input_device, cb(f)); + } + pub fn set_double_click_interval(&self, usec: u64) { self.send(&ClientMessage::SetDoubleClickIntervalUsec { usec }); } @@ -1258,7 +1270,9 @@ impl Client { run_cb("new input device", &handler, device); } } - ServerMessage::DelInputDevice { .. } => {} + ServerMessage::DelInputDevice { device } => { + self.on_switch_event.borrow_mut().remove(&device); + } ServerMessage::ConnectorConnect { device } => { let handler = self.on_connector_connected.borrow_mut().clone(); if let Some(handler) = handler { @@ -1332,6 +1346,17 @@ impl Client { } } } + ServerMessage::SwitchEvent { + seat, + input_device, + event, + } => { + let _ = seat; + let cb = self.on_switch_event.borrow().get(&input_device).cloned(); + if let Some(cb) = cb { + run_cb("switch event", &cb, event); + } + } } } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index f0bc6e91..16e57be1 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -2,7 +2,7 @@ use { crate::{ input::{ acceleration::AccelProfile, capability::Capability, FocusFollowsMouseMode, InputDevice, - Seat, + Seat, SwitchEvent, }, keyboard::{mods::Modifiers, syms::KeySym, Keymap}, logging::LogLevel, @@ -83,6 +83,11 @@ pub enum ServerMessage { effective_mods: Modifiers, sym: KeySym, }, + SwitchEvent { + seat: Seat, + input_device: InputDevice, + event: SwitchEvent, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index ace0f0fe..30edfb73 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -138,6 +138,11 @@ impl InputDevice { pub fn devnode(self) -> String { get!(String::new()).input_device_devnode(self) } + + /// Sets a callback that will be run if this device triggers a switch event. + pub fn on_switch_event(self, f: F) { + get!().on_switch_event(self, f) + } } /// A seat. @@ -479,3 +484,26 @@ pub fn set_double_click_distance(distance: i32) { pub fn disable_default_seat() { get!().disable_default_seat(); } + +/// An event generated by a switch. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum SwitchEvent { + /// The lid of the device (usually a laptop) has been opened. + /// + /// This is the default state. + LidOpened, + /// The lid of the device (usually a laptop) has been closed. + /// + /// If the device is already in this state when the device is discovered, a synthetic + /// event of this kind is generated. + LidClosed, + /// The device has been converted from tablet to laptop mode. + /// + /// This is the default state. + ConvertedToLaptop, + /// The device has been converted from laptop to tablet mode. + /// + /// If the device is already in this state when the device is discovered, a synthetic + /// event of this kind is generated. + ConvertedToTablet, +} diff --git a/release-notes.md b/release-notes.md index 2a4396d9..3d42166d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,7 @@ - Add support for wp-drm-lease-v1. - Focus-follows-mouse can now be disabled. - Add support for pointer-gestures-unstable-v1. +- Configs can now handle switch events (laptop lid closed/opened). # 1.1.0 (2024-04-22) diff --git a/src/backend.rs b/src/backend.rs index e688a402..f1ded7d8 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -8,7 +8,7 @@ use { libinput::consts::DeviceCapability, video::drm::{ConnectorType, DrmConnector, DrmError, DrmVersion}, }, - jay_config::video::GfxApi, + jay_config::{input::SwitchEvent, video::GfxApi}, std::{ any::Any, error::Error, @@ -312,6 +312,11 @@ pub enum InputEvent { time_usec: u64, cancelled: bool, }, + + SwitchEvent { + time_usec: u64, + event: SwitchEvent, + }, } pub enum DrmEvent { diff --git a/src/backends/metal/input.rs b/src/backends/metal/input.rs index c73bab8e..c64f683c 100644 --- a/src/backends/metal/input.rs +++ b/src/backends/metal/input.rs @@ -7,11 +7,14 @@ use { consts::{ LIBINPUT_BUTTON_STATE_PRESSED, LIBINPUT_KEY_STATE_PRESSED, LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, + LIBINPUT_SWITCH_LID, LIBINPUT_SWITCH_STATE_OFF, LIBINPUT_SWITCH_STATE_ON, + LIBINPUT_SWITCH_TABLET_MODE, }, event::LibInputEvent, }, utils::{bitflags::BitflagsExt, errorfmt::ErrorFmt}, }, + jay_config::input::SwitchEvent, std::rc::Rc, uapi::c, }; @@ -99,6 +102,7 @@ impl MetalBackend { c::LIBINPUT_EVENT_GESTURE_PINCH_END => self.handle_gesture_pinch_end(event), c::LIBINPUT_EVENT_GESTURE_HOLD_BEGIN => self.handle_gesture_hold_begin(event), c::LIBINPUT_EVENT_GESTURE_HOLD_END => self.handle_gesture_hold_end(event), + c::LIBINPUT_EVENT_SWITCH_TOGGLE => self.handle_switch_toggle(event), _ => {} } } @@ -297,4 +301,23 @@ impl MetalBackend { cancelled: event.cancelled(), }); } + + fn handle_switch_toggle(self: &Rc, event: LibInputEvent) { + let (event, dev) = unpack!(self, event, switch_event); + let switch_event = match (event.switch(), event.switch_state()) { + (LIBINPUT_SWITCH_LID, LIBINPUT_SWITCH_STATE_OFF) => SwitchEvent::LidOpened, + (LIBINPUT_SWITCH_LID, LIBINPUT_SWITCH_STATE_ON) => SwitchEvent::LidClosed, + (LIBINPUT_SWITCH_TABLET_MODE, LIBINPUT_SWITCH_STATE_OFF) => { + SwitchEvent::ConvertedToLaptop + } + (LIBINPUT_SWITCH_TABLET_MODE, LIBINPUT_SWITCH_STATE_ON) => { + SwitchEvent::ConvertedToTablet + } + _ => return, + }; + dev.event(InputEvent::SwitchEvent { + time_usec: event.time_usec(), + event: switch_event, + }); + } } diff --git a/src/cli/seat_test.rs b/src/cli/seat_test.rs index d948279d..7456ab37 100644 --- a/src/cli/seat_test.rs +++ b/src/cli/seat_test.rs @@ -8,7 +8,7 @@ use { jay_seat_events::{ Axis120, AxisFrame, AxisInverted, AxisPx, AxisSource, AxisStop, Button, HoldBegin, HoldEnd, Key, Modifiers, PinchBegin, PinchEnd, PinchUpdate, PointerAbs, PointerRel, - SwipeBegin, SwipeEnd, SwipeUpdate, + SwipeBegin, SwipeEnd, SwipeUpdate, SwitchEvent, }, }, }, @@ -324,6 +324,26 @@ async fn run(seat_test: Rc) { println!(); } }); + let st = seat_test.clone(); + SwitchEvent::handle(tc, se, (), move |_, ev| { + let event = match ev.event { + 0 => "lid opened", + 1 => "lid closed", + 2 => "converted to laptop", + 3 => "converted to tablet", + _ => "unknown event", + }; + if all || ev.seat == seat { + if all { + print!("Seat: {}, ", st.name(ev.seat)); + } + println!( + "Time: {:.4}, Device: {}, {event}", + time(ev.time_usec), + ev.input_device + ); + } + }); pending::<()>().await; } diff --git a/src/config.rs b/src/config.rs index 0305a4ad..22bc2600 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,7 +20,7 @@ use { ipc::{InitMessage, ServerFeature, ServerMessage, V1InitMessage}, ConfigEntry, VERSION, }, - input::{InputDevice, Seat}, + input::{InputDevice, Seat, SwitchEvent}, keyboard::{mods::Modifiers, syms::KeySym}, video::{Connector, DrmDevice}, }, @@ -144,6 +144,14 @@ impl ConfigProxy { pub fn idle(&self) { self.send(&ServerMessage::Idle); } + + pub fn switch_event(&self, seat: SeatId, input_device: InputDeviceId, event: SwitchEvent) { + self.send(&ServerMessage::SwitchEvent { + seat: Seat(seat.raw() as _), + input_device: InputDevice(input_device.raw() as _), + event, + }); + } } impl Drop for ConfigProxy { diff --git a/src/ifs/jay_seat_events.rs b/src/ifs/jay_seat_events.rs index 4d3fac6c..e9ddb35d 100644 --- a/src/ifs/jay_seat_events.rs +++ b/src/ifs/jay_seat_events.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::KeyState, + backend::{InputDeviceId, KeyState}, client::Client, fixed::Fixed, ifs::wl_seat::{wl_pointer::PendingScroll, SeatId}, @@ -220,6 +220,22 @@ impl JaySeatEvents { cancelled: cancelled as _, }); } + + pub fn send_switch_event( + &self, + seat: SeatId, + input_device: InputDeviceId, + time_usec: u64, + event: jay_config::input::SwitchEvent, + ) { + self.client.event(SwitchEvent { + self_id: self.id, + seat: seat.raw(), + time_usec, + input_device: input_device.raw(), + event: event as _, + }); + } } impl JaySeatEventsRequestHandler for JaySeatEvents { diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 9c6bdc12..0244838a 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::{ConnectorId, InputEvent, KeyState, AXIS_120}, + backend::{ConnectorId, InputDeviceId, InputEvent, KeyState, AXIS_120}, client::ClientId, config::InvokedShortcut, fixed::Fixed, @@ -37,9 +37,12 @@ use { xkbcommon::{KeyboardState, XkbState, XKB_KEY_DOWN, XKB_KEY_UP}, }, isnt::std_1::primitive::{IsntSlice2Ext, IsntSliceExt}, - jay_config::keyboard::{ - mods::{Modifiers, CAPS, NUM, RELEASE}, - syms::{KeySym, SYM_Escape}, + jay_config::{ + input::SwitchEvent, + keyboard::{ + mods::{Modifiers, CAPS, NUM, RELEASE}, + syms::{KeySym, SYM_Escape}, + }, }, smallvec::SmallVec, std::{cell::RefCell, collections::hash_map::Entry, rc::Rc}, @@ -200,7 +203,8 @@ impl WlSeatGlobal { | InputEvent::PinchUpdate { time_usec, .. } | InputEvent::PinchEnd { time_usec, .. } | InputEvent::HoldBegin { time_usec, .. } - | InputEvent::HoldEnd { time_usec, .. } => { + | InputEvent::HoldEnd { time_usec, .. } + | InputEvent::SwitchEvent { time_usec, .. } => { self.last_input_usec.set(time_usec); if self.idle_notifications.is_not_empty() { for (_, notification) in self.idle_notifications.lock().drain() { @@ -299,6 +303,9 @@ impl WlSeatGlobal { time_usec, cancelled, } => self.hold_end(time_usec, cancelled), + InputEvent::SwitchEvent { time_usec, event } => { + self.switch_event(dev.device.id(), time_usec, event) + } } } @@ -504,6 +511,15 @@ impl WlSeatGlobal { self.gesture_owner.hold_end(self, time_usec, cancelled) } + fn switch_event(self: &Rc, dev: InputDeviceId, time_usec: u64, event: SwitchEvent) { + self.state.for_each_seat_tester(|t| { + t.send_switch_event(self.id, dev, time_usec, event); + }); + if let Some(config) = self.state.config.get() { + config.switch_event(self.id, dev, event); + } + } + pub(super) fn key_event( self: &Rc, time_usec: u64, diff --git a/src/it/test_config.rs b/src/it/test_config.rs index c33bfac6..9ee4ce6a 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -120,6 +120,7 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { ServerMessage::DevicesEnumerated => {} ServerMessage::InterestReady { .. } => {} ServerMessage::Features { .. } => {} + ServerMessage::SwitchEvent { .. } => {} } } diff --git a/src/libinput/event.rs b/src/libinput/event.rs index af5b182c..60038fb6 100644 --- a/src/libinput/event.rs +++ b/src/libinput/event.rs @@ -1,6 +1,6 @@ use { crate::libinput::{ - consts::{ButtonState, EventType, KeyState, PointerAxis}, + consts::{ButtonState, EventType, KeyState, PointerAxis, Switch, SwitchState}, device::LibInputDevice, sys::{ libinput_event, libinput_event_destroy, libinput_event_gesture, @@ -10,14 +10,17 @@ use { libinput_event_gesture_get_finger_count, libinput_event_gesture_get_scale, libinput_event_gesture_get_time_usec, libinput_event_get_device, libinput_event_get_gesture_event, libinput_event_get_keyboard_event, - libinput_event_get_pointer_event, libinput_event_get_type, libinput_event_keyboard, - libinput_event_keyboard_get_key, libinput_event_keyboard_get_key_state, - libinput_event_keyboard_get_time_usec, libinput_event_pointer, - libinput_event_pointer_get_button, libinput_event_pointer_get_button_state, - libinput_event_pointer_get_dx, libinput_event_pointer_get_dx_unaccelerated, - libinput_event_pointer_get_dy, libinput_event_pointer_get_dy_unaccelerated, - libinput_event_pointer_get_scroll_value, libinput_event_pointer_get_scroll_value_v120, - libinput_event_pointer_get_time_usec, libinput_event_pointer_has_axis, + libinput_event_get_pointer_event, libinput_event_get_switch_event, + libinput_event_get_type, libinput_event_keyboard, libinput_event_keyboard_get_key, + libinput_event_keyboard_get_key_state, libinput_event_keyboard_get_time_usec, + libinput_event_pointer, libinput_event_pointer_get_button, + libinput_event_pointer_get_button_state, libinput_event_pointer_get_dx, + libinput_event_pointer_get_dx_unaccelerated, libinput_event_pointer_get_dy, + libinput_event_pointer_get_dy_unaccelerated, libinput_event_pointer_get_scroll_value, + libinput_event_pointer_get_scroll_value_v120, libinput_event_pointer_get_time_usec, + libinput_event_pointer_has_axis, libinput_event_switch, + libinput_event_switch_get_switch, libinput_event_switch_get_switch_state, + libinput_event_switch_get_time_usec, }, }, std::marker::PhantomData, @@ -43,6 +46,11 @@ pub struct LibInputEventGesture<'a> { pub(super) _phantom: PhantomData<&'a ()>, } +pub struct LibInputEventSwitch<'a> { + pub(super) event: *mut libinput_event_switch, + pub(super) _phantom: PhantomData<&'a ()>, +} + impl<'a> Drop for LibInputEvent<'a> { fn drop(&mut self) { unsafe { @@ -98,6 +106,18 @@ impl<'a> LibInputEvent<'a> { }) } } + + pub fn switch_event(&self) -> Option { + let res = unsafe { libinput_event_get_switch_event(self.event) }; + if res.is_null() { + None + } else { + Some(LibInputEventSwitch { + event: res, + _phantom: Default::default(), + }) + } + } } impl<'a> LibInputEventKeyboard<'a> { @@ -194,3 +214,17 @@ impl<'a> LibInputEventGesture<'a> { unsafe { libinput_event_gesture_get_angle_delta(self.event) } } } + +impl<'a> LibInputEventSwitch<'a> { + pub fn time_usec(&self) -> u64 { + unsafe { libinput_event_switch_get_time_usec(self.event) } + } + + pub fn switch(&self) -> Switch { + unsafe { Switch(libinput_event_switch_get_switch(self.event)) } + } + + pub fn switch_state(&self) -> SwitchState { + unsafe { SwitchState(libinput_event_switch_get_switch_state(self.event)) } + } +} diff --git a/src/libinput/sys.rs b/src/libinput/sys.rs index 89861d4c..31d860f2 100644 --- a/src/libinput/sys.rs +++ b/src/libinput/sys.rs @@ -16,6 +16,8 @@ pub struct libinput_event_keyboard(u8); pub struct libinput_event_pointer(u8); #[repr(transparent)] pub struct libinput_event_gesture(u8); +#[repr(transparent)] +pub struct libinput_event_switch(u8); #[link(name = "input")] extern "C" { @@ -155,6 +157,15 @@ extern "C" { pub fn libinput_event_gesture_get_dy_unaccelerated(event: *mut libinput_event_gesture) -> f64; pub fn libinput_event_gesture_get_scale(event: *mut libinput_event_gesture) -> f64; pub fn libinput_event_gesture_get_angle_delta(event: *mut libinput_event_gesture) -> f64; + + pub fn libinput_event_get_switch_event( + event: *mut libinput_event, + ) -> *mut libinput_event_switch; + pub fn libinput_event_switch_get_switch(event: *mut libinput_event_switch) -> libinput_switch; + pub fn libinput_event_switch_get_switch_state( + event: *mut libinput_event_switch, + ) -> libinput_switch_state; + pub fn libinput_event_switch_get_time_usec(event: *mut libinput_event_switch) -> u64; } #[repr(C)] diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index e7436ba6..44e7d9cb 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -15,8 +15,9 @@ use { }, toml::{self}, }, + ahash::AHashMap, jay_config::{ - input::acceleration::AccelProfile, + input::{acceleration::AccelProfile, SwitchEvent}, keyboard::{mods::Modifiers, Keymap, ModifiedKeySym}, logging::LogLevel, status::MessageFormat, @@ -244,6 +245,7 @@ pub struct Input { pub px_per_wheel_scroll: Option, pub transform_matrix: Option<[[f64; 2]; 2]>, pub keymap: Option, + pub switch_actions: AHashMap, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/input.rs b/toml-config/src/config/parsers/input.rs index ea4edc53..83ee3bed 100644 --- a/toml-config/src/config/parsers/input.rs +++ b/toml-config/src/config/parsers/input.rs @@ -5,6 +5,7 @@ use { extractor::{bol, fltorint, opt, recover, str, val, Extractor, ExtractorError}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ + action::ActionParser, input_match::{InputMatchParser, InputMatchParserError}, keymap::KeymapParser, }, @@ -15,8 +16,12 @@ use { toml_value::Value, }, }, + ahash::AHashMap, indexmap::IndexMap, - jay_config::input::acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT}, + jay_config::input::{ + acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT}, + SwitchEvent, + }, thiserror::Error, }; @@ -65,7 +70,14 @@ impl<'a> Parser for InputParser<'a> { natural_scrolling, px_per_wheel_scroll, ), - (transform_matrix, keymap), + ( + transform_matrix, + keymap, + on_lid_opened_val, + on_lid_closed_val, + on_converted_to_laptop_val, + on_converted_to_tablet_val, + ), ) = ext.extract(( ( opt(str("tag")), @@ -79,7 +91,14 @@ impl<'a> Parser for InputParser<'a> { recover(opt(bol("natural-scrolling"))), recover(opt(fltorint("px-per-wheel-scroll"))), ), - (recover(opt(val("transform-matrix"))), opt(val("keymap"))), + ( + recover(opt(val("transform-matrix"))), + opt(val("keymap")), + opt(val("on-lid-opened")), + opt(val("on-lid-closed")), + opt(val("on-converted-to-laptop")), + opt(val("on-converted-to-tablet")), + ), ))?; let accel_profile = match accel_profile { None => None, @@ -125,6 +144,38 @@ impl<'a> Parser for InputParser<'a> { } }, }; + let mut switch_actions = AHashMap::new(); + let mut parse_action = |val: Option>, name, event| { + if let Some(val) = val { + if !self.tag_ok { + log::warn!( + "{name} has no effect in this position: {}", + self.cx.error3(val.span) + ); + return; + } + match val.parse(&mut ActionParser(self.cx)) { + Ok(a) => { + switch_actions.insert(event, a); + } + Err(e) => { + log::warn!("Could not parse {name} action: {}", self.cx.error(e)); + } + } + } + }; + parse_action(on_lid_opened_val, "on-lid-opened", SwitchEvent::LidOpened); + parse_action(on_lid_closed_val, "on-lid-closed", SwitchEvent::LidClosed); + parse_action( + on_converted_to_laptop_val, + "on-converted-to-laptop", + SwitchEvent::ConvertedToLaptop, + ); + parse_action( + on_converted_to_tablet_val, + "on-converted-to-tablet", + SwitchEvent::ConvertedToTablet, + ); Ok(Input { tag: tag.despan_into(), match_: match_val.parse_map(&mut InputMatchParser(self.cx))?, @@ -138,6 +189,7 @@ impl<'a> Parser for InputParser<'a> { px_per_wheel_scroll: px_per_wheel_scroll.despan(), transform_matrix, keymap, + switch_actions, }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 52b3f6a9..73ce2329 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -16,7 +16,8 @@ use { exec::{set_env, unset_env, Command}, get_workspace, input::{ - get_seat, input_devices, on_new_input_device, FocusFollowsMouseMode, InputDevice, Seat, + capability::CAP_SWITCH, get_seat, input_devices, on_new_input_device, + FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, }, is_reload, keyboard::{Keymap, ModifiedKeySym}, @@ -559,6 +560,8 @@ impl Drop for State { } } +type SwitchActions = Vec<(InputMatch, AHashMap>)>; + impl State { fn unbind_all(&self) { let mut binds = self.persistent.binds.borrow_mut(); @@ -681,6 +684,23 @@ impl State { set_font(font); } } + + fn handle_switch_device(self: &Rc, dev: InputDevice, actions: &Rc) { + if !dev.has_capability(CAP_SWITCH) { + return; + } + let state = self.clone(); + let actions = actions.clone(); + dev.on_switch_event(move |ev| { + for (match_, actions) in &*actions { + if match_.matches(dev, &state) { + if let Some(action) = actions.get(&ev) { + action(); + } + } + } + }); + } } #[derive(Eq, PartialEq, Hash)] @@ -700,7 +720,7 @@ struct PersistentState { fn load_config(initial_load: bool, persistent: &Rc) { let mut path = PathBuf::from(config_dir()); path.push("config.toml"); - let config = match std::fs::read(&path) { + let mut config = match std::fs::read(&path) { Ok(input) => match parse_config(&input, |e| { log::warn!("Error while parsing {}: {}", path.display(), Report::new(e)) }) { @@ -768,6 +788,17 @@ fn load_config(initial_load: bool, persistent: &Rc) { keymaps, }); state.set_status(&config.status); + let mut switch_actions = vec![]; + for input in &mut config.inputs { + let mut actions = AHashMap::new(); + for (event, action) in input.switch_actions.drain() { + actions.insert(event, action.into_fn(&state)); + } + if actions.len() > 0 { + switch_actions.push((input.match_.clone(), actions)); + } + } + let switch_actions = Rc::new(switch_actions); match config.on_graphics_initialized { None => on_graphics_initialized(|| ()), Some(a) => on_graphics_initialized(a.into_fn(&state)), @@ -863,14 +894,19 @@ fn load_config(initial_load: bool, persistent: &Rc) { }); on_new_input_device({ let state = state.clone(); + let switch_actions = switch_actions.clone(); move |c| { for input in &config.inputs { if input.match_.matches(c, &state) { input.apply(c, &state); } } + state.handle_switch_device(c, &switch_actions); } }); + for c in jay_config::input::input_devices() { + state.handle_switch_device(c, &switch_actions); + } persistent .seat .set_focus_follows_mouse_mode(match config.focus_follows_mouse { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 5b5ac4da..86e68430 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -825,6 +825,22 @@ "keymap": { "description": "The keymap to use for this device.\n\nThis overrides the global keymap. The keymap becomes active when a key is pressed.\n\n- Example:\n\n ```toml\n [[inputs]]\n match.name = \"ZSA Technology Labs Inc ErgoDox EZ\"\n keymap.name = \"external\"\n ```\n", "$ref": "#/$defs/Keymap" + }, + "on-lid-closed": { + "description": "An action to execute when the laptop lid is closed.\n\nThis should only be used in the top-level inputs array.\n\n- Example:\n\n ```toml\n [[inputs]]\n match.name = \"\"\n on-lid-closed = { type = \"configure-connector\", connector = { match.name = \"eDP-1\", enabled = false } }\n on-lid-opened = { type = \"configure-connector\", connector = { match.name = \"eDP-1\", enabled = true } }\n ```\n", + "$ref": "#/$defs/Action" + }, + "on-lid-opened": { + "description": "An action to execute when the laptop lid is opened.\n\nThis should only be used in the top-level inputs array.\n\n- Example:\n\n ```toml\n [[inputs]]\n match.name = \"\"\n on-lid-closed = { type = \"configure-connector\", connector = { match.name = \"eDP-1\", enabled = false } }\n on-lid-opened = { type = \"configure-connector\", connector = { match.name = \"eDP-1\", enabled = true } }\n ```\n", + "$ref": "#/$defs/Action" + }, + "on-converted-to-laptop": { + "description": "An action to execute when the convertible device is converted to a laptop.\n\nThis should only be used in the top-level inputs array.\n", + "$ref": "#/$defs/Action" + }, + "on-converted-to-tablet": { + "description": "An action to execute when the convertible device is converted to a tablet.\n\nThis should only be used in the top-level inputs array.\n", + "$ref": "#/$defs/Action" } }, "required": [ diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 0c950fac..d50e2c1b 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1635,6 +1635,56 @@ The table has the following fields: The value of this field should be a [Keymap](#types-Keymap). +- `on-lid-closed` (optional): + + An action to execute when the laptop lid is closed. + + This should only be used in the top-level inputs array. + + - Example: + + ```toml + [[inputs]] + match.name = "" + on-lid-closed = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = false } } + on-lid-opened = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = true } } + ``` + + The value of this field should be a [Action](#types-Action). + +- `on-lid-opened` (optional): + + An action to execute when the laptop lid is opened. + + This should only be used in the top-level inputs array. + + - Example: + + ```toml + [[inputs]] + match.name = "" + on-lid-closed = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = false } } + on-lid-opened = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = true } } + ``` + + The value of this field should be a [Action](#types-Action). + +- `on-converted-to-laptop` (optional): + + An action to execute when the convertible device is converted to a laptop. + + This should only be used in the top-level inputs array. + + The value of this field should be a [Action](#types-Action). + +- `on-converted-to-tablet` (optional): + + An action to execute when the convertible device is converted to a tablet. + + This should only be used in the top-level inputs array. + + The value of this field should be a [Action](#types-Action). + ### `InputMatch` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index c445ef6e..58456a23 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1239,6 +1239,52 @@ Input: match.name = "ZSA Technology Labs Inc ErgoDox EZ" keymap.name = "external" ``` + on-lid-closed: + ref: Action + required: false + description: | + An action to execute when the laptop lid is closed. + + This should only be used in the top-level inputs array. + + - Example: + + ```toml + [[inputs]] + match.name = "" + on-lid-closed = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = false } } + on-lid-opened = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = true } } + ``` + on-lid-opened: + ref: Action + required: false + description: | + An action to execute when the laptop lid is opened. + + This should only be used in the top-level inputs array. + + - Example: + + ```toml + [[inputs]] + match.name = "" + on-lid-closed = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = false } } + on-lid-opened = { type = "configure-connector", connector = { match.name = "eDP-1", enabled = true } } + ``` + on-converted-to-laptop: + ref: Action + required: false + description: | + An action to execute when the convertible device is converted to a laptop. + + This should only be used in the top-level inputs array. + on-converted-to-tablet: + ref: Action + required: false + description: | + An action to execute when the convertible device is converted to a tablet. + + This should only be used in the top-level inputs array. AccelProfile: diff --git a/wire/jay_seat_events.txt b/wire/jay_seat_events.txt index b2d1c901..06bbdbb3 100644 --- a/wire/jay_seat_events.txt +++ b/wire/jay_seat_events.txt @@ -125,3 +125,10 @@ event hold_end { time_usec: pod(u64), cancelled: i32, } + +event switch_event { + seat: u32, + time_usec: pod(u64), + input_device: u32, + event: u32, +}