quinjet 0.0.50

A fast, live, keyboard-first Git source-control interface for the terminal
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::Terminal;
use ratatui::backend::TestBackend;

use super::*;
use crate::git::Worktree;
use crate::ssh::SshMachine;
use crate::theme::{Appearance, ThemeName};

fn key(code: KeyCode) -> KeyEvent {
    KeyEvent::new(code, KeyModifiers::NONE)
}

fn project_group() -> ProjectGroup {
    ProjectGroup {
        name: "quinjet".to_owned(),
        common_dir: PathBuf::from("/repos/quinjet/.git"),
        worktrees: vec![Worktree {
            path: PathBuf::from("/repos/quinjet"),
            head: "0123456789abcdef".to_owned(),
            updated_at: Some("2026-08-22T18:00:00Z".to_owned()),
            updated_unix: Some(1_776_964_800),
            branch: Some("main".to_owned()),
            current: false,
            bare: false,
            detached: false,
            locked: None,
            prunable: None,
        }],
    }
}

#[test]
fn empty_screen_reuses_the_grouped_project_picker() {
    let mut onboarding = Onboarding::from_groups(Path::new("/tmp"), vec![project_group()], None);
    let backend = TestBackend::new(100, 30);
    let mut terminal = Terminal::new(backend).unwrap();
    let theme = Theme::new(ThemeName::Quinjet, Appearance::Dark);
    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(rendered.contains("Open a project"));
    assert!(rendered.contains("quinjet"));
    assert!(rendered.contains("main"));
    assert!(rendered.contains("/repos/quinjet"));
    assert!(rendered.contains("[⌄]"));
    assert!(rendered.contains("Ctrl+E all"));
    assert!(!rendered.contains("fatal:"));
}

#[test]
fn project_loading_is_visible_until_background_results_arrive() {
    let mut onboarding = Onboarding::from_groups(Path::new("/tmp"), Vec::new(), None);
    let (sender, receiver) = crossbeam_channel::bounded(1);
    onboarding.project_loader = ProjectLoader::waiting(receiver);
    let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
    let theme = Theme::new(ThemeName::Quinjet, Appearance::Dark);

    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(rendered.contains("Loading projects…"));
    assert!(rendered.contains(''));

    sender.send(vec![project_group()]).unwrap();
    assert!(onboarding.poll_projects());
    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(!rendered.contains("Loading projects…"));
    assert!(rendered.contains("quinjet"));
}

#[test]
fn project_picker_compacts_long_paths_and_toggles_all_projects() {
    let mut group = project_group();
    let full_path = "/Users/pulkit/scripts/quinjet/features/a-very-long-worktree-name";
    group.worktrees[0].path = PathBuf::from(full_path);
    group.worktrees[0].updated_at = Some("2020-08-22T18:00:00Z".to_owned());
    group.worktrees[0].updated_unix = Some(1_598_119_200);
    let mut onboarding = Onboarding::from_groups(Path::new("/tmp"), vec![group], None);
    let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
    let theme = Theme::new(ThemeName::Quinjet, Appearance::Dark);

    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(rendered.contains(''));
    assert!(!rendered.contains(full_path));
    assert!(rendered.contains("years ago"));

    drop(onboarding.handle_key(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL)));
    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(rendered.contains("[›]"));
    assert!(!rendered.contains("main"));
    assert!(rendered.contains("Ctrl+E all"));

    drop(onboarding.handle_key(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL)));
    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(rendered.contains("[⌄]"));
    assert!(rendered.contains("main"));
    assert!(rendered.contains("Ctrl+E all"));
}

#[test]
fn project_picker_opens_the_selected_worktree() {
    let mut onboarding = Onboarding::from_groups(Path::new("/tmp"), vec![project_group()], None);
    assert_eq!(
        onboarding.handle_key(key(KeyCode::Enter)),
        OnboardingAction::Open(PathBuf::from("/repos/quinjet"))
    );
}

#[test]
fn path_entry_resolves_relative_to_the_launch_directory() {
    let mut onboarding = Onboarding::from_groups(Path::new("/work"), Vec::new(), None);
    drop(onboarding.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL)));
    for character in "project".chars() {
        drop(onboarding.handle_key(key(KeyCode::Char(character))));
    }
    assert_eq!(
        onboarding.handle_key(key(KeyCode::Enter)),
        OnboardingAction::Open(PathBuf::from("/work/project"))
    );
}

