8000 Add actions to keymap configuration by juliamertz · Pull Request #471 · aome510/spotify-player · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add actions to keymap configuration #471

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

Merged
merged 14 commits into from
Jun 23, 2024
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ A Spotify Premium account is **required**.
- [Rust and cargo](https://www.rust-lang.org/tools/install) as the build dependencies
- install `openssl`, `alsa-lib` (`streaming` feature), `libdbus` (`media-control` feature).
- For example, on Debian based systems, run the below command to install application's dependencies:

```shell
sudo apt install libssl-dev libasound2-dev libdbus-1-dev
```
Expand Down Expand Up @@ -372,10 +373,28 @@ To add new shortcuts or modify the default shortcuts, please refer to the [keyma

### Actions

A list of actions is available for each type of Spotify item (track, album, artist, or playlist).
For example, the list of available actions on a track is `[GoToAlbum, GoToArtist, GoToTrackRadio, GoToArtistRadio, GoToAlbumRadio, AddToPlaylist, DeleteFromCurrentPlaylist, AddToLikedTracks, DeleteFromLikedTracks]`.

To get the list of actions on an item, call the `ShowActionsOnCurrentTrack` command or `ShowActionsOnSelectedItem` command, then press enter (default binding for `ChooseSelected` command) to initiate the selected action.
A general list of actions is available; however, not all Spotify items (track, album, artist, or playlist) implement each action. To get the list of available actions on an item, call the `ShowActionsOnCurrentTrack` command or the `ShowActionsOnSelectedItem` command, then press enter (default binding for the `ChooseSelected` command) to initiate the selected action. Some actions may not appear in the popup but can be bound to a shortcut.

List of available actions:

- `GoToArtist`
- `GoToAlbum`
- `GoToRadio`
- `AddToLibrary`
- `AddToPlaylist`
- `AddToQueue`
- `AddToLiked`
- `DeleteFromLiked`
- `DeleteFromLibrary`
- `DeleteFromPlaylist`
- `ShowActionsOnAlbum`
- `ShowActionsOnArtist`
- `ToggleLiked`
- `CopyLink`
- `Follow`
- `Unfollow`

These actions can also be bound to a shortcut. To add new shortcuts, please refer to the [actions section](docs/config.md#actions) in the configuration documentation.

### Search Page

Expand Down
17 changes: 17 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,20 @@ key_sequence = "M-enter"
command = "None"
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,
a list of actions can be found [here](../README.md#actions).

```toml
[[actions]]
action = "GoToArtist"
key_sequence = "g A"
[[actions]]
action = "GoToAlbum"
key_sequence = "g B"
[[actions]]
action="ToggleLiked"
key_sequence="C-l"
```
149 changes: 85 additions & 64 deletions spotify_player/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,117 +78,138 @@ pub enum Command {
CreatePlaylist,
}

#[derive(Debug, Copy, Clone)]
pub enum TrackAction {
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum Action {
GoToArtist,
GoToAlbum,
GoToTrackRadio,
GoToRadio,
AddToLibrary,
AddToPlaylist,
AddToQueue,
AddToLiked,
DeleteFromLiked,
DeleteFromLibrary,
DeleteFromPlaylist,
ShowActionsOnAlbum,
ShowActionsOnArtist,
AddToQueue,
AddToPlaylist,
DeleteFromCurrentPlaylist,
AddToLikedTracks,
DeleteFromLikedTracks,
CopyTrackLink,
ToggleLiked,
CopyLink,
Follow,
Unfollow,
}

#[derive(Debug, Copy, Clone)]
pub enum AlbumAction {
GoToArtist,
GoToAlbumRadio,
ShowActionsOnArtist,
AddToLibrary,
DeleteFromLibrary,
CopyAlbumLink,
AddToQueue,
#[derive(Debug)]
pub enum ActionContext {
Track(Track),
Album(Album),
Artist(Artist),
Playlist(Playlist),
}

#[derive(Debug, Copy, Clone)]
pub enum ArtistAction {
GoToArtistRadio,
Follow,
Unfollow,
CopyArtistLink,
pub enum CommandOrAction {
Command(Command),
Action(Action),
}

#[derive(Debug, Copy, Clone)]
pub enum PlaylistAction {
GoToPlaylistRadio,
AddToLibrary,
DeleteFromLibrary,
CopyPlaylistLink,
impl From<Track> for ActionContext {
fn from(v: Track) -> Self {
Self::Track(v)
}
}

impl From<Artist> for ActionContext {
fn from(v: Artist) -> Self {
Self::Artist(v)
}
}

impl From<Album> for ActionContext {
fn from(v: Album) -> Self {
Self::Album(v)
}
}

impl From<Playlist> for ActionContext {
fn from(v: Playlist) -> Self {
Self::Playlist(v)
}
}

impl ActionContext {
pub fn get_available_actions(&self, data: &DataReadGuard) -> Vec<Action> {
match self {
Self::Track(track) => construct_track_actions(track, data),
Self::Album(album) => construct_album_actions(album, data),
Self::Artist(artist) => construct_artist_actions(artist, data),
Self::Playlist(playlist) => construct_playlist_actions(playlist, data),
}
}
}

/// constructs a list of actions on a track
pub fn construct_track_actions(track: &Track, data: &DataReadGuard) -> Vec<TrackAction> {
pub fn construct_track_actions(track: &Track, data: &DataReadGuard) -> Vec<Action> {
let mut actions = vec![
TrackAction::GoToArtist,
TrackAction::GoToAlbum,
TrackAction::GoToTrackRadio,
TrackAction::ShowActionsOnAlbum,
TrackAction::ShowActionsOnArtist,
TrackAction::CopyTrackLink,
TrackAction::AddToPlaylist,
TrackAction::AddToQueue,
Action::GoToArtist,
Action::GoToAlbum,
Action::GoToRadio,
Action::ShowActionsOnAlbum,
Action::ShowActionsOnArtist,
Action::CopyLink,
Action::AddToPlaylist,
Action::AddToQueue,
];

// check if the track is a liked track
if data.user_data.is_liked_track(track) {
actions.push(TrackAction::DeleteFromLikedTracks);
actions.push(Action::AddToLiked);
} else {
actions.push(TrackAction::AddToLikedTracks);
actions.push(Action::DeleteFromLiked);
}

actions
}

/// constructs a list of actions on an album
pub fn construct_album_actions(album: &Album, data: &DataReadGuard) -> Vec<AlbumAction> {
pub fn construct_album_actions(album: &Album, data: &DataReadGuard) -> Vec<Action> {
let mut actions = vec![
AlbumAction::GoToArtist,
AlbumAction::GoToAlbumRadio,
AlbumAction::ShowActionsOnArtist,
AlbumAction::CopyAlbumLink,
AlbumAction::AddToQueue,
Action::GoToArtist,
Action::GoToRadio,
Action::ShowActionsOnArtist,
Action::CopyLink,
Action::AddToQueue,
];
if data.user_data.saved_albums.iter().any(|a| a.id == album.id) {
actions.push(AlbumAction::DeleteFromLibrary);
actions.push(Action::DeleteFromLibrary);
} else {
actions.push(AlbumAction::AddToLibrary);
actions.push(Action::AddToLibrary);
}
actions
}

/// constructs a list of actions on an artist
pub fn construct_artist_actions(artist: &Artist, data: &DataReadGuard) -> Vec<ArtistAction> {
let mut actions = vec![ArtistAction::GoToArtistRadio, ArtistAction::CopyArtistLink];
pub fn construct_artist_actions(artist: &Artist, data: &DataReadGuard) -> Vec<Action> {
let mut actions = vec![Action::GoToRadio, Action::CopyLink];

if data
.user_data
.followed_artists
.iter()
.any(|a| a.id == artist.id)
{
actions.push(ArtistAction::Unfollow);
actions.push(Action::Unfollow);
} else {
actions.push(ArtistAction::Follow);
actions.push(Action::Follow);
}
actions
}

/// constructs a list of actions on an playlist
pub fn construct_playlist_actions(
playlist: &Playlist,
data: &DataReadGuard,
) -> Vec<PlaylistAction> {
let mut actions = vec![
PlaylistAction::GoToPlaylistRadio,
PlaylistAction::CopyPlaylistLink,
];
pub fn construct_playlist_actions(playlist: &Playlist, data: &DataReadGuard) -> Vec<Action> {
let mut actions = vec![Action::GoToRadio, Action::CopyLink];

if data.user_data.playlists.iter().any(|a| a.id == playlist.id) {
actions.push(PlaylistAction::DeleteFromLibrary);
actions.push(Action::DeleteFromLibrary);
} else {
actions.push(PlaylistAction::AddToLibrary);
actions.push(Action::AddToLibrary);
}
actions
}
Expand Down
66 changes: 62 additions & 4 deletions spotify_player/src/config/keymap.rs
F438
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
command::Command,
command::{Action, Command, CommandOrAction},
key::{Key, KeySequence},
};
use anyhow::Result;
Expand All @@ -10,6 +10,8 @@ use serde::Deserialize;
pub struct KeymapConfig {
#[serde(default)]
pub keymaps: Vec<Keymap>,
#[serde(default)]
pub actions: Vec<ActionMap>,
}

#[derive(Clone, Debug, Deserialize)]
Expand All @@ -19,9 +21,17 @@ pub struct Keymap {
pub command: Command,
}

#[derive(Clone, Debug, Deserialize)]
/// A keymap that triggers an `Action` when a key sequence is pressed
pub struct ActionMap {
pub key_sequence: KeySequence,
pub action: Action,
}

impl Default for KeymapConfig {
fn default() -> Self {
KeymapConfig {
actions: vec![],
keymaps: vec![
Keymap {
key_sequence: "n".into(),
Expand Down Expand Up @@ -325,12 +335,14 @@ impl KeymapConfig {
);
}
Ok(content) => {
let mut keymaps = toml::from_str::<Self>(&content)?.keymaps;
std::mem::swap(&mut self.keymaps, &mut keymaps);
let mut parsed = toml::from_str::<Self>(&content)?;
std::mem::swap(&mut self.keymaps, &mut parsed.keymaps);
std::mem::swap(&mut self.actions, &mut parsed.actions);

// a dumb approach (with quadratic complexity) to merge two different keymap arrays
// while keeping the invariant:
// - each `KeySequence` is mapped to only one `Command`.
keymaps.into_iter().for_each(|keymap| {
parsed.keymaps.into_iter().for_each(|keymap| {
if !self
.keymaps
.iter()
Expand All @@ -339,6 +351,15 @@ impl KeymapConfig {
self.keymaps.push(keymap);
}
});
parsed.actions.into_iter().for_each(|action| {
if !self
.actions
.iter()
.any(|k| k.key_sequence == action.key_sequence)
{
self.actions.push(action);
}
});
}
}
Ok(())
Expand All @@ -352,13 +373,50 @@ impl KeymapConfig {
.collect()
}

/// finds all actions whose mapped key sequence has a given `prefix` key sequence as its prefix
pub fn find_matched_prefix_actions(&self, prefix: &KeySequence) -> Vec<&ActionMap> {
self.actions
.iter()
.filter(|&action| prefix.is_prefix(&action.key_sequence))
.collect()
}

/// checks if there is any command or action that has a given `prefix` key sequence as its prefix
pub fn has_matched_prefix(&self, prefix: &KeySequence) -> bool {
let keymaps = self.find_matched_prefix_keymaps(prefix);
let actions = self.find_matched_prefix_actions(prefix);
!keymaps.is_empty() || !actions.is_empty()
}

/// finds a command from a mapped key sequence
pub fn find_command_from_key_sequence(&self, key_sequence: &KeySequence) -> Option<Command> {
self.keymaps
.iter()
.find(|&keymap| keymap.key_sequence == *key_sequence)
.map(|keymap| keymap.command)
}

/// finds an action from a mapped key sequence
pub fn find_action_from_key_sequence(&self, key_sequence: &KeySequence) -> Option<Action> {
self.actions
.iter()
.find(|&action| action.key_sequence == *key_sequence)
.map(|action| action.action)
}

/// finds a command or action from a mapped key sequence
pub fn find_command_or_action_from_key_sequence(
&self,
key_sequence: &KeySequence,
) -> Option<CommandOrAction> {
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));
}
None
}
}

impl Keymap {
Expand Down
Loading
Loading
0