Skip to content

Commit 0908d2e

Browse files
feat(cli): implement tab-completion for ids (#312)
tab-completion for files is still kinda misbehaving, it's not handling `~` correctly
2 parents fe8423a + e0685e7 commit 0908d2e

File tree

26 files changed

+640
-184
lines changed

26 files changed

+640
-184
lines changed

cli/src/handlers/complete.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! This module provides functionality for generating completion candidates for
2+
//! things like the import/export commands in the CLI.
3+
4+
use clap::builder::StyledStr;
5+
use clap_complete::CompletionCandidate;
6+
use mecomp_core::rpc::MusicPlayerClient;
7+
use mecomp_storage::db::schemas::dynamic::query::Compile;
8+
9+
#[derive(Debug, PartialEq, Eq)]
10+
pub enum CompletableTable {
11+
Artist,
12+
Album,
13+
Song,
14+
Playlist,
15+
DynamicPlaylist,
16+
Collection,
17+
}
18+
19+
/// Generate completion candidates for items in the database,
20+
/// Given the table name, returns a function that can be used to get completion candidates
21+
/// from that table.
22+
pub fn complete_things(table: CompletableTable) -> impl Fn() -> Vec<CompletionCandidate> {
23+
move || {
24+
let rt = tokio::runtime::Builder::new_multi_thread()
25+
.enable_all()
26+
.build()
27+
.expect("Failed to create Tokio runtime");
28+
29+
let g = rt.enter();
30+
31+
let handle = tokio::runtime::Handle::current();
32+
33+
let client: MusicPlayerClient = match handle.block_on(mecomp_core::rpc::init_client(6600)) {
34+
Ok(client) => client,
35+
Err(e) => {
36+
eprintln!("Failed to connect to daemon: {e}");
37+
return vec![];
38+
}
39+
};
40+
let ctx = tarpc::context::current();
41+
42+
let candidates = match table {
43+
CompletableTable::Song => get_song_candidates(&handle, &client, ctx),
44+
CompletableTable::Album => get_album_candidates(&handle, &client, ctx),
45+
CompletableTable::Artist => get_artist_candidates(&handle, &client, ctx),
46+
CompletableTable::Playlist => get_playlist_candidates(&handle, &client, ctx),
47+
CompletableTable::DynamicPlaylist => {
48+
get_dynamic_playlist_candidates(&handle, &client, ctx)
49+
}
50+
CompletableTable::Collection => get_collection_candidates(&handle, &client, ctx),
51+
};
52+
drop(g); // Exit the Tokio runtime context
53+
54+
candidates
55+
.iter()
56+
.cloned()
57+
.map(|(id, help)| CompletionCandidate::new(id).help(Some(help)))
58+
.collect::<Vec<_>>()
59+
}
60+
}
61+
62+
fn get_song_candidates(
63+
rt: &tokio::runtime::Handle,
64+
client: &MusicPlayerClient,
65+
ctx: tarpc::context::Context,
66+
) -> Vec<(String, StyledStr)> {
67+
let response = rt.block_on(client.library_songs_brief(ctx));
68+
if let Err(e) = response {
69+
eprintln!("Failed to fetch songs: {e}");
70+
return vec![];
71+
}
72+
let songs = response.unwrap().unwrap_or_default();
73+
74+
songs
75+
.into_iter()
76+
.map(|song| {
77+
(
78+
song.id.key().to_string(),
79+
StyledStr::from(format!(
80+
"\"{}\" (by: {:?}, album: {})",
81+
song.title, song.artist, song.album
82+
)),
83+
)
84+
})
85+
.collect()
86+
}
87+
88+
fn get_album_candidates(
89+
rt: &tokio::runtime::Handle,
90+
client: &MusicPlayerClient,
91+
ctx: tarpc::context::Context,
92+
) -> Vec<(String, StyledStr)> {
93+
let response = rt.block_on(client.library_albums_brief(ctx));
94+
if let Err(e) = response {
95+
eprintln!("Failed to fetch albums: {e}");
96+
return vec![];
97+
}
98+
let albums = response.unwrap().unwrap_or_default();
99+
100+
albums
101+
.into_iter()
102+
.map(|album| {
103+
(
104+
album.id.key().to_string(),
105+
StyledStr::from(format!(
106+
"\"{}\" (by: {:?}, {} songs)",
107+
album.title, album.artist, album.song_count
108+
)),
109+
)
110+
})
111+
.collect()
112+
}
113+
114+
fn get_artist_candidates(
115+
rt: &tokio::runtime::Handle,
116+
client: &MusicPlayerClient,
117+
ctx: tarpc::context::Context,
118+
) -> Vec<(String, StyledStr)> {
119+
let response = rt.block_on(client.library_artists_brief(ctx));
120+
if let Err(e) = response {
121+
eprintln!("Failed to fetch artists: {e}");
122+
return vec![];
123+
}
124+
let artists = response.unwrap().unwrap_or_default();
125+
126+
artists
127+
.into_iter()
128+
.map(|artist| {
129+
(
130+
artist.id.key().to_string(),
131+
StyledStr::from(format!(
132+
"\"{}\" ({} albums, {} songs)",
133+
artist.name, artist.albums, artist.songs
134+
)),
135+
)
136+
})
137+
.collect()
138+
}
139+
140+
fn get_playlist_candidates(
141+
rt: &tokio::runtime::Handle,
142+
client: &MusicPlayerClient,
143+
ctx: tarpc::context::Context,
144+
) -> Vec<(String, StyledStr)> {
145+
let response = rt.block_on(client.playlist_list(ctx));
146+
if let Err(e) = response {
147+
eprintln!("Failed to fetch playlists: {e}");
148+
return vec![];
149+
}
150+
let playlists = response.unwrap();
151+
152+
playlists
153+
.into_iter()
154+
.map(|playlist| {
155+
(
156+
playlist.id.key().to_string(),
157+
StyledStr::from(format!(
158+
"\"{}\" ({} songs, {:?})",
159+
playlist.name, playlist.songs, playlist.runtime
160+
)),
161+
)
162+
})
163+
.collect()
164+
}
165+
166+
fn get_dynamic_playlist_candidates(
167+
rt: &tokio::runtime::Handle,
168+
client: &MusicPlayerClient,
169+
ctx: tarpc::context::Context,
170+
) -> Vec<(String, StyledStr)> {
171+
let response = rt.block_on(client.dynamic_playlist_list(ctx));
172+
if let Err(e) = response {
173+
eprintln!("Failed to fetch dynamic playlists: {e}");
174+
return vec![];
175+
}
176+
let playlists = response.unwrap();
177+
178+
playlists
179+
.into_iter()
180+
.map(|playlist| {
181+
(
182+
playlist.id.key().to_string(),
183+
StyledStr::from(format!(
184+
"\"{}\" ({})",
185+
playlist.name,
186+
playlist.query.compile_for_storage()
187+
)),
188+
)
189+
})
190+
.collect()
191+
}
192+
193+
fn get_collection_candidates(
194+
rt: &tokio::runtime::Handle,
195+
client: &MusicPlayerClient,
196+
ctx: tarpc::context::Context,
197+
) -> Vec<(String, StyledStr)> {
198+
let response = rt.block_on(client.collection_list(ctx));
199+
if let Err(e) = response {
200+
eprintln!("Failed to fetch collections: {e}");
201+
return vec![];
202+
}
203+
let collections = response.unwrap();
204+
205+
collections
206+
.into_iter()
207+
.map(|collection| {
208+
(
209+
collection.id.key().to_string(),
210+
StyledStr::from(format!(
211+
"\"{}\" ({} songs, {:?})",
212+
collection.name, collection.songs, collection.runtime
213+
)),
214+
)
215+
})
216+
.collect()
217+
}

cli/src/handlers/implementations.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
use crate::handlers::{printing, utils};
77

88
use super::{
9-
Command, CommandHandler, CurrentTarget, LibraryCommand, LibraryGetTarget, LibraryListTarget,
9+
Command, CommandHandler, CurrentTarget, LibraryCommand, LibraryGetCommand, LibraryListTarget,
1010
PlaylistGetMethod, QueueAddTarget, QueueCommand, RandTarget, SearchTarget, SeekCommand,
1111
VolumeCommand,
1212
};
@@ -240,12 +240,37 @@ impl CommandHandler for LibraryCommand {
240240
printing::song_list("Songs", &resp, false)?
241241
)?;
242242
}
243+
LibraryListTarget::Playlists => {
244+
let resp: Box<[PlaylistBrief]> = client.playlist_list(ctx).await?;
245+
writeln!(
246+
stdout,
247+
"Daemon response:\n{}",
248+
printing::playlist_brief_list("Playlists", &resp)?
249+
)?;
250+
}
251+
LibraryListTarget::DynamicPlaylists => {
252+
let resp: Box<[DynamicPlaylist]> =
253+
client.dynamic_playlist_list(ctx).await?;
254+
writeln!(
255+
stdout,
256+
"Daemon response:\n{}",
257+
printing::dynamic_playlist_list("Dynamic Playlists", &resp)?
258+
)?;
259+
}
260+
LibraryListTarget::Collections => {
261+
let resp: Box<[CollectionBrief]> = client.collection_list(ctx).await?;
262+
writeln!(
263+
stdout,
264+
"Daemon response:\n{}",
265+
printing::collection_list("Collections", &resp)?
266+
)?;
267+
}
243268
}
244269
Ok(())
245270
}
246-
Self::Get { target, id } => {
247-
match target {
248-
LibraryGetTarget::Artist => {
271+
Self::Get { command } => {
272+
match command {
273+
LibraryGetCommand::Artist { id } => {
249274
let resp: Option<Artist> = client
250275
.library_artist_get(
251276
ctx,
@@ -257,7 +282,7 @@ impl CommandHandler for LibraryCommand {
257282
.await?;
258283
writeln!(stdout, "Daemon response:\n{resp:#?}")?;
259284
}
260-
LibraryGetTarget::Album => {
285+
LibraryGetCommand::Album { id } => {
261286
let resp: Option<Album> = client
262287
.library_album_get(
263288
ctx,
@@ -269,7 +294,7 @@ impl CommandHandler for LibraryCommand {
269294
.await?;
270295
writeln!(stdout, "Daemon response:\n{resp:#?}")?;
271296
}
272-
LibraryGetTarget::Song => {
297+
LibraryGetCommand::Song { id } => {
273298
let resp: Option<Song> = client
274299
.library_song_get(
275300
ctx,
@@ -281,7 +306,7 @@ impl CommandHandler for LibraryCommand {
281306
.await?;
282307
writeln!(stdout, "Daemon response:\n{resp:#?}")?;
283308
}
284-
LibraryGetTarget::Playlist => {
309+
LibraryGetCommand::Playlist { id } => {
285310
let resp: Option<Playlist> = client
286311
.playlist_get(
287312
ctx,
@@ -293,6 +318,30 @@ impl CommandHandler for LibraryCommand {
293318
.await?;
294319
writeln!(stdout, "Daemon response:\n{resp:#?}")?;
295320
}
321+
LibraryGetCommand::Dynamic { id } => {
322+
let resp: Option<DynamicPlaylist> = client
323+
.dynamic_playlist_get(
324+
ctx,
325+
RecordId {
326+
tb: dynamic::TABLE_NAME.to_owned(),
327+
id: Id::String(id.to_owned()),
328+
},
329+
)
330+
.await?;
331+
writeln!(stdout, "Daemon response:\n{resp:#?}")?;
332+
}
333+
LibraryGetCommand::Collection { id } => {
334+
let resp: Option<Collection> = client
335+
.collection_get(
336+
ctx,
337+
RecordId {
338+
tb: collection::TABLE_NAME.to_owned(),
339+
id: Id::String(id.to_owned()),
340+
},
341+
)
342+
.await?;
343+
writeln!(stdout, "Daemon response:\n{resp:#?}")?;
344+
}
296345
}
297346
Ok(())
298347
}
@@ -1066,7 +1115,7 @@ impl CommandHandler for super::CollectionCommand {
10661115
writeln!(
10671116
stdout,
10681117
"Daemon response:\n{}",
1069-
printing::playlist_collection_list("Collections", &resp)?
1118+
printing::collection_list("Collections", &resp)?
10701119
)?;
10711120
Ok(())
10721121
}

0 commit comments

Comments
 (0)