diff --git a/Cargo.lock b/Cargo.lock index 327ce37..587b63f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -129,9 +129,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] @@ -168,13 +168,14 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" dependencies = [ "castaway", "cfg-if", "itoa", + "rustversion", "ryu", "static_assertions", ] @@ -197,15 +198,15 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.4.2", "crossterm_winapi", - "libc", - "mio", + "mio 1.0.2", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -393,9 +394,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -406,6 +407,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "http" version = "0.2.12" @@ -498,10 +505,14 @@ dependencies = [ ] [[package]] -name = "indoc" -version = "2.0.4" +name = "instability" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn 2.0.48", +] [[package]] name = "ipnet" @@ -511,9 +522,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -541,15 +552,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -604,11 +615,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mockall" version = "0.12.1" @@ -653,6 +676,7 @@ dependencies = [ "structopt", "tempfile", "tokio", + "tui-input", "url", "uuid", "version_check", @@ -911,21 +935,22 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ "bitflags 2.4.2", "cassowary", "compact_str", "crossterm", - "indoc", + "instability", "itertools", "lru", "paste", - "stability", "strum", + "strum_macros", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] @@ -986,9 +1011,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.4.2", "errno", @@ -1127,12 +1152,12 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 1.0.2", "signal-hook", ] @@ -1189,16 +1214,6 @@ dependencies = [ "serde", ] -[[package]] -name = "stability" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -1246,11 +1261,11 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -1377,7 +1392,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 0.8.10", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1470,6 +1485,16 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-input" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd137780d743c103a391e06fe952487f914b299a4fe2c3626677f6a6339a7c6b" +dependencies = [ + "ratatui", + "unicode-width", +] + [[package]] name = "typed-builder" version = "0.18.1" @@ -1518,10 +1543,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] -name = "unicode-width" -version = "0.1.11" +name = "unicode-truncate" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" diff --git a/Cargo.toml b/Cargo.toml index ac138f4..d758e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,16 +7,18 @@ edition = "2021" [dependencies] aho-corasick = { version = "1.1.2", optional = true } -crossterm = { version = "0.27.0", optional = true} +crossterm = { version = "0.28.1", optional = true} once_cell = { version = "1.19.0", optional = true} openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, optional = true} paste = { version = "1.0.15", optional = true } -ratatui = { version = "0.26.0", optional = true} +ratatui = { version = "0.28.1", optional = true} reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true } serde = { version = "1.0.196", features = ["derive"], optional = true } serde_json = { version = "1.0.113", optional = true} structopt = { version = "0.3.26", optional = true} tokio = { version = "1.36.0", features = ["rt"], optional = true} +# ratatui/crossterm dependency version must match with musichhoard's ratatui/crossterm +tui-input = { version = "0.10.1", optional = true } url = { version = "2.5.0" } uuid = { version = "1.7.0" } @@ -35,7 +37,7 @@ database-json = ["serde", "serde_json"] library-beets = [] library-beets-ssh = ["openssh", "tokio"] musicbrainz = ["paste", "reqwest", "serde", "serde_json"] -tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"] +tui = ["aho-corasick", "crossterm", "once_cell", "ratatui", "tui-input"] [[bin]] name = "musichoard" diff --git a/src/tui/app/machine/input_state.rs b/src/tui/app/machine/input_state.rs index 1f1abb4..52e0e7d 100644 --- a/src/tui/app/machine/input_state.rs +++ b/src/tui/app/machine/input_state.rs @@ -1,12 +1,14 @@ +use tui_input::{backend::crossterm::EventHandler, Input}; + use crate::tui::app::{ machine::{App, AppInner, AppMachine}, - AppPublic, AppState, IAppInteractInput, + AppPublic, AppState, IAppInteractInput, InputEvent, }; use super::match_state::MatchState; pub struct InputState { - string: String, + input: Input, client: InputClient, } @@ -19,7 +21,7 @@ impl AppMachine { AppMachine { inner, state: InputState { - string: String::new(), + input: Input::default(), client, }, } @@ -36,7 +38,7 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { fn from(machine: &'a mut AppMachine) -> Self { AppPublic { inner: (&mut machine.inner).into(), - state: AppState::Input(&machine.state.string), + state: AppState::Input(&machine.state.input), } } } @@ -44,13 +46,8 @@ impl<'a> From<&'a mut AppMachine> for AppPublic<'a> { impl IAppInteractInput for AppMachine { type APP = App; - fn append_character(mut self, ch: char) -> Self::APP { - self.state.string.push(ch); - self.into() - } - - fn delete_character(mut self) -> Self::APP { - self.state.string.pop(); + fn input(mut self, input: InputEvent) -> Self::APP { + self.state.input.handle_event(&crossterm::event::Event::Key(input)); self.into() } diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index ae99314..aaaaedd 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -1,10 +1,12 @@ mod machine; mod selection; +use crossterm::event::KeyEvent; pub use machine::App; pub use selection::{Category, Delta, Selection, WidgetState}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; +use tui_input::Input; use crate::tui::lib::interface::musicbrainz::Match; @@ -133,11 +135,11 @@ pub trait IAppInteractMatch { fn abort(self) -> Self::APP; } +type InputEvent = KeyEvent; pub trait IAppInteractInput { type APP: IApp; - fn append_character(self, ch: char) -> Self::APP; - fn delete_character(self) -> Self::APP; + fn input(self, input: InputEvent) -> Self::APP; fn confirm(self) -> Self::APP; fn cancel(self) -> Self::APP; } @@ -214,8 +216,19 @@ pub struct MatchStatePublic<'app> { pub state: &'app mut WidgetState, } -pub type AppPublicState<'app> = - AppState<(), (), (), &'app str, (), MatchStatePublic<'app>, &'app str, &'app str, &'app str>; +pub type InputStatePublic<'app> = &'app Input; + +pub type AppPublicState<'app> = AppState< + (), + (), + (), + &'app str, + (), + MatchStatePublic<'app>, + InputStatePublic<'app>, + &'app str, + &'app str, +>; impl< BrowseState, diff --git a/src/tui/handler.rs b/src/tui/handler.rs index f46777d..e06f964 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -201,20 +201,17 @@ impl IEventHandlerPrivate for EventHandler { fn handle_input_key_event(app: ::InputState, key_event: KeyEvent) -> APP { if key_event.modifiers == KeyModifiers::CONTROL { - return match key_event.code { - KeyCode::Char('g') | KeyCode::Char('G') => app.cancel(), - _ => app.no_op(), + match key_event.code { + KeyCode::Char('g') | KeyCode::Char('G') => return app.cancel(), + _ => {}, }; } match key_event.code { - // Add/remove character. - KeyCode::Char(ch) => app.append_character(ch), - KeyCode::Backspace => app.delete_character(), // Return. KeyCode::Esc | KeyCode::Enter => app.confirm(), // Othey keys. - _ => app.no_op(), + _ => app.input(key_event), } } diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index a9b58bc..c78815b 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -32,6 +32,8 @@ use crate::tui::{ }, }; +use super::app::InputStatePublic; + pub trait IUi { fn render(app: &mut APP, frame: &mut Frame); } @@ -68,7 +70,7 @@ impl Ui { frame: &mut Frame, ) { let active = selection.category(); - let areas = FrameArea::new(frame.size()); + let areas = FrameArea::new(frame.area()); let artist_state = ArtistState::new( active == Category::Artist, @@ -106,7 +108,7 @@ impl Ui { } fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) { - let area = OverlayBuilder::default().build(frame.size()); + let area = OverlayBuilder::default().build(frame.area()); if selection.category() == Category::Artist { let overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list); @@ -126,13 +128,13 @@ impl Ui { let area = OverlayBuilder::default() .with_width(OverlaySize::Value(39)) .with_height(OverlaySize::Value(4)) - .build(frame.size()); + .build(frame.area()); let reload_text = ReloadOverlay::paragraph(); UiWidget::render_overlay_widget("Reload", reload_text, area, false, frame); } fn render_fetch_overlay(frame: &mut Frame) { - let area = OverlayBuilder::default().build(frame.size()); + let area = OverlayBuilder::default().build(frame.area()); let fetch_text = FetchOverlay::paragraph(); UiWidget::render_overlay_widget("Fetching", fetch_text, area, false, frame) } @@ -142,15 +144,29 @@ impl Ui { state: &mut WidgetState, frame: &mut Frame, ) { - let area = OverlayBuilder::default().build(frame.size()); + let area = OverlayBuilder::default().build(frame.area()); let st = MatchOverlay::new(info, state); UiWidget::render_overlay_list_widget(&st.matching, st.list, st.state, true, area, frame) } + fn render_input_overlay(input: InputStatePublic, frame: &mut Frame) { + let area = OverlayBuilder::default() + .with_height(OverlaySize::Value(3)) + .build(frame.area()); + UiWidget::render_overlay_widget("Input", Paragraph::new(input.value()), area, false, frame); + + let width = area.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let scroll = input.visual_scroll(width as usize); + frame.set_cursor_position(( + area.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 1, + area.y + 1, + )) + } + fn render_error_overlay>(title: S, msg: S, frame: &mut Frame) { let area = OverlayBuilder::default() .with_height(OverlaySize::Value(4)) - .build(frame.size()); + .build(frame.area()); let error_text = ErrorOverlay::paragraph(msg.as_ref()); UiWidget::render_overlay_widget(title.as_ref(), error_text, area, true, frame); } @@ -170,6 +186,7 @@ impl IUi for Ui { AppState::Reload(()) => Self::render_reload_overlay(frame), AppState::Fetch(()) => Self::render_fetch_overlay(frame), AppState::Match(public) => Self::render_match_overlay(public.info, public.state, frame), + AppState::Input(input) => Self::render_input_overlay(input, frame), AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame), AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame), _ => {} @@ -211,7 +228,7 @@ mod tests { info: m.info, state: m.state, }), - AppState::Input(s) => AppState::Input(s), + AppState::Input(ref i) => AppState::Input(i), AppState::Error(s) => AppState::Error(s), AppState::Critical(s) => AppState::Critical(s), },