Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
513 changes: 414 additions & 99 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions lsf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ clap = { version = "4", features = ["derive"] }
anyhow = "1.0.97"
crossbeam-channel = "0.5.15"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
rustyline = "18.0.0"
clap-verbosity-flag = {version = "3.0.4", features = ["tracing"]}
ratatui = "0.29.0"
crossterm = "0.28.1"
jiff = "0.2"
toml = "1.1"
13 changes: 13 additions & 0 deletions lsf/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@ pub struct Cli {
#[clap(long, default_value = "false")]
/// Open enabled, cache was ignored and filesystem will be rewalked.
pub refresh: bool,
#[clap(long, default_value = "false")]
/// Launch the ratatui search interface instead of the line-based prompt.
pub tui: bool,
#[clap(long, default_value = "false")]
/// Exit the TUI immediately without a quit confirmation prompt.
pub no_quit_confirm: bool,
#[clap(long, default_value = "~/.cardinal/cache.zstd")]
/// Cache file path. Supports a leading `~/`.
pub cache_path: PathBuf,
#[clap(long, default_value = "/")]
pub path: PathBuf,
/// Path to a TOML keybinding file. Defaults to ~/.cardinal/lsf-keys.toml
/// if the file exists; built-in defaults are used otherwise.
#[clap(long)]
pub keymap: Option<PathBuf>,
#[command(flatten)]
pub verbosity: clap_verbosity_flag::Verbosity,
}
1 change: 1 addition & 0 deletions lsf/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod tui;
178 changes: 50 additions & 128 deletions lsf/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
mod cli;

use anyhow::{Context, Result};
use cardinal_sdk::EventWatcher;
use anyhow::Result;
use clap::Parser;
use cli::Cli;
use crossbeam_channel::{Sender, bounded, unbounded};
use rustyline::{DefaultEditor, error::ReadlineError};
use search_cache::{HandleFSEError, SearchCache, SearchResultNode};
use search_cancel::CancellationToken;
use lsf::tui::{
app::{AppConfig, AppRuntime, resolve_cache_path},
keymap::Keymap,
run_with_options,
};
use std::{
path::{Path, PathBuf},
sync::atomic::AtomicBool,
io::{self, Write},
path::PathBuf,
};
use tracing_subscriber::EnvFilter;

const CACHE_PATH: &str = "target/cache.zstd";
const IGNORE_PATH: &str = "/System/Volumes/Data"; // macOS specific ignore path
static NEVER_STOPPED: AtomicBool = AtomicBool::new(false);

