diff --git a/flake.lock b/flake.lock index 0f87cc8..ffc4ab4 100644 --- a/flake.lock +++ b/flake.lock @@ -88,11 +88,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1748309015, - "narHash": "sha256-NVgo/saT8uehYYwwhzWrTTFlpH0icR2E3tHHKsUouJ4=", + "lastModified": 1748449332, + "narHash": "sha256-3Qr0pThpHgbIhscOia2ETxo2ge7el458Z9pv5+ONTKc=", "owner": "nix-community", "repo": "emacs-overlay", - "rev": "0fba546d9aa235fc726fe9c8c3bb703e918c14c4", + "rev": "44f10e03d04b678d13ee9c36a9bd632dbce84103", "type": "github" }, "original": { @@ -376,11 +376,11 @@ ] }, "locked": { - "lastModified": 1748391243, - "narHash": "sha256-7sCuihzsTRZemtbTXaFUoGJUfuQErhKEcL9v7HKIo1k=", + "lastModified": 1748455938, + "narHash": "sha256-mQ/iNzPra2WtDQ+x2r5IadcWNr0m3uHvLMzJkXKAG/8=", "owner": "nix-community", "repo": "home-manager", - "rev": "f5b12be834874f7661db4ced969a621ab2d57971", + "rev": "02077149e2921014511dac2729ae6dadb4ec50e2", "type": "github" }, "original": { @@ -785,11 +785,11 @@ "xwayland-satellite-unstable": "xwayland-satellite-unstable" }, "locked": { - "lastModified": 1748441725, - "narHash": "sha256-Coxe8x6tuE7URW73/qjwKeggrxMYTcdlmMx0u/TUfUg=", + "lastModified": 1748450232, + "narHash": "sha256-6w7klfyLkEaQ9PlE6z3vYPRJWsguL9JsUdU1Bkv6fbs=", "owner": "sodiboo", "repo": "niri-flake", - "rev": "2933c5d663b47e2934cf09fda3b5aebaa9bc2124", + "rev": "8319d7cc74becbce927ab6c1e5b0027d22855c9f", "type": "github" }, "original": { @@ -1004,11 +1004,11 @@ }, "nixpkgs-stable_2": { "locked": { - "lastModified": 1748162331, - "narHash": "sha256-rqc2RKYTxP3tbjA+PB3VMRQNnjesrT0pEofXQTrMsS8=", + "lastModified": 1748302896, + "narHash": "sha256-ixMT0a8mM091vSswlTORZj93WQAJsRNmEvqLL+qwTFM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", + "rev": "7848cd8c982f7740edf76ddb3b43d234cb80fc4d", "type": "github" }, "original": { @@ -1659,11 +1659,11 @@ ] }, "locked": { - "lastModified": 1748436066, - "narHash": "sha256-smVwlauRs/j/mlxYfEqncNg1tvxAlkC/taw1+pJHtWw=", + "lastModified": 1748467104, + "narHash": "sha256-EgYaWbzbtYFLZH1/Eni1ZdLgTwYhe8g9m10UPdBMCmY=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "f39ee486e6a14fc30c4e7d00bf389046640c2f63", + "rev": "68d2b647f845f6679db96468336aba5dfae901fa", "type": "github" }, "original": { diff --git a/homes/emenel/default.nix b/homes/emenel/default.nix index 41000c2..cdfc82f 100644 --- a/homes/emenel/default.nix +++ b/homes/emenel/default.nix @@ -37,6 +37,7 @@ in texlive.combined.scheme-full openrgb-with-all-plugins bazecor + faircamp ]) ]; diff --git a/hosts/eddie/configuration.nix b/hosts/eddie/configuration.nix index 603847d..b7e0c4a 100644 --- a/hosts/eddie/configuration.nix +++ b/hosts/eddie/configuration.nix @@ -28,10 +28,22 @@ inputs.nh.overlays.default # inputs.emacs-lsp-booster.overlays.default + # (final: prev: { + # termusic-patched = pkgs.termusic.overrideAttrs (old: { + # src = pkgs.fetchFromGithub { + # owner = ""; + # repo = ""; + # rev = ""; + # hash = ""; + # }; + # patches = old.patches ++ [ ../../patches/termusic-503.patch ]; + # }); + # }) + (final: prev: { wineWowPackages.stagingFull = pkgs-stable.wineWowPackages.stagingFull.overrideAttrs (old: { - patches = old.patches ++ [ ../../patches/wine-6006.patch ]; + patches = [ ../../patches/wine-6006.patch ]; waylandSupport = true; fontconfigSupport = true; vulkanSupport = true; diff --git a/modules/home/mpd.nix b/modules/home/mpd.nix index e1a60df..550181d 100644 --- a/modules/home/mpd.nix +++ b/modules/home/mpd.nix @@ -4,6 +4,8 @@ home.packages = with pkgs; [ mpc nix-config.packages.x86_64-linux.rmpc-latest + # termusic-patched + termusic ]; services = { diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index 78e4496..3718bf5 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -241,6 +241,14 @@ window#waybar { border: none; } +#cpu { + border-right: 0; +} + +#memory { + border-left: 0; +} + #taskbar button { margin: 0 3px; padding: 3px 6px; @@ -270,17 +278,14 @@ window#waybar { #battery.charging { color: #ffffff; - background-color: #26A65B; } #battery.warning:not(.charging) { - background-color: #ffbe61; - color: black; + color: #ffbe61; } #battery.critical:not(.charging) { - background-color: #f53c3c; - color: #ffffff; + color: #f53c3c; animation-name: blink; animation-duration: 0.5s; animation-timing-function: linear; diff --git a/patches/termusic-503.patch b/patches/termusic-503.patch new file mode 100644 index 0000000..0ae4ebc --- /dev/null +++ b/patches/termusic-503.patch @@ -0,0 +1,673 @@ +From a319c68c62ae93c1604c84d488192ca0e8551cb5 Mon Sep 17 00:00:00 2001 +From: hasezoey +Date: Wed, 28 May 2025 16:21:25 +0200 +Subject: [PATCH 1/3] feat(tui::components::music_library): change music + library tree loading to be async and non-blocking + +Downside is that the placeholder still acts as a correct path and that multiple loads can be done and overwrite eachother. + +fixes #481 +--- + CHANGELOG.md | 1 + + lib/src/types.rs | 13 ++ + tui/src/ui/components/config_editor/view.rs | 2 +- + tui/src/ui/components/music_library.rs | 226 +++++++++++--------- + tui/src/ui/components/playlist.rs | 2 +- + tui/src/ui/components/tag_editor/update.rs | 2 +- + tui/src/ui/model/mod.rs | 19 +- + tui/src/ui/model/update.rs | 32 +-- + 8 files changed, 172 insertions(+), 125 deletions(-) + +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 2cd911a25..0641e5f29 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -21,6 +21,7 @@ + - Fix(tui): on track change, dont select "current track" playlist item if the old "current track" was not selected. + - Fix(tui): re-select the (approximately) same playlist item after a shuffle. + - Fix(tui): escape key will now no longer also act as a quit key. ++- Fix(tui): changed the music library tree loading to be async. + - Fix(server): with `rusty-soundtouch` on `rusty` backend, dont take initial samples until necessary. + - Fix(server): on rusty backend, always decode and use `f32` samples. (instead of `i16`) + - Fix(server): on rusty backend, update `soundtouch` version to fix build issues on latest arch & gcc 15. +diff --git a/lib/src/types.rs b/lib/src/types.rs +index d26802f9d..f60080802 100644 +--- a/lib/src/types.rs ++++ b/lib/src/types.rs +@@ -1,3 +1,4 @@ ++use std::path::PathBuf; + use std::sync::Arc; + + use crate::config::v2::tui::{keys::KeyBinding, theme::styles::ColorTermusic}; +@@ -387,6 +388,14 @@ pub enum KFMsg { + PodcastRefreshAllFeedsBlurUp, + } + ++/// Basically a Tree Node, but without having to include `tui-realm-treeview` as another dependency for lib ++#[derive(Debug, Clone, PartialEq, Eq)] ++pub struct RecVec { ++ pub id: T, ++ pub value: V, ++ pub children: Vec>, ++} ++ + #[derive(Clone, Debug, PartialEq, Eq)] + pub enum LIMsg { + TreeStepInto(String), +@@ -397,6 +406,10 @@ pub enum LIMsg { + SwitchRoot, + AddRoot, + RemoveRoot, ++ ++ /// A requested node is ready from loading. ++ /// `(Tree, FocusNode)` ++ TreeNodeReady(RecVec, Option), + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] +diff --git a/tui/src/ui/components/config_editor/view.rs b/tui/src/ui/components/config_editor/view.rs +index de259702a..1a96d2177 100644 +--- a/tui/src/ui/components/config_editor/view.rs ++++ b/tui/src/ui/components/config_editor/view.rs +@@ -1732,7 +1732,7 @@ impl Model { + } + + pub fn umount_config_editor(&mut self) { +- self.library_reload_tree(); ++ self.library_scan_dir(&self.library.tree_path, None); + self.playlist_reload(); + self.database_reload(); + self.progress_reload(); +diff --git a/tui/src/ui/components/music_library.rs b/tui/src/ui/components/music_library.rs +index afd6cc6ad..6a4d65e5c 100644 +--- a/tui/src/ui/components/music_library.rs ++++ b/tui/src/ui/components/music_library.rs +@@ -1,14 +1,15 @@ +-use crate::ui::model::Model; ++use crate::ui::model::{DownloadTracker, Model}; + use crate::ui::tui_cmd::TuiCmd; + use crate::utils::get_pin_yin; + use anyhow::{bail, Context, Result}; + use std::fs::{remove_dir_all, remove_file, rename, DirEntry}; + use std::path::{Path, PathBuf}; ++use std::sync::mpsc::Sender; + use termusiclib::config::v2::server::config_extra::ServerConfigVersionedDefaulted; + use termusiclib::config::v2::server::ScanDepth; + use termusiclib::config::SharedTuiSettings; + use termusiclib::ids::Id; +-use termusiclib::types::{GSMsg, LIMsg, Msg, PLMsg, TEMsg, YSMsg}; ++use termusiclib::types::{GSMsg, LIMsg, Msg, PLMsg, RecVec, TEMsg, YSMsg}; + use tui_realm_treeview::{Node, Tree, TreeView, TREE_CMD_CLOSE, TREE_CMD_OPEN, TREE_INITIAL_NODE}; + use tuirealm::command::{Cmd, CmdResult, Direction, Position}; + use tuirealm::event::{Key, KeyEvent, KeyModifiers, NoUserEvent}; +@@ -234,14 +235,6 @@ impl Component for MusicLibrary { + } + + impl Model { +- pub fn library_scan_dir>(&mut self, p: P) { +- self.library.tree_path = p.into(); +- self.library.tree = Tree::new(Self::library_dir_tree( +- &self.library.tree_path, +- self.config_server.read().get_library_scan_depth(), +- )); +- } +- + pub fn library_upper_dir(&self) -> Option { + self.library + .tree_path +@@ -249,12 +242,58 @@ impl Model { + .map(std::path::Path::to_path_buf) + } + +- pub fn library_dir_tree(p: &Path, depth: ScanDepth) -> Node { +- let name: String = match p.file_name() { ++ /// Execute [`Self::library_scan`] from a `&self` instance. ++ #[inline] ++ pub fn library_scan_dir>(&self, path: P, focus_node: Option) { ++ Self::library_scan( ++ self.tx_to_main.clone(), ++ self.download_tracker.clone(), ++ path, ++ self.config_server.read().get_library_scan_depth(), ++ focus_node, ++ ); ++ } ++ ++ pub fn loading_tree() -> Tree { ++ Tree::new(Node::new("/dev/null".to_string(), "Loading...".to_string())) ++ } ++ ++ /// Execute a library scan on a different thread. ++ /// ++ /// Executes [`Self::library_dir_tree`] on a different thread and send a [`LIMsg::TreeNodeReady`] on finish ++ pub fn library_scan>( ++ tx: Sender, ++ download_tracker: DownloadTracker, ++ path: P, ++ depth: ScanDepth, ++ focus_node: Option, ++ ) { ++ let path = path.into(); ++ std::thread::Builder::new() ++ .name("library tree scan".to_string()) ++ .spawn(move || { ++ download_tracker.increase_one(path.to_string_lossy()); ++ let root_node = Self::library_dir_tree(&path, depth); ++ ++ let _ = tx.send(Msg::Library(LIMsg::TreeNodeReady(root_node, focus_node))); ++ download_tracker.decrease_one(&path.to_string_lossy()); ++ }) ++ .expect("Failed to spawn thread"); ++ } ++ ++ /// Scan the given `path` for up to `depth`, and return a [`Node`] tree. ++ /// ++ /// Note: consider using [`Self::library_scan`] instead of this directly. ++ fn library_dir_tree(path: &Path, depth: ScanDepth) -> RecVec { ++ let name: String = match path.file_name() { + None => "/".to_string(), + Some(n) => n.to_string_lossy().into_owned(), + }; +- let mut node: Node = Node::new(p.to_string_lossy().into_owned(), name); ++ let mut node = RecVec { ++ id: path.to_path_buf(), ++ value: name, ++ children: Vec::new(), ++ }; + + let depth = match depth { + ScanDepth::Limited(v) => v, +@@ -262,8 +301,8 @@ impl Model { + ScanDepth::Unlimited => u32::MAX, + }; + +- if depth > 0 && p.is_dir() { +- if let Ok(paths) = std::fs::read_dir(p) { ++ if depth > 0 && path.is_dir() { ++ if let Ok(paths) = std::fs::read_dir(path) { + let mut paths: Vec<(String, PathBuf)> = paths + .filter_map(std::result::Result::ok) + .filter(|p| !p.file_name().to_string_lossy().starts_with('.')) +@@ -273,12 +312,14 @@ impl Model { + paths.sort_by(|a, b| alphanumeric_sort::compare_str(&a.0, &b.0)); + + for p in paths { +- node.add_child(Self::library_dir_tree(&p.1, ScanDepth::Limited(depth - 1))); ++ node.children ++ .push(Self::library_dir_tree(&p.1, ScanDepth::Limited(depth - 1))); + } + } + } + node + } ++ + pub fn library_dir_children(p: &Path) -> Vec { + let mut children: Vec = vec![]; + if p.is_dir() { +@@ -300,88 +341,69 @@ impl Model { + children + } + +- pub fn library_reload_with_node_focus(&mut self, node: Option<&str>) { ++ /// Reload the library with the given `node` as a focus, also starts a new database sync worker for the current path. ++ pub fn library_reload_with_node_focus(&mut self, node: Option) { + self.db.sync_database(self.library.tree_path.as_path()); + self.database_reload(); +- self.library_reload_tree(); +- if let Some(n) = node { +- assert!(self +- .app +- .attr( +- &Id::Library, +- Attribute::Custom(TREE_INITIAL_NODE), +- AttrValue::String(n.to_string()), +- ) +- .is_ok()); ++ self.library_scan_dir(&self.library.tree_path, node); ++ } ++ ++ /// Convert a [`RecVec`] to a [`Node`]. ++ fn recvec_to_node(vec: RecVec) -> Node { ++ let mut node = Node::new(vec.id.to_string_lossy().to_string(), vec.value); ++ ++ for val in vec.children { ++ node.add_child(Self::recvec_to_node(val)); + } ++ ++ node + } + +- pub fn library_reload_tree(&mut self) { +- self.library.tree = Tree::new(Self::library_dir_tree( +- self.library.tree_path.as_ref(), +- self.config_server.read().get_library_scan_depth(), +- )); +- let current_node = match self.app.state(&Id::Library).ok().unwrap() { ++ /// Apply the given [`RecVec`] as a tree ++ pub fn library_apply_as_tree( ++ &mut self, ++ msg: RecVec, ++ focus_node: Option, ++ ) { ++ let root_path = msg.id.clone(); ++ let root_node = Self::recvec_to_node(msg); ++ ++ let old_current_node = match self.app.state(&Id::Library).ok().unwrap() { + State::One(StateValue::String(id)) => Some(id), + _ => None, + }; +- let mut focus = false; +- +- if let Ok(f) = self.app.query(&Id::Library, Attribute::Focus) { +- if Some(AttrValue::Flag(true)) == f { +- focus = true; +- } +- } + +- if focus { +- self.app.umount(&Id::Library).ok(); +- assert!(self +- .app +- .mount( +- Id::Library, +- Box::new(MusicLibrary::new( +- &self.library.tree, +- current_node, +- self.config_tui.clone(), +- ),), +- Vec::new() +- ) +- .is_ok()); +- self.app.active(&Id::Library).ok(); +- return; ++ self.library.tree_path = root_path; ++ self.library.tree = Tree::new(root_node); ++ ++ // remount preserves focus ++ let _ = self.app.remount( ++ Id::Library, ++ Box::new(MusicLibrary::new( ++ &self.library.tree, ++ old_current_node, ++ self.config_tui.clone(), ++ )), ++ Vec::new(), ++ ); ++ ++ // focus the specified node ++ if let Some(id) = focus_node { ++ let _ = self.app.attr( ++ &Id::Library, ++ Attribute::Custom(TREE_INITIAL_NODE), ++ AttrValue::String(id), ++ ); + } +- +- assert!(self +- .app +- .remount( +- Id::Library, +- Box::new(MusicLibrary::new( +- &self.library.tree, +- current_node, +- self.config_tui.clone(), +- ),), +- Vec::new() +- ) +- .is_ok()); + } + +- // Kept for debugging focus issue +- // if let Ok(f) = self.app.query(&Id::Library, Attribute::Focus) { +- // if Some(AttrValue::Flag(true)) == f { +- // error!("focus after remount: true"); +- // } else { +- // error!("focus after remount: false"); +- // } +- // } + pub fn library_stepinto(&mut self, node_id: &str) { +- self.library_scan_dir(PathBuf::from(node_id)); +- self.library_reload_tree(); ++ self.library_scan_dir(PathBuf::from(node_id), None); + } + + pub fn library_stepout(&mut self) { + if let Some(p) = self.library_upper_dir() { +- self.library_scan_dir(p); +- self.library_reload_tree(); ++ self.library_scan_dir(p, None); + } + } + +@@ -396,6 +418,7 @@ impl Model { + } + } + ++ /// Delete the currently selected node from the filesystem and reload the tree and remove the deleted paths from the playlist. + pub fn library_delete_node(&mut self) -> Result<()> { + if let Ok(State::One(StateValue::String(node_id))) = self.app.state(&Id::Library) { + if let Some(mut route) = self.library.tree.root().route_by_node(&node_id) { +@@ -407,28 +430,27 @@ impl Model { + remove_dir_all(p)?; + } + +- // this is to keep the state of playlist +- self.library_reload_tree(); +- let tree = self.library.tree.clone(); +- if let Some(new_node) = tree.root().node_by_route(&route) { +- self.library_reload_with_node_focus(Some(new_node.id())); +- } else { +- //special case 1: old route not available but have siblings +- if let Some(last) = route.last_mut() { +- if last > &mut 0 { +- *last -= 1; +- } +- } +- if let Some(new_node) = tree.root().node_by_route(&route) { +- self.library_reload_with_node_focus(Some(new_node.id())); +- } else { +- //special case 2: old route not available and no siblings +- route.truncate(route.len() - 1); +- if let Some(new_node) = tree.root().node_by_route(&route) { +- self.library_reload_with_node_focus(Some(new_node.id())); ++ let mut tree = self.library.tree.clone(); ++ tree.root_mut().remove_child(&node_id); ++ let mut focus_node: Option = None; ++ // case 1: the route still exists due to having a sibling beyond the index which now takes the same index ++ if let Some(node) = tree.root().node_by_route(&route) { ++ focus_node = Some(node.id().to_string()); ++ } else if !route.is_empty() { ++ let _ = route.pop(); ++ // case 2: the route does not exist anymore, but there is a parent in the route ++ if let Some(parent) = tree.root().node_by_route(&route) { ++ // case 2.1: the parent has children, select the last of them ++ if let Some(last_child) = parent.children().last() { ++ focus_node = Some(last_child.id().to_string()); ++ } else { ++ // case 2.2: the parent exists, but has no children ++ focus_node = Some(parent.id().to_string()); + } + } + } ++ ++ self.library_scan_dir(&self.library.tree_path, focus_node); + } + // this line remove the deleted songs from playlist + self.playlist_update_library_delete(); +@@ -459,7 +481,7 @@ impl Model { + p_parent.join(pold_filename) + }; + rename(pold, new_node_id.as_path())?; +- self.library_reload_with_node_focus(new_node_id.to_str()); ++ self.library_reload_with_node_focus(Some(new_node_id.to_string_lossy().to_string())); + } + self.library.yanked_node_id = None; + self.playlist_update_library_delete(); +@@ -520,7 +542,7 @@ impl Model { + } + if let Some(dir) = vec.get(index) { + let pathbuf = PathBuf::from(dir); +- self.library_scan_dir(pathbuf); ++ self.library_scan_dir(pathbuf, None); + self.library_reload_with_node_focus(None); + } + } +diff --git a/tui/src/ui/components/playlist.rs b/tui/src/ui/components/playlist.rs +index 96254dd8b..a2a333b9e 100644 +--- a/tui/src/ui/components/playlist.rs ++++ b/tui/src/ui/components/playlist.rs +@@ -888,7 +888,7 @@ impl Model { + // TODO: move this to server? + self.playback.playlist.save_m3u(filename)?; + +- self.library_reload_with_node_focus(Some(&filename.to_string_lossy())); ++ self.library_reload_with_node_focus(Some(filename.to_string_lossy().to_string())); + + Ok(()) + } +diff --git a/tui/src/ui/components/tag_editor/update.rs b/tui/src/ui/components/tag_editor/update.rs +index ca268cacf..5b2554501 100644 +--- a/tui/src/ui/components/tag_editor/update.rs ++++ b/tui/src/ui/components/tag_editor/update.rs +@@ -34,7 +34,7 @@ impl Model { + TEMsg::TagEditorClose => { + if let Some(s) = self.tageditor_song.clone() { + // TODO: this should be re-done and take actual track ids themself, or at least verified to use the same functions to result in the same id +- self.library_reload_with_node_focus(Some(&s.path_as_id_str())); ++ self.library_reload_with_node_focus(Some(s.path_as_id_str().to_string())); + } + self.umount_tageditor(); + } +diff --git a/tui/src/ui/model/mod.rs b/tui/src/ui/model/mod.rs +index 8916513aa..c5515e81d 100644 +--- a/tui/src/ui/model/mod.rs ++++ b/tui/src/ui/model/mod.rs +@@ -35,7 +35,7 @@ use super::components::TETrack; + use super::tui_cmd::TuiCmd; + use crate::ui::Application; + use crate::CombinedSettings; +-use download_tracker::DownloadTracker; ++pub use download_tracker::DownloadTracker; + + mod download_tracker; + mod playlist; +@@ -362,10 +362,7 @@ impl Model { + } = config; + let path = Self::get_full_path_from_config(&config_server.read()); + // TODO: refactor music library tree to be Paths instead? +- let tree = Tree::new(Self::library_dir_tree( +- &path, +- config_server.read().get_library_scan_depth(), +- )); ++ let tree = Self::loading_tree(); + + let viuer_supported = if config_tui.read().cover_features_enabled() { + get_viuer_support() +@@ -412,6 +409,16 @@ impl Model { + let ce_theme = config_tui.read().settings.theme.clone(); + let xywh = xywh::Xywh::from(&config_tui.read().settings.coverart); + ++ let download_tracker = DownloadTracker::default(); ++ ++ Self::library_scan( ++ tx_to_main.clone(), ++ download_tracker.clone(), ++ &path, ++ config_server.read().get_library_scan_depth(), ++ None, ++ ); ++ + Self { + app, + quit: false, +@@ -460,7 +467,7 @@ impl Model { + taskpool, + tx_to_main, + rx_to_main, +- download_tracker: DownloadTracker::default(), ++ download_tracker, + current_track_lyric: None, + playback: Playback::new(), + cmd_to_server_tx, +diff --git a/tui/src/ui/model/update.rs b/tui/src/ui/model/update.rs +index 7e9111ec5..066f56f8f 100644 +--- a/tui/src/ui/model/update.rs ++++ b/tui/src/ui/model/update.rs +@@ -57,7 +57,7 @@ impl Update for Model { + None + } + Msg::Library(m) => { +- self.update_library(&m); ++ self.update_library(m); + None + } + Msg::GeneralSearch(m) => { +@@ -110,7 +110,7 @@ impl Update for Model { + + Msg::Podcast(m) => self.update_podcast(m), + Msg::LyricMessage(m) => self.update_lyric_textarea(m), +- Msg::Download(m) => self.update_download_msg(&m), ++ Msg::Download(m) => self.update_download_msg(m), + Msg::Xywh(m) => self.update_xywh_msg(m), + + Msg::ForceRedraw => None, +@@ -558,13 +558,13 @@ impl Model { + None + } + +- fn update_library(&mut self, msg: &LIMsg) { ++ fn update_library(&mut self, msg: LIMsg) { + match msg { + LIMsg::TreeBlur => { + assert!(self.app.active(&Id::Playlist).is_ok()); + } + LIMsg::TreeStepInto(path) => { +- self.library_stepinto(path); ++ self.library_stepinto(&path); + } + LIMsg::TreeStepOut => { + self.library_stepout(); +@@ -588,6 +588,9 @@ impl Model { + self.mount_error_popup(e.context("library remove root")); + } + } ++ LIMsg::TreeNodeReady(vec, focus_node) => { ++ self.library_apply_as_tree(vec, focus_node); ++ } + } + } + +@@ -916,20 +919,20 @@ impl Model { + } + + // change status bar text to indicate the downloading state +- fn update_download_msg(&mut self, msg: &DLMsg) -> Option { ++ fn update_download_msg(&mut self, msg: DLMsg) -> Option { + self.redraw = true; + match msg { + DLMsg::DownloadRunning(url, title) => { +- self.download_tracker.increase_one(&**url); ++ self.download_tracker.increase_one(&*url); + self.show_message_timeout_label_help( +- self.download_tracker.message_download_start(title), ++ self.download_tracker.message_download_start(&title), + None, + None, + None, + ); + } + DLMsg::DownloadSuccess(url) => { +- self.download_tracker.decrease_one(url); ++ self.download_tracker.decrease_one(&url); + self.show_message_timeout_label_help( + self.download_tracker.message_download_complete(), + None, +@@ -945,13 +948,14 @@ impl Model { + if self.download_tracker.visible() { + return None; + } +- self.library_reload_with_node_focus(file.as_deref()); ++ self.library_reload_with_node_focus(file); + } + DLMsg::DownloadErrDownload(url, title, error_message) => { +- self.download_tracker.decrease_one(url); ++ self.download_tracker.decrease_one(&url); + self.mount_error_popup(anyhow!("download failed: {error_message}")); + self.show_message_timeout_label_help( +- self.download_tracker.message_download_error_response(title), ++ self.download_tracker ++ .message_download_error_response(&title), + None, + None, + None, +@@ -961,17 +965,17 @@ impl Model { + self.mount_error_popup(anyhow!("download ok but tag info is not complete.")); + self.show_message_timeout_label_help( + self.download_tracker +- .message_download_error_embed_data(title), ++ .message_download_error_embed_data(&title), + None, + None, + None, + ); + } + DLMsg::MessageShow((title, text)) => { +- self.mount_message(title, text); ++ self.mount_message(&title, &text); + } + DLMsg::MessageHide((title, text)) => { +- self.umount_message(title, text); ++ self.umount_message(&title, &text); + } + DLMsg::FetchPhotoSuccess(image_wrapper) => { + self.show_image(&image_wrapper.data).ok(); + +From 8b1fd1216fb81bfa982958086832c10b52d6e494 Mon Sep 17 00:00:00 2001 +From: hasezoey +Date: Wed, 28 May 2025 16:25:41 +0200 +Subject: [PATCH 2/3] fix(tui::components::music_library::library_stepout): + focus the last root node in the new tree + +Instead of always focusing the root node of the new tree. +--- + CHANGELOG.md | 1 + + tui/src/ui/components/music_library.rs | 5 ++++- + 2 files changed, 5 insertions(+), 1 deletion(-) + +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 0641e5f29..3e05516e9 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -22,6 +22,7 @@ + - Fix(tui): re-select the (approximately) same playlist item after a shuffle. + - Fix(tui): escape key will now no longer also act as a quit key. + - Fix(tui): changed the music library tree loading to be async. ++- Fix(tui): in the music library tree, when stepping out now the correct node if focused instead of the root node. + - Fix(server): with `rusty-soundtouch` on `rusty` backend, dont take initial samples until necessary. + - Fix(server): on rusty backend, always decode and use `f32` samples. (instead of `i16`) + - Fix(server): on rusty backend, update `soundtouch` version to fix build issues on latest arch & gcc 15. +diff --git a/tui/src/ui/components/music_library.rs b/tui/src/ui/components/music_library.rs +index 6a4d65e5c..498a29ffb 100644 +--- a/tui/src/ui/components/music_library.rs ++++ b/tui/src/ui/components/music_library.rs +@@ -397,13 +397,16 @@ impl Model { + } + } + ++ /// Handle stepping into a node on the tree + pub fn library_stepinto(&mut self, node_id: &str) { + self.library_scan_dir(PathBuf::from(node_id), None); + } + ++ /// Handle stepping out of the current root node on the tree + pub fn library_stepout(&mut self) { + if let Some(p) = self.library_upper_dir() { +- self.library_scan_dir(p, None); ++ let focus_node = Some(self.library.tree_path.to_string_lossy().to_string()); ++ self.library_scan_dir(p, focus_node); + } + } + + +From 47a13fc83e3c353a516d83e892ea87c7e26ed430 Mon Sep 17 00:00:00 2001 +From: hasezoey +Date: Wed, 28 May 2025 16:32:42 +0200 +Subject: [PATCH 3/3] style(tui::components::music_library): add a todo to try + to load the directory if not loaded yet + +--- + tui/src/ui/components/music_library.rs | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/tui/src/ui/components/music_library.rs b/tui/src/ui/components/music_library.rs +index 498a29ffb..d98cd6c09 100644 +--- a/tui/src/ui/components/music_library.rs ++++ b/tui/src/ui/components/music_library.rs +@@ -90,6 +90,7 @@ impl MusicLibrary { + let current_node = self.component.tree_state().selected().unwrap(); + let p: &Path = Path::new(current_node); + if p.is_dir() { ++ // TODO: try to load the directory if it is not loaded yet. + (self.perform(Cmd::Custom(TREE_CMD_OPEN)), None) + } else { + (