termusiclib/config/v2/tui/
mod.rs

1use std::{collections::HashSet, 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: CoverArt,
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 CoverArt {
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    /// Enabled coverart display protocols. Protocols not compiled-in will not have a effect.
105    ///
106    /// Remove items from this list to disable the protocol
107    pub protocols: CoverArtProtocolsSet,
108}
109
110#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
111#[serde(transparent)]
112pub struct CoverArtProtocolsSet(HashSet<CoverArtProtocol>);
113
114impl Default for CoverArtProtocolsSet {
115    fn default() -> Self {
116        Self(HashSet::from(*PROTOCOLS_DEFAULT))
117    }
118}
119
120impl CoverArtProtocolsSet {
121    /// Check if a given [`CoverArtProtocol`] is enabled.
122    #[inline]
123    #[must_use]
124    pub fn includes_protocol(&self, protocol: CoverArtProtocol) -> bool {
125        self.0.contains(&protocol)
126    }
127}
128
129/// All protocols are enabled by default.
130pub const PROTOCOLS_DEFAULT: &[CoverArtProtocol; 4] = &[
131    CoverArtProtocol::Kitty,
132    CoverArtProtocol::Iterm2,
133    CoverArtProtocol::Sixel,
134    CoverArtProtocol::Ueberzug,
135];
136
137/// All available Cover-Art protocols in the TUI.
138///
139/// Only has a effect if said protocol is compiled-in.
140#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
141pub enum CoverArtProtocol {
142    #[serde(rename = "sixel")]
143    Sixel,
144    #[serde(rename = "iterm2", alias = "iterm")]
145    Iterm2,
146    #[serde(rename = "kitty")]
147    Kitty,
148    #[serde(rename = "ueberzug")]
149    Ueberzug,
150}
151
152#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq)]
153pub enum Alignment {
154    #[serde(rename = "top right")]
155    TopRight,
156    #[serde(rename = "top left")]
157    TopLeft,
158    #[serde(rename = "bottom right")]
159    #[default]
160    BottomRight,
161    #[serde(rename = "bottom left")]
162    BottomLeft,
163}
164
165#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
166#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
167pub struct Ytdlp {
168    /// Extra args for yt-dlp
169    pub extra_args: String,
170}
171
172mod v1_interop {
173    use super::{Alignment, BehaviorSettings, CoverArt, MaybeComSettings, TuiSettings, Ytdlp};
174    use crate::config::{v1, v2::tui::CoverArtProtocolsSet};
175
176    impl From<v1::Alignment> for Alignment {
177        fn from(value: v1::Alignment) -> Self {
178            match value {
179                v1::Alignment::BottomRight => Self::BottomRight,
180                v1::Alignment::BottomLeft => Self::BottomLeft,
181                v1::Alignment::TopRight => Self::TopRight,
182                v1::Alignment::TopLeft => Self::TopLeft,
183            }
184        }
185    }
186
187    #[allow(clippy::cast_possible_truncation)] // clamped casts
188    impl From<v1::Xywh> for CoverArt {
189        fn from(value: v1::Xywh) -> Self {
190            Self {
191                align: value.align.into(),
192                // the value is named "width", but more use like a scale on both axis
193                size_scale: value.width_between_1_100.clamp(0, i8::MAX as u32) as i8,
194                hidden: Self::default().hidden,
195                protocols: CoverArtProtocolsSet::default(),
196            }
197        }
198    }
199
200    impl From<v1::Settings> for TuiSettings {
201        fn from(value: v1::Settings) -> Self {
202            let theme = (&value).into();
203            Self {
204                // using "same" as the previous config version was a combined config and so only really working for local interop
205                com: MaybeComSettings::Same,
206                com_resolved: None,
207                behavior: BehaviorSettings {
208                    quit_server_on_exit: value.kill_daemon_when_quit,
209                    confirm_quit: value.enable_exit_confirmation,
210                },
211                coverart: value.album_photo_xywh.into(),
212                theme,
213                keys: value.keys.into(),
214                ytdlp: Ytdlp::default(),
215            }
216        }
217    }
218
219    #[cfg(test)]
220    mod tests {
221        use super::*;
222
223        #[test]
224        fn should_convert_default_without_error() {
225            let converted: TuiSettings = v1::Settings::default().into();
226
227            assert_eq!(converted.com, MaybeComSettings::Same);
228            assert_eq!(
229                converted.behavior,
230                BehaviorSettings {
231                    quit_server_on_exit: true,
232                    confirm_quit: true
233                }
234            );
235
236            assert_eq!(
237                converted.coverart,
238                CoverArt {
239                    align: Alignment::BottomRight,
240                    size_scale: 20,
241                    hidden: false,
242                    protocols: CoverArtProtocolsSet::default()
243                }
244            );
245
246            // the following below are already checked in their separate tests and do not need to be repeated
247            // assert_eq!(converted.theme, ());
248            // assert_eq!(converted.keys, ());
249        }
250    }
251}