From 585f8e6336c7e109cd0550adab33879699e17f0e Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Thu, 5 Sep 2024 19:27:52 +0200 Subject: [PATCH 01/59] clipboard as a feature for android and server support --- cli/Cargo.toml | 3 ++- cli/src/main.rs | 41 ++++++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index de23ec8d..fe74af49 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -39,7 +39,7 @@ color-eyre = { workspace = true } number_prefix = { workspace = true } ctrlc = { workspace = true } qr2term = { workspace = true } -arboard = { workspace = true, features = ["wayland-data-control"] } # Wayland by default, fallback to X11. +arboard = { optional = true, workspace = true, features = ["wayland-data-control"] } # Wayland by default, fallback to X11. tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace=true, features = ["env-filter"] } @@ -47,6 +47,7 @@ tracing-subscriber = { workspace=true, features = ["env-filter"] } trycmd = { workspace = true } [features] +clipboard = ["dep:arboard"] # TLS implementations for websocket connections via async-tungstenite # required for optional wss connection to the mailbox server tls = ["magic-wormhole/tls"] diff --git a/cli/src/main.rs b/cli/src/main.rs index c2f12834..ec431b21 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3,7 +3,6 @@ mod util; use std::time::{Duration, Instant}; -use arboard::Clipboard; use async_std::sync::Arc; use clap::{Args, CommandFactory, Parser, Subcommand}; use color_eyre::{eyre, eyre::Context}; @@ -18,6 +17,9 @@ use magic_wormhole::{ use std::{io::Write, path::PathBuf}; use tracing_subscriber::EnvFilter; +#[cfg(feature = "clipboard")] +use arboard::Clipboard; + fn install_ctrlc_handler( ) -> eyre::Result futures::future::BoxFuture<'static, ()> + Clone> { use async_std::sync::{Condvar, Mutex}; @@ -41,7 +43,6 @@ fn install_ctrlc_handler( }) }) .context("Error setting Ctrl-C handler")?; - Ok(move || { /* Transform the notification into a future that waits */ let notifier = notifier.clone(); @@ -279,11 +280,6 @@ async fn main() -> eyre::Result<()> { .init(); }; - let mut clipboard = Clipboard::new() - .map_err(|err| { - tracing::warn!("Failed to initialize clipboard support: {}", err); - }) - .ok(); match app.command { WormholeCommand::Send { @@ -304,7 +300,6 @@ async fn main() -> eyre::Result<()> { true, transfer::APP_CONFIG, Some(&sender_print_code), - clipboard.as_mut(), )), ctrl_c(), ) @@ -342,7 +337,6 @@ async fn main() -> eyre::Result<()> { true, transfer::APP_CONFIG, Some(&sender_print_code), - clipboard.as_mut(), )); match futures::future::select(connect_fut, ctrl_c()).await { Either::Left((result, _)) => result?, @@ -382,7 +376,6 @@ async fn main() -> eyre::Result<()> { false, transfer::APP_CONFIG, None, - clipboard.as_mut(), )); match futures::future::select(connect_fut, ctrl_c()).await { Either::Left((result, _)) => result?, @@ -456,7 +449,6 @@ async fn main() -> eyre::Result<()> { true, app_config, Some(&server_print_code), - clipboard.as_mut(), )); let (wormhole, _code, relay_hints) = match futures::future::select(connect_fut, ctrl_c()).await { @@ -492,7 +484,6 @@ async fn main() -> eyre::Result<()> { false, app_config, None, - clipboard.as_mut(), ) .await?; @@ -578,7 +569,6 @@ async fn parse_and_connect( is_send: bool, mut app_config: magic_wormhole::AppConfig, print_code: Option<&PrintCodeFn>, - clipboard: Option<&mut Clipboard>, ) -> eyre::Result<(Wormhole, magic_wormhole::Code, Vec)> { // TODO handle relay servers with multiple endpoints better let mut relay_hints: Vec = common_args @@ -623,10 +613,18 @@ async fn parse_and_connect( /* Print code and also copy it to clipboard */ if is_send { - if let Some(clipboard) = clipboard { - match clipboard.set_text(mailbox_connection.code().to_string()) { - Ok(()) => tracing::info!("Code copied to clipboard"), - Err(err) => tracing::warn!("Failed to copy code to clipboard: {}", err), + #[cfg(feature = "clipboard")] { + let clipboard = Clipboard::new() + .map_err(|err| { + tracing::warn!("Failed to initialize clipboard support: {}", err); + }) + .ok(); + + if let Some(mut clipboard) = clipboard { + match clipboard.set_text(mailbox_connection.code().to_string()) { + Ok(()) => tracing::info!("Code copied to clipboard"), + Err(err) => tracing::warn!("Failed to copy code to clipboard: {}", err), + } } } @@ -747,11 +745,20 @@ fn sender_print_code( is_leader: false, } .to_string(); + + #[cfg(feature = "clipboard")] writeln!( term, "\nThis wormhole's code is: {} (it has been copied to your clipboard)", style(&code).bold() )?; + #[cfg(not(feature = "clipboard"))] + writeln!( + term, + "\nThis wormhole's code is: {}", + style(&code).bold() + )?; + writeln!(term, "This is equivalent to the following link: \u{001B}]8;;{}\u{001B}\\{}\u{001B}]8;;\u{001B}\\", &uri, &uri)?; let qr = qr2term::generate_qr_string(&uri).context("Failed to generate QR code for send link")?; From 73790ef24653241a4eda35de149aa63129f1ed5e Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Thu, 5 Sep 2024 19:29:15 +0200 Subject: [PATCH 02/59] cargo fmt --- cli/src/main.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index ec431b21..05fe0cd2 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -280,7 +280,6 @@ async fn main() -> eyre::Result<()> { .init(); }; - match app.command { WormholeCommand::Send { common, @@ -476,16 +475,8 @@ async fn main() -> eyre::Result<()> { tracing::warn!("This is an unstable feature. Make sure that your peer is running the exact same version of the program as you. Also, please report all bugs and crashes."); let mut app_config = forwarding::APP_CONFIG; app_config.app_version.transit_abilities = parse_transit_args(&common); - let (wormhole, _code, relay_hints) = parse_and_connect( - &mut term, - common, - code, - None, - false, - app_config, - None, - ) - .await?; + let (wormhole, _code, relay_hints) = + parse_and_connect(&mut term, common, code, None, false, app_config, None).await?; let offer = forwarding::connect( wormhole, @@ -613,7 +604,8 @@ async fn parse_and_connect( /* Print code and also copy it to clipboard */ if is_send { - #[cfg(feature = "clipboard")] { + #[cfg(feature = "clipboard")] + { let clipboard = Clipboard::new() .map_err(|err| { tracing::warn!("Failed to initialize clipboard support: {}", err); @@ -753,12 +745,8 @@ fn sender_print_code( style(&code).bold() )?; #[cfg(not(feature = "clipboard"))] - writeln!( - term, - "\nThis wormhole's code is: {}", - style(&code).bold() - )?; - + writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; + writeln!(term, "This is equivalent to the following link: \u{001B}]8;;{}\u{001B}\\{}\u{001B}]8;;\u{001B}\\", &uri, &uri)?; let qr = qr2term::generate_qr_string(&uri).context("Failed to generate QR code for send link")?; From cb72ef0c8da71b1cf63c71cf37ff0cf1d79b25e8 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Thu, 5 Sep 2024 20:55:14 +0200 Subject: [PATCH 03/59] improved wormhole code display for serve and send --- cli/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index 05fe0cd2..2e91a412 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -771,7 +771,15 @@ fn server_print_code( code: &magic_wormhole::Code, _: &Option, ) -> eyre::Result<()> { + #[cfg(feature = "clipboard")] + writeln!( + term, + "\nThis wormhole's code is: {} (it has been copied to your clipboard)", + style(&code).bold() + )?; + #[cfg(not(feature = "clipboard"))] writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; + writeln!( term, "On the other side, enter that code into a Magic Wormhole client\n" From b1c792853ecd7e369d2a0a382a86189f8a94c93c Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 6 Sep 2024 17:25:53 +0200 Subject: [PATCH 04/59] cargo fmt --- cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 2e91a412..2b30071e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -779,7 +779,7 @@ fn server_print_code( )?; #[cfg(not(feature = "clipboard"))] writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; - + writeln!( term, "On the other side, enter that code into a Magic Wormhole client\n" From 39585e873357e599258545e29fc3fef6e8f069e3 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 10 Sep 2024 17:59:10 +0200 Subject: [PATCH 05/59] clipboard as default feature --- cli/Cargo.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index fe74af49..1deadbf4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -39,9 +39,11 @@ color-eyre = { workspace = true } number_prefix = { workspace = true } ctrlc = { workspace = true } qr2term = { workspace = true } -arboard = { optional = true, workspace = true, features = ["wayland-data-control"] } # Wayland by default, fallback to X11. +arboard = { optional = true, workspace = true, features = [ + "wayland-data-control", +] } # Wayland by default, fallback to X11. tracing = { workspace = true, features = ["log", "log-always"] } -tracing-subscriber = { workspace=true, features = ["env-filter"] } +tracing-subscriber = { workspace = true, features = ["env-filter"] } [dev-dependencies] trycmd = { workspace = true } @@ -56,5 +58,5 @@ native-tls = ["magic-wormhole/native-tls"] experimental-transfer-v2 = ["magic-wormhole/experimental-transfer-v2"] experimental = ["experimental-transfer-v2"] -default = ["magic-wormhole/default", "magic-wormhole/forwarding"] +default = ["clipboard", "magic-wormhole/default", "magic-wormhole/forwarding"] all = ["default", "magic-wormhole/native-tls"] From 4affcc9d6b9df029783b49f7ad960cde161c35a3 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 10 Sep 2024 18:05:21 +0200 Subject: [PATCH 06/59] improved clipboard conditional wormhole code message --- cli/src/main.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 2b30071e..47cb34e8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -738,14 +738,15 @@ fn sender_print_code( } .to_string(); - #[cfg(feature = "clipboard")] - writeln!( - term, - "\nThis wormhole's code is: {} (it has been copied to your clipboard)", - style(&code).bold() - )?; - #[cfg(not(feature = "clipboard"))] - writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; + if cfg!(feature = "clipboard") { + writeln!( + term, + "\nThis wormhole's code is: {} (it has been copied to your clipboard)", + style(&code).bold() + )?; + } else { + writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; + } writeln!(term, "This is equivalent to the following link: \u{001B}]8;;{}\u{001B}\\{}\u{001B}]8;;\u{001B}\\", &uri, &uri)?; let qr = @@ -771,14 +772,15 @@ fn server_print_code( code: &magic_wormhole::Code, _: &Option, ) -> eyre::Result<()> { - #[cfg(feature = "clipboard")] - writeln!( - term, - "\nThis wormhole's code is: {} (it has been copied to your clipboard)", - style(&code).bold() - )?; - #[cfg(not(feature = "clipboard"))] - writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; + if cfg!(feature = "clipboard") { + writeln!( + term, + "\nThis wormhole's code is: {} (it has been copied to your clipboard)", + style(&code).bold() + )?; + } else { + writeln!(term, "\nThis wormhole's code is: {}", style(&code).bold())?; + } writeln!( term, From 63e92c75829576ef834fa256da8ad383f0efd21b Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 10 Sep 2024 18:09:09 +0200 Subject: [PATCH 07/59] added whitespace for readability --- cli/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index 47cb34e8..ebe3ba5a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -43,6 +43,7 @@ fn install_ctrlc_handler( }) }) .context("Error setting Ctrl-C handler")?; + Ok(move || { /* Transform the notification into a future that waits */ let notifier = notifier.clone(); From 049bca7be3ec7a580d2d7e0be8db643d6038ce3a Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 00:11:36 +0200 Subject: [PATCH 08/59] first working draft of code word spelling checks --- Cargo.lock | 214 ++++++++++++++++++++++++++++++++++++++++++- cli/Cargo.toml | 3 + cli/src/main.rs | 11 +-- cli/src/reedline.rs | 143 +++++++++++++++++++++++++++++ src/core.rs | 2 +- src/core/wordlist.rs | 2 +- src/lib.rs | 2 +- 7 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 cli/src/reedline.rs diff --git a/Cargo.lock b/Cargo.lock index c4481180..7b7a430e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.15" @@ -454,6 +469,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "blake2" @@ -592,6 +610,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "cipher" version = "0.4.4" @@ -832,13 +863,30 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio 1.0.2", + "parking_lot 0.12.3", + "rustix 0.38.34", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1132,6 +1180,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix 0.38.34", + "windows-sys 0.52.0", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -1510,6 +1569,29 @@ dependencies = [ "serde", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1607,6 +1689,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1759,9 +1850,12 @@ dependencies = [ "futures", "indicatif", "magic-wormhole", + "nu-ansi-term 0.50.1", "number_prefix", "qr2term", "rand", + "reedline", + "regex", "serde", "serde_derive", "serde_json", @@ -1829,6 +1923,19 @@ dependencies = [ "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 = "native-tls" version = "0.2.12" @@ -1920,6 +2027,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2345,7 +2461,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c2a1e77b5cd714b04247ad912b7c8fe9a1fe1d58425048249def91bcf690e4c" dependencies = [ - "crossterm", + "crossterm 0.25.0", "qrcode", ] @@ -2444,6 +2560,26 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "reedline" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5289de810296f8f2ff58d35544d92ae98d0a631453388bc3e608086be0fa596" +dependencies = [ + "chrono", + "crossterm 0.28.1", + "fd-lock", + "itertools", + "nu-ansi-term 0.50.1", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "regex" version = "1.10.6" @@ -2598,6 +2734,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -2800,7 +2942,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.2", "signal-hook", ] @@ -2909,12 +3052,40 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.76", +] + [[package]] name = "stun_codec" version = "0.3.5" @@ -3174,7 +3345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.46.0", "once_cell", "regex", "sharded-slab", @@ -3291,6 +3462,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.13" @@ -3367,6 +3544,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" @@ -3619,6 +3816,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2bb5ea74..de89aa15 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -43,6 +43,9 @@ arboard = { optional = true, workspace = true, features = [ ] } # Wayland by default, fallback to X11. tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } +reedline = "0.35.0" +regex = "1.10.6" +nu-ansi-term = "0.50.1" [dev-dependencies] trycmd = { workspace = true } diff --git a/cli/src/main.rs b/cli/src/main.rs index ebe3ba5a..c779dbb4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,4 +1,5 @@ #![allow(clippy::too_many_arguments)] +mod reedline; mod util; use std::time::{Duration, Instant}; @@ -14,6 +15,7 @@ use magic_wormhole::{ transit::{self, TransitInfo}, MailboxConnection, Wormhole, }; +use reedline::enter_code; use std::{io::Write, path::PathBuf}; use tracing_subscriber::EnvFilter; @@ -710,15 +712,6 @@ fn create_progress_handler(pb: ProgressBar) -> impl FnMut(u64, u64) { } } -fn enter_code() -> eyre::Result { - use dialoguer::Input; - - Input::new() - .with_prompt("Enter code") - .interact_text() - .map_err(From::from) -} - fn print_welcome(term: &mut Term, welcome: Option<&str>) -> eyre::Result<()> { if let Some(welcome) = &welcome { writeln!(term, "Got welcome from server: {}", welcome)?; diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs new file mode 100644 index 00000000..f47aa40f --- /dev/null +++ b/cli/src/reedline.rs @@ -0,0 +1,143 @@ +use std::{borrow::Cow, process::exit}; + +use color_eyre::eyre; +use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; +use nu_ansi_term::{Color, Style}; +use reedline::{ + Highlighter, Hinter, History, Prompt, PromptEditMode, PromptHistorySearch, Reedline, Signal, + StyledText, +}; +use regex::Regex; + +struct CodePrompt {} + +impl CodePrompt { + fn default() -> Self { + CodePrompt {} + } +} + +impl Prompt for CodePrompt { + fn render_prompt_left(&self) -> Cow<'_, str> { + Cow::Borrowed("Wormhole Code: ") + } + + fn render_prompt_right(&self) -> Cow<'_, str> { + Cow::Borrowed("") + } + + fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> { + Cow::Borrowed("") + } + + fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { + Cow::Borrowed("... ") + } + + fn render_prompt_history_search_indicator( + &self, + _history_search: PromptHistorySearch, + ) -> Cow<'_, str> { + Cow::Borrowed("") + } + + // Optionally override provided methods + // fn get_prompt_color(&self) -> Color { ... } + // fn get_prompt_multiline_color(&self) -> Color { ... } + // fn get_indicator_color(&self) -> Color { ... } + // fn get_prompt_right_color(&self) -> Color { ... } + // fn right_prompt_on_last_line(&self) -> bool { ... } +} + +pub struct CodeHinter { + wordlist: Wordlist, +} + +impl CodeHinter { + fn default() -> Self { + CodeHinter { + wordlist: default_wordlist(2), + } + } +} + +impl Hinter for CodeHinter { + fn handle( + &mut self, + _line: &str, + _pos: usize, + _history: &dyn History, + _use_ansi_coloring: bool, + _cwd: &str, + ) -> String { + "".to_string() + } + + fn complete_hint(&self) -> String { + "".to_string() + } + + fn next_hint_token(&self) -> String { + "".to_string() + } +} + +struct CodeHighliter { + regex: Regex, + wordlist: Wordlist, +} + +impl CodeHighliter { + fn default() -> Self { + CodeHighliter { + regex: Regex::new(r"^\d+(-[a-z]+)+$").unwrap(), + wordlist: default_wordlist(2), + } + } + + fn is_valid_code(&self, code: &str) -> bool { + if !self.regex.is_match(code) { + return false; + } + + let words: Vec<&str> = code.split('-').skip(1).collect(); + + words.iter().all(|&word| { + self.wordlist + .words + .iter() + .flatten() + .any(|valid_word| valid_word == word) + }) + } +} + +impl Highlighter for CodeHighliter { + fn highlight(&self, line: &str, _cursor: usize) -> StyledText { + let invalid = Style::new().fg(Color::White); + let valid = Style::new().fg(Color::Green); + + let style = match self.is_valid_code(line) { + true => valid, + false => invalid, + }; + + let mut t = StyledText::new(); + t.push((style, line.to_string())); + t + } +} + +pub fn enter_code() -> eyre::Result { + let mut line_editor = Reedline::create().with_highlighter(Box::new(CodeHighliter::default())); + let prompt = CodePrompt::default(); + + loop { + let sig = line_editor.read_line(&prompt); + match sig { + Ok(Signal::Success(buffer)) => return Ok(buffer), + Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => exit(0), + _ => {}, + } + } +} diff --git a/src/core.rs b/src/core.rs index 8844ba49..04b1828b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,7 +5,7 @@ pub mod rendezvous; mod server_messages; #[cfg(test)] mod test; -mod wordlist; +pub mod wordlist; use serde_derive::{Deserialize, Serialize}; use std::borrow::Cow; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 966d82ef..5d6ce74c 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -5,7 +5,7 @@ use std::fmt; #[derive(PartialEq)] pub struct Wordlist { pub num_words: usize, - words: Vec>, + pub words: Vec>, } impl fmt::Debug for Wordlist { diff --git a/src/lib.rs b/src/lib.rs index 03702736..94c13289 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ #[macro_use] mod util; -mod core; +pub mod core; #[cfg(feature = "forwarding")] pub mod forwarding; #[cfg(feature = "transfer")] From 39224e63d02152d345f82e8c4e56a8cfa9485c56 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 00:46:51 +0200 Subject: [PATCH 09/59] removed regex as it is not needed anymore --- Cargo.lock | 1 - Cargo.toml | 1 - cli/Cargo.toml | 1 - cli/src/reedline.rs | 13 ++++++------- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b7a430e..4dd1cbed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1855,7 +1855,6 @@ dependencies = [ "qr2term", "rand", "reedline", - "regex", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 0b3b7c15..03881e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ futures = "0.3.12" hex = "0.4.2" hkdf = "0.12.2" indicatif = "0.17.0" -log = "0.4.13" noise-protocol = "0.2" noise-rust-crypto = "0.6.0-rc.1" number_prefix = "0.4.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index de89aa15..57466c05 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -44,7 +44,6 @@ arboard = { optional = true, workspace = true, features = [ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" -regex = "1.10.6" nu-ansi-term = "0.50.1" [dev-dependencies] diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index f47aa40f..5e99403f 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -7,7 +7,6 @@ use reedline::{ Highlighter, Hinter, History, Prompt, PromptEditMode, PromptHistorySearch, Reedline, Signal, StyledText, }; -use regex::Regex; struct CodePrompt {} @@ -83,26 +82,26 @@ impl Hinter for CodeHinter { } struct CodeHighliter { - regex: Regex, wordlist: Wordlist, } impl CodeHighliter { fn default() -> Self { CodeHighliter { - regex: Regex::new(r"^\d+(-[a-z]+)+$").unwrap(), wordlist: default_wordlist(2), } } fn is_valid_code(&self, code: &str) -> bool { - if !self.regex.is_match(code) { + let words: Vec<&str> = code.split('-').collect(); + + // if the first element in code is not a valid number + if words.first().and_then(|w| w.parse::().ok()).is_none() { return false; } - let words: Vec<&str> = code.split('-').skip(1).collect(); - - words.iter().all(|&word| { + // check all words for validity + words.iter().skip(1).all(|&word| { self.wordlist .words .iter() From f1485560766e371e9a216fdb8daf7e71d41fb22c Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 00:59:24 +0200 Subject: [PATCH 10/59] improved coloring --- cli/src/reedline.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 5e99403f..0d477616 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -40,6 +40,9 @@ impl Prompt for CodePrompt { Cow::Borrowed("") } + fn get_prompt_color(&self) -> reedline::Color { + reedline::Color::Grey + } // Optionally override provided methods // fn get_prompt_color(&self) -> Color { ... } // fn get_prompt_multiline_color(&self) -> Color { ... } @@ -113,7 +116,7 @@ impl CodeHighliter { impl Highlighter for CodeHighliter { fn highlight(&self, line: &str, _cursor: usize) -> StyledText { - let invalid = Style::new().fg(Color::White); + let invalid = Style::new().fg(Color::Red); let valid = Style::new().fg(Color::Green); let style = match self.is_valid_code(line) { From 45f7b7f30035743d491dd0ee815aeba3410702ce Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 22:50:07 +0200 Subject: [PATCH 11/59] first working tab completion --- Cargo.lock | 8 +++ cli/Cargo.toml | 2 + cli/src/reedline.rs | 120 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 106 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dd1cbed..b5ad26f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,6 +1408,12 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a15f3d0fa42283a765e5fb609683ddab4ee4ff245d8db66a24d926c05e518c6" + [[package]] name = "generic-array" version = "0.14.7" @@ -1848,7 +1854,9 @@ dependencies = [ "dialoguer", "env_logger", "futures", + "fuzzt", "indicatif", + "lazy_static", "magic-wormhole", "nu-ansi-term 0.50.1", "number_prefix", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 57466c05..26a419de 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -45,6 +45,8 @@ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" nu-ansi-term = "0.50.1" +lazy_static = "1.5.0" +fuzzt = "0.3.1" [dev-dependencies] trycmd = { workspace = true } diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 0d477616..26dd13bd 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,13 +1,19 @@ use std::{borrow::Cow, process::exit}; use color_eyre::eyre; +use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ - Highlighter, Hinter, History, Prompt, PromptEditMode, PromptHistorySearch, Reedline, Signal, - StyledText, + default_emacs_keybindings, ColumnarMenu, Completer, DefaultCompleter, Emacs, Highlighter, + Hinter, History, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, + PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, StyledText, Suggestion, }; +lazy_static! { + static ref WORDLIST: Wordlist = default_wordlist(2); +} + struct CodePrompt {} impl CodePrompt { @@ -43,23 +49,13 @@ impl Prompt for CodePrompt { fn get_prompt_color(&self) -> reedline::Color { reedline::Color::Grey } - // Optionally override provided methods - // fn get_prompt_color(&self) -> Color { ... } - // fn get_prompt_multiline_color(&self) -> Color { ... } - // fn get_indicator_color(&self) -> Color { ... } - // fn get_prompt_right_color(&self) -> Color { ... } - // fn right_prompt_on_last_line(&self) -> bool { ... } } -pub struct CodeHinter { - wordlist: Wordlist, -} +pub struct CodeHinter {} impl CodeHinter { fn default() -> Self { - CodeHinter { - wordlist: default_wordlist(2), - } + CodeHinter {} } } @@ -72,27 +68,81 @@ impl Hinter for CodeHinter { _use_ansi_coloring: bool, _cwd: &str, ) -> String { - "".to_string() + _line.to_string() } fn complete_hint(&self) -> String { - "".to_string() + "accepted".to_string() } fn next_hint_token(&self) -> String { - "".to_string() + "test".to_string() } } -struct CodeHighliter { - wordlist: Wordlist, +struct CodeCompleter {} + +impl CodeCompleter { + fn default() -> Self { + CodeCompleter {} + } } +impl Completer for CodeCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + let parts: Vec<&str> = line.split('-').collect(); + let current_part = parts.last().unwrap_or(&""); + let current_part_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); + + let mut suggestions = Vec::new(); + + if parts.len() == 1 { + // Completing the number part + for i in 1..=255 { + if i.to_string().starts_with(current_part) { + suggestions.push(Suggestion { + value: i.to_string(), + description: None, + extra: None, + span: reedline::Span { + start: current_part_start, + end: pos, + }, + append_whitespace: false, + style: None, + }); + } + } + } else { + // Completing a word + for word_list in WORDLIST.words.iter() { + for word in word_list.iter() { + if word.starts_with(current_part) { + suggestions.push(Suggestion { + value: word.to_string(), + description: None, + extra: None, + span: reedline::Span { + start: current_part_start, + end: pos, + }, + append_whitespace: false, + style: None, + }); + } + } + } + } + + suggestions + } +} + +struct CodeHighliter {} + impl CodeHighliter { fn default() -> Self { - CodeHighliter { - wordlist: default_wordlist(2), - } + CodeHighliter {} } fn is_valid_code(&self, code: &str) -> bool { @@ -105,7 +155,7 @@ impl CodeHighliter { // check all words for validity words.iter().skip(1).all(|&word| { - self.wordlist + WORDLIST .words .iter() .flatten() @@ -131,7 +181,29 @@ impl Highlighter for CodeHighliter { } pub fn enter_code() -> eyre::Result { - let mut line_editor = Reedline::create().with_highlighter(Box::new(CodeHighliter::default())); + // Set up the required keybindings for completion menu + let mut keybindings = default_emacs_keybindings(); + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + + let edit_mode = Box::new(Emacs::new(keybindings)); + + let completion_menu = Box::new(ColumnarMenu::default()); + + let mut line_editor = Reedline::create() + .with_completer(Box::new(CodeCompleter::default())) + .with_highlighter(Box::new(CodeHighliter::default())) + .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) + .with_quick_completions(true) + .with_partial_completions(true) + .with_edit_mode(edit_mode); + let prompt = CodePrompt::default(); loop { From 99bcd6410fb4609add639afbfe601c2be1b70955 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 23:08:32 +0200 Subject: [PATCH 12/59] working tab complete --- cli/src/reedline.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 26dd13bd..e9d854e3 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, process::exit}; -use color_eyre::eyre; +use color_eyre::eyre::{self, bail}; use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; @@ -181,7 +181,7 @@ impl Highlighter for CodeHighliter { } pub fn enter_code() -> eyre::Result { - // Set up the required keybindings for completion menu + // Set up the required keybindings let mut keybindings = default_emacs_keybindings(); keybindings.add_binding( KeyModifiers::NONE, @@ -194,7 +194,7 @@ pub fn enter_code() -> eyre::Result { let edit_mode = Box::new(Emacs::new(keybindings)); - let completion_menu = Box::new(ColumnarMenu::default()); + let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); let mut line_editor = Reedline::create() .with_completer(Box::new(CodeCompleter::default())) @@ -203,14 +203,14 @@ pub fn enter_code() -> eyre::Result { .with_quick_completions(true) .with_partial_completions(true) .with_edit_mode(edit_mode); - let prompt = CodePrompt::default(); loop { let sig = line_editor.read_line(&prompt); match sig { Ok(Signal::Success(buffer)) => return Ok(buffer), - Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => exit(0), + // TODO: resolve this temporary solution + Ok(Signal::CtrlC) => bail!("Ctrl-C received"), _ => {}, } } From c2a294e4c221e5b9886a4c3564ee140ad622ffb5 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 23:25:13 +0200 Subject: [PATCH 13/59] temporary ctrlc workaround --- cli/src/reedline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index e9d854e3..d5af8623 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -209,7 +209,7 @@ pub fn enter_code() -> eyre::Result { let sig = line_editor.read_line(&prompt); match sig { Ok(Signal::Success(buffer)) => return Ok(buffer), - // TODO: resolve this temporary solution + // TODO: fix temporary work around Ok(Signal::CtrlC) => bail!("Ctrl-C received"), _ => {}, } From 98f948ca6f202a7e4302211c25f3a964a0d1a701 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sat, 21 Sep 2024 19:55:22 +0200 Subject: [PATCH 14/59] improved channel code checks --- cli/src/reedline.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index d5af8623..fcd2e988 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -146,15 +146,19 @@ impl CodeHighliter { } fn is_valid_code(&self, code: &str) -> bool { - let words: Vec<&str> = code.split('-').collect(); + let parts: Vec<&str> = code.split('-').collect(); // if the first element in code is not a valid number - if words.first().and_then(|w| w.parse::().ok()).is_none() { + if !parts + .first() + .and_then(|c| c.parse::().ok()) + .is_some_and(|c| (0..1000).contains(&c)) + { return false; } // check all words for validity - words.iter().skip(1).all(|&word| { + parts.iter().skip(1).all(|&word| { WORDLIST .words .iter() From cddb3faa87b82d1e14a1327040655f05c2a01678 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 00:34:37 +0200 Subject: [PATCH 15/59] completer optimizations --- cli/src/reedline.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index fcd2e988..ba65bf25 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -97,22 +97,7 @@ impl Completer for CodeCompleter { let mut suggestions = Vec::new(); if parts.len() == 1 { - // Completing the number part - for i in 1..=255 { - if i.to_string().starts_with(current_part) { - suggestions.push(Suggestion { - value: i.to_string(), - description: None, - extra: None, - span: reedline::Span { - start: current_part_start, - end: pos, - }, - append_whitespace: false, - style: None, - }); - } - } + return suggestions; } else { // Completing a word for word_list in WORDLIST.words.iter() { @@ -215,6 +200,7 @@ pub fn enter_code() -> eyre::Result { Ok(Signal::Success(buffer)) => return Ok(buffer), // TODO: fix temporary work around Ok(Signal::CtrlC) => bail!("Ctrl-C received"), + Ok(Signal::CtrlD) => bail!("Ctrl-D received"), _ => {}, } } From bbdcdbc303654401e528c2a1926e205a395527de Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 19:28:52 +0200 Subject: [PATCH 16/59] working tab completion with jaro winkler algorhythm --- cli/src/reedline.rs | 69 ++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index ba65bf25..db8920da 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,13 +1,15 @@ use std::{borrow::Cow, process::exit}; use color_eyre::eyre::{self, bail}; +use fuzzt::{algorithms::JaroWinkler, get_top_n, processors::NullStringProcessor}; use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ default_emacs_keybindings, ColumnarMenu, Completer, DefaultCompleter, Emacs, Highlighter, Hinter, History, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, - PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, StyledText, Suggestion, + PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, StyledText, + Suggestion, }; lazy_static! { @@ -91,35 +93,50 @@ impl CodeCompleter { impl Completer for CodeCompleter { fn complete(&mut self, line: &str, pos: usize) -> Vec { let parts: Vec<&str> = line.split('-').collect(); - let current_part = parts.last().unwrap_or(&""); - let current_part_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); - - let mut suggestions = Vec::new(); + // Skip autocomplete for the channel number (first part) if parts.len() == 1 { - return suggestions; - } else { - // Completing a word - for word_list in WORDLIST.words.iter() { - for word in word_list.iter() { - if word.starts_with(current_part) { - suggestions.push(Suggestion { - value: word.to_string(), - description: None, - extra: None, - span: reedline::Span { - start: current_part_start, - end: pos, - }, - append_whitespace: false, - style: None, - }); - } - } - } + return Vec::new(); } - suggestions + // Find the start and end of the current word + let current_word_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); + let current_word_end = line[pos..].find('-').map(|i| i + pos).unwrap_or(line.len()); + + let current_part = &line[current_word_start..current_word_end]; + + // Flatten the word list + let all_words: Vec<&str> = WORDLIST + .words + .iter() + .flatten() + .map(|s| s.as_str()) + .collect(); + + // Use fuzzy matching to find the best matches + let matches = get_top_n( + current_part, + &all_words, + Some(0.8), + Some(5), + Some(&NullStringProcessor), + Some(&JaroWinkler), + ); + + matches + .into_iter() + .map(|word| Suggestion { + value: word.to_string(), + description: None, + extra: None, + span: Span { + start: current_word_start, + end: current_word_end, + }, + append_whitespace: false, + style: None, + }) + .collect() } } From 87c4fda533bb7aaa6e23c3cf4b148e9c5cd88ae9 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 20:05:50 +0200 Subject: [PATCH 17/59] added documentation for wordlist module, allow missing_docs for core as of now --- cli/src/reedline.rs | 40 +++----------------- src/core.rs | 2 + src/core/wordlist.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 34 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index db8920da..7e8ed9f2 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, process::exit}; +use std::borrow::Cow; use color_eyre::eyre::{self, bail}; use fuzzt::{algorithms::JaroWinkler, get_top_n, processors::NullStringProcessor}; @@ -6,10 +6,9 @@ use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, DefaultCompleter, Emacs, Highlighter, - Hinter, History, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, - PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, StyledText, - Suggestion, + default_emacs_keybindings, ColumnarMenu, Completer, Emacs, Highlighter, KeyCode, KeyModifiers, + MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, + ReedlineMenu, Signal, Span, StyledText, Suggestion, }; lazy_static! { @@ -53,35 +52,6 @@ impl Prompt for CodePrompt { } } -pub struct CodeHinter {} - -impl CodeHinter { - fn default() -> Self { - CodeHinter {} - } -} - -impl Hinter for CodeHinter { - fn handle( - &mut self, - _line: &str, - _pos: usize, - _history: &dyn History, - _use_ansi_coloring: bool, - _cwd: &str, - ) -> String { - _line.to_string() - } - - fn complete_hint(&self) -> String { - "accepted".to_string() - } - - fn next_hint_token(&self) -> String { - "test".to_string() - } -} - struct CodeCompleter {} impl CodeCompleter { @@ -189,6 +159,7 @@ impl Highlighter for CodeHighliter { pub fn enter_code() -> eyre::Result { // Set up the required keybindings let mut keybindings = default_emacs_keybindings(); + keybindings.add_binding( KeyModifiers::NONE, KeyCode::Tab, @@ -209,6 +180,7 @@ pub fn enter_code() -> eyre::Result { .with_quick_completions(true) .with_partial_completions(true) .with_edit_mode(edit_mode); + let prompt = CodePrompt::default(); loop { diff --git a/src/core.rs b/src/core.rs index 04b1828b..3b21e398 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,6 @@ #![allow(deprecated)] +// TODO: Add missing documentation +#![allow(missing_docs)] pub(super) mod key; pub mod rendezvous; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 5d6ce74c..300a2694 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -2,9 +2,16 @@ use rand::{rngs::OsRng, seq::SliceRandom}; use serde_json::{self, Value}; use std::fmt; +/// Represents a collection of word lists used for generating and completing wormhole codes. +/// +/// The `Wordlist` struct contains multiple lists of words and information about +/// how many words should be in a complete wormhole code. +/// ``` #[derive(PartialEq)] pub struct Wordlist { + /// The number of words that should be in a complete wormhole code. pub num_words: usize, + /// A vector of word lists. Each inner vector represents a distinct list of words. pub words: Vec>, } @@ -20,6 +27,35 @@ impl Wordlist { Wordlist { num_words, words } } + /// Returns a list of word completions based on the given prefix. + /// + /// This method generates completions for a partial wormhole code. It takes into account + /// the number of dashes in the prefix to determine which word list to use for completions. + /// + /// # Arguments + /// + /// * `prefix` - A string slice that holds the partial wormhole code to complete. + /// + /// # Returns + /// + /// A vector of Strings containing all possible completions. + /// + /// # Behavior + /// + /// - The method cycles through word lists based on the number of dashes in the prefix. + /// - It completes the last partial word in the prefix. + /// - If the completion isn't for the last word in the code, it appends a dash. + /// - The returned completions are sorted alphabetically. + /// + /// # Examples + /// + /// ``` + /// use your_crate_name::WordList; + /// + /// let wordlist = WordList::new(); + /// let completions = wordlist.get_completions("7-ze"); + /// assert!(completions.contains(&"7-zebra-".to_string())); + /// ``` #[allow(dead_code)] // TODO make this API public one day pub fn get_completions(&self, prefix: &str) -> Vec { let count_dashes = prefix.matches('-').count(); @@ -55,6 +91,30 @@ impl Wordlist { completions } + /// Generates a random wormhole code. + /// + /// This method creates a new wormhole code by randomly selecting words from the word lists. + /// It ensures that the generated code has the correct number of words as specified by `num_words`. + /// + /// # Returns + /// + /// A String containing the randomly generated wormhole code, with words separated by dashes. + /// + /// # Behavior + /// + /// - Uses the OS's random number generator for secure randomness. + /// - Cycles through the word lists if `num_words` is greater than the number of available lists. + /// - Joins the selected words with dashes to form the final code. + /// + /// # Examples + /// + /// ``` + /// use your_crate_name::WordList; + /// + /// let wordlist = WordList::new(); + /// let code = wordlist.choose_words(); + /// assert_eq!(code.split('-').count(), wordlist.num_words()); + /// ``` pub fn choose_words(&self) -> String { let mut rng = OsRng; let components: Vec = self @@ -96,6 +156,35 @@ fn load_pgpwords() -> Vec> { vec![even_words, odd_words] } +/// Creates a default `Wordlist` with a specified number of words. +/// +/// This function generates a `Wordlist` using the PGP word list, which is a standardized +/// list of words often used for secure and human-readable encoding. +/// +/// # Arguments +/// +/// * `num_words` - The number of words that should be in a complete wormhole code. +/// +/// # Returns +/// +/// Returns a `Wordlist` instance initialized with the PGP word list and the specified +/// number of words. +/// +/// # Examples +/// +/// ``` +/// use your_crate_name::default_wordlist; +/// +/// let wordlist = default_wordlist(3); +/// assert_eq!(wordlist.num_words, 3); +/// assert_eq!(wordlist.words.len(), 2); // PGP word list has two lists (even and odd) +/// ``` +/// +/// # Note +/// +/// The PGP word list consists of two lists of 256 words each: one for even positions +/// and one for odd positions in the code. This function uses these lists regardless +/// of the `num_words` specified. pub fn default_wordlist(num_words: usize) -> Wordlist { Wordlist { num_words, From 4fba92624cc1ca311fa7c3d549645483638a6f59 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 23:52:26 +0200 Subject: [PATCH 18/59] bail on error while reading wormhole code --- cli/src/reedline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 7e8ed9f2..dca0dbeb 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -190,7 +190,7 @@ pub fn enter_code() -> eyre::Result { // TODO: fix temporary work around Ok(Signal::CtrlC) => bail!("Ctrl-C received"), Ok(Signal::CtrlD) => bail!("Ctrl-D received"), - _ => {}, + Err(e) => bail!(e), } } } From 6ff966d10e06de23939c43079d06cd472c10a16b Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Thu, 5 Sep 2024 19:27:52 +0200 Subject: [PATCH 19/59] clipboard as a feature for android and server support --- cli/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index ebe3ba5a..47cb34e8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -43,7 +43,6 @@ fn install_ctrlc_handler( }) }) .context("Error setting Ctrl-C handler")?; - Ok(move || { /* Transform the notification into a future that waits */ let notifier = notifier.clone(); From f450a27a2ebbfd9ce3108143553650ff16926ef2 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 10 Sep 2024 18:09:09 +0200 Subject: [PATCH 20/59] added whitespace for readability --- cli/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index 47cb34e8..ebe3ba5a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -43,6 +43,7 @@ fn install_ctrlc_handler( }) }) .context("Error setting Ctrl-C handler")?; + Ok(move || { /* Transform the notification into a future that waits */ let notifier = notifier.clone(); From 8ba3e690cb566c4ed454a1b4a097b9950d84f7d0 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 00:11:36 +0200 Subject: [PATCH 21/59] first working draft of code word spelling checks --- Cargo.lock | 214 ++++++++++++++++++++++++++++++++++++++++++- cli/Cargo.toml | 3 + cli/src/main.rs | 11 +-- cli/src/reedline.rs | 143 +++++++++++++++++++++++++++++ src/core.rs | 2 +- src/core/wordlist.rs | 2 +- src/lib.rs | 2 +- 7 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 cli/src/reedline.rs diff --git a/Cargo.lock b/Cargo.lock index c4481180..7b7a430e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.15" @@ -454,6 +469,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "blake2" @@ -592,6 +610,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "cipher" version = "0.4.4" @@ -832,13 +863,30 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio 1.0.2", + "parking_lot 0.12.3", + "rustix 0.38.34", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1132,6 +1180,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix 0.38.34", + "windows-sys 0.52.0", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -1510,6 +1569,29 @@ dependencies = [ "serde", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1607,6 +1689,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1759,9 +1850,12 @@ dependencies = [ "futures", "indicatif", "magic-wormhole", + "nu-ansi-term 0.50.1", "number_prefix", "qr2term", "rand", + "reedline", + "regex", "serde", "serde_derive", "serde_json", @@ -1829,6 +1923,19 @@ dependencies = [ "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 = "native-tls" version = "0.2.12" @@ -1920,6 +2027,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2345,7 +2461,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c2a1e77b5cd714b04247ad912b7c8fe9a1fe1d58425048249def91bcf690e4c" dependencies = [ - "crossterm", + "crossterm 0.25.0", "qrcode", ] @@ -2444,6 +2560,26 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "reedline" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5289de810296f8f2ff58d35544d92ae98d0a631453388bc3e608086be0fa596" +dependencies = [ + "chrono", + "crossterm 0.28.1", + "fd-lock", + "itertools", + "nu-ansi-term 0.50.1", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "regex" version = "1.10.6" @@ -2598,6 +2734,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -2800,7 +2942,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.2", "signal-hook", ] @@ -2909,12 +3052,40 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.76", +] + [[package]] name = "stun_codec" version = "0.3.5" @@ -3174,7 +3345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.46.0", "once_cell", "regex", "sharded-slab", @@ -3291,6 +3462,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.13" @@ -3367,6 +3544,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" @@ -3619,6 +3816,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2bb5ea74..de89aa15 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -43,6 +43,9 @@ arboard = { optional = true, workspace = true, features = [ ] } # Wayland by default, fallback to X11. tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } +reedline = "0.35.0" +regex = "1.10.6" +nu-ansi-term = "0.50.1" [dev-dependencies] trycmd = { workspace = true } diff --git a/cli/src/main.rs b/cli/src/main.rs index ebe3ba5a..c779dbb4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,4 +1,5 @@ #![allow(clippy::too_many_arguments)] +mod reedline; mod util; use std::time::{Duration, Instant}; @@ -14,6 +15,7 @@ use magic_wormhole::{ transit::{self, TransitInfo}, MailboxConnection, Wormhole, }; +use reedline::enter_code; use std::{io::Write, path::PathBuf}; use tracing_subscriber::EnvFilter; @@ -710,15 +712,6 @@ fn create_progress_handler(pb: ProgressBar) -> impl FnMut(u64, u64) { } } -fn enter_code() -> eyre::Result { - use dialoguer::Input; - - Input::new() - .with_prompt("Enter code") - .interact_text() - .map_err(From::from) -} - fn print_welcome(term: &mut Term, welcome: Option<&str>) -> eyre::Result<()> { if let Some(welcome) = &welcome { writeln!(term, "Got welcome from server: {}", welcome)?; diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs new file mode 100644 index 00000000..f47aa40f --- /dev/null +++ b/cli/src/reedline.rs @@ -0,0 +1,143 @@ +use std::{borrow::Cow, process::exit}; + +use color_eyre::eyre; +use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; +use nu_ansi_term::{Color, Style}; +use reedline::{ + Highlighter, Hinter, History, Prompt, PromptEditMode, PromptHistorySearch, Reedline, Signal, + StyledText, +}; +use regex::Regex; + +struct CodePrompt {} + +impl CodePrompt { + fn default() -> Self { + CodePrompt {} + } +} + +impl Prompt for CodePrompt { + fn render_prompt_left(&self) -> Cow<'_, str> { + Cow::Borrowed("Wormhole Code: ") + } + + fn render_prompt_right(&self) -> Cow<'_, str> { + Cow::Borrowed("") + } + + fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> { + Cow::Borrowed("") + } + + fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { + Cow::Borrowed("... ") + } + + fn render_prompt_history_search_indicator( + &self, + _history_search: PromptHistorySearch, + ) -> Cow<'_, str> { + Cow::Borrowed("") + } + + // Optionally override provided methods + // fn get_prompt_color(&self) -> Color { ... } + // fn get_prompt_multiline_color(&self) -> Color { ... } + // fn get_indicator_color(&self) -> Color { ... } + // fn get_prompt_right_color(&self) -> Color { ... } + // fn right_prompt_on_last_line(&self) -> bool { ... } +} + +pub struct CodeHinter { + wordlist: Wordlist, +} + +impl CodeHinter { + fn default() -> Self { + CodeHinter { + wordlist: default_wordlist(2), + } + } +} + +impl Hinter for CodeHinter { + fn handle( + &mut self, + _line: &str, + _pos: usize, + _history: &dyn History, + _use_ansi_coloring: bool, + _cwd: &str, + ) -> String { + "".to_string() + } + + fn complete_hint(&self) -> String { + "".to_string() + } + + fn next_hint_token(&self) -> String { + "".to_string() + } +} + +struct CodeHighliter { + regex: Regex, + wordlist: Wordlist, +} + +impl CodeHighliter { + fn default() -> Self { + CodeHighliter { + regex: Regex::new(r"^\d+(-[a-z]+)+$").unwrap(), + wordlist: default_wordlist(2), + } + } + + fn is_valid_code(&self, code: &str) -> bool { + if !self.regex.is_match(code) { + return false; + } + + let words: Vec<&str> = code.split('-').skip(1).collect(); + + words.iter().all(|&word| { + self.wordlist + .words + .iter() + .flatten() + .any(|valid_word| valid_word == word) + }) + } +} + +impl Highlighter for CodeHighliter { + fn highlight(&self, line: &str, _cursor: usize) -> StyledText { + let invalid = Style::new().fg(Color::White); + let valid = Style::new().fg(Color::Green); + + let style = match self.is_valid_code(line) { + true => valid, + false => invalid, + }; + + let mut t = StyledText::new(); + t.push((style, line.to_string())); + t + } +} + +pub fn enter_code() -> eyre::Result { + let mut line_editor = Reedline::create().with_highlighter(Box::new(CodeHighliter::default())); + let prompt = CodePrompt::default(); + + loop { + let sig = line_editor.read_line(&prompt); + match sig { + Ok(Signal::Success(buffer)) => return Ok(buffer), + Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => exit(0), + _ => {}, + } + } +} diff --git a/src/core.rs b/src/core.rs index 8844ba49..04b1828b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,7 +5,7 @@ pub mod rendezvous; mod server_messages; #[cfg(test)] mod test; -mod wordlist; +pub mod wordlist; use serde_derive::{Deserialize, Serialize}; use std::borrow::Cow; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 966d82ef..5d6ce74c 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -5,7 +5,7 @@ use std::fmt; #[derive(PartialEq)] pub struct Wordlist { pub num_words: usize, - words: Vec>, + pub words: Vec>, } impl fmt::Debug for Wordlist { diff --git a/src/lib.rs b/src/lib.rs index 03702736..94c13289 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ #[macro_use] mod util; -mod core; +pub mod core; #[cfg(feature = "forwarding")] pub mod forwarding; #[cfg(feature = "transfer")] From 7abbb5001cc3b1ee157310e1b5088e89ed052c8b Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 00:46:51 +0200 Subject: [PATCH 22/59] removed regex as it is not needed anymore --- Cargo.lock | 1 - Cargo.toml | 1 - cli/Cargo.toml | 1 - cli/src/reedline.rs | 13 ++++++------- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b7a430e..4dd1cbed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1855,7 +1855,6 @@ dependencies = [ "qr2term", "rand", "reedline", - "regex", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 0b3b7c15..03881e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ futures = "0.3.12" hex = "0.4.2" hkdf = "0.12.2" indicatif = "0.17.0" -log = "0.4.13" noise-protocol = "0.2" noise-rust-crypto = "0.6.0-rc.1" number_prefix = "0.4.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index de89aa15..57466c05 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -44,7 +44,6 @@ arboard = { optional = true, workspace = true, features = [ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" -regex = "1.10.6" nu-ansi-term = "0.50.1" [dev-dependencies] diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index f47aa40f..5e99403f 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -7,7 +7,6 @@ use reedline::{ Highlighter, Hinter, History, Prompt, PromptEditMode, PromptHistorySearch, Reedline, Signal, StyledText, }; -use regex::Regex; struct CodePrompt {} @@ -83,26 +82,26 @@ impl Hinter for CodeHinter { } struct CodeHighliter { - regex: Regex, wordlist: Wordlist, } impl CodeHighliter { fn default() -> Self { CodeHighliter { - regex: Regex::new(r"^\d+(-[a-z]+)+$").unwrap(), wordlist: default_wordlist(2), } } fn is_valid_code(&self, code: &str) -> bool { - if !self.regex.is_match(code) { + let words: Vec<&str> = code.split('-').collect(); + + // if the first element in code is not a valid number + if words.first().and_then(|w| w.parse::().ok()).is_none() { return false; } - let words: Vec<&str> = code.split('-').skip(1).collect(); - - words.iter().all(|&word| { + // check all words for validity + words.iter().skip(1).all(|&word| { self.wordlist .words .iter() From ba6ce021cbeecf8e3d58950c467c47d8672f1fe7 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 00:59:24 +0200 Subject: [PATCH 23/59] improved coloring --- cli/src/reedline.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 5e99403f..0d477616 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -40,6 +40,9 @@ impl Prompt for CodePrompt { Cow::Borrowed("") } + fn get_prompt_color(&self) -> reedline::Color { + reedline::Color::Grey + } // Optionally override provided methods // fn get_prompt_color(&self) -> Color { ... } // fn get_prompt_multiline_color(&self) -> Color { ... } @@ -113,7 +116,7 @@ impl CodeHighliter { impl Highlighter for CodeHighliter { fn highlight(&self, line: &str, _cursor: usize) -> StyledText { - let invalid = Style::new().fg(Color::White); + let invalid = Style::new().fg(Color::Red); let valid = Style::new().fg(Color::Green); let style = match self.is_valid_code(line) { From 780621747680fc5d4f4ec963bff0423e94c356f5 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 22:50:07 +0200 Subject: [PATCH 24/59] first working tab completion --- Cargo.lock | 8 +++ cli/Cargo.toml | 2 + cli/src/reedline.rs | 120 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 106 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dd1cbed..b5ad26f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,6 +1408,12 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a15f3d0fa42283a765e5fb609683ddab4ee4ff245d8db66a24d926c05e518c6" + [[package]] name = "generic-array" version = "0.14.7" @@ -1848,7 +1854,9 @@ dependencies = [ "dialoguer", "env_logger", "futures", + "fuzzt", "indicatif", + "lazy_static", "magic-wormhole", "nu-ansi-term 0.50.1", "number_prefix", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 57466c05..26a419de 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -45,6 +45,8 @@ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" nu-ansi-term = "0.50.1" +lazy_static = "1.5.0" +fuzzt = "0.3.1" [dev-dependencies] trycmd = { workspace = true } diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 0d477616..26dd13bd 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,13 +1,19 @@ use std::{borrow::Cow, process::exit}; use color_eyre::eyre; +use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ - Highlighter, Hinter, History, Prompt, PromptEditMode, PromptHistorySearch, Reedline, Signal, - StyledText, + default_emacs_keybindings, ColumnarMenu, Completer, DefaultCompleter, Emacs, Highlighter, + Hinter, History, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, + PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, StyledText, Suggestion, }; +lazy_static! { + static ref WORDLIST: Wordlist = default_wordlist(2); +} + struct CodePrompt {} impl CodePrompt { @@ -43,23 +49,13 @@ impl Prompt for CodePrompt { fn get_prompt_color(&self) -> reedline::Color { reedline::Color::Grey } - // Optionally override provided methods - // fn get_prompt_color(&self) -> Color { ... } - // fn get_prompt_multiline_color(&self) -> Color { ... } - // fn get_indicator_color(&self) -> Color { ... } - // fn get_prompt_right_color(&self) -> Color { ... } - // fn right_prompt_on_last_line(&self) -> bool { ... } } -pub struct CodeHinter { - wordlist: Wordlist, -} +pub struct CodeHinter {} impl CodeHinter { fn default() -> Self { - CodeHinter { - wordlist: default_wordlist(2), - } + CodeHinter {} } } @@ -72,27 +68,81 @@ impl Hinter for CodeHinter { _use_ansi_coloring: bool, _cwd: &str, ) -> String { - "".to_string() + _line.to_string() } fn complete_hint(&self) -> String { - "".to_string() + "accepted".to_string() } fn next_hint_token(&self) -> String { - "".to_string() + "test".to_string() } } -struct CodeHighliter { - wordlist: Wordlist, +struct CodeCompleter {} + +impl CodeCompleter { + fn default() -> Self { + CodeCompleter {} + } } +impl Completer for CodeCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + let parts: Vec<&str> = line.split('-').collect(); + let current_part = parts.last().unwrap_or(&""); + let current_part_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); + + let mut suggestions = Vec::new(); + + if parts.len() == 1 { + // Completing the number part + for i in 1..=255 { + if i.to_string().starts_with(current_part) { + suggestions.push(Suggestion { + value: i.to_string(), + description: None, + extra: None, + span: reedline::Span { + start: current_part_start, + end: pos, + }, + append_whitespace: false, + style: None, + }); + } + } + } else { + // Completing a word + for word_list in WORDLIST.words.iter() { + for word in word_list.iter() { + if word.starts_with(current_part) { + suggestions.push(Suggestion { + value: word.to_string(), + description: None, + extra: None, + span: reedline::Span { + start: current_part_start, + end: pos, + }, + append_whitespace: false, + style: None, + }); + } + } + } + } + + suggestions + } +} + +struct CodeHighliter {} + impl CodeHighliter { fn default() -> Self { - CodeHighliter { - wordlist: default_wordlist(2), - } + CodeHighliter {} } fn is_valid_code(&self, code: &str) -> bool { @@ -105,7 +155,7 @@ impl CodeHighliter { // check all words for validity words.iter().skip(1).all(|&word| { - self.wordlist + WORDLIST .words .iter() .flatten() @@ -131,7 +181,29 @@ impl Highlighter for CodeHighliter { } pub fn enter_code() -> eyre::Result { - let mut line_editor = Reedline::create().with_highlighter(Box::new(CodeHighliter::default())); + // Set up the required keybindings for completion menu + let mut keybindings = default_emacs_keybindings(); + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + + let edit_mode = Box::new(Emacs::new(keybindings)); + + let completion_menu = Box::new(ColumnarMenu::default()); + + let mut line_editor = Reedline::create() + .with_completer(Box::new(CodeCompleter::default())) + .with_highlighter(Box::new(CodeHighliter::default())) + .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) + .with_quick_completions(true) + .with_partial_completions(true) + .with_edit_mode(edit_mode); + let prompt = CodePrompt::default(); loop { From c80520d8cf07a0a35b94ef145e8928a2ff1bebcd Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 23:08:32 +0200 Subject: [PATCH 25/59] working tab complete --- cli/src/reedline.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 26dd13bd..e9d854e3 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, process::exit}; -use color_eyre::eyre; +use color_eyre::eyre::{self, bail}; use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; @@ -181,7 +181,7 @@ impl Highlighter for CodeHighliter { } pub fn enter_code() -> eyre::Result { - // Set up the required keybindings for completion menu + // Set up the required keybindings let mut keybindings = default_emacs_keybindings(); keybindings.add_binding( KeyModifiers::NONE, @@ -194,7 +194,7 @@ pub fn enter_code() -> eyre::Result { let edit_mode = Box::new(Emacs::new(keybindings)); - let completion_menu = Box::new(ColumnarMenu::default()); + let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); let mut line_editor = Reedline::create() .with_completer(Box::new(CodeCompleter::default())) @@ -203,14 +203,14 @@ pub fn enter_code() -> eyre::Result { .with_quick_completions(true) .with_partial_completions(true) .with_edit_mode(edit_mode); - let prompt = CodePrompt::default(); loop { let sig = line_editor.read_line(&prompt); match sig { Ok(Signal::Success(buffer)) => return Ok(buffer), - Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => exit(0), + // TODO: resolve this temporary solution + Ok(Signal::CtrlC) => bail!("Ctrl-C received"), _ => {}, } } From 9b0d6c9de78656194c3bd6b28be9433afb51a8a3 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 20 Sep 2024 23:25:13 +0200 Subject: [PATCH 26/59] temporary ctrlc workaround --- cli/src/reedline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index e9d854e3..d5af8623 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -209,7 +209,7 @@ pub fn enter_code() -> eyre::Result { let sig = line_editor.read_line(&prompt); match sig { Ok(Signal::Success(buffer)) => return Ok(buffer), - // TODO: resolve this temporary solution + // TODO: fix temporary work around Ok(Signal::CtrlC) => bail!("Ctrl-C received"), _ => {}, } From 4abfb29cbc2ab219bbcdcb09a4fbd5335ac6a62f Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sat, 21 Sep 2024 19:55:22 +0200 Subject: [PATCH 27/59] improved channel code checks --- cli/src/reedline.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index d5af8623..fcd2e988 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -146,15 +146,19 @@ impl CodeHighliter { } fn is_valid_code(&self, code: &str) -> bool { - let words: Vec<&str> = code.split('-').collect(); + let parts: Vec<&str> = code.split('-').collect(); // if the first element in code is not a valid number - if words.first().and_then(|w| w.parse::().ok()).is_none() { + if !parts + .first() + .and_then(|c| c.parse::().ok()) + .is_some_and(|c| (0..1000).contains(&c)) + { return false; } // check all words for validity - words.iter().skip(1).all(|&word| { + parts.iter().skip(1).all(|&word| { WORDLIST .words .iter() From d269554a0408c012a5097090e9a36c1614c3e239 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 00:34:37 +0200 Subject: [PATCH 28/59] completer optimizations --- cli/src/reedline.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index fcd2e988..ba65bf25 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -97,22 +97,7 @@ impl Completer for CodeCompleter { let mut suggestions = Vec::new(); if parts.len() == 1 { - // Completing the number part - for i in 1..=255 { - if i.to_string().starts_with(current_part) { - suggestions.push(Suggestion { - value: i.to_string(), - description: None, - extra: None, - span: reedline::Span { - start: current_part_start, - end: pos, - }, - append_whitespace: false, - style: None, - }); - } - } + return suggestions; } else { // Completing a word for word_list in WORDLIST.words.iter() { @@ -215,6 +200,7 @@ pub fn enter_code() -> eyre::Result { Ok(Signal::Success(buffer)) => return Ok(buffer), // TODO: fix temporary work around Ok(Signal::CtrlC) => bail!("Ctrl-C received"), + Ok(Signal::CtrlD) => bail!("Ctrl-D received"), _ => {}, } } From 9c3a26b32930734b56c25186bce940404407997a Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 19:28:52 +0200 Subject: [PATCH 29/59] working tab completion with jaro winkler algorhythm --- cli/src/reedline.rs | 69 ++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index ba65bf25..db8920da 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,13 +1,15 @@ use std::{borrow::Cow, process::exit}; use color_eyre::eyre::{self, bail}; +use fuzzt::{algorithms::JaroWinkler, get_top_n, processors::NullStringProcessor}; use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ default_emacs_keybindings, ColumnarMenu, Completer, DefaultCompleter, Emacs, Highlighter, Hinter, History, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, - PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, StyledText, Suggestion, + PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, StyledText, + Suggestion, }; lazy_static! { @@ -91,35 +93,50 @@ impl CodeCompleter { impl Completer for CodeCompleter { fn complete(&mut self, line: &str, pos: usize) -> Vec { let parts: Vec<&str> = line.split('-').collect(); - let current_part = parts.last().unwrap_or(&""); - let current_part_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); - - let mut suggestions = Vec::new(); + // Skip autocomplete for the channel number (first part) if parts.len() == 1 { - return suggestions; - } else { - // Completing a word - for word_list in WORDLIST.words.iter() { - for word in word_list.iter() { - if word.starts_with(current_part) { - suggestions.push(Suggestion { - value: word.to_string(), - description: None, - extra: None, - span: reedline::Span { - start: current_part_start, - end: pos, - }, - append_whitespace: false, - style: None, - }); - } - } - } + return Vec::new(); } - suggestions + // Find the start and end of the current word + let current_word_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); + let current_word_end = line[pos..].find('-').map(|i| i + pos).unwrap_or(line.len()); + + let current_part = &line[current_word_start..current_word_end]; + + // Flatten the word list + let all_words: Vec<&str> = WORDLIST + .words + .iter() + .flatten() + .map(|s| s.as_str()) + .collect(); + + // Use fuzzy matching to find the best matches + let matches = get_top_n( + current_part, + &all_words, + Some(0.8), + Some(5), + Some(&NullStringProcessor), + Some(&JaroWinkler), + ); + + matches + .into_iter() + .map(|word| Suggestion { + value: word.to_string(), + description: None, + extra: None, + span: Span { + start: current_word_start, + end: current_word_end, + }, + append_whitespace: false, + style: None, + }) + .collect() } } From 2e7f0389c82b79c2dcb74a918a096a9b36be42a6 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 20:05:50 +0200 Subject: [PATCH 30/59] added documentation for wordlist module, allow missing_docs for core as of now --- cli/src/reedline.rs | 40 +++----------------- src/core.rs | 2 + src/core/wordlist.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 34 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index db8920da..7e8ed9f2 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, process::exit}; +use std::borrow::Cow; use color_eyre::eyre::{self, bail}; use fuzzt::{algorithms::JaroWinkler, get_top_n, processors::NullStringProcessor}; @@ -6,10 +6,9 @@ use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, DefaultCompleter, Emacs, Highlighter, - Hinter, History, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, - PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, StyledText, - Suggestion, + default_emacs_keybindings, ColumnarMenu, Completer, Emacs, Highlighter, KeyCode, KeyModifiers, + MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, + ReedlineMenu, Signal, Span, StyledText, Suggestion, }; lazy_static! { @@ -53,35 +52,6 @@ impl Prompt for CodePrompt { } } -pub struct CodeHinter {} - -impl CodeHinter { - fn default() -> Self { - CodeHinter {} - } -} - -impl Hinter for CodeHinter { - fn handle( - &mut self, - _line: &str, - _pos: usize, - _history: &dyn History, - _use_ansi_coloring: bool, - _cwd: &str, - ) -> String { - _line.to_string() - } - - fn complete_hint(&self) -> String { - "accepted".to_string() - } - - fn next_hint_token(&self) -> String { - "test".to_string() - } -} - struct CodeCompleter {} impl CodeCompleter { @@ -189,6 +159,7 @@ impl Highlighter for CodeHighliter { pub fn enter_code() -> eyre::Result { // Set up the required keybindings let mut keybindings = default_emacs_keybindings(); + keybindings.add_binding( KeyModifiers::NONE, KeyCode::Tab, @@ -209,6 +180,7 @@ pub fn enter_code() -> eyre::Result { .with_quick_completions(true) .with_partial_completions(true) .with_edit_mode(edit_mode); + let prompt = CodePrompt::default(); loop { diff --git a/src/core.rs b/src/core.rs index 04b1828b..3b21e398 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,6 @@ #![allow(deprecated)] +// TODO: Add missing documentation +#![allow(missing_docs)] pub(super) mod key; pub mod rendezvous; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 5d6ce74c..300a2694 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -2,9 +2,16 @@ use rand::{rngs::OsRng, seq::SliceRandom}; use serde_json::{self, Value}; use std::fmt; +/// Represents a collection of word lists used for generating and completing wormhole codes. +/// +/// The `Wordlist` struct contains multiple lists of words and information about +/// how many words should be in a complete wormhole code. +/// ``` #[derive(PartialEq)] pub struct Wordlist { + /// The number of words that should be in a complete wormhole code. pub num_words: usize, + /// A vector of word lists. Each inner vector represents a distinct list of words. pub words: Vec>, } @@ -20,6 +27,35 @@ impl Wordlist { Wordlist { num_words, words } } + /// Returns a list of word completions based on the given prefix. + /// + /// This method generates completions for a partial wormhole code. It takes into account + /// the number of dashes in the prefix to determine which word list to use for completions. + /// + /// # Arguments + /// + /// * `prefix` - A string slice that holds the partial wormhole code to complete. + /// + /// # Returns + /// + /// A vector of Strings containing all possible completions. + /// + /// # Behavior + /// + /// - The method cycles through word lists based on the number of dashes in the prefix. + /// - It completes the last partial word in the prefix. + /// - If the completion isn't for the last word in the code, it appends a dash. + /// - The returned completions are sorted alphabetically. + /// + /// # Examples + /// + /// ``` + /// use your_crate_name::WordList; + /// + /// let wordlist = WordList::new(); + /// let completions = wordlist.get_completions("7-ze"); + /// assert!(completions.contains(&"7-zebra-".to_string())); + /// ``` #[allow(dead_code)] // TODO make this API public one day pub fn get_completions(&self, prefix: &str) -> Vec { let count_dashes = prefix.matches('-').count(); @@ -55,6 +91,30 @@ impl Wordlist { completions } + /// Generates a random wormhole code. + /// + /// This method creates a new wormhole code by randomly selecting words from the word lists. + /// It ensures that the generated code has the correct number of words as specified by `num_words`. + /// + /// # Returns + /// + /// A String containing the randomly generated wormhole code, with words separated by dashes. + /// + /// # Behavior + /// + /// - Uses the OS's random number generator for secure randomness. + /// - Cycles through the word lists if `num_words` is greater than the number of available lists. + /// - Joins the selected words with dashes to form the final code. + /// + /// # Examples + /// + /// ``` + /// use your_crate_name::WordList; + /// + /// let wordlist = WordList::new(); + /// let code = wordlist.choose_words(); + /// assert_eq!(code.split('-').count(), wordlist.num_words()); + /// ``` pub fn choose_words(&self) -> String { let mut rng = OsRng; let components: Vec = self @@ -96,6 +156,35 @@ fn load_pgpwords() -> Vec> { vec![even_words, odd_words] } +/// Creates a default `Wordlist` with a specified number of words. +/// +/// This function generates a `Wordlist` using the PGP word list, which is a standardized +/// list of words often used for secure and human-readable encoding. +/// +/// # Arguments +/// +/// * `num_words` - The number of words that should be in a complete wormhole code. +/// +/// # Returns +/// +/// Returns a `Wordlist` instance initialized with the PGP word list and the specified +/// number of words. +/// +/// # Examples +/// +/// ``` +/// use your_crate_name::default_wordlist; +/// +/// let wordlist = default_wordlist(3); +/// assert_eq!(wordlist.num_words, 3); +/// assert_eq!(wordlist.words.len(), 2); // PGP word list has two lists (even and odd) +/// ``` +/// +/// # Note +/// +/// The PGP word list consists of two lists of 256 words each: one for even positions +/// and one for odd positions in the code. This function uses these lists regardless +/// of the `num_words` specified. pub fn default_wordlist(num_words: usize) -> Wordlist { Wordlist { num_words, From 487733b129d39e2fa83c67eaafe9818d1704e840 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sun, 22 Sep 2024 23:52:26 +0200 Subject: [PATCH 31/59] bail on error while reading wormhole code --- cli/src/reedline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 7e8ed9f2..dca0dbeb 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -190,7 +190,7 @@ pub fn enter_code() -> eyre::Result { // TODO: fix temporary work around Ok(Signal::CtrlC) => bail!("Ctrl-C received"), Ok(Signal::CtrlD) => bail!("Ctrl-D received"), - _ => {}, + Err(e) => bail!(e), } } } From 6782f5fe6e647e070507a9c2ce35e515b60e2787 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Mon, 23 Sep 2024 23:24:43 +0200 Subject: [PATCH 32/59] fixed index out of range crash when suggestion > current word, added unit tests for tab completion --- cli/src/reedline.rs | 89 ++++++++++++++++++++++++++++++++++++-------- src/core/wordlist.rs | 1 - 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index dca0dbeb..c5e5b2bf 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -28,14 +28,17 @@ impl Prompt for CodePrompt { Cow::Borrowed("Wormhole Code: ") } + // Not needed fn render_prompt_right(&self) -> Cow<'_, str> { Cow::Borrowed("") } + // Not needed fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> { Cow::Borrowed("") } + // Not needed fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { Cow::Borrowed("... ") } @@ -65,13 +68,16 @@ impl Completer for CodeCompleter { let parts: Vec<&str> = line.split('-').collect(); // Skip autocomplete for the channel number (first part) - if parts.len() == 1 { + if parts.len() <= 1 { return Vec::new(); } // Find the start and end of the current word let current_word_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); - let current_word_end = line[pos..].find('-').map(|i| i + pos).unwrap_or(line.len()); + let current_word_end = line[pos..] + .find('-') + .map(|i| i + pos) + .unwrap_or_else(|| line.len()); let current_part = &line[current_word_start..current_word_end]; @@ -95,16 +101,28 @@ impl Completer for CodeCompleter { matches .into_iter() - .map(|word| Suggestion { - value: word.to_string(), - description: None, - extra: None, - span: Span { - start: current_word_start, - end: current_word_end, - }, - append_whitespace: false, - style: None, + .map(|word| { + let suggestion = word.to_string(); + + // Incase suggestion word length is larger then the current typed part + // Otherwise we get index out of range error in Span + let span_end = if suggestion.len() >= current_part.len() { + current_word_end + } else { + current_word_start + suggestion.len() + }; + + Suggestion { + value: suggestion, + description: None, + extra: None, + span: Span { + start: current_word_start, + end: span_end, + }, + append_whitespace: false, + style: None, + } }) .collect() } @@ -120,7 +138,7 @@ impl CodeHighliter { fn is_valid_code(&self, code: &str) -> bool { let parts: Vec<&str> = code.split('-').collect(); - // if the first element in code is not a valid number + // If the first element in code is not a valid number if !parts .first() .and_then(|c| c.parse::().ok()) @@ -129,7 +147,7 @@ impl CodeHighliter { return false; } - // check all words for validity + // Check all words for validity parts.iter().skip(1).all(|&word| { WORDLIST .words @@ -142,8 +160,8 @@ impl CodeHighliter { impl Highlighter for CodeHighliter { fn highlight(&self, line: &str, _cursor: usize) -> StyledText { - let invalid = Style::new().fg(Color::Red); - let valid = Style::new().fg(Color::Green); + let invalid = Style::new().fg(Color::Red).bold(); + let valid = Style::new().fg(Color::Green).bold(); let style = match self.is_valid_code(line) { true => valid, @@ -194,3 +212,42 @@ pub fn enter_code() -> eyre::Result { } } } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_tab_compeltion_complete_word() { + let mut completer = CodeCompleter::default(); + let input = "22-trombonist"; + let cursor_pos = input.len(); + + let suggestions = completer.complete(&input, cursor_pos); + + assert_eq!(suggestions.len(), 3); + + assert_eq!(suggestions.first().unwrap().value, "trombonist"); + } + + #[test] + fn test_tab_compeltion_partial_word() { + let mut completer = CodeCompleter::default(); + let input = "22-trmbn"; + let cursor_pos = input.len(); + + let suggestions = completer.complete(&input, cursor_pos); + + assert_eq!(suggestions.first().unwrap().value, "trombonist"); + } + + #[test] + fn test_tab_compeltion_partial_in_middle() { + let mut completer = CodeCompleter::default(); + let input = "22-trbis-zulu"; + let cursor_pos = input.len() - "-zulu".len(); + + let suggestions = completer.complete(&input, cursor_pos); + + assert_eq!(suggestions.first().unwrap().value, "trombonist"); + } +} diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 300a2694..b29da825 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -6,7 +6,6 @@ use std::fmt; /// /// The `Wordlist` struct contains multiple lists of words and information about /// how many words should be in a complete wormhole code. -/// ``` #[derive(PartialEq)] pub struct Wordlist { /// The number of words that should be in a complete wormhole code. From 6a134360527a9c2c0157640d5a653072b4f851fd Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 24 Sep 2024 01:41:31 +0200 Subject: [PATCH 33/59] removed temporary documentation, fixed clippy::never_loop --- cli/src/reedline.rs | 16 ++++---- src/core/wordlist.rs | 88 -------------------------------------------- 2 files changed, 7 insertions(+), 97 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index c5e5b2bf..2ccf1d0d 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -201,15 +201,13 @@ pub fn enter_code() -> eyre::Result { let prompt = CodePrompt::default(); - loop { - let sig = line_editor.read_line(&prompt); - match sig { - Ok(Signal::Success(buffer)) => return Ok(buffer), - // TODO: fix temporary work around - Ok(Signal::CtrlC) => bail!("Ctrl-C received"), - Ok(Signal::CtrlD) => bail!("Ctrl-D received"), - Err(e) => bail!(e), - } + let sig = line_editor.read_line(&prompt); + match sig { + Ok(Signal::Success(buffer)) => return Ok(buffer), + // TODO: fix temporary work around + Ok(Signal::CtrlC) => bail!("Ctrl-C received"), + Ok(Signal::CtrlD) => bail!("Ctrl-D received"), + Err(e) => bail!(e), } } diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index b29da825..5d6ce74c 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -2,15 +2,9 @@ use rand::{rngs::OsRng, seq::SliceRandom}; use serde_json::{self, Value}; use std::fmt; -/// Represents a collection of word lists used for generating and completing wormhole codes. -/// -/// The `Wordlist` struct contains multiple lists of words and information about -/// how many words should be in a complete wormhole code. #[derive(PartialEq)] pub struct Wordlist { - /// The number of words that should be in a complete wormhole code. pub num_words: usize, - /// A vector of word lists. Each inner vector represents a distinct list of words. pub words: Vec>, } @@ -26,35 +20,6 @@ impl Wordlist { Wordlist { num_words, words } } - /// Returns a list of word completions based on the given prefix. - /// - /// This method generates completions for a partial wormhole code. It takes into account - /// the number of dashes in the prefix to determine which word list to use for completions. - /// - /// # Arguments - /// - /// * `prefix` - A string slice that holds the partial wormhole code to complete. - /// - /// # Returns - /// - /// A vector of Strings containing all possible completions. - /// - /// # Behavior - /// - /// - The method cycles through word lists based on the number of dashes in the prefix. - /// - It completes the last partial word in the prefix. - /// - If the completion isn't for the last word in the code, it appends a dash. - /// - The returned completions are sorted alphabetically. - /// - /// # Examples - /// - /// ``` - /// use your_crate_name::WordList; - /// - /// let wordlist = WordList::new(); - /// let completions = wordlist.get_completions("7-ze"); - /// assert!(completions.contains(&"7-zebra-".to_string())); - /// ``` #[allow(dead_code)] // TODO make this API public one day pub fn get_completions(&self, prefix: &str) -> Vec { let count_dashes = prefix.matches('-').count(); @@ -90,30 +55,6 @@ impl Wordlist { completions } - /// Generates a random wormhole code. - /// - /// This method creates a new wormhole code by randomly selecting words from the word lists. - /// It ensures that the generated code has the correct number of words as specified by `num_words`. - /// - /// # Returns - /// - /// A String containing the randomly generated wormhole code, with words separated by dashes. - /// - /// # Behavior - /// - /// - Uses the OS's random number generator for secure randomness. - /// - Cycles through the word lists if `num_words` is greater than the number of available lists. - /// - Joins the selected words with dashes to form the final code. - /// - /// # Examples - /// - /// ``` - /// use your_crate_name::WordList; - /// - /// let wordlist = WordList::new(); - /// let code = wordlist.choose_words(); - /// assert_eq!(code.split('-').count(), wordlist.num_words()); - /// ``` pub fn choose_words(&self) -> String { let mut rng = OsRng; let components: Vec = self @@ -155,35 +96,6 @@ fn load_pgpwords() -> Vec> { vec![even_words, odd_words] } -/// Creates a default `Wordlist` with a specified number of words. -/// -/// This function generates a `Wordlist` using the PGP word list, which is a standardized -/// list of words often used for secure and human-readable encoding. -/// -/// # Arguments -/// -/// * `num_words` - The number of words that should be in a complete wormhole code. -/// -/// # Returns -/// -/// Returns a `Wordlist` instance initialized with the PGP word list and the specified -/// number of words. -/// -/// # Examples -/// -/// ``` -/// use your_crate_name::default_wordlist; -/// -/// let wordlist = default_wordlist(3); -/// assert_eq!(wordlist.num_words, 3); -/// assert_eq!(wordlist.words.len(), 2); // PGP word list has two lists (even and odd) -/// ``` -/// -/// # Note -/// -/// The PGP word list consists of two lists of 256 words each: one for even positions -/// and one for odd positions in the code. This function uses these lists regardless -/// of the `num_words` specified. pub fn default_wordlist(num_words: usize) -> Wordlist { Wordlist { num_words, From e83e96f5743879f5296e83674e9e32b62e947da6 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 24 Sep 2024 02:13:53 +0200 Subject: [PATCH 34/59] fixed typo --- cli/src/reedline.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 2ccf1d0d..dce86272 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -215,7 +215,7 @@ pub fn enter_code() -> eyre::Result { mod test { use super::*; #[test] - fn test_tab_compeltion_complete_word() { + fn test_tab_compeletion_complete_word() { let mut completer = CodeCompleter::default(); let input = "22-trombonist"; let cursor_pos = input.len(); @@ -228,7 +228,7 @@ mod test { } #[test] - fn test_tab_compeltion_partial_word() { + fn test_tab_completion_partial_word() { let mut completer = CodeCompleter::default(); let input = "22-trmbn"; let cursor_pos = input.len(); @@ -239,7 +239,7 @@ mod test { } #[test] - fn test_tab_compeltion_partial_in_middle() { + fn test_tab_completion_partial_in_middle() { let mut completer = CodeCompleter::default(); let input = "22-trbis-zulu"; let cursor_pos = input.len() - "-zulu".len(); From ec02d667f0f0dbb00c57ee787ab90484e993f482 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Wed, 25 Sep 2024 16:21:17 +0200 Subject: [PATCH 35/59] wormhole code validation length check --- cli/src/reedline.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index dce86272..e21ae1cb 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -147,6 +147,11 @@ impl CodeHighliter { return false; } + // Minimum code length + if parts.len() < 3 { + return false; + } + // Check all words for validity parts.iter().skip(1).all(|&word| { WORDLIST From cbfbf80d6265471dc44c1cfa5102a38ffc240974 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Wed, 25 Sep 2024 16:21:17 +0200 Subject: [PATCH 36/59] wormhole code validation length check, use defaults for fuzzy search --- cli/src/reedline.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index dce86272..9ed5e20f 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use color_eyre::eyre::{self, bail}; -use fuzzt::{algorithms::JaroWinkler, get_top_n, processors::NullStringProcessor}; +use fuzzt::{algorithms::JaroWinkler, get_top_n}; use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; @@ -93,9 +93,9 @@ impl Completer for CodeCompleter { let matches = get_top_n( current_part, &all_words, - Some(0.8), - Some(5), - Some(&NullStringProcessor), + None, + None, + None, Some(&JaroWinkler), ); @@ -147,6 +147,11 @@ impl CodeHighliter { return false; } + // Minimum code length + if parts.len() < 3 { + return false; + } + // Check all words for validity parts.iter().skip(1).all(|&word| { WORDLIST From 0ab6560f7ae3394b7c3d7e35375780c2780b3cfc Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 4 Oct 2024 06:45:15 +0200 Subject: [PATCH 37/59] use std::sync::LazyLock instead of lazy_static --- Cargo.lock | 1 - cli/Cargo.toml | 1 - cli/src/reedline.rs | 7 ++----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5ad26f6..ba4489b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1856,7 +1856,6 @@ dependencies = [ "futures", "fuzzt", "indicatif", - "lazy_static", "magic-wormhole", "nu-ansi-term 0.50.1", "number_prefix", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 26a419de..7d22b846 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -45,7 +45,6 @@ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" nu-ansi-term = "0.50.1" -lazy_static = "1.5.0" fuzzt = "0.3.1" [dev-dependencies] diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 9ed5e20f..1a43c988 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -1,8 +1,7 @@ -use std::borrow::Cow; +use std::{borrow::Cow, sync::LazyLock}; use color_eyre::eyre::{self, bail}; use fuzzt::{algorithms::JaroWinkler, get_top_n}; -use lazy_static::lazy_static; use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; use nu_ansi_term::{Color, Style}; use reedline::{ @@ -11,9 +10,7 @@ use reedline::{ ReedlineMenu, Signal, Span, StyledText, Suggestion, }; -lazy_static! { - static ref WORDLIST: Wordlist = default_wordlist(2); -} +static WORDLIST: LazyLock = LazyLock::new(|| default_wordlist(2)); struct CodePrompt {} From 71ec08a8e4f9c050dfded21ae44eb6d0f1e71cde Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 4 Oct 2024 07:03:12 +0200 Subject: [PATCH 38/59] use 0.8 confidence for menu system to work --- cli/src/reedline.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 1a43c988..c34d71eb 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -62,10 +62,8 @@ impl CodeCompleter { impl Completer for CodeCompleter { fn complete(&mut self, line: &str, pos: usize) -> Vec { - let parts: Vec<&str> = line.split('-').collect(); - // Skip autocomplete for the channel number (first part) - if parts.len() <= 1 { + if !line.contains('-') { return Vec::new(); } @@ -87,10 +85,12 @@ impl Completer for CodeCompleter { .collect(); // Use fuzzy matching to find the best matches + // Use cutoff for the menu system to be useful + // If cutoff is high enough and only one word is left, use Tab to complete directly let matches = get_top_n( current_part, &all_words, - None, + Some(0.8), None, None, Some(&JaroWinkler), From a958cddc815db83649911ddd8908da7ede01a3df Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 4 Oct 2024 07:06:44 +0200 Subject: [PATCH 39/59] fixed docs for get_n_top --- cli/src/reedline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index c34d71eb..63e65977 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -87,6 +87,7 @@ impl Completer for CodeCompleter { // Use fuzzy matching to find the best matches // Use cutoff for the menu system to be useful // If cutoff is high enough and only one word is left, use Tab to complete directly + // We use the Jaro-Winkler algorithm because it places more emphasis on the beginning part of the code word let matches = get_top_n( current_part, &all_words, From 0fe7823e4b73dc383f9daa9a2329028e73f0cedc Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 4 Oct 2024 22:20:23 +0200 Subject: [PATCH 40/59] made wordlist public without exposing core --- cli/src/reedline.rs | 25 ++++++++----------------- src/core.rs | 2 +- src/core/wordlist.rs | 12 ++++++++++-- src/lib.rs | 3 ++- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 63e65977..afe634ed 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, sync::LazyLock}; use color_eyre::eyre::{self, bail}; use fuzzt::{algorithms::JaroWinkler, get_top_n}; -use magic_wormhole::core::wordlist::{default_wordlist, Wordlist}; +use magic_wormhole::wordlist::default_wordlist_flatned; use nu_ansi_term::{Color, Style}; use reedline::{ default_emacs_keybindings, ColumnarMenu, Completer, Emacs, Highlighter, KeyCode, KeyModifiers, @@ -10,7 +10,7 @@ use reedline::{ ReedlineMenu, Signal, Span, StyledText, Suggestion, }; -static WORDLIST: LazyLock = LazyLock::new(|| default_wordlist(2)); +static WORDLIST: LazyLock> = LazyLock::new(|| default_wordlist_flatned()); struct CodePrompt {} @@ -76,13 +76,7 @@ impl Completer for CodeCompleter { let current_part = &line[current_word_start..current_word_end]; - // Flatten the word list - let all_words: Vec<&str> = WORDLIST - .words - .iter() - .flatten() - .map(|s| s.as_str()) - .collect(); + let list = &*WORDLIST.iter().map(|s| s.as_str()).collect::>(); // Use fuzzy matching to find the best matches // Use cutoff for the menu system to be useful @@ -90,7 +84,7 @@ impl Completer for CodeCompleter { // We use the Jaro-Winkler algorithm because it places more emphasis on the beginning part of the code word let matches = get_top_n( current_part, - &all_words, + list, Some(0.8), None, None, @@ -151,13 +145,10 @@ impl CodeHighliter { } // Check all words for validity - parts.iter().skip(1).all(|&word| { - WORDLIST - .words - .iter() - .flatten() - .any(|valid_word| valid_word == word) - }) + parts + .iter() + .skip(1) + .all(|&word| WORDLIST.iter().any(|valid_word| valid_word == word)) } } diff --git a/src/core.rs b/src/core.rs index 3b21e398..a640c77d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,12 +1,12 @@ #![allow(deprecated)] // TODO: Add missing documentation -#![allow(missing_docs)] pub(super) mod key; pub mod rendezvous; mod server_messages; #[cfg(test)] mod test; +#[doc(hidden)] pub mod wordlist; use serde_derive::{Deserialize, Serialize}; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 5d6ce74c..db8da1d5 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -4,8 +4,8 @@ use std::fmt; #[derive(PartialEq)] pub struct Wordlist { - pub num_words: usize, - pub words: Vec>, + num_words: usize, + words: Vec>, } impl fmt::Debug for Wordlist { @@ -103,6 +103,14 @@ pub fn default_wordlist(num_words: usize) -> Wordlist { } } +pub fn default_wordlist_flatned() -> Vec { + load_pgpwords() + .iter() + .flatten() + .map(|s| s.clone()) + .collect() +} + #[cfg(test)] mod test { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 94c13289..a01835c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,8 @@ #[macro_use] mod util; -pub mod core; +mod core; +pub use core::wordlist; #[cfg(feature = "forwarding")] pub mod forwarding; #[cfg(feature = "transfer")] From 55b57cd0b50433225198ff669b73d024c8ac1b9d Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sat, 5 Oct 2024 01:49:53 +0200 Subject: [PATCH 41/59] span_end using min --- cli/src/reedline.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index afe634ed..209f70d3 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -98,11 +98,8 @@ impl Completer for CodeCompleter { // Incase suggestion word length is larger then the current typed part // Otherwise we get index out of range error in Span - let span_end = if suggestion.len() >= current_part.len() { - current_word_end - } else { - current_word_start + suggestion.len() - }; + let span_end = + std::cmp::min(current_word_start + suggestion.len(), current_word_end); Suggestion { value: suggestion, From 4210dce3fab956f1f941f35a372c6d830bf5bbf0 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sat, 5 Oct 2024 01:55:40 +0200 Subject: [PATCH 42/59] removed partial completions --- cli/src/reedline.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 209f70d3..c448d441 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -187,7 +187,6 @@ pub fn enter_code() -> eyre::Result { .with_highlighter(Box::new(CodeHighliter::default())) .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) .with_quick_completions(true) - .with_partial_completions(true) .with_edit_mode(edit_mode); let prompt = CodePrompt::default(); From c9b2846665c0eae70d822e4a16d545cf257c4abd Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sat, 5 Oct 2024 02:01:48 +0200 Subject: [PATCH 43/59] fixed typo --- cli/src/reedline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index c448d441..62673b96 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -205,7 +205,7 @@ pub fn enter_code() -> eyre::Result { mod test { use super::*; #[test] - fn test_tab_compeletion_complete_word() { + fn test_tab_completion_complete_word() { let mut completer = CodeCompleter::default(); let input = "22-trombonist"; let cursor_pos = input.len(); From 1007c8c18e67a8c73a65a36e1c772fce18babe57 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Sat, 5 Oct 2024 02:06:34 +0200 Subject: [PATCH 44/59] removed unnecessary comment --- src/core.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core.rs b/src/core.rs index a640c77d..3696c495 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,5 +1,4 @@ #![allow(deprecated)] -// TODO: Add missing documentation pub(super) mod key; pub mod rendezvous; From bd19d093e73ee026d6a6e58210ccfcd5821cb2a4 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Mon, 7 Oct 2024 19:49:02 +0200 Subject: [PATCH 45/59] removed code highlighting because of custom wordlists --- Cargo.lock | 1 - cli/Cargo.toml | 1 - cli/src/reedline.rs | 56 +++------------------------------------------ 3 files changed, 3 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba4489b4..81e32c84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1857,7 +1857,6 @@ dependencies = [ "fuzzt", "indicatif", "magic-wormhole", - "nu-ansi-term 0.50.1", "number_prefix", "qr2term", "rand", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7d22b846..ec860ffa 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -44,7 +44,6 @@ arboard = { optional = true, workspace = true, features = [ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" -nu-ansi-term = "0.50.1" fuzzt = "0.3.1" [dev-dependencies] diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 62673b96..8f3a27af 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -3,11 +3,10 @@ use std::{borrow::Cow, sync::LazyLock}; use color_eyre::eyre::{self, bail}; use fuzzt::{algorithms::JaroWinkler, get_top_n}; use magic_wormhole::wordlist::default_wordlist_flatned; -use nu_ansi_term::{Color, Style}; use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, Emacs, Highlighter, KeyCode, KeyModifiers, - MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, - ReedlineMenu, Signal, Span, StyledText, Suggestion, + default_emacs_keybindings, ColumnarMenu, Completer, Emacs, KeyCode, KeyModifiers, MenuBuilder, + Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, + Span, Suggestion, }; static WORDLIST: LazyLock> = LazyLock::new(|| default_wordlist_flatned()); @@ -117,54 +116,6 @@ impl Completer for CodeCompleter { } } -struct CodeHighliter {} - -impl CodeHighliter { - fn default() -> Self { - CodeHighliter {} - } - - fn is_valid_code(&self, code: &str) -> bool { - let parts: Vec<&str> = code.split('-').collect(); - - // If the first element in code is not a valid number - if !parts - .first() - .and_then(|c| c.parse::().ok()) - .is_some_and(|c| (0..1000).contains(&c)) - { - return false; - } - - // Minimum code length - if parts.len() < 3 { - return false; - } - - // Check all words for validity - parts - .iter() - .skip(1) - .all(|&word| WORDLIST.iter().any(|valid_word| valid_word == word)) - } -} - -impl Highlighter for CodeHighliter { - fn highlight(&self, line: &str, _cursor: usize) -> StyledText { - let invalid = Style::new().fg(Color::Red).bold(); - let valid = Style::new().fg(Color::Green).bold(); - - let style = match self.is_valid_code(line) { - true => valid, - false => invalid, - }; - - let mut t = StyledText::new(); - t.push((style, line.to_string())); - t - } -} - pub fn enter_code() -> eyre::Result { // Set up the required keybindings let mut keybindings = default_emacs_keybindings(); @@ -184,7 +135,6 @@ pub fn enter_code() -> eyre::Result { let mut line_editor = Reedline::create() .with_completer(Box::new(CodeCompleter::default())) - .with_highlighter(Box::new(CodeHighliter::default())) .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) .with_quick_completions(true) .with_edit_mode(edit_mode); From 0e349cd1b81b99169f435b5bbf867136abb52b97 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Mon, 7 Oct 2024 19:56:05 +0200 Subject: [PATCH 46/59] rust version 1.81 for LazyLock support --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 03881e2e..3665b0a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ homepage = "http://magic-wormhole.io/" repository = "https://github.com/magic-wormhole/magic-wormhole.rs/tree/main/cli" license = "EUPL-1.2" -rust-version = "1.75" +rust-version = "1.81" edition = "2021" [workspace.dependencies] From 5cfdd4132a316ea6fb1204ff600442e2b72ec4d0 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Mon, 7 Oct 2024 19:49:02 +0200 Subject: [PATCH 47/59] removed code highlighting because of custom wordlists --- Cargo.lock | 1 - cli/Cargo.toml | 1 - cli/src/reedline.rs | 56 +++------------------------------------------ 3 files changed, 3 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba4489b4..81e32c84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1857,7 +1857,6 @@ dependencies = [ "fuzzt", "indicatif", "magic-wormhole", - "nu-ansi-term 0.50.1", "number_prefix", "qr2term", "rand", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7d22b846..ec860ffa 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -44,7 +44,6 @@ arboard = { optional = true, workspace = true, features = [ tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } reedline = "0.35.0" -nu-ansi-term = "0.50.1" fuzzt = "0.3.1" [dev-dependencies] diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs index 62673b96..8f3a27af 100644 --- a/cli/src/reedline.rs +++ b/cli/src/reedline.rs @@ -3,11 +3,10 @@ use std::{borrow::Cow, sync::LazyLock}; use color_eyre::eyre::{self, bail}; use fuzzt::{algorithms::JaroWinkler, get_top_n}; use magic_wormhole::wordlist::default_wordlist_flatned; -use nu_ansi_term::{Color, Style}; use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, Emacs, Highlighter, KeyCode, KeyModifiers, - MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, - ReedlineMenu, Signal, Span, StyledText, Suggestion, + default_emacs_keybindings, ColumnarMenu, Completer, Emacs, KeyCode, KeyModifiers, MenuBuilder, + Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, + Span, Suggestion, }; static WORDLIST: LazyLock> = LazyLock::new(|| default_wordlist_flatned()); @@ -117,54 +116,6 @@ impl Completer for CodeCompleter { } } -struct CodeHighliter {} - -impl CodeHighliter { - fn default() -> Self { - CodeHighliter {} - } - - fn is_valid_code(&self, code: &str) -> bool { - let parts: Vec<&str> = code.split('-').collect(); - - // If the first element in code is not a valid number - if !parts - .first() - .and_then(|c| c.parse::().ok()) - .is_some_and(|c| (0..1000).contains(&c)) - { - return false; - } - - // Minimum code length - if parts.len() < 3 { - return false; - } - - // Check all words for validity - parts - .iter() - .skip(1) - .all(|&word| WORDLIST.iter().any(|valid_word| valid_word == word)) - } -} - -impl Highlighter for CodeHighliter { - fn highlight(&self, line: &str, _cursor: usize) -> StyledText { - let invalid = Style::new().fg(Color::Red).bold(); - let valid = Style::new().fg(Color::Green).bold(); - - let style = match self.is_valid_code(line) { - true => valid, - false => invalid, - }; - - let mut t = StyledText::new(); - t.push((style, line.to_string())); - t - } -} - pub fn enter_code() -> eyre::Result { // Set up the required keybindings let mut keybindings = default_emacs_keybindings(); @@ -184,7 +135,6 @@ pub fn enter_code() -> eyre::Result { let mut line_editor = Reedline::create() .with_completer(Box::new(CodeCompleter::default())) - .with_highlighter(Box::new(CodeHighliter::default())) .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) .with_quick_completions(true) .with_edit_mode(edit_mode); From cbb3caae511bd40dc93f67826ff201220a1fe3b0 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Mon, 7 Oct 2024 19:56:05 +0200 Subject: [PATCH 48/59] rust version 1.81 for LazyLock support --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 03881e2e..3665b0a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ homepage = "http://magic-wormhole.io/" repository = "https://github.com/magic-wormhole/magic-wormhole.rs/tree/main/cli" license = "EUPL-1.2" -rust-version = "1.75" +rust-version = "1.81" edition = "2021" [workspace.dependencies] From 6fb8f86632df363df687f670af724541516ac138 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 8 Oct 2024 17:06:47 +0200 Subject: [PATCH 49/59] dialoguer tab completion implementation --- Cargo.lock | 1 + Cargo.toml | 6 +- cli/Cargo.toml | 2 +- cli/src/main.rs | 4 +- cli/src/reedline.rs | 191 ------------------------------------------- src/core/wordlist.rs | 137 +++++++++++++------------------ 6 files changed, 64 insertions(+), 277 deletions(-) delete mode 100644 cli/src/reedline.rs diff --git a/Cargo.lock b/Cargo.lock index 81e32c84..7864befd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1812,6 +1812,7 @@ dependencies = [ "derive_more", "eyre", "futures", + "fuzzt", "getrandom", "hex", "hkdf", diff --git a/Cargo.toml b/Cargo.toml index 3665b0a7..7c0a647f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,8 @@ percent-encoding = { workspace = true } tracing = { workspace = true, features = ["log", "log-always"] } +fuzzt = { version = "0.3.1", optional = true } + # Transit dependencies @@ -145,7 +147,6 @@ test-log = { workspace = true } eyre = { workspace = true } [features] - transfer = ["transit", "dep:tar", "dep:rmp-serde"] transit = [ "dep:noise-rust-crypto", @@ -157,7 +158,7 @@ transit = [ "dep:async-trait", ] forwarding = ["transit", "dep:rmp-serde"] -default = ["transit", "transfer"] +default = ["transit", "transfer", "fuzzy-complete"] all = ["default", "forwarding"] # TLS implementations for websocket connections via async-tungstenite @@ -168,6 +169,7 @@ native-tls = ["async-tungstenite/async-native-tls"] # By enabling this option you are opting out of semver stability. experimental-transfer-v2 = [] experimental = ["experimental-transfer-v2"] +fuzzy-complete = ["fuzzt"] [profile.release] overflow-checks = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ec860ffa..c3f1baaa 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,7 +33,7 @@ clap_complete = { workspace = true } env_logger = { workspace = true } console = { workspace = true } indicatif = { workspace = true } -dialoguer = { workspace = true } +dialoguer = { workspace = true, features = ["completion"] } color-eyre = { workspace = true } number_prefix = { workspace = true } ctrlc = { workspace = true } diff --git a/cli/src/main.rs b/cli/src/main.rs index c779dbb4..5cfb23c4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,5 @@ #![allow(clippy::too_many_arguments)] -mod reedline; +mod completer; mod util; use std::time::{Duration, Instant}; @@ -7,6 +7,7 @@ use std::time::{Duration, Instant}; use async_std::sync::Arc; use clap::{Args, CommandFactory, Parser, Subcommand}; use color_eyre::{eyre, eyre::Context}; +use completer::enter_code; use console::{style, Term}; use futures::{future::Either, Future, FutureExt}; use indicatif::{MultiProgress, ProgressBar}; @@ -15,7 +16,6 @@ use magic_wormhole::{ transit::{self, TransitInfo}, MailboxConnection, Wormhole, }; -use reedline::enter_code; use std::{io::Write, path::PathBuf}; use tracing_subscriber::EnvFilter; diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs deleted file mode 100644 index 8f3a27af..00000000 --- a/cli/src/reedline.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{borrow::Cow, sync::LazyLock}; - -use color_eyre::eyre::{self, bail}; -use fuzzt::{algorithms::JaroWinkler, get_top_n}; -use magic_wormhole::wordlist::default_wordlist_flatned; -use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, Emacs, KeyCode, KeyModifiers, MenuBuilder, - Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, - Span, Suggestion, -}; - -static WORDLIST: LazyLock> = LazyLock::new(|| default_wordlist_flatned()); - -struct CodePrompt {} - -impl CodePrompt { - fn default() -> Self { - CodePrompt {} - } -} - -impl Prompt for CodePrompt { - fn render_prompt_left(&self) -> Cow<'_, str> { - Cow::Borrowed("Wormhole Code: ") - } - - // Not needed - fn render_prompt_right(&self) -> Cow<'_, str> { - Cow::Borrowed("") - } - - // Not needed - fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> { - Cow::Borrowed("") - } - - // Not needed - fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { - Cow::Borrowed("... ") - } - - fn render_prompt_history_search_indicator( - &self, - _history_search: PromptHistorySearch, - ) -> Cow<'_, str> { - Cow::Borrowed("") - } - - fn get_prompt_color(&self) -> reedline::Color { - reedline::Color::Grey - } -} - -struct CodeCompleter {} - -impl CodeCompleter { - fn default() -> Self { - CodeCompleter {} - } -} - -impl Completer for CodeCompleter { - fn complete(&mut self, line: &str, pos: usize) -> Vec { - // Skip autocomplete for the channel number (first part) - if !line.contains('-') { - return Vec::new(); - } - - // Find the start and end of the current word - let current_word_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); - let current_word_end = line[pos..] - .find('-') - .map(|i| i + pos) - .unwrap_or_else(|| line.len()); - - let current_part = &line[current_word_start..current_word_end]; - - let list = &*WORDLIST.iter().map(|s| s.as_str()).collect::>(); - - // Use fuzzy matching to find the best matches - // Use cutoff for the menu system to be useful - // If cutoff is high enough and only one word is left, use Tab to complete directly - // We use the Jaro-Winkler algorithm because it places more emphasis on the beginning part of the code word - let matches = get_top_n( - current_part, - list, - Some(0.8), - None, - None, - Some(&JaroWinkler), - ); - - matches - .into_iter() - .map(|word| { - let suggestion = word.to_string(); - - // Incase suggestion word length is larger then the current typed part - // Otherwise we get index out of range error in Span - let span_end = - std::cmp::min(current_word_start + suggestion.len(), current_word_end); - - Suggestion { - value: suggestion, - description: None, - extra: None, - span: Span { - start: current_word_start, - end: span_end, - }, - append_whitespace: false, - style: None, - } - }) - .collect() - } -} - -pub fn enter_code() -> eyre::Result { - // Set up the required keybindings - let mut keybindings = default_emacs_keybindings(); - - keybindings.add_binding( - KeyModifiers::NONE, - KeyCode::Tab, - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::Menu("completion_menu".to_string()), - ReedlineEvent::MenuNext, - ]), - ); - - let edit_mode = Box::new(Emacs::new(keybindings)); - - let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); - - let mut line_editor = Reedline::create() - .with_completer(Box::new(CodeCompleter::default())) - .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) - .with_quick_completions(true) - .with_edit_mode(edit_mode); - - let prompt = CodePrompt::default(); - - let sig = line_editor.read_line(&prompt); - match sig { - Ok(Signal::Success(buffer)) => return Ok(buffer), - // TODO: fix temporary work around - Ok(Signal::CtrlC) => bail!("Ctrl-C received"), - Ok(Signal::CtrlD) => bail!("Ctrl-D received"), - Err(e) => bail!(e), - } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_tab_completion_complete_word() { - let mut completer = CodeCompleter::default(); - let input = "22-trombonist"; - let cursor_pos = input.len(); - - let suggestions = completer.complete(&input, cursor_pos); - - assert_eq!(suggestions.len(), 3); - - assert_eq!(suggestions.first().unwrap().value, "trombonist"); - } - - #[test] - fn test_tab_completion_partial_word() { - let mut completer = CodeCompleter::default(); - let input = "22-trmbn"; - let cursor_pos = input.len(); - - let suggestions = completer.complete(&input, cursor_pos); - - assert_eq!(suggestions.first().unwrap().value, "trombonist"); - } - - #[test] - fn test_tab_completion_partial_in_middle() { - let mut completer = CodeCompleter::default(); - let input = "22-trbis-zulu"; - let cursor_pos = input.len() - "-zulu".len(); - - let suggestions = completer.complete(&input, cursor_pos); - - assert_eq!(suggestions.first().unwrap().value, "trombonist"); - } -} diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index db8da1d5..7ad2280d 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -20,39 +20,56 @@ impl Wordlist { Wordlist { num_words, words } } - #[allow(dead_code)] // TODO make this API public one day pub fn get_completions(&self, prefix: &str) -> Vec { let count_dashes = prefix.matches('-').count(); - let mut completions = Vec::new(); let words = &self.words[count_dashes % self.words.len()]; - let last_partial_word = prefix.split('-').last(); - let lp = if let Some(w) = last_partial_word { - w.len() + let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix)); + + let matches = if cfg!(feature = "fuzzy-complete") { + self.fuzzy_complete(last_partial, words) } else { - 0 + words + .iter() + .filter(|word| word.starts_with(last_partial)) + .cloned() + .collect() }; - for word in words { - let mut suffix: String = prefix.to_owned(); - if word.starts_with(last_partial_word.unwrap()) { - if lp == 0 { - suffix.push_str(word); - } else { - let p = prefix.len() - lp; - suffix.truncate(p); - suffix.push_str(word); + matches + .into_iter() + .map(|word| { + let mut completion = String::new(); + completion.push_str(prefix_without_last); + if !prefix_without_last.is_empty() { + completion.push('-'); } + completion.push_str(&word); + completion + }) + .collect() + } - if count_dashes + 1 < self.num_words { - suffix.push('-'); - } + /// Get either even or odd wordlist + pub fn get_wordlist(&self, prefix: &str, cursor_pos: Option) -> &Vec { + let limited_prefix = match cursor_pos { + Some(pos) if pos < prefix.len() => &prefix[..pos], + _ => prefix, + }; + let count_dashes = limited_prefix.matches('-').count(); + &self.words[count_dashes % self.words.len()] + } - completions.push(suffix); - } - } - completions.sort(); - completions + #[cfg(feature = "fuzzy-complete")] + fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { + use fuzzt::algorithms::JaroWinkler; + + let words = words.iter().map(|w| w.as_str()).collect::>(); + + fuzzt::get_top_n(partial, &words, None, None, None, Some(&JaroWinkler)) + .into_iter() + .map(|s| s.to_string()) + .collect() } pub fn choose_words(&self) -> String { @@ -68,6 +85,17 @@ impl Wordlist { } } +/// Extract partial str from prefix with cursor position +pub fn extract_partial_from_prefix<'a>(prefix: &'a str, pos: usize) -> &'a str { + let current_word_start = prefix[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); + let current_word_end = prefix[pos..] + .find('-') + .map(|i| i + pos) + .unwrap_or_else(|| prefix.len()); + + &prefix[current_word_start..current_word_end] +} + fn load_pgpwords() -> Vec> { let raw_words_value: Value = serde_json::from_str(include_str!("pgpwords.json")).unwrap(); let raw_words = raw_words_value.as_object().unwrap(); @@ -103,14 +131,6 @@ pub fn default_wordlist(num_words: usize) -> Wordlist { } } -pub fn default_wordlist_flatned() -> Vec { - load_pgpwords() - .iter() - .flatten() - .map(|s| s.clone()) - .collect() -} - #[cfg(test)] mod test { use super::*; @@ -147,20 +167,6 @@ mod test { .collect() } - #[test] - fn test_completion() { - let words: Vec> = vec![ - vecstrings("purple green yellow"), - vecstrings("sausages seltzer snobol"), - ]; - - let w = Wordlist::new(2, words); - assert_eq!(w.get_completions(""), vec!["green-", "purple-", "yellow-"]); - assert_eq!(w.get_completions("pur"), vec!["purple-"]); - assert_eq!(w.get_completions("blu"), Vec::::new()); - assert_eq!(w.get_completions("purple-sa"), vec!["purple-sausages"]); - } - #[test] fn test_choose_words() { let few_words: Vec> = vec![vecstrings("purple"), vecstrings("sausages")]; @@ -201,45 +207,14 @@ mod test { } #[test] - fn test_default_completions() { - let w = default_wordlist(2); - let c = w.get_completions("ar"); - assert_eq!(c.len(), 2); - assert!(c.contains(&String::from("article-"))); - assert!(c.contains(&String::from("armistice-"))); - - let c = w.get_completions("armis"); - assert_eq!(c.len(), 1); - assert!(c.contains(&String::from("armistice-"))); - - let c = w.get_completions("armistice-"); - assert_eq!(c.len(), 256); + fn test_wormhole_code_completions() { + let list = default_wordlist(2); - let c = w.get_completions("armistice-ba"); - assert_eq!( - c, - vec![ - "armistice-baboon", - "armistice-backfield", - "armistice-backward", - "armistice-banjo", - ] - ); + assert_eq!(list.get_completions("22"), Vec::::new()); - let w = default_wordlist(3); - let c = w.get_completions("armistice-ba"); assert_eq!( - c, - vec![ - "armistice-baboon-", - "armistice-backfield-", - "armistice-backward-", - "armistice-banjo-", - ] + list.get_completions("22-chisel"), + ["22-chisel", "22-chairlift", "22-christmas"] ); - - let w = default_wordlist(4); - let c = w.get_completions("armistice-baboon"); - assert_eq!(c, vec!["armistice-baboon-"]); } } From e7fefce70ccba209a3b07a7e07e5ffbf874d9212 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 8 Oct 2024 17:12:31 +0200 Subject: [PATCH 50/59] added complter, removed reedline crate --- Cargo.lock | 212 +------------------------------------------ cli/Cargo.toml | 1 - cli/src/completer.rs | 31 +++++++ 3 files changed, 35 insertions(+), 209 deletions(-) create mode 100644 cli/src/completer.rs diff --git a/Cargo.lock b/Cargo.lock index 7864befd..a0409bd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,21 +67,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.15" @@ -469,9 +454,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "blake2" @@ -610,19 +592,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-targets 0.52.6", -] - [[package]] name = "cipher" version = "0.4.4" @@ -863,30 +832,13 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio 0.8.11", + "mio", "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", ] -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.6.0", - "crossterm_winapi", - "mio 1.0.2", - "parking_lot 0.12.3", - "rustix 0.38.34", - "serde", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1180,17 +1132,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" -[[package]] -name = "fd-lock" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" -dependencies = [ - "cfg-if", - "rustix 0.38.34", - "windows-sys 0.52.0", -] - [[package]] name = "fdeflate" version = "0.3.4" @@ -1575,29 +1516,6 @@ dependencies = [ "serde", ] -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "idna" version = "0.5.0" @@ -1695,15 +1613,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -1861,7 +1770,6 @@ dependencies = [ "number_prefix", "qr2term", "rand", - "reedline", "serde", "serde_derive", "serde_json", @@ -1929,19 +1837,6 @@ dependencies = [ "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 = "native-tls" version = "0.2.12" @@ -2033,15 +1928,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2467,7 +2353,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c2a1e77b5cd714b04247ad912b7c8fe9a1fe1d58425048249def91bcf690e4c" dependencies = [ - "crossterm 0.25.0", + "crossterm", "qrcode", ] @@ -2566,26 +2452,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "reedline" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5289de810296f8f2ff58d35544d92ae98d0a631453388bc3e608086be0fa596" -dependencies = [ - "chrono", - "crossterm 0.28.1", - "fd-lock", - "itertools", - "nu-ansi-term 0.50.1", - "serde", - "strip-ansi-escapes", - "strum", - "strum_macros", - "thiserror", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "regex" version = "1.10.6" @@ -2740,12 +2606,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - [[package]] name = "ryu" version = "1.0.18" @@ -2948,8 +2808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 0.8.11", - "mio 1.0.2", + "mio", "signal-hook", ] @@ -3058,40 +2917,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "strip-ansi-escapes" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" -dependencies = [ - "vte", -] - [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.76", -] - [[package]] name = "stun_codec" version = "0.3.5" @@ -3351,7 +3182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", @@ -3468,12 +3299,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.1.13" @@ -3550,26 +3375,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vte" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" -dependencies = [ - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "wait-timeout" version = "0.2.0" @@ -3822,15 +3627,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c3f1baaa..84a2c6be 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -43,7 +43,6 @@ arboard = { optional = true, workspace = true, features = [ ] } # Wayland by default, fallback to X11. tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } -reedline = "0.35.0" fuzzt = "0.3.1" [dev-dependencies] diff --git a/cli/src/completer.rs b/cli/src/completer.rs new file mode 100644 index 00000000..8ed0b0b0 --- /dev/null +++ b/cli/src/completer.rs @@ -0,0 +1,31 @@ +use std::sync::LazyLock; + +use color_eyre::eyre; +use dialoguer::{Completion, Input}; +use magic_wormhole::wordlist::{default_wordlist, Wordlist}; + +static WORDLIST: LazyLock = LazyLock::new(|| default_wordlist(2)); + +struct CustomCompletion {} + +impl CustomCompletion { + pub fn default() -> Self { + CustomCompletion {} + } +} + +impl Completion for CustomCompletion { + fn get(&self, input: &str) -> Option { + WORDLIST.get_completions(input).first().cloned() + } +} + +pub fn enter_code() -> eyre::Result { + let custom_completion = CustomCompletion::default(); + + Input::new() + .with_prompt("Wormhole Code") + .completion_with(&custom_completion) + .interact_text() + .map_err(From::from) +} From f3419988112b64819acdd0adb9ecb072a8d14a74 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Tue, 8 Oct 2024 20:17:56 +0200 Subject: [PATCH 51/59] added documentation --- cli/src/completer.rs | 1 + src/core.rs | 1 - src/core/wordlist.rs | 43 ++++++++++++++++++++++++++----------------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/cli/src/completer.rs b/cli/src/completer.rs index 8ed0b0b0..577d444b 100644 --- a/cli/src/completer.rs +++ b/cli/src/completer.rs @@ -16,6 +16,7 @@ impl CustomCompletion { impl Completion for CustomCompletion { fn get(&self, input: &str) -> Option { + dbg!(WORDLIST.get_completions(input)); WORDLIST.get_completions(input).first().cloned() } } diff --git a/src/core.rs b/src/core.rs index 3696c495..04b1828b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,7 +5,6 @@ pub mod rendezvous; mod server_messages; #[cfg(test)] mod test; -#[doc(hidden)] pub mod wordlist; use serde_derive::{Deserialize, Serialize}; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 7ad2280d..7f9c125f 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -2,9 +2,13 @@ use rand::{rngs::OsRng, seq::SliceRandom}; use serde_json::{self, Value}; use std::fmt; +/// Represents a list of words used to generate and complete wormhole codes. +/// A wormhole code is a sequence of words used for secure communication or identification. #[derive(PartialEq)] pub struct Wordlist { + /// Number of words in a wormhole code num_words: usize, + /// Odd and even wordlist words: Vec>, } @@ -20,6 +24,10 @@ impl Wordlist { Wordlist { num_words, words } } + /// Completes a wormhole code + /// + /// Completion can be done either with fuzzy search (approximate string matching) + /// or simple `starts_with` matching. pub fn get_completions(&self, prefix: &str) -> Vec { let count_dashes = prefix.matches('-').count(); let words = &self.words[count_dashes % self.words.len()]; @@ -51,17 +59,16 @@ impl Wordlist { } /// Get either even or odd wordlist - pub fn get_wordlist(&self, prefix: &str, cursor_pos: Option) -> &Vec { - let limited_prefix = match cursor_pos { - Some(pos) if pos < prefix.len() => &prefix[..pos], - _ => prefix, - }; - let count_dashes = limited_prefix.matches('-').count(); + pub fn get_wordlist(&self, prefix: &str) -> &Vec { + let count_dashes = prefix.matches('-').count(); &self.words[count_dashes % self.words.len()] } + /// Fuzzy wormhole code completion #[cfg(feature = "fuzzy-complete")] fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { + // We use Jaro-Winkler algorithm because it emphasizes the beginning of a word + use fuzzt::algorithms::JaroWinkler; let words = words.iter().map(|w| w.as_str()).collect::>(); @@ -72,6 +79,7 @@ impl Wordlist { .collect() } + /// Choose wormhole code word pub fn choose_words(&self) -> String { let mut rng = OsRng; let components: Vec = self @@ -85,17 +93,6 @@ impl Wordlist { } } -/// Extract partial str from prefix with cursor position -pub fn extract_partial_from_prefix<'a>(prefix: &'a str, pos: usize) -> &'a str { - let current_word_start = prefix[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); - let current_word_end = prefix[pos..] - .find('-') - .map(|i| i + pos) - .unwrap_or_else(|| prefix.len()); - - &prefix[current_word_start..current_word_end] -} - fn load_pgpwords() -> Vec> { let raw_words_value: Value = serde_json::from_str(include_str!("pgpwords.json")).unwrap(); let raw_words = raw_words_value.as_object().unwrap(); @@ -124,6 +121,7 @@ fn load_pgpwords() -> Vec> { vec![even_words, odd_words] } +/// Construct Wordlist struct with given number of words in a wormhole code pub fn default_wordlist(num_words: usize) -> Wordlist { Wordlist { num_words, @@ -211,10 +209,21 @@ mod test { let list = default_wordlist(2); assert_eq!(list.get_completions("22"), Vec::::new()); + assert_eq!(list.get_completions("22-"), Vec::::new()); + + // Invalid wormhole code check + assert_eq!(list.get_completions("trj"), Vec::::new()); assert_eq!( list.get_completions("22-chisel"), ["22-chisel", "22-chairlift", "22-christmas"] ); + + assert_eq!( + list.get_completions("22-chle"), + ["22-chisel", "22-chatter", "22-checkup"] + ); + + assert_eq!(list.get_completions("22-chisel-tba"), ["22-chisel-tobacco"]); } } From ceeeef354e0c3b7715aeb90285df55d9abfa6071 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Wed, 9 Oct 2024 01:44:51 +0200 Subject: [PATCH 52/59] documentation, cleanup --- src/core.rs | 1 + src/core/wordlist.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/core.rs b/src/core.rs index 04b1828b..3696c495 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,6 +5,7 @@ pub mod rendezvous; mod server_messages; #[cfg(test)] mod test; +#[doc(hidden)] pub mod wordlist; use serde_derive::{Deserialize, Serialize}; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 7f9c125f..4058c4a5 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -1,3 +1,4 @@ +///! Wordlist generation and wormhole code utilities use rand::{rngs::OsRng, seq::SliceRandom}; use serde_json::{self, Value}; use std::fmt; @@ -20,6 +21,7 @@ impl fmt::Debug for Wordlist { impl Wordlist { #[cfg(test)] + #[doc(hidden)] pub fn new(num_words: usize, words: Vec>) -> Wordlist { Wordlist { num_words, words } } From 711875a3fa3901721b0c20225e435780d8420275 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Wed, 9 Oct 2024 01:50:09 +0200 Subject: [PATCH 53/59] removed fuzzt from cli --- Cargo.lock | 1 - cli/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0409bd2..d56ac88c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1764,7 +1764,6 @@ dependencies = [ "dialoguer", "env_logger", "futures", - "fuzzt", "indicatif", "magic-wormhole", "number_prefix", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 84a2c6be..a79b43b2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -43,7 +43,6 @@ arboard = { optional = true, workspace = true, features = [ ] } # Wayland by default, fallback to X11. tracing = { workspace = true, features = ["log", "log-always"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } -fuzzt = "0.3.1" [dev-dependencies] trycmd = { workspace = true } From ece999b03ebe4137ddbbe8d02e88cf4a03e6b5a9 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Wed, 9 Oct 2024 03:51:55 +0200 Subject: [PATCH 54/59] removed reedline --- cli/src/completer.rs | 1 - cli/src/reedline.rs | 191 ------------------------------------------- src/core/wordlist.rs | 1 - 3 files changed, 193 deletions(-) delete mode 100644 cli/src/reedline.rs diff --git a/cli/src/completer.rs b/cli/src/completer.rs index 577d444b..8ed0b0b0 100644 --- a/cli/src/completer.rs +++ b/cli/src/completer.rs @@ -16,7 +16,6 @@ impl CustomCompletion { impl Completion for CustomCompletion { fn get(&self, input: &str) -> Option { - dbg!(WORDLIST.get_completions(input)); WORDLIST.get_completions(input).first().cloned() } } diff --git a/cli/src/reedline.rs b/cli/src/reedline.rs deleted file mode 100644 index 8f3a27af..00000000 --- a/cli/src/reedline.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{borrow::Cow, sync::LazyLock}; - -use color_eyre::eyre::{self, bail}; -use fuzzt::{algorithms::JaroWinkler, get_top_n}; -use magic_wormhole::wordlist::default_wordlist_flatned; -use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, Emacs, KeyCode, KeyModifiers, MenuBuilder, - Prompt, PromptEditMode, PromptHistorySearch, Reedline, ReedlineEvent, ReedlineMenu, Signal, - Span, Suggestion, -}; - -static WORDLIST: LazyLock> = LazyLock::new(|| default_wordlist_flatned()); - -struct CodePrompt {} - -impl CodePrompt { - fn default() -> Self { - CodePrompt {} - } -} - -impl Prompt for CodePrompt { - fn render_prompt_left(&self) -> Cow<'_, str> { - Cow::Borrowed("Wormhole Code: ") - } - - // Not needed - fn render_prompt_right(&self) -> Cow<'_, str> { - Cow::Borrowed("") - } - - // Not needed - fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> { - Cow::Borrowed("") - } - - // Not needed - fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { - Cow::Borrowed("... ") - } - - fn render_prompt_history_search_indicator( - &self, - _history_search: PromptHistorySearch, - ) -> Cow<'_, str> { - Cow::Borrowed("") - } - - fn get_prompt_color(&self) -> reedline::Color { - reedline::Color::Grey - } -} - -struct CodeCompleter {} - -impl CodeCompleter { - fn default() -> Self { - CodeCompleter {} - } -} - -impl Completer for CodeCompleter { - fn complete(&mut self, line: &str, pos: usize) -> Vec { - // Skip autocomplete for the channel number (first part) - if !line.contains('-') { - return Vec::new(); - } - - // Find the start and end of the current word - let current_word_start = line[..pos].rfind('-').map(|i| i + 1).unwrap_or(0); - let current_word_end = line[pos..] - .find('-') - .map(|i| i + pos) - .unwrap_or_else(|| line.len()); - - let current_part = &line[current_word_start..current_word_end]; - - let list = &*WORDLIST.iter().map(|s| s.as_str()).collect::>(); - - // Use fuzzy matching to find the best matches - // Use cutoff for the menu system to be useful - // If cutoff is high enough and only one word is left, use Tab to complete directly - // We use the Jaro-Winkler algorithm because it places more emphasis on the beginning part of the code word - let matches = get_top_n( - current_part, - list, - Some(0.8), - None, - None, - Some(&JaroWinkler), - ); - - matches - .into_iter() - .map(|word| { - let suggestion = word.to_string(); - - // Incase suggestion word length is larger then the current typed part - // Otherwise we get index out of range error in Span - let span_end = - std::cmp::min(current_word_start + suggestion.len(), current_word_end); - - Suggestion { - value: suggestion, - description: None, - extra: None, - span: Span { - start: current_word_start, - end: span_end, - }, - append_whitespace: false, - style: None, - } - }) - .collect() - } -} - -pub fn enter_code() -> eyre::Result { - // Set up the required keybindings - let mut keybindings = default_emacs_keybindings(); - - keybindings.add_binding( - KeyModifiers::NONE, - KeyCode::Tab, - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::Menu("completion_menu".to_string()), - ReedlineEvent::MenuNext, - ]), - ); - - let edit_mode = Box::new(Emacs::new(keybindings)); - - let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); - - let mut line_editor = Reedline::create() - .with_completer(Box::new(CodeCompleter::default())) - .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) - .with_quick_completions(true) - .with_edit_mode(edit_mode); - - let prompt = CodePrompt::default(); - - let sig = line_editor.read_line(&prompt); - match sig { - Ok(Signal::Success(buffer)) => return Ok(buffer), - // TODO: fix temporary work around - Ok(Signal::CtrlC) => bail!("Ctrl-C received"), - Ok(Signal::CtrlD) => bail!("Ctrl-D received"), - Err(e) => bail!(e), - } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_tab_completion_complete_word() { - let mut completer = CodeCompleter::default(); - let input = "22-trombonist"; - let cursor_pos = input.len(); - - let suggestions = completer.complete(&input, cursor_pos); - - assert_eq!(suggestions.len(), 3); - - assert_eq!(suggestions.first().unwrap().value, "trombonist"); - } - - #[test] - fn test_tab_completion_partial_word() { - let mut completer = CodeCompleter::default(); - let input = "22-trmbn"; - let cursor_pos = input.len(); - - let suggestions = completer.complete(&input, cursor_pos); - - assert_eq!(suggestions.first().unwrap().value, "trombonist"); - } - - #[test] - fn test_tab_completion_partial_in_middle() { - let mut completer = CodeCompleter::default(); - let input = "22-trbis-zulu"; - let cursor_pos = input.len() - "-zulu".len(); - - let suggestions = completer.complete(&input, cursor_pos); - - assert_eq!(suggestions.first().unwrap().value, "trombonist"); - } -} diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 4058c4a5..6ac256fd 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -70,7 +70,6 @@ impl Wordlist { #[cfg(feature = "fuzzy-complete")] fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { // We use Jaro-Winkler algorithm because it emphasizes the beginning of a word - use fuzzt::algorithms::JaroWinkler; let words = words.iter().map(|w| w.as_str()).collect::>(); From 3ccb0f44efe80b84cec22f4fe85c4a30a8dc2edc Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Wed, 9 Oct 2024 18:49:15 +0200 Subject: [PATCH 55/59] fuzzy-complete feature tests --- src/core/wordlist.rs | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 6ac256fd..5a43ffd6 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -36,15 +36,15 @@ impl Wordlist { let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix)); - let matches = if cfg!(feature = "fuzzy-complete") { - self.fuzzy_complete(last_partial, words) - } else { - words - .iter() - .filter(|word| word.starts_with(last_partial)) - .cloned() - .collect() - }; + #[cfg(feature = "fuzzy-complete")] + let matches = self.fuzzy_complete(last_partial, words); + + #[cfg(not(feature = "fuzzy-complete"))] + let matches = words + .iter() + .filter(|word| word.starts_with(last_partial)) + .cloned() + .collect(); matches .into_iter() @@ -206,7 +206,8 @@ mod test { } #[test] - fn test_wormhole_code_completions() { + #[cfg(feature = "fuzzy-complete")] + fn test_wormhole_code_fuzzy_completions() { let list = default_wordlist(2); assert_eq!(list.get_completions("22"), Vec::::new()); @@ -227,4 +228,20 @@ mod test { assert_eq!(list.get_completions("22-chisel-tba"), ["22-chisel-tobacco"]); } + + #[test] + #[cfg(not(feature = "fuzzy-complete"))] + fn test_wormhole_code_normal_completions() { + let list = default_wordlist(2); + + assert_eq!(list.get_completions("22"), Vec::::new()); + assert_eq!(list.get_completions("22-"), Vec::::new()); + + // Invalid wormhole code check + assert_eq!(list.get_completions("tro"), Vec::::new()); + + assert_eq!(list.get_completions("22-chisel"), ["22-chisel"]); + + assert_eq!(list.get_completions("22-chisel-tob"), ["22-chisel-tobacco"]); + } } From afa509002302161b18eb8b90f2c122070b730890 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Thu, 10 Oct 2024 18:02:52 +0200 Subject: [PATCH 56/59] fixed conditional compile errors --- src/core/wordlist.rs | 53 +++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 5a43ffd6..8ffb5f4a 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -37,14 +37,10 @@ impl Wordlist { let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix)); #[cfg(feature = "fuzzy-complete")] - let matches = self.fuzzy_complete(last_partial, words); + let matches: Vec = self.fuzzy_complete(last_partial, words); #[cfg(not(feature = "fuzzy-complete"))] - let matches = words - .iter() - .filter(|word| word.starts_with(last_partial)) - .cloned() - .collect(); + let matches: Vec = self.normal_complete(last_partial, words); matches .into_iter() @@ -68,7 +64,7 @@ impl Wordlist { /// Fuzzy wormhole code completion #[cfg(feature = "fuzzy-complete")] - fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { + pub fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { // We use Jaro-Winkler algorithm because it emphasizes the beginning of a word use fuzzt::algorithms::JaroWinkler; @@ -80,6 +76,14 @@ impl Wordlist { .collect() } + pub fn normal_complete(&self, partial: &str, words: &[String]) -> Vec { + words + .iter() + .filter(|word| word.starts_with(partial)) + .cloned() + .collect() + } + /// Choose wormhole code word pub fn choose_words(&self) -> String { let mut rng = OsRng; @@ -230,18 +234,35 @@ mod test { } #[test] - #[cfg(not(feature = "fuzzy-complete"))] - fn test_wormhole_code_normal_completions() { - let list = default_wordlist(2); + #[cfg(feature = "fuzzy-complete")] + fn test_completion_fuzzy() { + let wl = default_wordlist(2); + let list = wl.get_wordlist("22-"); - assert_eq!(list.get_completions("22"), Vec::::new()); - assert_eq!(list.get_completions("22-"), Vec::::new()); + assert_eq!(wl.fuzzy_complete("chck", list), ["checkup", "choking"]); + assert_eq!(wl.fuzzy_complete("checkp", list), ["checkup"]); + assert_eq!( + wl.fuzzy_complete("checkup", list), + ["checkup", "lockup", "cleanup"] + ); + } - // Invalid wormhole code check - assert_eq!(list.get_completions("tro"), Vec::::new()); + #[test] + fn test_completion_normal() { + let wl = default_wordlist(2); + let list = wl.get_wordlist("22-"); - assert_eq!(list.get_completions("22-chisel"), ["22-chisel"]); + assert_eq!(wl.normal_complete("che", list), ["checkup"]); + } + + #[test] + fn test_full_wormhole_completion() { + let wl = default_wordlist(2); - assert_eq!(list.get_completions("22-chisel-tob"), ["22-chisel-tobacco"]); + assert_eq!(wl.get_completions("22-chec").first().unwrap(), "22-checkup"); + assert_eq!( + wl.get_completions("22-checkup-t").first().unwrap(), + "22-checkup-tobacco" + ); } } From 99dc3186acc1bf15a923433b73d6c7912cca53a2 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 11 Oct 2024 19:24:39 +0200 Subject: [PATCH 57/59] made helper functions private --- src/core.rs | 3 ++- src/core/wordlist.rs | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/core.rs b/src/core.rs index 3696c495..45601823 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,7 +5,8 @@ pub mod rendezvous; mod server_messages; #[cfg(test)] mod test; -#[doc(hidden)] + +/// Module for wormhole code generation and completion. pub mod wordlist; use serde_derive::{Deserialize, Serialize}; diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 8ffb5f4a..94485cba 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -31,16 +31,15 @@ impl Wordlist { /// Completion can be done either with fuzzy search (approximate string matching) /// or simple `starts_with` matching. pub fn get_completions(&self, prefix: &str) -> Vec { - let count_dashes = prefix.matches('-').count(); - let words = &self.words[count_dashes % self.words.len()]; + let words = self.get_wordlist(prefix); let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix)); - #[cfg(feature = "fuzzy-complete")] - let matches: Vec = self.fuzzy_complete(last_partial, words); - - #[cfg(not(feature = "fuzzy-complete"))] - let matches: Vec = self.normal_complete(last_partial, words); + let matches = if cfg!(feature = "fuzzy-complete") { + self.fuzzy_complete(last_partial, words) + } else { + self.normal_complete(last_partial, words) + }; matches .into_iter() @@ -56,15 +55,13 @@ impl Wordlist { .collect() } - /// Get either even or odd wordlist - pub fn get_wordlist(&self, prefix: &str) -> &Vec { + fn get_wordlist(&self, prefix: &str) -> &Vec { let count_dashes = prefix.matches('-').count(); &self.words[count_dashes % self.words.len()] } - /// Fuzzy wormhole code completion - #[cfg(feature = "fuzzy-complete")] - pub fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { + #[allow(unused)] + fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { // We use Jaro-Winkler algorithm because it emphasizes the beginning of a word use fuzzt::algorithms::JaroWinkler; @@ -76,7 +73,8 @@ impl Wordlist { .collect() } - pub fn normal_complete(&self, partial: &str, words: &[String]) -> Vec { + #[allow(unused)] + fn normal_complete(&self, partial: &str, words: &[String]) -> Vec { words .iter() .filter(|word| word.starts_with(partial)) From 3dead09810a368172734c47ff04dd766fadb3832 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 11 Oct 2024 22:00:43 +0200 Subject: [PATCH 58/59] fixed compilation error with --no-default-features --- src/core/wordlist.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 4c12f26e..53de21ed 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -37,11 +37,10 @@ impl Wordlist { let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix)); - let matches = if cfg!(feature = "fuzzy-complete") { - self.fuzzy_complete(last_partial, words) - } else { - self.normal_complete(last_partial, words) - }; + #[cfg(feature = "fuzzy-complete")] + let matches = self.fuzzy_complete(last_partial, words); + #[cfg(not(feature = "fuzzy-complete"))] + let matches = self.fuzzy_complete(last_partial, words); matches .into_iter() @@ -62,7 +61,7 @@ impl Wordlist { &self.words[count_dashes % self.words.len()] } - #[allow(unused)] + #[cfg(feature = "fuzzy-complete")] fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { // We use Jaro-Winkler algorithm because it emphasizes the beginning of a word use fuzzt::algorithms::JaroWinkler; From faff3cd8843f71b370cb5393dc01be1d03362741 Mon Sep 17 00:00:00 2001 From: Elias Dalbeck Date: Fri, 11 Oct 2024 22:00:43 +0200 Subject: [PATCH 59/59] fixed compilation error with --no-default-features --- src/core/wordlist.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/wordlist.rs b/src/core/wordlist.rs index 4c12f26e..f059f5d4 100644 --- a/src/core/wordlist.rs +++ b/src/core/wordlist.rs @@ -37,11 +37,10 @@ impl Wordlist { let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix)); - let matches = if cfg!(feature = "fuzzy-complete") { - self.fuzzy_complete(last_partial, words) - } else { - self.normal_complete(last_partial, words) - }; + #[cfg(feature = "fuzzy-complete")] + let matches = self.fuzzy_complete(last_partial, words); + #[cfg(not(feature = "fuzzy-complete"))] + let matches = self.normal_complete(last_partial, words); matches .into_iter() @@ -62,7 +61,7 @@ impl Wordlist { &self.words[count_dashes % self.words.len()] } - #[allow(unused)] + #[cfg(feature = "fuzzy-complete")] fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec { // We use Jaro-Winkler algorithm because it emphasizes the beginning of a word use fuzzt::algorithms::JaroWinkler;