From 05d6aa8f4dbf950845341cb5e8aa5b58d61061ce Mon Sep 17 00:00:00 2001 From: Isaiah Linares Date: Fri, 30 Oct 2026 18:08:20 -0400 Subject: [PATCH 1/5] test(create-playlist): add failing cursor-overflow tests for #829 --- src/tui/ui/create_playlist.rs | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/tui/ui/create_playlist.rs b/src/tui/ui/create_playlist.rs index eff60595..83279964 100644 --- a/src/tui/ui/create_playlist.rs +++ b/src/tui/ui/create_playlist.rs @@ -260,4 +260,43 @@ mod tests { assert!(title.starts_with(expected), "{title}"); } } + + #[test] + fn long_playlist_name_keeps_the_cursor_inside_the_input_box() { + let mut app = App::default(); + app.view.create_playlist_stage = CreatePlaylistStage::Name; + // 200 chars is far wider than the box; cursor is a display-width count + app.view.create_playlist_name = "a".repeat(200).chars().collect(); + app.view.create_playlist_name_cursor = 200; + + let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap(); + terminal + .draw(|f| draw_create_playlist_form(f, &app)) + .unwrap(); + let cursor = terminal.get_cursor_position().unwrap(); + + // 100x30 screen: popup is 80 wide at x=10; margin 2 puts the input box at + // x=12, width 76, so its right border is at x=87. + assert!(cursor.x < 87, "cursor escaped the box: x={}", cursor.x); + assert!(cursor.x > 12, "cursor is on the left border: x={}", cursor.x); + } + + #[test] + fn long_search_query_keeps_the_cursor_inside_the_search_box() { + let mut app = App::default(); + app.view.create_playlist_stage = CreatePlaylistStage::AddTracks; + app.view.create_playlist_focus = CreatePlaylistFocus::SearchInput; + app.view.create_playlist_search_input = "a".repeat(200).chars().collect(); + app.view.create_playlist_search_cursor = 200; + + let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap(); + terminal + .draw(|f| draw_create_playlist_form(f, &app)) + .unwrap(); + let cursor = terminal.get_cursor_position().unwrap(); + + // Margin is 1 here: box at x=11, width 78, right border at x=88. + assert!(cursor.x < 88, "cursor escaped the box: x={}", cursor.x); + assert!(cursor.x > 11, "cursor is on the left border: x={}", cursor.x); + } } From cb5bc61c3bab256fc0b34e17130bce783a727c77 Mon Sep 17 00:00:00 2001 From: Isaiah Linares Date: Fri, 30 Oct 2026 18:24:34 -0400 Subject: [PATCH 2/5] feat(input_scroll): add helper function for input box width and cursor clamp with accompanying tests --- src/tui/ui/create_playlist.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/tui/ui/create_playlist.rs b/src/tui/ui/create_playlist.rs index 83279964..d325e800 100644 --- a/src/tui/ui/create_playlist.rs +++ b/src/tui/ui/create_playlist.rs @@ -16,6 +16,18 @@ fn centered_rect(bounds: Rect, width_pct: u16, height_pct: u16) -> Rect { Rect::new(x, y, width, height) } +// Columns to scroll a bordered single-line input so the cursor stays inside it. +// Only used by tests until the draw functions call it. +#[allow(dead_code)] +fn input_scroll(cursor: u16, box_width: u16) -> u16 { + let inner_width = box_width.saturating_sub(2); + if inner_width > 0 && cursor >= inner_width { + cursor - inner_width + 1 + } else { + 0 + } +} + pub fn draw_create_playlist_form(f: &mut Frame<'_>, app: &App) { let area = centered_rect(f.area(), 80, 80); f.render_widget(Clear, area); @@ -261,6 +273,20 @@ mod tests { } } + #[test] + fn input_scroll_is_zero_while_the_cursor_fits() { + assert_eq!(input_scroll(5, 20), 0); + // A zero-width box must not underflow or scroll + assert_eq!(input_scroll(5, 0), 0); + } + + #[test] + fn input_scroll_keeps_the_cursor_inside_the_box() { + // box 20 wide -> 18 usable columns (2 are the borders) + assert_eq!(input_scroll(18, 20), 1); + assert_eq!(input_scroll(40, 20), 23); + } + #[test] fn long_playlist_name_keeps_the_cursor_inside_the_input_box() { let mut app = App::default(); From ea3e7d413660af5b0188a55b05fe951cf26b9be8 Mon Sep 17 00:00:00 2001 From: Isaiah Linares Date: Fri, 30 Oct 2026 18:30:10 -0400 Subject: [PATCH 3/5] fix: create playlist name and search scroll with cursor clamp (#829) --- src/tui/ui/create_playlist.rs | 54 ++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/tui/ui/create_playlist.rs b/src/tui/ui/create_playlist.rs index d325e800..93816e50 100644 --- a/src/tui/ui/create_playlist.rs +++ b/src/tui/ui/create_playlist.rs @@ -17,8 +17,6 @@ fn centered_rect(bounds: Rect, width_pct: u16, height_pct: u16) -> Rect { } // Columns to scroll a bordered single-line input so the cursor stays inside it. -// Only used by tests until the draw functions call it. -#[allow(dead_code)] fn input_scroll(cursor: u16, box_width: u16) -> u16 { let inner_width = box_width.saturating_sub(2); if inner_width > 0 && cursor >= inner_width { @@ -67,14 +65,18 @@ fn draw_name_stage(f: &mut Frame<'_>, app: &App, area: Rect) { f.render_widget(label, inner[0]); let name_text: String = app.view.create_playlist_name.iter().collect(); - let input = Paragraph::new(name_text).style(theme.base_style()).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(theme.active.into())), - ); + let offset = input_scroll(app.view.create_playlist_name_cursor, inner[1].width); + let input = Paragraph::new(name_text) + .style(theme.base_style()) + .scroll((0, offset)) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(theme.active.into())), + ); f.render_widget(input, inner[1]); f.set_cursor_position(( - inner[1].x + 1 + app.view.create_playlist_name_cursor, + inner[1].x + 1 + app.view.create_playlist_name_cursor - offset, inner[1].y + 1, )); @@ -113,19 +115,23 @@ fn draw_add_tracks_stage(f: &mut Frame<'_>, app: &App, area: Rect) { } else { Style::default().fg(theme.inactive.into()) }; - let search_input = Paragraph::new(search_text).style(theme.base_style()).block( - Block::default() - .title(Span::styled( - app.strings().popups.create_search_title, - Style::default().fg(theme.header.into()), - )) - .borders(Borders::ALL) - .border_style(search_border_style), - ); + let offset = input_scroll(app.view.create_playlist_search_cursor, inner[0].width); + let search_input = Paragraph::new(search_text) + .style(theme.base_style()) + .scroll((0, offset)) + .block( + Block::default() + .title(Span::styled( + app.strings().popups.create_search_title, + Style::default().fg(theme.header.into()), + )) + .borders(Borders::ALL) + .border_style(search_border_style), + ); f.render_widget(search_input, inner[0]); if app.view.create_playlist_focus == CreatePlaylistFocus::SearchInput { f.set_cursor_position(( - inner[0].x + 1 + app.view.create_playlist_search_cursor, + inner[0].x + 1 + app.view.create_playlist_search_cursor - offset, inner[0].y + 1, )); } @@ -304,7 +310,11 @@ mod tests { // 100x30 screen: popup is 80 wide at x=10; margin 2 puts the input box at // x=12, width 76, so its right border is at x=87. assert!(cursor.x < 87, "cursor escaped the box: x={}", cursor.x); - assert!(cursor.x > 12, "cursor is on the left border: x={}", cursor.x); + assert!( + cursor.x > 12, + "cursor is on the left border: x={}", + cursor.x + ); } #[test] @@ -323,6 +333,10 @@ mod tests { // Margin is 1 here: box at x=11, width 78, right border at x=88. assert!(cursor.x < 88, "cursor escaped the box: x={}", cursor.x); - assert!(cursor.x > 11, "cursor is on the left border: x={}", cursor.x); + assert!( + cursor.x > 11, + "cursor is on the left border: x={}", + cursor.x + ); } } From 7f08034617f66bec89120ba8e920b8ab853cd21f Mon Sep 17 00:00:00 2001 From: Isaiah Linares Date: Fri, 30 Oct 2026 18:38:26 -0400 Subject: [PATCH 4/5] docs: update changelog with fixed #829 description --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 119ef8df..5e98487e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **Smoother scrolling in terminal Settings**: Fixed an issue where navigating the Settings list caused the screen to jump erratically. The view now stays stable, scrolling only when your cursor reaches the top or bottom of the box. - **The AI DJ prompt's caret follows the cursor**: after Left, Home or a Backspace in the middle of the text, the caret stayed painted at the end of the prompt. It now sits where the next character goes ([#945](https://github.com/LargeModGames/spotatui/issues/945)). - **The log's startup line lists every compiled feature**: `features:` left out `soloist`, `gui`, `apple-music`, `spotify-session`, the playback backends and the shared decode features, so a release build with Soloist looked like it had none. A test now fails when a feature in `Cargo.toml` is missing from the list ([#949](https://github.com/LargeModGames/spotatui/issues/949)). +- **Long names and queries stay inside the Create Playlist inputs**: typing a playlist name or an Add Tracks search longer than the box ran the text past the right border and carried the cursor out of it. The text now scrolls and the cursor stays at the right edge ([#829](https://github.com/LargeModGames/spotatui/issues/829)). ## [v0.47.0] 2026-10-25 From 5ffc79748bcdebc3c5f1f54c67619820dfcf542c Mon Sep 17 00:00:00 2001 From: Isaiah Linares Date: Tue, 3 Nov 2026 14:32:46 -0500 Subject: [PATCH 5/5] test(create-playlist): pin cursor render tests to exact x values --- src/tui/ui/create_playlist.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/tui/ui/create_playlist.rs b/src/tui/ui/create_playlist.rs index 93816e50..006c4b24 100644 --- a/src/tui/ui/create_playlist.rs +++ b/src/tui/ui/create_playlist.rs @@ -308,13 +308,9 @@ mod tests { let cursor = terminal.get_cursor_position().unwrap(); // 100x30 screen: popup is 80 wide at x=10; margin 2 puts the input box at - // x=12, width 76, so its right border is at x=87. - assert!(cursor.x < 87, "cursor escaped the box: x={}", cursor.x); - assert!( - cursor.x > 12, - "cursor is on the left border: x={}", - cursor.x - ); + // x=12, width 76 (74 usable). offset = 200 - 74 + 1 = 127; cursor x = + // 12 + 1 + 200 - 127 = 86, one column left of the right border at x=87. + assert_eq!(cursor.x, 86, "cursor x was {}", cursor.x); } #[test] @@ -331,12 +327,9 @@ mod tests { .unwrap(); let cursor = terminal.get_cursor_position().unwrap(); - // Margin is 1 here: box at x=11, width 78, right border at x=88. - assert!(cursor.x < 88, "cursor escaped the box: x={}", cursor.x); - assert!( - cursor.x > 11, - "cursor is on the left border: x={}", - cursor.x - ); + // Margin is 1 here: box at x=11, width 78 (76 usable). offset = + // 200 - 76 + 1 = 125; cursor x = 11 + 1 + 200 - 125 = 87, one column + // left of the right border at x=88. + assert_eq!(cursor.x, 87, "cursor x was {}", cursor.x); } }