feat(settings): add grouped configuration hub, languages and tray controls

This commit is contained in:
aria committed 2026-10-28 03:37:22 +09:00
1 parent 6f34fd080f
commit cc3b6f903f
90 files changed
+3625 -408

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ rust-version.workspace = true
workspace = true
[dependencies]
schemars.workspace = true
ferese-activities = { path = "../crates/ferese-activities" }
ferese-panel = { path = "../crates/ferese-panel" }
ferese-shape.workspace = true
+31 -20
View File
@@ -36,8 +36,10 @@ pub(crate) use tab_indicator::TabIndicatorSettings;
use crate::window_rules;
use crate::window_rules::{WindowRule, WindowRuleConfig};
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub struct Config {
#[serde(default)]
ui: ferese_config::locale::Settings,
#[serde(default)]
activities: ferese_config::activities::Settings,
#[serde(default)]
@@ -79,10 +81,10 @@ pub struct Config {
#[serde(default)]
pub(crate) xwayland: XwaylandConfig,
#[serde(default, rename = "status")]
_status: ShellStatusConfig,
_status: ferese_config::status::Settings,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq, schemars::JsonSchema, serde::Serialize)]
pub(crate) struct DaemonConfig {
pub command: Vec<String>,
#[serde(default = "default_true")]
@@ -93,16 +95,6 @@ pub(crate) struct DaemonConfig {
pub nested: bool,
}
// Validate shell-only fields too, before publishing an accepted source.
#[derive(Debug, Default, Deserialize)]
#[allow(dead_code)]
#[serde(deny_unknown_fields)]
struct ShellStatusConfig {
keybinding_guide: Option<bool>,
low_battery_threshold: Option<u8>,
settings_command: Option<Vec<String>>,
}
impl Config {
#[cfg(test)]
pub(crate) fn parse_source(source: &str) -> Result<Self, ConfigError> {
@@ -129,6 +121,7 @@ impl Config {
}
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
self._status.validate().map_err(ConfigError::Binding)?;
if let Some(panels) = &self.panels {
ferese_panel::validate(panels).map_err(ConfigError::Binding)?;
}
@@ -212,13 +205,13 @@ impl Config {
}
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
struct WorkspacesConfig {
#[serde(default)]
auto_back_and_forth: bool,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
struct LayoutConfig {
mode: Option<LayoutModeValue>,
#[serde(default = "default_inner_gap")]
@@ -240,14 +233,14 @@ impl Default for LayoutConfig {
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[derive(Clone, Copy, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(rename_all = "lowercase")]
enum LayoutModeValue {
Scrolling,
Tree,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
struct ScrollingConfig {
#[serde(default)]
tab_indicator: tab_indicator::TabIndicatorConfig,
@@ -258,14 +251,14 @@ struct ScrollingConfig {
width_presets: Vec<ColumnWidthValue>,
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[derive(Clone, Copy, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(rename_all = "lowercase")]
enum ColumnDisplayValue {
Stacked,
Tabbed,
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[derive(Clone, Copy, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum FocusStrategyValue {
Minimal,
@@ -273,7 +266,7 @@ enum FocusStrategyValue {
Paged,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(untagged)]
enum ColumnWidthValue {
Proportion(f64),
@@ -428,6 +421,24 @@ pub(crate) fn config_path() -> Option<PathBuf> {
#[cfg(test)]
mod tests {
#[test]
fn configuration_schema_exposes_effective_panel_defaults_and_typed_values() {
let exported = crate::configuration_schema();
assert_eq!(exported["version"], 1);
assert_eq!(
exported["defaults"]["panels"],
serde_json::to_value(
Config::default()
.panels
.unwrap_or_else(|| vec![ferese_panel::Panel::default()])
)
.unwrap()
);
let config: Config = serde_json::from_value(exported["defaults"].clone()).unwrap();
config.runtime_config().unwrap();
assert!(exported["schema"]["properties"].get("ui").is_some());
assert!(exported["schema"]["properties"].get("status").is_some());
}
#[test]
fn geometry_rejects_removed_and_misspelled_keys_in_both_consumption_paths() {
for key in [
+16 -14
View File
@@ -202,14 +202,14 @@ pub struct BorderGradient {
pub angle: f64,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct BorderPaintConfig {
#[serde(default)]
pub(super) style: ferese_config::theme::PaintStyle,
pub(super) gradient: Option<BorderGradientConfig>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct BorderGradientConfig {
pub(super) from: String,
pub(super) to: String,
@@ -278,7 +278,9 @@ pub struct ThemeSettings {
pub reduce_transparency: bool,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum MaterialStyle {
Translucent,
@@ -286,7 +288,7 @@ pub enum MaterialStyle {
Solid,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct AppearanceConfig {
#[serde(default)]
@@ -303,7 +305,7 @@ pub struct FocusEffectSettings {
pub duration_ms: f64,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(default, deny_unknown_fields)]
pub(super) struct FocusEffectConfig {
pub(super) enabled: bool,
@@ -327,7 +329,7 @@ impl Default for FocusEffectConfig {
}
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct ThemeConfig {
#[serde(default)]
pub(super) border: BorderPaintConfig,
@@ -349,23 +351,23 @@ pub(super) struct ThemeConfig {
pub(super) surface: ThemeSurfaceConfig,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct ThemeSurfaceConfig {
#[serde(default)]
pub(super) bar: ThemeBarConfig,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct ThemeBarConfig {
pub(super) background: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct OverviewTypographyConfig {
pub(super) font_family: Option<String>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct ThemeMaterialConfig {
#[serde(default = "default_shell_opacity")]
pub(super) opacity: f64,
@@ -400,7 +402,7 @@ fn default_backdrop_blur() -> f64 {
12.0
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct ThemeColorsConfig {
#[serde(default = "default_text_primary_color")]
pub(super) text_primary: String,
@@ -429,13 +431,13 @@ impl Default for ThemeColorsConfig {
}
}
#[derive(Debug, Default, Deserialize)]
#[derive(Debug, Default, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct ThemeShadowConfig {
#[serde(default)]
pub(super) soft: SoftShadowConfig,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct SoftShadowConfig {
#[serde(default = "default_shadow_offset_y")]
pub(super) offset_y: f64,
@@ -455,7 +457,7 @@ impl Default for SoftShadowConfig {
}
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ThemeGeometryConfig {
#[serde(default, rename = "control_gap")]
+13 -2
View File
@@ -832,7 +832,18 @@ impl Binding {
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)]
#[derive(
Clone,
Copy,
Debug,
Default,
Deserialize,
Eq,
Hash,
PartialEq,
schemars::JsonSchema,
serde::Serialize,
)]
#[serde(rename_all = "lowercase")]
pub(super) enum BindingMatch {
#[default]
@@ -843,7 +854,7 @@ pub(super) enum BindingMatch {
Pointer,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct BindingConfig {
pub(super) keys: String,
#[serde(default, rename = "match")]
+1 -1
View File
@@ -6,7 +6,7 @@ use super::{
#[cfg(test)]
use ferese_layout::Direction;
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct BindingModeConfig {
name: String,
+6 -4
View File
@@ -141,7 +141,7 @@ fn default_swipe_threshold() -> u16 {
80
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct InputConfig {
#[serde(default)]
pub(super) focus_follows_mouse: bool,
@@ -176,7 +176,7 @@ impl Default for InputConfig {
}
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct TouchpadConfig {
#[serde(default)]
pub(super) accel_speed: f64,
@@ -257,14 +257,16 @@ mod tests {
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[derive(
Clone, Copy, Debug, Deserialize, PartialEq, Eq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(rename_all = "lowercase")]
pub enum AccelerationProfile {
Adaptive,
Flat,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, schemars::JsonSchema, serde::Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct MouseSettings {
pub natural_scroll: bool,
+11 -5
View File
@@ -191,7 +191,9 @@ pub struct OutputModeRequest {
pub refresh_millihertz: Option<u32>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum OutputTransform {
#[default]
@@ -211,7 +213,9 @@ pub enum OutputTransform {
Flipped270,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, serde::Serialize, PartialEq, Eq)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, serde::Serialize, PartialEq, Eq, schemars::JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum LidPolicy {
#[default]
@@ -223,7 +227,9 @@ const fn default_confirm_timeout() -> u32 {
15
}
#[derive(Clone, Copy, Debug, Default, Deserialize, serde::Serialize, PartialEq, Eq)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, serde::Serialize, PartialEq, Eq, schemars::JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum OutputLayout {
InternalOnly,
@@ -233,7 +239,7 @@ pub enum OutputLayout {
Mirror,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct OutputProfileConfig {
pub(super) name: String,
#[serde(default)]
@@ -248,7 +254,7 @@ pub(super) struct OutputProfileConfig {
pub(super) outputs: Vec<OutputConfig>,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(super) struct OutputConfig {
#[serde(default)]
pub(super) auto_refresh: bool,
+1 -1
View File
@@ -1,6 +1,6 @@
use super::{Config, ConfigError, Deserialize, parse_color};
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(default, deny_unknown_fields)]
pub(super) struct TabIndicatorConfig {
width: f64,
+5 -3
View File
@@ -2,7 +2,9 @@ use std::path::PathBuf;
use serde::Deserialize;
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum XwaylandStartup {
#[default]
@@ -10,7 +12,7 @@ pub(crate) enum XwaylandStartup {
Eager,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, schemars::JsonSchema, serde::Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct XwaylandConfig {
pub enabled: bool,
@@ -79,7 +81,7 @@ mod tests {
#[test]
fn startup_mode_accepts_kebab_case() {
#[derive(Deserialize)]
#[derive(Deserialize, schemars::JsonSchema, serde::Serialize)]
struct Wrapper {
startup: XwaylandStartup,
}
+6 -2
View File
@@ -11,7 +11,9 @@ use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
use crate::Ferese;
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[derive(
Clone, Copy, Debug, Deserialize, PartialEq, Eq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct Settings {
pub fullscreen_playback: bool,
@@ -25,7 +27,9 @@ impl Default for Settings {
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[derive(
Clone, Copy, Debug, Deserialize, PartialEq, Eq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
None,
+16
View File
@@ -65,12 +65,28 @@ use tracing::{info, warn};
use crate::backends::LaunchConfig;
fn configuration_schema() -> serde_json::Value {
let mut schema =
serde_json::to_value(schemars::schema_for!(config::Config)).expect("serializable schema");
let mut defaults =
serde_json::to_value(config::Config::default()).expect("serializable defaults");
// None means the implicit default panel, not an empty desktop. Present its
// effective value so the complete editor can modify it directly.
defaults["panels"] = serde_json::json!([ferese_panel::Panel::default()]);
schema["properties"]["panels"]["default"] = defaults["panels"].clone();
serde_json::json!({"version":1,"schema":schema,"defaults":defaults})
}
fn main() -> Result<(), Box<dyn Error>> {
init_logging();
// Settings validates a candidate before atomically replacing the user's file.
// This mode never opens a display, input device, or compositor socket.
let args = std::env::args_os().skip(1).collect::<Vec<_>>();
if args == ["--config-schema"] || args == ["--config-schema", "--backend=schema-query"] {
println!("{}", configuration_schema());
return Ok(());
}
if args.first().is_some_and(|arg| arg == "--check-config") {
if args.len() != 2 {
return Err("usage: ferese --check-config PATH".into());
+1 -1
View File
@@ -17,7 +17,7 @@ use smithay::wayland::session_lock::{
use crate::Ferese;
#[derive(Clone, Copy, Debug, serde::Deserialize)]
#[derive(Clone, Copy, Debug, serde::Deserialize, schemars::JsonSchema, serde::Serialize)]
#[serde(default)]
pub(crate) struct IdleSettings {
pub dim_after_seconds: u64,
+4 -2
View File
@@ -27,7 +27,7 @@ use std::sync::Arc;
const UPLOAD_RETRY_DELAY: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq, schemars::JsonSchema, serde::Serialize)]
pub(crate) struct WallpaperConfig {
#[serde(default = "default_wallpaper_path")]
pub path: Option<PathBuf>,
@@ -48,7 +48,9 @@ impl Default for WallpaperConfig {
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
#[derive(
Clone, Copy, Debug, Default, Deserialize, PartialEq, schemars::JsonSchema, serde::Serialize,
)]
#[serde(rename_all = "snake_case")]
pub(crate) enum WallpaperMode {
#[default]
+1 -1
View File
@@ -1,6 +1,6 @@
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
pub(crate) struct WindowRuleConfig {
pub app_id: Option<String>,
pub title: Option<String>,