termusiclib/config/v2/tui/
mod.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6use super::server::ComSettings;
7
8pub mod config_extra;
9pub mod keys;
10pub mod theme;
11
12#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
13#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
14#[allow(clippy::module_name_repetitions)]
15pub struct TuiSettings {
16    pub com: MaybeComSettings,
17    /// Field that holds the resolved `com` data, in case `same` was used
18    #[serde(skip)]
19    pub com_resolved: Option<ComSettings>,
20    pub behavior: BehaviorSettings,
21    pub coverart: CoverArtPosition,
22    #[serde(flatten)]
23    pub theme: theme::ThemeWrap,
24    pub keys: keys::Keys,
25}
26
27impl TuiSettings {
28    /// Resolve the [`ComSettings`] or directly get them.
29    ///
30    /// If result is [`Ok`], then `com_resolved` is set and [`Self::get_com`] will always return [`Some`]
31    pub fn resolve_com(&mut self, tui_path: &Path) -> Result<()> {
32        if self.com_resolved.is_some() {
33            return Ok(());
34        }
35
36        match self.com {
37            MaybeComSettings::ComSettings(ref v) => {
38                // this could likely be avoided, but for simplicity this is set
39                self.com_resolved = Some(*v);
40                return Ok(());
41            }
42            MaybeComSettings::Same => (),
43        }
44
45        let server_path = tui_path
46            .parent()
47            .context("tui_path should have a parent directory")?
48            .join(super::server::config_extra::FILE_NAME);
49
50        let server_settings =
51            super::server::config_extra::ServerConfigVersionedDefaulted::from_file(server_path)
52                .context("parsing server config")?;
53        self.com_resolved = Some(server_settings.into_settings().com);
54
55        Ok(())
56    }
57
58    /// Get the resolved com-settings, if resolved
59    #[must_use]
60    pub fn get_com(&self) -> Option<&ComSettings> {
61        self.com_resolved.as_ref()
62    }
63}
64
65#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
66pub struct BehaviorSettings {
67    /// Stop / Exit the Server on TUI quit
68    pub quit_server_on_exit: bool,
69    /// Ask before exiting the TUI (popup)
70    pub confirm_quit: bool,
71}
72
73impl Default for BehaviorSettings {
74    fn default() -> Self {
75        Self {
76            quit_server_on_exit: true,
77            confirm_quit: true,
78        }
79    }
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
83#[serde(rename_all = "lowercase")]
84pub enum MaybeComSettings {
85    ComSettings(ComSettings),
86    // Same as server, local, read adjacent server config for configuration
87    #[default]
88    Same,
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
92#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
93#[derive(Default)]
94pub struct CoverArtPosition {
95    /// Alignment of the Cover-Art in the tui
96    // TODO: clarify whether it is about the whole terminal size or just a specific component
97    pub align: Alignment,
98    /// Scale of the image
99    pub size_scale: i8,
100    /// Whether to show or hide the coverart if it is compiled in
101    pub hidden: bool,
102}
103
104#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq)]
105pub enum Alignment {
106    #[serde(rename = "top right")]
107    TopRight,
108    #[serde(rename = "top left")]
109    TopLeft,
110    #[serde(rename = "bottom right")]
111    #[default]
112    BottomRight,
113    #[serde(rename = "bottom left")]
114    BottomLeft,
115}
116
117mod v1_interop {
118    use super::{Alignment, BehaviorSettings, CoverArtPosition, MaybeComSettings, TuiSettings};
119    use crate::config::v1;
120
121    impl From<v1::Alignment> for Alignment {
122        fn from(value: v1::Alignment) -> Self {
123            match value {
124                v1::Alignment::BottomRight => Self::BottomRight,
125                v1::Alignment::BottomLeft => Self::BottomLeft,
126                v1::Alignment::TopRight => Self::TopRight,
127                v1::Alignment::TopLeft => Self::TopLeft,
128            }
129        }
130    }
131
132    #[allow(clippy::cast_possible_truncation)] // clamped casts
133    impl From<v1::Xywh> for CoverArtPosition {
134        fn from(value: v1::Xywh) -> Self {
135            Self {
136                align: value.align.into(),
137                // the value is named "width", but more use like a scale on both axis
138                size_scale: value.width_between_1_100.clamp(0, i8::MAX as u32) as i8,
139                hidden: Self::default().hidden,
140            }
141        }
142    }
143
144    impl From<v1::Settings> for TuiSettings {
145        fn from(value: v1::Settings) -> Self {
146            let theme = (&value).into();
147            Self {
148                // using "same" as the previous config version was a combined config and so only really working for local interop
149                com: MaybeComSettings::Same,
150                com_resolved: None,
151                behavior: BehaviorSettings {
152                    quit_server_on_exit: value.kill_daemon_when_quit,
153                    confirm_quit: value.enable_exit_confirmation,
154                },
155                coverart: value.album_photo_xywh.into(),
156                theme,
157                keys: value.keys.into(),
158            }
159        }
160    }
161
162    #[cfg(test)]
163    mod tests {
164        use super::*;
165
166        #[test]
167        fn should_convert_default_without_error() {
168            let converted: TuiSettings = v1::Settings::default().into();
169
170            assert_eq!(converted.com, MaybeComSettings::Same);
171            assert_eq!(
172                converted.behavior,
173                BehaviorSettings {
174                    quit_server_on_exit: true,
175                    confirm_quit: true
176                }
177            );
178
179            assert_eq!(
180                converted.coverart,
181                CoverArtPosition {
182                    align: Alignment::BottomRight,
183                    size_scale: 20,
184                    hidden: false
185                }
186            );
187
188            // the following below are already checked in their separate tests and do not need to be repeated
189            // assert_eq!(converted.theme, ());
190            // assert_eq!(converted.keys, ());
191        }
192    }
193}