feat: configure keyboard and touchpad input

This commit is contained in:
Samuel Onoja committed 2026-09-25 02:50:19 +01:00
1 parent 94a83c83bb
commit 2f83c58040
5 files changed
+233 -5

No files matched your search

+20
View File
@@ -55,6 +55,26 @@ to center the focused column, `Super+[` to consume the focused window into an
adjacent column, and `Super+]` to expel it. `Super+Ctrl+H/J/K/L` resizes in the adjacent column, and `Super+]` to expel it. `Super+Ctrl+H/J/K/L` resizes in the
corresponding direction. corresponding direction.
Keyboard and direct-session touchpad settings are applied at startup:
```toml
[input]
xkb_layout = "us"
xkb_variant = ""
xkb_options = []
repeat_rate = 25
repeat_delay_ms = 600
[input.touchpad]
tap = true
natural_scroll = true
disable_while_typing = true
```
Touchpad settings apply to libinput devices in the direct DRM session. In the
nested backend, the host compositor remains responsible for physical touchpad
configuration.
Text-input-v3 is available to applications by default. Input-method-v2 is Text-input-v3 is available to applications by default. Input-method-v2 is
hidden unless the session explicitly opts in, because an input method can hidden unless the session explicitly opts in, because an input method can
request privileged keyboard access. Enable it only when launching a trusted request privileged keyboard access. Enable it only when launching a trusted
+37 -3
View File
@@ -14,6 +14,7 @@ use smithay::{
}, },
drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface}, drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface},
egl::{EGLContext, EGLDisplay}, egl::{EGLContext, EGLDisplay},
input::InputEvent,
libinput::{LibinputInputBackend, LibinputSessionInterface}, libinput::{LibinputInputBackend, LibinputSessionInterface},
renderer::{ renderer::{
Bind, Frame, ImportDma, ImportMemWl, Renderer, damage::OutputDamageTracker, Bind, Frame, ImportDma, ImportMemWl, Renderer, damage::OutputDamageTracker,
@@ -29,7 +30,7 @@ use smithay::{
drm::control::{ drm::control::{
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property, Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
}, },
input::Libinput, input::{Device as LibinputDevice, Libinput},
rustix::fs::OFlags, rustix::fs::OFlags,
wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind, wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind,
wayland_server::backend::GlobalId, wayland_server::backend::GlobalId,
@@ -108,8 +109,9 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
event_loop event_loop
.handle() .handle()
.insert_source(libinput_backend, |event, _, state| { .insert_source(libinput_backend, |event, _, state| match event {
state.process_input_event(event); InputEvent::DeviceAdded { mut device } => configure_libinput_device(state, &mut device),
event => state.process_input_event(event),
})?; })?;
event_loop event_loop
.handle() .handle()
@@ -176,6 +178,38 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
Ok(()) Ok(())
} }
fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
let settings = state.input_settings.touchpad;
if device.config_tap_finger_count() > 0
&& let Err(error) = device.config_tap_set_enabled(settings.tap)
{
tracing::warn!(
device = device.name(),
?error,
"failed to configure tap-to-click"
);
}
if device.config_scroll_has_natural_scroll()
&& let Err(error) = device.config_scroll_set_natural_scroll_enabled(settings.natural_scroll)
{
tracing::warn!(
device = device.name(),
?error,
"failed to configure natural scrolling"
);
}
if device.config_dwt_is_available()
&& let Err(error) = device.config_dwt_set_enabled(settings.disable_while_typing)
{
tracing::warn!(
device = device.name(),
?error,
"failed to configure disable-while-typing"
);
}
}
fn open_primary_device( fn open_primary_device(
event_loop: &mut EventLoop<Ferese>, event_loop: &mut EventLoop<Ferese>,
state: &mut Ferese, state: &mut Ferese,
+157 -1
View File
@@ -17,9 +17,77 @@ pub struct Config {
#[serde(default)] #[serde(default)]
layout: LayoutConfig, layout: LayoutConfig,
#[serde(default)] #[serde(default)]
input: InputConfig,
#[serde(default)]
scrolling: ScrollingConfig, scrolling: ScrollingConfig,
} }
#[derive(Clone, Debug, PartialEq)]
pub struct InputSettings {
pub xkb_layout: String,
pub xkb_variant: String,
pub xkb_options: Vec<String>,
pub repeat_rate: i32,
pub repeat_delay_ms: i32,
pub touchpad: TouchpadSettings,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TouchpadSettings {
pub tap: bool,
pub natural_scroll: bool,
pub disable_while_typing: bool,
}
#[derive(Debug, Deserialize)]
struct InputConfig {
#[serde(default = "default_xkb_layout")]
xkb_layout: String,
#[serde(default)]
xkb_variant: String,
#[serde(default)]
xkb_options: Vec<String>,
#[serde(default = "default_repeat_rate")]
repeat_rate: i32,
#[serde(default = "default_repeat_delay")]
repeat_delay_ms: i32,
#[serde(default)]
touchpad: TouchpadConfig,
}
impl Default for InputConfig {
fn default() -> Self {
Self {
xkb_layout: default_xkb_layout(),
xkb_variant: String::new(),
xkb_options: Vec::new(),
repeat_rate: default_repeat_rate(),
repeat_delay_ms: default_repeat_delay(),
touchpad: TouchpadConfig::default(),
}
}
}
#[derive(Debug, Deserialize)]
struct TouchpadConfig {
#[serde(default = "enabled_by_default")]
tap: bool,
#[serde(default = "enabled_by_default")]
natural_scroll: bool,
#[serde(default = "enabled_by_default")]
disable_while_typing: bool,
}
impl Default for TouchpadConfig {
fn default() -> Self {
Self {
tap: true,
natural_scroll: true,
disable_while_typing: true,
}
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct AnimationsConfig { struct AnimationsConfig {
#[serde(default = "enabled_by_default")] #[serde(default = "enabled_by_default")]
@@ -159,6 +227,10 @@ pub enum ConfigError {
field: &'static str, field: &'static str,
value: f64, value: f64,
}, },
InvalidInputValue {
field: &'static str,
value: String,
},
} }
impl fmt::Display for ConfigError { impl fmt::Display for ConfigError {
@@ -180,6 +252,9 @@ impl fmt::Display for ConfigError {
Self::InvalidLayoutValue { field, value } => { Self::InvalidLayoutValue { field, value } => {
write!(formatter, "invalid layout.{field} value {value}") write!(formatter, "invalid layout.{field} value {value}")
} }
Self::InvalidInputValue { field, value } => {
write!(formatter, "invalid input.{field} value {value}")
}
} }
} }
} }
@@ -191,7 +266,8 @@ impl Error for ConfigError {
Self::Parse { source, .. } => Some(source), Self::Parse { source, .. } => Some(source),
Self::InvalidColumnWidth { .. } Self::InvalidColumnWidth { .. }
| Self::InvalidAnimationValue { .. } | Self::InvalidAnimationValue { .. }
| Self::InvalidLayoutValue { .. } => None, | Self::InvalidLayoutValue { .. }
| Self::InvalidInputValue { .. } => None,
} }
} }
} }
@@ -266,6 +342,40 @@ impl Config {
} }
} }
pub fn input_settings(&self) -> Result<InputSettings, ConfigError> {
if self.input.xkb_layout.trim().is_empty() {
return Err(ConfigError::InvalidInputValue {
field: "xkb_layout",
value: self.input.xkb_layout.clone(),
});
}
if self.input.repeat_rate <= 0 {
return Err(ConfigError::InvalidInputValue {
field: "repeat_rate",
value: self.input.repeat_rate.to_string(),
});
}
if self.input.repeat_delay_ms < 0 {
return Err(ConfigError::InvalidInputValue {
field: "repeat_delay_ms",
value: self.input.repeat_delay_ms.to_string(),
});
}
Ok(InputSettings {
xkb_layout: self.input.xkb_layout.clone(),
xkb_variant: self.input.xkb_variant.clone(),
xkb_options: self.input.xkb_options.clone(),
repeat_rate: self.input.repeat_rate,
repeat_delay_ms: self.input.repeat_delay_ms,
touchpad: TouchpadSettings {
tap: self.input.touchpad.tap,
natural_scroll: self.input.touchpad.natural_scroll,
disable_while_typing: self.input.touchpad.disable_while_typing,
},
})
}
pub fn animations_enabled(&self) -> bool { pub fn animations_enabled(&self) -> bool {
self.animations.enabled && !self.animations.reduced_motion self.animations.enabled && !self.animations.reduced_motion
} }
@@ -343,6 +453,18 @@ const fn default_outer_gap() -> f64 {
10.0 10.0
} }
fn default_xkb_layout() -> String {
"us".to_owned()
}
const fn default_repeat_rate() -> i32 {
25
}
const fn default_repeat_delay() -> i32 {
600
}
const fn default_animation_speed() -> f64 { const fn default_animation_speed() -> f64 {
1.0 1.0
} }
@@ -519,6 +641,40 @@ mod tests {
assert!(invalid.gap_config().is_err()); assert!(invalid.gap_config().is_err());
} }
#[test]
fn parses_input_and_touchpad_settings() {
let config = parse(
"[input]\nxkb_layout = \"us,de\"\nxkb_variant = \",nodeadkeys\"\nxkb_options = [\"grp:alt_shift_toggle\"]\nrepeat_rate = 30\nrepeat_delay_ms = 450\n\n[input.touchpad]\ntap = false\nnatural_scroll = false\ndisable_while_typing = true",
);
assert_eq!(
config.input_settings().unwrap(),
InputSettings {
xkb_layout: "us,de".to_owned(),
xkb_variant: ",nodeadkeys".to_owned(),
xkb_options: vec!["grp:alt_shift_toggle".to_owned()],
repeat_rate: 30,
repeat_delay_ms: 450,
touchpad: TouchpadSettings {
tap: false,
natural_scroll: false,
disable_while_typing: true,
},
}
);
}
#[test]
fn rejects_invalid_input_settings() {
let layout = parse("[input]\nxkb_layout = \"\"");
let rate = parse("[input]\nrepeat_rate = 0");
let delay = parse("[input]\nrepeat_delay_ms = -1");
assert!(layout.input_settings().is_err());
assert!(rate.input_settings().is_err());
assert!(delay.input_settings().is_err());
}
#[test] #[test]
fn derives_critical_viewport_damping() { fn derives_critical_viewport_damping() {
let config = parse(""); let config = parse("");
+1
View File
@@ -29,6 +29,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let runtime = RuntimeConfig { let runtime = RuntimeConfig {
layout_mode: config.layout_mode(), layout_mode: config.layout_mode(),
gap_config: config.gap_config()?, gap_config: config.gap_config()?,
input_settings: config.input_settings()?,
default_column_width: config.default_column_width()?, default_column_width: config.default_column_width()?,
scrolling_focus_strategy: config.scrolling_focus_strategy(), scrolling_focus_strategy: config.scrolling_focus_strategy(),
column_width_presets: config.width_presets()?, column_width_presets: config.width_presets()?,
+18 -1
View File
@@ -28,6 +28,7 @@ use smithay::{
}, },
input::{ input::{
Seat, SeatState, Seat, SeatState,
keyboard::XkbConfig,
pointer::{CursorIcon, CursorImageStatus}, pointer::{CursorIcon, CursorImageStatus},
}, },
output::Output, output::Output,
@@ -74,6 +75,8 @@ use smithay::{
}, },
}; };
use crate::config::InputSettings;
pub struct Ferese { pub struct Ferese {
pub start_time: Instant, pub start_time: Instant,
pub socket_name: OsString, pub socket_name: OsString,
@@ -91,6 +94,7 @@ pub struct Ferese {
pub focused_window: Option<WindowId>, pub focused_window: Option<WindowId>,
column_width_presets: Vec<ColumnWidth>, column_width_presets: Vec<ColumnWidth>,
gap_config: GapConfig, gap_config: GapConfig,
pub(crate) input_settings: InputSettings,
animations_enabled: bool, animations_enabled: bool,
animation_speed: f64, animation_speed: f64,
spring_config: SpringConfig, spring_config: SpringConfig,
@@ -139,6 +143,7 @@ pub struct Ferese {
pub struct RuntimeConfig { pub struct RuntimeConfig {
pub layout_mode: LayoutMode, pub layout_mode: LayoutMode,
pub gap_config: GapConfig, pub gap_config: GapConfig,
pub input_settings: InputSettings,
pub default_column_width: ColumnWidth, pub default_column_width: ColumnWidth,
pub scrolling_focus_strategy: ViewportFocusStrategy, pub scrolling_focus_strategy: ViewportFocusStrategy,
pub column_width_presets: Vec<ColumnWidth>, pub column_width_presets: Vec<ColumnWidth>,
@@ -335,7 +340,18 @@ impl Ferese {
let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::<Self>(&display_handle); let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::<Self>(&display_handle);
let mut seat_state = SeatState::new(); let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit"); let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit");
seat.add_keyboard(Default::default(), 200, 25)?; let xkb_options = (!config.input_settings.xkb_options.is_empty())
.then(|| config.input_settings.xkb_options.join(","));
seat.add_keyboard(
XkbConfig {
layout: &config.input_settings.xkb_layout,
variant: &config.input_settings.xkb_variant,
options: xkb_options,
..XkbConfig::default()
},
config.input_settings.repeat_delay_ms,
config.input_settings.repeat_rate,
)?;
seat.add_pointer(); seat.add_pointer();
seat.add_touch(); seat.add_touch();
let socket_name = Self::init_wayland_listener(display, event_loop)?; let socket_name = Self::init_wayland_listener(display, event_loop)?;
@@ -366,6 +382,7 @@ impl Ferese {
focused_window: None, focused_window: None,
column_width_presets: config.column_width_presets, column_width_presets: config.column_width_presets,
gap_config: config.gap_config, gap_config: config.gap_config,
input_settings: config.input_settings,
animations_enabled: config.animations_enabled, animations_enabled: config.animations_enabled,
animation_speed: config.animation_speed, animation_speed: config.animation_speed,
spring_config: config.spring_config, spring_config: config.spring_config,