use std::net::IpAddr;
use std::num::{NonZeroU8, NonZeroU32};
use std::path::PathBuf;
use anyhow::{Result, bail};
use include_dir::DirEntry;
use termusiclib::THEME_DIR;
use termusiclib::config::v2::server::{
Backend, ComProtocol, PositionYesNo, PositionYesNoLower, RememberLastPosition,
};
use termusiclib::config::v2::tui::Alignment as XywhAlign;
use termusiclib::config::v2::tui::theme::ThemeColors;
use termusiclib::utils::{get_app_config_path, get_pin_yin};
use tuirealm::props::{PropPayload, PropValue, Style, TableBuilder, TextSpan};
use tuirealm::ratatui::layout::{Constraint, Layout, Rect};
use tuirealm::ratatui::widgets::Clear;
use tuirealm::{AttrValue, Attribute, Frame, State, StateValue};
use crate::ui::Application;
use crate::ui::components::ConfigSavePopup;
use crate::ui::components::config_editor::update::THEMES_WITHOUT_FILES;
use crate::ui::components::raw::dynamic_height_grid::DynamicHeightGrid;
use crate::ui::components::raw::uniform_dynamic_grid::UniformDynamicGrid;
use crate::ui::ids::{Id, IdCEGeneral, IdCETheme, IdConfigEditor, IdKey, IdKeyGlobal, IdKeyOther};
use crate::ui::model::{Model, UserEvent};
use crate::ui::msg::{ConfigEditorLayout, KFGLOBAL_FOCUS_ORDER, KFOTHER_FOCUS_ORDER, Msg};
use crate::ui::utils::draw_area_in_absolute;
macro_rules! sat_add {
($first:expr$(,)?) => {
$first
};
(
$first:expr
$(
, $second:expr
)* $(,)?
) => {
$first.saturating_add(sat_add!($($second,)*))
}
}
macro_rules! to_boxed_slice {
($first:expr$(,)?) => {
$first
};
(
$first:expr
$(
, $second:expr
)* $(,)?
) => {
Box::from([$first, $($second,)*])
}
}
macro_rules! app_view {
(
$app:expr, $f:expr,
$($id:expr => $cell:expr$(,)?)*
) => {
$($app.view($id, $f, $cell);)*
}
}
impl Model {
pub fn view_config_editor(&mut self) {
self.terminal
.raw_mut()
.draw(|f| {
let common_style =
if self.config_editor.last_layout == ConfigEditorLayout::ThemeAndColor {
Style::new().bg(self.config_editor.theme.fallback_background())
} else {
Style::new().bg(self
.config_tui
.read_recursive()
.settings
.theme
.fallback_background())
};
let chunk_main = Self::view_config_editor_common(&mut self.app, f, common_style);
match self.config_editor.last_layout {
ConfigEditorLayout::General => {
Self::view_config_editor_general(&mut self.app, f, chunk_main);
}
ConfigEditorLayout::ThemeAndColor => {
Self::view_config_editor_color(&mut self.app, f, chunk_main);
}
ConfigEditorLayout::KeyGlobal => {
Self::view_config_editor_key1(&mut self.app, f, chunk_main);
}
ConfigEditorLayout::KeyOther => {
Self::view_config_editor_key2(&mut self.app, f, chunk_main);
}
}
Self::view_config_editor_popups(&mut self.app, f);
})
.expect("Expected to draw without error");
}
fn view_config_editor_common(
app: &mut Application<Id, Msg, UserEvent>,
f: &mut Frame<'_>,
common_style: Style,
) -> Rect {
let [header, chunk_main, footer] = Layout::vertical([
Constraint::Length(2), Constraint::Min(3),
Constraint::Length(1), ])
.areas(f.area());
app.view(&Id::ConfigEditor(IdConfigEditor::Header), f, header);
f.buffer_mut().set_style(chunk_main, common_style);
app.view(&Id::ConfigEditor(IdConfigEditor::Footer), f, footer);
chunk_main
}
fn view_config_editor_general(
app: &mut Application<Id, Msg, UserEvent>,
f: &mut Frame<'_>,
chunk_main: Rect,
) {
let focus_elem = app
.focus()
.and_then(|v| {
if let Id::ConfigEditor(id) = *v {
Some(id)
} else {
None
}
})
.and_then(|v| {
if let IdConfigEditor::General(v) = v {
Some(match v {
IdCEGeneral::MusicDir => 0,
IdCEGeneral::ExitConfirmation => 1,
IdCEGeneral::PlaylistDisplaySymbol => 2,
IdCEGeneral::PlaylistRandomTrack => 3,
IdCEGeneral::PlaylistRandomAlbum => 4,
IdCEGeneral::PodcastDir => 5,
IdCEGeneral::PodcastSimulDownload => 6,
IdCEGeneral::PodcastMaxRetries => 7,
IdCEGeneral::AlbumPhotoAlign => 8,
IdCEGeneral::SaveLastPosition => 9,
IdCEGeneral::SeekStep => 10,
IdCEGeneral::KillDamon => 11,
IdCEGeneral::PlayerUseMpris => 12,
IdCEGeneral::PlayerUseDiscord => 13,
IdCEGeneral::PlayerPort => 14,
IdCEGeneral::PlayerAddress => 15,
IdCEGeneral::PlayerProtocol => 16,
IdCEGeneral::PlayerUDSPath => 17,
IdCEGeneral::PlayerBackend => 18,
IdCEGeneral::ExtraYtdlpArgs => 19,
})
} else {
None
}
});
let cells = UniformDynamicGrid::new(20, 3, 56 + 2)
.draw_row_low_space()
.distribute_row_space()
.focus_node(focus_elem)
.split(chunk_main);
app_view! {
app, f,
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::MusicDir)) => cells[0],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::ExitConfirmation)) => cells[1],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlaylistDisplaySymbol)) => cells[2],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlaylistRandomTrack)) => cells[3],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlaylistRandomAlbum)) => cells[4],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PodcastDir)) => cells[5],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PodcastSimulDownload)) => cells[6],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PodcastMaxRetries)) => cells[7],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::AlbumPhotoAlign)) => cells[8],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::SaveLastPosition)) => cells[9],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::SeekStep)) => cells[10],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::KillDamon)) => cells[11],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerUseMpris)) => cells[12],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerUseDiscord)) => cells[13],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerPort)) => cells[14],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerAddress)) => cells[15],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerProtocol)) => cells[16],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerUDSPath)) => cells[17],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerBackend)) => cells[18],
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::ExtraYtdlpArgs)) => cells[19],
}
}
fn view_config_editor_popups(app: &mut Application<Id, Msg, UserEvent>, f: &mut Frame<'_>) {
if app.mounted(&Id::ConfigEditor(IdConfigEditor::ConfigSavePopup)) {
let popup = draw_area_in_absolute(f.area(), 50, 3);
f.render_widget(Clear, popup);
app.view(&Id::ConfigEditor(IdConfigEditor::ConfigSavePopup), f, popup);
}
if app.mounted(&Id::ErrorPopup) {
let popup = draw_area_in_absolute(f.area(), 50, 4);
f.render_widget(Clear, popup);
app.view(&Id::ErrorPopup, f, popup);
}
}
#[allow(clippy::too_many_lines)]
fn view_config_editor_color(
app: &mut Application<Id, Msg, UserEvent>,
f: &mut Frame<'_>,
chunk_main: Rect,
) {
macro_rules! is_expanded {
($id:expr, $yes:expr, $no:expr) => {
match app.state(&Id::ConfigEditor($id)) {
Ok(State::One(_)) => $no,
_ => $yes,
}
};
}
let library_foreground_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LibraryForeground), 8, 3);
let library_background_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LibraryBackground), 8, 3);
let library_border_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LibraryBorder), 8, 3);
let library_highlight_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LibraryHighlight), 8, 3);
let playlist_foreground_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::PlaylistForeground), 8, 3);
let playlist_background_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::PlaylistBackground), 8, 3);
let playlist_border_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::PlaylistBorder), 8, 3);
let playlist_highlight_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::PlaylistHighlight), 8, 3);
let progress_foreground_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::ProgressForeground), 8, 3);
let progress_background_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::ProgressBackground), 8, 3);
let progress_border_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::ProgressBorder), 8, 3);
let lyric_foreground_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LyricForeground), 8, 3);
let lyric_background_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LyricBackground), 8, 3);
let lyric_border_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::LyricBorder), 8, 3);
let important_popup_foreground_len: u16 = is_expanded!(
IdConfigEditor::Theme(IdCETheme::ImportantPopupForeground),
8,
3
);
let important_popup_background_len: u16 = is_expanded!(
IdConfigEditor::Theme(IdCETheme::ImportantPopupBackground),
8,
3
);
let important_popup_border_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::ImportantPopupBorder), 8, 3);
let fallback_foreground_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::FallbackForeground), 8, 3);
let fallback_background_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::FallbackBackground), 8, 3);
let fallback_border_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::FallbackBorder), 8, 3);
let fallback_highlight_len: u16 =
is_expanded!(IdConfigEditor::Theme(IdCETheme::FallbackHighlight), 8, 3);
let [left, right] = Layout::horizontal([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
.areas(chunk_main);
let library_height = sat_add! {
1u16, library_foreground_len,
library_background_len,
library_border_len,
library_highlight_len,
3u16, };
let playlist_height = sat_add! {
1u16, playlist_foreground_len,
playlist_background_len,
playlist_border_len,
playlist_highlight_len,
3u16, 3u16, };
let progress_height = sat_add! {
1u16, progress_foreground_len,
progress_background_len,
progress_border_len
};
let lyric_height = sat_add! {
1u16, lyric_foreground_len,
lyric_background_len,
lyric_border_len
};
let important_popup_height = sat_add! {
1u16, important_popup_foreground_len,
important_popup_background_len,
important_popup_border_len
};
let fallback_height = sat_add! {
1u16, fallback_foreground_len,
fallback_background_len,
fallback_border_len,
fallback_highlight_len
};
let elem_height = to_boxed_slice! {
library_height,
playlist_height,
progress_height,
lyric_height,
important_popup_height,
fallback_height,
};
let focus_elem = app
.focus()
.and_then(|v| {
if let Id::ConfigEditor(id) = *v {
Some(id)
} else {
None
}
})
.and_then(|v| {
if let IdConfigEditor::Theme(v) = v {
Some(match v {
IdCETheme::LibraryLabel
| IdCETheme::LibraryForeground
| IdCETheme::LibraryBackground
| IdCETheme::LibraryBorder
| IdCETheme::LibraryHighlight
| IdCETheme::LibraryHighlightSymbol => 0,
IdCETheme::PlaylistLabel
| IdCETheme::PlaylistForeground
| IdCETheme::PlaylistBackground
| IdCETheme::PlaylistBorder
| IdCETheme::PlaylistHighlight
| IdCETheme::PlaylistHighlightSymbol
| IdCETheme::CurrentlyPlayingTrackSymbol => 1,
IdCETheme::ProgressLabel
| IdCETheme::ProgressForeground
| IdCETheme::ProgressBackground
| IdCETheme::ProgressBorder => 2,
IdCETheme::LyricLabel
| IdCETheme::LyricForeground
| IdCETheme::LyricBackground
| IdCETheme::LyricBorder => 3,
IdCETheme::ImportantPopupLabel
| IdCETheme::ImportantPopupForeground
| IdCETheme::ImportantPopupBackground
| IdCETheme::ImportantPopupBorder => 4,
IdCETheme::FallbackLabel
| IdCETheme::FallbackForeground
| IdCETheme::FallbackBackground
| IdCETheme::FallbackBorder
| IdCETheme::FallbackHighlight => 5,
IdCETheme::ThemeSelectTable => return None,
})
} else {
None
}
});
let cells = DynamicHeightGrid::new(elem_height, 16 + 2)
.with_row_spacing(1)
.draw_row_low_space()
.distribute_row_space()
.focus_node(focus_elem)
.split(right);
let chunks_library = Layout::vertical([
Constraint::Length(1),
Constraint::Length(library_foreground_len),
Constraint::Length(library_background_len),
Constraint::Length(library_border_len),
Constraint::Length(library_highlight_len),
Constraint::Length(3),
Constraint::Min(0),
])
.split(cells[0]);
let chunks_playlist = Layout::vertical([
Constraint::Length(1),
Constraint::Length(playlist_foreground_len),
Constraint::Length(playlist_background_len),
Constraint::Length(playlist_border_len),
Constraint::Length(playlist_highlight_len),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(cells[1]);
let chunks_progress = Layout::vertical([
Constraint::Length(1),
Constraint::Length(progress_foreground_len),
Constraint::Length(progress_background_len),
Constraint::Length(progress_border_len),
Constraint::Min(0),
])
.split(cells[2]);
let chunks_lyric = Layout::vertical([
Constraint::Length(1),
Constraint::Length(lyric_foreground_len),
Constraint::Length(lyric_background_len),
Constraint::Length(lyric_border_len),
Constraint::Min(0),
])
.split(cells[3]);
let chunks_important_popup = Layout::vertical([
Constraint::Length(1),
Constraint::Length(important_popup_foreground_len),
Constraint::Length(important_popup_background_len),
Constraint::Length(important_popup_border_len),
Constraint::Min(0),
])
.split(cells[4]);
let chunks_fallback = Layout::vertical([
Constraint::Length(1),
Constraint::Length(fallback_foreground_len),
Constraint::Length(fallback_background_len),
Constraint::Length(fallback_border_len),
Constraint::Length(fallback_highlight_len),
Constraint::Min(0),
])
.split(cells[5]);
app_view! {
app, f,
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ThemeSelectTable)) => left,
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LibraryLabel)) => chunks_library[0],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LibraryForeground)) => chunks_library[1],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LibraryBackground)) => chunks_library[2],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LibraryBorder)) => chunks_library[3],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LibraryHighlight)) => chunks_library[4],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LibraryHighlightSymbol)) => chunks_library[5],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::PlaylistLabel)) => chunks_playlist[0],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::PlaylistForeground)) => chunks_playlist[1],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::PlaylistBackground)) => chunks_playlist[2],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::PlaylistBorder)) => chunks_playlist[3],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::PlaylistHighlight)) => chunks_playlist[4],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::PlaylistHighlightSymbol)) => chunks_playlist[5],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::CurrentlyPlayingTrackSymbol)) => chunks_playlist[6],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ProgressLabel)) => chunks_progress[0],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ProgressForeground)) => chunks_progress[1],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ProgressBackground)) => chunks_progress[2],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ProgressBorder)) => chunks_progress[3],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LyricLabel)) => chunks_lyric[0],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LyricForeground)) => chunks_lyric[1],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LyricBackground)) => chunks_lyric[2],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::LyricBorder)) => chunks_lyric[3],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ImportantPopupLabel)) => chunks_important_popup[0],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ImportantPopupForeground)) => chunks_important_popup[1],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ImportantPopupBackground)) => chunks_important_popup[2],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ImportantPopupBorder)) => chunks_important_popup[3],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::FallbackLabel)) => chunks_fallback[0],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::FallbackForeground)) => chunks_fallback[1],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::FallbackBackground)) => chunks_fallback[2],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::FallbackBorder)) => chunks_fallback[3],
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::FallbackHighlight)) => chunks_fallback[4],
}
}
fn view_config_editor_key1(
app: &mut Application<Id, Msg, UserEvent>,
f: &mut Frame<'_>,
chunk_main: Rect,
) {
KeyDisplay::new(KFGLOBAL_FOCUS_ORDER, 23 + 2).view(app, chunk_main, f);
}
fn view_config_editor_key2(
app: &mut Application<Id, Msg, UserEvent>,
f: &mut Frame<'_>,
chunk_main: Rect,
) {
KeyDisplay::new(KFOTHER_FOCUS_ORDER, 25 + 2).view(app, chunk_main, f);
}
pub fn mount_config_editor(&mut self) {
self.mount_config_editor_components()
.expect("Expected Config Editor Components to mount correctly");
if let Err(e) = self.theme_select_load_themes() {
self.mount_error_popup(e.context("load themes"));
}
self.theme_select_sync(None);
if let Err(e) = self.update_photo() {
self.mount_error_popup(e.context("update_photo"));
}
}
fn mount_config_editor_components(&mut self) -> Result<()> {
self.config_editor.last_layout = ConfigEditorLayout::default();
self.remount_config_general()?;
self.remount_config_color(&self.config_tui.clone(), None)?;
self.remount_config_keys()?;
self.app.active(&Id::ConfigEditor(IdConfigEditor::General(
IdCEGeneral::MusicDir,
)))?;
self.remount_config_header_footer()?;
Ok(())
}
pub fn umount_config_editor(&mut self) {
self.playlist_reload();
self.database_reload();
self.progress_reload();
self.mount_label_help();
self.lyric_reload();
self.umount_config_editor_components()
.expect("Expected Config Editor Components to unmount correctly");
self.remount_global_listener()
.expect("Expected Global Hotkey Listener to remount correctly");
if let Err(e) = self.update_photo() {
self.mount_error_popup(e.context("update_photo"));
}
}
fn umount_config_editor_components(&mut self) -> Result<()> {
self.umount_config_header_footer()?;
self.umount_config_general()?;
self.umount_config_color()?;
self.umount_config_keys()?;
Ok(())
}
pub fn mount_config_save_popup(&mut self) {
assert!(
self.app
.remount(
Id::ConfigEditor(IdConfigEditor::ConfigSavePopup),
Box::new(ConfigSavePopup::new(self.config_tui.clone())),
vec![]
)
.is_ok()
);
assert!(
self.app
.active(&Id::ConfigEditor(IdConfigEditor::ConfigSavePopup))
.is_ok()
);
}
#[allow(clippy::too_many_lines)]
pub fn collect_config_data(&mut self) -> Result<()> {
let mut config_tui = self.config_tui.write();
match self.config_editor.key_config.check_keys() {
Ok(()) => config_tui.settings.keys = self.config_editor.key_config.clone(),
Err(err) => bail!(err),
}
config_tui.settings.theme = self.config_editor.theme.clone();
let mut config_server = self.config_server.write();
if let Ok(State::One(StateValue::String(music_dir))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::MusicDir),
)) {
let vec = music_dir
.split(';')
.map(PathBuf::from)
.filter(|p| {
let absolute_dir = shellexpand::path::tilde(p);
absolute_dir.exists()
})
.collect();
config_server.settings.player.music_dirs = vec;
}
if let Ok(State::One(StateValue::Usize(exit_confirmation))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::ExitConfirmation)),
) {
config_tui.settings.behavior.confirm_quit = matches!(exit_confirmation, 0);
}
if let Ok(State::One(StateValue::Usize(display_symbol))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlaylistDisplaySymbol)),
) {
config_tui
.settings
.theme
.style
.playlist
.use_loop_mode_symbol = matches!(display_symbol, 0);
}
if let Ok(State::One(StateValue::String(random_track_quantity_str))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlaylistRandomTrack)),
) && let Ok(quantity) = random_track_quantity_str.parse::<NonZeroU32>()
{
config_server.settings.player.random_track_quantity = quantity;
}
if let Ok(State::One(StateValue::String(random_album_quantity_str))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlaylistRandomAlbum)),
) && let Ok(quantity) = random_album_quantity_str.parse::<NonZeroU32>()
{
config_server.settings.player.random_album_min_quantity = quantity;
}
if let Ok(State::One(StateValue::String(podcast_dir))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::PodcastDir),
)) {
let absolute_dir = shellexpand::path::tilde(&podcast_dir);
if absolute_dir.exists() {
config_server.settings.podcast.download_dir = absolute_dir.into_owned();
}
}
if let Ok(State::One(StateValue::String(podcast_simul_download))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PodcastSimulDownload)),
) && let Ok(quantity) = podcast_simul_download.parse::<NonZeroU8>()
{
config_server.settings.podcast.concurrent_downloads_max = quantity;
}
if let Ok(State::One(StateValue::String(podcast_max_retries))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PodcastMaxRetries)),
) && let Ok(quantity) = podcast_max_retries.parse::<u8>()
{
if (1..11).contains(&quantity) {
config_server.settings.podcast.max_download_retries = quantity;
} else {
bail!(" It's not recommended to set max retries to more than 10. ");
}
}
if let Ok(State::One(StateValue::Usize(align))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::AlbumPhotoAlign),
)) {
let align = match align {
0 => XywhAlign::BottomRight,
1 => XywhAlign::BottomLeft,
2 => XywhAlign::TopRight,
_ => XywhAlign::TopLeft,
};
config_tui.settings.coverart.align = align;
}
if let Ok(State::One(StateValue::Usize(save_last_position))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::SaveLastPosition)),
) {
if save_last_position != 0 {
let new_val = match save_last_position {
1 => RememberLastPosition::All(PositionYesNo::Simple(PositionYesNoLower::No)),
2 => RememberLastPosition::All(PositionYesNo::Simple(PositionYesNoLower::Yes)),
_ => unreachable!(),
};
config_server.settings.player.remember_position = new_val;
}
}
if let Ok(State::One(StateValue::Usize(seek_step))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::SeekStep),
)) {
let _ = seek_step;
}
if let Ok(State::One(StateValue::Usize(kill_daemon))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::KillDamon),
)) {
config_tui.settings.behavior.quit_server_on_exit = matches!(kill_daemon, 0);
}
if let Ok(State::One(StateValue::Usize(player_use_mpris))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerUseMpris)),
) {
config_server.settings.player.use_mediacontrols = matches!(player_use_mpris, 0);
}
if let Ok(State::One(StateValue::Usize(player_use_discord))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::PlayerUseDiscord)),
) {
config_server.settings.player.set_discord_status = matches!(player_use_discord, 0);
}
if let Ok(State::One(StateValue::String(player_port))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::PlayerPort),
)) && let Ok(port) = player_port.parse::<u16>()
{
if (1024..u16::MAX).contains(&port) {
config_server.settings.com.port = port;
} else {
bail!(" It's not recommended to use ports below 1024 for the player. ");
}
}
if let Ok(State::One(StateValue::String(player_port))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::PlayerAddress),
)) && let Ok(addr) = player_port.parse::<IpAddr>()
{
config_server.settings.com.address = addr;
}
if let Ok(State::One(StateValue::Usize(align))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::PlayerProtocol),
)) {
let protocol = match align {
0 => ComProtocol::HTTP,
1 => {
if cfg!(not(unix)) {
bail!("UDS Protocol is only supported on unix systems");
}
ComProtocol::UDS
}
_ => unreachable!(),
};
config_server.settings.com.protocol = protocol;
}
if let Ok(State::One(StateValue::String(podcast_dir))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::PlayerUDSPath),
)) {
let abs_path = shellexpand::path::tilde(&podcast_dir);
if !abs_path.has_root()
|| abs_path.file_name().is_none()
|| abs_path.extension().is_none_or(|v| v != "socket")
{
bail!(
"Invalid UDS socket Path.\nPath need to be absolute and end with \".socket\"."
);
}
config_server.settings.com.socket_path = abs_path.into_owned();
}
if let Ok(State::One(StateValue::Usize(align))) = self.app.state(&Id::ConfigEditor(
IdConfigEditor::General(IdCEGeneral::PlayerBackend),
)) {
let backend = match align {
0 => Backend::Rusty,
1 => Backend::Mpv,
2 => Backend::Gstreamer,
_ => unreachable!(),
};
config_server.settings.player.backend = backend;
}
if let Ok(State::One(StateValue::String(extra_ytdlp_args))) = self.app.state(
&Id::ConfigEditor(IdConfigEditor::General(IdCEGeneral::ExtraYtdlpArgs)),
) {
config_tui.settings.ytdlp.extra_args = extra_ytdlp_args;
}
Ok(())
}
pub fn theme_extract_all() -> Result<()> {
let mut path = get_app_config_path()?;
path.push("themes");
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
let base_path = &path;
for entry in THEME_DIR.entries() {
let path = base_path.join(entry.path());
match entry {
DirEntry::Dir(d) => {
std::fs::create_dir_all(&path)?;
d.extract(base_path)?;
}
DirEntry::File(f) => {
if !path.exists() {
std::fs::write(path, f.contents())?;
}
}
}
}
Ok(())
}
pub fn theme_select_load_themes(&mut self) -> Result<()> {
let mut path = get_app_config_path()?;
path.push("themes");
if let Ok(paths) = std::fs::read_dir(path) {
self.config_editor.themes.clear();
let mut paths: Vec<_> = paths.filter_map(std::result::Result::ok).collect();
paths.sort_by_cached_key(|k| get_pin_yin(&k.file_name().to_string_lossy()));
for entry in paths {
let path = entry.path();
let Some(stem) = path.file_stem() else {
warn!("Theme {:#?} does not have a filestem!", path.display());
continue;
};
self.config_editor
.themes
.push(stem.to_string_lossy().to_string());
}
}
Ok(())
}
pub fn theme_select_sync(&mut self, previous_index: Option<usize>) {
let mut table: TableBuilder = TableBuilder::default();
table
.add_col(TextSpan::new(0.to_string()))
.add_col(TextSpan::new("Termusic Default"));
table.add_row();
table
.add_col(TextSpan::new(1.to_string()))
.add_col(TextSpan::new("Native"));
for (idx, record) in self.config_editor.themes.iter().enumerate() {
table.add_row();
table
.add_col(TextSpan::new((idx + THEMES_WITHOUT_FILES).to_string()))
.add_col(TextSpan::new(record));
}
let table = table.build();
self.app
.attr(
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ThemeSelectTable)),
Attribute::Content,
AttrValue::Table(table),
)
.ok();
let index = if let Some(index) = previous_index {
index
} else {
let mut index = None;
if let Some(current_file_name) = self.config_editor.theme.theme.file_name.as_ref() {
for (idx, name) in self.config_editor.themes.iter().enumerate() {
if name == current_file_name {
index = Some(idx + THEMES_WITHOUT_FILES);
break;
}
}
} else if self.config_editor.theme.theme.name == ThemeColors::full_default().name {
index = Some(0);
} else if self.config_editor.theme.theme.name == ThemeColors::full_native().name {
index = Some(1);
}
index.unwrap_or(0)
};
assert!(
self.app
.attr(
&Id::ConfigEditor(IdConfigEditor::Theme(IdCETheme::ThemeSelectTable)),
Attribute::Value,
AttrValue::Payload(PropPayload::One(PropValue::Usize(index))),
)
.is_ok()
);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum KeyDisplayType {
Global,
Other,
}
#[derive(Debug)]
struct KeyDisplay<'a> {
elems: &'a [IdKey],
discriminant: KeyDisplayType,
width: u16,
}
impl<'a> KeyDisplay<'a> {
pub fn new(elems: &'a [IdKey], width: u16) -> Self {
let discriminant = std::mem::discriminant(&IdConfigEditor::from(&elems[0]));
let discriminant = if discriminant
== std::mem::discriminant(&IdConfigEditor::KeyGlobal(IdKeyGlobal::Config))
{
KeyDisplayType::Global
} else if discriminant
== std::mem::discriminant(&IdConfigEditor::KeyOther(IdKeyOther::DatabaseAddAll))
{
KeyDisplayType::Other
} else {
unimplemented!("Invalid Discriminant: {:#?}", discriminant)
};
Self {
elems,
discriminant,
width,
}
}
pub fn view(&self, model: &mut Application<Id, Msg, UserEvent>, area: Rect, f: &mut Frame<'_>) {
macro_rules! is_expanded {
($id:expr, $yes:expr, $no:expr) => {
match model.state(&Id::ConfigEditor($id)) {
Ok(State::One(_)) => $no,
_ => $yes,
}
};
}
let mut elems_heights = Vec::with_capacity(self.elems.len());
for id in self.elems {
elems_heights.push(is_expanded!(IdConfigEditor::from(id), 8, 3));
}
let focus_elem = model
.focus()
.and_then(|v| match self.discriminant {
KeyDisplayType::Global => {
if let Id::ConfigEditor(IdConfigEditor::KeyGlobal(key)) = *v {
Some(IdKey::Global(key))
} else {
None
}
}
KeyDisplayType::Other => {
if let Id::ConfigEditor(IdConfigEditor::KeyOther(key)) = *v {
Some(IdKey::Other(key))
} else {
None
}
}
})
.and_then(|focus| {
self.elems
.iter()
.enumerate()
.find(|(_, v)| **v == focus)
.map(|(idx, _)| idx)
});
let cells = DynamicHeightGrid::new(elems_heights, self.width)
.draw_row_low_space()
.distribute_row_space()
.focus_node(focus_elem)
.split(area);
for (id, cell) in self.elems.iter().zip(cells.iter()) {
model.view(&Id::ConfigEditor(id.into()), f, *cell);
}
}
}