Repository: aome510/spotify-player (language: rust)

Issue:
Remap key to key combination
I'm thinking how to bind y to the equivalent of 'a 5'. So, current song, copy link.
I think either having it so you can bind to a sequence of commands, or bind a key to a sequence of keys would be nice.
Something like
[[keymaps]]
command = "ShowActionsOnCurrentTrack; CopyTrackLink"
key_sequence = "y"
or
[[keymaps]]
command = "a 5" // maybe a different name instead of command? idk
key_sequence = "y"



Discussion / hints:
It's better to define a separate mapping for actions, for example

```toml
[[actions]]
action = "GoToTrackRadio"
key = "r"

[[actions]]
action = "AddToLikedTracks"
key = "y"
```
I just started to take a look at the actions! Nice one.
Unfortunately it seems it still doesnt allow to implement the example I gave? I can only for example copy the link of the current selection. I can't do the equivalent of 'a 5' right now.
I'd need to go to currently playing context and select the song I'm playing for example and then use the CopyLink action.
Unless I'm getting something wrong? 
> Unfortunately it seems it still doesnt allow to implement the example I gave? I can only for example copy the link of the current selection. I can't do the equivalent of 'a 5' right now.
I'd need to go to currently playing context and select the song I'm playing for example and then use the CopyLink action.
Unless I'm getting something wrong?

Yeah, you're right. The current action only works on **currently selected item**. We can update `action` keymap to also support currently playing track, which can be configured by a new field `ActionType`

```rust
enum ActionType {
   PlayingTrack
   SelectedItem
}
```

cc @juliamertz is this something you're interested in working on as a follow-up?
> cc @juliamertz is this something you're interested in working on as a follow-up?

Doesn't look like this would be too much work, i'll take a look at it when i get some time
> > cc @juliamertz is this something you're interested in working on as a follow-up?
> 
> Doesn't look like this would be too much work, i'll take a look at it when i get some time

Great, thank you!

Fix patch:
diff --git a/docs/config.md b/docs/config.md
index d60b7c03..b85024fe 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -286,9 +286,11 @@ key_sequence = "q"
 
 ## Actions
 
