From 62ae594b4c4b8ea8a11501586cc7f2ad95d848ed Mon Sep 17 00:00:00 2001 From: ur-fault Date: Thu, 29 Jan 2026 00:11:02 +0100 Subject: [PATCH 01/23] Begin: new settings system New system uses macro for generation full and partial config structs. - You can merge full + partial -> full - Primitives merge by overriding - Settings are split into sections allowing for easier management --- Cargo.lock | 7 + tmaze/Cargo.toml | 1 + tmaze/src/settings/mod.rs | 2 + tmaze/src/settings/new_settings.rs | 291 +++++++++++++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 tmaze/src/settings/new_settings.rs diff --git a/Cargo.lock b/Cargo.lock index ce42509..ddef941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1521,6 +1521,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pausable_clock" version = "1.0.2" @@ -2384,6 +2390,7 @@ dependencies = [ "json5", "log", "pad", + "paste", "pausable_clock", "rand 0.8.5", "rodio", diff --git a/tmaze/Cargo.toml b/tmaze/Cargo.toml index bbac27e..5d78570 100644 --- a/tmaze/Cargo.toml +++ b/tmaze/Cargo.toml @@ -38,6 +38,7 @@ crates_io_api = { version = "0.11.0", optional = true, default-features = false, semver = { version = "1.0.23", optional = true } tokio = { version = "1.40.0", optional = true, features = ["rt", "rt-multi-thread"] } rodio = { version = "0.18.1", optional = true, default-features = false, features = ["wav", "mp3"] } +paste = "1.0.15" [features] default = ["updates", "sound"] diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 492be4b..d106fe0 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -2,6 +2,8 @@ mod attribute; pub mod style_browser; pub mod theme; +mod new_settings; + use cmaze::{ algorithms::{MazeSpec, MazeSpecType}, dims::{Dims, Offset}, diff --git a/tmaze/src/settings/new_settings.rs b/tmaze/src/settings/new_settings.rs new file mode 100644 index 0000000..46ba8c3 --- /dev/null +++ b/tmaze/src/settings/new_settings.rs @@ -0,0 +1,291 @@ +use cmaze::{ + algorithms::MazeSpec, + dims::{Dims, Dims3D, Offset}, +}; +use paste::paste; +use std::sync::Arc; + +use hashbrown::HashMap; + +struct Settings { + inner: Arc, +} + +impl Settings { + fn new() -> Self { + Settings { + inner: Arc::new(SettingsInner::new()), + } + } +} + +struct SettingsInner { + // ui_layer: PartialConfig, + config_layer: PartialConfig, + base: Config, +} + +impl SettingsInner { + fn new() -> Self { + Self { + config_layer: PartialConfig { + general: todo!(), + viewport: todo!(), + nagivation: todo!(), + updates: todo!(), + audio: todo!(), + }, + base: Config { + general: todo!(), + viewport: todo!(), + nagivation: todo!(), + updates: todo!(), + audio: todo!(), + }, + } + } +} + +trait Mergeable { + fn merge(&mut self, other: &O); +} + +// trace_macros!(true); + +macro_rules! config { + (@step $name:ident + [$($fields:tt)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + { $field:ident : $type:ty, $($rest:tt)* } + ) => { + config!{ @step + $name + [ $($fields)* $field Default::default() ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* pub $field : Option<$type>, ] + { $($rest)* } + } + }; + + (@step $name:ident + [$($fields:tt)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + { #[nest] $field:ident : $type:ty, $($rest:tt)* } + ) => { + config!{ @step + $name + [ $($fields)* $field Default::default() ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* pub $field : Option<[]>, ] + { $($rest)* } + } + }; + + (@step $name:ident + [$($fields:tt)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + { $field:ident : $type:ty = $def:expr, $($rest:tt)* } + ) => { + config!{ @step + $name + [ $($fields)* $field ($def) ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* pub $field : Option<$type>, ] + { $($rest)* } + } + }; + + (@step $name:ident + [$($fields:ident $def_vals:expr)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + { } + ) => { + pub struct $name { + $($rfields)* + } + + impl ::std::default::Default for $name { + fn default() -> Self { + Self { + $( + $fields : $def_vals, + )* + } + } + } + + paste! { + pub struct [] { + $($pfields)* + } + + impl Mergeable<[]> for $name { + fn merge(&mut self, other: &[]) { + $( + if let Some(value) = &other.$fields { + self.$fields.merge(value); + } + )* + } + } + } + }; + + ($(pub struct $name:ident { $($body:tt)* })*) => { + $(config!{ @step $name [] [] [] { $($body)* } })* + }; +} + +macro_rules! impl_merge_prims { + ($($t:ty)*) => { + $(impl Mergeable<$t> for $t where $t: Clone { + fn merge(&mut self, other: &$t) { + *self = other.clone(); + } + })* + }; +} + +type Rgb = (u8, u8, u8); + +impl_merge_prims! { + String + f64 + i64 + bool + log::Level + Rgb + Dims + Dims3D + CameraMode +} + +config! { + pub struct Config { + #[nest] general: General, + #[nest] viewport: Viewport, + #[nest] nagivation: Navigation, + #[nest] updates: Updates, + #[nest] audio: Audio, + } + + pub struct General { + theme: String, + logging_level: log::Level = log::Level::Info, + debug_logging_level: log::Level = log::Level::Info, + file_logging_level: log::Level = log::Level::Info, + #[nest] terminal_scheme: TerminalColorScheme, + } + + pub struct Viewport { + slow: bool, + disable_tower_auto_up: bool, + camera_mode: CameraMode, + camera_smoothing: f64 = 0.5, + player_smoothing: f64 = 0.5, + viewport_margin: Dims = Dims(4, 3), + } + + pub struct Navigation { + enable_mouse: bool = true, + enable_dpad: bool, + landscape_dpad_on_left: bool, + dpad_swap_up_down: bool, + enable_margin_around_dpad: bool, + enable_dpad_highlight: bool = true, + } + + pub struct Updates { + check_interval: bool, + include_prereleases: bool, + } + + pub struct Audio { + enable_audio: bool, + audio_volume: f64, + enable_music: bool, + music_volume: f64, + } + + pub struct Presets { + presets: PresetList, + } +} + +config! { + pub struct TerminalColorScheme { + primary_fg: Rgb, + primary_bg: Rgb, + black: Rgb, // grey + dark_grey: Rgb, // dark grey + red: Rgb, + dark_red: Rgb, + green: Rgb, + dark_green: Rgb, + yellow: Rgb, + dark_yellow: Rgb, + blue: Rgb, + dark_blue: Rgb, + magenta: Rgb, + dark_magenta: Rgb, + cyan: Rgb, + dark_cyan: Rgb, + white: Rgb, + grey: Rgb, + } +} + +#[derive(Default, Clone, Copy, Debug)] +pub enum CameraMode { + #[default] + CloseFollow, + EdgeFollow { + x: Offset, + y: Offset, + }, +} + +#[derive(Debug, Clone, Copy, Default)] +pub enum UpdateCheckInterval { + Never, + #[default] + Daily, + Weekly, + Monthly, + Yearly, + Always, +} + +#[derive(Debug, Clone, Default)] +pub struct PresetList { + presets: Vec, +} + +impl Mergeable for PresetList { + fn merge(&mut self, other: &Self) { + self.presets.extend_from_slice(&other.presets); + } +} + +#[derive(Debug, Clone)] +pub struct MazePreset { + pub title: String, + pub description: Option, + + pub default: bool, + + pub maze_spec: MazeSpec, +} + +enum Value { + Object(HashMap), + List(Vec), + String(String), + Number(f64), + Int(i64), + Bool(bool), +} From ba3745b1b2023fb277c4e87800b1586c3674c026 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Thu, 29 Jan 2026 10:44:50 +0100 Subject: [PATCH 02/23] Update: config! and friends to sep file config! now also has more features, namely supports custom attributes on Partial struct fields. --- Cargo.lock | 3 + tmaze/Cargo.toml | 2 +- tmaze/src/settings/config_utils.rs | 105 +++++++++++++++ tmaze/src/settings/mod.rs | 3 +- tmaze/src/settings/new_settings.rs | 201 +++++++++++------------------ 5 files changed, 189 insertions(+), 125 deletions(-) create mode 100644 tmaze/src/settings/config_utils.rs diff --git a/Cargo.lock b/Cargo.lock index ddef941..1f121e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,6 +1257,9 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "serde", +] [[package]] name = "loom" diff --git a/tmaze/Cargo.toml b/tmaze/Cargo.toml index 5d78570..a8dff85 100644 --- a/tmaze/Cargo.toml +++ b/tmaze/Cargo.toml @@ -26,7 +26,7 @@ derivative = "2.2.0" unicode-width = "0.1.14" thiserror = "2" chrono = { version = "0.4.38", features = ["serde"] } -log = "0.4" +log = { version = "0.4", features = ["serde"] } smallvec = { version = "1.13.2", features = ["const_generics"] } hashbrown = { version = "0.14", features = ["serde"] } toml = "0.8" diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs new file mode 100644 index 0000000..033686e --- /dev/null +++ b/tmaze/src/settings/config_utils.rs @@ -0,0 +1,105 @@ +pub trait Mergeable { + fn merge(&mut self, other: &O); +} + +#[macro_export] +macro_rules! config { + (@step $name:ident + [$($fields:tt)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + // this branch must be 1st so that #[nest] is parsed as special attribute + { #[nest] $(#[$attr:meta])* $field:ident : $type:ty, $($rest:tt)* } + ) => { + config!{ @step + $name + [ $($fields)* $field = ::std::default::Default::default(), ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* $(#[$attr])* pub $field : Option<[]>, ] + { $($rest)* } + } + }; + + (@step $name:ident + [$($fields:tt)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + { $(#[$attr:meta])* $field:ident : $type:ty, $($rest:tt)* } + ) => { + config!{ @step + $name + [ $($fields)* $field = ::std::default::Default::default(), ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* $(#[$attr])* pub $field : Option<$type>, ] + { $($rest)* } + } + }; + + (@step $name:ident + [$($fields:tt)*] + [$($rfields:tt)*] + [$($pfields:tt)*] + { $(#[$attr:meta])* $field:ident : $type:ty = $def:expr, $($rest:tt)* } + ) => { + config!{ @step + $name + [ $($fields)* $field = ($def), ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* $(#[$attr])* pub $field : Option<$type>, ] + { $($rest)* } + } + }; + + (@step $name:ident + [$($fields:ident = $def_vals:expr),* ,] + [$($rfields:tt)*] + [$($pfields:tt)*] + { } + ) => { + pub struct $name { + $($rfields)* + } + + impl ::std::default::Default for $name { + fn default() -> Self { + Self { + $( + $fields : $def_vals, + )* + } + } + } + + ::paste::paste! { + #[derive(Default, Clone, Serialize, Deserialize)] + pub struct [] { + $($pfields)* + } + + impl Mergeable<[]> for $name { + fn merge(&mut self, other: &[]) { + $( + if let Some(value) = &other.$fields { + self.$fields.merge(value); + } + )* + } + } + } + }; + + ($(pub struct $name:ident { $($body:tt)* })*) => { + $(config!{ @step $name [] [] [] { $($body)* } })* + }; +} + +#[macro_export] +macro_rules! impl_merge_prims { + ($($t:ty)*) => { + $(impl Mergeable<$t> for $t where $t: Clone { + fn merge(&mut self, other: &$t) { + *self = other.clone(); + } + })* + }; +} diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index d106fe0..c6e5fc1 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -2,7 +2,8 @@ mod attribute; pub mod style_browser; pub mod theme; -mod new_settings; +pub mod new_settings; +pub mod config_utils; use cmaze::{ algorithms::{MazeSpec, MazeSpecType}, diff --git a/tmaze/src/settings/new_settings.rs b/tmaze/src/settings/new_settings.rs index 46ba8c3..8cc180a 100644 --- a/tmaze/src/settings/new_settings.rs +++ b/tmaze/src/settings/new_settings.rs @@ -2,10 +2,13 @@ use cmaze::{ algorithms::MazeSpec, dims::{Dims, Dims3D, Offset}, }; -use paste::paste; + use std::sync::Arc; use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::{config, impl_merge_prims, settings::config_utils::Mergeable}; struct Settings { inner: Arc, @@ -28,126 +31,20 @@ struct SettingsInner { impl SettingsInner { fn new() -> Self { Self { - config_layer: PartialConfig { - general: todo!(), - viewport: todo!(), - nagivation: todo!(), - updates: todo!(), - audio: todo!(), - }, - base: Config { - general: todo!(), - viewport: todo!(), - nagivation: todo!(), - updates: todo!(), - audio: todo!(), - }, + config_layer: PartialConfig::default(), + base: Config::default(), } } } -trait Mergeable { - fn merge(&mut self, other: &O); +enum ConfigLoadError { + IoError(std::io::Error), + ParseError(String), } -// trace_macros!(true); - -macro_rules! config { - (@step $name:ident - [$($fields:tt)*] - [$($rfields:tt)*] - [$($pfields:tt)*] - { $field:ident : $type:ty, $($rest:tt)* } - ) => { - config!{ @step - $name - [ $($fields)* $field Default::default() ] - [ $($rfields)* pub $field : $type, ] - [ $($pfields)* pub $field : Option<$type>, ] - { $($rest)* } - } - }; - - (@step $name:ident - [$($fields:tt)*] - [$($rfields:tt)*] - [$($pfields:tt)*] - { #[nest] $field:ident : $type:ty, $($rest:tt)* } - ) => { - config!{ @step - $name - [ $($fields)* $field Default::default() ] - [ $($rfields)* pub $field : $type, ] - [ $($pfields)* pub $field : Option<[]>, ] - { $($rest)* } - } - }; - - (@step $name:ident - [$($fields:tt)*] - [$($rfields:tt)*] - [$($pfields:tt)*] - { $field:ident : $type:ty = $def:expr, $($rest:tt)* } - ) => { - config!{ @step - $name - [ $($fields)* $field ($def) ] - [ $($rfields)* pub $field : $type, ] - [ $($pfields)* pub $field : Option<$type>, ] - { $($rest)* } - } - }; - - (@step $name:ident - [$($fields:ident $def_vals:expr)*] - [$($rfields:tt)*] - [$($pfields:tt)*] - { } - ) => { - pub struct $name { - $($rfields)* - } - - impl ::std::default::Default for $name { - fn default() -> Self { - Self { - $( - $fields : $def_vals, - )* - } - } - } +fn load_config_from_file(path: &str) -> Result { + todo!() - paste! { - pub struct [] { - $($pfields)* - } - - impl Mergeable<[]> for $name { - fn merge(&mut self, other: &[]) { - $( - if let Some(value) = &other.$fields { - self.$fields.merge(value); - } - )* - } - } - } - }; - - ($(pub struct $name:ident { $($body:tt)* })*) => { - $(config!{ @step $name [] [] [] { $($body)* } })* - }; -} - -macro_rules! impl_merge_prims { - ($($t:ty)*) => { - $(impl Mergeable<$t> for $t where $t: Clone { - fn merge(&mut self, other: &$t) { - *self = other.clone(); - } - })* - }; } type Rgb = (u8, u8, u8); @@ -157,11 +54,14 @@ impl_merge_prims! { f64 i64 bool - log::Level + Rgb Dims Dims3D + + log::Level CameraMode + UpdateCheckInterval } config! { @@ -200,8 +100,8 @@ config! { } pub struct Updates { - check_interval: bool, - include_prereleases: bool, + check_interval: UpdateCheckInterval, + display_update_check_errors: bool, } pub struct Audio { @@ -210,7 +110,9 @@ config! { enable_music: bool, music_volume: f64, } +} +config! { pub struct Presets { presets: PresetList, } @@ -239,7 +141,8 @@ config! { } } -#[derive(Default, Clone, Copy, Debug)] +#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(tag = "mode")] pub enum CameraMode { #[default] CloseFollow, @@ -249,7 +152,7 @@ pub enum CameraMode { }, } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] pub enum UpdateCheckInterval { Never, #[default] @@ -260,7 +163,7 @@ pub enum UpdateCheckInterval { Always, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct PresetList { presets: Vec, } @@ -271,7 +174,7 @@ impl Mergeable for PresetList { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MazePreset { pub title: String, pub description: Option, @@ -281,11 +184,63 @@ pub struct MazePreset { pub maze_spec: MazeSpec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +// Note: order of variants matters for correct deserialization enum Value { Object(HashMap), List(Vec), - String(String), - Number(f64), Int(i64), + Float(f64), Bool(bool), + String(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_value_deserialize() { + let json_data = r#" + { + "name": "Example", + "enabled": true, + "threshold": 10.5, + "count": 42, + "items": [1, 2, 3], + "settings": { + "option1": "value1", + "option2": false + } + } + "#; + + let parsed: Value = serde_json::from_str(json_data).unwrap(); + + if let Value::Object(map) = parsed { + assert_eq!(map.get("name"), Some(&Value::String("Example".to_string()))); + assert_eq!(map.get("enabled"), Some(&Value::Bool(true))); + assert_eq!(map.get("threshold"), Some(&Value::Float(10.5))); + assert_eq!(map.get("count"), Some(&Value::Int(42))); + + if let Some(Value::List(items)) = map.get("items") { + assert_eq!(items.len(), 3); + assert_eq!(items[0], Value::Int(1)); + assert_eq!(items[1], Value::Int(2)); + assert_eq!(items[2], Value::Int(3)); + } else { + panic!("Expected 'items' to be a list"); + } + + if let Some(Value::Object(settings)) = map.get("settings") { + assert_eq!(settings.get("option1"), Some(&Value::String("value1".to_string()))); + assert_eq!(settings.get("option2"), Some(&Value::Bool(false))); + } else { + panic!("Expected 'settings' to be an object"); + } + } else { + panic!("Expected top-level value to be an object"); + } + } } From e1fb37f699c86a4450d9f2054544e360f73c8eeb Mon Sep 17 00:00:00 2001 From: ur-fault Date: Thu, 29 Jan 2026 15:15:42 +0100 Subject: [PATCH 03/23] Update: sep to model.rs, Begin: loading settings --- tmaze/src/helpers/mod.rs | 16 ++ tmaze/src/settings/config_utils.rs | 24 +++ tmaze/src/settings/mod.rs | 1 + tmaze/src/settings/model.rs | 150 ++++++++++++++++ tmaze/src/settings/new_settings.rs | 279 ++++++++++++----------------- 5 files changed, 310 insertions(+), 160 deletions(-) create mode 100644 tmaze/src/settings/model.rs diff --git a/tmaze/src/helpers/mod.rs b/tmaze/src/helpers/mod.rs index 604b330..9bc65c7 100644 --- a/tmaze/src/helpers/mod.rs +++ b/tmaze/src/helpers/mod.rs @@ -210,3 +210,19 @@ macro_rules! make_even { } }; } + +pub trait TupleMap { + fn map_first T2>(self, f: F) -> (T2, U); + + fn map_second U2>(self, f: F) -> (T, U2); +} + +impl TupleMap for (T, U) { + fn map_first T2>(self, f: F) -> (T2, U) { + (f(self.0), self.1) + } + + fn map_second U2>(self, f: F) -> (T, U2) { + (self.0, f(self.1)) + } +} diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index 033686e..bc08122 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -85,6 +85,30 @@ macro_rules! config { )* } } + + impl From<&[]> for $name { + fn from(partial: &[]) -> Self { + let mut config = Self::default(); + config.merge(partial); + config + } + } + + impl TryFrom for [] { + type Error = (String, Self); + + fn try_from(value: Value) -> Result { + match value { + Value::Object(map) => { + let json_value = serde_json::to_value(map) + .expect("Failed to convert map to JSON value"); // should not happen + serde_json::from_value(json_value) + .map_err(|e| (e.to_string(), Self::default())) + } + _ => Err(("Expected an object for partial config".to_string(), Self::default())), + } + } + } } }; diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index c6e5fc1..5a24361 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -4,6 +4,7 @@ pub mod theme; pub mod new_settings; pub mod config_utils; +pub mod model; use cmaze::{ algorithms::{MazeSpec, MazeSpecType}, diff --git a/tmaze/src/settings/model.rs b/tmaze/src/settings/model.rs new file mode 100644 index 0000000..1294f8e --- /dev/null +++ b/tmaze/src/settings/model.rs @@ -0,0 +1,150 @@ +use cmaze::{algorithms::MazeSpec, dims::{Dims, Offset}}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::{config, impl_merge_prims, settings::config_utils::Mergeable}; + +config! { + pub struct Config { + #[nest] general: General, + #[nest] viewport: Viewport, + #[nest] nagivation: Navigation, + #[nest] updates: Updates, + #[nest] audio: Audio, + } + + pub struct General { + theme: String, + logging_level: log::Level = log::Level::Info, + debug_logging_level: log::Level = log::Level::Info, + file_logging_level: log::Level = log::Level::Info, + #[nest] terminal_scheme: TerminalColorScheme, + } + + pub struct Viewport { + slow: bool, + disable_tower_auto_up: bool, + camera_mode: CameraMode, + camera_smoothing: f64 = 0.5, + player_smoothing: f64 = 0.5, + viewport_margin: Dims = Dims(4, 3), + } + + pub struct Navigation { + enable_mouse: bool = true, + enable_dpad: bool, + landscape_dpad_on_left: bool, + dpad_swap_up_down: bool, + enable_margin_around_dpad: bool, + enable_dpad_highlight: bool = true, + } + + pub struct Updates { + check_interval: UpdateCheckInterval, + display_update_check_errors: bool, + } + + pub struct Audio { + enable_audio: bool, + audio_volume: f64, + enable_music: bool, + music_volume: f64, + } +} + +config! { + pub struct Presets { + presets: PresetList, + } + + pub struct TerminalColorScheme { + primary_fg: Rgb, + primary_bg: Rgb, + black: Rgb, // grey + dark_grey: Rgb, // dark grey + red: Rgb, + dark_red: Rgb, + green: Rgb, + dark_green: Rgb, + yellow: Rgb, + dark_yellow: Rgb, + blue: Rgb, + dark_blue: Rgb, + magenta: Rgb, + dark_magenta: Rgb, + cyan: Rgb, + dark_cyan: Rgb, + white: Rgb, + grey: Rgb, + } +} +impl_merge_prims! { + String + f64 + i64 + bool + + Rgb + Dims + + log::Level + CameraMode + UpdateCheckInterval +} + +type Rgb = (u8, u8, u8); + +#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(tag = "mode")] +pub enum CameraMode { + #[default] + CloseFollow, + EdgeFollow { + x: Offset, + y: Offset, + }, +} + +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub enum UpdateCheckInterval { + Never, + #[default] + Daily, + Weekly, + Monthly, + Yearly, + Always, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PresetList { + presets: Vec, +} + +impl Mergeable for PresetList { + fn merge(&mut self, other: &Self) { + self.presets.extend_from_slice(&other.presets); + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MazePreset { + pub title: String, + pub description: Option, + + pub default: bool, + + pub maze_spec: MazeSpec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +// Note: order of variants matters for correct deserialization +pub enum Value { + Object(HashMap), + List(Vec), + Int(i64), + Float(f64), + Bool(bool), + String(String), +} diff --git a/tmaze/src/settings/new_settings.rs b/tmaze/src/settings/new_settings.rs index 8cc180a..1f4b1c6 100644 --- a/tmaze/src/settings/new_settings.rs +++ b/tmaze/src/settings/new_settings.rs @@ -1,24 +1,24 @@ -use cmaze::{ - algorithms::MazeSpec, - dims::{Dims, Dims3D, Offset}, -}; - -use std::sync::Arc; +use std::{fmt::Display, sync::Arc}; use hashbrown::HashMap; -use serde::{Deserialize, Serialize}; -use crate::{config, impl_merge_prims, settings::config_utils::Mergeable}; +use crate::{helpers::TupleMap, settings::model::{Config, PartialConfig, Value}}; struct Settings { inner: Arc, } impl Settings { - fn new() -> Self { - Settings { - inner: Arc::new(SettingsInner::new()), - } + /// Loads settings from configuration files. + /// + /// Returns the loaded settings and a boolean indicating whether any errors/warnings occurred + /// during loading. + /// + /// TODO: Report the actual errors/warnings to the user. + fn load() -> (Self, bool) { + SettingsInner::load().map_first(|inner| Self { + inner: Arc::new(inner), + }) } } @@ -29,171 +29,127 @@ struct SettingsInner { } impl SettingsInner { - fn new() -> Self { - Self { - config_layer: PartialConfig::default(), - base: Config::default(), - } - } -} - -enum ConfigLoadError { - IoError(std::io::Error), - ParseError(String), -} + fn load() -> (Self, bool) { + let mut errored = false; + + let base_config = Config::default(); + let config_layer = match load_config_from_file("config.json5") { + Ok(config) => config, + Err(_err) => { + errored = true; + PartialConfig::default() + } + }; -fn load_config_from_file(path: &str) -> Result { - todo!() + let config = Self { + config_layer, + base: base_config, + }; + (config, errored) + } } -type Rgb = (u8, u8, u8); - -impl_merge_prims! { - String - f64 - i64 - bool - - Rgb - Dims - Dims3D - - log::Level - CameraMode - UpdateCheckInterval +#[derive(Debug, thiserror::Error)] +enum ConfigLoadError { + IoError(#[from] std::io::Error), + JsonError(#[from] json5::Error), + SettingsFormatError(String), } -config! { - pub struct Config { - #[nest] general: General, - #[nest] viewport: Viewport, - #[nest] nagivation: Navigation, - #[nest] updates: Updates, - #[nest] audio: Audio, - } - - pub struct General { - theme: String, - logging_level: log::Level = log::Level::Info, - debug_logging_level: log::Level = log::Level::Info, - file_logging_level: log::Level = log::Level::Info, - #[nest] terminal_scheme: TerminalColorScheme, - } - - pub struct Viewport { - slow: bool, - disable_tower_auto_up: bool, - camera_mode: CameraMode, - camera_smoothing: f64 = 0.5, - player_smoothing: f64 = 0.5, - viewport_margin: Dims = Dims(4, 3), - } - - pub struct Navigation { - enable_mouse: bool = true, - enable_dpad: bool, - landscape_dpad_on_left: bool, - dpad_swap_up_down: bool, - enable_margin_around_dpad: bool, - enable_dpad_highlight: bool = true, - } - - pub struct Updates { - check_interval: UpdateCheckInterval, - display_update_check_errors: bool, - } - - pub struct Audio { - enable_audio: bool, - audio_volume: f64, - enable_music: bool, - music_volume: f64, +impl Display for ConfigLoadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigLoadError::IoError(e) => write!(f, "I/O error: {}", e), + ConfigLoadError::JsonError(e) => write!(f, "Parse error: {}", e), + ConfigLoadError::SettingsFormatError(e) => write!(f, "Settings format error: {}", e), + } } } -config! { - pub struct Presets { - presets: PresetList, +fn load_config_from_file(path: &str) -> Result { + match load_values_from_file(path) { + Ok(value) => PartialConfig::try_from(value) + .map_err(|(e, val)| (ConfigLoadError::SettingsFormatError(e), val)), + Err((e, val)) => Err((e, PartialConfig::try_from(val).unwrap_or_default())), } } -config! { - pub struct TerminalColorScheme { - primary_fg: Rgb, - primary_bg: Rgb, - black: Rgb, // grey - dark_grey: Rgb, // dark grey - red: Rgb, - dark_red: Rgb, - green: Rgb, - dark_green: Rgb, - yellow: Rgb, - dark_yellow: Rgb, - blue: Rgb, - dark_blue: Rgb, - magenta: Rgb, - dark_magenta: Rgb, - cyan: Rgb, - dark_cyan: Rgb, - white: Rgb, - grey: Rgb, +fn load_values_from_file(path: &str) -> Result { + macro_rules! pack_error { + ($err:expr) => { + match $err { + Ok(val) => Ok(val), + Err(e) => Err((e.into(), Value::Object(HashMap::new()))), + } + }; } -} -#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize)] -#[serde(tag = "mode")] -pub enum CameraMode { - #[default] - CloseFollow, - EdgeFollow { - x: Offset, - y: Offset, - }, -} - -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] -pub enum UpdateCheckInterval { - Never, - #[default] - Daily, - Weekly, - Monthly, - Yearly, - Always, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct PresetList { - presets: Vec, -} - -impl Mergeable for PresetList { - fn merge(&mut self, other: &Self) { - self.presets.extend_from_slice(&other.presets); + // json5 doesn't support reading from reader + let content = pack_error!(std::fs::read_to_string(path))?; + let config_value = pack_error!(json5::from_str::(&content))?; + let values_with_extensions = load_extension_blocks(config_value)?; + Ok(values_with_extensions) +} + +mod config_file_constants { + pub const IMPORT_KEY: &str = "#from"; +} + +fn load_extension_blocks(config: Value) -> Result { + use config_file_constants::IMPORT_KEY; + + /// Merges `ext` into `base`. In case of conflict, `ext` takes precedence. + /// Note that in this case, `base` is file behing `#from`, and `ext` is the current file. + /// + /// For objects, merging is done recursively. + /// + /// TODO: Allow rules customization in the future, for example to support list contatenation. + fn merge(base: &mut Value, ext: Value) { + match (base, ext) { + (Value::Object(base_map), Value::Object(ext_map)) => { + for (key, ext_value) in ext_map { + if key == IMPORT_KEY { + continue; // skip #from key during merge + } + + if let Some(base_value) = base_map.get_mut(&key) { + merge(base_value, ext_value); + } else { + base_map.insert(key, ext_value); + } + } + } + (base_val, ext) => { + *base_val = ext; + } + } } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MazePreset { - pub title: String, - pub description: Option, - - pub default: bool, + let value = match config { + Value::Object(map) => { + if map.contains_key(IMPORT_KEY) { + if let Value::String(file_path) = &map[IMPORT_KEY] { + let mut base_config = load_values_from_file(file_path)?; + merge(&mut base_config, Value::Object(map)); + base_config + } else { + return Err(( + ConfigLoadError::SettingsFormatError(format!( + "{IMPORT_KEY} value must be a string", + )), + Value::Object(map), + )); + } + } else { + Value::Object(map) + } + } - pub maze_spec: MazeSpec, -} + val => val, + }; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -// Note: order of variants matters for correct deserialization -enum Value { - Object(HashMap), - List(Vec), - Int(i64), - Float(f64), - Bool(bool), - String(String), + Ok(value) } #[cfg(test)] @@ -234,7 +190,10 @@ mod tests { } if let Some(Value::Object(settings)) = map.get("settings") { - assert_eq!(settings.get("option1"), Some(&Value::String("value1".to_string()))); + assert_eq!( + settings.get("option1"), + Some(&Value::String("value1".to_string())) + ); assert_eq!(settings.get("option2"), Some(&Value::Bool(false))); } else { panic!("Expected 'settings' to be an object"); From ddd1a61debb15aa6adb560b47862f125b4c7ec82 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Thu, 29 Jan 2026 15:24:29 +0100 Subject: [PATCH 04/23] Update: move static files to their dir --- .../settings/{ => files}/default_settings.json5 | 0 .../src/settings/{ => files}/default_settings.ron | 0 .../src/settings/{ => files}/default_theme.json5 | 0 tmaze/src/settings/mod.rs | 2 +- tmaze/src/settings/new_settings.rs | 15 +++++++++------ tmaze/src/settings/theme.rs | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) rename tmaze/src/settings/{ => files}/default_settings.json5 (100%) rename tmaze/src/settings/{ => files}/default_settings.ron (100%) rename tmaze/src/settings/{ => files}/default_theme.json5 (100%) diff --git a/tmaze/src/settings/default_settings.json5 b/tmaze/src/settings/files/default_settings.json5 similarity index 100% rename from tmaze/src/settings/default_settings.json5 rename to tmaze/src/settings/files/default_settings.json5 diff --git a/tmaze/src/settings/default_settings.ron b/tmaze/src/settings/files/default_settings.ron similarity index 100% rename from tmaze/src/settings/default_settings.ron rename to tmaze/src/settings/files/default_settings.ron diff --git a/tmaze/src/settings/default_theme.json5 b/tmaze/src/settings/files/default_theme.json5 similarity index 100% rename from tmaze/src/settings/default_theme.json5 rename to tmaze/src/settings/files/default_theme.json5 diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 5a24361..0ea3fe1 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -31,7 +31,7 @@ use crate::{ #[cfg(feature = "sound")] use crate::sound::create_audio_settings; -const DEFAULT_SETTINGS_JSON: &str = include_str!("./default_settings.json5"); +const DEFAULT_SETTINGS_JSON: &str = include_str!("./files/default_settings.json5"); #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] #[serde(tag = "mode")] diff --git a/tmaze/src/settings/new_settings.rs b/tmaze/src/settings/new_settings.rs index 1f4b1c6..88c73a7 100644 --- a/tmaze/src/settings/new_settings.rs +++ b/tmaze/src/settings/new_settings.rs @@ -1,8 +1,11 @@ -use std::{fmt::Display, sync::Arc}; +use std::{fmt::Display, path::Path, sync::Arc}; use hashbrown::HashMap; -use crate::{helpers::TupleMap, settings::model::{Config, PartialConfig, Value}}; +use crate::{ + helpers::{constants::paths, TupleMap}, + settings::model::{Config, PartialConfig, Value}, +}; struct Settings { inner: Arc, @@ -33,7 +36,7 @@ impl SettingsInner { let mut errored = false; let base_config = Config::default(); - let config_layer = match load_config_from_file("config.json5") { + let config_layer = match load_config_from_file(&paths::settings_path()) { Ok(config) => config, Err(_err) => { errored = true; @@ -67,7 +70,7 @@ impl Display for ConfigLoadError { } } -fn load_config_from_file(path: &str) -> Result { +fn load_config_from_file(path: &Path) -> Result { match load_values_from_file(path) { Ok(value) => PartialConfig::try_from(value) .map_err(|(e, val)| (ConfigLoadError::SettingsFormatError(e), val)), @@ -75,7 +78,7 @@ fn load_config_from_file(path: &str) -> Result Result { +fn load_values_from_file(path: &Path) -> Result { macro_rules! pack_error { ($err:expr) => { match $err { @@ -130,7 +133,7 @@ fn load_extension_blocks(config: Value) -> Result { if map.contains_key(IMPORT_KEY) { if let Value::String(file_path) = &map[IMPORT_KEY] { - let mut base_config = load_values_from_file(file_path)?; + let mut base_config = load_values_from_file(Path::new(file_path))?; merge(&mut base_config, Value::Object(map)); base_config } else { diff --git a/tmaze/src/settings/theme.rs b/tmaze/src/settings/theme.rs index 771db8c..252e84a 100644 --- a/tmaze/src/settings/theme.rs +++ b/tmaze/src/settings/theme.rs @@ -54,7 +54,7 @@ macro_rules! default_theme_name { }; } const DEFAULT_THEME_NAME: &str = default_theme_name!(); -const DEFAULT_THEME: &str = include_str!(concat!("./", default_theme_name!())); +const DEFAULT_THEME: &str = include_str!(concat!("./files/", default_theme_name!())); impl ThemeDefinition { pub fn parse_default() -> Self { From a56825a2222638d5d003306c43014a2bbbfd0033 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Thu, 29 Jan 2026 15:29:15 +0100 Subject: [PATCH 05/23] Update: move style browser to ui::usecase --- tmaze/src/app/game.rs | 3 +-- tmaze/src/settings/mod.rs | 1 - tmaze/src/ui/mod.rs | 8 ++------ tmaze/src/ui/usecase/mod.rs | 7 +++++-- tmaze/src/{settings => ui/usecase}/style_browser.rs | 4 +--- 5 files changed, 9 insertions(+), 14 deletions(-) rename tmaze/src/{settings => ui/usecase}/style_browser.rs (99%) diff --git a/tmaze/src/app/game.rs b/tmaze/src/app/game.rs index e65df97..1de193f 100644 --- a/tmaze/src/app/game.rs +++ b/tmaze/src/app/game.rs @@ -18,7 +18,6 @@ use crate::{ renderer::{draw::Align, CellContent, GBuffer, GMutView, Padding}, settings::{ self, - style_browser::StyleBrowser, theme::{SharedScheme, Theme, ThemeResolver}, CameraMode, MazePreset, Settings, SettingsActivity, }, @@ -26,7 +25,7 @@ use crate::{ self, helpers::format_duration, multisize_duration_format, split_menu_actions, - usecase::dpad::{DPad, DPadType}, + usecase::{dpad::{DPad, DPadType}, style_browser::StyleBrowser}, Menu, MenuAction, MenuConfig, Popup, ProgressBar, Rect, RedirectMenu, Screen, ScreenError, }, }; diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 0ea3fe1..a6a3ced 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -1,5 +1,4 @@ mod attribute; -pub mod style_browser; pub mod theme; pub mod new_settings; diff --git a/tmaze/src/ui/mod.rs b/tmaze/src/ui/mod.rs index 540aa29..fabff22 100644 --- a/tmaze/src/ui/mod.rs +++ b/tmaze/src/ui/mod.rs @@ -2,10 +2,7 @@ pub use std::time::Duration; use crate::{ renderer::GMutView, - settings::{ - style_browser, - theme::{Theme, ThemeResolver}, - }, + settings::theme::{Theme, ThemeResolver}, }; pub mod button; @@ -49,8 +46,7 @@ pub fn theme_resolver() -> ThemeResolver { .extend(popup::popup_theme_resolver()) .extend(progressbar::progressbar_theme_resolver()) .extend(rect::rect_theme_resolver()) - .extend(usecase::usedcase_ui_theme_resolver()) - .extend(style_browser::style_browser_theme_resolver()); + .extend(usecase::usecase_ui_theme_resolver()); resolver } diff --git a/tmaze/src/ui/usecase/mod.rs b/tmaze/src/ui/usecase/mod.rs index a2fbca9..32ba851 100644 --- a/tmaze/src/ui/usecase/mod.rs +++ b/tmaze/src/ui/usecase/mod.rs @@ -3,10 +3,13 @@ use dpad::dpad_theme_resolver; use crate::settings::theme::ThemeResolver; pub mod dpad; +pub mod style_browser; -pub fn usedcase_ui_theme_resolver() -> ThemeResolver { +pub fn usecase_ui_theme_resolver() -> ThemeResolver { let mut resolver = ThemeResolver::new(); - resolver.extend(dpad_theme_resolver()); + resolver + .extend(dpad_theme_resolver()) + .extend(style_browser::style_browser_theme_resolver()); resolver } diff --git a/tmaze/src/settings/style_browser.rs b/tmaze/src/ui/usecase/style_browser.rs similarity index 99% rename from tmaze/src/settings/style_browser.rs rename to tmaze/src/ui/usecase/style_browser.rs index a2e42b7..bc6c8a9 100644 --- a/tmaze/src/settings/style_browser.rs +++ b/tmaze/src/ui/usecase/style_browser.rs @@ -10,12 +10,10 @@ use crate::{ app::{app::AppData, ActivityHandler, Change, Event}, helpers::not_release, renderer::{CellContent, GMutView, Padding}, - settings::theme::Style, + settings::theme::{Style, StyleNode, Theme, ThemeResolver}, ui::{CapsuleText, Screen, ScreenError}, }; -use super::theme::{StyleNode, Theme, ThemeResolver}; - const CONTENT_MARGIN: Dims = Dims(4, 1); const LEFT_MARGIN: i32 = 1; const RIGHT_MARGIN: i32 = 1; From d393245edd4562d1f8908fd6497b17d616c2537a Mon Sep 17 00:00:00 2001 From: ur-fault Date: Fri, 30 Jan 2026 15:31:03 +0100 Subject: [PATCH 06/23] Update: move stuff, Add: integrate new config, Fix: crash on empty presets --- cmaze/src/dims.rs | 12 + tmaze/src/app/app.rs | 118 ++- tmaze/src/app/game.rs | 102 ++- tmaze/src/app/game_state.rs | 10 +- tmaze/src/data/mod.rs | 9 +- tmaze/src/helpers/strings.rs | 4 +- tmaze/src/logging.rs | 9 +- tmaze/src/main.rs | 25 +- tmaze/src/renderer/mod.rs | 9 +- tmaze/src/settings/config_utils.rs | 28 +- tmaze/src/settings/mod.rs | 756 ++++-------------- tmaze/src/settings/model.rs | 96 ++- tmaze/src/settings/new_settings.rs | 208 ----- tmaze/src/settings/old_settings.rs | 469 +++++++++++ tmaze/src/settings/theme.rs | 78 +- tmaze/src/sound/mod.rs | 28 +- tmaze/src/ui/menu.rs | 1 + tmaze/src/ui/usecase/dpad.rs | 10 +- tmaze/src/ui/usecase/mod.rs | 4 +- tmaze/src/ui/usecase/screens/mod.rs | 2 + tmaze/src/ui/usecase/screens/settings.rs | 157 ++++ .../ui/usecase/{ => screens}/style_browser.rs | 0 tmaze/src/updates.rs | 9 +- 23 files changed, 1134 insertions(+), 1010 deletions(-) delete mode 100644 tmaze/src/settings/new_settings.rs create mode 100644 tmaze/src/settings/old_settings.rs create mode 100644 tmaze/src/ui/usecase/screens/mod.rs create mode 100644 tmaze/src/ui/usecase/screens/settings.rs rename tmaze/src/ui/usecase/{ => screens}/style_browser.rs (100%) diff --git a/cmaze/src/dims.rs b/cmaze/src/dims.rs index aa6f58f..9952cc5 100644 --- a/cmaze/src/dims.rs +++ b/cmaze/src/dims.rs @@ -190,6 +190,18 @@ impl Mul for Dims3D { } } +impl Mul for Dims3D { + type Output = Dims3D; + + fn mul(self, other: f64) -> Dims3D { + Dims3D( + (self.0 as f64 * other).round() as i32, + (self.1 as f64 * other).round() as i32, + (self.2 as f64 * other).round() as i32, + ) + } +} + impl MulAssign for Dims3D { fn mul_assign(&mut self, other: i32) { self.0 *= other; diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index ef964dc..1c72d86 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -1,4 +1,5 @@ use std::{ + rc::Rc, sync::Arc, time::{Duration, Instant}, }; @@ -16,11 +17,12 @@ use crossterm::event::{read, KeyCode, KeyEvent, KeyEventKind}; use crate::{ data::SaveData, - helpers::{constants::paths::settings_path, on_off}, + helpers::on_off, logging::{self, AppLogger, LoggerOptions, UiLogs}, renderer::{self, draw::Draw, CellContent, GMutView, Renderer}, settings::{ - theme::{Theme, ThemeResolver}, + model::Config, + theme::{SharedScheme, TerminalColorScheme, Theme, ThemeDefinition, ThemeResolver}, Settings, }, ui, @@ -50,13 +52,14 @@ pub struct AppData { pub settings: Settings, pub save: SaveData, pub use_data: AppStateData, + pub appearance: Appearance, pub screen_size: Dims, - pub theme: Theme, - pub theme_resolver: ThemeResolver, pub logs: UiLogs, pub registries: Registries, jobs: Jobs, + app_start: Instant, + read_only: bool, #[cfg(feature = "sound")] pub sound_player: SoundPlayer, @@ -77,11 +80,13 @@ impl AppData { } } - let volume = if self.settings.get_enable_audio() && self.settings.get_enable_music() { - self.settings.get_audio_volume() * self.settings.get_music_volume() + let cfg = &self.settings.read().audio; + let volume = if cfg.enable_audio && cfg.enable_music { + cfg.audio_volume * cfg.music_volume } else { 0.0 - }; + } as f32; + self.sound_player.set_volume(volume); self.bgm_track = Some(track); @@ -92,6 +97,10 @@ impl AppData { pub fn queuer(&self) -> Qer { self.jobs.queuer() } + + pub fn is_ro(&self) -> bool { + self.read_only + } } pub struct Registries { @@ -126,22 +135,18 @@ impl App { /// - initializes the job queue, /// - initializes the registries, pub fn empty(read_only: bool) -> Self { - let settings = - Settings::load_json(settings_path(), read_only).expect("failed to load settings"); - - let renderer = Renderer::new( - &settings - .get_terminal_scheme() - .expect("unknown built-in terminal color scheme, use '--print-terminal-schemes' to see options"), - ) - .expect("failed to create renderer"); + let (settings, settings_error) = Settings::load(); + let config = settings.read(); + + let renderer = Renderer::new(&Rc::new(config.general.terminal_scheme.clone())) + .expect("failed to create renderer"); let activities = Activities::empty(); let (logger, logs) = AppLogger::new_with_options( - settings.get_logging_level(), + config.general.logging_level, LoggerOptions::default() .read_only(read_only) - .file_level(settings.get_file_logging_level()), + .file_level(config.general.file_logging_level), ); logger.init(); @@ -164,13 +169,12 @@ impl App { }; log::info!("Loading theme"); - let resolver = init_theme_resolver(); - let theme_def = settings.get_theme(); - let theme = resolver.resolve(&theme_def); #[cfg(feature = "sound")] let sound_player = SoundPlayer::new(settings.clone()); + let appereance = Appearance::new(&config); + Self { renderer, activities, @@ -179,12 +183,12 @@ impl App { settings, save, use_data, + appearance: appereance, screen_size: frame_size, jobs, - theme, - theme_resolver: resolver, logs, registries, + read_only, #[cfg(feature = "sound")] sound_player, @@ -219,7 +223,7 @@ impl App { .. }) => self.switch_debug(), event @ crossterm::event::Event::Mouse(_) => { - if self.data.settings.get_enable_mouse() { + if self.data.settings.read().nagivation.enable_mouse { events.push(Event::Term(event)); } } @@ -265,29 +269,28 @@ impl App { } } + let theme = &self.data.appearance.theme; self.renderer .frame() .mut_view() - .fill(CellContent::styled(' ', self.data.theme.get("background"))); + .fill(CellContent::styled(' ', theme.get("background"))); match self .activities .active_mut() .expect("No active active") .screen() - .draw(&mut self.renderer.frame().mut_view(), &self.data.theme) + .draw(&mut self.renderer.frame().mut_view(), &theme) { Ok(_) => {} Err(ui::ScreenError::SmallScreen) => { - draw_small_screen_info(&mut self.renderer.frame().mut_view(), &self.data.theme) + draw_small_screen_info(&mut self.renderer.frame().mut_view(), &theme) } } - self.data.logs.draw_on( - Dims(0, 0), - &mut self.renderer.frame().mut_view(), - &self.data.theme, - ); + self.data + .logs + .draw_on(Dims(0, 0), &mut self.renderer.frame().mut_view(), &theme); // TODO: let activities show debug info and about the app itself // then we can draw it here @@ -305,7 +308,7 @@ impl App { fn switch_debug(&mut self) { self.data.use_data.show_debug = !self.data.use_data.show_debug; - self.data.logs.switch_debug(&self.data.settings); + self.data.logs.switch_debug(self.data.settings.read()); log::warn!( "Debug mode: {}", on_off(self.data.use_data.show_debug, false) @@ -343,6 +346,55 @@ pub struct AppStateData { pub show_debug: bool, } +pub struct Appearance { + theme: Theme, + scheme: SharedScheme, + resolver: ThemeResolver, +} + +impl Appearance { + pub fn new(config: &Config) -> Self { + let resolver = init_theme_resolver(); + + Self { + theme: Self::load_theme(config, &resolver), + scheme: Self::load_scheme(config), + resolver, + } + } + + pub fn theme(&self) -> &Theme { + &self.theme + } + + pub fn scheme(&self) -> &Rc { + &self.scheme + } + + pub fn resolver(&self) -> &ThemeResolver { + &self.resolver + } +} + +impl Appearance { + fn load_theme(config: &Config, resolver: &ThemeResolver) -> Theme { + match config.general.theme.as_str() { + "" => resolver.resolve(&ThemeDefinition::parse_default()), + name => match ThemeDefinition::load_by_name(name) { + Ok(def) => resolver.resolve(&def), + Err(err) => { + log::error!("Failed to load theme '{}': {}", name, err); + resolver.resolve(&ThemeDefinition::parse_default()) + } + }, + } + } + + fn load_scheme(config: &Config) -> SharedScheme { + Rc::new(config.general.terminal_scheme.clone()) + } +} + pub fn init_theme_resolver() -> ThemeResolver { let mut resolver = ThemeResolver::new(); diff --git a/tmaze/src/app/game.rs b/tmaze/src/app/game.rs index 1de193f..1e806b8 100644 --- a/tmaze/src/app/game.rs +++ b/tmaze/src/app/game.rs @@ -10,22 +10,25 @@ use cmaze::{ }; use crate::{ - app::{game_state::GameData, GameViewMode}, + app::{self, game_state::GameData, GameViewMode}, helpers::{ constants, is_release, maze2screen, maze2screen_3d, maze_render_size, strings, LineDir, }, lerp, menu_actions, renderer::{draw::Align, CellContent, GBuffer, GMutView, Padding}, settings::{ - self, + model::{CameraMode, Config, MazePreset, Viewport}, theme::{SharedScheme, Theme, ThemeResolver}, - CameraMode, MazePreset, Settings, SettingsActivity, }, ui::{ self, helpers::format_duration, multisize_duration_format, split_menu_actions, - usecase::{dpad::{DPad, DPadType}, style_browser::StyleBrowser}, + usecase::{ + dpad::{DPad, DPadType}, + settings::SettingsActivity, + style_browser::StyleBrowser, + }, Menu, MenuAction, MenuConfig, Popup, ProgressBar, Rect, RedirectMenu, Screen, ScreenError, }, }; @@ -79,7 +82,7 @@ pub struct MainMenu { impl MainMenu { pub fn new() -> Self { let options = menu_actions!( - "New Game" -> data => Self::start_new_game(&data.settings, &data.use_data), + "New Game" -> data => Self::start_new_game(data.settings.read(), &data.use_data), "Settings" -> _ => Self::show_settings_screen(), "Controls" -> _ => Self::show_controls_popup(), "Info" -> _ => Self::show_info_menu(), @@ -98,7 +101,7 @@ impl MainMenu { fn show_settings_screen() -> Change { Change::push(Activity::new_base_boxed( "settings".to_string(), - settings::SettingsActivity::new(), + SettingsActivity::new(), )) } @@ -164,7 +167,7 @@ impl MainMenu { "Style options browser" -> data => Change::Push( Activity::new_base_boxed( "style options browser".to_string(), - StyleBrowser::new(data.theme_resolver.clone()) + StyleBrowser::new(data.appearance.resolver().clone()) ) ), "Back" -> _ => Change::pop_top(), @@ -181,11 +184,20 @@ impl MainMenu { ) } - fn start_new_game(settings: &Settings, use_data: &AppStateData) -> Change { - Change::push(Activity::new_base_boxed( - "maze size", - MazePresetMenu::new(settings, use_data), - )) + fn start_new_game(settings: &Config, use_data: &AppStateData) -> Change { + match MazePresetMenu::new(settings, use_data) { + Some(preset_menu) => Change::push(Activity::new_base_boxed("maze preset", preset_menu)), + None => { + // TODO: reference settings once ready + const MSG: &str = "No maze presets available, please add some in config"; + log::warn!("{}", MSG); + + Change::push(Activity::new_base_boxed( + "no presets", + Popup::new("No presets".to_string(), vec![MSG.to_string()]), + )) + } + } } #[cfg(feature = "sound")] @@ -224,11 +236,15 @@ pub struct MazePresetMenu { } impl MazePresetMenu { - pub fn new(settings: &Settings, app_state_data: &AppStateData) -> Self { + pub fn new(config: &Config, app_state_data: &AppStateData) -> Option { + if config.presets.is_empty() { + return None; + } + let mut menu_config = MenuConfig::new_from_strings( - "Maze size".to_string(), - settings - .get_presets() + "Maze preset".to_string(), + config + .presets .iter() .map(|maze| maze.title.clone()) .collect::>(), @@ -236,7 +252,7 @@ impl MazePresetMenu { let default = app_state_data .last_selected_preset - .or_else(|| settings.get_presets().iter().position(|maze| maze.default)); + .or_else(|| config.presets.iter().position(|maze| maze.default)); if let Some(i) = default { menu_config = menu_config.default(i); @@ -244,9 +260,9 @@ impl MazePresetMenu { let menu = Menu::new(menu_config); - let presets = settings.get_presets().to_vec(); + let presets = config.presets.to_vec(); - Self { menu, presets } + Some(Self { menu, presets }) } } @@ -509,17 +525,12 @@ pub struct GameActivity { impl GameActivity { pub fn new(game: GameData, app_data: &mut AppData) -> Self { - let settings = &app_data.settings; - - let camera_mode = settings.get_camera_mode(); - let maze_board = MazeBoard::new( - &game.game, - &app_data.theme, - settings - .get_terminal_scheme() - .expect("invalid built-in terminal color scheme"), - ); - let margins = settings.get_viewport_margin(); + let config = app_data.settings.read(); + let appear = &app_data.appearance; + + let camera_mode = config.viewport.camera_mode; + let maze_board = MazeBoard::new(&game.game, appear.theme(), appear.scheme().clone()); + let margins = config.viewport.viewport_margin; #[cfg(feature = "sound")] app_data.play_bgm(MusicTrack::choose_for_maze(game.game.get_maze())); @@ -643,10 +654,11 @@ impl GameActivity { } fn update_viewport(&mut self, data: &AppData) { + let cfg = data.settings.read(); if self.is_dpad_enabled() { let (viewport_rect, dpad_rect) = DPad::split_screen(data); let mut dpad_rect = dpad_rect; - if data.settings.get_enable_margin_around_dpad() { + if cfg.nagivation.enable_margin_around_dpad { dpad_rect = dpad_rect.margin(self.margins); } @@ -664,17 +676,16 @@ impl GameActivity { fn init_dpad(&mut self, data: &AppData) { let dpad_type = DPadType::from_maze(self.data.game.get_maze()); - let swap_up_down = data.settings.get_dpad_swap_up_down(); + let swap_up_down = data.settings.read().nagivation.dpad_swap_up_down; let touch_controls = DPad::new(None, swap_up_down, dpad_type); self.touch_controls = Some(Box::new(touch_controls)); } fn update_dpad(&mut self, data: &AppData) { - if (data.settings.get_enable_dpad() && data.settings.get_enable_mouse()) - != self.is_dpad_enabled() - { - if data.settings.get_enable_dpad() { + let config = &data.settings.read().nagivation; + if (config.enable_dpad && config.enable_mouse) != self.is_dpad_enabled() { + if config.enable_dpad { log::info!("Enabling dpad"); self.init_dpad(data); } else { @@ -686,8 +697,8 @@ impl GameActivity { if self.is_dpad_enabled() { let dpad = self.touch_controls.as_mut().unwrap(); - dpad.swap_up_down = data.settings.get_dpad_swap_up_down(); - dpad.disable_highlight(!data.settings.get_enable_dpad_highlight()); + dpad.swap_up_down = config.dpad_swap_up_down; + dpad.disable_highlight(!config.enable_dpad_highlight); } } @@ -719,7 +730,7 @@ impl ActivityHandler for GameActivity { match event { Event::Term(event) => match event { TermEvent::Key(key_event) => { - match self.data.handle_event(&data.settings, key_event) { + match self.data.handle_event(data.settings.read(), key_event) { Err(false) => { self.data.game.pause().unwrap(); @@ -735,7 +746,7 @@ impl ActivityHandler for GameActivity { TermEvent::Mouse(event) => { if let Some(ref mut touch_controls) = self.touch_controls { if let Some(dir) = touch_controls.apply_mouse_event(event) { - self.data.apply_move(&data.settings, dir, false); + self.data.apply_move(data.settings.read(), dir, false); } } } @@ -785,8 +796,15 @@ impl ActivityHandler for GameActivity { } } - self.sm_player_pos = lerp!((self.sm_player_pos) -> (maze2screen_3d(self.data.game.get_player_pos())) at data.settings.get_player_smoothing()); - self.sm_camera_pos = lerp!((self.sm_camera_pos) -> (self.data.camera_pos) at data.settings.get_camera_smoothing()); + let Viewport { + camera_smoothing, + player_smoothing, + .. + } = data.settings.read().viewport; + + self.sm_player_pos = lerp!((self.sm_player_pos) -> (maze2screen_3d(self.data.game.get_player_pos())) at player_smoothing); + self.sm_camera_pos = + lerp!((self.sm_camera_pos) -> (self.data.camera_pos) at camera_smoothing); self.show_debug = data.use_data.show_debug; diff --git a/tmaze/src/app/game_state.rs b/tmaze/src/app/game_state.rs index 3ebd251..4207830 100644 --- a/tmaze/src/app/game_state.rs +++ b/tmaze/src/app/game_state.rs @@ -9,7 +9,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::{ helpers::{is_release, maze2screen_3d}, - settings::{MazePreset, Settings}, + settings::model::{Config, MazePreset}, }; #[derive(PartialEq, Eq, Clone, Copy)] @@ -45,7 +45,7 @@ pub struct GameData { } impl GameData { - pub fn handle_event(&mut self, settings: &Settings, event: KeyEvent) -> Result<(), bool> { + pub fn handle_event(&mut self, settings: &Config, event: KeyEvent) -> Result<(), bool> { let KeyEvent { code, modifiers, @@ -103,7 +103,7 @@ impl GameData { Ok(()) } - pub fn apply_move(&mut self, settings: &Settings, wall: CellWall, fast: bool) { + pub fn apply_move(&mut self, settings: &Config, wall: CellWall, fast: bool) { match self.view_mode { GameViewMode::Spectator => { let mut off = wall.reverse_wall().to_coord(); @@ -122,14 +122,14 @@ impl GameData { self.game .move_player( wall, - if settings.get_slow() { + if settings.viewport.slow { MoveMode::Slow } else if fast { MoveMode::Fast } else { MoveMode::Normal }, - !settings.get_disable_tower_auto_up(), + !settings.viewport.disable_tower_auto_up, ) .unwrap(); } diff --git a/tmaze/src/data/mod.rs b/tmaze/src/data/mod.rs index ab71cb3..ab5f05e 100644 --- a/tmaze/src/data/mod.rs +++ b/tmaze/src/data/mod.rs @@ -8,10 +8,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{ - helpers::constants::paths::save_data_path, - settings::{Settings, UpdateCheckInterval}, -}; +use crate::{helpers::constants::paths::save_data_path, settings::model::{Config, UpdateCheckInterval}}; pub mod model { use cmaze::{algorithms::MazeType, dims::Dims3D}; @@ -105,10 +102,10 @@ impl SaveData { } impl SaveData { - pub fn is_update_checked(&self, settings: &Settings) -> bool { + pub fn is_update_checked(&self, settings: &Config) -> bool { use UpdateCheckInterval::*; - match settings.get_check_interval() { + match settings.updates.check_interval { Never => true, Daily => self.check_date(|d| d), Weekly => self.check_date(|d| d.iso_week()), diff --git a/tmaze/src/helpers/strings.rs b/tmaze/src/helpers/strings.rs index 71b75a8..8a084ef 100644 --- a/tmaze/src/helpers/strings.rs +++ b/tmaze/src/helpers/strings.rs @@ -10,9 +10,7 @@ use substring::Substring; use unicode_width::UnicodeWidthStr as _; use crate::{ - renderer::{draw::Draw, GMutView}, - settings::theme::Style, - ui::draw_str, + renderer::{draw::Draw, GMutView}, settings::theme::Style, ui::draw_str }; pub fn trim_center(text: &str, width: usize) -> &str { diff --git a/tmaze/src/logging.rs b/tmaze/src/logging.rs index 0fb06e2..1281d34 100644 --- a/tmaze/src/logging.rs +++ b/tmaze/src/logging.rs @@ -15,6 +15,7 @@ use crate::{ helpers::constants::paths, renderer::{draw::Draw, GMutView}, settings::{ + model::Config, theme::{Color, NamedColor, Style, Theme}, Settings, }, @@ -108,14 +109,16 @@ impl UiLogs { } } - pub fn switch_debug(&self, settings: &Settings) { + pub fn switch_debug(&self, settings: &Config) { let mut debug = self.debug.write().unwrap(); *debug = !*debug; + let config = &settings.general; + if *debug { - *self.min_level.write().unwrap() = settings.get_debug_logging_level(); + *self.min_level.write().unwrap() = config.debug_logging_level; } else { - *self.min_level.write().unwrap() = settings.get_logging_level(); + *self.min_level.write().unwrap() = config.logging_level; } } diff --git a/tmaze/src/main.rs b/tmaze/src/main.rs index f9d9d1d..5cfd501 100644 --- a/tmaze/src/main.rs +++ b/tmaze/src/main.rs @@ -1,7 +1,7 @@ use tmaze::{ app::{app::init_theme_resolver, game::MainMenu, Activity, App, GameError}, helpers::constants::paths::{save_data_path, settings_path}, - settings::Settings, + settings::{theme::TerminalColorScheme, Settings}, }; #[cfg(feature = "updates")] @@ -12,8 +12,9 @@ use clap::{Parser, ValueEnum}; #[derive(Parser, Debug)] #[clap(version, author, about, name = "tmaze")] struct Args { - #[clap(long, action, help = "Reset config to default and quit")] - reset_config: bool, + // FIXME: This should reset UI config, not the user config + // #[clap(long, action, help = "Reset config to default and quit")] + // reset_config: bool, #[clap(short, long, action, help = "Show config path and quit")] show_config_path: bool, #[clap(long, help = "Show config in debug format and quit")] @@ -59,10 +60,10 @@ enum StylesPrintMode { fn main() -> Result<(), GameError> { let _args = Args::parse(); - if _args.reset_config { - Settings::reset_json_config(settings_path()); - return Ok(()); - } + // if _args.reset_config { + // Settings::reset_json_config(settings_path()); + // return Ok(()); + // } if _args.show_config_path { let settings_path = settings_path(); @@ -75,7 +76,13 @@ fn main() -> Result<(), GameError> { } if _args.debug_config { - println!("{:#?}", Settings::load_json(settings_path(), true)?.read()); + let (config, errd) = Settings::load(); + if errd { + eprintln!("Warning: Errors were encountered while loading the config."); + } + + println!("{:#?}", config.read()); + return Ok(()); } @@ -146,7 +153,7 @@ fn print_style_options(mode: StylesPrintMode, counted: bool) { fn print_builtin_terminal_schemes() { println!("Built-in terminal color schemes:"); - for name in tmaze::settings::theme::TerminalColorScheme::all_schemes() { + for name in TerminalColorScheme::all_schemes() { println!("- {}", name); } println!("Credit to https://github.com/alacritty/alacritty-theme"); diff --git a/tmaze/src/renderer/mod.rs b/tmaze/src/renderer/mod.rs index 3ed8bb1..3298f59 100644 --- a/tmaze/src/renderer/mod.rs +++ b/tmaze/src/renderer/mod.rs @@ -299,7 +299,7 @@ impl Cell { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct GBuffer(Array3D, SharedScheme); impl GBuffer { @@ -369,6 +369,13 @@ impl GBuffer { } } +impl PartialEq for GBuffer { + fn eq(&self, other: &Self) -> bool { + // Note: we purposely ignore the scheme + self.0 == other.0 + } +} + #[derive(Clone, Copy, Debug)] pub struct GView<'a> { buf: &'a GBuffer, diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index bc08122..da74e63 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -1,3 +1,6 @@ +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; + pub trait Mergeable { fn merge(&mut self, other: &O); } @@ -56,6 +59,7 @@ macro_rules! config { [$($pfields:tt)*] { } ) => { + #[derive(Clone, Debug)] pub struct $name { $($rfields)* } @@ -76,7 +80,7 @@ macro_rules! config { $($pfields)* } - impl Mergeable<[]> for $name { + impl $crate::settings::config_utils::Mergeable<[]> for $name { fn merge(&mut self, other: &[]) { $( if let Some(value) = &other.$fields { @@ -94,15 +98,15 @@ macro_rules! config { } } - impl TryFrom for [] { + impl TryFrom<$crate::settings::config_utils::Value> for [] { type Error = (String, Self); - fn try_from(value: Value) -> Result { + fn try_from(value: $crate::settings::config_utils::Value) -> Result { match value { - Value::Object(map) => { - let json_value = serde_json::to_value(map) + $crate::settings::config_utils::Value::Object(map) => { + let json_value = ::serde_json::to_value(map) .expect("Failed to convert map to JSON value"); // should not happen - serde_json::from_value(json_value) + ::serde_json::from_value(json_value) .map_err(|e| (e.to_string(), Self::default())) } _ => Err(("Expected an object for partial config".to_string(), Self::default())), @@ -127,3 +131,15 @@ macro_rules! impl_merge_prims { })* }; } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +// Note: order of variants matters for correct deserialization +pub enum Value { + Object(HashMap), + List(Vec), + Int(i64), + Float(f64), + Bool(bool), + String(String), +} diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index a6a3ced..924cf21 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -1,629 +1,223 @@ -mod attribute; -pub mod theme; - -pub mod new_settings; -pub mod config_utils; +pub mod attribute; pub mod model; +pub mod theme; -use cmaze::{ - algorithms::{MazeSpec, MazeSpecType}, - dims::{Dims, Offset}, -}; -use derivative::Derivative; -use serde::{Deserialize, Serialize}; -use std::{ - fs, io, - path::PathBuf, - sync::{Arc, RwLock}, -}; -use theme::ThemeDefinition; - -use crate::{ - app::{self, app::AppData, Activity, ActivityHandler, Change}, - helpers::constants::paths::settings_path, - menu_actions, - renderer::MouseGuard, - settings::theme::{SharedScheme, TerminalColorScheme}, - ui::{split_menu_actions, Menu, MenuAction, MenuConfig, MenuItem, OptionDef, Popup, Screen}, -}; - -#[cfg(feature = "sound")] -use crate::sound::create_audio_settings; - -const DEFAULT_SETTINGS_JSON: &str = include_str!("./files/default_settings.json5"); - -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] -#[serde(tag = "mode")] -pub enum CameraMode { - #[default] - CloseFollow, - EdgeFollow { - x: Offset, - y: Offset, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MazePreset { - pub title: String, - pub description: Option, - - #[serde(default)] - pub default: bool, - - // TODO: make `serde(flatten)` once switched to TOML/JSON - #[serde(flatten)] - pub maze_spec: MazeSpec, -} +// mod old_settings; -impl MazePreset { - pub fn short_desc(&self) -> Option { - let (size, cells): (_, usize) = match &self.maze_spec.inner_spec { - MazeSpecType::Regions { regions, .. } => ( - self.maze_spec.size()?, - regions.iter().map(|r| r.mask.enabled_count()).sum(), - ), - MazeSpecType::Simple { mask, .. } => ( - self.maze_spec.size()?, - mask.as_ref() - .map(|m| m.enabled_count()) - .unwrap_or(self.maze_spec.size()?.product() as usize), - ), - }; +mod config_utils; - if size.2 == 1 { - Some(format!( - "{}: {}x{} ({} cells)", - self.title, size.0, size.1, cells - )) - } else { - Some(format!( - "{}: {}x{}x{} ({} cells)", - self.title, size.0, size.1, size.2, cells - )) - } - } -} +use std::{fmt::Display, path::Path, sync::Arc}; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] -pub enum UpdateCheckInterval { - Never, - #[default] - Daily, - Weekly, - Monthly, - Yearly, - Always, -} +use hashbrown::HashMap; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum TerminalSchemeDef { - Named(String), - Custom(TerminalColorScheme), -} +use crate::{ + helpers::{constants::paths, TupleMap}, + settings::config_utils::Value, +}; -#[derive(Debug, Derivative, Serialize, Deserialize)] -#[derivative(Default)] -#[serde(rename = "Settings")] -// FIXME: separate sections into their own struct -pub struct SettingsInner { - // general - #[serde(default)] - pub theme: Option, - #[serde(default)] - pub logging_level: Option, - #[serde(default)] - pub debug_logging_level: Option, - #[serde(default)] - pub file_logging_level: Option, - #[serde(default)] - pub terminal_scheme: Option, - - // viewport - #[serde(default)] - pub slow: Option, - #[serde(default)] - pub disable_tower_auto_up: Option, - #[serde(default)] - pub camera_mode: Option, - #[serde(default)] - pub camera_smoothing: Option, - #[serde[default]] - pub player_smoothing: Option, - #[serde(default)] - pub viewport_margin: Option<(i32, i32)>, - - // navigation - #[serde(default)] - pub enable_mouse: Option, - #[serde(default)] - pub enable_dpad: Option, - #[serde(default)] - pub landscape_dpad_on_left: Option, - #[serde(default)] - pub dpad_swap_up_down: Option, - #[serde(default)] - pub enable_margin_around_dpad: Option, - #[serde(default)] - pub enable_dpad_highlight: Option, - - // update check - #[serde(default)] - pub update_check_interval: Option, - #[serde(default)] - pub display_update_check_errors: Option, - - // audio - #[serde(default)] - pub enable_audio: Option, - #[serde(default)] - pub audio_volume: Option, - #[serde(default)] - pub enable_music: Option, - #[serde(default)] - pub music_volume: Option, - - // presets - #[serde(default)] - pub presets: Option>, - // TODO: it's not possible in RON to have a HashMap with flattened keys, - // so we will support it in different way formats - // once we support them - this would mean dropping RON support - // https://github.com/ron-rs/ron/issues/115 - // pub unknown_fields: HashMap, -} +use model::{Config, PartialConfig}; -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Settings { - shared: Arc>, - path: PathBuf, - read_only: bool, -} - -impl Default for Settings { - fn default() -> Self { - let settings = SettingsInner::default(); - Self { - shared: Arc::new(RwLock::new(settings)), - path: settings_path(), - read_only: false, - } - } + inner: Arc, } -#[allow(dead_code)] impl Settings { - pub fn new() -> Self { - Self::default() - } - - pub fn path(&self) -> PathBuf { - self.path.clone() - } - - pub fn is_ro(&self) -> bool { - self.read_only + /// Loads settings from configuration files. + /// + /// Returns the loaded settings and a boolean indicating whether any warnings occurred during + /// loading. + /// + /// TODO: Report the actual errors/warnings to the user. + pub fn load() -> (Self, bool) { + SettingsInner::load().map_first(|inner| Self { + inner: Arc::new(inner), + }) } - pub fn read(&self) -> std::sync::RwLockReadGuard<'_, SettingsInner> { - self.shared.read().unwrap() + pub fn read(&self) -> &Config { + &self.inner.base } +} - pub fn write(&mut self) -> std::sync::RwLockWriteGuard<'_, SettingsInner> { - self.shared.write().unwrap() - } +struct SettingsInner { + // ui_layer: PartialConfig, + config_layer: PartialConfig, + base: Config, } -impl Settings { - pub fn get_theme(&self) -> ThemeDefinition { - let name = self.read().theme.clone(); - let maybe_theme = match &name { - Some(theme_name) => ThemeDefinition::load_by_name(theme_name), - None => ThemeDefinition::load_default(self.read_only), - }; +impl SettingsInner { + fn load() -> (Self, bool) { + let mut errored = false; - match maybe_theme { - Ok(theme) => theme, - Err(err) if name.is_some() => { - log::error!("Could not load the theme: {}", err); - ThemeDefinition::parse_default() - } - Err(err) if name.is_none() => { - log::error!("Could not load the default theme: {}", err); - ThemeDefinition::parse_default() + let base_config = Config::default(); + let config_layer = match load_config_from_file(&paths::settings_path()) { + Ok(config) => config, + Err(_err) => { + errored = true; + PartialConfig::default() } - _ => unreachable!("`is_none` and `is_some` handle all cases"), - } - } + }; - pub fn get_logging_level(&self) -> log::Level { - self.read() - .logging_level - .clone() - .and_then(|level| level.parse().ok()) - .unwrap_or(log::Level::Info) - } + let config = Self { + config_layer, + base: base_config, + }; - pub fn get_debug_logging_level(&self) -> log::Level { - self.read() - .debug_logging_level - .clone() - .and_then(|level| level.parse().ok()) - .unwrap_or(log::Level::Info) + (config, errored) } +} - pub fn get_file_logging_level(&self) -> log::Level { - self.read() - .file_logging_level - .clone() - .and_then(|level| level.parse().ok()) - .unwrap_or(log::Level::Info) - } +#[derive(Debug, thiserror::Error)] +enum ConfigLoadError { + IoError(#[from] std::io::Error), + JsonError(#[from] json5::Error), + SettingsFormatError(String), +} - pub fn get_terminal_scheme(&self) -> Option { - match &self.read().terminal_scheme { - Some(TerminalSchemeDef::Named(name)) => { - Some(SharedScheme::new(TerminalColorScheme::named(name)?)) - } - Some(TerminalSchemeDef::Custom(scheme)) => Some(SharedScheme::new(scheme.clone())), - None => Some(SharedScheme::new(TerminalColorScheme::default())), +impl Display for ConfigLoadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigLoadError::IoError(e) => write!(f, "I/O error: {}", e), + ConfigLoadError::JsonError(e) => write!(f, "Parse error: {}", e), + ConfigLoadError::SettingsFormatError(e) => write!(f, "Settings format error: {}", e), } } +} - pub fn get_slow(&self) -> bool { - self.read().slow.unwrap_or_default() - } - - pub fn set_slow(&mut self, value: bool) -> &mut Self { - self.write().slow = Some(value); - self - } - - pub fn get_disable_tower_auto_up(&self) -> bool { - self.read().disable_tower_auto_up.unwrap_or_default() - } - - pub fn set_disable_tower_auto_up(&mut self, value: bool) -> &mut Self { - self.write().disable_tower_auto_up = Some(value); - self - } - - pub fn get_camera_mode(&self) -> CameraMode { - self.read().camera_mode.unwrap_or_default() - } - - pub fn set_camera_mode(&mut self, value: CameraMode) -> &mut Self { - self.write().camera_mode = Some(value); - self - } - - pub fn get_camera_smoothing(&self) -> f32 { - self.read().camera_smoothing.unwrap_or(0.5).clamp(0.5, 1.0) - } - - pub fn set_camera_smoothing(&mut self, value: f32) -> &mut Self { - self.write().camera_smoothing = Some(value.clamp(0.5, 1.0)); - self - } - - pub fn get_player_smoothing(&self) -> f32 { - self.read().player_smoothing.unwrap_or(0.8).clamp(0.5, 1.0) - } - - pub fn set_player_smoothing(&mut self, value: f32) -> &mut Self { - self.write().player_smoothing = Some(value.clamp(0.5, 1.0)); - self - } - - pub fn get_viewport_margin(&self) -> Dims { - self.read() - .viewport_margin - .map(Dims::from) - .unwrap_or(Dims(4, 3)) - } - - pub fn set_viewport_margin(&mut self, value: Dims) -> &mut Self { - self.write().viewport_margin = Some(value.into()); - self - } - - pub fn get_enable_mouse(&self) -> bool { - self.read().enable_mouse.unwrap_or(true) - } - - pub fn set_enable_mouse(&mut self, value: bool) -> &mut Self { - self.write().enable_mouse = Some(value); - self - } - - pub fn get_enable_dpad(&self) -> bool { - self.read().enable_dpad.unwrap_or(false) - } - - pub fn set_enable_dpad(&mut self, value: bool) -> &mut Self { - self.write().enable_dpad = Some(value); - self - } - - pub fn get_landscape_dpad_on_left(&self) -> bool { - self.read().landscape_dpad_on_left.unwrap_or(false) - } - - pub fn set_landscape_dpad_on_left(&mut self, value: bool) -> &mut Self { - self.write().landscape_dpad_on_left = Some(value); - self - } - - pub fn get_dpad_swap_up_down(&self) -> bool { - self.read().dpad_swap_up_down.unwrap_or(false) - } - - pub fn set_dpad_swap_up_down(&mut self, value: bool) -> &mut Self { - self.write().dpad_swap_up_down = Some(value); - self - } - - pub fn get_enable_margin_around_dpad(&self) -> bool { - self.read().enable_margin_around_dpad.unwrap_or(false) - } - - pub fn set_enable_margin_around_dpad(&mut self, value: bool) -> &mut Self { - self.write().enable_margin_around_dpad = Some(value); - self - } - - pub fn get_enable_dpad_highlight(&self) -> bool { - self.read().enable_dpad_highlight.unwrap_or(true) - } - - pub fn set_enable_dpad_highlight(&mut self, value: bool) -> &mut Self { - self.write().enable_dpad_highlight = Some(value); - self - } - - pub fn set_check_interval(&mut self, value: UpdateCheckInterval) -> &mut Self { - self.write().update_check_interval = Some(value); - self - } - - pub fn get_check_interval(&self) -> UpdateCheckInterval { - self.read().update_check_interval.unwrap_or_default() - } - - pub fn get_display_update_check_errors(&self) -> bool { - self.read().display_update_check_errors.unwrap_or(true) - } - - pub fn set_display_update_check_errors(&mut self, value: bool) -> &mut Self { - self.write().display_update_check_errors = Some(value); - self - } - - pub fn get_enable_audio(&self) -> bool { - self.read().enable_audio.unwrap_or_default() - } - - pub fn set_enable_audio(&mut self, value: bool) -> &mut Self { - self.write().enable_audio = Some(value); - self - } - - pub fn get_audio_volume(&self) -> f32 { - self.read().audio_volume.unwrap_or_default().clamp(0., 1.) - } - - pub fn set_audio_volume(&mut self, value: f32) -> &mut Self { - self.write().audio_volume = Some(value.clamp(0., 1.)); - self - } - - pub fn get_enable_music(&self) -> bool { - self.read().enable_music.unwrap_or_default() - } - - pub fn set_enable_music(&mut self, value: bool) -> &mut Self { - self.write().enable_music = Some(value); - self - } - - pub fn get_music_volume(&self) -> f32 { - self.read().music_volume.unwrap_or_default().clamp(0., 1.) - } - - pub fn set_music_volume(&mut self, value: f32) -> &mut Self { - self.write().music_volume = Some(value.clamp(0., 1.)); - self - } - - pub fn set_presets(&mut self, value: Vec) -> &mut Self { - self.write().presets = Some(value); - self - } - - pub fn get_presets(&self) -> Vec { - self.read().presets.clone().unwrap_or_default() +fn load_config_from_file(path: &Path) -> Result { + match load_values_from_file(path) { + Ok(value) => PartialConfig::try_from(value) + .map_err(|(e, val)| (ConfigLoadError::SettingsFormatError(e), val)), + Err((e, val)) => Err((e, PartialConfig::try_from(val).unwrap_or_default())), } } -// JSON -impl Settings { - pub fn load_json(path: PathBuf, read_only: bool) -> io::Result { - let settings_string = fs::read_to_string(&path); - let settings: SettingsInner = if let Ok(settings_string) = settings_string { - json5::from_str(&settings_string) - .expect("Could not parse settings file: check the syntax") - } else { - if !read_only { - fs::create_dir_all(path.parent().unwrap())?; - fs::write(&path, DEFAULT_SETTINGS_JSON)?; +fn load_values_from_file(path: &Path) -> Result { + macro_rules! pack_error { + ($err:expr) => { + match $err { + Ok(val) => Ok(val), + Err(e) => Err((e.into(), Value::Object(HashMap::new()))), } - json5::from_str(DEFAULT_SETTINGS_JSON).unwrap() }; - - Ok(Self { - shared: Arc::new(RwLock::new(settings)), - path, - read_only, - }) - } - - pub fn reset_json(&mut self) { - *self.write() = json5::from_str(DEFAULT_SETTINGS_JSON).unwrap(); - - let path = settings_path(); - fs::write(&path, DEFAULT_SETTINGS_JSON).unwrap(); - - self.path = path; - } - - pub fn reset_json_config(path: PathBuf) { - fs::write(path, DEFAULT_SETTINGS_JSON).unwrap(); - } -} - -struct OtherSettingsPopup(Popup, MouseGuard); - -impl OtherSettingsPopup { - fn new(settings: &Settings) -> Self { - let popup = Popup::new( - "Other settings".to_string(), - vec![ - "Path to the current settings:".to_string(), - format!(" {}", settings.path().to_string_lossy().to_string()), - "".to_string(), - "Other settings are not implemented in UI yet.".to_string(), - "Please edit the settings file directly.".to_string(), - ], - ); - - Self(popup, MouseGuard::new().unwrap()) } -} -impl ActivityHandler for OtherSettingsPopup { - fn update(&mut self, events: Vec, data: &mut AppData) -> Option { - self.0.update(events, data) + // json5 doesn't support reading from reader + let content = pack_error!(std::fs::read_to_string(path))?; + let config_value = pack_error!(json5::from_str::(&content))?; + let values_with_extensions = load_extension_blocks(config_value)?; + Ok(values_with_extensions) +} + +mod config_file_constants { + pub const IMPORT_KEY: &str = "#from"; +} + +fn load_extension_blocks(config: Value) -> Result { + use config_file_constants::IMPORT_KEY; + + /// Merges `ext` into `base`. In case of conflict, `ext` takes precedence. + /// Note that in this case, `base` is file behing `#from`, and `ext` is the current file. + /// + /// For objects, merging is done recursively. + /// + /// TODO: Allow rules customization in the future, for example to support list contatenation. + fn merge(base: &mut Value, ext: Value) { + match (base, ext) { + (Value::Object(base_map), Value::Object(ext_map)) => { + for (key, ext_value) in ext_map { + if key == IMPORT_KEY { + continue; // skip #from key during merge + } + + if let Some(base_value) = base_map.get_mut(&key) { + merge(base_value, ext_value); + } else { + base_map.insert(key, ext_value); + } + } + } + (base_val, ext) => { + *base_val = ext; + } + } } - fn screen(&mut self) -> &mut dyn Screen { - &mut self.0 - } -} + let value = match config { + Value::Object(map) => { + if map.contains_key(IMPORT_KEY) { + if let Value::String(file_path) = &map[IMPORT_KEY] { + let mut base_config = load_values_from_file(Path::new(file_path))?; + merge(&mut base_config, Value::Object(map)); + base_config + } else { + return Err(( + ConfigLoadError::SettingsFormatError(format!( + "{IMPORT_KEY} value must be a string", + )), + Value::Object(map), + )); + } + } else { + Value::Object(map) + } + } -pub struct SettingsActivity { - actions: Vec>, - menu: Menu, -} + val => val, + }; -impl SettingsActivity { - fn other_settings_popup(settings: &Settings) -> Activity { - Activity::new_base_boxed("settings".to_string(), OtherSettingsPopup::new(settings)) - } + Ok(value) } -#[allow(clippy::new_without_default)] -impl SettingsActivity { - pub fn new() -> Self { - let options = menu_actions!( - "Audio" on "sound" -> data => Change::push(create_audio_settings(data)), - "Controls" -> data => Change::push(create_controls_settings(data)), - "Other settings" -> data => Change::push(SettingsActivity::other_settings_popup(&data.settings)), - "Back" -> _ => Change::pop_top(), - ); - - let (options, actions) = split_menu_actions(options); +#[cfg(test)] +mod tests { + use super::*; - let menu_config = MenuConfig::new("Settings", options).subtitle("Changes are not saved"); - - Self { - actions, - menu: Menu::new(menu_config), + #[test] + fn test_value_deserialize() { + let json_data = r#" + { + "name": "Example", + "enabled": true, + "threshold": 10.5, + "count": 42, + "items": [1, 2, 3], + "settings": { + "option1": "value1", + "option2": false + } } - } - - pub fn new_activity() -> Activity { - Activity::new_base_boxed("settings".to_string(), Self::new()) - } -} + "#; + + let parsed: Value = serde_json::from_str(json_data).unwrap(); + + if let Value::Object(map) = parsed { + assert_eq!(map.get("name"), Some(&Value::String("Example".to_string()))); + assert_eq!(map.get("enabled"), Some(&Value::Bool(true))); + assert_eq!(map.get("threshold"), Some(&Value::Float(10.5))); + assert_eq!(map.get("count"), Some(&Value::Int(42))); + + if let Some(Value::List(items)) = map.get("items") { + assert_eq!(items.len(), 3); + assert_eq!(items[0], Value::Int(1)); + assert_eq!(items[1], Value::Int(2)); + assert_eq!(items[2], Value::Int(3)); + } else { + panic!("Expected 'items' to be a list"); + } -impl ActivityHandler for SettingsActivity { - fn update(&mut self, events: Vec, data: &mut AppData) -> Option { - match self.menu.update(events, data)? { - Change::Pop { - res: Some(sub_activity), - .. - } => { - let index = *sub_activity - .downcast::() - .expect("menu should return index"); - Some((self.actions[index])(data)) + if let Some(Value::Object(settings)) = map.get("settings") { + assert_eq!( + settings.get("option1"), + Some(&Value::String("value1".to_string())) + ); + assert_eq!(settings.get("option2"), Some(&Value::Bool(false))); + } else { + panic!("Expected 'settings' to be an object"); } - res => Some(res), + } else { + panic!("Expected top-level value to be an object"); } } - - fn screen(&mut self) -> &mut dyn Screen { - &mut self.menu - } -} - -pub fn create_controls_settings(data: &mut AppData) -> Activity { - let menu_config = MenuConfig::new( - "Controls settings", - [ - MenuItem::Option(OptionDef { - text: "Enable mouse input".into(), - val: data.settings.get_enable_mouse(), - fun: Box::new(|enabled, data| { - *enabled = !*enabled; - data.settings.set_enable_mouse(*enabled); - }), - }), - MenuItem::Option(OptionDef { - text: "Enable dpad".into(), - val: data.settings.get_enable_dpad(), - fun: Box::new(|enabled, data| { - *enabled = !*enabled; - data.settings.set_enable_dpad(*enabled); - }), - }), - MenuItem::Option(OptionDef { - text: "Left-handed dpad".into(), - val: data.settings.get_landscape_dpad_on_left(), - fun: Box::new(|is_on_left, data| { - *is_on_left = !*is_on_left; - data.settings.set_landscape_dpad_on_left(*is_on_left); - }), - }), - MenuItem::Option(OptionDef { - text: "Swap Up and Down buttons".into(), - val: data.settings.get_dpad_swap_up_down(), - fun: Box::new(|do_swap, data| { - *do_swap = !*do_swap; - data.settings.set_dpad_swap_up_down(*do_swap); - }), - }), - MenuItem::Option(OptionDef { - text: "Enable margin around dpad".into(), - val: data.settings.get_enable_margin_around_dpad(), - fun: Box::new(|enabled, data| { - *enabled = !*enabled; - data.settings.set_enable_margin_around_dpad(*enabled); - }), - }), - MenuItem::Option(OptionDef { - text: "Enable dpad highlight".into(), - val: data.settings.get_enable_dpad_highlight(), - fun: Box::new(|enabled, data| { - *enabled = !*enabled; - data.settings.set_enable_dpad_highlight(*enabled); - }), - }), - MenuItem::Separator, - MenuItem::Text("Exit".into()), - ], - ); - - Activity::new_base_boxed("controls settings", Menu::new(menu_config)) } diff --git a/tmaze/src/settings/model.rs b/tmaze/src/settings/model.rs index 1294f8e..a71f93e 100644 --- a/tmaze/src/settings/model.rs +++ b/tmaze/src/settings/model.rs @@ -1,8 +1,19 @@ -use cmaze::{algorithms::MazeSpec, dims::{Dims, Offset}}; +use std::ops::Deref; + +use cmaze::{ + algorithms::{MazeSpec, MazeSpecType}, + dims::{Dims, Offset}, +}; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; -use crate::{config, impl_merge_prims, settings::config_utils::Mergeable}; +use crate::{ + config, impl_merge_prims, + settings::{ + config_utils::Mergeable, + theme::{PartialTerminalColorScheme, TerminalColorScheme}, + }, +}; config! { pub struct Config { @@ -11,6 +22,7 @@ config! { #[nest] nagivation: Navigation, #[nest] updates: Updates, #[nest] audio: Audio, + presets: PresetList, } pub struct General { @@ -52,37 +64,11 @@ config! { } } -config! { - pub struct Presets { - presets: PresetList, - } - - pub struct TerminalColorScheme { - primary_fg: Rgb, - primary_bg: Rgb, - black: Rgb, // grey - dark_grey: Rgb, // dark grey - red: Rgb, - dark_red: Rgb, - green: Rgb, - dark_green: Rgb, - yellow: Rgb, - dark_yellow: Rgb, - blue: Rgb, - dark_blue: Rgb, - magenta: Rgb, - dark_magenta: Rgb, - cyan: Rgb, - dark_cyan: Rgb, - white: Rgb, - grey: Rgb, - } -} impl_merge_prims! { - String - f64 i64 bool + f64 + String Rgb Dims @@ -94,7 +80,7 @@ impl_merge_prims! { type Rgb = (u8, u8, u8); -#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Default, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] #[serde(tag = "mode")] pub enum CameraMode { #[default] @@ -105,7 +91,7 @@ pub enum CameraMode { }, } -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] pub enum UpdateCheckInterval { Never, #[default] @@ -118,9 +104,18 @@ pub enum UpdateCheckInterval { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct PresetList { + #[serde(flatten)] presets: Vec, } +impl Deref for PresetList { + type Target = [MazePreset]; + + fn deref(&self) -> &Self::Target { + &self.presets + } +} + impl Mergeable for PresetList { fn merge(&mut self, other: &Self) { self.presets.extend_from_slice(&other.presets); @@ -137,14 +132,31 @@ pub struct MazePreset { pub maze_spec: MazeSpec, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -// Note: order of variants matters for correct deserialization -pub enum Value { - Object(HashMap), - List(Vec), - Int(i64), - Float(f64), - Bool(bool), - String(String), +impl MazePreset { + pub fn short_desc(&self) -> Option { + let (size, cells): (_, usize) = match &self.maze_spec.inner_spec { + MazeSpecType::Regions { regions, .. } => ( + self.maze_spec.size()?, + regions.iter().map(|r| r.mask.enabled_count()).sum(), + ), + MazeSpecType::Simple { mask, .. } => ( + self.maze_spec.size()?, + mask.as_ref() + .map(|m| m.enabled_count()) + .unwrap_or(self.maze_spec.size()?.product() as usize), + ), + }; + + if size.2 == 1 { + Some(format!( + "{}: {}x{} ({} cells)", + self.title, size.0, size.1, cells + )) + } else { + Some(format!( + "{}: {}x{}x{} ({} cells)", + self.title, size.0, size.1, size.2, cells + )) + } + } } diff --git a/tmaze/src/settings/new_settings.rs b/tmaze/src/settings/new_settings.rs deleted file mode 100644 index 88c73a7..0000000 --- a/tmaze/src/settings/new_settings.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::{fmt::Display, path::Path, sync::Arc}; - -use hashbrown::HashMap; - -use crate::{ - helpers::{constants::paths, TupleMap}, - settings::model::{Config, PartialConfig, Value}, -}; - -struct Settings { - inner: Arc, -} - -impl Settings { - /// Loads settings from configuration files. - /// - /// Returns the loaded settings and a boolean indicating whether any errors/warnings occurred - /// during loading. - /// - /// TODO: Report the actual errors/warnings to the user. - fn load() -> (Self, bool) { - SettingsInner::load().map_first(|inner| Self { - inner: Arc::new(inner), - }) - } -} - -struct SettingsInner { - // ui_layer: PartialConfig, - config_layer: PartialConfig, - base: Config, -} - -impl SettingsInner { - fn load() -> (Self, bool) { - let mut errored = false; - - let base_config = Config::default(); - let config_layer = match load_config_from_file(&paths::settings_path()) { - Ok(config) => config, - Err(_err) => { - errored = true; - PartialConfig::default() - } - }; - - let config = Self { - config_layer, - base: base_config, - }; - - (config, errored) - } -} - -#[derive(Debug, thiserror::Error)] -enum ConfigLoadError { - IoError(#[from] std::io::Error), - JsonError(#[from] json5::Error), - SettingsFormatError(String), -} - -impl Display for ConfigLoadError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConfigLoadError::IoError(e) => write!(f, "I/O error: {}", e), - ConfigLoadError::JsonError(e) => write!(f, "Parse error: {}", e), - ConfigLoadError::SettingsFormatError(e) => write!(f, "Settings format error: {}", e), - } - } -} - -fn load_config_from_file(path: &Path) -> Result { - match load_values_from_file(path) { - Ok(value) => PartialConfig::try_from(value) - .map_err(|(e, val)| (ConfigLoadError::SettingsFormatError(e), val)), - Err((e, val)) => Err((e, PartialConfig::try_from(val).unwrap_or_default())), - } -} - -fn load_values_from_file(path: &Path) -> Result { - macro_rules! pack_error { - ($err:expr) => { - match $err { - Ok(val) => Ok(val), - Err(e) => Err((e.into(), Value::Object(HashMap::new()))), - } - }; - } - - // json5 doesn't support reading from reader - let content = pack_error!(std::fs::read_to_string(path))?; - let config_value = pack_error!(json5::from_str::(&content))?; - let values_with_extensions = load_extension_blocks(config_value)?; - Ok(values_with_extensions) -} - -mod config_file_constants { - pub const IMPORT_KEY: &str = "#from"; -} - -fn load_extension_blocks(config: Value) -> Result { - use config_file_constants::IMPORT_KEY; - - /// Merges `ext` into `base`. In case of conflict, `ext` takes precedence. - /// Note that in this case, `base` is file behing `#from`, and `ext` is the current file. - /// - /// For objects, merging is done recursively. - /// - /// TODO: Allow rules customization in the future, for example to support list contatenation. - fn merge(base: &mut Value, ext: Value) { - match (base, ext) { - (Value::Object(base_map), Value::Object(ext_map)) => { - for (key, ext_value) in ext_map { - if key == IMPORT_KEY { - continue; // skip #from key during merge - } - - if let Some(base_value) = base_map.get_mut(&key) { - merge(base_value, ext_value); - } else { - base_map.insert(key, ext_value); - } - } - } - (base_val, ext) => { - *base_val = ext; - } - } - } - - let value = match config { - Value::Object(map) => { - if map.contains_key(IMPORT_KEY) { - if let Value::String(file_path) = &map[IMPORT_KEY] { - let mut base_config = load_values_from_file(Path::new(file_path))?; - merge(&mut base_config, Value::Object(map)); - base_config - } else { - return Err(( - ConfigLoadError::SettingsFormatError(format!( - "{IMPORT_KEY} value must be a string", - )), - Value::Object(map), - )); - } - } else { - Value::Object(map) - } - } - - val => val, - }; - - Ok(value) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_value_deserialize() { - let json_data = r#" - { - "name": "Example", - "enabled": true, - "threshold": 10.5, - "count": 42, - "items": [1, 2, 3], - "settings": { - "option1": "value1", - "option2": false - } - } - "#; - - let parsed: Value = serde_json::from_str(json_data).unwrap(); - - if let Value::Object(map) = parsed { - assert_eq!(map.get("name"), Some(&Value::String("Example".to_string()))); - assert_eq!(map.get("enabled"), Some(&Value::Bool(true))); - assert_eq!(map.get("threshold"), Some(&Value::Float(10.5))); - assert_eq!(map.get("count"), Some(&Value::Int(42))); - - if let Some(Value::List(items)) = map.get("items") { - assert_eq!(items.len(), 3); - assert_eq!(items[0], Value::Int(1)); - assert_eq!(items[1], Value::Int(2)); - assert_eq!(items[2], Value::Int(3)); - } else { - panic!("Expected 'items' to be a list"); - } - - if let Some(Value::Object(settings)) = map.get("settings") { - assert_eq!( - settings.get("option1"), - Some(&Value::String("value1".to_string())) - ); - assert_eq!(settings.get("option2"), Some(&Value::Bool(false))); - } else { - panic!("Expected 'settings' to be an object"); - } - } else { - panic!("Expected top-level value to be an object"); - } - } -} diff --git a/tmaze/src/settings/old_settings.rs b/tmaze/src/settings/old_settings.rs new file mode 100644 index 0000000..dc7a741 --- /dev/null +++ b/tmaze/src/settings/old_settings.rs @@ -0,0 +1,469 @@ +use cmaze::{ + algorithms::{MazeSpec, MazeSpecType}, + dims::{Dims, Offset}, +}; +use derivative::Derivative; +use serde::{Deserialize, Serialize}; +use std::{ + fs, io, + path::PathBuf, + sync::{Arc, RwLock}, +}; +use theme::ThemeDefinition; + +use crate::{ + helpers::constants::paths::settings_path, + settings::theme::{SharedScheme, TerminalColorScheme, ThemeDefinition}, +}; + +const DEFAULT_SETTINGS_JSON: &str = include_str!("./files/default_settings.json5"); + +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +#[serde(tag = "mode")] +pub enum CameraMode { + #[default] + CloseFollow, + EdgeFollow { + x: Offset, + y: Offset, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MazePreset { + pub title: String, + pub description: Option, + + #[serde(default)] + pub default: bool, + + // TODO: make `serde(flatten)` once switched to TOML/JSON + #[serde(flatten)] + pub maze_spec: MazeSpec, +} + +impl MazePreset { + pub fn short_desc(&self) -> Option { + let (size, cells): (_, usize) = match &self.maze_spec.inner_spec { + MazeSpecType::Regions { regions, .. } => ( + self.maze_spec.size()?, + regions.iter().map(|r| r.mask.enabled_count()).sum(), + ), + MazeSpecType::Simple { mask, .. } => ( + self.maze_spec.size()?, + mask.as_ref() + .map(|m| m.enabled_count()) + .unwrap_or(self.maze_spec.size()?.product() as usize), + ), + }; + + if size.2 == 1 { + Some(format!( + "{}: {}x{} ({} cells)", + self.title, size.0, size.1, cells + )) + } else { + Some(format!( + "{}: {}x{}x{} ({} cells)", + self.title, size.0, size.1, size.2, cells + )) + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub enum UpdateCheckInterval { + Never, + #[default] + Daily, + Weekly, + Monthly, + Yearly, + Always, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TerminalSchemeDef { + Named(String), + Custom(TerminalColorScheme), +} + +#[derive(Debug, Derivative, Serialize, Deserialize)] +#[derivative(Default)] +#[serde(rename = "Settings")] +// FIXME: separate sections into their own struct +pub struct SettingsInner { + // general + #[serde(default)] + pub theme: Option, + #[serde(default)] + pub logging_level: Option, + #[serde(default)] + pub debug_logging_level: Option, + #[serde(default)] + pub file_logging_level: Option, + #[serde(default)] + pub terminal_scheme: Option, + + // viewport + #[serde(default)] + pub slow: Option, + #[serde(default)] + pub disable_tower_auto_up: Option, + #[serde(default)] + pub camera_mode: Option, + #[serde(default)] + pub camera_smoothing: Option, + #[serde[default]] + pub player_smoothing: Option, + #[serde(default)] + pub viewport_margin: Option<(i32, i32)>, + + // navigation + #[serde(default)] + pub enable_mouse: Option, + #[serde(default)] + pub enable_dpad: Option, + #[serde(default)] + pub landscape_dpad_on_left: Option, + #[serde(default)] + pub dpad_swap_up_down: Option, + #[serde(default)] + pub enable_margin_around_dpad: Option, + #[serde(default)] + pub enable_dpad_highlight: Option, + + // update check + #[serde(default)] + pub update_check_interval: Option, + #[serde(default)] + pub display_update_check_errors: Option, + + // audio + #[serde(default)] + pub enable_audio: Option, + #[serde(default)] + pub audio_volume: Option, + #[serde(default)] + pub enable_music: Option, + #[serde(default)] + pub music_volume: Option, + + // presets + #[serde(default)] + pub presets: Option>, + // TODO: it's not possible in RON to have a HashMap with flattened keys, + // so we will support it in different way formats + // once we support them - this would mean dropping RON support + // https://github.com/ron-rs/ron/issues/115 + // pub unknown_fields: HashMap, +} + +#[derive(Debug, Clone)] +pub struct Settings { + shared: Arc>, + path: PathBuf, + read_only: bool, +} + +impl Default for Settings { + fn default() -> Self { + let settings = SettingsInner::default(); + Self { + shared: Arc::new(RwLock::new(settings)), + path: settings_path(), + read_only: false, + } + } +} + +#[allow(dead_code)] +impl Settings { + pub fn new() -> Self { + Self::default() + } + + pub fn path(&self) -> PathBuf { + self.path.clone() + } + + pub fn is_ro(&self) -> bool { + self.read_only + } + + pub fn read(&self) -> std::sync::RwLockReadGuard<'_, SettingsInner> { + self.shared.read().unwrap() + } + + pub fn write(&mut self) -> std::sync::RwLockWriteGuard<'_, SettingsInner> { + self.shared.write().unwrap() + } +} + +impl Settings { + pub fn get_theme(&self) -> ThemeDefinition { + let name = self.read().theme.clone(); + let maybe_theme = match &name { + Some(theme_name) => ThemeDefinition::load_by_name(theme_name), + None => ThemeDefinition::load_default(self.read_only), + }; + + match maybe_theme { + Ok(theme) => theme, + Err(err) if name.is_some() => { + log::error!("Could not load the theme: {}", err); + ThemeDefinition::parse_default() + } + Err(err) if name.is_none() => { + log::error!("Could not load the default theme: {}", err); + ThemeDefinition::parse_default() + } + _ => unreachable!("`is_none` and `is_some` handle all cases"), + } + } + + pub fn get_logging_level(&self) -> log::Level { + self.read() + .logging_level + .clone() + .and_then(|level| level.parse().ok()) + .unwrap_or(log::Level::Info) + } + + pub fn get_debug_logging_level(&self) -> log::Level { + self.read() + .debug_logging_level + .clone() + .and_then(|level| level.parse().ok()) + .unwrap_or(log::Level::Info) + } + + pub fn get_file_logging_level(&self) -> log::Level { + self.read() + .file_logging_level + .clone() + .and_then(|level| level.parse().ok()) + .unwrap_or(log::Level::Info) + } + + pub fn get_terminal_scheme(&self) -> Option { + match &self.read().terminal_scheme { + Some(TerminalSchemeDef::Named(name)) => { + Some(SharedScheme::new(TerminalColorScheme::named(name)?)) + } + Some(TerminalSchemeDef::Custom(scheme)) => Some(SharedScheme::new(scheme.clone())), + None => Some(SharedScheme::new(TerminalColorScheme::default())), + } + } + + pub fn get_slow(&self) -> bool { + self.read().slow.unwrap_or_default() + } + + pub fn set_slow(&mut self, value: bool) -> &mut Self { + self.write().slow = Some(value); + self + } + + pub fn get_disable_tower_auto_up(&self) -> bool { + self.read().disable_tower_auto_up.unwrap_or_default() + } + + pub fn set_disable_tower_auto_up(&mut self, value: bool) -> &mut Self { + self.write().disable_tower_auto_up = Some(value); + self + } + + pub fn get_camera_mode(&self) -> CameraMode { + self.read().camera_mode.unwrap_or_default() + } + + pub fn set_camera_mode(&mut self, value: CameraMode) -> &mut Self { + self.write().camera_mode = Some(value); + self + } + + pub fn get_camera_smoothing(&self) -> f32 { + self.read().camera_smoothing.unwrap_or(0.5).clamp(0.5, 1.0) + } + + pub fn set_camera_smoothing(&mut self, value: f32) -> &mut Self { + self.write().camera_smoothing = Some(value.clamp(0.5, 1.0)); + self + } + + pub fn get_player_smoothing(&self) -> f32 { + self.read().player_smoothing.unwrap_or(0.8).clamp(0.5, 1.0) + } + + pub fn set_player_smoothing(&mut self, value: f32) -> &mut Self { + self.write().player_smoothing = Some(value.clamp(0.5, 1.0)); + self + } + + pub fn get_viewport_margin(&self) -> Dims { + self.read() + .viewport_margin + .map(Dims::from) + .unwrap_or(Dims(4, 3)) + } + + pub fn set_viewport_margin(&mut self, value: Dims) -> &mut Self { + self.write().viewport_margin = Some(value.into()); + self + } + + pub fn get_enable_mouse(&self) -> bool { + self.read().enable_mouse.unwrap_or(true) + } + + pub fn set_enable_mouse(&mut self, value: bool) -> &mut Self { + self.write().enable_mouse = Some(value); + self + } + + pub fn get_enable_dpad(&self) -> bool { + self.read().enable_dpad.unwrap_or(false) + } + + pub fn set_enable_dpad(&mut self, value: bool) -> &mut Self { + self.write().enable_dpad = Some(value); + self + } + + pub fn get_landscape_dpad_on_left(&self) -> bool { + self.read().landscape_dpad_on_left.unwrap_or(false) + } + + pub fn set_landscape_dpad_on_left(&mut self, value: bool) -> &mut Self { + self.write().landscape_dpad_on_left = Some(value); + self + } + + pub fn get_dpad_swap_up_down(&self) -> bool { + self.read().dpad_swap_up_down.unwrap_or(false) + } + + pub fn set_dpad_swap_up_down(&mut self, value: bool) -> &mut Self { + self.write().dpad_swap_up_down = Some(value); + self + } + + pub fn get_enable_margin_around_dpad(&self) -> bool { + self.read().enable_margin_around_dpad.unwrap_or(false) + } + + pub fn set_enable_margin_around_dpad(&mut self, value: bool) -> &mut Self { + self.write().enable_margin_around_dpad = Some(value); + self + } + + pub fn get_enable_dpad_highlight(&self) -> bool { + self.read().enable_dpad_highlight.unwrap_or(true) + } + + pub fn set_enable_dpad_highlight(&mut self, value: bool) -> &mut Self { + self.write().enable_dpad_highlight = Some(value); + self + } + + pub fn set_check_interval(&mut self, value: UpdateCheckInterval) -> &mut Self { + self.write().update_check_interval = Some(value); + self + } + + pub fn get_check_interval(&self) -> UpdateCheckInterval { + self.read().update_check_interval.unwrap_or_default() + } + + pub fn get_display_update_check_errors(&self) -> bool { + self.read().display_update_check_errors.unwrap_or(true) + } + + pub fn set_display_update_check_errors(&mut self, value: bool) -> &mut Self { + self.write().display_update_check_errors = Some(value); + self + } + + pub fn get_enable_audio(&self) -> bool { + self.read().enable_audio.unwrap_or_default() + } + + pub fn set_enable_audio(&mut self, value: bool) -> &mut Self { + self.write().enable_audio = Some(value); + self + } + + pub fn get_audio_volume(&self) -> f32 { + self.read().audio_volume.unwrap_or_default().clamp(0., 1.) + } + + pub fn set_audio_volume(&mut self, value: f32) -> &mut Self { + self.write().audio_volume = Some(value.clamp(0., 1.)); + self + } + + pub fn get_enable_music(&self) -> bool { + self.read().enable_music.unwrap_or_default() + } + + pub fn set_enable_music(&mut self, value: bool) -> &mut Self { + self.write().enable_music = Some(value); + self + } + + pub fn get_music_volume(&self) -> f32 { + self.read().music_volume.unwrap_or_default().clamp(0., 1.) + } + + pub fn set_music_volume(&mut self, value: f32) -> &mut Self { + self.write().music_volume = Some(value.clamp(0., 1.)); + self + } + + pub fn set_presets(&mut self, value: Vec) -> &mut Self { + self.write().presets = Some(value); + self + } + + pub fn get_presets(&self) -> Vec { + self.read().presets.clone().unwrap_or_default() + } +} + +// JSON +impl Settings { + pub fn load_json(path: PathBuf, read_only: bool) -> io::Result { + let settings_string = fs::read_to_string(&path); + let settings: SettingsInner = if let Ok(settings_string) = settings_string { + json5::from_str(&settings_string) + .expect("Could not parse settings file: check the syntax") + } else { + if !read_only { + fs::create_dir_all(path.parent().unwrap())?; + fs::write(&path, DEFAULT_SETTINGS_JSON)?; + } + json5::from_str(DEFAULT_SETTINGS_JSON).unwrap() + }; + + Ok(Self { + shared: Arc::new(RwLock::new(settings)), + path, + read_only, + }) + } + + pub fn reset_json(&mut self) { + *self.write() = json5::from_str(DEFAULT_SETTINGS_JSON).unwrap(); + + let path = settings_path(); + fs::write(&path, DEFAULT_SETTINGS_JSON).unwrap(); + + self.path = path; + } + + pub fn reset_json_config(path: PathBuf) { + fs::write(path, DEFAULT_SETTINGS_JSON).unwrap(); + } +} diff --git a/tmaze/src/settings/theme.rs b/tmaze/src/settings/theme.rs index 252e84a..b330215 100644 --- a/tmaze/src/settings/theme.rs +++ b/tmaze/src/settings/theme.rs @@ -6,10 +6,14 @@ use serde::{de::Error, Deserialize, Serialize}; use thiserror::Error; use crate::{ + config, helpers::{constants::paths::theme_file_path, ToDebug}, - settings::attribute::deserialize_attributes, }; +use super::config_utils::Mergeable; + +use super::attribute::deserialize_attributes; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Theme { styles: HashMap, @@ -87,8 +91,8 @@ impl ThemeDefinition { Self::load_by_path(path) } - pub fn load_by_name(path: &str) -> Result { - Self::load_by_path(theme_file_path(path)) + pub fn load_by_name(name: &str) -> Result { + Self::load_by_path(theme_file_path(name)) } pub fn load_by_path(path: PathBuf) -> Result { @@ -99,6 +103,8 @@ impl ThemeDefinition { .and_then(|s| s.to_str()) .expect("No extension"); + // TODO: names without extension + match ext { "toml" => Self::load_toml(path), "json" | "json5" => Self::load_json(path), @@ -461,26 +467,27 @@ pub type Rgb = (u8, u8, u8); pub type SharedScheme = Rc; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct TerminalColorScheme { - primary_fg: Rgb, - primary_bg: Rgb, - black: Rgb, // grey - dark_grey: Rgb, // dark grey - red: Rgb, - dark_red: Rgb, - green: Rgb, - dark_green: Rgb, - yellow: Rgb, - dark_yellow: Rgb, - blue: Rgb, - dark_blue: Rgb, - magenta: Rgb, - dark_magenta: Rgb, - cyan: Rgb, - dark_cyan: Rgb, - white: Rgb, - grey: Rgb, +config! { + pub struct TerminalColorScheme { + primary_fg: Rgb = (255, 255, 255), + primary_bg: Rgb = (0, 0, 0), + black: Rgb = (0, 0, 0), + dark_grey: Rgb = (64, 64, 64), + red: Rgb = (255, 0, 0), + dark_red: Rgb = (128, 0, 0), + green: Rgb = (0, 255, 0), + dark_green: Rgb = (0, 128, 0), + yellow: Rgb = (255, 255, 0), + dark_yellow: Rgb = (128, 128, 0), + blue: Rgb = (0, 0, 255), + dark_blue: Rgb = (0, 0, 128), + magenta: Rgb = (255, 0, 255), + dark_magenta: Rgb = (128, 0, 128), + cyan: Rgb = (0, 255, 255), + dark_cyan: Rgb = (0, 128, 128), + white: Rgb = (255, 255, 255), + grey: Rgb = (192, 192, 192), + } } impl TerminalColorScheme { @@ -531,31 +538,6 @@ impl TerminalColorScheme { } } -impl Default for TerminalColorScheme { - fn default() -> Self { - TerminalColorScheme { - primary_fg: (255, 255, 255), - primary_bg: (0, 0, 0), - black: (0, 0, 0), - dark_grey: (64, 64, 64), - red: (255, 0, 0), - dark_red: (128, 0, 0), - green: (0, 255, 0), - dark_green: (0, 128, 0), - yellow: (255, 255, 0), - dark_yellow: (128, 128, 0), - blue: (0, 0, 255), - dark_blue: (0, 0, 128), - magenta: (255, 0, 255), - dark_magenta: (128, 0, 128), - cyan: (0, 255, 255), - dark_cyan: (0, 128, 128), - white: (255, 255, 255), - grey: (192, 192, 192), - } - } -} - #[derive(Clone, Debug, Default)] pub struct ThemeResolver(HashMap); diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index 17e48b9..0824cac 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -73,7 +73,7 @@ impl SoundPlayer { return; }; let sink = Sink::try_new(handle).expect("Failed to create sink"); - sink.set_volume(self.settings.get_audio_volume()); + sink.set_volume(self.settings.read().audio.audio_volume as f32); sink.append(track); sink.play(); sink.detach(); @@ -91,54 +91,60 @@ impl SoundPlayer { pub fn create_audio_settings(data: &mut AppData) -> Activity { fn update_vol(data: &mut AppData) { - if data.settings.get_enable_audio() && data.settings.get_enable_music() { + let cfg = &data.settings.read().audio; + + if cfg.enable_audio && cfg.enable_music { data.sound_player - .set_volume(data.settings.get_audio_volume() * data.settings.get_music_volume()); + .set_volume((cfg.audio_volume * cfg.music_volume) as f32); } else { data.sound_player.set_volume(0.0); } } + // FIXME: re-add settings updating once supported + + let config = &data.settings.read().audio; + let menu_config = menu::MenuConfig::new( "Audio settings", [ MenuItem::Option(OptionDef { text: "Global mute".into(), - val: !data.settings.get_enable_audio(), + val: !config.enable_audio, fun: Box::new(|mute, data| { *mute = !*mute; - data.settings.set_enable_audio(!*mute); + // data.settings.set_enable_audio(!*mute); update_vol(data); }), }), MenuItem::Slider(SliderDef { text: "Global volume".into(), - val: (data.settings.get_audio_volume() * 5.0) as i32, + val: (config.audio_volume * 5.0) as i32, range: 0..=5, as_num: false, fun: Box::new(|up, vol, data| { *vol += if up { 1 } else { -1 }; - data.settings.set_audio_volume(*vol as f32 / 5.0); + // data.settings.set_audio_volume(*vol as f32 / 5.0); update_vol(data); }), }), MenuItem::Option(OptionDef { text: "Music mute".into(), - val: !data.settings.get_enable_music(), + val: !config.enable_music, fun: Box::new(|mute, data| { *mute = !*mute; - data.settings.set_enable_music(!*mute); + // data.settings.set_enable_music(!*mute); update_vol(data); }), }), MenuItem::Slider(SliderDef { text: "Music volume".into(), - val: (data.settings.get_music_volume() * 5.0) as i32, + val: (config.music_volume * 5.0) as i32, range: 0..=5, as_num: false, fun: Box::new(|up, vol, data| { *vol += if up { 1 } else { -1 }; - data.settings.set_music_volume(*vol as f32 / 5.0); + // data.settings.set_music_volume(*vol as f32 / 5.0); update_vol(data); }), }), diff --git a/tmaze/src/ui/menu.rs b/tmaze/src/ui/menu.rs index 33a1842..c1f8565 100644 --- a/tmaze/src/ui/menu.rs +++ b/tmaze/src/ui/menu.rs @@ -311,6 +311,7 @@ pub struct Menu { impl Menu { pub fn new(config: MenuConfig) -> Self { let MenuConfig { options, .. } = &config; + debug_assert!(!options.is_empty(), "Menu must have at least one option"); let default = config.default.unwrap_or(0).clamp(0, options.len() - 1); diff --git a/tmaze/src/ui/usecase/dpad.rs b/tmaze/src/ui/usecase/dpad.rs index bec8b74..19dd2c1 100644 --- a/tmaze/src/ui/usecase/dpad.rs +++ b/tmaze/src/ui/usecase/dpad.rs @@ -101,11 +101,7 @@ impl DPad { /// Splits the screen into space for the viewport and the dpad. /// - /// Returns a tuple containing: - /// - Whether the dpad is vertical - /// - Whether the dpad is on the left side - /// - The split offset - // pub fn split_screen(screen_size: Dims, on_left: bool) -> (bool, bool, Offset) { + /// Returns (viewport_rect, dpad_rect) pub fn split_screen(data: &AppData) -> (Rect, Rect) { let screen_size = data.screen_size; let screen_ratio = (screen_size.0 as f32 / 2.0) / screen_size.1 as f32; @@ -123,10 +119,10 @@ impl DPad { if is_vertical { screen_rect.split_y_end(Offset::Abs(dpad_size)) } else { - let on_right = !data.settings.get_landscape_dpad_on_left(); + let on_left = data.settings.read().nagivation.landscape_dpad_on_left; let offset = Offset::Abs(dpad_size); - if !on_right { + if on_left { let (dpad, vp) = screen_rect.split_x(offset); (vp, dpad) } else { diff --git a/tmaze/src/ui/usecase/mod.rs b/tmaze/src/ui/usecase/mod.rs index 32ba851..a3bc0c2 100644 --- a/tmaze/src/ui/usecase/mod.rs +++ b/tmaze/src/ui/usecase/mod.rs @@ -3,7 +3,9 @@ use dpad::dpad_theme_resolver; use crate::settings::theme::ThemeResolver; pub mod dpad; -pub mod style_browser; +mod screens; + +pub use screens::*; pub fn usecase_ui_theme_resolver() -> ThemeResolver { let mut resolver = ThemeResolver::new(); diff --git a/tmaze/src/ui/usecase/screens/mod.rs b/tmaze/src/ui/usecase/screens/mod.rs new file mode 100644 index 0000000..a8efbe3 --- /dev/null +++ b/tmaze/src/ui/usecase/screens/mod.rs @@ -0,0 +1,2 @@ +pub mod style_browser; +pub mod settings; diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs new file mode 100644 index 0000000..ce4dfc5 --- /dev/null +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -0,0 +1,157 @@ +use crate::{ + app::{self, app::AppData, Activity, ActivityHandler, Change}, + helpers::constants::paths::settings_path, + menu_actions, + renderer::MouseGuard, + settings::Settings, + sound::create_audio_settings, + ui::{split_menu_actions, Menu, MenuAction, MenuConfig, MenuItem, OptionDef, Popup, Screen}, +}; + +struct OtherSettingsPopup(Popup, MouseGuard); + +impl OtherSettingsPopup { + fn new() -> Self { + let popup = Popup::new( + "Other settings".to_string(), + vec![ + "Path to the current settings:".to_string(), + format!(" {}", settings_path().to_string_lossy()), + "".to_string(), + "Other settings are not implemented in UI yet.".to_string(), + "Please edit the settings file directly.".to_string(), + ], + ); + + Self(popup, MouseGuard::new().unwrap()) + } +} + +impl ActivityHandler for OtherSettingsPopup { + fn update(&mut self, events: Vec, data: &mut AppData) -> Option { + self.0.update(events, data) + } + + fn screen(&mut self) -> &mut dyn Screen { + &mut self.0 + } +} + +pub struct SettingsActivity { + actions: Vec>, + menu: Menu, +} + +impl SettingsActivity { + fn other_settings_popup() -> Activity { + Activity::new_base_boxed("settings".to_string(), OtherSettingsPopup::new()) + } +} + +#[allow(clippy::new_without_default)] +impl SettingsActivity { + pub fn new() -> Self { + let options = menu_actions!( + "Audio" on "sound" -> data => Change::push(create_audio_settings(data)), + "Controls" -> data => Change::push(create_controls_settings(data)), + "Other settings" -> _ => Change::push(SettingsActivity::other_settings_popup()), + "Back" -> _ => Change::pop_top(), + ); + + let (options, actions) = split_menu_actions(options); + + let menu_config = MenuConfig::new("Settings", options).subtitle("Changes are not saved"); + + Self { + actions, + menu: Menu::new(menu_config), + } + } + + pub fn new_activity() -> Activity { + Activity::new_base_boxed("settings".to_string(), Self::new()) + } +} + +impl ActivityHandler for SettingsActivity { + fn update(&mut self, events: Vec, data: &mut AppData) -> Option { + match self.menu.update(events, data)? { + Change::Pop { + res: Some(sub_activity), + .. + } => { + let index = *sub_activity + .downcast::() + .expect("menu should return index"); + Some((self.actions[index])(data)) + } + res => Some(res), + } + } + + fn screen(&mut self) -> &mut dyn Screen { + &mut self.menu + } +} + +pub fn create_controls_settings(data: &mut AppData) -> Activity { + let cfg = &data.settings.read().nagivation; + + let menu_config = MenuConfig::new( + "Controls settings", + [ + MenuItem::Option(OptionDef { + text: "Enable mouse input".into(), + val: cfg.enable_mouse, + fun: Box::new(|enabled, data| { + *enabled = !*enabled; + // data.settings.set_enable_mouse(*enabled); + }), + }), + MenuItem::Option(OptionDef { + text: "Enable dpad".into(), + val: cfg.enable_dpad, + fun: Box::new(|enabled, data| { + *enabled = !*enabled; + // data.settings.set_enable_dpad(*enabled); + }), + }), + MenuItem::Option(OptionDef { + text: "Left-handed dpad".into(), + val: cfg.landscape_dpad_on_left, + fun: Box::new(|is_on_left, data| { + *is_on_left = !*is_on_left; + // data.settings.set_landscape_dpad_on_left(*is_on_left); + }), + }), + MenuItem::Option(OptionDef { + text: "Swap Up and Down buttons".into(), + val: cfg.dpad_swap_up_down, + fun: Box::new(|do_swap, data| { + *do_swap = !*do_swap; + // data.settings.set_dpad_swap_up_down(*do_swap); + }), + }), + MenuItem::Option(OptionDef { + text: "Enable margin around dpad".into(), + val: cfg.enable_margin_around_dpad, + fun: Box::new(|enabled, data| { + *enabled = !*enabled; + // data.settings.set_enable_margin_around_dpad(*enabled); + }), + }), + MenuItem::Option(OptionDef { + text: "Enable dpad highlight".into(), + val: cfg.enable_dpad_highlight, + fun: Box::new(|enabled, data| { + *enabled = !*enabled; + // data.settings.set_enable_dpad_highlight(*enabled); + }), + }), + MenuItem::Separator, + MenuItem::Text("Exit".into()), + ], + ); + + Activity::new_base_boxed("controls settings", Menu::new(menu_config)) +} diff --git a/tmaze/src/ui/usecase/style_browser.rs b/tmaze/src/ui/usecase/screens/style_browser.rs similarity index 100% rename from tmaze/src/ui/usecase/style_browser.rs rename to tmaze/src/ui/usecase/screens/style_browser.rs diff --git a/tmaze/src/updates.rs b/tmaze/src/updates.rs index 0e8cb97..d2f0a60 100644 --- a/tmaze/src/updates.rs +++ b/tmaze/src/updates.rs @@ -34,11 +34,12 @@ pub async fn get_newer_async() -> Result, CratesError> { } pub fn check(app_data: &mut AppData) { - if app_data.save.is_update_checked(&app_data.settings) { + let cfg = app_data.settings.read(); + if app_data.save.is_update_checked(cfg) { return; } - let display_update_errors = app_data.settings.get_display_update_check_errors(); + let display_update_errors = cfg.updates.display_update_check_errors; let qer = app_data.queuer(); @@ -54,7 +55,7 @@ pub fn check(app_data: &mut AppData) { Ok(Some(version)) => { log::warn!("Newer version found: {}", version); qer.queue(Job::new(|data| { - if !data.settings.is_ro() { + if !data.is_ro() { data.save .update_last_check() .expect("Failed to save the save data"); @@ -64,7 +65,7 @@ pub fn check(app_data: &mut AppData) { Ok(None) => { log::info!("No newer version found"); qer.queue(Job::new(|data| { - if !data.settings.is_ro() { + if !data.is_ro() { data.save .update_last_check() .expect("Failed to save the save data"); From 29a0cafd7283766bd9864e0b9a1fdbc7bd4a364e Mon Sep 17 00:00:00 2001 From: ur-fault Date: Fri, 30 Jan 2026 16:45:09 +0100 Subject: [PATCH 07/23] Fix: warnings, rename paths constants --- tmaze/src/app/app.rs | 4 +++ tmaze/src/app/game.rs | 2 +- tmaze/src/data/mod.rs | 11 ++++--- tmaze/src/helpers/constants.rs | 37 +++++++++++++++-------- tmaze/src/logging.rs | 3 +- tmaze/src/main.rs | 15 +++++----- tmaze/src/settings/mod.rs | 38 ++++++++++++++++++------ tmaze/src/settings/model.rs | 1 - tmaze/src/settings/theme.rs | 6 ++-- tmaze/src/ui/usecase/screens/settings.rs | 17 +++++------ 10 files changed, 85 insertions(+), 49 deletions(-) diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index 1c72d86..51f8884 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -150,6 +150,10 @@ impl App { ); logger.init(); + if settings_error { + log::error!("Errors were encountered while loading the config."); + } + let save = SaveData::load().expect("failed to load save data"); let use_data = AppStateData::default(); let jobs = Jobs::new(); diff --git a/tmaze/src/app/game.rs b/tmaze/src/app/game.rs index 1e806b8..6ee874a 100644 --- a/tmaze/src/app/game.rs +++ b/tmaze/src/app/game.rs @@ -10,7 +10,7 @@ use cmaze::{ }; use crate::{ - app::{self, game_state::GameData, GameViewMode}, + app::{game_state::GameData, GameViewMode}, helpers::{ constants, is_release, maze2screen, maze2screen_3d, maze_render_size, strings, LineDir, }, diff --git a/tmaze/src/data/mod.rs b/tmaze/src/data/mod.rs index ab5f05e..473e20f 100644 --- a/tmaze/src/data/mod.rs +++ b/tmaze/src/data/mod.rs @@ -8,7 +8,10 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{helpers::constants::paths::save_data_path, settings::model::{Config, UpdateCheckInterval}}; +use crate::{ + helpers::constants::paths::managed::save_data, + settings::model::{Config, UpdateCheckInterval}, +}; pub mod model { use cmaze::{algorithms::MazeType, dims::Dims3D}; @@ -59,12 +62,12 @@ pub enum SaveDataError { impl SaveData { pub fn load() -> Result { - match Self::load_from(&save_data_path()) { + match Self::load_from(&save_data()) { Ok(data) => Ok(data), Err(SaveDataError::Io(_)) => Ok(SaveData { last_update_check: None, best_results: HashMap::new(), - path: save_data_path(), + path: save_data(), }), Err(err) => Err(err), } @@ -74,7 +77,7 @@ impl SaveData { Self::load().unwrap_or_else(|_| Self { last_update_check: None, best_results: HashMap::new(), - path: save_data_path(), + path: save_data(), }) } diff --git a/tmaze/src/helpers/constants.rs b/tmaze/src/helpers/constants.rs index 8f7d833..f84f033 100644 --- a/tmaze/src/helpers/constants.rs +++ b/tmaze/src/helpers/constants.rs @@ -26,7 +26,7 @@ pub mod paths { use std::path::PathBuf; #[cfg(not(feature = "local_paths"))] - pub fn base_path() -> PathBuf { + pub fn base() -> PathBuf { use dirs::preference_dir; preference_dir().unwrap().join("tmaze") @@ -37,23 +37,36 @@ pub mod paths { PathBuf::from("./") } - pub fn theme_path() -> PathBuf { - base_path().join("themes/") + pub fn theme() -> PathBuf { + base().join("themes/") } - pub fn theme_file_path(theme: &str) -> PathBuf { - theme_path().join(theme) + pub fn theme_file(theme_name: &str) -> PathBuf { + theme().join(theme_name) } - pub fn settings_path() -> PathBuf { - base_path().join("settings.json5") + pub fn config() -> PathBuf { + base().join("settings.json5") } - pub fn save_data_path() -> PathBuf { - base_path().join("data.json") - } + pub mod managed { + use super::base; + use std::path::PathBuf; - pub fn log_file_path() -> PathBuf { - base_path().join("log.txt") + pub fn path() -> PathBuf { + base().join(".managed/") + } + + pub fn ui_settings() -> PathBuf { + path().join("ui_settings.json") + } + + pub fn save_data() -> PathBuf { + path().join("data.json") + } + + pub fn log_file() -> PathBuf { + path().join("log.txt") + } } } diff --git a/tmaze/src/logging.rs b/tmaze/src/logging.rs index 1281d34..04921ff 100644 --- a/tmaze/src/logging.rs +++ b/tmaze/src/logging.rs @@ -17,7 +17,6 @@ use crate::{ settings::{ model::Config, theme::{Color, NamedColor, Style, Theme}, - Settings, }, }; @@ -189,7 +188,7 @@ impl Default for LoggerOptions { Self { decay: DEFAULT_DECAY, max_visible: DEFAULT_MAX_VISIBLE, - path: Some(paths::log_file_path()), + path: Some(paths::managed::log_file()), file_level: log::Level::Debug, } } diff --git a/tmaze/src/main.rs b/tmaze/src/main.rs index 5cfd501..94cdc49 100644 --- a/tmaze/src/main.rs +++ b/tmaze/src/main.rs @@ -1,6 +1,8 @@ +use std::{io::Write, os::unix::ffi::OsStrExt}; + use tmaze::{ app::{app::init_theme_resolver, game::MainMenu, Activity, App, GameError}, - helpers::constants::paths::{save_data_path, settings_path}, + helpers::constants::paths, settings::{theme::TerminalColorScheme, Settings}, }; @@ -66,12 +68,9 @@ fn main() -> Result<(), GameError> { // } if _args.show_config_path { - let settings_path = settings_path(); - if let Some(s) = settings_path.to_str() { - println!("{}", s); - } else { - println!("{:?}", settings_path); - } + let settings_path = paths::config(); + std::io::stdout().write_all(settings_path.as_os_str().as_bytes())?; + std::io::stdout().flush()?; return Ok(()); } @@ -87,7 +86,7 @@ fn main() -> Result<(), GameError> { } if _args.delete_data { - let _ = std::fs::remove_file(save_data_path()); + let _ = std::fs::remove_file(paths::managed::save_data()); return Ok(()); } diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 924cf21..4228621 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -12,7 +12,7 @@ use hashbrown::HashMap; use crate::{ helpers::{constants::paths, TupleMap}, - settings::config_utils::Value, + settings::config_utils::{Mergeable, Value}, }; use model::{Config, PartialConfig}; @@ -36,22 +36,21 @@ impl Settings { } pub fn read(&self) -> &Config { - &self.inner.base + &self.inner.config } } struct SettingsInner { - // ui_layer: PartialConfig, config_layer: PartialConfig, - base: Config, + ui_layer: PartialConfig, + config: Config, } impl SettingsInner { fn load() -> (Self, bool) { let mut errored = false; - let base_config = Config::default(); - let config_layer = match load_config_from_file(&paths::settings_path()) { + let config_layer = match load_config_from_file(&paths::config()) { Ok(config) => config, Err(_err) => { errored = true; @@ -59,12 +58,25 @@ impl SettingsInner { } }; - let config = Self { + let ui_layer = match load_config_from_file(&paths::managed::ui_settings()) { + Ok(config) => config, + Err(_err) => { + errored = true; + PartialConfig::default() + } + }; + + let mut config = Config::default(); + config.merge(&config_layer); + config.merge(&ui_layer); + + let settings = Self { config_layer, - base: base_config, + ui_layer, + config, }; - (config, errored) + (settings, errored) } } @@ -85,6 +97,14 @@ impl Display for ConfigLoadError { } } +fn load_ui_config_from_file(path: &Path) -> PartialConfig { + PartialConfig::try_from( + json5::from_str(&std::fs::read_to_string(path).unwrap_or_default()) + .unwrap_or(Value::Object(HashMap::new())), + ) + .unwrap_or_default() +} + fn load_config_from_file(path: &Path) -> Result { match load_values_from_file(path) { Ok(value) => PartialConfig::try_from(value) diff --git a/tmaze/src/settings/model.rs b/tmaze/src/settings/model.rs index a71f93e..778d199 100644 --- a/tmaze/src/settings/model.rs +++ b/tmaze/src/settings/model.rs @@ -4,7 +4,6 @@ use cmaze::{ algorithms::{MazeSpec, MazeSpecType}, dims::{Dims, Offset}, }; -use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use crate::{ diff --git a/tmaze/src/settings/theme.rs b/tmaze/src/settings/theme.rs index b330215..ef30225 100644 --- a/tmaze/src/settings/theme.rs +++ b/tmaze/src/settings/theme.rs @@ -7,7 +7,7 @@ use thiserror::Error; use crate::{ config, - helpers::{constants::paths::theme_file_path, ToDebug}, + helpers::{constants::paths::theme_file, ToDebug}, }; use super::config_utils::Mergeable; @@ -81,7 +81,7 @@ impl ThemeDefinition { } fn prepare_default_theme() -> Result { - let path = theme_file_path(DEFAULT_THEME_NAME); + let path = theme_file(DEFAULT_THEME_NAME); std::fs::create_dir_all(path.parent().unwrap())?; if !path.exists() { @@ -92,7 +92,7 @@ impl ThemeDefinition { } pub fn load_by_name(name: &str) -> Result { - Self::load_by_path(theme_file_path(name)) + Self::load_by_path(theme_file(name)) } pub fn load_by_path(path: PathBuf) -> Result { diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs index ce4dfc5..82f6225 100644 --- a/tmaze/src/ui/usecase/screens/settings.rs +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -1,9 +1,8 @@ use crate::{ app::{self, app::AppData, Activity, ActivityHandler, Change}, - helpers::constants::paths::settings_path, + helpers::constants::paths::config, menu_actions, renderer::MouseGuard, - settings::Settings, sound::create_audio_settings, ui::{split_menu_actions, Menu, MenuAction, MenuConfig, MenuItem, OptionDef, Popup, Screen}, }; @@ -16,7 +15,7 @@ impl OtherSettingsPopup { "Other settings".to_string(), vec![ "Path to the current settings:".to_string(), - format!(" {}", settings_path().to_string_lossy()), + format!(" {}", config().to_string_lossy()), "".to_string(), "Other settings are not implemented in UI yet.".to_string(), "Please edit the settings file directly.".to_string(), @@ -103,7 +102,7 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Enable mouse input".into(), val: cfg.enable_mouse, - fun: Box::new(|enabled, data| { + fun: Box::new(|enabled, _data| { *enabled = !*enabled; // data.settings.set_enable_mouse(*enabled); }), @@ -111,7 +110,7 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Enable dpad".into(), val: cfg.enable_dpad, - fun: Box::new(|enabled, data| { + fun: Box::new(|enabled, _data| { *enabled = !*enabled; // data.settings.set_enable_dpad(*enabled); }), @@ -119,7 +118,7 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Left-handed dpad".into(), val: cfg.landscape_dpad_on_left, - fun: Box::new(|is_on_left, data| { + fun: Box::new(|is_on_left, _data| { *is_on_left = !*is_on_left; // data.settings.set_landscape_dpad_on_left(*is_on_left); }), @@ -127,7 +126,7 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Swap Up and Down buttons".into(), val: cfg.dpad_swap_up_down, - fun: Box::new(|do_swap, data| { + fun: Box::new(|do_swap, _data| { *do_swap = !*do_swap; // data.settings.set_dpad_swap_up_down(*do_swap); }), @@ -135,7 +134,7 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Enable margin around dpad".into(), val: cfg.enable_margin_around_dpad, - fun: Box::new(|enabled, data| { + fun: Box::new(|enabled, _data| { *enabled = !*enabled; // data.settings.set_enable_margin_around_dpad(*enabled); }), @@ -143,7 +142,7 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Enable dpad highlight".into(), val: cfg.enable_dpad_highlight, - fun: Box::new(|enabled, data| { + fun: Box::new(|enabled, _data| { *enabled = !*enabled; // data.settings.set_enable_dpad_highlight(*enabled); }), From 45fd977c998ed485db8e193883a7b6b10494828e Mon Sep 17 00:00:00 2001 From: ur-fault Date: Fri, 30 Jan 2026 16:52:37 +0100 Subject: [PATCH 08/23] Add: Deref for settings instead of read() --- tmaze/src/app/app.rs | 15 +++++++-------- tmaze/src/app/game.rs | 23 ++++++++++++----------- tmaze/src/app/game_state.rs | 14 +++++++------- tmaze/src/settings/mod.rs | 10 +++++++++- tmaze/src/sound/mod.rs | 6 +++--- tmaze/src/ui/usecase/dpad.rs | 2 +- tmaze/src/ui/usecase/screens/settings.rs | 2 +- tmaze/src/updates.rs | 3 ++- 8 files changed, 42 insertions(+), 33 deletions(-) diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index 51f8884..4013fc7 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -80,7 +80,7 @@ impl AppData { } } - let cfg = &self.settings.read().audio; + let cfg = &self.settings.audio; let volume = if cfg.enable_audio && cfg.enable_music { cfg.audio_volume * cfg.music_volume } else { @@ -136,17 +136,16 @@ impl App { /// - initializes the registries, pub fn empty(read_only: bool) -> Self { let (settings, settings_error) = Settings::load(); - let config = settings.read(); - let renderer = Renderer::new(&Rc::new(config.general.terminal_scheme.clone())) + let renderer = Renderer::new(&Rc::new(settings.general.terminal_scheme.clone())) .expect("failed to create renderer"); let activities = Activities::empty(); let (logger, logs) = AppLogger::new_with_options( - config.general.logging_level, + settings.general.logging_level, LoggerOptions::default() .read_only(read_only) - .file_level(config.general.file_logging_level), + .file_level(settings.general.file_logging_level), ); logger.init(); @@ -177,7 +176,7 @@ impl App { #[cfg(feature = "sound")] let sound_player = SoundPlayer::new(settings.clone()); - let appereance = Appearance::new(&config); + let appereance = Appearance::new(&settings); Self { renderer, @@ -227,7 +226,7 @@ impl App { .. }) => self.switch_debug(), event @ crossterm::event::Event::Mouse(_) => { - if self.data.settings.read().nagivation.enable_mouse { + if self.data.settings.nagivation.enable_mouse { events.push(Event::Term(event)); } } @@ -312,7 +311,7 @@ impl App { fn switch_debug(&mut self) { self.data.use_data.show_debug = !self.data.use_data.show_debug; - self.data.logs.switch_debug(self.data.settings.read()); + self.data.logs.switch_debug(&self.data.settings); log::warn!( "Debug mode: {}", on_off(self.data.use_data.show_debug, false) diff --git a/tmaze/src/app/game.rs b/tmaze/src/app/game.rs index 6ee874a..1084efe 100644 --- a/tmaze/src/app/game.rs +++ b/tmaze/src/app/game.rs @@ -82,7 +82,7 @@ pub struct MainMenu { impl MainMenu { pub fn new() -> Self { let options = menu_actions!( - "New Game" -> data => Self::start_new_game(data.settings.read(), &data.use_data), + "New Game" -> data => Self::start_new_game(&data.settings, &data.use_data), "Settings" -> _ => Self::show_settings_screen(), "Controls" -> _ => Self::show_controls_popup(), "Info" -> _ => Self::show_info_menu(), @@ -525,12 +525,12 @@ pub struct GameActivity { impl GameActivity { pub fn new(game: GameData, app_data: &mut AppData) -> Self { - let config = app_data.settings.read(); + let config = &app_data.settings.viewport; let appear = &app_data.appearance; - let camera_mode = config.viewport.camera_mode; + let camera_mode = config.camera_mode; let maze_board = MazeBoard::new(&game.game, appear.theme(), appear.scheme().clone()); - let margins = config.viewport.viewport_margin; + let margins = config.viewport_margin; #[cfg(feature = "sound")] app_data.play_bgm(MusicTrack::choose_for_maze(game.game.get_maze())); @@ -654,11 +654,12 @@ impl GameActivity { } fn update_viewport(&mut self, data: &AppData) { - let cfg = data.settings.read(); + let cfg = &data.settings.nagivation; + if self.is_dpad_enabled() { let (viewport_rect, dpad_rect) = DPad::split_screen(data); let mut dpad_rect = dpad_rect; - if cfg.nagivation.enable_margin_around_dpad { + if cfg.enable_margin_around_dpad { dpad_rect = dpad_rect.margin(self.margins); } @@ -676,14 +677,14 @@ impl GameActivity { fn init_dpad(&mut self, data: &AppData) { let dpad_type = DPadType::from_maze(self.data.game.get_maze()); - let swap_up_down = data.settings.read().nagivation.dpad_swap_up_down; + let swap_up_down = data.settings.nagivation.dpad_swap_up_down; let touch_controls = DPad::new(None, swap_up_down, dpad_type); self.touch_controls = Some(Box::new(touch_controls)); } fn update_dpad(&mut self, data: &AppData) { - let config = &data.settings.read().nagivation; + let config = &data.settings.nagivation; if (config.enable_dpad && config.enable_mouse) != self.is_dpad_enabled() { if config.enable_dpad { log::info!("Enabling dpad"); @@ -730,7 +731,7 @@ impl ActivityHandler for GameActivity { match event { Event::Term(event) => match event { TermEvent::Key(key_event) => { - match self.data.handle_event(data.settings.read(), key_event) { + match self.data.handle_event(&data.settings, key_event) { Err(false) => { self.data.game.pause().unwrap(); @@ -746,7 +747,7 @@ impl ActivityHandler for GameActivity { TermEvent::Mouse(event) => { if let Some(ref mut touch_controls) = self.touch_controls { if let Some(dir) = touch_controls.apply_mouse_event(event) { - self.data.apply_move(data.settings.read(), dir, false); + self.data.apply_move(&data.settings, dir, false); } } } @@ -800,7 +801,7 @@ impl ActivityHandler for GameActivity { camera_smoothing, player_smoothing, .. - } = data.settings.read().viewport; + } = data.settings.viewport; self.sm_player_pos = lerp!((self.sm_player_pos) -> (maze2screen_3d(self.data.game.get_player_pos())) at player_smoothing); self.sm_camera_pos = diff --git a/tmaze/src/app/game_state.rs b/tmaze/src/app/game_state.rs index 4207830..feb4153 100644 --- a/tmaze/src/app/game_state.rs +++ b/tmaze/src/app/game_state.rs @@ -45,7 +45,7 @@ pub struct GameData { } impl GameData { - pub fn handle_event(&mut self, settings: &Config, event: KeyEvent) -> Result<(), bool> { + pub fn handle_event(&mut self, config: &Config, event: KeyEvent) -> Result<(), bool> { let KeyEvent { code, modifiers, @@ -60,23 +60,23 @@ impl GameData { match code { KeyCode::Up | KeyCode::Char('w' | 'W') => { - self.apply_move(settings, CellWall::Top, is_fast); + self.apply_move(config, CellWall::Top, is_fast); } KeyCode::Down | KeyCode::Char('s' | 'S') => { - self.apply_move(settings, CellWall::Bottom, is_fast); + self.apply_move(config, CellWall::Bottom, is_fast); } KeyCode::Left | KeyCode::Char('a' | 'A') => { - self.apply_move(settings, CellWall::Left, is_fast); + self.apply_move(config, CellWall::Left, is_fast); } KeyCode::Right | KeyCode::Char('d' | 'D') => { - self.apply_move(settings, CellWall::Right, is_fast); + self.apply_move(config, CellWall::Right, is_fast); } KeyCode::Char('Q') => return Err(true), KeyCode::Char('f' | 'q' | 'l') => { - self.apply_move(settings, CellWall::Down, is_fast); + self.apply_move(config, CellWall::Down, is_fast); } KeyCode::Char('r' | 'e' | 'p') => { - self.apply_move(settings, CellWall::Up, is_fast); + self.apply_move(config, CellWall::Up, is_fast); } KeyCode::Char(' ') => { match self.view_mode { diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 4228621..8361152 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -6,7 +6,7 @@ pub mod theme; mod config_utils; -use std::{fmt::Display, path::Path, sync::Arc}; +use std::{fmt::Display, ops::Deref, path::Path, sync::Arc}; use hashbrown::HashMap; @@ -40,6 +40,14 @@ impl Settings { } } +impl Deref for Settings { + type Target = Config; + + fn deref(&self) -> &Self::Target { + &self.inner.config + } +} + struct SettingsInner { config_layer: PartialConfig, ui_layer: PartialConfig, diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index 0824cac..9237c37 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -73,7 +73,7 @@ impl SoundPlayer { return; }; let sink = Sink::try_new(handle).expect("Failed to create sink"); - sink.set_volume(self.settings.read().audio.audio_volume as f32); + sink.set_volume(self.settings.audio.audio_volume as f32); sink.append(track); sink.play(); sink.detach(); @@ -91,7 +91,7 @@ impl SoundPlayer { pub fn create_audio_settings(data: &mut AppData) -> Activity { fn update_vol(data: &mut AppData) { - let cfg = &data.settings.read().audio; + let cfg = &data.settings.audio; if cfg.enable_audio && cfg.enable_music { data.sound_player @@ -103,7 +103,7 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { // FIXME: re-add settings updating once supported - let config = &data.settings.read().audio; + let config = &data.settings.audio; let menu_config = menu::MenuConfig::new( "Audio settings", diff --git a/tmaze/src/ui/usecase/dpad.rs b/tmaze/src/ui/usecase/dpad.rs index 19dd2c1..267c631 100644 --- a/tmaze/src/ui/usecase/dpad.rs +++ b/tmaze/src/ui/usecase/dpad.rs @@ -119,7 +119,7 @@ impl DPad { if is_vertical { screen_rect.split_y_end(Offset::Abs(dpad_size)) } else { - let on_left = data.settings.read().nagivation.landscape_dpad_on_left; + let on_left = data.settings.nagivation.landscape_dpad_on_left; let offset = Offset::Abs(dpad_size); if on_left { diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs index 82f6225..041cbc1 100644 --- a/tmaze/src/ui/usecase/screens/settings.rs +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -94,7 +94,7 @@ impl ActivityHandler for SettingsActivity { } pub fn create_controls_settings(data: &mut AppData) -> Activity { - let cfg = &data.settings.read().nagivation; + let cfg = &data.settings.nagivation; let menu_config = MenuConfig::new( "Controls settings", diff --git a/tmaze/src/updates.rs b/tmaze/src/updates.rs index d2f0a60..96df243 100644 --- a/tmaze/src/updates.rs +++ b/tmaze/src/updates.rs @@ -34,7 +34,8 @@ pub async fn get_newer_async() -> Result, CratesError> { } pub fn check(app_data: &mut AppData) { - let cfg = app_data.settings.read(); + let cfg = &app_data.settings; + if app_data.save.is_update_checked(cfg) { return; } From b3e8f6f976bdc74a25f21f73f9512f95c9923462 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sat, 31 Jan 2026 12:10:10 +0100 Subject: [PATCH 09/23] Revert: back to Settings::read(), Add: show settings errors --- tmaze/src/app/app.rs | 40 +++++++++---- tmaze/src/app/game.rs | 46 ++++++++------- tmaze/src/helpers/constants.rs | 4 ++ tmaze/src/main.rs | 25 ++++---- tmaze/src/settings/mod.rs | 73 +++++++++++++++--------- tmaze/src/sound/mod.rs | 6 +- tmaze/src/ui/usecase/dpad.rs | 2 +- tmaze/src/ui/usecase/screens/settings.rs | 2 +- tmaze/src/updates.rs | 2 +- 9 files changed, 124 insertions(+), 76 deletions(-) diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index 4013fc7..42fee54 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -17,7 +17,7 @@ use crossterm::event::{read, KeyCode, KeyEvent, KeyEventKind}; use crate::{ data::SaveData, - helpers::on_off, + helpers::{constants::paths, on_off}, logging::{self, AppLogger, LoggerOptions, UiLogs}, renderer::{self, draw::Draw, CellContent, GMutView, Renderer}, settings::{ @@ -80,7 +80,7 @@ impl AppData { } } - let cfg = &self.settings.audio; + let cfg = &self.settings.read().audio; let volume = if cfg.enable_audio && cfg.enable_music { cfg.audio_volume * cfg.music_volume } else { @@ -135,22 +135,31 @@ impl App { /// - initializes the job queue, /// - initializes the registries, pub fn empty(read_only: bool) -> Self { - let (settings, settings_error) = Settings::load(); + if !read_only { + Self::prepare_dirs() + .expect("Failed to prepare application directories. Please check permissions."); + } + + let (settings, settings_errors) = Settings::load(); + let config = settings.read(); - let renderer = Renderer::new(&Rc::new(settings.general.terminal_scheme.clone())) + let renderer = Renderer::new(&Rc::new(config.general.terminal_scheme.clone())) .expect("failed to create renderer"); let activities = Activities::empty(); let (logger, logs) = AppLogger::new_with_options( - settings.general.logging_level, + config.general.logging_level, LoggerOptions::default() .read_only(read_only) - .file_level(settings.general.file_logging_level), + .file_level(config.general.file_logging_level), ); logger.init(); - if settings_error { + if let Some(errors) = settings_errors { log::error!("Errors were encountered while loading the config."); + for err in errors { + log::error!(" - {}", err); + } } let save = SaveData::load().expect("failed to load save data"); @@ -176,7 +185,9 @@ impl App { #[cfg(feature = "sound")] let sound_player = SoundPlayer::new(settings.clone()); - let appereance = Appearance::new(&settings); + let appereance = Appearance::new(&config); + + drop(config); Self { renderer, @@ -212,6 +223,7 @@ impl App { let mut events = vec![]; + // FIXME: better polling strategy, IO will need faster response times let mut delay = Duration::from_millis(45); while let Ok(true) = crossterm::event::poll(delay) { let event = read().unwrap(); @@ -226,7 +238,7 @@ impl App { .. }) => self.switch_debug(), event @ crossterm::event::Event::Mouse(_) => { - if self.data.settings.nagivation.enable_mouse { + if self.data.settings.read().nagivation.enable_mouse { events.push(Event::Term(event)); } } @@ -311,13 +323,21 @@ impl App { fn switch_debug(&mut self) { self.data.use_data.show_debug = !self.data.use_data.show_debug; - self.data.logs.switch_debug(&self.data.settings); + self.data.logs.switch_debug(&self.data.settings.read()); log::warn!( "Debug mode: {}", on_off(self.data.use_data.show_debug, false) ); } + fn prepare_dirs() -> std::io::Result<()> { + for dir in paths::all_dirs() { + std::fs::create_dir_all(&dir)?; + } + + Ok(()) + } + pub fn activity_count(&self) -> usize { self.activities.len() } diff --git a/tmaze/src/app/game.rs b/tmaze/src/app/game.rs index 1084efe..7b740a6 100644 --- a/tmaze/src/app/game.rs +++ b/tmaze/src/app/game.rs @@ -82,7 +82,7 @@ pub struct MainMenu { impl MainMenu { pub fn new() -> Self { let options = menu_actions!( - "New Game" -> data => Self::start_new_game(&data.settings, &data.use_data), + "New Game" -> data => Self::start_new_game(&data.settings.read(), &data.use_data), "Settings" -> _ => Self::show_settings_screen(), "Controls" -> _ => Self::show_controls_popup(), "Info" -> _ => Self::show_info_menu(), @@ -525,12 +525,14 @@ pub struct GameActivity { impl GameActivity { pub fn new(game: GameData, app_data: &mut AppData) -> Self { - let config = &app_data.settings.viewport; + let config = app_data.settings.read(); + let viewport_cfg = &config.viewport; let appear = &app_data.appearance; - let camera_mode = config.camera_mode; + let camera_mode = viewport_cfg.camera_mode; let maze_board = MazeBoard::new(&game.game, appear.theme(), appear.scheme().clone()); - let margins = config.viewport_margin; + let margins = viewport_cfg.viewport_margin; + drop(config); #[cfg(feature = "sound")] app_data.play_bgm(MusicTrack::choose_for_maze(game.game.get_maze())); @@ -654,7 +656,7 @@ impl GameActivity { } fn update_viewport(&mut self, data: &AppData) { - let cfg = &data.settings.nagivation; + let cfg = &data.settings.read().nagivation; if self.is_dpad_enabled() { let (viewport_rect, dpad_rect) = DPad::split_screen(data); @@ -677,14 +679,14 @@ impl GameActivity { fn init_dpad(&mut self, data: &AppData) { let dpad_type = DPadType::from_maze(self.data.game.get_maze()); - let swap_up_down = data.settings.nagivation.dpad_swap_up_down; + let swap_up_down = data.settings.read().nagivation.dpad_swap_up_down; let touch_controls = DPad::new(None, swap_up_down, dpad_type); self.touch_controls = Some(Box::new(touch_controls)); } fn update_dpad(&mut self, data: &AppData) { - let config = &data.settings.nagivation; + let config = &data.settings.read().nagivation; if (config.enable_dpad && config.enable_mouse) != self.is_dpad_enabled() { if config.enable_dpad { log::info!("Enabling dpad"); @@ -719,6 +721,8 @@ impl ActivityHandler for GameActivity { _ => {} } + let config = data.settings.read(); + self.update_dpad(data); self.update_viewport(data); @@ -730,24 +734,22 @@ impl ActivityHandler for GameActivity { #[allow(clippy::single_match)] match event { Event::Term(event) => match event { - TermEvent::Key(key_event) => { - match self.data.handle_event(&data.settings, key_event) { - Err(false) => { - self.data.game.pause().unwrap(); - - return Some(Change::push(Activity::new_base_boxed( - "pause".to_string(), - PauseMenu::new(), - ))); - } - Err(true) => return Some(Change::pop_until("main menu")), - Ok(_) => {} + TermEvent::Key(key_event) => match self.data.handle_event(&config, key_event) { + Err(false) => { + self.data.game.pause().unwrap(); + + return Some(Change::push(Activity::new_base_boxed( + "pause".to_string(), + PauseMenu::new(), + ))); } - } + Err(true) => return Some(Change::pop_until("main menu")), + Ok(_) => {} + }, TermEvent::Mouse(event) => { if let Some(ref mut touch_controls) = self.touch_controls { if let Some(dir) = touch_controls.apply_mouse_event(event) { - self.data.apply_move(&data.settings, dir, false); + self.data.apply_move(&config, dir, false); } } } @@ -801,7 +803,7 @@ impl ActivityHandler for GameActivity { camera_smoothing, player_smoothing, .. - } = data.settings.viewport; + } = data.settings.read().viewport; self.sm_player_pos = lerp!((self.sm_player_pos) -> (maze2screen_3d(self.data.game.get_player_pos())) at player_smoothing); self.sm_camera_pos = diff --git a/tmaze/src/helpers/constants.rs b/tmaze/src/helpers/constants.rs index f84f033..ec46aef 100644 --- a/tmaze/src/helpers/constants.rs +++ b/tmaze/src/helpers/constants.rs @@ -49,6 +49,10 @@ pub mod paths { base().join("settings.json5") } + pub fn all_dirs() -> impl Iterator { + vec![theme(), managed::path()].into_iter() + } + pub mod managed { use super::base; use std::path::PathBuf; diff --git a/tmaze/src/main.rs b/tmaze/src/main.rs index 94cdc49..703e3be 100644 --- a/tmaze/src/main.rs +++ b/tmaze/src/main.rs @@ -60,49 +60,52 @@ enum StylesPrintMode { } fn main() -> Result<(), GameError> { - let _args = Args::parse(); + let args = Args::parse(); // if _args.reset_config { // Settings::reset_json_config(settings_path()); // return Ok(()); // } - if _args.show_config_path { + if args.show_config_path { let settings_path = paths::config(); std::io::stdout().write_all(settings_path.as_os_str().as_bytes())?; std::io::stdout().flush()?; return Ok(()); } - if _args.debug_config { - let (config, errd) = Settings::load(); - if errd { + if args.debug_config { + let (config, errors) = Settings::load(); + if let Some(errors) = errors { eprintln!("Warning: Errors were encountered while loading the config."); + for error in errors { + eprintln!("- {}", error); + } } - println!("{:#?}", config.read()); + println!("{:#?}", *config.read()); return Ok(()); } - if _args.delete_data { + if args.delete_data { let _ = std::fs::remove_file(paths::managed::save_data()); return Ok(()); } - if let Some(mode) = _args.print_theme_options { - print_style_options(mode.unwrap_or_default(), _args.counted_styles); + if let Some(mode) = args.print_theme_options { + print_style_options(mode.unwrap_or_default(), args.counted_styles); return Ok(()); } - if _args.print_terminal_schemes { + if args.print_terminal_schemes { print_builtin_terminal_schemes(); return Ok(()); } better_panic::install(); - let mut app = App::empty(_args.read_only); + let mut app = App::empty(args.read_only); let menu = MainMenu::new(); app.activities_mut() .push(Activity::new_base_boxed("main menu", menu)); diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 8361152..1f8387f 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -6,7 +6,12 @@ pub mod theme; mod config_utils; -use std::{fmt::Display, ops::Deref, path::Path, sync::Arc}; +use std::{ + fmt::Display, + ops::Deref, + path::Path, + sync::{Arc, Mutex}, +}; use hashbrown::HashMap; @@ -29,62 +34,76 @@ impl Settings { /// loading. /// /// TODO: Report the actual errors/warnings to the user. - pub fn load() -> (Self, bool) { + pub fn load() -> (Self, Option>) { SettingsInner::load().map_first(|inner| Self { inner: Arc::new(inner), }) } - pub fn read(&self) -> &Config { - &self.inner.config + pub fn read(&self) -> impl Deref + use<'_> { + self.inner.config.lock().unwrap() } -} -impl Deref for Settings { - type Target = Config; - - fn deref(&self) -> &Self::Target { - &self.inner.config + pub fn update_ui(&self, with: impl FnOnce(&mut PartialConfig)) { + let mut ui_layer = self.inner.ui_layer.lock().unwrap(); + with(&mut ui_layer); + self.inner.rebuild(); } } struct SettingsInner { config_layer: PartialConfig, - ui_layer: PartialConfig, - config: Config, + ui_layer: Mutex, + config: Mutex, } impl SettingsInner { - fn load() -> (Self, bool) { - let mut errored = false; + fn load() -> (Self, Option>) { + let mut errors = vec![]; let config_layer = match load_config_from_file(&paths::config()) { Ok(config) => config, - Err(_err) => { - errored = true; - PartialConfig::default() + Err((err, config)) => { + errors.push(format!( + "Failed to load user config, please check for syntax or invalid options. {err}" + )); + config } }; let ui_layer = match load_config_from_file(&paths::managed::ui_settings()) { Ok(config) => config, - Err(_err) => { - errored = true; - PartialConfig::default() + Err((err, config)) => { + errors.push(format!( + "Failed to load UI settings, possible corruption. {err}" + )); + config } }; - let mut config = Config::default(); - config.merge(&config_layer); - config.merge(&ui_layer); - let settings = Self { config_layer, - ui_layer, - config, + ui_layer: Mutex::new(ui_layer), + config: Mutex::new(Config::default()), }; - (settings, errored) + settings.rebuild(); + + let errors = if errors.is_empty() { + None + } else { + Some(errors) + }; + + (settings, errors) + } + + fn rebuild(&self) { + let mut config = Config::default(); + config.merge(&self.config_layer); + config.merge(&self.ui_layer.lock().unwrap()); + + *self.config.lock().unwrap() = config; } } diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index 9237c37..0824cac 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -73,7 +73,7 @@ impl SoundPlayer { return; }; let sink = Sink::try_new(handle).expect("Failed to create sink"); - sink.set_volume(self.settings.audio.audio_volume as f32); + sink.set_volume(self.settings.read().audio.audio_volume as f32); sink.append(track); sink.play(); sink.detach(); @@ -91,7 +91,7 @@ impl SoundPlayer { pub fn create_audio_settings(data: &mut AppData) -> Activity { fn update_vol(data: &mut AppData) { - let cfg = &data.settings.audio; + let cfg = &data.settings.read().audio; if cfg.enable_audio && cfg.enable_music { data.sound_player @@ -103,7 +103,7 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { // FIXME: re-add settings updating once supported - let config = &data.settings.audio; + let config = &data.settings.read().audio; let menu_config = menu::MenuConfig::new( "Audio settings", diff --git a/tmaze/src/ui/usecase/dpad.rs b/tmaze/src/ui/usecase/dpad.rs index 267c631..19dd2c1 100644 --- a/tmaze/src/ui/usecase/dpad.rs +++ b/tmaze/src/ui/usecase/dpad.rs @@ -119,7 +119,7 @@ impl DPad { if is_vertical { screen_rect.split_y_end(Offset::Abs(dpad_size)) } else { - let on_left = data.settings.nagivation.landscape_dpad_on_left; + let on_left = data.settings.read().nagivation.landscape_dpad_on_left; let offset = Offset::Abs(dpad_size); if on_left { diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs index 041cbc1..82f6225 100644 --- a/tmaze/src/ui/usecase/screens/settings.rs +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -94,7 +94,7 @@ impl ActivityHandler for SettingsActivity { } pub fn create_controls_settings(data: &mut AppData) -> Activity { - let cfg = &data.settings.nagivation; + let cfg = &data.settings.read().nagivation; let menu_config = MenuConfig::new( "Controls settings", diff --git a/tmaze/src/updates.rs b/tmaze/src/updates.rs index 96df243..70a1545 100644 --- a/tmaze/src/updates.rs +++ b/tmaze/src/updates.rs @@ -34,7 +34,7 @@ pub async fn get_newer_async() -> Result, CratesError> { } pub fn check(app_data: &mut AppData) { - let cfg = &app_data.settings; + let cfg = &app_data.settings.read(); if app_data.save.is_update_checked(cfg) { return; From 1b3c9668d61da918e89602def70318599d708e99 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sat, 31 Jan 2026 12:14:17 +0100 Subject: [PATCH 10/23] Update: PresetList -> tuple struct --- tmaze/src/settings/model.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tmaze/src/settings/model.rs b/tmaze/src/settings/model.rs index 778d199..8a11466 100644 --- a/tmaze/src/settings/model.rs +++ b/tmaze/src/settings/model.rs @@ -102,22 +102,20 @@ pub enum UpdateCheckInterval { } #[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct PresetList { - #[serde(flatten)] - presets: Vec, -} +#[serde(transparent)] +pub struct PresetList(Vec); impl Deref for PresetList { type Target = [MazePreset]; fn deref(&self) -> &Self::Target { - &self.presets + &self.0 } } impl Mergeable for PresetList { fn merge(&mut self, other: &Self) { - self.presets.extend_from_slice(&other.presets); + self.0.extend_from_slice(&other.0); } } From 7f99d38a80565eff7c2a055ce7ac8f3eb8b6af72 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sat, 31 Jan 2026 12:55:30 +0100 Subject: [PATCH 11/23] Fix: don't report missing ui settings file --- tmaze/src/settings/config_utils.rs | 52 ++++++++++++++++++++++++++ tmaze/src/settings/mod.rs | 59 +++--------------------------- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index da74e63..b415638 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -143,3 +143,55 @@ pub enum Value { Bool(bool), String(String), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_value_deserialize() { + let json_data = r#" + { + "name": "Example", + "enabled": true, + "threshold": 10.5, + "count": 42, + "items": [1, 2, 3], + "settings": { + "option1": "value1", + "option2": false + } + } + "#; + + let parsed: Value = serde_json::from_str(json_data).unwrap(); + + if let Value::Object(map) = parsed { + assert_eq!(map.get("name"), Some(&Value::String("Example".to_string()))); + assert_eq!(map.get("enabled"), Some(&Value::Bool(true))); + assert_eq!(map.get("threshold"), Some(&Value::Float(10.5))); + assert_eq!(map.get("count"), Some(&Value::Int(42))); + + if let Some(Value::List(items)) = map.get("items") { + assert_eq!(items.len(), 3); + assert_eq!(items[0], Value::Int(1)); + assert_eq!(items[1], Value::Int(2)); + assert_eq!(items[2], Value::Int(3)); + } else { + panic!("Expected 'items' to be a list"); + } + + if let Some(Value::Object(settings)) = map.get("settings") { + assert_eq!( + settings.get("option1"), + Some(&Value::String("value1".to_string())) + ); + assert_eq!(settings.get("option2"), Some(&Value::Bool(false))); + } else { + panic!("Expected 'settings' to be an object"); + } + } else { + panic!("Expected top-level value to be an object"); + } + } +} diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 1f8387f..f8305ae 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -8,6 +8,7 @@ mod config_utils; use std::{ fmt::Display, + io, ops::Deref, path::Path, sync::{Arc, Mutex}, @@ -73,6 +74,9 @@ impl SettingsInner { let ui_layer = match load_config_from_file(&paths::managed::ui_settings()) { Ok(config) => config, + Err((ConfigLoadError::IoError(err), _)) if err.kind() == io::ErrorKind::NotFound => { + PartialConfig::default() + } Err((err, config)) => { errors.push(format!( "Failed to load UI settings, possible corruption. {err}" @@ -169,7 +173,8 @@ fn load_extension_blocks(config: Value) -> Result { @@ -216,55 +221,3 @@ fn load_extension_blocks(config: Value) -> Result Date: Sat, 31 Jan 2026 12:56:46 +0100 Subject: [PATCH 12/23] Fix: remove useless Partial -> Config conv --- tmaze/src/settings/config_utils.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index b415638..690b390 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -90,14 +90,6 @@ macro_rules! config { } } - impl From<&[]> for $name { - fn from(partial: &[]) -> Self { - let mut config = Self::default(); - config.merge(partial); - config - } - } - impl TryFrom<$crate::settings::config_utils::Value> for [] { type Error = (String, Self); From c5064cf799408fa26489ab36bbbcf41158c7b69c Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sat, 31 Jan 2026 23:01:48 +0100 Subject: [PATCH 13/23] Add: specific problems reporting, Fix: wrong preset format --- cmaze/src/algorithms/types.rs | 2 - tmaze/src/main.rs | 1 + tmaze/src/settings/config_utils.rs | 184 +++++++++++++++++++++++++---- tmaze/src/settings/mod.rs | 52 +++----- tmaze/src/settings/model.rs | 43 ++++++- 5 files changed, 221 insertions(+), 61 deletions(-) diff --git a/cmaze/src/algorithms/types.rs b/cmaze/src/algorithms/types.rs index 6154f4c..b6bd2f5 100644 --- a/cmaze/src/algorithms/types.rs +++ b/cmaze/src/algorithms/types.rs @@ -78,8 +78,6 @@ pub type Algorithm = (String, Params); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MazeSpec { - // /// Size of the maze. - // pub size: Dims3D, /// Specification of the maze. #[serde(default, flatten)] pub inner_spec: MazeSpecType, diff --git a/tmaze/src/main.rs b/tmaze/src/main.rs index 703e3be..a649e38 100644 --- a/tmaze/src/main.rs +++ b/tmaze/src/main.rs @@ -70,6 +70,7 @@ fn main() -> Result<(), GameError> { if args.show_config_path { let settings_path = paths::config(); std::io::stdout().write_all(settings_path.as_os_str().as_bytes())?; + std::io::stdout().write_all(b"\n")?; std::io::stdout().flush()?; return Ok(()); } diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index 690b390..ec90ce4 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use hashbrown::HashMap; use serde::{Deserialize, Serialize}; @@ -5,6 +7,93 @@ pub trait Mergeable { fn merge(&mut self, other: &O); } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Segment { + Key(String), + Index(usize), +} + +impl Display for Segment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Segment::Key(key) => write!(f, "{}", key), + Segment::Index(index) => write!(f, "[{}]", index), + } + } +} + +pub type Path = Vec; + +#[derive(Clone, Debug)] +pub struct ConvertError { + // TODO: Add source file/line info + pub path: Path, + pub detail: String, +} + +impl std::fmt::Display for ConvertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let path_str = self.path.iter().map(Segment::to_string).collect::>(); + let path_str = path_str.join("."); + write!(f, "'{}': {}", path_str, self.detail) + } +} + +pub struct ConvertContext { + pub path: Path, + pub errors: Vec, +} + +impl ConvertContext { + pub fn new() -> Self { + Self { + path: vec![], + errors: vec![], + } + } + + pub fn at(&mut self, segment: Segment, inside: impl FnOnce(&mut Self) -> T) -> T { + self.push(segment); + let t = inside(self); + self.pop(); + t + } + + pub fn push(&mut self, segment: Segment) { + self.path.push(segment); + } + + pub fn push_key(&mut self, key: &str) { + self.path.push(Segment::Key(key.to_string())); + } + + pub fn push_index(&mut self, index: usize) { + self.path.push(Segment::Index(index)); + } + + pub fn pop(&mut self) { + self.path.pop(); + } + + pub fn err(&mut self, detail: String) { + self.errors.push(ConvertError { + path: self.path.clone(), + detail, + }); + } + + pub fn errors(self) -> Vec { + self.errors + } +} + +pub trait LenientConvert: Sized { + fn convert(value: Value, context: &mut ConvertContext) -> Option; +} + +pub trait ConfigValue: Mergeable + LenientConvert + Default {} +impl ConfigValue for T where T: Mergeable + LenientConvert + Default {} + #[macro_export] macro_rules! config { (@step $name:ident @@ -14,12 +103,14 @@ macro_rules! config { // this branch must be 1st so that #[nest] is parsed as special attribute { #[nest] $(#[$attr:meta])* $field:ident : $type:ty, $($rest:tt)* } ) => { - config!{ @step - $name - [ $($fields)* $field = ::std::default::Default::default(), ] - [ $($rfields)* pub $field : $type, ] - [ $($pfields)* $(#[$attr])* pub $field : Option<[]>, ] - { $($rest)* } + ::paste::paste! { + config!{ @step + $name + [ $($fields)* $field : [] = ::std::default::Default::default(), ] + [ $($rfields)* pub $field : $type, ] + [ $($pfields)* $(#[$attr])* pub $field : Option<[]>, ] + { $($rest)* } + } } }; @@ -31,7 +122,7 @@ macro_rules! config { ) => { config!{ @step $name - [ $($fields)* $field = ::std::default::Default::default(), ] + [ $($fields)* $field : $type = ::std::default::Default::default(), ] [ $($rfields)* pub $field : $type, ] [ $($pfields)* $(#[$attr])* pub $field : Option<$type>, ] { $($rest)* } @@ -46,7 +137,7 @@ macro_rules! config { ) => { config!{ @step $name - [ $($fields)* $field = ($def), ] + [ $($fields)* $field : $type = ($def), ] [ $($rfields)* pub $field : $type, ] [ $($pfields)* $(#[$attr])* pub $field : Option<$type>, ] { $($rest)* } @@ -54,7 +145,7 @@ macro_rules! config { }; (@step $name:ident - [$($fields:ident = $def_vals:expr),* ,] + [$($fields:ident : $type:ty = $def_vals:expr),* ,] [$($rfields:tt)*] [$($pfields:tt)*] { } @@ -67,9 +158,7 @@ macro_rules! config { impl ::std::default::Default for $name { fn default() -> Self { Self { - $( - $fields : $def_vals, - )* + $($fields : $def_vals,)* } } } @@ -90,19 +179,30 @@ macro_rules! config { } } - impl TryFrom<$crate::settings::config_utils::Value> for [] { - type Error = (String, Self); + impl $crate::settings::config_utils::LenientConvert for [] { + fn convert( + value: super::config_utils::Value, + context: &mut super::config_utils::ConvertContext, + ) -> Option { + let super::config_utils::Value::Object(mut map) = value else { + context.err("expected an object".to_string()); + return None; + }; - fn try_from(value: $crate::settings::config_utils::Value) -> Result { - match value { - $crate::settings::config_utils::Value::Object(map) => { - let json_value = ::serde_json::to_value(map) - .expect("Failed to convert map to JSON value"); // should not happen - ::serde_json::from_value(json_value) - .map_err(|e| (e.to_string(), Self::default())) - } - _ => Err(("Expected an object for partial config".to_string(), Self::default())), - } + Some(Self { + $( + $fields: { + if let Some(value) = map.remove(stringify!($fields)) { + context.at( + $crate::settings::config_utils::Segment::Key(stringify!($fields).to_string()), + |ctx| <$type as $crate::settings::config_utils::LenientConvert>::convert(value, ctx) + ) + } else { + None + } + }, + )* + }) } } } @@ -124,6 +224,42 @@ macro_rules! impl_merge_prims { }; } +#[macro_export] +macro_rules! impl_lenient_prims { + ($($t:ty => $($variant:ident)+),* $(,)?) => { + $(impl $crate::settings::config_utils::LenientConvert for $t { + fn convert(value: super::config_utils::Value, context: &mut super::config_utils::ConvertContext) -> Option { + match value { + $(super::config_utils::Value::$variant(v) => Some(v as $t),)+ + _ => { + context.err(format!("expected one of: {}", stringify!($($variant),+))); + None + } + } + } + })* + }; +} + +#[macro_export] +macro_rules! impl_lenient_deserialize { + ($($t:ty)*) => { + $(impl $crate::settings::config_utils::LenientConvert for $t { + fn convert(value: super::config_utils::Value, context: &mut super::config_utils::ConvertContext) -> Option { + let json_value = ::serde_json::to_value(&value) + .expect("Failed to convert Value to JSON value"); // should not happen + match ::serde_json::from_value::<$t>(json_value) { + Ok(v) => Some(v), + Err(e) => { + context.err(format!("deserialization error: {}", e)); + None + } + } + } + })* + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(untagged)] // Note: order of variants matters for correct deserialization diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index f8305ae..7400d7d 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -18,7 +18,7 @@ use hashbrown::HashMap; use crate::{ helpers::{constants::paths, TupleMap}, - settings::config_utils::{Mergeable, Value}, + settings::config_utils::{ConvertContext, ConvertError, LenientConvert, Mergeable, Value}, }; use model::{Config, PartialConfig}; @@ -62,28 +62,10 @@ impl SettingsInner { fn load() -> (Self, Option>) { let mut errors = vec![]; - let config_layer = match load_config_from_file(&paths::config()) { - Ok(config) => config, - Err((err, config)) => { - errors.push(format!( - "Failed to load user config, please check for syntax or invalid options. {err}" - )); - config - } - }; + let (config_layer, load_errors) = load_config_from_file(&paths::config()); + errors.extend(load_errors.iter().map(ConvertError::to_string)); - let ui_layer = match load_config_from_file(&paths::managed::ui_settings()) { - Ok(config) => config, - Err((ConfigLoadError::IoError(err), _)) if err.kind() == io::ErrorKind::NotFound => { - PartialConfig::default() - } - Err((err, config)) => { - errors.push(format!( - "Failed to load UI settings, possible corruption. {err}" - )); - config - } - }; + let ui_layer = load_ui_config_from_file(&paths::managed::ui_settings()); let settings = Self { config_layer, @@ -129,19 +111,23 @@ impl Display for ConfigLoadError { } fn load_ui_config_from_file(path: &Path) -> PartialConfig { - PartialConfig::try_from( - json5::from_str(&std::fs::read_to_string(path).unwrap_or_default()) - .unwrap_or(Value::Object(HashMap::new())), - ) - .unwrap_or_default() + std::fs::read_to_string(path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() } -fn load_config_from_file(path: &Path) -> Result { - match load_values_from_file(path) { - Ok(value) => PartialConfig::try_from(value) - .map_err(|(e, val)| (ConfigLoadError::SettingsFormatError(e), val)), - Err((e, val)) => Err((e, PartialConfig::try_from(val).unwrap_or_default())), - } +fn load_config_from_file(path: &Path) -> (PartialConfig, Vec) { + let mut context = ConvertContext::new(); + let config = match load_values_from_file(path) { + Ok(value) => PartialConfig::convert(value, &mut context), + Err((e, val)) => { + context.err(format!("Failed to load config: {}", e)); + PartialConfig::convert(val, &mut context) + } + }; + + (config.unwrap_or_default(), context.errors()) } fn load_values_from_file(path: &Path) -> Result { diff --git a/tmaze/src/settings/model.rs b/tmaze/src/settings/model.rs index 8a11466..2a5d9a9 100644 --- a/tmaze/src/settings/model.rs +++ b/tmaze/src/settings/model.rs @@ -7,9 +7,9 @@ use cmaze::{ use serde::{Deserialize, Serialize}; use crate::{ - config, impl_merge_prims, + config, impl_lenient_deserialize, impl_lenient_prims, impl_merge_prims, settings::{ - config_utils::Mergeable, + config_utils::{ConvertContext, LenientConvert, Mergeable, Value}, theme::{PartialTerminalColorScheme, TerminalColorScheme}, }, }; @@ -77,6 +77,22 @@ impl_merge_prims! { UpdateCheckInterval } +impl_lenient_prims! { + i64 => Int, + bool => Bool, + f64 => Float Int, + String => String, +} + +impl_lenient_deserialize! { + log::Level + CameraMode + UpdateCheckInterval + MazePreset + Dims + Rgb +} + type Rgb = (u8, u8, u8); #[derive(Default, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] @@ -119,13 +135,36 @@ impl Mergeable for PresetList { } } +impl LenientConvert for PresetList { + fn convert(value: Value, context: &mut ConvertContext) -> Option { + let Value::List(list) = value else { + context.err("expected a list of maze presets".to_string()); + return None; + }; + + let mut presets = vec![]; + for (i, item) in list.into_iter().enumerate() { + context.push_index(i); + match MazePreset::convert(item, context) { + Some(preset) => presets.push(preset), + None => { /* error already recorded */ } + } + context.pop(); + } + + Some(PresetList(presets)) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MazePreset { pub title: String, pub description: Option, + #[serde(default)] pub default: bool, + #[serde(flatten)] pub maze_spec: MazeSpec, } From e5b708ed3e21b88602ce4feb4a2da79afc735aeb Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sat, 31 Jan 2026 23:03:25 +0100 Subject: [PATCH 14/23] Fix: code style/format --- cmaze/src/array.rs | 3 ++- tmaze/src/helpers/strings.rs | 4 +++- tmaze/src/settings/config_utils.rs | 4 +--- tmaze/src/settings/mod.rs | 1 - tmaze/src/settings/theme.rs | 2 -- tmaze/src/ui/usecase/screens/mod.rs | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/cmaze/src/array.rs b/cmaze/src/array.rs index ede105b..893a6db 100644 --- a/cmaze/src/array.rs +++ b/cmaze/src/array.rs @@ -51,7 +51,8 @@ impl Array3D { } pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut T> { - self.dim_to_idx(pos.into()).and_then(move |i| self.buf.get_mut(i)) + self.dim_to_idx(pos.into()) + .and_then(move |i| self.buf.get_mut(i)) } } diff --git a/tmaze/src/helpers/strings.rs b/tmaze/src/helpers/strings.rs index 8a084ef..71b75a8 100644 --- a/tmaze/src/helpers/strings.rs +++ b/tmaze/src/helpers/strings.rs @@ -10,7 +10,9 @@ use substring::Substring; use unicode_width::UnicodeWidthStr as _; use crate::{ - renderer::{draw::Draw, GMutView}, settings::theme::Style, ui::draw_str + renderer::{draw::Draw, GMutView}, + settings::theme::Style, + ui::draw_str, }; pub fn trim_center(text: &str, width: usize) -> &str { diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index ec90ce4..955c750 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -63,6 +63,7 @@ impl ConvertContext { self.path.push(segment); } + #[allow(dead_code)] pub fn push_key(&mut self, key: &str) { self.path.push(Segment::Key(key.to_string())); } @@ -91,9 +92,6 @@ pub trait LenientConvert: Sized { fn convert(value: Value, context: &mut ConvertContext) -> Option; } -pub trait ConfigValue: Mergeable + LenientConvert + Default {} -impl ConfigValue for T where T: Mergeable + LenientConvert + Default {} - #[macro_export] macro_rules! config { (@step $name:ident diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 7400d7d..f3777b8 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -8,7 +8,6 @@ mod config_utils; use std::{ fmt::Display, - io, ops::Deref, path::Path, sync::{Arc, Mutex}, diff --git a/tmaze/src/settings/theme.rs b/tmaze/src/settings/theme.rs index ef30225..e26c76e 100644 --- a/tmaze/src/settings/theme.rs +++ b/tmaze/src/settings/theme.rs @@ -10,8 +10,6 @@ use crate::{ helpers::{constants::paths::theme_file, ToDebug}, }; -use super::config_utils::Mergeable; - use super::attribute::deserialize_attributes; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/tmaze/src/ui/usecase/screens/mod.rs b/tmaze/src/ui/usecase/screens/mod.rs index a8efbe3..636e716 100644 --- a/tmaze/src/ui/usecase/screens/mod.rs +++ b/tmaze/src/ui/usecase/screens/mod.rs @@ -1,2 +1,2 @@ -pub mod style_browser; pub mod settings; +pub mod style_browser; From 9900ee5bfa8519eacf4fbfb2e75de3b1779c891b Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 1 Feb 2026 11:11:27 +0100 Subject: [PATCH 15/23] Update: mutex in settings -> arc swap --- Cargo.lock | 10 ++++++++++ tmaze/Cargo.toml | 3 ++- tmaze/src/app/app.rs | 5 +---- tmaze/src/settings/mod.rs | 39 +++++++++++++++++++++++---------------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f121e0..9e66f99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,15 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -2380,6 +2389,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "tmaze" version = "1.17.1" dependencies = [ + "arc-swap", "better-panic", "boml", "chrono", diff --git a/tmaze/Cargo.toml b/tmaze/Cargo.toml index a8dff85..83ffc9b 100644 --- a/tmaze/Cargo.toml +++ b/tmaze/Cargo.toml @@ -32,13 +32,14 @@ hashbrown = { version = "0.14", features = ["serde"] } toml = "0.8" json5 = "0.4.1" serde_json = "1.0.137" +paste = "1.0.15" +arc-swap = "1.8.0" # optional crates_io_api = { version = "0.11.0", optional = true, default-features = false, features = ["rustls"] } semver = { version = "1.0.23", optional = true } tokio = { version = "1.40.0", optional = true, features = ["rt", "rt-multi-thread"] } rodio = { version = "0.18.1", optional = true, default-features = false, features = ["wav", "mp3"] } -paste = "1.0.15" [features] default = ["updates", "sound"] diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index 42fee54..0cec7f8 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -250,10 +250,7 @@ impl App { } while let Some(change) = match self.activities.active_mut() { - Some(active) => { - log::trace!("Updating activity: '{}'", active.name()); - active - } + Some(active) => active, None => break 'mainloop events, } .update(std::mem::take(&mut events), &mut self.data) diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index f3777b8..8c6a063 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -6,13 +6,9 @@ pub mod theme; mod config_utils; -use std::{ - fmt::Display, - ops::Deref, - path::Path, - sync::{Arc, Mutex}, -}; +use std::{fmt::Display, ops::Deref, path::Path, sync::Arc}; +use arc_swap::ArcSwap; use hashbrown::HashMap; use crate::{ @@ -40,21 +36,32 @@ impl Settings { }) } - pub fn read(&self) -> impl Deref + use<'_> { - self.inner.config.lock().unwrap() + #[track_caller] + pub fn read(&self) -> impl Deref> + use<'_> { + log::trace!( + "Locking settings for read at {}", + std::panic::Location::caller() + ); + self.inner.config.load() } + #[track_caller] pub fn update_ui(&self, with: impl FnOnce(&mut PartialConfig)) { - let mut ui_layer = self.inner.ui_layer.lock().unwrap(); - with(&mut ui_layer); + log::trace!( + "Locking settings for update at {}", + std::panic::Location::caller() + ); + let mut new_ui = (**self.inner.ui_layer.load()).clone(); + with(&mut new_ui); + self.inner.ui_layer.store(Arc::new(new_ui)); self.inner.rebuild(); } } struct SettingsInner { config_layer: PartialConfig, - ui_layer: Mutex, - config: Mutex, + ui_layer: ArcSwap, + config: ArcSwap, } impl SettingsInner { @@ -68,8 +75,8 @@ impl SettingsInner { let settings = Self { config_layer, - ui_layer: Mutex::new(ui_layer), - config: Mutex::new(Config::default()), + ui_layer: ArcSwap::from_pointee(ui_layer), + config: ArcSwap::default(), }; settings.rebuild(); @@ -86,9 +93,9 @@ impl SettingsInner { fn rebuild(&self) { let mut config = Config::default(); config.merge(&self.config_layer); - config.merge(&self.ui_layer.lock().unwrap()); + config.merge(&self.ui_layer.load()); - *self.config.lock().unwrap() = config; + self.config.store(Arc::new(config)); } } From e94208487b118cc97a9bd8cf97aaaa77b41c96ee Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 11:42:09 +0100 Subject: [PATCH 16/23] Add: change settings, dump to file, load from file --- tmaze/src/app/app.rs | 35 ++++++++++++++++++++-- tmaze/src/app/event.rs | 13 +++++++++ tmaze/src/main.rs | 3 +- tmaze/src/settings/config_utils.rs | 9 ++++++ tmaze/src/settings/mod.rs | 47 +++++++++++++++++++++++------- tmaze/src/sound/mod.rs | 26 +++++++++++++---- 6 files changed, 112 insertions(+), 21 deletions(-) diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index 0cec7f8..c8445ca 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -1,6 +1,6 @@ use std::{ rc::Rc, - sync::Arc, + sync::{mpsc, Arc}, time::{Duration, Instant}, }; @@ -16,6 +16,7 @@ use cmaze::{ use crossterm::event::{read, KeyCode, KeyEvent, KeyEventKind}; use crate::{ + app::event::{EventReceiver, EventReceiverFn}, data::SaveData, helpers::{constants::paths, on_off}, logging::{self, AppLogger, LoggerOptions, UiLogs}, @@ -46,6 +47,7 @@ pub struct App { renderer: Renderer, activities: Activities, data: AppData, + event_drain: mpsc::Receiver, } pub struct AppData { @@ -57,6 +59,8 @@ pub struct AppData { pub logs: UiLogs, pub registries: Registries, jobs: Jobs, + pub event_sink: EventSink, + pub event_receivers: Vec, app_start: Instant, read_only: bool, @@ -140,8 +144,12 @@ impl App { .expect("Failed to prepare application directories. Please check permissions."); } - let (settings, settings_errors) = Settings::load(); + let (event_sink, event_drain) = Self::init_event_sink(); + let mut event_receivers = vec![]; + + let (settings, settings_errors) = Settings::load(event_sink.clone()); let config = settings.read(); + event_receivers.push(settings.register()); let renderer = Renderer::new(&Rc::new(config.general.terminal_scheme.clone())) .expect("failed to create renderer"); @@ -183,7 +191,7 @@ impl App { log::info!("Loading theme"); #[cfg(feature = "sound")] - let sound_player = SoundPlayer::new(settings.clone()); + let sound_player = SoundPlayer::new(settings.clone(), event_sink.clone()); let appereance = Appearance::new(&config); @@ -192,6 +200,7 @@ impl App { Self { renderer, activities, + event_drain, data: AppData { app_start, settings, @@ -200,6 +209,8 @@ impl App { appearance: appereance, screen_size: frame_size, jobs, + event_sink, + event_receivers, logs, registries, read_only, @@ -249,6 +260,18 @@ impl App { delay = Duration::from_nanos(1) } + // Read events from the drain + while let Ok(event) = self.event_drain.try_recv() { + events.push(event); + } + + // Update handle the event receivers + for receiver in &mut self.data.event_receivers { + for event in &events { + receiver(event); + } + } + while let Some(change) = match self.activities.active_mut() { Some(active) => active, None => break 'mainloop events, @@ -335,6 +358,10 @@ impl App { Ok(()) } + pub fn init_event_sink() -> (EventSink, mpsc::Receiver) { + mpsc::channel() + } + pub fn activity_count(&self) -> usize { self.activities.len() } @@ -360,6 +387,8 @@ impl App { } } +pub type EventSink = mpsc::Sender; + #[derive(Default)] pub struct AppStateData { pub last_selected_preset: Option, diff --git a/tmaze/src/app/event.rs b/tmaze/src/app/event.rs index 86de5a7..1e6469f 100644 --- a/tmaze/src/app/event.rs +++ b/tmaze/src/app/event.rs @@ -5,4 +5,17 @@ use super::activity::ActivityResult; pub enum Event { Term(TermEvent), ActiveAfterPop(Option), + SettingsChanged, +} + +pub type EventReceiverFn = Box; + +// FIXME: fix the API, this is really bad +// +// There are two main problems with this API: +// - This API leaks the implementation details of the event system. +// - This cannot be optimized in any way, it would be much better if we knew what events a receiver +// is interested in, so we could avoid calling it for irrelevant events. +pub trait EventReceiver { + fn register(self) -> EventReceiverFn; } diff --git a/tmaze/src/main.rs b/tmaze/src/main.rs index a649e38..59d5597 100644 --- a/tmaze/src/main.rs +++ b/tmaze/src/main.rs @@ -76,7 +76,8 @@ fn main() -> Result<(), GameError> { } if args.debug_config { - let (config, errors) = Settings::load(); + let (event_sink, _drain) = tmaze::app::app::App::init_event_sink(); + let (config, errors) = Settings::load(event_sink); if let Some(errors) = errors { eprintln!("Warning: Errors were encountered while loading the config."); for error in errors { diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index 955c750..054f7ad 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -167,6 +167,15 @@ macro_rules! config { $($pfields)* } + impl [] { + $( + #[allow(dead_code)] + pub fn $fields(&mut self) -> &mut $type { + self.$fields.get_or_insert($def_vals) + } + )* + } + impl $crate::settings::config_utils::Mergeable<[]> for $name { fn merge(&mut self, other: &[]) { $( diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index 8c6a063..dcedd99 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -6,12 +6,13 @@ pub mod theme; mod config_utils; -use std::{fmt::Display, ops::Deref, path::Path, sync::Arc}; +use std::{fmt::Display, ops::Deref, panic::Location, path::Path, sync::Arc}; use arc_swap::ArcSwap; use hashbrown::HashMap; use crate::{ + app::{app::EventSink, event::EventReceiver, Event}, helpers::{constants::paths, TupleMap}, settings::config_utils::{ConvertContext, ConvertError, LenientConvert, Mergeable, Value}, }; @@ -21,6 +22,7 @@ use model::{Config, PartialConfig}; #[derive(Clone)] pub struct Settings { inner: Arc, + event_sink: EventSink, } impl Settings { @@ -30,31 +32,54 @@ impl Settings { /// loading. /// /// TODO: Report the actual errors/warnings to the user. - pub fn load() -> (Self, Option>) { + pub fn load(event_sink: EventSink) -> (Self, Option>) { SettingsInner::load().map_first(|inner| Self { inner: Arc::new(inner), + event_sink, }) } - #[track_caller] pub fn read(&self) -> impl Deref> + use<'_> { - log::trace!( - "Locking settings for read at {}", - std::panic::Location::caller() - ); self.inner.config.load() } #[track_caller] pub fn update_ui(&self, with: impl FnOnce(&mut PartialConfig)) { - log::trace!( - "Locking settings for update at {}", - std::panic::Location::caller() - ); + log::trace!("Updating UI settings from {}", Location::caller()); let mut new_ui = (**self.inner.ui_layer.load()).clone(); with(&mut new_ui); self.inner.ui_layer.store(Arc::new(new_ui)); + self.inner.rebuild(); + self.notify(); + } + + fn write_ui(&self) { + log::trace!("Writing UI settings to file"); + std::fs::write( + paths::managed::ui_settings(), + serde_json::to_string_pretty(&**self.inner.ui_layer.load()) + .expect("UI settings should be serializable"), + ) + .expect("Failed to write UI settings to file"); + } + + fn notify(&self) { + self.event_sink + .send(Event::SettingsChanged) + .expect("Event drain should be alive"); + } +} + +impl EventReceiver for &Settings { + fn register(self) -> Box { + let settings = self.clone(); + Box::new(move |event| { + if let Event::SettingsChanged = event { + log::trace!("Writing UI settings to file from event"); + settings.write_ui(); + } + }) } } diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index 0824cac..3eb9146 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -4,7 +4,10 @@ use menu::OptionDef; use rodio::{OutputStream, OutputStreamHandle, Sink}; use crate::{ - app::{app::AppData, Activity}, + app::{ + app::{AppData, EventSink}, + Activity, + }, settings::Settings, ui::{menu, MenuItem, SliderDef}, }; @@ -20,15 +23,17 @@ struct SoundHandles { pub struct SoundPlayer { handles: Option, settings: Settings, + event_sink: EventSink, } impl SoundPlayer { - pub fn new(settings: Settings) -> Self { + pub fn new(settings: Settings, event_sink: EventSink) -> Self { let Ok((stream, handle)) = rodio::OutputStream::try_default() else { log::warn!("Failed to create audio stream, no sound will be played"); return Self { handles: None, settings, + event_sink, }; }; @@ -41,6 +46,7 @@ impl SoundPlayer { sink, }), settings, + event_sink, } } @@ -113,7 +119,9 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { val: !config.enable_audio, fun: Box::new(|mute, data| { *mute = !*mute; - // data.settings.set_enable_audio(!*mute); + data.settings.update_ui(|cfg| { + *cfg.audio().enable_audio() = *mute; + }); update_vol(data); }), }), @@ -124,7 +132,9 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { as_num: false, fun: Box::new(|up, vol, data| { *vol += if up { 1 } else { -1 }; - // data.settings.set_audio_volume(*vol as f32 / 5.0); + data.settings.update_ui(|cfg| { + *cfg.audio().audio_volume() = *vol as f64 / 5.0; + }); update_vol(data); }), }), @@ -133,7 +143,9 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { val: !config.enable_music, fun: Box::new(|mute, data| { *mute = !*mute; - // data.settings.set_enable_music(!*mute); + data.settings.update_ui(|cfg| { + *cfg.audio().enable_music() = !*mute; + }); update_vol(data); }), }), @@ -144,7 +156,9 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { as_num: false, fun: Box::new(|up, vol, data| { *vol += if up { 1 } else { -1 }; - // data.settings.set_music_volume(*vol as f32 / 5.0); + data.settings.update_ui(|cfg| { + *cfg.audio().music_volume() = *vol as f64 / 5.0; + }); update_vol(data); }), }), From 6e3fd19648243c138dad49c8146e8d1dade671d0 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 11:53:26 +0100 Subject: [PATCH 17/23] Fix: handling of limits in menu sliders --- tmaze/src/sound/mod.rs | 16 ++++++---------- tmaze/src/ui/menu.rs | 12 ++++++++---- tmaze/src/ui/usecase/screens/settings.rs | 6 ------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index 3eb9146..b17997c 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -118,9 +118,8 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { text: "Global mute".into(), val: !config.enable_audio, fun: Box::new(|mute, data| { - *mute = !*mute; data.settings.update_ui(|cfg| { - *cfg.audio().enable_audio() = *mute; + *cfg.audio().enable_audio() = !mute; }); update_vol(data); }), @@ -130,10 +129,9 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { val: (config.audio_volume * 5.0) as i32, range: 0..=5, as_num: false, - fun: Box::new(|up, vol, data| { - *vol += if up { 1 } else { -1 }; + fun: Box::new(|vol, data| { data.settings.update_ui(|cfg| { - *cfg.audio().audio_volume() = *vol as f64 / 5.0; + *cfg.audio().audio_volume() = vol as f64 / 5.0; }); update_vol(data); }), @@ -142,9 +140,8 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { text: "Music mute".into(), val: !config.enable_music, fun: Box::new(|mute, data| { - *mute = !*mute; data.settings.update_ui(|cfg| { - *cfg.audio().enable_music() = !*mute; + *cfg.audio().enable_music() = !mute; }); update_vol(data); }), @@ -154,10 +151,9 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { val: (config.music_volume * 5.0) as i32, range: 0..=5, as_num: false, - fun: Box::new(|up, vol, data| { - *vol += if up { 1 } else { -1 }; + fun: Box::new(|vol, data| { data.settings.update_ui(|cfg| { - *cfg.audio().music_volume() = *vol as f64 / 5.0; + *cfg.audio().music_volume() = vol as f64 / 5.0; }); update_vol(data); }), diff --git a/tmaze/src/ui/menu.rs b/tmaze/src/ui/menu.rs index c1f8565..828c430 100644 --- a/tmaze/src/ui/menu.rs +++ b/tmaze/src/ui/menu.rs @@ -33,7 +33,7 @@ pub struct SliderDef { #[allow(clippy::type_complexity)] // FIXME: take value instead of change direction (bool), // this should allow for mouse support - pub fun: Box, + pub fun: Box, pub as_num: bool, } @@ -42,7 +42,7 @@ pub struct OptionDef { pub val: bool, #[allow(clippy::type_complexity)] // FIXME: return the bool instead - pub fun: Box, + pub fun: Box, } // TODO: styling individual items @@ -349,7 +349,10 @@ impl Menu { match selected_opt { MenuItem::Text(_) => return Some(Change::pop_top_with(self.selected)), - MenuItem::Option(OptionDef { val, fun, .. }) => fun(val, data), + MenuItem::Option(OptionDef { val, fun, .. }) => { + *val = !*val; + fun(*val, data); + } MenuItem::Slider(_) | MenuItem::Separator => {} } @@ -361,8 +364,9 @@ impl Menu { val, range, fun, .. }) = &mut self.config.options[self.selected] { - fun(right, val, data); + *val += if right { 1 } else { -1 }; *val = (*val).clamp(*range.start(), *range.end()); + fun(*val, data); } } diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs index 82f6225..71eb0b2 100644 --- a/tmaze/src/ui/usecase/screens/settings.rs +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -103,7 +103,6 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { text: "Enable mouse input".into(), val: cfg.enable_mouse, fun: Box::new(|enabled, _data| { - *enabled = !*enabled; // data.settings.set_enable_mouse(*enabled); }), }), @@ -111,7 +110,6 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { text: "Enable dpad".into(), val: cfg.enable_dpad, fun: Box::new(|enabled, _data| { - *enabled = !*enabled; // data.settings.set_enable_dpad(*enabled); }), }), @@ -119,7 +117,6 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { text: "Left-handed dpad".into(), val: cfg.landscape_dpad_on_left, fun: Box::new(|is_on_left, _data| { - *is_on_left = !*is_on_left; // data.settings.set_landscape_dpad_on_left(*is_on_left); }), }), @@ -127,7 +124,6 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { text: "Swap Up and Down buttons".into(), val: cfg.dpad_swap_up_down, fun: Box::new(|do_swap, _data| { - *do_swap = !*do_swap; // data.settings.set_dpad_swap_up_down(*do_swap); }), }), @@ -135,7 +131,6 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { text: "Enable margin around dpad".into(), val: cfg.enable_margin_around_dpad, fun: Box::new(|enabled, _data| { - *enabled = !*enabled; // data.settings.set_enable_margin_around_dpad(*enabled); }), }), @@ -143,7 +138,6 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { text: "Enable dpad highlight".into(), val: cfg.enable_dpad_highlight, fun: Box::new(|enabled, _data| { - *enabled = !*enabled; // data.settings.set_enable_dpad_highlight(*enabled); }), }), From c975ae823c59b0ed36342cea57028a8511815a01 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 12:12:52 +0100 Subject: [PATCH 18/23] Update: skip null fields in ser. --- tmaze/src/settings/config_utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tmaze/src/settings/config_utils.rs b/tmaze/src/settings/config_utils.rs index 054f7ad..2e797c1 100644 --- a/tmaze/src/settings/config_utils.rs +++ b/tmaze/src/settings/config_utils.rs @@ -106,7 +106,7 @@ macro_rules! config { $name [ $($fields)* $field : [] = ::std::default::Default::default(), ] [ $($rfields)* pub $field : $type, ] - [ $($pfields)* $(#[$attr])* pub $field : Option<[]>, ] + [ $($pfields)* #[serde(skip_serializing_if = "Option::is_none")] $(#[$attr])* pub $field : Option<[]>, ] { $($rest)* } } } @@ -122,7 +122,7 @@ macro_rules! config { $name [ $($fields)* $field : $type = ::std::default::Default::default(), ] [ $($rfields)* pub $field : $type, ] - [ $($pfields)* $(#[$attr])* pub $field : Option<$type>, ] + [ $($pfields)* #[serde(skip_serializing_if = "Option::is_none")] $(#[$attr])* pub $field : Option<$type>, ] { $($rest)* } } }; @@ -137,7 +137,7 @@ macro_rules! config { $name [ $($fields)* $field : $type = ($def), ] [ $($rfields)* pub $field : $type, ] - [ $($pfields)* $(#[$attr])* pub $field : Option<$type>, ] + [ $($pfields)* #[serde(skip_serializing_if = "Option::is_none")] $(#[$attr])* pub $field : Option<$type>, ] { $($rest)* } } }; From b8452653f5f360fcf0174d4891a050a8b772e39d Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 12:29:29 +0100 Subject: [PATCH 19/23] Add: control settings --- tmaze/src/ui/usecase/screens/settings.rs | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs index 71eb0b2..e43d8ce 100644 --- a/tmaze/src/ui/usecase/screens/settings.rs +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -102,43 +102,55 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Enable mouse input".into(), val: cfg.enable_mouse, - fun: Box::new(|enabled, _data| { - // data.settings.set_enable_mouse(*enabled); + fun: Box::new(|enabled, data| { + data.settings.update_ui(|cfg| { + *cfg.nagivation().enable_mouse() = enabled; + }); }), }), MenuItem::Option(OptionDef { text: "Enable dpad".into(), val: cfg.enable_dpad, - fun: Box::new(|enabled, _data| { - // data.settings.set_enable_dpad(*enabled); + fun: Box::new(|enabled, data| { + data.settings.update_ui(|cfg| { + *cfg.nagivation().enable_dpad() = enabled; + }); }), }), MenuItem::Option(OptionDef { text: "Left-handed dpad".into(), val: cfg.landscape_dpad_on_left, - fun: Box::new(|is_on_left, _data| { - // data.settings.set_landscape_dpad_on_left(*is_on_left); + fun: Box::new(|is_on_left, data| { + data.settings.update_ui(|cfg| { + *cfg.nagivation().landscape_dpad_on_left() = is_on_left; + }); }), }), MenuItem::Option(OptionDef { text: "Swap Up and Down buttons".into(), val: cfg.dpad_swap_up_down, - fun: Box::new(|do_swap, _data| { - // data.settings.set_dpad_swap_up_down(*do_swap); + fun: Box::new(|do_swap, data| { + data.settings.update_ui(|cfg| { + *cfg.nagivation().dpad_swap_up_down() = do_swap; + }); }), }), MenuItem::Option(OptionDef { text: "Enable margin around dpad".into(), val: cfg.enable_margin_around_dpad, - fun: Box::new(|enabled, _data| { - // data.settings.set_enable_margin_around_dpad(*enabled); + fun: Box::new(|enabled, data| { + data.settings.update_ui(|cfg| { + *cfg.nagivation().enable_margin_around_dpad() = enabled; + }); }), }), MenuItem::Option(OptionDef { text: "Enable dpad highlight".into(), val: cfg.enable_dpad_highlight, - fun: Box::new(|enabled, _data| { - // data.settings.set_enable_dpad_highlight(*enabled); + fun: Box::new(|enabled, data| { + data.settings.update_ui(|cfg| { + *cfg.nagivation().enable_dpad_highlight() = enabled; + }); }), }), MenuItem::Separator, From 330ef6758fe00c43f61d188fdd853dcad1fc2e53 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 13:14:12 +0100 Subject: [PATCH 20/23] Fix: -F local_paths build --- tmaze/src/helpers/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmaze/src/helpers/constants.rs b/tmaze/src/helpers/constants.rs index ec46aef..1a6a0f0 100644 --- a/tmaze/src/helpers/constants.rs +++ b/tmaze/src/helpers/constants.rs @@ -33,7 +33,7 @@ pub mod paths { } #[cfg(feature = "local_paths")] - pub fn base_path() -> PathBuf { + pub fn base() -> PathBuf { PathBuf::from("./") } From 551d496814eb179920987b1a1ed219e33623f197 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 13:21:36 +0100 Subject: [PATCH 21/23] Fix: build on non-unix OS --- tmaze/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tmaze/src/main.rs b/tmaze/src/main.rs index 59d5597..cfa0d57 100644 --- a/tmaze/src/main.rs +++ b/tmaze/src/main.rs @@ -1,4 +1,4 @@ -use std::{io::Write, os::unix::ffi::OsStrExt}; +use std::io::Write; use tmaze::{ app::{app::init_theme_resolver, game::MainMenu, Activity, App, GameError}, @@ -69,7 +69,12 @@ fn main() -> Result<(), GameError> { if args.show_config_path { let settings_path = paths::config(); - std::io::stdout().write_all(settings_path.as_os_str().as_bytes())?; + std::io::stdout().write_all( + &settings_path + .as_os_str() + .to_os_string() + .into_encoded_bytes(), + )?; std::io::stdout().write_all(b"\n")?; std::io::stdout().flush()?; return Ok(()); From 1ee856a8a6f9b7afb2ee0af47a73c799a0ed4aa4 Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 14:56:49 +0100 Subject: [PATCH 22/23] Add: resetting values in settings, Update: sound player volume change --- tmaze/src/app/app.rs | 10 ++-- tmaze/src/app/event.rs | 4 +- tmaze/src/settings/mod.rs | 10 ++-- tmaze/src/sound/mod.rs | 68 ++++++++++++++++-------- tmaze/src/ui/menu.rs | 40 ++++++++++++-- tmaze/src/ui/usecase/screens/settings.rs | 48 ++++++++++++++--- 6 files changed, 141 insertions(+), 39 deletions(-) diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index c8445ca..8d814f9 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -192,6 +192,7 @@ impl App { #[cfg(feature = "sound")] let sound_player = SoundPlayer::new(settings.clone(), event_sink.clone()); + event_receivers.push(sound_player.register()); let appereance = Appearance::new(&config); @@ -235,7 +236,7 @@ impl App { let mut events = vec![]; // FIXME: better polling strategy, IO will need faster response times - let mut delay = Duration::from_millis(45); + let mut delay = Duration::from_millis(10); while let Ok(true) = crossterm::event::poll(delay) { let event = read().unwrap(); @@ -266,11 +267,14 @@ impl App { } // Update handle the event receivers - for receiver in &mut self.data.event_receivers { + // (hack): due to borrow issues + let mut receivers = std::mem::take(&mut self.data.event_receivers); + for receiver in receivers.iter_mut() { for event in &events { - receiver(event); + receiver(event, &mut self.data); } } + self.data.event_receivers = receivers; while let Some(change) = match self.activities.active_mut() { Some(active) => active, diff --git a/tmaze/src/app/event.rs b/tmaze/src/app/event.rs index 1e6469f..c3807b7 100644 --- a/tmaze/src/app/event.rs +++ b/tmaze/src/app/event.rs @@ -1,5 +1,7 @@ use crossterm::event::Event as TermEvent; +use crate::app::app::AppData; + use super::activity::ActivityResult; pub enum Event { @@ -8,7 +10,7 @@ pub enum Event { SettingsChanged, } -pub type EventReceiverFn = Box; +pub type EventReceiverFn = Box; // FIXME: fix the API, this is really bad // diff --git a/tmaze/src/settings/mod.rs b/tmaze/src/settings/mod.rs index dcedd99..c43d914 100644 --- a/tmaze/src/settings/mod.rs +++ b/tmaze/src/settings/mod.rs @@ -12,7 +12,11 @@ use arc_swap::ArcSwap; use hashbrown::HashMap; use crate::{ - app::{app::EventSink, event::EventReceiver, Event}, + app::{ + app::{AppData, EventSink}, + event::EventReceiver, + Event, + }, helpers::{constants::paths, TupleMap}, settings::config_utils::{ConvertContext, ConvertError, LenientConvert, Mergeable, Value}, }; @@ -72,9 +76,9 @@ impl Settings { } impl EventReceiver for &Settings { - fn register(self) -> Box { + fn register(self) -> Box { let settings = self.clone(); - Box::new(move |event| { + Box::new(move |event, _| { if let Event::SettingsChanged = event { log::trace!("Writing UI settings to file from event"); settings.write_ui(); diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index b17997c..92aa56c 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -6,7 +6,8 @@ use rodio::{OutputStream, OutputStreamHandle, Sink}; use crate::{ app::{ app::{AppData, EventSink}, - Activity, + event::EventReceiver, + Activity, Event, }, settings::Settings, ui::{menu, MenuItem, SliderDef}, @@ -95,20 +96,24 @@ impl SoundPlayer { } } -pub fn create_audio_settings(data: &mut AppData) -> Activity { - fn update_vol(data: &mut AppData) { - let cfg = &data.settings.read().audio; - - if cfg.enable_audio && cfg.enable_music { - data.sound_player - .set_volume((cfg.audio_volume * cfg.music_volume) as f32); - } else { - data.sound_player.set_volume(0.0); - } +impl EventReceiver for &SoundPlayer { + fn register(self) -> crate::app::event::EventReceiverFn { + Box::new(move |event, data| { + if let Event::SettingsChanged = event { + let cfg = &data.settings.read().audio; + + if cfg.enable_audio && cfg.enable_music { + data.sound_player + .set_volume((cfg.audio_volume * cfg.music_volume) as f32); + } else { + data.sound_player.set_volume(0.0); + } + } + }) } +} - // FIXME: re-add settings updating once supported - +pub fn create_audio_settings(data: &mut AppData) -> Activity { let config = &data.settings.read().audio; let menu_config = menu::MenuConfig::new( @@ -117,46 +122,67 @@ pub fn create_audio_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Global mute".into(), val: !config.enable_audio, - fun: Box::new(|mute, data| { + update_fn: Box::new(|mute, data| { data.settings.update_ui(|cfg| { *cfg.audio().enable_audio() = !mute; }); - update_vol(data); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.audio().enable_audio = None; + }); + !data.settings.read().audio.enable_audio + })), }), MenuItem::Slider(SliderDef { text: "Global volume".into(), val: (config.audio_volume * 5.0) as i32, range: 0..=5, as_num: false, - fun: Box::new(|vol, data| { + update_fn: Box::new(|vol, data| { data.settings.update_ui(|cfg| { *cfg.audio().audio_volume() = vol as f64 / 5.0; }); - update_vol(data); }), + + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.audio().audio_volume = None; + }); + (data.settings.read().audio.audio_volume * 5.0) as i32 + })), }), MenuItem::Option(OptionDef { text: "Music mute".into(), val: !config.enable_music, - fun: Box::new(|mute, data| { + update_fn: Box::new(|mute, data| { data.settings.update_ui(|cfg| { *cfg.audio().enable_music() = !mute; }); - update_vol(data); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.audio().enable_music = None; + }); + !data.settings.read().audio.enable_music + })), }), MenuItem::Slider(SliderDef { text: "Music volume".into(), val: (config.music_volume * 5.0) as i32, range: 0..=5, as_num: false, - fun: Box::new(|vol, data| { + update_fn: Box::new(|vol, data| { data.settings.update_ui(|cfg| { *cfg.audio().music_volume() = vol as f64 / 5.0; }); - update_vol(data); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.audio().music_volume = None; + }); + (data.settings.read().audio.music_volume * 5.0) as i32 + })), }), MenuItem::Separator, MenuItem::Text("Exit".into()), diff --git a/tmaze/src/ui/menu.rs b/tmaze/src/ui/menu.rs index 828c430..3459f37 100644 --- a/tmaze/src/ui/menu.rs +++ b/tmaze/src/ui/menu.rs @@ -33,7 +33,8 @@ pub struct SliderDef { #[allow(clippy::type_complexity)] // FIXME: take value instead of change direction (bool), // this should allow for mouse support - pub fun: Box, + pub update_fn: Box, + pub reset_fn: Option i32>>, pub as_num: bool, } @@ -42,7 +43,8 @@ pub struct OptionDef { pub val: bool, #[allow(clippy::type_complexity)] // FIXME: return the bool instead - pub fun: Box, + pub update_fn: Box, + pub reset_fn: Option bool>>, } // TODO: styling individual items @@ -349,7 +351,11 @@ impl Menu { match selected_opt { MenuItem::Text(_) => return Some(Change::pop_top_with(self.selected)), - MenuItem::Option(OptionDef { val, fun, .. }) => { + MenuItem::Option(OptionDef { + val, + update_fn: fun, + .. + }) => { *val = !*val; fun(*val, data); } @@ -361,7 +367,10 @@ impl Menu { fn update_slider(&mut self, right: bool, data: &mut AppData) { if let MenuItem::Slider(SliderDef { - val, range, fun, .. + val, + range, + update_fn: fun, + .. }) = &mut self.config.options[self.selected] { *val += if right { 1 } else { -1 }; @@ -387,6 +396,24 @@ impl Menu { Some(selected) } + + fn reset(&mut self, data: &mut AppData) { + let selected_opt = &mut self.config.options[self.selected]; + match selected_opt { + MenuItem::Text(_) => {} + MenuItem::Option(OptionDef { val, reset_fn, .. }) => { + if let Some(reset_fn) = reset_fn { + *val = reset_fn(data); + } + } + MenuItem::Slider(SliderDef { val, reset_fn, .. }) => { + if let Some(reset_fn) = reset_fn { + *val = reset_fn(data); + } + } + MenuItem::Separator => {} + } + } } impl ActivityHandler for Menu { @@ -426,7 +453,7 @@ impl ActivityHandler for Menu { for event in events { match event { - Event::Term(TermEvent::Key(KeyEvent { code, kind, .. })) if !is_release(kind) => { + Event::Term(TermEvent::Key(KeyEvent { code, kind, modifiers, .. })) if !is_release(kind) => { match code { KeyCode::Up | KeyCode::Char('w') => { self.select(false); @@ -459,6 +486,9 @@ impl ActivityHandler for Menu { KeyCode::Right => { self.update_slider(true, app_data); } + KeyCode::Char('r') if modifiers.contains(KeyModifiers::CONTROL) => { + self.reset(app_data); + } _ => {} } } diff --git a/tmaze/src/ui/usecase/screens/settings.rs b/tmaze/src/ui/usecase/screens/settings.rs index e43d8ce..4861dfa 100644 --- a/tmaze/src/ui/usecase/screens/settings.rs +++ b/tmaze/src/ui/usecase/screens/settings.rs @@ -102,56 +102,92 @@ pub fn create_controls_settings(data: &mut AppData) -> Activity { MenuItem::Option(OptionDef { text: "Enable mouse input".into(), val: cfg.enable_mouse, - fun: Box::new(|enabled, data| { + update_fn: Box::new(|enabled, data| { data.settings.update_ui(|cfg| { *cfg.nagivation().enable_mouse() = enabled; }); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.nagivation().enable_mouse = None; + }); + data.settings.read().nagivation.enable_mouse + })), }), MenuItem::Option(OptionDef { text: "Enable dpad".into(), val: cfg.enable_dpad, - fun: Box::new(|enabled, data| { + update_fn: Box::new(|enabled, data| { data.settings.update_ui(|cfg| { *cfg.nagivation().enable_dpad() = enabled; }); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.nagivation().enable_dpad = None; + }); + data.settings.read().nagivation.enable_dpad + })), }), MenuItem::Option(OptionDef { text: "Left-handed dpad".into(), val: cfg.landscape_dpad_on_left, - fun: Box::new(|is_on_left, data| { + update_fn: Box::new(|is_on_left, data| { data.settings.update_ui(|cfg| { *cfg.nagivation().landscape_dpad_on_left() = is_on_left; }); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.nagivation().landscape_dpad_on_left = None; + }); + data.settings.read().nagivation.landscape_dpad_on_left + })), }), MenuItem::Option(OptionDef { text: "Swap Up and Down buttons".into(), val: cfg.dpad_swap_up_down, - fun: Box::new(|do_swap, data| { + update_fn: Box::new(|do_swap, data| { data.settings.update_ui(|cfg| { *cfg.nagivation().dpad_swap_up_down() = do_swap; }); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.nagivation().dpad_swap_up_down = None; + }); + data.settings.read().nagivation.dpad_swap_up_down + })), }), MenuItem::Option(OptionDef { text: "Enable margin around dpad".into(), val: cfg.enable_margin_around_dpad, - fun: Box::new(|enabled, data| { + update_fn: Box::new(|enabled, data| { data.settings.update_ui(|cfg| { *cfg.nagivation().enable_margin_around_dpad() = enabled; }); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.nagivation().enable_margin_around_dpad = None; + }); + data.settings.read().nagivation.enable_margin_around_dpad + })), }), MenuItem::Option(OptionDef { text: "Enable dpad highlight".into(), val: cfg.enable_dpad_highlight, - fun: Box::new(|enabled, data| { + update_fn: Box::new(|enabled, data| { data.settings.update_ui(|cfg| { *cfg.nagivation().enable_dpad_highlight() = enabled; }); }), + reset_fn: Some(Box::new(|data| { + data.settings.update_ui(|cfg| { + cfg.nagivation().enable_dpad_highlight = None; + }); + data.settings.read().nagivation.enable_dpad_highlight + })), }), MenuItem::Separator, MenuItem::Text("Exit".into()), From 79c74cba87183ec353bbd7f62b572175eac7858a Mon Sep 17 00:00:00 2001 From: ur-fault Date: Sun, 8 Feb 2026 14:58:15 +0100 Subject: [PATCH 23/23] Fix: remove old settings --- tmaze/src/app/app.rs | 2 +- tmaze/src/settings/old_settings.rs | 469 ----------------------------- tmaze/src/sound/mod.rs | 11 +- 3 files changed, 3 insertions(+), 479 deletions(-) delete mode 100644 tmaze/src/settings/old_settings.rs diff --git a/tmaze/src/app/app.rs b/tmaze/src/app/app.rs index 8d814f9..6fb309c 100644 --- a/tmaze/src/app/app.rs +++ b/tmaze/src/app/app.rs @@ -191,7 +191,7 @@ impl App { log::info!("Loading theme"); #[cfg(feature = "sound")] - let sound_player = SoundPlayer::new(settings.clone(), event_sink.clone()); + let sound_player = SoundPlayer::new(settings.clone()); event_receivers.push(sound_player.register()); let appereance = Appearance::new(&config); diff --git a/tmaze/src/settings/old_settings.rs b/tmaze/src/settings/old_settings.rs deleted file mode 100644 index dc7a741..0000000 --- a/tmaze/src/settings/old_settings.rs +++ /dev/null @@ -1,469 +0,0 @@ -use cmaze::{ - algorithms::{MazeSpec, MazeSpecType}, - dims::{Dims, Offset}, -}; -use derivative::Derivative; -use serde::{Deserialize, Serialize}; -use std::{ - fs, io, - path::PathBuf, - sync::{Arc, RwLock}, -}; -use theme::ThemeDefinition; - -use crate::{ - helpers::constants::paths::settings_path, - settings::theme::{SharedScheme, TerminalColorScheme, ThemeDefinition}, -}; - -const DEFAULT_SETTINGS_JSON: &str = include_str!("./files/default_settings.json5"); - -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] -#[serde(tag = "mode")] -pub enum CameraMode { - #[default] - CloseFollow, - EdgeFollow { - x: Offset, - y: Offset, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MazePreset { - pub title: String, - pub description: Option, - - #[serde(default)] - pub default: bool, - - // TODO: make `serde(flatten)` once switched to TOML/JSON - #[serde(flatten)] - pub maze_spec: MazeSpec, -} - -impl MazePreset { - pub fn short_desc(&self) -> Option { - let (size, cells): (_, usize) = match &self.maze_spec.inner_spec { - MazeSpecType::Regions { regions, .. } => ( - self.maze_spec.size()?, - regions.iter().map(|r| r.mask.enabled_count()).sum(), - ), - MazeSpecType::Simple { mask, .. } => ( - self.maze_spec.size()?, - mask.as_ref() - .map(|m| m.enabled_count()) - .unwrap_or(self.maze_spec.size()?.product() as usize), - ), - }; - - if size.2 == 1 { - Some(format!( - "{}: {}x{} ({} cells)", - self.title, size.0, size.1, cells - )) - } else { - Some(format!( - "{}: {}x{}x{} ({} cells)", - self.title, size.0, size.1, size.2, cells - )) - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] -pub enum UpdateCheckInterval { - Never, - #[default] - Daily, - Weekly, - Monthly, - Yearly, - Always, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum TerminalSchemeDef { - Named(String), - Custom(TerminalColorScheme), -} - -#[derive(Debug, Derivative, Serialize, Deserialize)] -#[derivative(Default)] -#[serde(rename = "Settings")] -// FIXME: separate sections into their own struct -pub struct SettingsInner { - // general - #[serde(default)] - pub theme: Option, - #[serde(default)] - pub logging_level: Option, - #[serde(default)] - pub debug_logging_level: Option, - #[serde(default)] - pub file_logging_level: Option, - #[serde(default)] - pub terminal_scheme: Option, - - // viewport - #[serde(default)] - pub slow: Option, - #[serde(default)] - pub disable_tower_auto_up: Option, - #[serde(default)] - pub camera_mode: Option, - #[serde(default)] - pub camera_smoothing: Option, - #[serde[default]] - pub player_smoothing: Option, - #[serde(default)] - pub viewport_margin: Option<(i32, i32)>, - - // navigation - #[serde(default)] - pub enable_mouse: Option, - #[serde(default)] - pub enable_dpad: Option, - #[serde(default)] - pub landscape_dpad_on_left: Option, - #[serde(default)] - pub dpad_swap_up_down: Option, - #[serde(default)] - pub enable_margin_around_dpad: Option, - #[serde(default)] - pub enable_dpad_highlight: Option, - - // update check - #[serde(default)] - pub update_check_interval: Option, - #[serde(default)] - pub display_update_check_errors: Option, - - // audio - #[serde(default)] - pub enable_audio: Option, - #[serde(default)] - pub audio_volume: Option, - #[serde(default)] - pub enable_music: Option, - #[serde(default)] - pub music_volume: Option, - - // presets - #[serde(default)] - pub presets: Option>, - // TODO: it's not possible in RON to have a HashMap with flattened keys, - // so we will support it in different way formats - // once we support them - this would mean dropping RON support - // https://github.com/ron-rs/ron/issues/115 - // pub unknown_fields: HashMap, -} - -#[derive(Debug, Clone)] -pub struct Settings { - shared: Arc>, - path: PathBuf, - read_only: bool, -} - -impl Default for Settings { - fn default() -> Self { - let settings = SettingsInner::default(); - Self { - shared: Arc::new(RwLock::new(settings)), - path: settings_path(), - read_only: false, - } - } -} - -#[allow(dead_code)] -impl Settings { - pub fn new() -> Self { - Self::default() - } - - pub fn path(&self) -> PathBuf { - self.path.clone() - } - - pub fn is_ro(&self) -> bool { - self.read_only - } - - pub fn read(&self) -> std::sync::RwLockReadGuard<'_, SettingsInner> { - self.shared.read().unwrap() - } - - pub fn write(&mut self) -> std::sync::RwLockWriteGuard<'_, SettingsInner> { - self.shared.write().unwrap() - } -} - -impl Settings { - pub fn get_theme(&self) -> ThemeDefinition { - let name = self.read().theme.clone(); - let maybe_theme = match &name { - Some(theme_name) => ThemeDefinition::load_by_name(theme_name), - None => ThemeDefinition::load_default(self.read_only), - }; - - match maybe_theme { - Ok(theme) => theme, - Err(err) if name.is_some() => { - log::error!("Could not load the theme: {}", err); - ThemeDefinition::parse_default() - } - Err(err) if name.is_none() => { - log::error!("Could not load the default theme: {}", err); - ThemeDefinition::parse_default() - } - _ => unreachable!("`is_none` and `is_some` handle all cases"), - } - } - - pub fn get_logging_level(&self) -> log::Level { - self.read() - .logging_level - .clone() - .and_then(|level| level.parse().ok()) - .unwrap_or(log::Level::Info) - } - - pub fn get_debug_logging_level(&self) -> log::Level { - self.read() - .debug_logging_level - .clone() - .and_then(|level| level.parse().ok()) - .unwrap_or(log::Level::Info) - } - - pub fn get_file_logging_level(&self) -> log::Level { - self.read() - .file_logging_level - .clone() - .and_then(|level| level.parse().ok()) - .unwrap_or(log::Level::Info) - } - - pub fn get_terminal_scheme(&self) -> Option { - match &self.read().terminal_scheme { - Some(TerminalSchemeDef::Named(name)) => { - Some(SharedScheme::new(TerminalColorScheme::named(name)?)) - } - Some(TerminalSchemeDef::Custom(scheme)) => Some(SharedScheme::new(scheme.clone())), - None => Some(SharedScheme::new(TerminalColorScheme::default())), - } - } - - pub fn get_slow(&self) -> bool { - self.read().slow.unwrap_or_default() - } - - pub fn set_slow(&mut self, value: bool) -> &mut Self { - self.write().slow = Some(value); - self - } - - pub fn get_disable_tower_auto_up(&self) -> bool { - self.read().disable_tower_auto_up.unwrap_or_default() - } - - pub fn set_disable_tower_auto_up(&mut self, value: bool) -> &mut Self { - self.write().disable_tower_auto_up = Some(value); - self - } - - pub fn get_camera_mode(&self) -> CameraMode { - self.read().camera_mode.unwrap_or_default() - } - - pub fn set_camera_mode(&mut self, value: CameraMode) -> &mut Self { - self.write().camera_mode = Some(value); - self - } - - pub fn get_camera_smoothing(&self) -> f32 { - self.read().camera_smoothing.unwrap_or(0.5).clamp(0.5, 1.0) - } - - pub fn set_camera_smoothing(&mut self, value: f32) -> &mut Self { - self.write().camera_smoothing = Some(value.clamp(0.5, 1.0)); - self - } - - pub fn get_player_smoothing(&self) -> f32 { - self.read().player_smoothing.unwrap_or(0.8).clamp(0.5, 1.0) - } - - pub fn set_player_smoothing(&mut self, value: f32) -> &mut Self { - self.write().player_smoothing = Some(value.clamp(0.5, 1.0)); - self - } - - pub fn get_viewport_margin(&self) -> Dims { - self.read() - .viewport_margin - .map(Dims::from) - .unwrap_or(Dims(4, 3)) - } - - pub fn set_viewport_margin(&mut self, value: Dims) -> &mut Self { - self.write().viewport_margin = Some(value.into()); - self - } - - pub fn get_enable_mouse(&self) -> bool { - self.read().enable_mouse.unwrap_or(true) - } - - pub fn set_enable_mouse(&mut self, value: bool) -> &mut Self { - self.write().enable_mouse = Some(value); - self - } - - pub fn get_enable_dpad(&self) -> bool { - self.read().enable_dpad.unwrap_or(false) - } - - pub fn set_enable_dpad(&mut self, value: bool) -> &mut Self { - self.write().enable_dpad = Some(value); - self - } - - pub fn get_landscape_dpad_on_left(&self) -> bool { - self.read().landscape_dpad_on_left.unwrap_or(false) - } - - pub fn set_landscape_dpad_on_left(&mut self, value: bool) -> &mut Self { - self.write().landscape_dpad_on_left = Some(value); - self - } - - pub fn get_dpad_swap_up_down(&self) -> bool { - self.read().dpad_swap_up_down.unwrap_or(false) - } - - pub fn set_dpad_swap_up_down(&mut self, value: bool) -> &mut Self { - self.write().dpad_swap_up_down = Some(value); - self - } - - pub fn get_enable_margin_around_dpad(&self) -> bool { - self.read().enable_margin_around_dpad.unwrap_or(false) - } - - pub fn set_enable_margin_around_dpad(&mut self, value: bool) -> &mut Self { - self.write().enable_margin_around_dpad = Some(value); - self - } - - pub fn get_enable_dpad_highlight(&self) -> bool { - self.read().enable_dpad_highlight.unwrap_or(true) - } - - pub fn set_enable_dpad_highlight(&mut self, value: bool) -> &mut Self { - self.write().enable_dpad_highlight = Some(value); - self - } - - pub fn set_check_interval(&mut self, value: UpdateCheckInterval) -> &mut Self { - self.write().update_check_interval = Some(value); - self - } - - pub fn get_check_interval(&self) -> UpdateCheckInterval { - self.read().update_check_interval.unwrap_or_default() - } - - pub fn get_display_update_check_errors(&self) -> bool { - self.read().display_update_check_errors.unwrap_or(true) - } - - pub fn set_display_update_check_errors(&mut self, value: bool) -> &mut Self { - self.write().display_update_check_errors = Some(value); - self - } - - pub fn get_enable_audio(&self) -> bool { - self.read().enable_audio.unwrap_or_default() - } - - pub fn set_enable_audio(&mut self, value: bool) -> &mut Self { - self.write().enable_audio = Some(value); - self - } - - pub fn get_audio_volume(&self) -> f32 { - self.read().audio_volume.unwrap_or_default().clamp(0., 1.) - } - - pub fn set_audio_volume(&mut self, value: f32) -> &mut Self { - self.write().audio_volume = Some(value.clamp(0., 1.)); - self - } - - pub fn get_enable_music(&self) -> bool { - self.read().enable_music.unwrap_or_default() - } - - pub fn set_enable_music(&mut self, value: bool) -> &mut Self { - self.write().enable_music = Some(value); - self - } - - pub fn get_music_volume(&self) -> f32 { - self.read().music_volume.unwrap_or_default().clamp(0., 1.) - } - - pub fn set_music_volume(&mut self, value: f32) -> &mut Self { - self.write().music_volume = Some(value.clamp(0., 1.)); - self - } - - pub fn set_presets(&mut self, value: Vec) -> &mut Self { - self.write().presets = Some(value); - self - } - - pub fn get_presets(&self) -> Vec { - self.read().presets.clone().unwrap_or_default() - } -} - -// JSON -impl Settings { - pub fn load_json(path: PathBuf, read_only: bool) -> io::Result { - let settings_string = fs::read_to_string(&path); - let settings: SettingsInner = if let Ok(settings_string) = settings_string { - json5::from_str(&settings_string) - .expect("Could not parse settings file: check the syntax") - } else { - if !read_only { - fs::create_dir_all(path.parent().unwrap())?; - fs::write(&path, DEFAULT_SETTINGS_JSON)?; - } - json5::from_str(DEFAULT_SETTINGS_JSON).unwrap() - }; - - Ok(Self { - shared: Arc::new(RwLock::new(settings)), - path, - read_only, - }) - } - - pub fn reset_json(&mut self) { - *self.write() = json5::from_str(DEFAULT_SETTINGS_JSON).unwrap(); - - let path = settings_path(); - fs::write(&path, DEFAULT_SETTINGS_JSON).unwrap(); - - self.path = path; - } - - pub fn reset_json_config(path: PathBuf) { - fs::write(path, DEFAULT_SETTINGS_JSON).unwrap(); - } -} diff --git a/tmaze/src/sound/mod.rs b/tmaze/src/sound/mod.rs index 92aa56c..8fd3816 100644 --- a/tmaze/src/sound/mod.rs +++ b/tmaze/src/sound/mod.rs @@ -4,11 +4,7 @@ use menu::OptionDef; use rodio::{OutputStream, OutputStreamHandle, Sink}; use crate::{ - app::{ - app::{AppData, EventSink}, - event::EventReceiver, - Activity, Event, - }, + app::{app::AppData, event::EventReceiver, Activity, Event}, settings::Settings, ui::{menu, MenuItem, SliderDef}, }; @@ -24,17 +20,15 @@ struct SoundHandles { pub struct SoundPlayer { handles: Option, settings: Settings, - event_sink: EventSink, } impl SoundPlayer { - pub fn new(settings: Settings, event_sink: EventSink) -> Self { + pub fn new(settings: Settings) -> Self { let Ok((stream, handle)) = rodio::OutputStream::try_default() else { log::warn!("Failed to create audio stream, no sound will be played"); return Self { handles: None, settings, - event_sink, }; }; @@ -47,7 +41,6 @@ impl SoundPlayer { sink, }), settings, - event_sink, } }