mod app;
mod event;
mod terminal;
mod tree;
mod ui;
pub mod vterm;
use anyhow::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::prelude::*;
use std::io;
use std::path::PathBuf;
use app::App;
use event::EventHandler;
struct Args {
path: PathBuf,
tree_width: u16,
show_hidden: bool,
depth: usize,
claude_args: Vec<String>,
}
fn parse_args() -> Args {
let raw: Vec<String> = std::env::args().skip(1).collect();
let mut path = PathBuf::from(".");
let mut tree_width: u16 = 30;
let mut show_hidden = false;
let mut depth: usize = 10;
let mut claude_args = Vec::new();
let value_flags: &[&[&str]] = &[
&["-p", "--path"],
&["-w", "--tree-width"],
&["-d", "--depth"],
];
let mut i = 0;
while i < raw.len() {
let arg = &raw[i];
if arg == "-h" || arg == "--help" {
eprintln!(
"A TUI file explorer for Claude Code CLI\n\n\
Usage: cltree [OPTIONS] [CLAUDE_ARGS...]\n\n\
Options:\n\
\x20 -p, --path <PATH> Working directory [default: .]\n\
\x20 -w, --tree-width <WIDTH> Tree panel width %% (10-50) [default: 30]\n\
\x20 -a, --show-hidden Show hidden files\n\
\x20 -d, --depth <DEPTH> Max tree depth [default: 10]\n\
\x20 -h, --help Print help\n\
\x20 -V, --version Print version\n\n\
All other arguments are passed through to Claude Code CLI.\n\
Example: cltree --resume\n\
Example: cltree -p /my/project --continue"
);
std::process::exit(0);
}
if arg == "-V" || arg == "--version" {
eprintln!("cltree {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let mut matched_value_flag = false;
for names in value_flags {
for name in *names {
if let Some(val) = arg.strip_prefix(&format!("{name}=")) {
match *name {
"-p" | "--path" => path = PathBuf::from(val),
"-w" | "--tree-width" => tree_width = val.parse().unwrap_or(30),
"-d" | "--depth" => depth = val.parse().unwrap_or(10),
_ => {}
}
matched_value_flag = true;
break;
}
}
if matched_value_flag {
break;
}
if names.contains(&arg.as_str()) {
let val = raw.get(i + 1).cloned().unwrap_or_default();
match names[1] {
"--path" => path = PathBuf::from(&val),
"--tree-width" => tree_width = val.parse().unwrap_or(30),
"--depth" => depth = val.parse().unwrap_or(10),
_ => {}
}
i += 2;
matched_value_flag = true;
break;
}
}
if matched_value_flag {
if !arg.contains('=') {
continue; }
i += 1;
continue;
}
if arg == "-a" || arg == "--show-hidden" {
show_hidden = true;
i += 1;
continue;
}
claude_args.push(arg.clone());
i += 1;
}
Args {
path,
tree_width,
show_hidden,
depth,
claude_args,
}
}
#[tokio::main]
async fn main() -> Result<()> {
let args = parse_args();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let (pty_tx, pty_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
args.path,
args.tree_width,
args.show_hidden,
args.depth,
args.claude_args,
pty_tx,
)?;
let watch_path = Some(app.tree.root_path().to_path_buf());
let event_handler = EventHandler::new(200, watch_path, pty_rx);
let result = run_app(&mut terminal, &mut app, event_handler).await;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = result {
eprintln!("Error: {err:?}");
std::process::exit(1);
}
Ok(())
}
async fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
mut event_handler: EventHandler,
) -> Result<()> {
loop {
terminal.draw(|frame| ui::draw(frame, app))?;
match event_handler.next().await? {
event::Event::Tick => {
if app.tick() {
return Ok(());
}
}
event::Event::Key(key_event) => {
if app.handle_key(key_event) {
return Ok(());
}
}
event::Event::Mouse(mouse_event) => {
app.handle_mouse(mouse_event);
}
event::Event::Resize(_width, _height) => {}
event::Event::FileChange(path) => {
app.handle_file_change(path);
}
event::Event::PtyOutput => {
}
}
}
}