add fuzzy filename searching for sending files
This commit is contained in:
parent
3dd7a7281b
commit
8150bfacf2
5 changed files with 188 additions and 31 deletions
27
Cargo.lock
generated
27
Cargo.lock
generated
|
@ -414,7 +414,7 @@ dependencies = [
|
|||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -545,7 +545,7 @@ dependencies = [
|
|||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
|
@ -1348,6 +1348,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha256",
|
||||
"simsearch",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
@ -2276,6 +2277,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simsearch"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c869b25830e4824ef7279015cfc298a0674aca6a54eeff2efce8d12bf3701fe"
|
||||
dependencies = [
|
||||
"strsim 0.10.0",
|
||||
"triple_accel",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
|
@ -2310,6 +2321,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
@ -2678,6 +2695,12 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triple_accel"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c"
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
|
|
@ -32,6 +32,7 @@ rustix = { version = "1", default-features = false, features = ["system"] }
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha256 = "1.6"
|
||||
simsearch = "0.2"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12", "logging"] }
|
||||
|
|
164
src/app/mod.rs
164
src/app/mod.rs
|
@ -1,4 +1,9 @@
|
|||
use std::{collections::BTreeMap, net::SocketAddr, time::Duration};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
|
@ -7,9 +12,10 @@ use julid::Julid;
|
|||
use log::{LevelFilter, debug, error, warn};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
widgets::{ListState, TableState},
|
||||
widgets::{ListState, TableState, WidgetRef},
|
||||
};
|
||||
use ratatui_explorer::FileExplorer;
|
||||
use simsearch::{SearchOptions, SimSearch};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||
|
||||
|
@ -22,6 +28,22 @@ pub struct Peer {
|
|||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FileFinder {
|
||||
explorer: FileExplorer,
|
||||
fuzzy: SimSearch<usize>,
|
||||
working_dir: Option<PathBuf>,
|
||||
input: Input,
|
||||
}
|
||||
|
||||
fn searcher() -> SimSearch<usize> {
|
||||
SimSearch::new_with(
|
||||
SearchOptions::new()
|
||||
.stop_words(vec![std::path::MAIN_SEPARATOR_STR.to_string()])
|
||||
.stop_whitespace(false),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub service: JocalService,
|
||||
pub events: EventStream,
|
||||
|
@ -33,7 +55,7 @@ pub struct App {
|
|||
// for getting messages back from the web server or web client about things we've done; the
|
||||
// other end is held by the service
|
||||
event_listener: UnboundedReceiver<TransferEvent>,
|
||||
file_picker: FileExplorer,
|
||||
file_finder: FileFinder,
|
||||
text: Option<String>,
|
||||
input: Input,
|
||||
}
|
||||
|
@ -49,18 +71,24 @@ pub enum CurrentScreen {
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SendingScreen {
|
||||
Files,
|
||||
Files(FileMode),
|
||||
Peers,
|
||||
Text,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileMode {
|
||||
Picking,
|
||||
Fuzzy,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(service: JocalService, event_listener: UnboundedReceiver<TransferEvent>) -> Self {
|
||||
App {
|
||||
service,
|
||||
event_listener,
|
||||
screen: vec![CurrentScreen::Main],
|
||||
file_picker: FileExplorer::new().expect("could not create file explorer"),
|
||||
file_finder: FileFinder::new().expect("could not create file explorer"),
|
||||
text: None,
|
||||
events: Default::default(),
|
||||
peers: Default::default(),
|
||||
|
@ -78,7 +106,7 @@ impl App {
|
|||
match evt {
|
||||
Event::Key(key)
|
||||
if key.kind == KeyEventKind::Press
|
||||
=> self.handle_key_event(key, evt).await,
|
||||
=> self.handle_key_event(key, evt).await,
|
||||
Event::Mouse(_) => {}
|
||||
Event::Resize(_, _) => {}
|
||||
_ => {}
|
||||
|
@ -109,7 +137,7 @@ impl App {
|
|||
}
|
||||
|
||||
pub fn files(&mut self) -> &mut FileExplorer {
|
||||
&mut self.file_picker
|
||||
&mut self.file_finder.explorer
|
||||
}
|
||||
|
||||
pub fn text(&mut self) -> &mut Option<String> {
|
||||
|
@ -130,18 +158,18 @@ impl App {
|
|||
CurrentScreen::Main
|
||||
| CurrentScreen::Logging
|
||||
| CurrentScreen::Receiving
|
||||
| CurrentScreen::Sending(SendingScreen::Files)
|
||||
| CurrentScreen::Sending(SendingScreen::Files(FileMode::Picking))
|
||||
| CurrentScreen::Sending(SendingScreen::Peers) => match code {
|
||||
KeyCode::Esc => self.pop(),
|
||||
KeyCode::Char('q') => self.exit().await,
|
||||
KeyCode::Char('s') => self.send(),
|
||||
KeyCode::Char('r') => self.recv(),
|
||||
KeyCode::Char('l') => self.logs(),
|
||||
KeyCode::Char('m') => self.main(),
|
||||
KeyCode::Esc => self.pop(),
|
||||
_ => match mode {
|
||||
CurrentScreen::Main => {
|
||||
if code == KeyCode::Char('d') {
|
||||
self.service.refresh_peers().await;
|
||||
if let KeyCode::Char('d') = code {
|
||||
self.service.refresh_peers().await
|
||||
}
|
||||
}
|
||||
CurrentScreen::Logging => match code {
|
||||
|
@ -157,14 +185,20 @@ impl App {
|
|||
_ => {}
|
||||
},
|
||||
CurrentScreen::Sending(sending_screen) => match sending_screen {
|
||||
SendingScreen::Files => match code {
|
||||
// we can only be in picking mode
|
||||
SendingScreen::Files(fmode) => match code {
|
||||
KeyCode::Char('t') => *sending_screen = SendingScreen::Text,
|
||||
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
||||
KeyCode::Enter => self.chdir_or_send_file().await,
|
||||
_ => self.file_picker.handle(&event).unwrap_or_default(),
|
||||
KeyCode::Char('/') => {
|
||||
*fmode = FileMode::Fuzzy;
|
||||
}
|
||||
_ => self.file_finder.handle(&event).unwrap_or_default(),
|
||||
},
|
||||
SendingScreen::Peers => match code {
|
||||
KeyCode::Tab => *sending_screen = SendingScreen::Files,
|
||||
KeyCode::Tab => {
|
||||
*sending_screen = SendingScreen::Files(FileMode::Picking)
|
||||
}
|
||||
KeyCode::Char('t') => *sending_screen = SendingScreen::Text,
|
||||
KeyCode::Enter => self.send_content().await,
|
||||
KeyCode::Up => self.peer_state.select_previous(),
|
||||
|
@ -176,7 +210,7 @@ impl App {
|
|||
CurrentScreen::Stopping => unreachable!(),
|
||||
},
|
||||
},
|
||||
// we only need to deal with sending text now
|
||||
// we only need to deal with sending text now or doing fuzzy matching
|
||||
CurrentScreen::Sending(sending_screen) => match sending_screen {
|
||||
SendingScreen::Text => match code {
|
||||
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
||||
|
@ -184,7 +218,7 @@ impl App {
|
|||
KeyCode::Esc => {
|
||||
self.text = None;
|
||||
self.input.reset();
|
||||
*sending_screen = SendingScreen::Files;
|
||||
*sending_screen = SendingScreen::Files(FileMode::Picking);
|
||||
}
|
||||
_ => {
|
||||
if let Some(changed) = self.input.handle_event(&event)
|
||||
|
@ -198,8 +232,39 @@ impl App {
|
|||
}
|
||||
}
|
||||
},
|
||||
// we've already handled the other sending modes
|
||||
SendingScreen::Files | SendingScreen::Peers => unreachable!(),
|
||||
SendingScreen::Files(fmode) => {
|
||||
if *fmode == FileMode::Fuzzy {
|
||||
match code {
|
||||
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
|
||||
KeyCode::Enter => self.chdir_or_send_file().await,
|
||||
KeyCode::Esc => {
|
||||
self.file_finder.reset_fuzzy();
|
||||
*fmode = FileMode::Picking;
|
||||
}
|
||||
KeyCode::Up | KeyCode::Down => {
|
||||
if let Err(e) = self.file_finder.handle(&event) {
|
||||
log::error!("error selecting file: {e:?}");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.file_finder.index();
|
||||
if let Some(changed) = self.file_finder.input.handle_event(&event)
|
||||
&& changed.value
|
||||
{
|
||||
let id = self
|
||||
.file_finder
|
||||
.fuzzy
|
||||
.search(self.file_finder.input.value())
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
self.file_finder.explorer.set_selected_idx(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SendingScreen::Peers => unreachable!(),
|
||||
},
|
||||
CurrentScreen::Stopping => {}
|
||||
}
|
||||
|
@ -220,7 +285,9 @@ impl App {
|
|||
Some(CurrentScreen::Sending(_)) => {}
|
||||
_ => self
|
||||
.screen
|
||||
.push(CurrentScreen::Sending(SendingScreen::Files)),
|
||||
.push(CurrentScreen::Sending(SendingScreen::Files(
|
||||
FileMode::Picking,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,12 +362,14 @@ impl App {
|
|||
}
|
||||
|
||||
async fn chdir_or_send_file(&mut self) {
|
||||
let file = self.file_picker.current().path().clone();
|
||||
let file = self.file_finder.explorer.current().path().clone();
|
||||
if file.is_dir()
|
||||
&& let Err(e) = self.file_picker.set_cwd(&file)
|
||||
&& let Err(e) = self.file_finder.set_cwd(&file)
|
||||
{
|
||||
error!("could not list directory {file:?}: {e}");
|
||||
return;
|
||||
} else if file.is_dir() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(peer_idx) = self.peer_state.selected() else {
|
||||
|
@ -352,6 +421,59 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
impl FileFinder {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
explorer: FileExplorer::new()?,
|
||||
fuzzy: searcher(),
|
||||
working_dir: None,
|
||||
input: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle(&mut self, event: &Event) -> Result<()> {
|
||||
self.index();
|
||||
Ok(self.explorer.handle(event)?)
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> &Path {
|
||||
self.explorer.cwd()
|
||||
}
|
||||
|
||||
pub fn set_cwd(&mut self, cwd: &Path) -> Result<()> {
|
||||
self.explorer.set_cwd(cwd)?;
|
||||
self.index();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn widget(&self) -> impl WidgetRef {
|
||||
self.explorer.widget()
|
||||
}
|
||||
|
||||
pub fn reset_fuzzy(&mut self) {
|
||||
self.clear_fuzzy();
|
||||
self.input.reset();
|
||||
}
|
||||
|
||||
fn clear_fuzzy(&mut self) {
|
||||
self.fuzzy = searcher();
|
||||
}
|
||||
|
||||
fn index(&mut self) {
|
||||
if let Some(owd) = self.working_dir.as_ref()
|
||||
&& owd == self.cwd()
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.working_dir = Some(self.cwd().to_path_buf());
|
||||
self.clear_fuzzy();
|
||||
|
||||
for (i, f) in self.explorer.files().iter().enumerate() {
|
||||
self.fuzzy.insert(i, f.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn change_log_level(delta: isize) {
|
||||
let level = log::max_level() as isize;
|
||||
let max = log::LevelFilter::max() as isize;
|
||||
|
|
|
@ -15,7 +15,7 @@ use ratatui::{
|
|||
};
|
||||
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState};
|
||||
|
||||
use super::{App, CurrentScreen, Peer, SendingScreen};
|
||||
use super::{App, CurrentScreen, FileMode, Peer, SendingScreen};
|
||||
|
||||
static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| {
|
||||
Line::from(vec![
|
||||
|
@ -175,7 +175,7 @@ impl Widget for &mut App {
|
|||
}
|
||||
CurrentScreen::Sending(s) => {
|
||||
match s {
|
||||
SendingScreen::Files => {
|
||||
SendingScreen::Files(_) => {
|
||||
outer_frame(¤t_screen, &CONTENT_SEND_FILE_MENU, area, buf)
|
||||
}
|
||||
SendingScreen::Peers => {
|
||||
|
@ -186,9 +186,21 @@ impl Widget for &mut App {
|
|||
}
|
||||
}
|
||||
|
||||
self.file_picker
|
||||
.widget()
|
||||
.render(header_left.inner(header_margin), buf);
|
||||
let file_area = header_left.inner(header_margin);
|
||||
|
||||
match s {
|
||||
SendingScreen::Files(FileMode::Picking) => {
|
||||
self.file_finder.widget().render(file_area, buf);
|
||||
}
|
||||
SendingScreen::Files(FileMode::Fuzzy) => {
|
||||
let layout = Layout::vertical([Constraint::Max(6), Constraint::Min(5)]);
|
||||
let [input, files] = layout.areas(file_area);
|
||||
text_popup(self.file_finder.input.value(), "fuzzy search", input, buf);
|
||||
self.file_finder.widget().render(files, buf);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
logger(header_right.inner(header_margin), buf);
|
||||
|
||||
peers(
|
||||
|
|
|
@ -69,13 +69,12 @@ async fn start_and_run(terminal: &mut DefaultTerminal, config: Config) -> Result
|
|||
}
|
||||
|
||||
if app.screen() == CurrentScreen::Stopping {
|
||||
log::info!("shutting down");
|
||||
tokio::select! {
|
||||
_ = shutdown.as_mut() => {
|
||||
break;
|
||||
}
|
||||
_ = tick.tick() => {
|
||||
log::info!("shutting down");
|
||||
}
|
||||
_ = tick.tick() => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue