//! 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 git;
mod http;
mod models;
mod orchestrator;
mod remote;
mod session;
mod settings;
mod store;
mod terminal;
mod textgen;
mod ui;
mod usage;
mod worktree;
use std::{
path::PathBuf,
sync::Mutex,
time::{Duration, Instant},
};
use gpui_kit::component::{WindowExt as _, 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);
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);
cx.new(|cx| Workspace::new(project, window, cx))
})
.expect("failed to open the main window");
cx.activate(true);
});
}
/// Quits, 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<Option<Instant>> = Mutex::new(None);
if settings::get(|settings| settings.quit_shortcut) == settings::QuitShortcut::Press {
cx.quit();
return;
}
let mut first = FIRST_PRESS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if first.is_some_and(|at| at.elapsed() < QUIT_WINDOW) {
cx.quit();
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);
});
}
}