From b3b7f0f65295f2100eb3c9880e870349c43701a6 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 9 May 2026 15:14:33 +0200 Subject: Major rewrite: play command, queue display, and shuffle modes - Play command: no args queues to end (play only if queue empty + player playing), -i inserts after current and skips, -n inserts after current, removed -l flag - Queue display: detect terminal size, use pager only if queue exceeds terminal rows - Shuffle modes: always remember original queue, restore on 'shuffle none', album shuffle shuffles within each album's range separately --- Cargo.lock | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 1 + src/cli.rs | 11 +++--- src/daemon.rs | 14 +++++--- src/queue.rs | 85 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 200 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5893217..52ab8c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -177,7 +177,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -203,7 +203,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -1327,6 +1327,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1707,7 +1713,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -1989,6 +1995,7 @@ dependencies = [ "serde_json", "symphonia", "tempfile", + "terminal_size", "tokio", "toml", ] @@ -2033,6 +2040,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2042,7 +2062,7 @@ dependencies = [ "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -2595,10 +2615,20 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix 0.38.44", + "windows-sys 0.48.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3308,6 +3338,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3350,6 +3389,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3416,6 +3470,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3434,6 +3494,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3452,6 +3518,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3482,6 +3554,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3500,6 +3578,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3518,6 +3602,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3536,6 +3626,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3702,7 +3798,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix", + "rustix 1.1.4", "serde", "serde_repr", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 3482486..0fe9f43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ libc = "0.2.186" ctrlc = "3.5.2" rand = "0.10.1" notify-rust = "4.17.0" +terminal_size = "0.3" [dev-dependencies] tempfile = "3.17" diff --git a/src/cli.rs b/src/cli.rs index 0da3078..bd81983 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,15 +21,12 @@ pub enum Commands { Restart, /// Play a track, album, or artist Play { - /// Play immediately (add after current and skip to it) - #[arg(short = 'i', long = "now", group = "position")] + /// Add after current track and skip to it + #[arg(short = 'i', long = "now")] now: bool, /// Add to queue after current track (don't skip) - #[arg(short = 'n', long = "next", group = "position")] + #[arg(short = 'n', long = "next")] next: bool, - /// Add to end of queue (default) - #[arg(short = 'l', long = "last", group = "position")] - last: bool, #[arg(value_name = "QUERY")] query: Option, }, @@ -126,7 +123,7 @@ impl Cli { }; crate::daemon::send_command(format!("play {} {}", pos, q))?; } else { - crate::daemon::send_command("play".to_string())?; + crate::daemon::send_command("play last".to_string())?; } } Commands::Pause => { diff --git a/src/daemon.rs b/src/daemon.rs index e88669a..f4a72b7 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -399,9 +399,9 @@ fn process_command( format!("Queued next: {}\n", path_str) } _ => { - let was_playing = audio.is_playing(); + let was_empty = q.is_empty(); q.add_track(path.clone()); - if !was_playing { + if was_empty && audio.is_playing() { let track = Track::from_path(path.clone()); audio.play(path); notification::notify_track(&track); @@ -452,7 +452,8 @@ fn process_command( for file in &files { q.add_track(file.clone()); } - if !audio.is_playing() + if was_empty + && audio.is_playing() && let Some(track) = q.current() { audio.play(track.path.clone()); @@ -519,8 +520,11 @@ fn process_command( } } } - // Use pager if output is long (more than 24 lines) - if output.lines().count() > 24 { + // Use pager if queue is longer than terminal height + let terminal_rows = terminal_size::terminal_size() + .map(|(w, _)| w.0 as usize) + .unwrap_or(24); + if output.lines().count() > terminal_rows { use std::process::{Command, Stdio}; let pager = Command::new("less").arg("-R").stdin(Stdio::piped()).spawn(); if let Ok(mut child) = pager { diff --git a/src/queue.rs b/src/queue.rs index 9e8e1af..109b748 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -124,6 +124,7 @@ pub struct Queue { position: usize, shuffle: ShuffleMode, shuffled_indices: Vec, + original_tracks: Vec, } impl Default for Queue { @@ -139,6 +140,7 @@ impl Queue { position: 0, shuffle: ShuffleMode::None, shuffled_indices: Vec::new(), + original_tracks: Vec::new(), } } @@ -162,8 +164,38 @@ impl Queue { } pub fn set_shuffle(&mut self, mode: ShuffleMode) { - self.shuffle = mode; - self.update_shuffle_indices(); + match mode { + ShuffleMode::None => { + self.shuffle = ShuffleMode::None; + self.restore_from_original(); + } + ShuffleMode::All => { + self.original_tracks = self.tracks.clone(); + self.shuffle = ShuffleMode::All; + self.update_shuffle_indices(); + } + ShuffleMode::Album => { + self.original_tracks = self.tracks.clone(); + self.shuffle = ShuffleMode::Album; + self.update_shuffle_indices_album(); + } + } + } + + fn restore_from_original(&mut self) { + if self.original_tracks.is_empty() { + self.shuffled_indices.clear(); + return; + } + + let current_pos = self.position; + let played: Vec = self.tracks[..current_pos].to_vec(); + let unplayed_original: Vec = self.original_tracks[current_pos..].to_vec(); + + self.tracks = played; + self.tracks.extend(unplayed_original); + self.position = current_pos; + self.shuffled_indices.clear(); } fn update_shuffle_indices(&mut self) { @@ -180,6 +212,55 @@ impl Queue { } } + fn update_shuffle_indices_album(&mut self) { + let len = self.tracks.len(); + self.shuffled_indices = (0..len).collect(); + + let mut rng = rand::rng(); + let pos = self.position.min(len.saturating_sub(1)); + + if pos == 0 && len > 1 { + self.shuffle_album_range(0..len, &mut rng); + } else if pos + 1 < len { + self.shuffle_album_range(pos + 1..len, &mut rng); + } + } + + fn shuffle_album_range(&mut self, range: std::ops::Range, rng: &mut impl rand::Rng) { + if range.start >= range.end { + return; + } + + let mut album_ranges: Vec<(usize, usize)> = Vec::new(); + let mut current_album: Option<&str> = None; + let mut range_start: Option = None; + + for i in range.clone() { + let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); + match (current_album, range_start) { + (None, None) => { + current_album = album; + range_start = Some(i); + } + (Some(_), Some(start)) if album != current_album => { + album_ranges.push((start, i)); + current_album = album; + range_start = Some(i); + } + _ => {} + } + } + if let Some(start) = range_start { + album_ranges.push((start, range.end)); + } + + for (start, end) in album_ranges { + if start + 1 < end { + self.shuffled_indices[start..end].shuffle(rng); + } + } + } + pub fn current(&self) -> Option<&Track> { if self.tracks.is_empty() { None -- cgit v1.2.3