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