1235 lines
40 KiB
Rust
1235 lines
40 KiB
Rust
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||
pub enum Page {
|
||
Appearance,
|
||
General,
|
||
Theme,
|
||
Tray,
|
||
Configuration,
|
||
Accessibility,
|
||
Wallpaper,
|
||
Bar,
|
||
Notifications,
|
||
LockScreen,
|
||
Windows,
|
||
Motion,
|
||
Keyboard,
|
||
Shortcuts,
|
||
Startup,
|
||
Displays,
|
||
Connections,
|
||
Calendar,
|
||
Mouse,
|
||
Touchpad,
|
||
Shell,
|
||
}
|
||
|
||
impl Page {
|
||
pub const NAVIGATION: [(&'static str, &'static [Self]); 5] = [
|
||
("Appearance", &[Self::Appearance, Self::General]),
|
||
(
|
||
"Desktop",
|
||
&[Self::Theme, Self::Wallpaper, Self::Bar, Self::Tray],
|
||
),
|
||
(
|
||
"Interaction",
|
||
&[
|
||
Self::Windows,
|
||
Self::Keyboard,
|
||
Self::Mouse,
|
||
Self::Touchpad,
|
||
Self::Shortcuts,
|
||
Self::Accessibility,
|
||
],
|
||
),
|
||
(
|
||
"System",
|
||
&[
|
||
Self::Displays,
|
||
Self::Connections,
|
||
Self::Notifications,
|
||
Self::Calendar,
|
||
Self::LockScreen,
|
||
Self::Startup,
|
||
],
|
||
),
|
||
("Advanced", &[Self::Configuration]),
|
||
];
|
||
|
||
/// Subpages share one entry in navigation and search.
|
||
pub fn navigation_page(self) -> Self {
|
||
match self {
|
||
Self::Motion => Self::Windows,
|
||
Self::Shell => Self::Bar,
|
||
_ => self,
|
||
}
|
||
}
|
||
|
||
pub fn subpages(self) -> &'static [(Self, &'static str)] {
|
||
match self.navigation_page() {
|
||
Self::Windows => &[(Self::Windows, "Windows"), (Self::Motion, "Motion")],
|
||
Self::Bar => &[(Self::Bar, "Panel"), (Self::Shell, "Shell")],
|
||
_ => &[],
|
||
}
|
||
}
|
||
|
||
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; 21] = [
|
||
Self::Appearance,
|
||
Self::General,
|
||
Self::Theme,
|
||
Self::Tray,
|
||
Self::Configuration,
|
||
Self::Accessibility,
|
||
Self::Wallpaper,
|
||
Self::Bar,
|
||
Self::Notifications,
|
||
Self::LockScreen,
|
||
Self::Windows,
|
||
Self::Motion,
|
||
Self::Keyboard,
|
||
Self::Shortcuts,
|
||
Self::Startup,
|
||
Self::Displays,
|
||
Self::Connections,
|
||
Self::Calendar,
|
||
Self::Mouse,
|
||
Self::Touchpad,
|
||
Self::Shell,
|
||
];
|
||
|
||
pub fn title(self) -> &'static str {
|
||
ferese_config::locale::text(self.title_key())
|
||
}
|
||
fn title_key(self) -> &'static str {
|
||
match self {
|
||
Self::Appearance => "Appearance",
|
||
Self::General => "General",
|
||
Self::Tray => "Tray icons",
|
||
Self::Configuration => "All options",
|
||
Self::Theme => "Theme",
|
||
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",
|
||
Self::Mouse => "Mouse",
|
||
Self::Touchpad => "Touchpad",
|
||
Self::Shell => "Shell",
|
||
Self::Shortcuts => "Shortcuts",
|
||
Self::Startup => "Login items",
|
||
Self::Displays => "Displays",
|
||
Self::Connections => "Connections",
|
||
Self::Calendar => "Calendar",
|
||
}
|
||
}
|
||
|
||
pub fn subtitle(self) -> &'static str {
|
||
ferese_config::locale::text(match self {
|
||
Self::Appearance => "All Ferese settings, grouped by function.",
|
||
Self::General => "Language and desktop preferences.",
|
||
Self::Tray => "Choose which status and application icons appear in the panel.",
|
||
Self::Configuration => {
|
||
"Every supported configuration option, including advanced settings."
|
||
}
|
||
Self::Theme => "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, variants and repeat behavior.",
|
||
Self::Mouse => "Mouse buttons, pointer movement and scrolling.",
|
||
Self::Touchpad => "Touchpad taps, scrolling and gestures.",
|
||
Self::Shell => "Shared surfaces, launcher and system menus.",
|
||
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.",
|
||
Self::Calendar => "Calendar, Japanese holiday rules and custom holidays.",
|
||
})
|
||
}
|
||
|
||
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 | Self::General | Self::Configuration => {
|
||
"M4 5h16 M4 12h16 M4 19h16 M8 3v4 M16 10v4 M10 17v4"
|
||
}
|
||
Self::Tray => "M3 5h18v14H3z M6 9h3v3H6z M12 9h3v3h-3z M18 9h.01",
|
||
Self::Theme => {
|
||
"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::Calendar => {
|
||
"M4 5h16v16H4z M8 3v4 M16 3v4 M4 10h16 M8 14h.01 M12 14h.01 M16 14h.01 M8 18h.01 M12 18h.01"
|
||
}
|
||
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::Mouse => "M8 10V7a4 4 0 0 1 8 0v10a4 4 0 0 1-8 0z M12 3v7 M8 10h8",
|
||
Self::Touchpad => "M3 5h18v14H3z M3 15h18 M12 15v4",
|
||
Self::Shell => "M3 4h18v16H3z M3 9h18 M8 9v11",
|
||
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.title_key().to_lowercase().contains(&query)
|
||
|| ferese_config::locale::in_language(
|
||
ferese_config::locale::Language::Chinese,
|
||
self.title_key(),
|
||
)
|
||
.to_lowercase()
|
||
.contains(&query)
|
||
|| self.subtitle().to_lowercase().contains(&query)
|
||
|| fields(self).iter().any(|f| {
|
||
format!(
|
||
"{} {} {} {} {}",
|
||
f.label,
|
||
f.description,
|
||
f.path,
|
||
ferese_config::locale::in_language(
|
||
ferese_config::locale::Language::Chinese,
|
||
&f.label
|
||
),
|
||
ferese_config::locale::in_language(
|
||
ferese_config::locale::Language::Chinese,
|
||
&f.description
|
||
)
|
||
)
|
||
.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::Theme => 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::Shell => 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,
|
||
},
|
||
),
|
||
],
|
||
Page::Bar => vec![
|
||
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,
|
||
},
|
||
),
|
||
],
|
||
Page::Mouse => vec![
|
||
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,
|
||
},
|
||
),
|
||
],
|
||
Page::Touchpad => vec![
|
||
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,
|
||
},
|
||
),
|
||
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::General => vec![choice(
|
||
"ui.language",
|
||
"Language",
|
||
"Changes the Ferese interface language. Applies immediately.",
|
||
"auto",
|
||
&[
|
||
("auto", "Follow system"),
|
||
("zh-CN", "简体中文"),
|
||
("en", "English"),
|
||
],
|
||
)],
|
||
Page::Calendar => vec![toggle(
|
||
"status.calendar.show_adjacent_days",
|
||
"Show dates from adjacent months",
|
||
"Display the previous and next month's dates in the calendar.",
|
||
true,
|
||
)],
|
||
Page::Tray => vec![
|
||
toggle(
|
||
"status.tray.enabled",
|
||
"Show tray icons",
|
||
"Show application icons in panel tray items.",
|
||
true,
|
||
),
|
||
toggle(
|
||
"status.tray.show_passive",
|
||
"Show passive icons",
|
||
"Include applications without active status.",
|
||
false,
|
||
),
|
||
range(
|
||
"status.tray.icon_size",
|
||
"Tray icon size",
|
||
"Size of application tray icons.",
|
||
RangeSpec {
|
||
default: 16.,
|
||
min: 8.,
|
||
max: 48.,
|
||
step: 1.,
|
||
suffix: " px",
|
||
integer: true,
|
||
},
|
||
),
|
||
Field::new(
|
||
"status.tray.hidden_items",
|
||
"Hidden applications",
|
||
"Application IDs to hide, separated by spaces.",
|
||
Kind::Text {
|
||
default: "",
|
||
argv: true,
|
||
},
|
||
),
|
||
],
|
||
Page::Appearance
|
||
| Page::Configuration
|
||
| Page::Startup
|
||
| Page::Displays
|
||
| Page::Connections => vec![],
|
||
}
|
||
}
|
||
|
||
pub struct FieldGroup {
|
||
pub title: &'static str,
|
||
pub fields: Vec<Field>,
|
||
}
|
||
|
||
/// Semantic groups, preserving every field and the declaration order in each group.
|
||
pub fn grouped_fields(page: Page, fields: Vec<Field>) -> Vec<FieldGroup> {
|
||
let mut groups: Vec<FieldGroup> = Vec::new();
|
||
for field in fields {
|
||
let p = field.path.as_str();
|
||
let title = match page {
|
||
Page::General => "Interface",
|
||
Page::Calendar => "Calendar display",
|
||
Page::Windows => {
|
||
if p.starts_with("workspaces.") || p == "layout.mode" || p.starts_with("scrolling.")
|
||
{
|
||
"Workspaces and layout"
|
||
} else if p == "layout.inner_gap"
|
||
|| p == "layout.outer_gap"
|
||
|| p == "theme.geometry.window_radius"
|
||
{
|
||
"Spacing and corners"
|
||
} else {
|
||
"Focus and window effects"
|
||
}
|
||
}
|
||
Page::Shell => "Shell surfaces",
|
||
Page::Mouse => "Mouse",
|
||
Page::Touchpad => "Touchpad",
|
||
Page::Keyboard => {
|
||
if p.starts_with("input.xkb_") {
|
||
"Keyboard layout"
|
||
} else if p.starts_with("input.repeat_") {
|
||
"Keyboard repeat"
|
||
} else if p.starts_with("input.mouse.") {
|
||
"Mouse"
|
||
} else {
|
||
"Touchpad"
|
||
}
|
||
}
|
||
Page::LockScreen => {
|
||
if p.starts_with("lock_screen.background_") {
|
||
"Lock background"
|
||
} else if p.contains("after_seconds") {
|
||
"Display power"
|
||
} else {
|
||
"Clock and date"
|
||
}
|
||
}
|
||
Page::Theme => {
|
||
if p.ends_with(".file") {
|
||
"Theme overrides"
|
||
} else if p == "theme.mode" || p.starts_with("theme.schedule.") {
|
||
"Automatic appearance"
|
||
} else if p.starts_with("theme.typography.") {
|
||
"Typography"
|
||
} else if p.starts_with("theme.material.") {
|
||
"Surface and transparency"
|
||
} else {
|
||
"Colors and accents"
|
||
}
|
||
}
|
||
Page::Wallpaper => "Wallpaper image",
|
||
Page::Notifications => {
|
||
if p.ends_with("timeout_ms") {
|
||
"Notification timing"
|
||
} else {
|
||
"Notification behavior"
|
||
}
|
||
}
|
||
Page::Accessibility => {
|
||
if p.starts_with("animations.") {
|
||
"Motion"
|
||
} else {
|
||
"Visual accessibility"
|
||
}
|
||
}
|
||
Page::Motion => {
|
||
if p.starts_with("animations.") {
|
||
"Animations"
|
||
} else {
|
||
"Focus transition"
|
||
}
|
||
}
|
||
Page::Shortcuts => "Shortcut guide",
|
||
Page::Tray => "Application tray",
|
||
_ => "General",
|
||
};
|
||
if let Some(group) = groups.iter_mut().find(|group| group.title == title) {
|
||
group.fields.push(field)
|
||
} else {
|
||
groups.push(FieldGroup {
|
||
title,
|
||
fields: vec![field],
|
||
});
|
||
}
|
||
}
|
||
groups
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod group_tests {
|
||
use super::*;
|
||
#[test]
|
||
fn every_standard_field_and_choice_has_chinese_interface_text() {
|
||
use ferese_config::locale::{Language, in_language};
|
||
for page in Page::ALL {
|
||
for field in fields(page) {
|
||
for text in [&field.label, &field.description] {
|
||
if !text.is_empty() {
|
||
assert_ne!(
|
||
in_language(Language::Chinese, text),
|
||
text,
|
||
"{}: {text}",
|
||
field.path
|
||
);
|
||
}
|
||
}
|
||
if let Kind::Choice { choices, .. } = field.kind {
|
||
for (_, text) in choices {
|
||
if !matches!(*text, "English" | "简体中文") {
|
||
assert_ne!(
|
||
in_language(Language::Chinese, text),
|
||
*text,
|
||
"{}: {text}",
|
||
field.path
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#[test]
|
||
fn semantic_groups_keep_every_field_once_and_separate_input_devices() {
|
||
for page in Page::ALL {
|
||
let mut expected = fields(page).into_iter().map(|f| f.path).collect::<Vec<_>>();
|
||
let groups = grouped_fields(page, fields(page));
|
||
assert!(groups.iter().all(|g| !g.fields.is_empty()));
|
||
let mut actual = groups
|
||
.into_iter()
|
||
.flat_map(|g| g.fields.into_iter().map(|f| f.path))
|
||
.collect::<Vec<_>>();
|
||
expected.sort();
|
||
actual.sort();
|
||
assert_eq!(actual, expected, "{page:?}");
|
||
}
|
||
let mouse = grouped_fields(Page::Mouse, fields(Page::Mouse));
|
||
let touchpad = grouped_fields(Page::Touchpad, fields(Page::Touchpad));
|
||
assert!(
|
||
mouse
|
||
.iter()
|
||
.find(|g| g.title == "Mouse")
|
||
.unwrap()
|
||
.fields
|
||
.iter()
|
||
.all(|f| f.path.starts_with("input.mouse."))
|
||
);
|
||
assert!(
|
||
touchpad
|
||
.iter()
|
||
.find(|g| g.title == "Touchpad")
|
||
.unwrap()
|
||
.fields
|
||
.iter()
|
||
.all(|f| f.path.starts_with("input.touchpad."))
|
||
);
|
||
let windows = grouped_fields(Page::Windows, fields(Page::Windows));
|
||
assert_eq!(
|
||
windows.iter().map(|g| g.title).collect::<Vec<_>>(),
|
||
vec![
|
||
"Workspaces and layout",
|
||
"Spacing and corners",
|
||
"Focus and window effects"
|
||
]
|
||
);
|
||
}
|
||
}
|