Files
feresaria/shell/ferese-settings/src/schema.rs
T
Samuel Onoja ad00e55fe4 Add mouse window controls and finer display scaling (#63)
Add configurable mouse and wheel bindings, window move and resize, tab drops, cross-output dragging, and device settings. Adjust display scaling in 0.05 increments.

Preserve opposite-edge positioning during scrolling resizes and show rows for edge drops into tabbed columns.
2026-10-25 14:14:35 +01:00

916 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Page {
Appearance,
Accessibility,
Wallpaper,
Bar,
Notifications,
LockScreen,
Windows,
Motion,
Keyboard,
Shortcuts,
Startup,
Displays,
Connections,
}
impl Page {
pub const NAVIGATION: [(&'static str, &'static [Self]); 3] = [
("Desktop", &[Self::Appearance, Self::Wallpaper, Self::Bar]),
(
"Interaction",
&[Self::Windows, Self::Keyboard, Self::Accessibility],
),
(
"System",
&[
Self::Displays,
Self::Connections,
Self::Notifications,
Self::LockScreen,
Self::Startup,
],
),
];
/// Subpages share one entry in navigation and search.
pub fn navigation_page(self) -> Self {
match self {
Self::Motion => Self::Windows,
Self::Shortcuts => Self::Keyboard,
_ => self,
}
}
pub fn subpages(self) -> &'static [(Self, &'static str)] {
match self.navigation_page() {
Self::Windows => &[(Self::Windows, "Windows"), (Self::Motion, "Motion")],
Self::Keyboard => &[
(Self::Keyboard, "Keyboard & mouse"),
(Self::Shortcuts, "Shortcuts"),
],
_ => &[],
}
}
pub fn search_destination(self, query: &str) -> Self {
if !self.matches_own(query) {
for &(page, _) in self.subpages() {
if page.matches_own(query) {
return page;
}
}
}
self
}
#[cfg(test)]
pub const ALL: [Self; 13] = [
Self::Appearance,
Self::Accessibility,
Self::Wallpaper,
Self::Bar,
Self::Notifications,
Self::LockScreen,
Self::Windows,
Self::Motion,
Self::Keyboard,
Self::Shortcuts,
Self::Startup,
Self::Displays,
Self::Connections,
];
pub fn title(self) -> &'static str {
match self {
Self::Appearance => "Appearance",
Self::Accessibility => "Accessibility",
Self::Wallpaper => "Wallpaper",
Self::Bar => "Panel & Shell",
Self::Notifications => "Notifications",
Self::LockScreen => "Lock screen",
Self::Windows => "Windows",
Self::Motion => "Motion",
Self::Keyboard => "Keyboard & mouse",
Self::Shortcuts => "Shortcuts",
Self::Startup => "Login items",
Self::Displays => "Displays",
Self::Connections => "Connections",
}
}
pub fn subtitle(self) -> &'static str {
match self {
Self::Appearance => "A desktop that feels like yours.",
Self::Accessibility => "Make the desktop easier to see and use.",
Self::Wallpaper => "Set the scene for your workspace.",
Self::Bar => "Arrange your panel and change the shell's appearance.",
Self::Notifications => "Stay informed on your terms.",
Self::LockScreen => "Your desktop, safely put away.",
Self::Windows => "Window layouts, spacing and focus effects.",
Self::Motion => "Animations and transition speed.",
Self::Keyboard => "Keyboard layouts, pointer behavior and touchpad gestures.",
Self::Shortcuts => "Keyboard shortcuts and swipe actions.",
Self::Startup => "Ready when you sign in.",
Self::Displays => "A place for every screen.",
Self::Connections => "Wi-Fi networks and Bluetooth devices.",
}
}
pub fn icon(self) -> &'static str {
match self {
Self::Accessibility => {
"M12 3a2 2 0 1 0 0 4a2 2 0 1 0 0-4 M4 9h16 M12 9v6 M12 15l-5 6 M12 15l5 6"
}
Self::Appearance => {
"M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 0-4h-1a1 1 0 0 1 0-2h3a6 6 0 0 0 0-12z M7 9h.01 M10 6h.01 M15 6h.01 M18 10h.01"
}
Self::Wallpaper => "M4 4h16v16H4z M4 16l5-5 4 4 3-3 4 4 M15 8h.01",
Self::Bar => "M3 5h18v14H3z M3 9h18 M6 7h.01 M18 7h.01",
Self::Notifications => "M6 8a6 6 0 0 1 12 0v6l2 3H4l2-3z M10 21h4",
Self::LockScreen => "M7 10V7a5 5 0 0 1 10 0v3 M5 10h14v11H5z M12 14v3",
Self::Windows => "M3 4h12v12H3z M8 9h13v12H8z",
Self::Motion => "M3 8h9a3 3 0 1 0-3-3 M3 12h15a3 3 0 1 1-3 3 M3 16h5",
Self::Keyboard => {
"M3 6h18v12H3z M6 9h.01 M10 9h.01 M14 9h.01 M18 9h.01 M6 12h.01 M10 12h.01 M14 12h.01 M18 12h.01 M8 15h8"
}
Self::Shortcuts => {
"M8 8h8v8H8z M8 8H5a3 3 0 1 1 3-3v3z M16 8V5a3 3 0 1 1 3 3h-3z M16 16h3a3 3 0 1 1-3 3v-3z M8 16v3a3 3 0 1 1-3-3h3z"
}
Self::Startup => "M12 3v9 M7 5a9 9 0 1 0 10 0",
Self::Displays => "M3 4h18v13H3z M12 17v4 M8 21h8",
Self::Connections => {
"M2 8a16 16 0 0 1 20 0 M5 12a11 11 0 0 1 14 0 M8 16a6 6 0 0 1 8 0 M12 20h.01"
}
}
}
pub fn matches(self, query: &str) -> bool {
self.matches_own(query)
|| self
.subpages()
.iter()
.any(|(page, _)| page.matches_own(query))
}
fn matches_own(self, query: &str) -> bool {
let query = query.to_lowercase();
self.title().to_lowercase().contains(&query)
|| self.subtitle().to_lowercase().contains(&query)
|| fields(self).iter().any(|f| {
format!("{} {} {}", f.label, f.description, f.path)
.to_lowercase()
.contains(&query)
})
}
}
#[derive(Clone, Debug)]
pub enum Kind {
Toggle(bool),
Range {
default: f64,
min: f64,
max: f64,
step: f64,
suffix: &'static str,
integer: bool,
},
Choice {
default: &'static str,
choices: &'static [(&'static str, &'static str)],
},
Text {
default: &'static str,
argv: bool,
},
}
#[derive(Clone, Debug)]
pub struct Field {
pub path: String,
pub label: String,
pub description: String,
pub kind: Kind,
}
impl Field {
pub fn new(
path: impl Into<String>,
label: impl Into<String>,
description: impl Into<String>,
kind: Kind,
) -> Self {
Self {
path: path.into(),
label: label.into(),
description: description.into(),
kind,
}
}
}
fn toggle(path: &str, label: &str, description: &str, default: bool) -> Field {
Field::new(path, label, description, Kind::Toggle(default))
}
#[derive(Clone, Copy, Debug)]
pub struct RangeSpec {
pub default: f64,
pub min: f64,
pub max: f64,
pub step: f64,
pub suffix: &'static str,
pub integer: bool,
}
pub fn range(
path: impl Into<String>,
label: impl Into<String>,
description: impl Into<String>,
spec: RangeSpec,
) -> Field {
let RangeSpec {
default,
min,
max,
step,
suffix,
integer,
} = spec;
Field::new(
path,
label,
description,
Kind::Range {
default,
min,
max,
step,
suffix,
integer,
},
)
}
pub fn text(
path: impl Into<String>,
label: impl Into<String>,
description: impl Into<String>,
default: &'static str,
) -> Field {
Field::new(
path,
label,
description,
Kind::Text {
default,
argv: false,
},
)
}
pub fn choice(
path: &str,
label: &str,
description: &str,
default: &'static str,
choices: &'static [(&'static str, &'static str)],
) -> Field {
Field::new(path, label, description, Kind::Choice { default, choices })
}
pub fn fields(page: Page) -> Vec<Field> {
match page {
Page::LockScreen => vec![
range(
"lock_screen.dim_after_seconds",
"Dim after",
"Dim the locked display after inactivity. Zero disables dimming.",
RangeSpec {
default: 30.0,
min: 0.0,
max: 600.0,
step: 5.0,
suffix: "s",
integer: true,
},
),
range(
"lock_screen.sleep_after_seconds",
"Display sleep after",
"Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.",
RangeSpec {
default: 120.0,
min: 0.0,
max: 1800.0,
step: 15.0,
suffix: "s",
integer: true,
},
),
toggle(
"lock_screen.show_clock",
"Show clock",
"Display the time above the sign-in card.",
true,
),
toggle(
"lock_screen.show_date",
"Show date",
"Display the weekday and date.",
true,
),
choice(
"lock_screen.clock_format",
"Clock format",
"Use a 12-hour or 24-hour clock.",
"24h",
&[("24h", "24-hour"), ("12h", "12-hour")],
),
range(
"lock_screen.background_blur",
"Wallpaper softness",
"Blur the lock screen wallpaper. Zero keeps the original image sharp.",
RangeSpec {
default: 18.0,
min: 0.0,
max: 40.0,
step: 1.0,
suffix: "",
integer: false,
},
),
range(
"lock_screen.background_dim",
"Wallpaper dimming",
"Darken the wallpaper behind the clock and sign-in card.",
RangeSpec {
default: 0.48,
min: 0.0,
max: 0.9,
step: 0.02,
suffix: "",
integer: false,
},
),
],
Page::Accessibility => vec![
toggle(
"theme.accessibility.increase_contrast",
"Increase contrast",
"Stronger text and borders.",
false,
),
toggle(
"theme.accessibility.reduce_transparency",
"Reduce transparency",
"Solid surfaces; background blur is disabled.",
false,
),
toggle(
"animations.reduced_motion",
"Reduce motion",
"Use immediate changes in place of animations.",
false,
),
],
Page::Appearance => vec![
choice(
"theme.mode",
"Appearance",
"Choose a fixed appearance or let Auto follow a source.",
"dark",
&[("light", "Light"), ("dark", "Dark"), ("auto", "Auto")],
),
text(
"theme.file",
"Shared theme file",
"Optional KDL overrides. Relative paths start in your config folder.",
"",
),
text(
"theme.light.file",
"Light theme file",
"Optional overrides for Light appearance.",
"",
),
text(
"theme.dark.file",
"Dark theme file",
"Optional overrides for Dark appearance.",
"",
),
text(
"theme.schedule.light_at",
"Light at",
"Local time in HH:MM format.",
"07:00",
),
text(
"theme.schedule.dark_at",
"Dark at",
"Local time in HH:MM format.",
"19:00",
),
text(
"theme.schedule.timezone",
"Timezone",
"system follows your desktop timezone; or enter an IANA timezone.",
"system",
),
text(
"theme.accent",
"Accent color",
"Focus rings and selected controls. Use a hex color.",
"#3D7BE6",
),
choice(
"theme.focus_ring.style",
"Accent style",
"Window focus borders and selected controls. Gradients use the theme colors unless overridden.",
"auto",
&[("auto", "Gradient"), ("solid", "Solid")],
),
range(
"theme.material.blur_radius",
"Background blur",
"Softness behind translucent shell surfaces and windows. Zero disables blur.",
RangeSpec {
default: 12.0,
min: 0.0,
max: 32.0,
step: 1.0,
suffix: " px",
integer: false,
},
),
text(
"theme.typography.font_family",
"Interface font",
"Use the name of an installed font family.",
"Inter",
),
],
Page::Wallpaper => vec![
text(
"theme.background.path",
"Image location",
"Choose an image above, or enter its full path.",
ferese_config::default_wallpaper(),
),
choice(
"theme.background.mode",
"Image placement",
"Fill crops to the screen. Fit keeps the whole image.",
"fill",
&[("fill", "Fill"), ("fit", "Fit")],
),
],
Page::Notifications => vec![
toggle(
"notifications.show_popups",
"Show popup notifications",
"Keep notifications in history when popups are hidden.",
true,
),
toggle(
"notifications.do_not_disturb",
"Do Not Disturb",
"Silence normal popups; critical notifications can still appear.",
false,
),
range(
"notifications.timeout_ms",
"Popup timeout",
"Default duration; apps can request another timeout. Pauses while hovered.",
RangeSpec {
default: 6000.0,
min: 1000.0,
max: 30000.0,
step: 1000.0,
suffix: " ms",
integer: true,
},
),
],
Page::Bar => vec![
range(
"theme.geometry.shell_radius",
"Shell corner radius",
"Rounds the bar, menus, notifications and hover backgrounds. Window corners are separate.",
RangeSpec {
default: 14.0,
min: 0.0,
max: 32.0,
step: 1.0,
suffix: " px",
integer: false,
},
),
choice(
"theme.material.style",
"Surface style",
"Choose the finish for the panel, launcher, menus and notifications.",
"solid",
&[("solid", "Solid"), ("translucent", "Translucent")],
),
range(
"theme.material.opacity",
"Shell opacity",
"Background opacity for the bar, popovers, menus, notifications and sharing dialog. Text and icons stay opaque.",
RangeSpec {
default: ferese_config::DEFAULT_MATERIAL_OPACITY,
min: 0.0,
max: 1.0,
step: 0.01,
suffix: "",
integer: false,
},
),
range(
"panels.0.geometry.height",
"Height",
"Keeps icon and text sizes unchanged.",
RangeSpec {
default: 36.0,
min: 24.0,
max: 48.0,
step: 1.0,
suffix: " px",
integer: false,
},
),
range(
"panels.0.geometry.edge_margin",
"Edge margin",
"Space from the selected screen edge. Set to zero for a flush panel.",
RangeSpec {
default: 0.0,
min: 0.0,
max: 24.0,
step: 1.0,
suffix: " px",
integer: true,
},
),
range(
"panels.0.geometry.side_margins",
"Side margins",
"Inset from both edges; stops before extra controls overflow.",
RangeSpec {
default: 0.0,
min: 0.0,
max: 4096.0,
step: 1.0,
suffix: " px",
integer: true,
},
),
range(
"panels.0.geometry.window_clearance",
"Window clearance",
"Space between the panel and application windows.",
RangeSpec {
default: 0.0,
min: 0.0,
max: 24.0,
step: 1.0,
suffix: " px",
integer: true,
},
),
],
Page::Windows => vec![
toggle(
"workspaces.auto_back_and_forth",
"Toggle back with the same workspace shortcut",
"Press the current workspace's shortcut again to return to the previous workspace on this monitor.",
false,
),
choice(
"layout.mode",
"Layout for new workspaces",
"Existing workspaces keep their current layout.",
"scrolling",
&[("scrolling", "Scrolling"), ("tree", "Tiling")],
),
choice(
"scrolling.focus_strategy",
"Scroll behavior",
"How the viewport follows the focused column.",
"minimal",
&[
("minimal", "Minimal"),
("center_on_focus", "Centered"),
("paged", "Paged"),
],
),
range(
"scrolling.default_column_width",
"New window width",
"Fraction of the available workspace.",
RangeSpec {
default: 0.5,
min: 0.25,
max: 1.0,
step: 0.05,
suffix: "",
integer: false,
},
),
range(
"layout.inner_gap",
"Between windows",
"Space between neighboring windows.",
RangeSpec {
default: 10.0,
min: 0.0,
max: 32.0,
step: 1.0,
suffix: " px",
integer: false,
},
),
range(
"layout.outer_gap",
"Workspace edges",
"Space around the outside of the layout.",
RangeSpec {
default: 4.0,
min: 0.0,
max: 32.0,
step: 1.0,
suffix: " px",
integer: false,
},
),
range(
"theme.geometry.window_radius",
"Corner radius",
"Rounded corners for application windows.",
RangeSpec {
default: 14.0,
min: 0.0,
max: 28.0,
step: 1.0,
suffix: " px",
integer: false,
},
),
choice(
"theme.border.style",
"Inactive border style",
"Gradients retain the border color's transparency.",
"auto",
&[("auto", "Gradient"), ("solid", "Solid")],
),
range(
"theme.geometry.focus_ring_width",
"Focus border",
"A subtle outline around the active window.",
RangeSpec {
default: 2.0,
min: 0.0,
max: 4.0,
step: 0.5,
suffix: " px",
integer: false,
},
),
toggle(
"input.focus_follows_mouse",
"Focus follows pointer",
"Focus visible windows without raising or scrolling. Click to bring them into view.",
false,
),
toggle(
"appearance.focus_effect.enabled",
"Window focus effects",
"Apply window opacity and inactive dimming. Turning this off keeps your settings.",
true,
),
range(
"appearance.focus_effect.active_opacity",
"Active window opacity",
"Opacity of the active application, including fullscreen windows.",
RangeSpec {
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
suffix: "",
integer: false,
},
),
range(
"appearance.focus_effect.inactive_opacity",
"Inactive window opacity",
"Opacity of other application windows.",
RangeSpec {
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
suffix: "",
integer: false,
},
),
toggle(
"appearance.focus_effect.blur",
"Window blur",
"Soften the background behind translucent windows. Adjust the amount in Appearance.",
true,
),
range(
"appearance.focus_effect.inactive_dim",
"Inactive window dimming",
"Darken inactive windows. Zero disables dimming.",
RangeSpec {
default: 0.0,
min: 0.0,
max: 1.0,
step: 0.01,
suffix: "",
integer: false,
},
),
],
Page::Motion => vec![
toggle(
"animations.enabled",
"Animations",
"Animate window changes and shell transitions.",
true,
),
range(
"animations.speed",
"Animation speed",
"Lower values give transitions more time.",
RangeSpec {
default: 1.0,
min: 0.25,
max: 2.0,
step: 0.05,
suffix: "×",
integer: false,
},
),
range(
"appearance.focus_effect.duration_ms",
"Focus transition",
"Duration of opacity and dimming changes when the active window changes.",
RangeSpec {
default: 150.0,
min: 0.0,
max: 600.0,
step: 10.0,
suffix: " ms",
integer: false,
},
),
],
Page::Keyboard => vec![
text(
"input.xkb_layout",
"Keyboard layouts",
"Layout codes, separated by commas. For example: us,ru.",
"us",
),
text(
"input.xkb_variant",
"Layout variant",
"Leave empty to use the standard variant.",
"",
),
range(
"input.repeat_rate",
"Key repeat",
"Characters repeated each second while a key is held.",
RangeSpec {
default: 25.0,
min: 1.0,
max: 70.0,
step: 1.0,
suffix: " /s",
integer: true,
},
),
range(
"input.repeat_delay_ms",
"Delay before repeat",
"How long to hold a key before it starts repeating.",
RangeSpec {
default: 600.0,
min: 150.0,
max: 1000.0,
step: 25.0,
suffix: " ms",
integer: true,
},
),
toggle(
"input.touchpad.tap",
"Tap to click",
"Tap the touchpad to click. Updates connected devices immediately.",
true,
),
toggle(
"input.touchpad.natural_scroll",
"Natural scrolling",
"Move content in the direction of your fingers. Updates immediately.",
true,
),
toggle(
"input.touchpad.disable_while_typing",
"Ignore touchpad while typing",
"Avoid accidental pointer movement.",
true,
),
toggle(
"input.touchpad.disable_while_mouse_connected",
"Disable touchpad with a mouse",
"Pause the built-in touchpad while an external mouse is connected.",
false,
),
range(
"input.touchpad.accel_speed",
"Touchpad speed",
"Adjust pointer acceleration for the touchpad.",
RangeSpec {
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.05,
suffix: "",
integer: false,
},
),
toggle(
"input.mouse.natural_scroll",
"Mouse natural scrolling",
"Reverse the direction of the mouse wheel.",
false,
),
toggle(
"input.mouse.left_handed",
"Left-handed mouse",
"Swap the primary and secondary mouse buttons.",
false,
),
toggle(
"input.mouse.middle_emulation",
"Emulate middle click",
"Press both mouse buttons together to middle-click.",
false,
),
range(
"input.mouse.accel_speed",
"Mouse speed",
"Adjust pointer acceleration for connected mice.",
RangeSpec {
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.05,
suffix: "",
integer: false,
},
),
range(
"input.mouse.scroll_factor",
"Mouse scroll speed",
"Scale wheel scrolling in applications.",
RangeSpec {
default: 1.0,
min: 0.1,
max: 5.0,
step: 0.1,
suffix: "×",
integer: false,
},
),
range(
"input.touchpad.swipe_threshold",
"Swipe distance",
"Distance needed to navigate. Larger values reduce accidental swipes.",
RangeSpec {
default: 80.0,
min: 16.0,
max: 1000.0,
step: 8.0,
suffix: " px",
integer: true,
},
),
],
Page::Shortcuts => vec![toggle(
"status.keybinding_guide",
"Show keybinding guide at login",
"Keep the shortcut guide until you are comfortable with Ferese.",
true,
)],
Page::Startup | Page::Displays | Page::Connections => vec![],
}
}