claude-manager 0.2.2

A terminal UI for managing multiple Claude Code sessions organized by projects and tasks
mod app;
mod config;
mod tmux;
mod ui;
mod worker;

use std::io;
use std::time::Duration;

use anyhow::Result;
use crossterm::ExecutableCommand;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;

use app::{App, InputMode};

fn main() -> Result<()> {
    let mut app = App::new()?;

    loop {
        enable_raw_mode()?;
        io::stdout().execute(EnterAlternateScreen)?;
        let backend = CrosstermBackend::new(io::stdout());
        let mut terminal = Terminal::new(backend)?;

        run_tui(&mut terminal, &mut app)?;

        disable_raw_mode()?;
        io::stdout().execute(LeaveAlternateScreen)?;

        if app.should_quit {
            break;
        }

        if let Some(session_name) = app.should_attach.take() {
            tmux::attach_session(&session_name)?;
        } else if let Some((session_name, window_idx)) = app.should_attach_window.take() {
            tmux::attach_session_window(&session_name, window_idx)?;
        } else if let Some(path) = app.should_open_editor.take() {
            let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".into());
            std::process::Command::new(&editor).arg(&path).status()?;
        }
    }

    Ok(())
}

fn run_tui(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) -> Result<()> {
    app.sync_worker_hints();

    loop {
        terminal.draw(|f| ui::draw(f, app))?;

        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                if key.kind != KeyEventKind::Press {
                    continue;
                }

                if app.loading {
                    // Only allow quit while loading
                    if key.code == KeyCode::Char(app.keybindings.quit) {
                        app.should_quit = true;
                        return Ok(());
                    }
                    continue;
                }

                let kb = app.keybindings.clone();
                match app.input_mode {
                    InputMode::Normal => match key.code {
                        KeyCode::Char(c) if c == kb.quit => {
                            app.should_quit = true;
                            return Ok(());
                        }
                        KeyCode::Up => app.move_up(),
                        KeyCode::Down => app.move_down(),
                        KeyCode::Char(c) if c == kb.move_up => app.move_up(),
                        KeyCode::Char(c) if c == kb.move_down => app.move_down(),
                        KeyCode::Enter => {
                            app.enter_selected();
                            if app.should_attach.is_some()
                                || app.should_attach_window.is_some()
                                || app.should_open_editor.is_some()
                            {
                                return Ok(());
                            }
                        }
                        KeyCode::Char(c) if c == kb.toggle_collapse => app.toggle_collapse(),
                        KeyCode::Char(c) if c == kb.context_menu => app.open_context_menu(),
                        KeyCode::Char(c) if c == kb.add_project => app.start_add_project(),
                        KeyCode::Char(c) if c == kb.scroll_preview_down => {
                            app.scroll_preview_down()
                        }
                        KeyCode::Char(c) if c == kb.scroll_preview_up => app.scroll_preview_up(),
                        KeyCode::Tab => app.toggle_preview_mode(),
                        _ => {}
                    },
                    InputMode::ContextMenu => match key.code {
                        KeyCode::Esc => {
                            app.input_mode = InputMode::Normal;
                        }
                        KeyCode::Char(c) if c == kb.context_menu => {
                            app.input_mode = InputMode::Normal;
                        }
                        KeyCode::Up => {
                            if app.context_menu_selected > 0 {
                                app.context_menu_selected -= 1;
                            }
                        }
                        KeyCode::Char(c) if c == kb.move_up => {
                            if app.context_menu_selected > 0 {
                                app.context_menu_selected -= 1;
                            }
                        }
                        KeyCode::Down => {
                            if app.context_menu_selected + 1 < app.context_menu_items.len() {
                                app.context_menu_selected += 1;
                            }
                        }
                        KeyCode::Char(c) if c == kb.move_down => {
                            if app.context_menu_selected + 1 < app.context_menu_items.len() {
                                app.context_menu_selected += 1;
                            }
                        }
                        KeyCode::Enter => {
                            if let Some(item) =
                                app.context_menu_items.get(app.context_menu_selected)
                            {
                                let action = item.action;
                                app.execute_context_action(action);
                            }
                        }
                        KeyCode::Char(c) => {
                            if let Some(item) = app.context_menu_items.iter().find(|i| i.key == c) {
                                let action = item.action;
                                app.execute_context_action(action);
                            }
                        }
                        _ => {}
                    },
                    InputMode::AddProjectName => match key.code {
                        KeyCode::Enter => app.confirm_add_project(),
                        KeyCode::Esc => app.cancel_input(),
                        KeyCode::Backspace => {
                            app.input_buffer.pop();
                        }
                        KeyCode::Char(c) => app.input_buffer.push(c),
                        _ => {}
                    },
                    InputMode::AddTaskName => match key.code {
                        KeyCode::Enter => app.confirm_add_task(),
                        KeyCode::Esc => app.cancel_input(),
                        KeyCode::Backspace => {
                            app.input_buffer.pop();
                        }
                        KeyCode::Char(c) => app.input_buffer.push(c),
                        _ => {}
                    },
                    InputMode::AddTaskBranch => match key.code {
                        KeyCode::Enter => app.confirm_add_task_branch(),
                        KeyCode::Esc => app.cancel_input(),
                        KeyCode::Backspace => {
                            app.input_buffer.pop();
                        }
                        KeyCode::Char(c) => app.input_buffer.push(c),
                        _ => {}
                    },
                    InputMode::AddSessionName => match key.code {
                        KeyCode::Enter => {
                            app.confirm_new_session();
                        }
                        KeyCode::Esc => app.cancel_input(),
                        KeyCode::Backspace => {
                            app.input_buffer.pop();
                        }
                        KeyCode::Char(c) => app.input_buffer.push(c),
                        _ => {}
                    },
                    InputMode::AddSessionPrompt => match key.code {
                        KeyCode::Enter => {
                            app.confirm_new_session_with_prompt();
                        }
                        KeyCode::Esc => app.cancel_input(),
                        KeyCode::Backspace => {
                            app.input_buffer.pop();
                        }
                        KeyCode::Char(c) => app.input_buffer.push(c),
                        _ => {}
                    },
                    InputMode::ConfirmDelete => match key.code {
                        KeyCode::Char('y') => app.confirm_delete(),
                        KeyCode::Char('n') | KeyCode::Esc => app.cancel_input(),
                        _ => {}
                    },
                    InputMode::RenameProject | InputMode::RenameTask | InputMode::RenameSession => {
                        match key.code {
                            KeyCode::Enter => app.confirm_rename(),
                            KeyCode::Esc => app.cancel_input(),
                            KeyCode::Backspace => {
                                app.input_buffer.pop();
                            }
                            KeyCode::Char(c) => app.input_buffer.push(c),
                            _ => {}
                        }
                    }
                    InputMode::ConfirmCreatePr => match key.code {
                        KeyCode::Char('y') => app.confirm_create_pr(),
                        KeyCode::Char('n') | KeyCode::Esc => app.cancel_input(),
                        _ => {}
                    },
                    InputMode::MergeCommitMessage => match key.code {
                        KeyCode::Enter => app.confirm_merge_commit(),
                        KeyCode::Esc => app.cancel_input(),
                        KeyCode::Backspace => {
                            app.input_buffer.pop();
                        }
                        KeyCode::Char(c) => app.input_buffer.push(c),
                        _ => {}
                    },
                }
            }
        }

        // Apply background updates (non-blocking)
        app.apply_worker_updates();
        app.apply_op_results();
        app.tick = app.tick.wrapping_add(1);

        if app.should_quit {
            return Ok(());
        }
    }
}