fn main() -> Result<()> {
let cli = Cli::parse();

Expand All @@ -28,137 +24,63 @@ fn main() -> Result<()> {
builder.with_max_level(cli.verbosity.tracing_level()).init();
}

let path = cli.path;
let ignore_paths = vec![PathBuf::from(IGNORE_PATH)];
let mut cache = if cli.refresh {
println!("Walking filesystem...");
SearchCache::walk_fs_with_ignore(&path, &ignore_paths)
} else {
println!("Try reading cache...");
SearchCache::try_read_persistent_cache(
&path,
Path::new(CACHE_PATH),
&ignore_paths,
&NEVER_STOPPED,
)
.unwrap_or_else(|e| {
println!("Failed to read cache: {e:?}. Re-walking filesystem...");
SearchCache::walk_fs_with_ignore(&path, &ignore_paths)
})
};
let runtime = AppRuntime::start(AppConfig {
path: cli.path,
cache_path: resolve_cache_path(&cli.cache_path)?,
refresh: cli.refresh,
})?;

println!("Cache is: {cache:?}");
if cli.tui {
let keymap = load_keymap(cli.keymap)?;
let tui_result = run_with_options(&runtime, !cli.no_quit_confirm, keymap);
runtime.shutdown()?;
return tui_result;
}

let (finish_tx, finish_rx) = bounded::<Sender<SearchCache>>(1);
let (search_tx, search_rx) = unbounded::<String>();
let (search_result_tx, search_result_rx) = unbounded::<Result<Vec<SearchResultNode>>>();
loop {
print!("> ");
io::stdout().flush()?;

std::thread::spawn(move || {
let (dev, mut event_watcher) = EventWatcher::spawn(
"/".to_string(),
cache.last_event_id(),
0.1,
cache.ignore_paths(),
);
println!("Processing changes of dev:{dev} during preparation.");
loop {
crossbeam_channel::select! {
recv(finish_rx) -> tx => {
let tx = tx.expect("finish_tx is closed");
tx.send(cache).expect("finish_tx is closed");
break;
}
recv(search_rx) -> query => {
let query = query.expect("search_tx is closed");
let files = cache.query_files(query, CancellationToken::noop()).map(|x| x.unwrap());
search_result_tx
.send(files)
.expect("search_result_tx is closed");
}
recv(event_watcher) -> events => {
let events = events.expect("event_stream is closed");
if let Err(HandleFSEError::Rescan) = cache.handle_fs_events(events) {
println!("!!!!!!!!!! Rescan triggered !!!!!!!!");
// Here we clear event_watcher first as rescan may take a lot of time
#[allow(unused_assignments)]
{
event_watcher = EventWatcher::noop();
}
let mut scan_root = PathBuf::new();
let mut scan_ignore_paths = Vec::new();
let walk_data = cache.walk_data(
&mut scan_root,
&mut scan_ignore_paths,
CancellationToken::new_scan(),
);
let _ = cache.rescan_with_walk_data(&walk_data);
event_watcher = EventWatcher::spawn(
"/".to_string(),
cache.last_event_id(),
0.1,
cache.ignore_paths(),
)
.1;
}
}
}
let mut line = String::new();
let read = io::stdin().read_line(&mut line)?;
if read == 0 {
eprintln!("EOF");
break;
}
println!("fsevent processing is done");
});

let mut rl = DefaultEditor::new().expect("Failed to create rustyline editor");
loop {
let readline = rl.readline("> ");
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
} else if line == "/bye" {
break;
}
let line = line.trim();
if line.is_empty() {
continue;
} else if line == "/bye" {
break;
}

let _ = rl.add_history_entry(line);
runtime.record_history(line)?;

search_tx
.send(line.to_string())
.context("search_tx is closed")?;
let search_result = search_result_rx
.recv()
.context("search_result_rx is closed")?;
match search_result {
Ok(path_set) => {
for (i, path) in path_set.into_iter().enumerate() {
println!("[{i}] {:?} {:?}", path.path, path.metadata);
}
}
Err(e) => {
eprintln!("Failed to search: {e:?}");
}
match runtime.search(line.to_string()) {
Ok(path_set) => {
for (i, path) in path_set.results.into_iter().enumerate() {
println!("[{i}] {:?} {:?}", path.path, path.metadata);
}
}
Err(ReadlineError::Interrupted) => {
eprintln!("Interrupted (Ctrl-C)");
break;
}
Err(ReadlineError::Eof) => {
eprintln!("EOF (Ctrl-D)");
break;
}
Err(err) => {
eprintln!("Error: {:?}", err);
break;
}
}
}

let (cache_tx, cache_rx) = bounded::<SearchCache>(1);
finish_tx.send(cache_tx).context("cache_tx is closed")?;
let cache = cache_rx.recv().context("cache_tx is closed")?;
println!("start writing cache: {cache:?}");
cache
.flush_to_file(Path::new(CACHE_PATH))
.context("Failed to write cache to file")?;
runtime.shutdown()
}

Ok(())
/// Resolve and load the keymap. Explicit `--keymap` path is required to exist;
/// the default path is tried silently and falls back to built-in defaults.
fn load_keymap(explicit: Option<PathBuf>) -> Result<Keymap> {
if let Some(path) = explicit {
return Keymap::load(&path);
}
let default_path = std::env::var_os("HOME")
.map(|h| PathBuf::from(h).join(".cardinal").join("lsf-keys.toml"))
.unwrap_or_default();
Comment on lines +82 to +84

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_keymap uses HOME and falls back to PathBuf::default() when it isn't set. An empty PathBuf typically resolves to the current directory ("."), so Keymap::load may try to read a directory and fail unexpectedly. Handle missing HOME by directly returning Keymap::default() (or use a platform-appropriate config dir) instead of passing an empty path to Keymap::load.

Suggested change
let default_path = std::env::var_os("HOME")
.map(|h| PathBuf::from(h).join(".cardinal").join("lsf-keys.toml"))
.unwrap_or_default();
let Some(home) = std::env::var_os("HOME") else {
return Ok(Keymap::default());
};
let default_path = PathBuf::from(home).join(".cardinal").join("lsf-keys.toml");

Copilot uses AI. Check for mistakes.
Keymap::load(&default_path)
}
Loading
Loading