-Actions are located in the same `keymap.toml` file as keymaps. An action can be triggered by a key sequence that is not bound to any command. Once the mapped key sequence is pressed, the corresponding action will be triggered **on the currently selected item**. For example,
+Actions are located in the same `keymap.toml` file as keymaps. An action can be triggered by a key sequence that is not bound to any command. Once the mapped key sequence is pressed, the corresponding action will be triggered. By default actions will act upon the currently selected item, you can change this behaviour by setting the `target` field for a keymap to either `PlayingTrack` or `SelectedItem`. 
 a list of actions can be found [here](../README.md#actions).
 
+For example,
+
 ```toml
 [[actions]]
 action = "GoToArtist"
@@ -296,6 +298,7 @@ key_sequence = "g A"
 [[actions]]
 action = "GoToAlbum"
 key_sequence = "g B"
+target = "PlayingTrack"
 [[actions]]
 action="ToggleLiked"
 key_sequence="C-l"
diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs
index 7fbd7047..908ee17c 100644
--- a/spotify_player/src/command.rs
+++ b/spotify_player/src/command.rs
@@ -112,9 +112,16 @@ pub enum ActionContext {
     PlaylistFolder(PlaylistFolder),
 }
 
+#[derive(Debug, PartialEq, Clone, Deserialize, Default, Copy)]
+pub enum ActionTarget {
+    PlayingTrack,
+    #[default]
+    SelectedItem,
+}
+
 pub enum CommandOrAction {
     Command(Command),
-    Action(Action),
+    Action(Action, ActionTarget),
 }
 
 impl From<Track> for ActionContext {
diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs
index 452a43bf..256db7e3 100644
--- a/spotify_player/src/config/keymap.rs
+++ b/spotify_player/src/config/keymap.rs
@@ -1,5 +1,5 @@
 use crate::{
-    command::{Action, Command, CommandOrAction},
+    command::{Action, ActionTarget, Command, CommandOrAction},
     key::{Key, KeySequence},
 };
 use anyhow::Result;
@@ -25,6 +25,8 @@ pub struct Keymap {
 /// A keymap that triggers an `Action` when a key sequence is pressed
 pub struct ActionMap {
     pub key_sequence: KeySequence,
+    #[serde(default)]
+    pub target: ActionTarget,
     pub action: Action,
 }
 
@@ -401,11 +403,14 @@ impl KeymapConfig {
     }
 
     /// finds an action from a mapped key sequence
-    pub fn find_action_from_key_sequence(&self, key_sequence: &KeySequence) -> Option<Action> {
+    pub fn find_action_from_key_sequence(
+        &self,
+        key_sequence: &KeySequence,
+    ) -> Option<(Action, ActionTarget)> {
         self.actions
             .iter()
             .find(|&action| action.key_sequence == *key_sequence)
-            .map(|action| action.action)
+            .map(|action| (action.action, action.target))
     }
 
     /// finds a command or action from a mapped key sequence
@@ -416,8 +421,8 @@ impl KeymapConfig {
         if let Some(command) = self.find_command_from_key_sequence(key_sequence) {
             return Some(CommandOrAction::Command(command));
         }
-        if let Some(action) = self.find_action_from_key_sequence(key_sequence) {
-            return Some(CommandOrAction::Action(action));
+        if let Some((action, target)) = self.find_action_from_key_sequence(key_sequence) {
+            return Some(CommandOrAction::Action(action, target));
         }
         None
     }
diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs
index 22598c34..a80b53c4 100644
--- a/spotify_player/src/event/mod.rs
+++ b/spotify_player/src/event/mod.rs
@@ -1,6 +1,9 @@
 use crate::{
     client::{ClientRequest, PlayerRequest},
-    command::{self, construct_artist_actions, Action, ActionContext, Command},
+    command::{
+        self, construct_artist_actions, Action, ActionContext, ActionTarget, Command,
+        CommandOrAction,
+    },
     config,
     key::{Key, KeySequence},
     state::*,
@@ -103,10 +106,15 @@ fn handle_key_event(
         }
     };
 
-    // if the key sequence is not handled, let the global command handler handle it
+    // if the key sequence is not handled, let the global handler handle it
     let handled = if !handled {
-        match keymap_config.find_command_from_key_sequence(&key_sequence) {
-            Some(command) => handle_global_command(command, client_pub, state, &mut ui)?,
+        match keymap_config.find_command_or_action_from_key_sequence(&key_sequence) {
+            Some(CommandOrAction::Action(action, target)) => {
+                handle_global_action(action, target, client_pub, state, &mut ui)?
+            }
+            Some(CommandOrAction::Command(command)) => {
+                handle_global_command(command, client_pub, state, &mut ui)?
+            }
             None => false,
         }
     } else {
@@ -129,7 +137,7 @@ pub fn handle_action_in_context(
     client_pub: &flume::Sender<ClientRequest>,
     data: &DataReadGuard,
     ui: &mut UIStateGuard,
-) -> Result<()> {
+) -> Result<bool> {
     match context {
         ActionContext::Track(track) => match action {
             Action::GoToAlbum => {
@@ -142,19 +150,24 @@ pub fn handle_action_in_context(
                         context_page_type: ContextPageType::Browsing(context_id),
                         state: None,
                     });
+                    return Ok(true);
                 }
+                Ok(false)
             }
             Action::GoToArtist => {
                 handle_go_to_artist(track.artists, ui);
+                Ok(true)
             }
             Action::AddToQueue => {
                 client_pub.send(ClientRequest::AddTrackToQueue(track.id))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::CopyLink => {
                 let track_url = format!("https://open.spotify.com/track/{}", track.id.id());
                 execute_copy_command(track_url)?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::AddToPlaylist => {
                 client_pub.send(ClientRequest::GetUserPlaylists)?;
@@ -165,6 +178,7 @@ pub fn handle_action_in_context(
                     },
                     ListState::default(),
                 ));
+                Ok(true)
             }
             Action::ToggleLiked => {
                 if data.user_data.is_liked_track(&track) {
@@ -173,14 +187,17 @@ pub fn handle_action_in_context(
                     client_pub.send(ClientRequest::AddToLibrary(Item::Track(track)))?;
                 }
                 ui.popup = None;
+                Ok(true)
             }
             Action::AddToLiked => {
                 client_pub.send(ClientRequest::AddToLibrary(Item::Track(track)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::DeleteFromLiked => {
                 client_pub.send(ClientRequest::DeleteFromLibrary(ItemId::Track(track.id)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::GoToRadio => {
                 let uri = track.id.uri();
@@ -190,8 +207,12 @@ pub fn handle_action_in_context(
                     seed_uri: uri,
                     seed_name: name,
                 })?;
+                Ok(true)
+            }
+            Action::ShowActionsOnArtist => {
+                handle_show_actions_on_artist(track.artists, data, ui);
+                Ok(true)
             }
-            Action::ShowActionsOnArtist => handle_show_actions_on_artist(track.artists, data, ui),
             Action::ShowActionsOnAlbum => {
                 if let Some(album) = track.album {
                     let context = ActionContext::Album(album.clone());
@@ -202,7 +223,9 @@ pub fn handle_action_in_context(
                         )),
                         ListState::default(),
                     ));
+                    return Ok(true);
                 }
+                Ok(false)
             }
             Action::DeleteFromPlaylist => {
                 if let PageState::Context {
@@ -216,12 +239,14 @@ pub fn handle_action_in_context(
                     ))?;
                 }
                 ui.popup = None;
+                Ok(true)
             }
-            _ => {}
+            _ => Ok(false),
         },
         ActionContext::Album(album) => match action {
             Action::GoToArtist => {
                 handle_go_to_artist(album.artists, ui);
+                Ok(true)
             }
             Action::GoToRadio => {
                 let uri = album.id.uri();
@@ -231,42 +256,51 @@ pub fn handle_action_in_context(
                     seed_uri: uri,
                     seed_name: name,
                 })?;
+                Ok(true)
             }
             Action::ShowActionsOnArtist => {
                 handle_show_actions_on_artist(album.artists, data, ui);
+                Ok(true)
             }
             Action::AddToLibrary => {
                 client_pub.send(ClientRequest::AddToLibrary(Item::Album(album)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::DeleteFromLibrary => {
                 client_pub.send(ClientRequest::DeleteFromLibrary(ItemId::Album(album.id)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::CopyLink => {
                 let album_url = format!("https://open.spotify.com/album/{}", album.id.id());
                 execute_copy_command(album_url)?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::AddToQueue => {
                 client_pub.send(ClientRequest::AddAlbumToQueue(album.id))?;
                 ui.popup = None;
+                Ok(true)
             }
-            _ => {}
+            _ => Ok(false),
         },
         ActionContext::Artist(artist) => match action {
             Action::Follow => {
                 client_pub.send(ClientRequest::AddToLibrary(Item::Artist(artist)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::Unfollow => {
                 client_pub.send(ClientRequest::DeleteFromLibrary(ItemId::Artist(artist.id)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::CopyLink => {
                 let artist_url = format!("https://open.spotify.com/artist/{}", artist.id.id());
                 execute_copy_command(artist_url)?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::GoToRadio => {
                 let uri = artist.id.uri();
@@ -276,13 +310,15 @@ pub fn handle_action_in_context(
                     seed_uri: uri,
                     seed_name: name,
                 })?;
+                Ok(true)
             }
-            _ => {}
+            _ => Ok(false),
         },
         ActionContext::Playlist(playlist) => match action {
             Action::AddToLibrary => {
                 client_pub.send(ClientRequest::AddToLibrary(Item::Playlist(playlist)))?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::GoToRadio => {
                 let uri = playlist.id.uri();
@@ -292,26 +328,27 @@ pub fn handle_action_in_context(
                     seed_uri: uri,
                     seed_name: name,
                 })?;
+                Ok(true)
             }
             Action::CopyLink => {
                 let playlist_url =
                     format!("https://open.spotify.com/playlist/{}", playlist.id.id());
                 execute_copy_command(playlist_url)?;
                 ui.popup = None;
+                Ok(true)
             }
             Action::DeleteFromLibrary => {
                 client_pub.send(ClientRequest::DeleteFromLibrary(ItemId::Playlist(
                     playlist.id,
                 )))?;
                 ui.popup = None;
+                Ok(true)
             }
-            _ => {}
+            _ => Ok(false),
         },
         // TODO: support actions for playlist folders
-        ActionContext::PlaylistFolder(_) => {}
+        ActionContext::PlaylistFolder(_) => Ok(false),
     }
-
-    Ok(())
 }
 
 fn handle_go_to_artist(artists: Vec<Artist>, ui: &mut UIStateGuard) {
@@ -351,6 +388,35 @@ fn handle_show_actions_on_artist(
     }
 }
 
+/// Handle a global action, currently this is only used to target
+/// the currently playing song instead of the selection.
+fn handle_global_action(
+    action: Action,
+    target: ActionTarget,
+    client_pub: &flume::Sender<ClientRequest>,
+    state: &SharedState,
+    ui: &mut UIStateGuard,
+) -> Result<bool> {
+    if target == ActionTarget::PlayingTrack {
+        let player = state.player.read();
+        let data = state.data.read();
+
+        if let Some(currently_playing) = player.current_playing_track() {
+            if let Some(track) = Track::try_from_full_track(currently_playing.clone()) {
+                return handle_action_in_context(
+                    action,
+                    ActionContext::Track(track),
+                    client_pub,
+                    &data,
+                    ui,
+                );
+            }
+        }
+    };
+
+    Ok(false)
+}
+
 /// Handle a global command that is not specific to any page/popup
 fn handle_global_command(
     command: Command,
diff --git a/spotify_player/src/event/page.rs b/spotify_player/src/event/page.rs
index 57fb7c10..87800612 100644
--- a/spotify_player/src/event/page.rs
+++ b/spotify_player/src/event/page.rs
@@ -30,7 +30,7 @@ pub fn handle_key_sequence_for_page(
             PageType::Queue => handle_command_for_queue_page(command, ui),
             PageType::CommandHelp => handle_command_for_command_help_page(command, ui),
         },
-        Some(CommandOrAction::Action(action)) => match page_type {
+        Some(CommandOrAction::Action(action, ActionTarget::SelectedItem)) => match page_type {
             PageType::Search => anyhow::bail!("page search type should already be handled!"),
             PageType::Library => handle_action_for_library_page(action, client_pub, ui, state),
             PageType::Context => {
@@ -39,7 +39,7 @@ pub fn handle_key_sequence_for_page(
             PageType::Browse => handle_action_for_browse_page(action, client_pub, ui, state),
             _ => Ok(false),
         },
-        None => Ok(false),
+        _ => Ok(false),
     }
 }
 
@@ -186,9 +186,10 @@ fn handle_key_sequence_for_search_page(
                 CommandOrAction::Command(command) => window::handle_command_for_track_list_window(
                     command, client_pub, tracks, &data, ui,
                 ),
-                CommandOrAction::Action(action) => {
+                CommandOrAction::Action(action, ActionTarget::SelectedItem) => {
                     window::handle_action_for_selected_item(action, tracks, &data, ui, client_pub)
                 }
+                _ => Ok(false),
             }
         }
         SearchFocusState::Artists => {
@@ -200,9 +201,10 @@ fn handle_key_sequence_for_search_page(
                 CommandOrAction::Command(command) => {
                     window::handle_command_for_artist_list_window(command, artists, &data, ui)
                 }
-                CommandOrAction::Action(action) => {
+                CommandOrAction::Action(action, ActionTarget::SelectedItem) => {
                     window::handle_action_for_selected_item(action, artists, &data, ui, client_pub)
                 }
+                _ => Ok(false),
             }
         }
         SearchFocusState::Albums => {
@@ -214,9 +216,10 @@ fn handle_key_sequence_for_search_page(
                 CommandOrAction::Command(command) => window::handle_command_for_album_list_window(
                     command, albums, &data, ui, client_pub,
                 ),
-                CommandOrAction::Action(action) => {
+                CommandOrAction::Action(action, ActionTarget::SelectedItem) => {
                     window::handle_action_for_selected_item(action, albums, &data, ui, client_pub)
                 }
+                _ => Ok(false),
             }
         }
         SearchFocusState::Playlists => {
@@ -239,13 +242,16 @@ fn handle_key_sequence_for_search_page(
                         ui,
                     )
                 }
-                CommandOrAction::Action(action) => window::handle_action_for_selected_item(
-                    action,
-                    playlist_refs,
-                    &data,
-                    ui,
-                    client_pub,
-                ),
+                CommandOrAction::Action(action, ActionTarget::SelectedItem) => {
+                    window::handle_action_for_selected_item(
+                        action,
+                        playlist_refs,
+                        &data,
+                        ui,
+                        client_pub,
+                    )
+                }
+                _ => Ok(false),
             }
         }
     }
diff --git a/spotify_player/src/event/popup.rs b/spotify_player/src/event/popup.rs
index 41718275..ea78c0c3 100644
--- a/spotify_player/src/event/popup.rs
+++ b/spotify_player/src/event/popup.rs
@@ -465,7 +465,8 @@ fn handle_key_sequence_for_action_list_popup(
         n_actions,
         |_, _| {},
         |ui: &mut UIStateGuard, id: usize| -> Result<()> {
-            handle_item_action(id, client_pub, state, ui)
+            handle_item_action(id, client_pub, state, ui)?;
+            Ok(())
         },
         |ui: &mut UIStateGuard| {
             ui.popup = None;
@@ -479,10 +480,10 @@ pub fn handle_item_action(
     client_pub: &flume::Sender<ClientRequest>,
     state: &SharedState,
     ui: &mut UIStateGuard,
-) -> Result<()> {
+) -> Result<bool> {
     let item = match ui.popup {
         Some(PopupState::ActionList(ref item, ..)) => *item.clone(),
-        _ => return Ok(()),
+        _ => return Ok(false),
     };
 
     let data = state.data.read();
diff --git a/spotify_player/src/event/window.rs b/spotify_player/src/event/window.rs
index 5bc14a71..49d89925 100644
--- a/spotify_player/src/event/window.rs
+++ b/spotify_player/src/event/window.rs
@@ -95,9 +95,7 @@ pub fn handle_action_for_selected_item<T: Into<ActionContext> + Clone>(
         return Ok(false);
     }
 
-    handle_action_in_context(action, items[id].clone().into(), client_pub, data, ui)?;
-
-    Ok(true)
+    handle_action_in_context(action, items[id].clone().into(), client_pub, data, ui)
 }
 
 /// Handle a command for the currently focused context window


Test patch: