//! Vectrum Code: a minimal T3 Code-style desktop client for Claude Code. // GPUI's derive macros expand to `gpui::` paths; route them through the Kit. extern crate gpui_kit as gpui; mod browser; mod claude; mod clock; mod device; mod git; mod http; mod inline_ui; mod models; mod orchestrator; mod projects; mod remote; mod session; mod settings; mod store; mod terminal; mod textgen; mod ui; mod update; mod usage; mod worktree; use std::{ path::PathBuf, sync::{ Mutex, atomic::{AtomicBool, Ordering}, }, time::{Duration, Instant}, }; use gpui_kit::component::{WindowExt as _, button::ButtonVariant, notification::Notification}; use gpui_kit::*; use crate::ui::{icons::AppAssets, metal, workspace::Workspace}; gpui_kit::actions!( vectrum_code, [ NewThread, OpenProject, OpenInEditor, Interrupt, ToggleSidebar, ToggleProjectPanel, ToggleSubagents, ToggleBrowser, FocusSearch, OpenSettings, CloseSettings, Quit, // T3 Code's commands, by its default keymap. ToggleCommandPalette, NewThreadLocal, NewThreadWithoutProject, PreviousThread, NextThread, SettleThread, PinThread, UndoThreadAction, CopyThreadReference, OpenUsage, ToggleRightPanel, SelectTheme, CycleAppearance, // The composer's, in the open thread. ToggleModelPicker, ComposerEffort, ComposerMode, ComposerBranch, ComposerWorkspace, ComposerHost, ComposerPreviousWorktree, ComposerStash, FindInThread, // T3 Code's terminal drawer commands. ToggleTerminal, SplitTerminal, SplitTerminalVertical, NewTerminal, CloseTerminal, TerminalCopy, TerminalPaste, // Keys for lists with a text field: the palette, sidebar search, the // project filter, and inline renames. SelectPrevious, SelectNext, Dismiss, PaletteBack ] ); /// With the Hold quit shortcut, a second Ctrl+Q (or the key repeat of a /// held one) within this long quits. const QUIT_WINDOW: Duration = Duration::from_millis(1200); fn main() { let project = std::env::args_os() .nth(1) .map(PathBuf::from) .map(|project| project.canonicalize().unwrap_or(project)); gpui_kit::application() .with_assets(AppAssets) .run(move |cx| { gpui_kit::init(cx); metal::load_fonts(cx); models::Catalog::init(cx); ui::browser::BrowserWatch::init(cx); ui::device::DeviceWatch::init(cx); update::Updater::init(cx); cx.bind_keys([ // T3 Code's default keymap, for the commands the app has. KeyBinding::new("ctrl-b", ToggleSidebar, None), KeyBinding::new("ctrl-k", ToggleCommandPalette, None), KeyBinding::new("ctrl-n", NewThread, None), KeyBinding::new("ctrl-shift-o", NewThread, None), KeyBinding::new("ctrl-shift-n", NewThreadLocal, None), KeyBinding::new("ctrl-alt-n", NewThreadWithoutProject, None), KeyBinding::new("ctrl-shift-[", PreviousThread, None), KeyBinding::new("ctrl-shift-]", NextThread, None), KeyBinding::new("ctrl-{", PreviousThread, None), KeyBinding::new("ctrl-}", NextThread, None), KeyBinding::new("ctrl-shift-s", SettleThread, None), KeyBinding::new("ctrl-shift-p", PinThread, None), KeyBinding::new("ctrl-z", UndoThreadAction, Some("Workspace")), KeyBinding::new("ctrl-z", UndoThreadAction, Some("Workspace > Input")), KeyBinding::new("ctrl-shift-c", CopyThreadReference, None), KeyBinding::new("ctrl-u", OpenUsage, None), KeyBinding::new("ctrl-alt-b", ToggleRightPanel, None), KeyBinding::new("ctrl-alt-a", SelectTheme, None), KeyBinding::new("ctrl-alt-shift-a", CycleAppearance, None), // The app's own. KeyBinding::new("ctrl-o", OpenInEditor, None), KeyBinding::new("ctrl-shift-b", ToggleProjectPanel, None), KeyBinding::new("ctrl-alt-s", ToggleSubagents, None), KeyBinding::new("ctrl-shift-j", ToggleBrowser, None), KeyBinding::new("ctrl-,", OpenSettings, None), KeyBinding::new("ctrl-q", Quit, None), KeyBinding::new("escape", Interrupt, Some("ChatView")), KeyBinding::new("ctrl-shift-m", ToggleModelPicker, Some("ChatView")), KeyBinding::new("ctrl-shift-e", ComposerEffort, Some("ChatView")), KeyBinding::new("ctrl-shift-a", ComposerMode, Some("ChatView")), KeyBinding::new("ctrl-shift-g", ComposerBranch, Some("ChatView")), KeyBinding::new("ctrl-shift-x", ComposerWorkspace, Some("ChatView")), KeyBinding::new("ctrl-shift-h", ComposerHost, Some("ChatView")), KeyBinding::new("ctrl-shift-l", ComposerPreviousWorktree, Some("ChatView")), KeyBinding::new("ctrl-s", ComposerStash, Some("ChatView")), KeyBinding::new("ctrl-f", FindInThread, Some("ChatView")), // The composer's text field binds Ctrl+F to its own search. KeyBinding::new("ctrl-f", FindInThread, Some("ChatView > Input")), KeyBinding::new("escape", CloseSettings, Some("SettingsView")), // Lists with a text field. KeyBinding::new("up", SelectPrevious, Some("CommandPalette > Input")), KeyBinding::new("down", SelectNext, Some("CommandPalette > Input")), KeyBinding::new("escape", Dismiss, Some("CommandPalette > Input")), KeyBinding::new("backspace", PaletteBack, Some("CommandPalette > Input")), KeyBinding::new("up", SelectPrevious, Some("SidebarSearch > Input")), KeyBinding::new("down", SelectNext, Some("SidebarSearch > Input")), KeyBinding::new("escape", Dismiss, Some("SidebarSearch > Input")), KeyBinding::new("up", SelectPrevious, Some("ScopePicker > Input")), KeyBinding::new("down", SelectNext, Some("ScopePicker > Input")), KeyBinding::new("escape", Dismiss, Some("ScopePicker > Input")), KeyBinding::new("escape", Dismiss, Some("InlineRename > Input")), // The terminal drawer. A focused terminal's keys come last so // they win over the app's (Ctrl+N is a new terminal there). KeyBinding::new("ctrl-j", ToggleTerminal, None), KeyBinding::new("ctrl-d", SplitTerminal, Some("Terminal")), KeyBinding::new("ctrl-shift-d", SplitTerminalVertical, Some("Terminal")), KeyBinding::new("ctrl-n", NewTerminal, Some("Terminal")), KeyBinding::new("ctrl-w", CloseTerminal, Some("Terminal")), KeyBinding::new("ctrl-shift-c", TerminalCopy, Some("Terminal")), KeyBinding::new("ctrl-shift-v", TerminalPaste, Some("Terminal")), KeyBinding::new("shift-insert", TerminalPaste, Some("Terminal")), ]); // Keys T3 Code keeps from the app while a terminal has focus // (its "!terminalFocus"), so they reach the shell. cx.bind_keys( [ "ctrl-k", "ctrl-u", "ctrl-z", "ctrl-shift-o", "ctrl-shift-n", "ctrl-alt-n", "ctrl-shift-s", "ctrl-shift-p", "ctrl-alt-a", "ctrl-alt-shift-a", "tab", "shift-tab", ] .map(|key| KeyBinding::new(key, NoAction, Some("Terminal"))), ); cx.on_action(quit); cx.on_window_closed(|cx, _| cx.quit()).detach(); let bounds = Bounds::centered(None, size(px(1280.), px(840.)), cx); let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), titlebar: Some(TitlebarOptions { title: Some("Vectrum Code".into()), ..Default::default() }), app_id: Some("vectrum-code".into()), window_min_size: Some(size(px(820.), px(520.))), ..Default::default() }; gpui_kit::open_window(options, cx, |window, cx| { metal::sync_appearance(window, cx); // Alt+F4 and the window manager's close button. window.on_window_should_close(cx, |window, cx| !ask_to_close(window, cx)); cx.new(|cx| Workspace::new(project, window, cx)) }) .expect("failed to open the main window"); cx.activate(true); }); } /// Quits (through the close confirmation), or with the Hold shortcut, asks /// for the keys to be held or pressed again. fn quit(_: &Quit, cx: &mut App) { static FIRST_PRESS: Mutex> = Mutex::new(None); if settings::get(|settings| settings.quit_shortcut) == settings::QuitShortcut::Press { quit_or_ask(cx); return; } let mut first = FIRST_PRESS.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); if first.is_some_and(|at| at.elapsed() < QUIT_WINDOW) { quit_or_ask(cx); return; } *first = Some(Instant::now()); if let Some(window) = cx.active_window() { let _ = window.update(cx, |_, window, cx| { let message = match settings::get(|settings| settings.quit_shortcut) { settings::QuitShortcut::Double => "Press Ctrl+Q again to quit.", _ => "Hold Ctrl+Q, or press it again, to quit.", }; window.push_notification(Notification::info(message), cx); }); } } /// Quits, or asks first when closing needs a confirmation. Deferred, since a /// key's action runs while its window is mid-update. fn quit_or_ask(cx: &mut App) { cx.defer(|cx| { let Some(window) = cx.active_window().or_else(|| cx.windows().first().copied()) else { cx.quit(); return; }; if let Ok(false) = window.update(cx, |_, window, cx| ask_to_close(window, cx)) { cx.quit(); } }); } /// With Close confirmation on, opens the "Quit Vectrum Code?" dialog (Enter /// or a click on Quit quits, Escape or Cancel stays) and returns true; the /// caller then must not close. Returns false when closing needs no asking. fn ask_to_close(window: &mut Window, cx: &mut App) -> bool { /// The dialog is up: a held Ctrl+Q's key repeat or a second Alt+F4 /// leaves it be rather than stacking another. static ASKING: AtomicBool = AtomicBool::new(false); if !settings::get(|settings| settings.confirm_close) { return false; } if ASKING.load(Ordering::Relaxed) && window.has_active_dialog(cx) { return true; } ASKING.store(true, Ordering::Relaxed); window.open_alert_dialog(cx, |alert, _, _| { alert .title("Quit Vectrum Code?") .description("Running agents and terminals stop. Press Enter to quit, or Escape to stay.") .ok_text("Quit") .ok_variant(ButtonVariant::Danger) .cancel_text("Cancel") .show_cancel(true) .on_ok(|_, _, cx| { cx.quit(); true }) .on_close(|_, _, _| ASKING.store(false, Ordering::Relaxed)) }); true }