From 8620fcd4ec83d126cc3ba4f8b7b1244a35bce060 Mon Sep 17 00:00:00 2001 From: Mark <> Date: Sat, 20 Jan 2024 01:19:19 +0100 Subject: [PATCH] add some more functions to -mers and add hooks --- musicdb-client/Cargo.toml | 2 +- musicdb-client/mers/cli.mers | 289 +++++ musicdb-client/mers/log.mers | 22 + musicdb-client/{ => mers}/sleep_timer.mers | 0 musicdb-client/src/gui.rs | 14 +- musicdb-client/src/main.rs | 16 +- musicdb-client/src/merscfg.rs | 8 +- musicdb-mers/src/lib.rs | 1114 +++++++++++++------- 8 files changed, 1079 insertions(+), 386 deletions(-) create mode 100644 musicdb-client/mers/cli.mers create mode 100644 musicdb-client/mers/log.mers rename musicdb-client/{ => mers}/sleep_timer.mers (100%) diff --git a/musicdb-client/Cargo.toml b/musicdb-client/Cargo.toml index fd9f302..0b251fe 100755 --- a/musicdb-client/Cargo.toml +++ b/musicdb-client/Cargo.toml @@ -16,7 +16,7 @@ mers_lib = { version = "0.3.2", optional = true } musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true } [features] -default = ["gui"] +default = ["gui", "mers", "merscfg"] # gui: # enables the gui mode # merscfg: diff --git a/musicdb-client/mers/cli.mers b/musicdb-client/mers/cli.mers new file mode 100644 index 0000000..1ab63d2 --- /dev/null +++ b/musicdb-client/mers/cli.mers @@ -0,0 +1,289 @@ +gen_escape_str := str -> { + "echo".run_command(("-ne", str)).try(( + (s, stdout, stderr) -> if s.eq(0) stdout else "" + e -> "" + )) +} +gen_color := mode -> ("\\x1b[", mode, "m").concat.gen_escape_str + +// 0: reset | 1: bold, 2: dim, 3: italic, 4: underline, 9: strikethrough +// Colors: FG/BG=3x/4x (bright, if supported: 9x/10x) | 1red 2green 3yellow 4blue 5magenta 6cyan 7white 9default +clr_reset := "0".gen_color +clr_red := "31".gen_color +clr_green := "32".gen_color +clr_yellow := "33".gen_color +clr_blue := "34".gen_color +clr_magenta := "35".gen_color +clr_cyan := "36".gen_color +clr_white := "37".gen_color +clr_default := "39".gen_color +clr_dim := "2".gen_color + +clr_top_bar_decorations := "2;34" // dim blue +clr_top_bar_title := "1;37".gen_color // bold white +clr_top_bar_err := "1;31".gen_color // bold red +clr_top_bar_empty := clr_dim // dim +clr_artist := "35".gen_color // magenta +clr_album := "32".gen_color // green +clr_user_input_line_decor := "2;36".gen_color // dim cyan +clr_user_input_line := "1;36".gen_color // bold cyan +clr_unknown_cmd := "31".gen_color // red +clr_buf_timestamp := "2;34".gen_color // dim blue +clr_search := "1;32".gen_color // dim green + +// clear terminal and reset cursor and colors +"\\x1b[2J\\x1b[H\\x1b[0m".gen_escape_str.eprint +// save cursor, reset cursor and color +term_before_redraw := "\\x1b[s\\x1b[H\\x1b[0m".gen_escape_str +// restore cursor pos +term_put_cursor_back := "\\x1b[u".gen_escape_str +// erase rest of line in case previous version of this line was longer than new version, then add newline +term_clear_rest_of_screen := "\\x1b[0J".gen_escape_str +line_end := ("\\x1b[0K".gen_escape_str, clr_reset, "\n").concat + +str_repeat := (str, count) -> { + out := "" + ().loop(() -> if count.gt(0) { + &count = count.subtract(1) + &out = (out, str).concat + } else (())) + out +} + +current_queue_index := [List] ().as_list + +reset_cursor_pos := true + +term_buf_len := 10 +term_buf := ("Welcome! Type `help` for help.").as_list + +bprintln := line -> { + time := "date".run_command(("+%T")).try(( + (s, stdout, stderr) -> if s.eq(0) stdout.trim else "" + e -> "" + )) + &term_buf.push((clr_buf_timestamp, time, " | ", clr_reset, line).concat) + // remove start of term_buf if it gets too long + if term_buf.len.gt(term_buf_len.product(2)) { + &term_buf = term_buf.enumerate.filter_map((i, v) -> if i.lt(term_buf_len) () else (v)).as_list + } +} + +custom_input_handler := [()/(String, String, List)] () + +update_ui := () -> { + screen := term_before_redraw + sprintln := line -> &screen = (screen, line, line_end).concat + // top bar + (top_bar_line_1, top_bar_line_2) := ().queue_get_current_song.try(( + () -> ((clr_top_bar_empty, "[-]").concat, "") + id -> id.get_song.try(( + () -> ((clr_top_bar_err, "[!]").concat, "") + {id: _, title: title, album: album, artist: artist, cover: _} -> { + l1 := (clr_top_bar_title, title).concat + artist := artist.get_artist + album := album.try_allow_unused(( + () -> () + id -> album.get_album + )) + l2 := (artist, album).try_allow_unused(( + ((), ()) -> "" + ({id: _, name: artist_name, cover: _, albums: _, singles: _}, ()) -> (clr_dim, "by ", clr_artist, artist_name).concat + ((), {id: _, name: album_name, artist: _, cover: _, songs: _}) -> (clr_dim, "on ", clr_album, album_name).concat + ({id: _, name: artist_name, cover: _, albums: _, singles: _}, {id: _, name: album_name, artist: _, cover: _, songs: _}) -> (clr_dim, "by ", clr_reset, clr_artist, artist_name, clr_reset, clr_dim, " on ", clr_reset, clr_album, album_name).concat + )) + (l1, l2) + } + )) + )) + (if ().get_playing "⏵" else "⏸", " ", top_bar_line_1).concat.sprintln + top_bar_line_2.sprintln + + // term buf + { + buf_ln := 0 + ().loop(() -> if buf_ln.lt(term_buf_len){ + term_buf.get(term_buf.len.subtract(term_buf_len).sum(buf_ln)).try(( + () -> "".sprintln + (line) -> line.sprintln + )) + &buf_ln = buf_ln.sum(1) + } else (())) + } + + // user input line + user_input_line := (clr_user_input_line_decor, custom_input_handler.try((() -> if current_queue_index.len.gt(0) ">> " else " > ", (_, v, _) -> v)), clr_user_input_line).concat + + // print screen + ( + screen, user_input_line, + if reset_cursor_pos term_clear_rest_of_screen else term_put_cursor_back + ).concat.eprint + &reset_cursor_pos = false +} + +if false ().update_ui // check + +show_queue_recursive := max_depth_layers -> { + stack := ((current_queue_index, -1, false)).as_list + ().loop(() -> { + &stack.pop.try(( + () -> (()) + ((index, depth, show)) -> if (max_depth_layers.eq(0), depth.lt(max_depth_layers)).any { + index := [List] index + line := if show { + o := "" + i := 0 + ().loop(() -> if i.lt(depth) { &o = (o, " ").concat, &i = i.sum(1) } else (())) + o + } else "" + if show &line = (clr_dim, line).concat + index.queue_get_elem.try(( + () -> () + {enabled: _, song: id} -> id.get_song.try(( + () -> &line = (line, clr_reset, "[!] ", id).concat + {id: _, title: title, album: _, artist: _, cover: _} -> &line = (line, clr_reset, title).concat + )) + {enabled: _, loop: {total: total, done: done}} -> { + index := index + &index.push(0) + &stack.push((index, depth, false)) + &line = (line, "Loop").concat + } + {enabled: _, random: ()} -> { + &line = (line, "Random").concat + index := index + &index.push(0) + &stack.push((index, depth.sum(1), true)) + } + {enabled: _, shuffle: ()} -> { + &line = (line, "Shuffle").concat + index := index + &index.push(0) + &stack.push((index, depth, false)) + } + {enabled: _, folder: {index: ix, length: length, name: name}} -> { + &line = (line, "[", name, "]").concat + i := length + ().loop(() -> { + &i = i.subtract(1) + if i.lt(0) (()) else { + index := index + &index.push(i) + &stack.push((index, depth.sum(1), true)) + } + }) + } + )) + if show line.bprintln + } + )) + }) +} + +// handlers +on_resume := () -> ().update_ui +on_pause := () -> ().update_ui +on_next_song := () -> ().update_ui +on_library_changed := () -> ().update_ui +on_queue_changed := () -> ().update_ui +on_notification_received := (title, content) -> { + ("Notification: ", title).concat.bprintln + content.bprintln + ().update_ui +} +// add as handlers +on_resume.handle_event_resume +on_pause.handle_event_pause +on_next_song.handle_event_next_song +on_library_changed.handle_event_library_changed +on_queue_changed.handle_event_queue_changed +on_notification_received.handle_event_notification_received + +&found_songs := [List] ().as_list + +().loop(() -> { + ().update_ui + line := ().read_line + exit := line.len.eq(0) + line := line.trim + &reset_cursor_pos = true + custom_input_handler.try(( + () -> { + if (exit, line.eq("exit")).any { + (()) + } else if line.eq("help") { + "===== Help =====".bprintln + "Commands:".bprintln + "- exit, pause, play".bprintln + "- next, search, add song, clear queue, queue random, queue show".bprintln + "- send notif, set buf len, clear".bprintln + } else if line.eq("pause") { + ().pause + } else if line.eq("play") { + ().resume + } else if line.eq("next") { + ().next_song + } else if line.eq("clear") { + &term_buf = ().as_list + } else if line.eq("queue show") { + "== Queue ==".bprintln + 0.show_queue_recursive + } else if line.eq("clear queue") { + ().queue_clear + } else if line.eq("queue random") { + ().queue_clear + (().as_list, 0).queue_add_loop + (0, 0).as_list.queue_add_random + } else if line.eq("send notif") { + &custom_input_handler = ("SN", "Notification: ", ().as_list) + } else if line.eq("set buf len") { + &custom_input_handler = ("SetBufLen", "Length (lines): ", ().as_list) + } else if line.eq("search") { + &custom_input_handler = ("Search", "Song: ", ().as_list) + } else if line.eq("add song") { + if found_songs.len.gt(0) { + &custom_input_handler = ("AddSong", ("(1-", found_songs.len, ") ").concat, ().as_list) + } else { + "Use `search` to find songs first!".bprintln + } + } else if line.len.gt(0) { + (clr_unknown_cmd, "Unknown command: ", line).concat.bprintln + } + } + (id, desc, args) -> { + &custom_input_handler = () + args := [List] args + if id.eq("SN") { + line.send_server_notification + } else if id.eq("SetBufLen") { + line.parse_int.try(( + () -> (clr_unknown_cmd, "not an int").concat.bprintln + n -> &term_buf_len = n, + )) + } else if id.eq("Search") { + songs := ().all_songs.filter_map({id: id, title: title, album: _, artist: _, cover: _} -> title.index_of(line).try((() -> (), n -> ((id, title))))).take(term_buf_len) + if (songs.len.gt(0), songs.len.eq(term_buf_len)).all { + &songs = songs.take(term_buf_len.subtract(1)) + (clr_search, "Search: ", clr_reset, line, clr_dim, " (found more results than will be displayed)").concat.bprintln + } else { + (clr_search, "Search: ", clr_reset, line).concat.bprintln + } + &found_songs = songs.map((id, _) -> id).as_list + songs.enumerate.for_each((i, (_, song)) -> (clr_dim, i.sum(1), " ", clr_reset, song).concat.bprintln) + } else if id.eq("AddSong") { + line.parse_int.try(( + () -> (clr_unknown_cmd, "not an int (must be 1-", found_songs.len, ")").concat.bprintln + n -> found_songs.get(n.subtract(1)).try(( + () -> (clr_unknown_cmd, "out of range (must be 1-", found_songs.len, ")").concat.bprintln + (id) -> (current_queue_index, id).queue_add_song + )) + )) + } else { + ("Unknown CIH ID ", id, ".").concat.eprintln + 1.panic + } + } +}) + +clr_reset.eprintln diff --git a/musicdb-client/mers/log.mers b/musicdb-client/mers/log.mers new file mode 100644 index 0000000..45000ec --- /dev/null +++ b/musicdb-client/mers/log.mers @@ -0,0 +1,22 @@ +// define handlers +on_resume := () -> "Resumed".eprintln +on_pause := () -> "Paused".eprintln +on_next_song := () -> "Next song".eprintln +on_library_changed := () -> "Library changed".eprintln +on_queue_changed := () -> "Queue changed".eprintln +on_notification_received := (title, content) -> ("Notif:\n - ", title, " -\n", content).concat.eprintln + +// use handlers to handle events +on_resume.handle_event_resume +on_pause.handle_event_pause +on_next_song.handle_event_next_song +on_library_changed.handle_event_library_changed +on_queue_changed.handle_event_queue_changed +on_notification_received.handle_event_notification_received + +// because on_resume won't be called if playback was resumed before this client connected +if ().get_playing { + "Resumed (was playing)".eprintln +} + +().loop(() -> 60.sleep) diff --git a/musicdb-client/sleep_timer.mers b/musicdb-client/mers/sleep_timer.mers similarity index 100% rename from musicdb-client/sleep_timer.mers rename to musicdb-client/mers/sleep_timer.mers diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index 1636f66..2141327 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -80,6 +80,7 @@ pub fn main( connection: TcpStream, get_con: get::Client, event_sender_arc: Arc>>>, + after_db_cmd: &Arc>>>, ) { let config_dir = super::get_config_file_path(); let config_file = config_dir.join("config_gui.toml"); @@ -262,6 +263,7 @@ pub fn main( #[cfg(feature = "merscfg")] merscfg: crate::merscfg::MersCfg::new(config_dir.join("dynamic_config.mers"), database), }, + after_db_cmd, )); } @@ -320,14 +322,18 @@ impl Gui { scroll_pages_multiplier: f64, #[cfg(not(feature = "merscfg"))] gui_config: GuiConfig, #[cfg(feature = "merscfg")] mut gui_config: GuiConfig, + #[cfg(feature = "merscfg")] after_db_cmd: &Arc< + Mutex>>, + >, ) -> Self { let (notif_overlay, notif_sender) = NotifOverlay::new(); let notif_sender_two = notif_sender.clone(); #[cfg(feature = "merscfg")] - match gui_config - .merscfg - .load(Arc::clone(&event_sender), notif_sender.clone()) - { + match gui_config.merscfg.load( + Arc::clone(&event_sender), + notif_sender.clone(), + after_db_cmd, + ) { Err(e) => { if !matches!(e.kind(), std::io::ErrorKind::NotFound) { eprintln!("Couldn't load merscfg: {e}") diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index 1edd4f1..b806612 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -105,7 +105,12 @@ fn main() { Arc::new(Mutex::new(None)); #[cfg(feature = "speedy2d")] let sender = Arc::clone(&update_gui_sender); + #[cfg(any(feature = "mers", feature = "merscfg"))] + let mers_after_db_updated_action: Arc< + Mutex>>, + > = Arc::new(Mutex::new(None)); let con_thread = { + let mers_after_db_updated_action = Arc::clone(&mers_after_db_updated_action); let mode = mode.clone(); let database = Arc::clone(&database); let mut con = con.try_clone().unwrap(); @@ -148,7 +153,13 @@ fn main() { if let Some(player) = &mut player { player.handle_command(&update); } - database.lock().unwrap().apply_command(update); + #[cfg(any(feature = "mers", feature = "merscfg"))] + if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() { + database.lock().unwrap().apply_command(update.clone()); + action(update); + } else { + database.lock().unwrap().apply_command(update); + } #[cfg(feature = "speedy2d")] if let Some(v) = &*update_gui_sender.lock().unwrap() { v.send_event(GuiEvent::Refresh).unwrap(); @@ -175,6 +186,8 @@ fn main() { )) .expect("initializing get client connection"), sender, + #[cfg(feature = "merscfg")] + &mers_after_db_updated_action, ) }; } @@ -191,6 +204,7 @@ fn main() { mers_lib::prelude_compile::Config::new().bundle_std(), &database, &Arc::new(move |cmd: Command| cmd.to_bytes(&mut *con.lock().unwrap()).unwrap()), + &mers_after_db_updated_action, ) .infos(); let program = mers_lib::prelude_compile::parse(&mut src, &srca) diff --git a/musicdb-client/src/merscfg.rs b/musicdb-client/src/merscfg.rs index dd243f5..66da22d 100644 --- a/musicdb-client/src/merscfg.rs +++ b/musicdb-client/src/merscfg.rs @@ -139,13 +139,14 @@ impl MersCfg { notif_sender: Sender< Box (Box, NotifInfo) + Send>, >, + after_db_cmd: &Arc>>>, ) -> mers_lib::prelude_extend_config::Config { let cmd_es = event_sender.clone(); let cmd_ga = self.channel_gui_actions.0.clone(); musicdb_mers::add(cfg, db, &Arc::new(move |cmd| { cmd_ga.send(cmd).unwrap(); cmd_es.send_event(GuiEvent::RefreshMers).unwrap(); - })) + }), after_db_cmd) .add_var_arc( "is_playing".to_owned(), Arc::clone(&self.var_is_playing), @@ -704,9 +705,10 @@ impl MersCfg { notif_sender: Sender< Box (Box, NotifInfo) + Send>, >, + after_db_cmd: &Arc>>>, ) -> std::io::Result)>, CheckError>> { let src = mers_lib::prelude_compile::Source::new_from_file(self.source_file.clone())?; - Ok(self.load2(src, event_sender, notif_sender)) + Ok(self.load2(src, event_sender, notif_sender, after_db_cmd)) } fn load2( &mut self, @@ -715,6 +717,7 @@ impl MersCfg { notif_sender: Sender< Box (Box, NotifInfo) + Send>, >, + after_db_cmd: &Arc>>>, ) -> Result)>, CheckError> { let srca = Arc::new(src.clone()); let (mut i1, mut i2, mut i3) = self @@ -723,6 +726,7 @@ impl MersCfg { &self.database, event_sender, notif_sender, + after_db_cmd, ) .infos(); let compiled = mers_lib::prelude_compile::parse(&mut src, &srca)? diff --git a/musicdb-mers/src/lib.rs b/musicdb-mers/src/lib.rs index e14862b..29b5d45 100644 --- a/musicdb-mers/src/lib.rs +++ b/musicdb-mers/src/lib.rs @@ -4,7 +4,7 @@ use std::{ }; use mers_lib::{ - data::{self, Data, MersData, MersType, Type}, + data::{self, function::Function, Data, MersData, MersType, Type}, info::Info, prelude_extend_config::Config, }; @@ -20,9 +20,10 @@ use musicdb_lib::{ }; pub fn add( - cfg: Config, + mut cfg: Config, db: &Arc>, cmd: &Arc, + after_db_cmd: &Arc>>>, ) -> Config { macro_rules! func { ($out:expr, $run:expr) => { @@ -35,407 +36,764 @@ pub fn add( }) }; } - cfg.with_list() - .add_type(MusicDbIdT.to_string(), Ok(Arc::new(MusicDbIdT))) - .add_var( - "send_server_notification".to_owned(), + /// handle commands received from server (for handler functions) + /// `T` can be used to return generated data to avoid calculating something twice if one event may call multiple handlers. + fn handle( + handler: &Arc>, + gen: impl FnOnce() -> (Data, T), + ) -> Option<(T, Data)> { + if let Some(func) = handler + .read() + .unwrap() + .get() + .as_any() + .downcast_ref::() + { + let (data, t) = gen(); + Some((t, func.run(data))) + } else { + None + } + } + let handler_resume = Arc::new(RwLock::new(Data::empty_tuple())); + let handler_pause = Arc::new(RwLock::new(Data::empty_tuple())); + let handler_next_song = Arc::new(RwLock::new(Data::empty_tuple())); + let handler_queue_changed = Arc::new(RwLock::new(Data::empty_tuple())); + let handler_library_changed = Arc::new(RwLock::new(Data::empty_tuple())); + let handler_notification_received = Arc::new(RwLock::new(Data::empty_tuple())); + { + *after_db_cmd.lock().unwrap() = Some({ + let handler_resume = Arc::clone(&handler_resume); + let handler_pause = Arc::clone(&handler_pause); + let handler_next_song = Arc::clone(&handler_next_song); + let handler_queue_changed = Arc::clone(&handler_queue_changed); + let handler_library_changed = Arc::clone(&handler_library_changed); + let handler_notification_received = Arc::clone(&handler_notification_received); + Box::new(move |cmd| match cmd { + Command::Resume => { + handle(&handler_resume, move || (Data::empty_tuple(), ())); + } + Command::Pause | Command::Stop => { + handle(&handler_pause, move || (Data::empty_tuple(), ())); + } + Command::NextSong => { + handle(&handler_next_song, move || (Data::empty_tuple(), ())); + handle(&handler_queue_changed, move || (Data::empty_tuple(), ())); + } + Command::SyncDatabase(..) => { + handle(&handler_library_changed, move || (Data::empty_tuple(), ())); + } + Command::QueueUpdate(..) + | Command::QueueAdd(..) + | Command::QueueInsert(..) + | Command::QueueRemove(..) + | Command::QueueGoto(..) + | Command::QueueSetShuffle(..) => { + handle(&handler_queue_changed, move || (Data::empty_tuple(), ())); + } + Command::AddSong(_) + | Command::AddAlbum(_) + | Command::AddArtist(_) + | Command::AddCover(_) + | Command::ModifySong(_) + | Command::ModifyAlbum(_) + | Command::ModifyArtist(_) + | Command::RemoveSong(_) + | Command::RemoveAlbum(_) + | Command::RemoveArtist(_) => { + handle(&handler_library_changed, move || (Data::empty_tuple(), ())); + } + Command::SetSongDuration(..) => { + handle(&handler_library_changed, move || (Data::empty_tuple(), ())); + } + Command::TagSongFlagSet(..) + | Command::TagSongFlagUnset(..) + | Command::TagAlbumFlagSet(..) + | Command::TagAlbumFlagUnset(..) + | Command::TagArtistFlagSet(..) + | Command::TagArtistFlagUnset(..) + | Command::TagSongPropertySet(..) + | Command::TagSongPropertyUnset(..) + | Command::TagAlbumPropertySet(..) + | Command::TagAlbumPropertyUnset(..) + | Command::TagArtistPropertySet(..) + | Command::TagArtistPropertyUnset(..) => { + handle(&handler_library_changed, move || (Data::empty_tuple(), ())); + } + Command::InitComplete => (), + Command::Save => (), + Command::ErrorInfo(title, body) => { + handle(&handler_notification_received, move || { + ( + Data::new(data::tuple::Tuple(vec![ + Data::new(data::string::String(title)), + Data::new(data::string::String(body)), + ])), + (), + ) + }); + } + }) + }); + } + // MusicDb type + cfg = cfg + .with_list() + .add_type(MusicDbIdT.to_string(), Ok(Arc::new(MusicDbIdT))); + // handler setters + for (name, handler, in_type) in [ + ("resume", handler_resume, Type::empty_tuple()), + ("pause", handler_pause, Type::empty_tuple()), + ("next_song", handler_next_song, Type::empty_tuple()), + ( + "library_changed", + handler_library_changed, + Type::empty_tuple(), + ), + ("queue_changed", handler_queue_changed, Type::empty_tuple()), + ( + "notification_received", + handler_notification_received, + Type::new(data::tuple::TupleT(vec![ + Type::new(data::string::StringT), + Type::new(data::string::StringT), + ])), + ), + ] { + cfg = cfg.add_var( + format!("handle_event_{name}"), func!( - |a, _| { - if a.is_included_in(&data::string::StringT) { + move |a, _| { + if a.types.iter().all(|a| { + Type::newm(vec![Arc::clone(a)]).is_zero_tuple() + || a.as_any() + .downcast_ref::() + .is_some_and(|a| { + (a.0)(&in_type).is_ok_and(|opt| opt.is_zero_tuple()) + }) + }) { Ok(Type::empty_tuple()) } else { - Err(format!("Function argument must be `()`.").into()) + Err(format!("Handler function must be `{in_type} -> ()`").into()) } }, - { - let cmd = Arc::clone(cmd); - move |a, _| { - cmd(Command::ErrorInfo( - String::new(), - a.get() - .as_any() - .downcast_ref::() - .unwrap() - .0 - .clone(), - )); + move |a, _| { + *handler.write().unwrap() = a; + Data::empty_tuple() + } + ), + ); + } + // actions + cfg.add_var( + "send_server_notification".to_owned(), + func!( + |a, _| { + if a.is_included_in(&data::string::StringT) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `String`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + cmd(Command::ErrorInfo( + String::new(), + a.get() + .as_any() + .downcast_ref::() + .unwrap() + .0 + .clone(), + )); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "resume".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |_, _| { + cmd(Command::Resume); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "pause".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |_, _| { + cmd(Command::Pause); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "stop_playback".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |_, _| { + cmd(Command::Stop); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "next_song".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |_, _| { + cmd(Command::NextSong); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "get_playing".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::new(data::bool::BoolT)) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let db = Arc::clone(db); + move |_, _| Data::new(data::bool::Bool(db.lock().unwrap().playing)) + } + ), + ) + .add_var( + "queue_get_current_song".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::newm(vec![ + Arc::new(MusicDbIdT), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let db = Arc::clone(db); + move |_, _| match db.lock().unwrap().queue.get_current_song() { + Some(id) => Data::new(MusicDbId(*id)), + None => Data::empty_tuple(), + } + } + ), + ) + .add_var( + "queue_get_next_song".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::newm(vec![ + Arc::new(MusicDbIdT), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let db = Arc::clone(db); + move |_, _| match db.lock().unwrap().queue.get_next_song() { + Some(id) => Data::new(MusicDbId(*id)), + None => Data::empty_tuple(), + } + } + ), + ) + .add_var( + "queue_get_elem".to_owned(), + func!( + |a, _| { + if a.is_included_in(&mers_lib::program::configs::with_list::ListT(Type::new( + data::int::IntT, + ))) { + Ok(gen_queue_elem_type()) + } else { + Err(format!("Function argument must be `List`.").into()) + } + }, + { + let db = Arc::clone(db); + move |a, _| { + let a = int_list_to_usize_vec(&a); + if let Some(elem) = db.lock().unwrap().queue.get_item_at_index(&a, 0) { + gen_queue_elem(elem) + } else { Data::empty_tuple() } } - ), - ) - .add_var( - "resume".to_owned(), - func!( - |a, _| { - if a.is_included_in(&Type::empty_tuple()) { - Ok(Type::empty_tuple()) - } else { - Err(format!("Function argument must be `()`.").into()) - } - }, - { - let cmd = Arc::clone(cmd); - move |_, _| { - cmd(Command::Resume); - Data::empty_tuple() - } + } + ), + ) + .add_var( + "queue_goto".to_owned(), + func!( + |a, _| { + if a.is_included_in(&mers_lib::program::configs::with_list::ListT(Type::new( + data::int::IntT, + ))) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `List`.").into()) } - ), - ) - .add_var( - "pause".to_owned(), - func!( - |a, _| { - if a.is_included_in(&Type::empty_tuple()) { - Ok(Type::empty_tuple()) - } else { - Err(format!("Function argument must be `()`.").into()) - } - }, - { - let cmd = Arc::clone(cmd); - move |_, _| { - cmd(Command::Pause); - Data::empty_tuple() - } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + cmd(Command::QueueGoto(int_list_to_usize_vec(&a))); + Data::empty_tuple() } - ), - ) - .add_var( - "stop_playback".to_owned(), - func!( - |a, _| { - if a.is_included_in(&Type::empty_tuple()) { - Ok(Type::empty_tuple()) - } else { - Err(format!("Function argument must be `()`.").into()) - } - }, - { - let cmd = Arc::clone(cmd); - move |_, _| { - cmd(Command::Stop); - Data::empty_tuple() - } + } + ), + ) + .add_var( + "queue_clear".to_owned(), + func!( + |a, _| { + if a.is_included_in(&Type::empty_tuple()) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `()`.").into()) } - ), - ) - .add_var( - "queue_get_current_song".to_owned(), - func!( - |a, _| { - if a.is_included_in(&Type::empty_tuple()) { - Ok(Type::newm(vec![ - Arc::new(MusicDbIdT), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `()`.").into()) - } - }, - { - let db = Arc::clone(db); - move |_, _| match db.lock().unwrap().queue.get_current_song() { - Some(id) => Data::new(MusicDbId(*id)), - None => Data::empty_tuple(), - } + }, + { + let cmd = Arc::clone(cmd); + move |_, _| { + cmd(Command::QueueUpdate( + vec![], + QueueContent::Folder(0, vec![], String::new()).into(), + )); + Data::empty_tuple() } - ), - ) - .add_var( - "queue_get_next_song".to_owned(), - func!( - |a, _| { - if a.is_included_in(&Type::empty_tuple()) { - Ok(Type::newm(vec![ - Arc::new(MusicDbIdT), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `()`.").into()) - } - }, - { - let db = Arc::clone(db); - move |_, _| match db.lock().unwrap().queue.get_next_song() { - Some(id) => Data::new(MusicDbId(*id)), - None => Data::empty_tuple(), - } - } - ), - ) - .add_var( - "queue_get_elem".to_owned(), - func!( - |a, _| { - if a.is_included_in(&mers_lib::program::configs::with_list::ListT(Type::new( + } + ), + ) + .add_var( + "queue_add_song".to_owned(), + func!( + |a, _| { + if a.is_included_in(&data::tuple::TupleT(vec![ + Type::new(mers_lib::program::configs::with_list::ListT(Type::new( data::int::IntT, - ))) { - Ok(gen_queue_elem_type()) - } else { - Err(format!("Function argument must be `List`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let a = int_list_to_usize_vec(&a); - if let Some(elem) = db.lock().unwrap().queue.get_item_at_index(&a, 0) { - gen_queue_elem(elem) - } else { - Data::empty_tuple() + ))), + Type::new(MusicDbIdT), + ])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `(List, MusicDbId)`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + let a = a.get(); + let a = &a.as_any().downcast_ref::().unwrap().0; + let path = int_list_to_usize_vec(&a[0]); + let song_id = a[1].get().as_any().downcast_ref::().unwrap().0; + cmd(Command::QueueAdd( + path, + vec![QueueContent::Song(song_id).into()], + )); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "queue_add_loop".to_owned(), + func!( + |a, _| { + if a.is_included_in(&data::tuple::TupleT(vec![ + Type::new(mers_lib::program::configs::with_list::ListT(Type::new( + data::int::IntT, + ))), + Type::new(data::int::IntT), + ])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `(List, Int)`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + let a = a.get(); + let a = &a.as_any().downcast_ref::().unwrap().0; + let path = int_list_to_usize_vec(&a[0]); + let repeat_count = a[1] + .get() + .as_any() + .downcast_ref::() + .unwrap() + .0; + cmd(Command::QueueAdd( + path, + vec![QueueContent::Loop( + repeat_count.max(0) as _, + 0, + Box::new(QueueContent::Folder(0, vec![], String::new()).into()), + ) + .into()], + )); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "queue_add_folder".to_owned(), + func!( + |a, _| { + if a.is_included_in(&data::tuple::TupleT(vec![ + Type::new(mers_lib::program::configs::with_list::ListT(Type::new( + data::int::IntT, + ))), + Type::new(data::string::StringT), + ])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `(List, String)`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + let a = a.get(); + let a = &a.as_any().downcast_ref::().unwrap().0; + let path = int_list_to_usize_vec(&a[0]); + let name = a[1] + .get() + .as_any() + .downcast_ref::() + .unwrap() + .0 + .clone(); + cmd(Command::QueueAdd( + path, + vec![QueueContent::Folder(0, vec![], name).into()], + )); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "queue_add_random".to_owned(), + func!( + |a, _| { + if a.is_included_in(&mers_lib::program::configs::with_list::ListT(Type::new( + data::int::IntT, + ))) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `List`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + let path = int_list_to_usize_vec(&a); + cmd(Command::QueueAdd( + path, + vec![QueueContent::Random(Default::default()).into()], + )); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "queue_add_shuffle".to_owned(), + func!( + |a, _| { + if a.is_included_in(&mers_lib::program::configs::with_list::ListT(Type::new( + data::int::IntT, + ))) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Function argument must be `List`.").into()) + } + }, + { + let cmd = Arc::clone(cmd); + move |a, _| { + let path = int_list_to_usize_vec(&a); + cmd(Command::QueueAdd( + path, + vec![QueueContent::Shuffle { + inner: Box::new(QueueContent::Folder(0, vec![], String::new()).into()), + state: musicdb_lib::data::queue::ShuffleState::NotShuffled, } + .into()], + )); + Data::empty_tuple() + } + } + ), + ) + .add_var( + "all_songs".to_owned(), + func!( + |a, _| { + if a.is_zero_tuple() { + Ok(Type::new(mers_lib::program::configs::with_list::ListT( + gen_song_type(), + ))) + } else { + Err(format!("Function argument must be `()`.").into()) + } + }, + { + let db = Arc::clone(db); + move |_, _| { + Data::new(mers_lib::program::configs::with_list::List( + db.lock() + .unwrap() + .songs() + .values() + .map(|s| Arc::new(RwLock::new(gen_song(s)))) + .collect(), + )) + } + } + ), + ) + .add_var( + "get_song".to_owned(), + func!( + |a, _| { + if a.is_included_in(&MusicDbIdT) { + Ok(Type::newm(vec![ + Arc::new(gen_song_type()), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `MusicDbId`.").into()) + } + }, + { + let db = Arc::clone(db); + move |a, _| { + let id = a.get().as_any().downcast_ref::().unwrap().0; + match db.lock().unwrap().get_song(&id) { + Some(song) => gen_song(song), + None => Data::empty_tuple(), } } - ), - ) - .add_var( - "queue_add_song".to_owned(), - func!( - |a, _| { - if a.is_included_in(&data::tuple::TupleT(vec![ - Type::new(mers_lib::program::configs::with_list::ListT(Type::new( - data::int::IntT, + } + ), + ) + .add_var( + "get_album".to_owned(), + func!( + |a, _| { + if a.is_included_in(&MusicDbIdT) { + Ok(Type::newm(vec![ + Arc::new(gen_album_type()), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `MusicDbId`.").into()) + } + }, + { + let db = Arc::clone(db); + move |a, _| { + let id = a.get().as_any().downcast_ref::().unwrap().0; + match db.lock().unwrap().albums().get(&id) { + Some(album) => gen_album(album), + None => Data::empty_tuple(), + } + } + } + ), + ) + .add_var( + "get_artist".to_owned(), + func!( + |a, _| { + if a.is_included_in(&MusicDbIdT) { + Ok(Type::newm(vec![ + Arc::new(gen_artist_type()), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `MusicDbId`.").into()) + } + }, + { + let db = Arc::clone(db); + move |a, _| { + let id = a.get().as_any().downcast_ref::().unwrap().0; + match db.lock().unwrap().artists().get(&id) { + Some(artist) => gen_artist(artist), + None => Data::empty_tuple(), + } + } + } + ), + ) + .add_var( + "get_song_tags".to_owned(), + func!( + |a, _| { + if a.is_included_in(&MusicDbIdT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::program::configs::with_list::ListT(Type::new( + data::string::StringT, ))), - Type::new(MusicDbIdT), - ])) { - Ok(Type::empty_tuple()) - } else { - Err(format!("Function argument must be `(List, MusicDbId)`.").into()) - } - }, - { - let cmd = Arc::clone(cmd); - move |a, _| { - let a = a.get(); - let a = &a.as_any().downcast_ref::().unwrap().0; - let path = int_list_to_usize_vec(&a[0]); - let song_id = a[1].get().as_any().downcast_ref::().unwrap().0; - cmd(Command::QueueAdd( - path, - vec![QueueContent::Song(song_id).into()], - )); - Data::empty_tuple() - } + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `MusicDbId`.").into()) } - ), - ) - .add_var( - "all_songs".to_owned(), - func!( - |a, _| { - if a.is_zero_tuple() { - Ok(Type::new(mers_lib::program::configs::with_list::ListT( - gen_song_type(), - ))) - } else { - Err(format!("Function argument must be `()`.").into()) - } - }, - { - let db = Arc::clone(db); - move |_, _| { - Data::new(mers_lib::program::configs::with_list::List( - db.lock() - .unwrap() - .songs() - .values() - .map(|s| Arc::new(RwLock::new(gen_song(s)))) + }, + { + let db = Arc::clone(db); + move |a, _| { + let id = a.get().as_any().downcast_ref::().unwrap().0; + match db.lock().unwrap().get_song(&id) { + Some(song) => Data::new(mers_lib::program::configs::with_list::List( + song.general + .tags + .iter() + .map(|t| { + Arc::new(RwLock::new(Data::new(data::string::String( + t.clone(), + )))) + }) .collect(), - )) + )), + None => Data::empty_tuple(), } } - ), - ) - .add_var( - "get_song".to_owned(), - func!( - |a, _| { - if a.is_included_in(&MusicDbIdT) { - Ok(Type::newm(vec![ - Arc::new(gen_song_type()), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `MusicDbId`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let id = a.get().as_any().downcast_ref::().unwrap().0; - match db.lock().unwrap().get_song(&id) { - Some(song) => gen_song(song), - None => Data::empty_tuple(), - } + } + ), + ) + .add_var( + "get_album_tags".to_owned(), + func!( + |a, _| { + if a.is_included_in(&MusicDbIdT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::program::configs::with_list::ListT(Type::new( + data::string::StringT, + ))), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `MusicDbId`.").into()) + } + }, + { + let db = Arc::clone(db); + move |a, _| { + let id = a.get().as_any().downcast_ref::().unwrap().0; + match db.lock().unwrap().albums().get(&id) { + Some(album) => Data::new(mers_lib::program::configs::with_list::List( + album + .general + .tags + .iter() + .map(|t| { + Arc::new(RwLock::new(Data::new(data::string::String( + t.clone(), + )))) + }) + .collect(), + )), + None => Data::empty_tuple(), } } - ), - ) - .add_var( - "get_album".to_owned(), - func!( - |a, _| { - if a.is_included_in(&MusicDbIdT) { - Ok(Type::newm(vec![ - Arc::new(gen_album_type()), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `MusicDbId`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let id = a.get().as_any().downcast_ref::().unwrap().0; - match db.lock().unwrap().albums().get(&id) { - Some(album) => gen_album(album), - None => Data::empty_tuple(), - } + } + ), + ) + .add_var( + "get_artist_tags".to_owned(), + func!( + |a, _| { + if a.is_included_in(&MusicDbIdT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::program::configs::with_list::ListT(Type::new( + data::string::StringT, + ))), + Arc::new(data::tuple::TupleT(vec![])), + ])) + } else { + Err(format!("Function argument must be `MusicDbId`.").into()) + } + }, + { + let db = Arc::clone(db); + move |a, _| { + let id = a.get().as_any().downcast_ref::().unwrap().0; + match db.lock().unwrap().artists().get(&id) { + Some(artist) => Data::new(mers_lib::program::configs::with_list::List( + artist + .general + .tags + .iter() + .map(|t| { + Arc::new(RwLock::new(Data::new(data::string::String( + t.clone(), + )))) + }) + .collect(), + )), + None => Data::empty_tuple(), } } - ), - ) - .add_var( - "get_artist".to_owned(), - func!( - |a, _| { - if a.is_included_in(&MusicDbIdT) { - Ok(Type::newm(vec![ - Arc::new(gen_artist_type()), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `MusicDbId`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let id = a.get().as_any().downcast_ref::().unwrap().0; - match db.lock().unwrap().artists().get(&id) { - Some(artist) => gen_artist(artist), - None => Data::empty_tuple(), - } - } - } - ), - ) - .add_var( - "get_song_tags".to_owned(), - func!( - |a, _| { - if a.is_included_in(&MusicDbIdT) { - Ok(Type::newm(vec![ - Arc::new(mers_lib::program::configs::with_list::ListT(Type::new( - data::string::StringT, - ))), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `MusicDbId`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let id = a.get().as_any().downcast_ref::().unwrap().0; - match db.lock().unwrap().get_song(&id) { - Some(song) => Data::new(mers_lib::program::configs::with_list::List( - song.general - .tags - .iter() - .map(|t| { - Arc::new(RwLock::new(Data::new(data::string::String( - t.clone(), - )))) - }) - .collect(), - )), - None => Data::empty_tuple(), - } - } - } - ), - ) - .add_var( - "get_album_tags".to_owned(), - func!( - |a, _| { - if a.is_included_in(&MusicDbIdT) { - Ok(Type::newm(vec![ - Arc::new(mers_lib::program::configs::with_list::ListT(Type::new( - data::string::StringT, - ))), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `MusicDbId`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let id = a.get().as_any().downcast_ref::().unwrap().0; - match db.lock().unwrap().albums().get(&id) { - Some(album) => Data::new(mers_lib::program::configs::with_list::List( - album - .general - .tags - .iter() - .map(|t| { - Arc::new(RwLock::new(Data::new(data::string::String( - t.clone(), - )))) - }) - .collect(), - )), - None => Data::empty_tuple(), - } - } - } - ), - ) - .add_var( - "get_artist_tags".to_owned(), - func!( - |a, _| { - if a.is_included_in(&MusicDbIdT) { - Ok(Type::newm(vec![ - Arc::new(mers_lib::program::configs::with_list::ListT(Type::new( - data::string::StringT, - ))), - Arc::new(data::tuple::TupleT(vec![])), - ])) - } else { - Err(format!("Function argument must be `MusicDbId`.").into()) - } - }, - { - let db = Arc::clone(db); - move |a, _| { - let id = a.get().as_any().downcast_ref::().unwrap().0; - match db.lock().unwrap().artists().get(&id) { - Some(artist) => Data::new(mers_lib::program::configs::with_list::List( - artist - .general - .tags - .iter() - .map(|t| { - Arc::new(RwLock::new(Data::new(data::string::String( - t.clone(), - )))) - }) - .collect(), - )), - None => Data::empty_tuple(), - } - } - } - ), - ) + } + ), + ) } fn gen_song_type() -> Type {