#[test]
fn pasted_control_characters_are_removed_from_paths() {
    let mut onboarding = Onboarding::from_groups(Path::new("/work"), Vec::new(), None);
    drop(onboarding.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL)));
    onboarding.handle_paste("repo\nname\u{0000}");
    assert_eq!(onboarding.path_input, "reponame");
}

#[test]
fn local_project_picker_uses_the_inline_machine_strip() {
    let context = SshContext {
        current: "Pulkits-MacBook-Pro.local".to_owned(),
        machines: vec![
            SshMachine {
                target: "Pulkits-MacBook-Pro.local".to_owned(),
                folder: PathBuf::from("/work"),
                accessible: true,
                uses: 0,
                local: true,
            },
            SshMachine {
                target: "busy-host".to_owned(),
                folder: PathBuf::from("/work/busy"),
                accessible: true,
                uses: 8,
                local: false,
            },
            SshMachine {
                target: "offline-host".to_owned(),
                folder: PathBuf::from("/work/offline"),
                accessible: false,
                uses: 2,
                local: false,
            },
        ],
        tabs: crate::ssh::SshTabs::default(),
        probing: false,
    };
    let mut onboarding = Onboarding::from_groups(Path::new("/work"), Vec::new(), Some(context));
    let backend = TestBackend::new(100, 30);
    let mut terminal = Terminal::new(backend).unwrap();
    let theme = Theme::new(ThemeName::Quinjet, Appearance::Dark);

    assert_eq!(
        terminal
            .draw(|frame| onboarding.draw(frame, &theme))
            .unwrap()
            .area,
        Rect::new(0, 0, 100, 30),
    );
    let rendered = terminal.backend().to_string();
    assert!(rendered.contains("Machine"));
    assert!(rendered.contains("Pulkits-MacBook-Pro.local"));
    assert!(rendered.contains("busy-host"));
    assert!(rendered.contains("Tab machine"));
    let button = onboarding
        .machine_hits
        .iter()
        .find(|(_, index)| *index == 1)
        .map(|(area, _)| *area)
        .expect("SSH machine chip");

    assert_eq!(
        onboarding.handle_mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: button.x,
            row: button.y,
            modifiers: KeyModifiers::NONE,
        }),
        OnboardingAction::SwitchSshMachine(SshSwitch {
            index: 1,
            mode: SshProjectOpenMode::Current,
        })
    );
    assert_eq!(
        onboarding.handle_key(key(KeyCode::Tab)),
        OnboardingAction::SwitchSshMachine(SshSwitch {
            index: 1,
            mode: SshProjectOpenMode::Current,
        })
    );
}

#[test]
fn new_tab_onboarding_keeps_its_pending_identity_across_switch_and_cancel() {
    let machines = vec![
        SshMachine {
            target: "macbook".to_owned(),
            folder: PathBuf::from("/local"),
            accessible: true,
            uses: 0,
            local: true,
        },
        SshMachine {
            target: "tof".to_owned(),
            folder: PathBuf::from("/remote"),
            accessible: true,
            uses: 2,
            local: false,
        },
    ];
    let mut tabs = crate::ssh::SshTabs::default();
    let local = tabs.append("macbook", "local", "/local/repo");
    let pending = tabs.append_pending("macbook", "/local/repo");
    let context = SshContext {
        current: "macbook".to_owned(),
        machines,
        tabs,
        probing: false,
    };
    let mut switching = Onboarding::from_groups_with_mode(
        Path::new("/local"),
        Vec::new(),
        Some(context.clone()),
        ProjectOpenMode::NewTab,
    );

    assert_eq!(
        switching.handle_key(key(KeyCode::Tab)),
        OnboardingAction::SwitchSshMachine(SshSwitch {
            index: 1,
            mode: SshProjectOpenMode::New,
        })
    );
    assert_eq!(
        switching
            .ssh_context()
            .and_then(|saved| saved.tabs.get(pending).map(|tab| tab.machine.clone())),
        Some("tof".to_owned())
    );

    let mut canceling = Onboarding::from_groups_with_mode(
        Path::new("/local"),
        Vec::new(),
        Some(context),
        ProjectOpenMode::NewTab,
    );
    assert_eq!(
        canceling.handle_key(key(KeyCode::Esc)),
        OnboardingAction::SwitchSshMachine(SshSwitch {
            index: 0,
            mode: SshProjectOpenMode::Activate,
        })
    );
    let saved = canceling.ssh_context().expect("updated context");
    assert!(saved.tabs.get(pending).is_none());
    assert_eq!(saved.tabs.active_id(), Some(local));
}