Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: target configuration for action keymaps #539

Merged
merged 6 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,16 +286,19 @@ 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"
key_sequence = "g A"
[[actions]]
action = "GoToAlbum"
key_sequence = "g B"
target = "PlayingTrack"
[[actions]]
action="ToggleLiked"
key_sequence="C-l"
Expand Down
9 changes: 8 additions & 1 deletion spotify_player/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 10 additions & 5 deletions spotify_player/src/config/keymap.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
command::{Action, Command, CommandOrAction},
command::{Action, ActionTarget, Command, CommandOrAction},
key::{Key, KeySequence},
};
use anyhow::Result;
Expand All @@ -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,
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
92 changes: 79 additions & 13 deletions spotify_player/src/event/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 => {
Expand All @@ -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)?;
Expand All @@ -165,6 +178,7 @@ pub fn handle_action_in_context(
},
ListState::default(),
));
Ok(true)
}
Action::ToggleLiked => {
if data.user_data.is_liked_track(&track) {
Expand All @@ -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();
Expand All @@ -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());
Expand All @@ -202,7 +223,9 @@ pub fn handle_action_in_context(
)),
ListState::default(),
));
return Ok(true);
}
Ok(false)
}
Action::DeleteFromPlaylist => {
if let PageState::Context {
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading