From 88068a7d03e6d9d32a3f9b762124aa9ee9992772 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 13 Jun 2026 23:22:26 -0400 Subject: [PATCH 1/6] add move_* as configurable key bindings --- src/core/app.rs | 52 +++++++++++++++++++++++++++++++++++++++++ src/core/user_config.rs | 34 +++++++++++++++++++++------ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/core/app.rs b/src/core/app.rs index b05f928..9061b06 100644 --- a/src/core/app.rs +++ b/src/core/app.rs @@ -3531,6 +3531,30 @@ impl App { description: "Go back / quit".to_string(), value: SettingValue::Key(key_to_string(&self.user_config.keys.back)), }, + SettingItem { + id: "keys.move_up".to_string(), + name: "Move Up".to_string(), + description: "Move selection up".to_string(), + value: SettingValue::Key(key_to_string(&self.user_config.keys.move_up)), + }, + SettingItem { + id: "keys.move_down".to_string(), + name: "Move Down".to_string(), + description: "Move selection down".to_string(), + value: SettingValue::Key(key_to_string(&self.user_config.keys.move_down)), + }, + SettingItem { + id: "keys.move_left".to_string(), + name: "Move Left".to_string(), + description: "Move selection left".to_string(), + value: SettingValue::Key(key_to_string(&self.user_config.keys.move_left)), + }, + SettingItem { + id: "keys.move_right".to_string(), + name: "Move Right".to_string(), + description: "Move selection right".to_string(), + value: SettingValue::Key(key_to_string(&self.user_config.keys.move_right)), + }, SettingItem { id: "keys.next_page".to_string(), name: "Next Page".to_string(), @@ -3985,6 +4009,34 @@ impl App { } } } + "keys.move_up" => { + if let SettingValue::Key(v) = &setting.value { + if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { + self.user_config.keys.move_up = key; + } + } + } + "keys.move_down" => { + if let SettingValue::Key(v) = &setting.value { + if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { + self.user_config.keys.move_down = key; + } + } + } + "keys.move_left" => { + if let SettingValue::Key(v) = &setting.value { + if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { + self.user_config.keys.move_left = key; + } + } + } + "keys.move_right" => { + if let SettingValue::Key(v) = &setting.value { + if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { + self.user_config.keys.move_right = key; + } + } + } "keys.next_page" => { if let SettingValue::Key(v) = &setting.value { if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { diff --git a/src/core/user_config.rs b/src/core/user_config.rs index d7a9354..5d63c54 100644 --- a/src/core/user_config.rs +++ b/src/core/user_config.rs @@ -599,10 +599,6 @@ pub fn parse_key_public(key: String) -> Result { fn check_reserved_keys(key: Key) -> Result<()> { let reserved = [ - Key::Char('h'), - Key::Char('j'), - Key::Char('k'), - Key::Char('l'), Key::Char('H'), Key::Char('M'), Key::Char('L'), @@ -638,6 +634,10 @@ pub struct UserConfigPaths { #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct KeyBindingsString { back: Option, + move_up: Option, + move_down: Option, + move_left: Option, + move_right: Option, next_page: Option, previous_page: Option, jump_to_start: Option, @@ -678,6 +678,10 @@ pub struct KeyBindingsString { #[derive(Clone)] pub struct KeyBindings { pub back: Key, + pub move_up: Key, + pub move_down: Key, + pub move_left: Key, + pub move_right: Key, pub next_page: Key, pub previous_page: Key, pub jump_to_start: Key, @@ -845,6 +849,10 @@ impl UserConfig { custom_theme: Default::default(), keys: KeyBindings { back: Key::Char('q'), + move_up: Key::Char('k'), + move_down: Key::Char('j'), + move_left: Key::Char('h'), + move_right: Key::Char('l'), next_page: Key::Ctrl('d'), previous_page: Key::Ctrl('u'), jump_to_start: Key::Ctrl('a'), @@ -971,6 +979,10 @@ impl UserConfig { } to_keys!(back); + to_keys!(move_up); + to_keys!(move_down); + to_keys!(move_left); + to_keys!(move_right); to_keys!(next_page); to_keys!(previous_page); to_keys!(jump_to_start); @@ -1283,6 +1295,10 @@ impl UserConfig { let k = &self.keys; vec![ k.back, + k.move_up, + k.move_down, + k.move_left, + k.move_right, k.next_page, k.previous_page, k.jump_to_start, @@ -1478,6 +1494,10 @@ impl UserConfig { // Helper to build keybindings config from current values let build_keybindings = || KeyBindingsString { back: Some(key_to_config_string(self.keys.back)), + move_up: Some(key_to_config_string(self.keys.move_up)), + move_down: Some(key_to_config_string(self.keys.move_down)), + move_left: Some(key_to_config_string(self.keys.move_left)), + move_right: Some(key_to_config_string(self.keys.move_right)), next_page: Some(key_to_config_string(self.keys.next_page)), previous_page: Some(key_to_config_string(self.keys.previous_page)), jump_to_start: Some(key_to_config_string(self.keys.jump_to_start)), @@ -1901,10 +1921,10 @@ mod tests { let mut config = UserConfig::new(); let mut entries = HashMap::new(); - // 'j' is a reserved key - entries.insert("go_down".to_string(), "j".to_string()); + // Enter is a reserved key + entries.insert("submit_action".to_string(), "enter".to_string()); config.load_plugin_commands(entries); - assert!(!config.plugin_command_keys.contains_key(&Key::Char('j'))); + assert!(!config.plugin_command_keys.contains_key(&Key::Enter)); } #[test] From d7ac2bf5d738fe51d7a607830ee1cbe51959bf82 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 13 Jun 2026 23:24:08 -0400 Subject: [PATCH 2/6] update common_key_events to stop using hard coded keys and refer to user keybindings instead --- src/tui/handlers/common_key_events.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tui/handlers/common_key_events.rs b/src/tui/handlers/common_key_events.rs index 46d32d5..1df4a1a 100644 --- a/src/tui/handlers/common_key_events.rs +++ b/src/tui/handlers/common_key_events.rs @@ -1,20 +1,21 @@ use crate::core::app::{ActiveBlock, App, RouteId}; +use crate::core::user_config::KeyBindings; use crate::tui::event::Key; -pub fn down_event(key: Key) -> bool { - matches!(key, Key::Down | Key::Char('j') | Key::Ctrl('n')) +pub fn down_event(key: Key, keys: &KeyBindings) -> bool { + matches!(key, Key::Down | Key::Ctrl('n')) || key == keys.move_down } -pub fn up_event(key: Key) -> bool { - matches!(key, Key::Up | Key::Char('k') | Key::Ctrl('p')) +pub fn up_event(key: Key, keys: &KeyBindings) -> bool { + matches!(key, Key::Up | Key::Ctrl('p')) || key == keys.move_up } -pub fn left_event(key: Key) -> bool { - matches!(key, Key::Left | Key::Char('h') | Key::Ctrl('b')) +pub fn left_event(key: Key, keys: &KeyBindings) -> bool { + matches!(key, Key::Left | Key::Ctrl('b')) || key == keys.move_left } -pub fn right_event(key: Key) -> bool { - matches!(key, Key::Right | Key::Char('l') | Key::Ctrl('f')) +pub fn right_event(key: Key, keys: &KeyBindings) -> bool { + matches!(key, Key::Right | Key::Ctrl('f')) || key == keys.move_right } pub fn high_event(key: Key) -> bool { From 294b7ceebc29e6481080eaa39c212477b7977be5 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 13 Jun 2026 23:27:28 -0400 Subject: [PATCH 3/6] update callsites due to changes from common_key_events --- src/tui/handlers/album_list.rs | 8 ++- src/tui/handlers/album_tracks.rs | 8 ++- src/tui/handlers/artist.rs | 8 +-- src/tui/handlers/artist_albums.rs | 6 +- src/tui/handlers/artists.rs | 8 ++- src/tui/handlers/dialog.rs | 8 +-- src/tui/handlers/discover.rs | 12 ++-- src/tui/handlers/empty.rs | 90 ++++++++++++++++------------- src/tui/handlers/episode_table.rs | 8 ++- src/tui/handlers/friends.rs | 10 ++-- src/tui/handlers/help_menu.rs | 4 +- src/tui/handlers/home.rs | 8 ++- src/tui/handlers/library.rs | 8 ++- src/tui/handlers/mod.rs | 4 +- src/tui/handlers/playbar.rs | 2 +- src/tui/handlers/playlist.rs | 8 ++- src/tui/handlers/podcasts.rs | 8 ++- src/tui/handlers/queue_menu.rs | 4 +- src/tui/handlers/recently_played.rs | 8 ++- src/tui/handlers/search_results.rs | 8 +-- src/tui/handlers/select_device.rs | 4 +- src/tui/handlers/settings.rs | 20 +++---- src/tui/handlers/sort_menu.rs | 5 +- src/tui/handlers/track_table.rs | 8 ++- 24 files changed, 150 insertions(+), 115 deletions(-) diff --git a/src/tui/handlers/album_list.rs b/src/tui/handlers/album_list.rs index 91c538c..ebc48bb 100644 --- a/src/tui/handlers/album_list.rs +++ b/src/tui/handlers/album_list.rs @@ -6,15 +6,17 @@ use crate::{ pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(albums) = &mut app.library.saved_albums.get_results(None) { let next_index = common_key_events::on_down_press_handler(&albums.items, Some(app.album_list_index)); app.album_list_index = next_index; } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(albums) = &mut app.library.saved_albums.get_results(None) { let next_index = common_key_events::on_up_press_handler(&albums.items, Some(app.album_list_index)); diff --git a/src/tui/handlers/album_tracks.rs b/src/tui/handlers/album_tracks.rs index fa8e9f3..219fec2 100644 --- a/src/tui/handlers/album_tracks.rs +++ b/src/tui/handlers/album_tracks.rs @@ -9,8 +9,10 @@ use rspotify::{ pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => match app.album_table_context { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => match app.album_table_context { AlbumTableContext::Full => { if let Some(selected_album) = &app.selected_album_full { let next_index = common_key_events::on_down_press_handler( @@ -30,7 +32,7 @@ pub fn handler(key: Key, app: &mut App) { } } }, - k if common_key_events::up_event(k) => match app.album_table_context { + k if common_key_events::up_event(k, &app.user_config.keys) => match app.album_table_context { AlbumTableContext::Full => { if let Some(selected_album) = &app.selected_album_full { let next_index = common_key_events::on_up_press_handler( diff --git a/src/tui/handlers/artist.rs b/src/tui/handlers/artist.rs index 8f7148a..146e912 100644 --- a/src/tui/handlers/artist.rs +++ b/src/tui/handlers/artist.rs @@ -248,21 +248,21 @@ pub fn handler(key: Key, app: &mut App) { Key::Esc => { artist.artist_selected_block = ArtistBlock::Empty; } - k if common_key_events::down_event(k) => { + k if common_key_events::down_event(k, &app.user_config.keys) => { if artist.artist_selected_block != ArtistBlock::Empty { handle_down_press_on_selected_block(app); } else { handle_down_press_on_hovered_block(app); } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if artist.artist_selected_block != ArtistBlock::Empty { handle_up_press_on_selected_block(app); } else { handle_up_press_on_hovered_block(app); } } - k if common_key_events::left_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { artist.artist_selected_block = ArtistBlock::Empty; match artist.artist_hovered_block { ArtistBlock::TopTracks => common_key_events::handle_left_event(app), @@ -275,7 +275,7 @@ pub fn handler(key: Key, app: &mut App) { ArtistBlock::Empty => {} } } - k if common_key_events::right_event(k) => { + k if common_key_events::right_event(k, &app.user_config.keys) => { artist.artist_selected_block = ArtistBlock::Empty; handle_down_press_on_hovered_block(app); } diff --git a/src/tui/handlers/artist_albums.rs b/src/tui/handlers/artist_albums.rs index 2ff5714..d36aabb 100644 --- a/src/tui/handlers/artist_albums.rs +++ b/src/tui/handlers/artist_albums.rs @@ -6,8 +6,8 @@ use crate::{ pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => common_key_events::handle_left_event(app), + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(artist_albums) = &mut app.artist_albums { let next_index = common_key_events::on_down_press_handler( &artist_albums.albums.items, @@ -16,7 +16,7 @@ pub fn handler(key: Key, app: &mut App) { artist_albums.selected_index = next_index; } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(artist_albums) = &mut app.artist_albums { let next_index = common_key_events::on_up_press_handler( &artist_albums.albums.items, diff --git a/src/tui/handlers/artists.rs b/src/tui/handlers/artists.rs index e0df040..bd833cb 100644 --- a/src/tui/handlers/artists.rs +++ b/src/tui/handlers/artists.rs @@ -6,15 +6,17 @@ use rspotify::prelude::*; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(artists) = &mut app.library.saved_artists.get_results(None) { let next_index = common_key_events::on_down_press_handler(&artists.items, Some(app.artists_list_index)); app.artists_list_index = next_index; } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(artists) = &mut app.library.saved_artists.get_results(None) { let next_index = common_key_events::on_up_press_handler(&artists.items, Some(app.artists_list_index)); diff --git a/src/tui/handlers/dialog.rs b/src/tui/handlers/dialog.rs index 22e9cd6..ee9e8a3 100644 --- a/src/tui/handlers/dialog.rs +++ b/src/tui/handlers/dialog.rs @@ -46,8 +46,8 @@ fn handle_confirmation_dialog(key: Key, app: &mut App, dialog_context: DialogCon } close_dialog(app); } - k if common_key_events::right_event(k) => app.confirm = !app.confirm, - k if common_key_events::left_event(k) => app.confirm = !app.confirm, + k if common_key_events::right_event(k, &app.user_config.keys) => app.confirm = !app.confirm, + k if common_key_events::left_event(k, &app.user_config.keys) => app.confirm = !app.confirm, _ => {} } } @@ -56,14 +56,14 @@ fn handle_add_to_playlist_picker(key: Key, app: &mut App) { let editable_playlists = app.editable_playlists(); let playlist_count = editable_playlists.len(); match key { - k if common_key_events::down_event(k) && playlist_count > 0 => { + k if common_key_events::down_event(k, &app.user_config.keys) && playlist_count > 0 => { let next = common_key_events::on_down_press_handler( &editable_playlists, Some(app.playlist_picker_selected_index), ); app.playlist_picker_selected_index = next; } - k if common_key_events::up_event(k) && playlist_count > 0 => { + k if common_key_events::up_event(k, &app.user_config.keys) && playlist_count > 0 => { let next = common_key_events::on_up_press_handler( &editable_playlists, Some(app.playlist_picker_selected_index), diff --git a/src/tui/handlers/discover.rs b/src/tui/handlers/discover.rs index 290e42d..80d3572 100644 --- a/src/tui/handlers/discover.rs +++ b/src/tui/handlers/discover.rs @@ -8,8 +8,10 @@ const DISCOVER_OPTIONS_COUNT: usize = 2; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { let next_index = if app.discover_selected_index >= DISCOVER_OPTIONS_COUNT - 1 { 0 } else { @@ -17,7 +19,7 @@ pub fn handler(key: Key, app: &mut App) { }; app.discover_selected_index = next_index; } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { let next_index = if app.discover_selected_index == 0 { DISCOVER_OPTIONS_COUNT - 1 } else { @@ -26,7 +28,9 @@ pub fn handler(key: Key, app: &mut App) { app.discover_selected_index = next_index; } // Left/Right to cycle time range (for Top Tracks) - k if common_key_events::right_event(k) && app.discover_selected_index == 1 => { + k if common_key_events::right_event(k, &app.user_config.keys) + && app.discover_selected_index == 1 => + { // Only cycle time range when Top Tracks is selected app.discover_time_range = app.discover_time_range.next(); // Clear cache so it refetches with new time range diff --git a/src/tui/handlers/empty.rs b/src/tui/handlers/empty.rs index 760edd1..5fb8cea 100644 --- a/src/tui/handlers/empty.rs +++ b/src/tui/handlers/empty.rs @@ -11,50 +11,58 @@ pub fn handler(key: Key, app: &mut App) { let current_hovered = app.get_current_route().hovered_block; app.set_current_route_state(Some(current_hovered), None); } - k if common_key_events::down_event(k) => match app.get_current_route().hovered_block { - ActiveBlock::Library => { - app.set_current_route_state(None, Some(ActiveBlock::MyPlaylists)); + k if common_key_events::down_event(k, &app.user_config.keys) => { + match app.get_current_route().hovered_block { + ActiveBlock::Library => { + app.set_current_route_state(None, Some(ActiveBlock::MyPlaylists)); + } + ActiveBlock::ArtistBlock + | ActiveBlock::AlbumList + | ActiveBlock::AlbumTracks + | ActiveBlock::Artists + | ActiveBlock::Podcasts + | ActiveBlock::EpisodeTable + | ActiveBlock::Home + | ActiveBlock::Discover + | ActiveBlock::MyPlaylists + | ActiveBlock::RecentlyPlayed + | ActiveBlock::TrackTable => { + app.set_current_route_state(None, Some(ActiveBlock::PlayBar)); + } + _ => {} } - ActiveBlock::ArtistBlock - | ActiveBlock::AlbumList - | ActiveBlock::AlbumTracks - | ActiveBlock::Artists - | ActiveBlock::Podcasts - | ActiveBlock::EpisodeTable - | ActiveBlock::Home - | ActiveBlock::Discover - | ActiveBlock::MyPlaylists - | ActiveBlock::RecentlyPlayed - | ActiveBlock::TrackTable => { - app.set_current_route_state(None, Some(ActiveBlock::PlayBar)); - } - _ => {} - }, - k if common_key_events::up_event(k) => match app.get_current_route().hovered_block { - ActiveBlock::MyPlaylists => { - app.set_current_route_state(None, Some(ActiveBlock::Library)); - } - ActiveBlock::PlayBar => { - app.set_current_route_state(None, Some(ActiveBlock::MyPlaylists)); + } + k if common_key_events::up_event(k, &app.user_config.keys) => { + match app.get_current_route().hovered_block { + ActiveBlock::MyPlaylists => { + app.set_current_route_state(None, Some(ActiveBlock::Library)); + } + ActiveBlock::PlayBar => { + app.set_current_route_state(None, Some(ActiveBlock::MyPlaylists)); + } + _ => {} } - _ => {} - }, - k if common_key_events::left_event(k) => match app.get_current_route().hovered_block { - ActiveBlock::ArtistBlock - | ActiveBlock::AlbumList - | ActiveBlock::AlbumTracks - | ActiveBlock::Artists - | ActiveBlock::Podcasts - | ActiveBlock::EpisodeTable - | ActiveBlock::Home - | ActiveBlock::Discover - | ActiveBlock::RecentlyPlayed - | ActiveBlock::TrackTable => { - app.set_current_route_state(None, Some(ActiveBlock::Library)); + } + k if common_key_events::left_event(k, &app.user_config.keys) => { + match app.get_current_route().hovered_block { + ActiveBlock::ArtistBlock + | ActiveBlock::AlbumList + | ActiveBlock::AlbumTracks + | ActiveBlock::Artists + | ActiveBlock::Podcasts + | ActiveBlock::EpisodeTable + | ActiveBlock::Home + | ActiveBlock::Discover + | ActiveBlock::RecentlyPlayed + | ActiveBlock::TrackTable => { + app.set_current_route_state(None, Some(ActiveBlock::Library)); + } + _ => {} } - _ => {} - }, - k if common_key_events::right_event(k) => common_key_events::handle_right_event(app), + } + k if common_key_events::right_event(k, &app.user_config.keys) => { + common_key_events::handle_right_event(app) + } Key::Char('w') if app.get_current_route().hovered_block == ActiveBlock::PlayBar => { super::playbar::handler(key, app); } diff --git a/src/tui/handlers/episode_table.rs b/src/tui/handlers/episode_table.rs index 9ad3ec7..963ed2d 100644 --- a/src/tui/handlers/episode_table.rs +++ b/src/tui/handlers/episode_table.rs @@ -7,15 +7,17 @@ use rspotify::{model::PlayableId, prelude::*}; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(episodes) = &mut app.library.show_episodes.get_results(None) { let next_index = common_key_events::on_down_press_handler(&episodes.items, Some(app.episode_list_index)); app.episode_list_index = next_index; } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(episodes) = &mut app.library.show_episodes.get_results(None) { let next_index = common_key_events::on_up_press_handler(&episodes.items, Some(app.episode_list_index)); diff --git a/src/tui/handlers/friends.rs b/src/tui/handlers/friends.rs index 0bcd61f..7866bf1 100644 --- a/src/tui/handlers/friends.rs +++ b/src/tui/handlers/friends.rs @@ -36,8 +36,8 @@ pub fn handler(key: Key, app: &mut App) { match key { // Navigation - k if common_key_events::down_event(k) => move_down(app), - k if common_key_events::up_event(k) => move_up(app), + k if common_key_events::down_event(k, &app.user_config.keys) => move_down(app), + k if common_key_events::up_event(k, &app.user_config.keys) => move_up(app), k if common_key_events::high_event(k) => app.friend_selected_index = 0, k if common_key_events::low_event(k) => { let count = filtered_count(app); @@ -137,7 +137,9 @@ fn handle_add_dialog(key: Key, app: &mut App) { }, // Navigate search results - k if app.friend_add_mode == FriendAddMode::Search && common_key_events::down_event(k) => { + k if app.friend_add_mode == FriendAddMode::Search + && common_key_events::down_event(k, &app.user_config.keys) => + { let count = app.friend_user_search_results.len(); if count > 0 { app.friend_user_search_selected = (app.friend_user_search_selected + 1).min(count - 1); @@ -145,7 +147,7 @@ fn handle_add_dialog(key: Key, app: &mut App) { } k if app.friend_add_mode == FriendAddMode::Search - && common_key_events::up_event(k) + && common_key_events::up_event(k, &app.user_config.keys) && app.friend_user_search_selected > 0 => { app.friend_user_search_selected -= 1; diff --git a/src/tui/handlers/help_menu.rs b/src/tui/handlers/help_menu.rs index b7307be..0770d62 100644 --- a/src/tui/handlers/help_menu.rs +++ b/src/tui/handlers/help_menu.rs @@ -9,10 +9,10 @@ enum Direction { pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::down_event(k) => { + k if common_key_events::down_event(k, &app.user_config.keys) => { move_page(Direction::Down, app); } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { move_page(Direction::Up, app); } Key::Ctrl('d') => { diff --git a/src/tui/handlers/home.rs b/src/tui/handlers/home.rs index 1227629..2e73981 100644 --- a/src/tui/handlers/home.rs +++ b/src/tui/handlers/home.rs @@ -7,11 +7,13 @@ const SMALL_SCROLL: u16 = 1; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { app.home_scroll += SMALL_SCROLL; } - k if common_key_events::up_event(k) && app.home_scroll > 0 => { + k if common_key_events::up_event(k, &app.user_config.keys) && app.home_scroll > 0 => { app.home_scroll -= SMALL_SCROLL; } k if k == app.user_config.keys.next_page => { diff --git a/src/tui/handlers/library.rs b/src/tui/handlers/library.rs index 3f6a95f..32f8d96 100644 --- a/src/tui/handlers/library.rs +++ b/src/tui/handlers/library.rs @@ -5,15 +5,17 @@ use crate::tui::event::Key; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::right_event(k) => common_key_events::handle_right_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::right_event(k, &app.user_config.keys) => { + common_key_events::handle_right_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { let next_index = common_key_events::on_down_press_handler( &LIBRARY_OPTIONS, Some(app.library.selected_index), ); app.library.selected_index = next_index; } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { let next_index = common_key_events::on_up_press_handler(&LIBRARY_OPTIONS, Some(app.library.selected_index)); app.library.selected_index = next_index; diff --git a/src/tui/handlers/mod.rs b/src/tui/handlers/mod.rs index 83cc14d..5c0f9bf 100644 --- a/src/tui/handlers/mod.rs +++ b/src/tui/handlers/mod.rs @@ -85,10 +85,10 @@ pub fn handle_app(key: Key, app: &mut App) { app.plugin_popup = None; app.plugin_popup_scroll = 0; } - Key::Up | Key::Char('k') => { + k if common_key_events::up_event(k, &app.user_config.keys) => { app.plugin_popup_scroll = app.plugin_popup_scroll.saturating_sub(1); } - Key::Down | Key::Char('j') => { + k if common_key_events::down_event(k, &app.user_config.keys) => { let max_scroll = app .plugin_popup .as_ref() diff --git a/src/tui/handlers/playbar.rs b/src/tui/handlers/playbar.rs index 68fc4a4..9395980 100644 --- a/src/tui/handlers/playbar.rs +++ b/src/tui/handlers/playbar.rs @@ -7,7 +7,7 @@ use rspotify::model::{context::CurrentPlaybackContext, PlayableId, PlayableItem} pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { app.set_current_route_state(Some(ActiveBlock::Empty), Some(ActiveBlock::MyPlaylists)); } k => { diff --git a/src/tui/handlers/playlist.rs b/src/tui/handlers/playlist.rs index 45f9ddf..86f0516 100644 --- a/src/tui/handlers/playlist.rs +++ b/src/tui/handlers/playlist.rs @@ -11,15 +11,17 @@ fn total_display_count(app: &App) -> usize { pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::right_event(k) => common_key_events::handle_right_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::right_event(k, &app.user_config.keys) => { + common_key_events::handle_right_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { let count = total_display_count(app); if count > 0 { let current = app.selected_playlist_index.unwrap_or(0); app.selected_playlist_index = Some((current + 1) % count); } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { let count = total_display_count(app); if count > 0 { let current = app.selected_playlist_index.unwrap_or(0); diff --git a/src/tui/handlers/podcasts.rs b/src/tui/handlers/podcasts.rs index fc711cb..8a78c52 100644 --- a/src/tui/handlers/podcasts.rs +++ b/src/tui/handlers/podcasts.rs @@ -5,15 +5,17 @@ use crate::tui::event::Key; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(shows) = &mut app.library.saved_shows.get_results(None) { let next_index = common_key_events::on_down_press_handler(&shows.items, Some(app.shows_list_index)); app.shows_list_index = next_index; } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(shows) = &mut app.library.saved_shows.get_results(None) { let next_index = common_key_events::on_up_press_handler(&shows.items, Some(app.shows_list_index)); diff --git a/src/tui/handlers/queue_menu.rs b/src/tui/handlers/queue_menu.rs index 12d8c1b..61419a3 100644 --- a/src/tui/handlers/queue_menu.rs +++ b/src/tui/handlers/queue_menu.rs @@ -4,10 +4,10 @@ use crate::tui::event::Key; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::down_event(k) => { + k if common_key_events::down_event(k, &app.user_config.keys) => { move_selection(1, app); } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { move_selection(-1, app); } _ => {} diff --git a/src/tui/handlers/recently_played.rs b/src/tui/handlers/recently_played.rs index 2e5394a..d170754 100644 --- a/src/tui/handlers/recently_played.rs +++ b/src/tui/handlers/recently_played.rs @@ -8,8 +8,10 @@ use rspotify::prelude::Id; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(recently_played_result) = &app.recently_played.result { let next_index = common_key_events::on_down_press_handler( &recently_played_result.items, @@ -18,7 +20,7 @@ pub fn handler(key: Key, app: &mut App) { app.recently_played.index = next_index; } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(recently_played_result) = &app.recently_played.result { let next_index = common_key_events::on_up_press_handler( &recently_played_result.items, diff --git a/src/tui/handlers/search_results.rs b/src/tui/handlers/search_results.rs index fcc2b68..849f926 100644 --- a/src/tui/handlers/search_results.rs +++ b/src/tui/handlers/search_results.rs @@ -423,21 +423,21 @@ pub fn handler(key: Key, app: &mut App) { Key::Esc => { app.search_results.selected_block = SearchResultBlock::Empty; } - k if common_key_events::down_event(k) => { + k if common_key_events::down_event(k, &app.user_config.keys) => { if app.search_results.selected_block != SearchResultBlock::Empty { handle_down_press_on_selected_block(app); } else { handle_down_press_on_hovered_block(app); } } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if app.search_results.selected_block != SearchResultBlock::Empty { handle_up_press_on_selected_block(app); } else { handle_up_press_on_hovered_block(app); } } - k if common_key_events::left_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { app.search_results.selected_block = SearchResultBlock::Empty; match app.search_results.hovered_block { SearchResultBlock::AlbumSearch => { @@ -458,7 +458,7 @@ pub fn handler(key: Key, app: &mut App) { SearchResultBlock::Empty => {} } } - k if common_key_events::right_event(k) => { + k if common_key_events::right_event(k, &app.user_config.keys) => { app.search_results.selected_block = SearchResultBlock::Empty; match app.search_results.hovered_block { SearchResultBlock::AlbumSearch => { diff --git a/src/tui/handlers/select_device.rs b/src/tui/handlers/select_device.rs index 801e667..ca9fb55 100644 --- a/src/tui/handlers/select_device.rs +++ b/src/tui/handlers/select_device.rs @@ -5,7 +5,7 @@ use crate::tui::event::Key; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::down_event(k) => { + k if common_key_events::down_event(k, &app.user_config.keys) => { if let Some(p) = &app.devices { if let Some(selected_device_index) = app.selected_device_index { let next_index = @@ -14,7 +14,7 @@ pub fn handler(key: Key, app: &mut App) { } }; } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if let Some(p) = &app.devices { if let Some(selected_device_index) = app.selected_device_index { let next_index = diff --git a/src/tui/handlers/settings.rs b/src/tui/handlers/settings.rs index 88cdb3b..16240c2 100644 --- a/src/tui/handlers/settings.rs +++ b/src/tui/handlers/settings.rs @@ -18,12 +18,12 @@ pub fn handler(key: Key, app: &mut App) { fn handle_navigation(key: Key, app: &mut App) { match key { // Category switching with left/right (only when not in edit mode) - key if left_event(key) => switch_category_left(app), - key if right_event(key) => switch_category_right(app), + key if left_event(key, &app.user_config.keys) => switch_category_left(app), + key if right_event(key, &app.user_config.keys) => switch_category_right(app), // Item selection with up/down - key if down_event(key) => select_next_item(app), - key if up_event(key) => select_previous_item(app), + key if down_event(key, &app.user_config.keys) => select_next_item(app), + key if up_event(key, &app.user_config.keys) => select_previous_item(app), // Enter edit mode Key::Enter => enter_edit_mode(app), @@ -59,7 +59,7 @@ fn handle_unsaved_changes_prompt(key: Key, app: &mut App) { close_settings(app); } } - key if left_event(key) || right_event(key) => { + key if left_event(key, &app.user_config.keys) || right_event(key, &app.user_config.keys) => { app.settings_unsaved_prompt_save_selected = !app.settings_unsaved_prompt_save_selected; } key if key == app.user_config.keys.back => { @@ -119,7 +119,7 @@ fn handle_bool_edit(key: Key, app: &mut App) { Key::Esc => { app.settings_edit_mode = false; } - key if left_event(key) || right_event(key) => { + key if left_event(key, &app.user_config.keys) || right_event(key, &app.user_config.keys) => { // Toggle on left/right as well for better UX if let Some(setting) = app.settings_items.get_mut(app.settings_selected_index) { if let SettingValue::Bool(v) = setting.value { @@ -153,7 +153,7 @@ fn handle_number_edit(key: Key, app: &mut App) { Key::Backspace => { app.settings_edit_buffer.pop(); } - key if up_event(key) => { + key if up_event(key, &app.user_config.keys) => { // Increment value if let Some(setting) = app.settings_items.get_mut(app.settings_selected_index) { if let SettingValue::Number(v) = setting.value { @@ -163,7 +163,7 @@ fn handle_number_edit(key: Key, app: &mut App) { } } } - key if down_event(key) => { + key if down_event(key, &app.user_config.keys) => { // Decrement value if let Some(setting) = app.settings_items.get_mut(app.settings_selected_index) { if let SettingValue::Number(v) = setting.value { @@ -478,8 +478,8 @@ fn handle_preset_edit(key: Key, app: &mut App) { Key::Esc => { app.settings_edit_mode = false; } - key if right_event(key) => cycle(app, true), - key if left_event(key) => cycle(app, false), + key if right_event(key, &app.user_config.keys) => cycle(app, true), + key if left_event(key, &app.user_config.keys) => cycle(app, false), _ => {} } } diff --git a/src/tui/handlers/sort_menu.rs b/src/tui/handlers/sort_menu.rs index 4312ef7..0839375 100644 --- a/src/tui/handlers/sort_menu.rs +++ b/src/tui/handlers/sort_menu.rs @@ -2,6 +2,7 @@ //! //! Handles keyboard input for the sort menu popup +use super::common_key_events; use crate::core::app::{ActiveBlock, App}; use crate::core::sort::{SortContext, SortField}; use crate::tui::event::Key; @@ -21,14 +22,14 @@ pub fn handler(key: Key, app: &mut App) { Key::Esc | Key::Char(',') => { close_sort_menu(app); } - Key::Up | Key::Char('k') => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if app.sort_menu_selected > 0 { app.sort_menu_selected -= 1; } else { app.sort_menu_selected = available_fields.len().saturating_sub(1); } } - Key::Down | Key::Char('j') => { + k if common_key_events::down_event(k, &app.user_config.keys) => { if app.sort_menu_selected < available_fields.len().saturating_sub(1) { app.sort_menu_selected += 1; } else { diff --git a/src/tui/handlers/track_table.rs b/src/tui/handlers/track_table.rs index 08592f9..75e4c69 100644 --- a/src/tui/handlers/track_table.rs +++ b/src/tui/handlers/track_table.rs @@ -14,8 +14,10 @@ use rspotify::prelude::Id; pub fn handler(key: Key, app: &mut App) { match key { - k if common_key_events::left_event(k) => common_key_events::handle_left_event(app), - k if common_key_events::down_event(k) => { + k if common_key_events::left_event(k, &app.user_config.keys) => { + common_key_events::handle_left_event(app) + } + k if common_key_events::down_event(k, &app.user_config.keys) => { let current_index = app.track_table.selected_index; let tracks_len = app.track_table.tracks.len(); @@ -57,7 +59,7 @@ pub fn handler(key: Key, app: &mut App) { ); app.track_table.selected_index = next_index; } - k if common_key_events::up_event(k) => { + k if common_key_events::up_event(k, &app.user_config.keys) => { if app.track_table.tracks.is_empty() { return; } From ffb3243f07fb5f788dd7388b1108b675f15a9d50 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 13 Jun 2026 23:27:52 -0400 Subject: [PATCH 4/6] update hardcoded keys in ui strings (these should read from keybindings now) --- src/tui/ui/help.rs | 8 ++++---- src/tui/ui/player.rs | 20 +++++++++++++++----- src/tui/ui/popups.rs | 9 ++++++--- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/tui/ui/help.rs b/src/tui/ui/help.rs index 5fa0d7a..08f9b25 100644 --- a/src/tui/ui/help.rs +++ b/src/tui/ui/help.rs @@ -95,22 +95,22 @@ pub fn get_help_docs(app: &App) -> Vec> { ], vec![ String::from("Move selection left"), - String::from("h | | "), + format!("{} | | ", key_bindings.move_left), String::from("General"), ], vec![ String::from("Move selection down"), - String::from("j | | "), + format!("{} | | ", key_bindings.move_down), String::from("General"), ], vec![ String::from("Move selection up"), - String::from("k | | "), + format!("{} | | ", key_bindings.move_up), String::from("General"), ], vec![ String::from("Move selection right"), - String::from("l | | "), + format!("{} | | ", key_bindings.move_right), String::from("General (Ctrl+f searches inside playlist track tables)"), ], vec![ diff --git a/src/tui/ui/player.rs b/src/tui/ui/player.rs index bd2f0d1..d2722a0 100644 --- a/src/tui/ui/player.rs +++ b/src/tui/ui/player.rs @@ -1000,12 +1000,22 @@ pub fn draw_device_list(f: &mut Frame<'_>, app: &App) { .area() .layout(&Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]).margin(5)); + let move_instructions = format!( + "Use `{}`/`{}` or up/down arrow keys to move up and down and to select. ", + app.user_config.keys.move_down, app.user_config.keys.move_up, + ); let device_instructions: Vec = vec![ - "To play tracks, please select a device. ", - "Use `j/k` or up/down arrow keys to move up and down and to select. ", - "Your choice here will be cached so you can jump straight back in when you next open `spotatui`. ", - "You can change the playback device at any time by pressing `d`.", - ].into_iter().map(|instruction| Line::from(Span::raw(instruction))).collect(); + Line::from(Span::raw( + "To play tracks, please select a device. ", + )), + Line::from(Span::raw(move_instructions)), + Line::from(Span::raw( + "Your choice here will be cached so you can jump straight back in when you next open `spotatui`. ", + )), + Line::from(Span::raw( + "You can change the playback device at any time by pressing `d`.", + )), + ]; let instructions = Paragraph::new(device_instructions) .style(Style::default().fg(app.user_config.theme.text)) diff --git a/src/tui/ui/popups.rs b/src/tui/ui/popups.rs index ebdda3a..d61f7d6 100644 --- a/src/tui/ui/popups.rs +++ b/src/tui/ui/popups.rs @@ -415,9 +415,12 @@ fn draw_add_track_to_playlist_picker_dialog(f: &mut Frame<'_>, app: &App) { f.render_stateful_widget(list, vchunks[1], &mut list_state); } - let footer = Paragraph::new("Enter add | q cancel | j/k or arrows move | H/M/L jump") - .style(Style::default().fg(app.user_config.theme.inactive)) - .alignment(Alignment::Center); + let footer = Paragraph::new(format!( + "Enter add | q cancel | {}/{} or arrows move | H/M/L jump", + app.user_config.keys.move_down, app.user_config.keys.move_up, + )) + .style(Style::default().fg(app.user_config.theme.inactive)) + .alignment(Alignment::Center); f.render_widget(footer, vchunks[2]); } From 8924ef296bcf3e2b236558d3036bee8b4261e8ce Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 13 Jun 2026 23:28:03 -0400 Subject: [PATCH 5/6] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f94b3..6b52155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v0.39.2] 2026-06-14 + +### Added + +- **Configurable movement keybindings**: `h`/`j`/`k`/`l` keys (traditionally left/down/up/right) are now configurable via `move_left`, `move_down`, `move_up`, and `move_right` under `keybindings` in `config.yml` and via the Settings screen. Defaults match the previous hard-coded bindings, and the arrow key(s) fallbacks still work. + ## [v0.39.1] 2026-06-12 ### Fixed From 7b43c4151eb55350484ed0067ff4da5909da8452 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 13 Jun 2026 23:36:05 -0400 Subject: [PATCH 6/6] fix changelog format --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b52155..d95dc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [v0.39.2] 2026-06-14 +## [Unreleased] ### Added