Files
lumbridge-code/spikes/gpui-shell/src/main.rs
T
karti 4ed7613b22
CI / rust (push) Successful in 4m19s
feat: index runtime sessions by pane
2026-08-31 18:29:28 -07:00

1995 lines
71 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::{BTreeMap, VecDeque};
use std::time::{Duration, Instant};
use gpui::{
App, Application, Bounds, Context, FocusHandle, FontWeight, KeyBinding, KeyDownEvent, Pixels,
Size, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, relative, rgb, size,
};
use lumbridge_runtime::{
CommandConfig, PtyOptions, RuntimeActorError, RuntimeActorOptions, RuntimeCommand,
RuntimeEvent, RuntimeRegistry, RuntimeRegistryError, TerminalSize,
};
use lumbridge_spike_model::{
ActionOutcome, FOOTER_RIGHT, FocusDirection, INITIAL_ATTACHED_PANEL_COUNT, OutputSource,
PaneId, PaneState, ShellAction, ShellModel, SurfaceKind, WORKSPACES,
};
use lumbridge_terminal::{
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
TerminalScroll, TerminalSnapshot,
};
const BG: u32 = 0x090c12;
const PANEL: u32 = 0x101620;
const PANEL_ALT: u32 = 0x151d29;
const PANEL_ACTIVE: u32 = 0x182334;
const BORDER: u32 = 0x263246;
const BORDER_QUIET: u32 = 0x1c2636;
const TEXT: u32 = 0xdbe5f4;
const MUTED: u32 = 0x8290a8;
const ACCENT: u32 = 0x68b5f8;
const ATTENTION: u32 = 0xf1b96a;
const SUCCESS: u32 = 0x70d6a8;
const TIMING_SAMPLE_LIMIT: usize = 256;
const RUNTIME_POLL_INTERVAL: Duration = Duration::from_millis(16);
const RUNTIME_DRAIN_LIMIT: usize = 64;
const LIVE_PANES: [PaneId; 3] = [PaneId::CodexRuntime, PaneId::ClaudeUi, PaneId::PiDocs];
const SIDEBAR_WIDTH: f32 = 248.0;
const APP_HEADER_HEIGHT: f32 = 48.0;
const TAB_BAR_HEIGHT: f32 = 38.0;
const APP_FOOTER_HEIGHT: f32 = 32.0;
const TERMINAL_CONTENT_VERTICAL_INSET: f32 = 24.0;
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
const TERMINAL_CELL_WIDTH: f32 = 8.4;
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
const WORK_PANEL_GAP: f32 = 4.0;
const TERMINAL_ROW_STEP: u16 = 2;
const TERMINAL_COLUMN_STEP: u16 = 10;
actions!(
lumbridge,
[
FocusLeft,
FocusRight,
FocusUp,
FocusDown,
OpenPalette,
ClosePalette,
SelectPane1,
SelectPane2,
SelectPane3,
SelectPane4,
SelectPane5,
SelectPane6,
AddPanel,
DetachSelectedPanel,
TerminalTaller,
TerminalShorter,
TerminalWider,
TerminalNarrower,
]
);
struct LumbridgeShell {
model: ShellModel,
timing: RenderTiming,
runtimes: RuntimeRegistry<PaneId>,
live_terminals: BTreeMap<PaneId, LiveTerminalState>,
root_focus: FocusHandle,
}
struct LiveTerminalState {
status: LiveRuntimeStatus,
terminal: TerminalEngine,
snapshot: TerminalSnapshot,
last_runtime_sequence: u64,
}
impl LiveTerminalState {
fn new(dimensions: TerminalDimensions) -> Self {
let terminal = TerminalEngine::new(TerminalEngineOptions {
dimensions,
..TerminalEngineOptions::default()
});
let snapshot = terminal.snapshot();
Self {
status: LiveRuntimeStatus::Starting,
terminal,
snapshot,
last_runtime_sequence: 0,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum LiveRuntimeStatus {
Starting,
Running {
session_id: u64,
process_id: Option<u32>,
},
Exited(String),
Fault(String),
}
impl LiveRuntimeStatus {
fn badge(&self) -> &'static str {
match self {
Self::Starting => "PTY STARTING",
Self::Running { .. } => "LIVE PTY",
Self::Exited(_) => "PTY EXITED",
Self::Fault(_) => "PTY FAULT",
}
}
fn detail(&self) -> String {
match self {
Self::Starting => "local · runtime actor starting".to_owned(),
Self::Running {
session_id,
process_id,
} => match process_id {
Some(process_id) => {
format!("local · runtime session {session_id} · pid {process_id}")
}
None => format!("local · runtime session {session_id}"),
},
Self::Exited(status) => format!("local · {status}"),
Self::Fault(message) => format!("local · {message}"),
}
}
const fn is_terminal(&self) -> bool {
matches!(self, Self::Exited(_) | Self::Fault(_))
}
}
#[derive(Default)]
struct RenderTiming {
pending_dispatch: Option<Instant>,
dispatch_to_element_micros: VecDeque<u128>,
}
#[derive(Clone, Eq, PartialEq)]
struct TerminalPaintRun {
text: String,
columns: u16,
foreground: u32,
background: u32,
style: TerminalCellStyle,
cursor: Option<TerminalCursorShape>,
hyperlink: bool,
}
impl TerminalPaintRun {
fn can_merge(&self, other: &Self) -> bool {
self.foreground == other.foreground
&& self.background == other.background
&& self.style == other.style
&& self.cursor == other.cursor
&& self.hyperlink == other.hyperlink
}
}
fn terminal_paint_rows(snapshot: &TerminalSnapshot) -> Vec<Vec<TerminalPaintRun>> {
(0..snapshot.dimensions.rows())
.map(|row| {
let mut runs: Vec<TerminalPaintRun> = Vec::new();
for column in 0..snapshot.dimensions.columns() {
let Some(cell) = snapshot.cell(row, column) else {
continue;
};
if cell.wide_spacer {
continue;
}
let mut foreground = terminal_color(cell.foreground);
let mut background = terminal_color(cell.background);
if cell.style.contains(TerminalCellStyle::INVERSE) {
std::mem::swap(&mut foreground, &mut background);
}
if cell.style.contains(TerminalCellStyle::DIM) {
foreground = dim_color(foreground);
}
if cell.style.contains(TerminalCellStyle::HIDDEN) {
foreground = background;
}
let cursor = (snapshot.cursor.row == row
&& snapshot.cursor.column == column
&& snapshot.cursor.shape != TerminalCursorShape::Hidden)
.then_some(snapshot.cursor.shape);
let text = if cell.text.is_empty() {
" ".to_owned()
} else {
cell.text.clone()
};
let next = TerminalPaintRun {
text,
columns: if cell.wide { 2 } else { 1 },
foreground,
background,
style: cell.style,
cursor,
hyperlink: cell.hyperlink.is_some(),
};
if let Some(current) = runs.last_mut()
&& current.can_merge(&next)
{
current.text.push_str(&next.text);
current.columns = current.columns.saturating_add(next.columns);
continue;
}
runs.push(next);
}
runs
})
.collect()
}
fn terminal_color(color: TerminalColor) -> u32 {
match color {
TerminalColor::Rgb { red, green, blue } => {
(u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue)
}
TerminalColor::Indexed(index) => indexed_terminal_color(index),
TerminalColor::Named(named) => match named {
TerminalNamedColor::Black => 0x1d2430,
TerminalNamedColor::Red => 0xff6b6b,
TerminalNamedColor::Green => 0x70d6a8,
TerminalNamedColor::Yellow => 0xf1c76a,
TerminalNamedColor::Blue => 0x68b5f8,
TerminalNamedColor::Magenta => 0xc79bf2,
TerminalNamedColor::Cyan => 0x63d5da,
TerminalNamedColor::White => 0xdbe5f4,
TerminalNamedColor::BrightBlack => 0x6d7a91,
TerminalNamedColor::BrightRed => 0xff8b8b,
TerminalNamedColor::BrightGreen => 0x93e6be,
TerminalNamedColor::BrightYellow => 0xf8d98c,
TerminalNamedColor::BrightBlue => 0x8bc8ff,
TerminalNamedColor::BrightMagenta => 0xd9b4fb,
TerminalNamedColor::BrightCyan => 0x86e7eb,
TerminalNamedColor::BrightWhite | TerminalNamedColor::BrightForeground => 0xf4f8ff,
TerminalNamedColor::Foreground => TEXT,
TerminalNamedColor::Background => BG,
TerminalNamedColor::Cursor => 0xf4f8ff,
TerminalNamedColor::DimBlack => 0x121820,
TerminalNamedColor::DimRed => 0x9f4a4a,
TerminalNamedColor::DimGreen => 0x4e9878,
TerminalNamedColor::DimYellow => 0xa88a49,
TerminalNamedColor::DimBlue => 0x477fac,
TerminalNamedColor::DimMagenta => 0x896ca4,
TerminalNamedColor::DimCyan => 0x459397,
TerminalNamedColor::DimWhite | TerminalNamedColor::DimForeground => MUTED,
},
}
}
fn indexed_terminal_color(index: u8) -> u32 {
const ANSI: [u32; 16] = [
0x1d2430, 0xff6b6b, 0x70d6a8, 0xf1c76a, 0x68b5f8, 0xc79bf2, 0x63d5da, 0xdbe5f4, 0x6d7a91,
0xff8b8b, 0x93e6be, 0xf8d98c, 0x8bc8ff, 0xd9b4fb, 0x86e7eb, 0xf4f8ff,
];
match index {
0..=15 => ANSI[usize::from(index)],
16..=231 => {
const LEVELS: [u32; 6] = [0, 95, 135, 175, 215, 255];
let offset = u32::from(index - 16);
let red = LEVELS[(offset / 36) as usize];
let green = LEVELS[((offset % 36) / 6) as usize];
let blue = LEVELS[(offset % 6) as usize];
(red << 16) | (green << 8) | blue
}
232..=255 => {
let level = 8 + 10 * u32::from(index - 232);
(level << 16) | (level << 8) | level
}
}
}
fn dim_color(color: u32) -> u32 {
let dim = |channel: u32| channel * 13 / 20;
(dim((color >> 16) & 0xff) << 16) | (dim((color >> 8) & 0xff) << 8) | dim(color & 0xff)
}
fn terminal_dimensions_for_window(
window_size: Size<Pixels>,
attached_panel_count: usize,
) -> TerminalDimensions {
let width = f32::from(window_size.width);
let height = f32::from(window_size.height);
let panel_count = visible_panel_count(window_size)
.min(attached_panel_count)
.max(1);
let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
let terminal_height = (workspace_height * 0.60 - TERMINAL_CONTENT_VERTICAL_INSET).max(0.0);
let workspace_width = (width - SIDEBAR_WIDTH).max(0.0);
let panel_gaps = WORK_PANEL_GAP * panel_count.saturating_sub(1) as f32;
let panel_width = ((workspace_width - panel_gaps).max(0.0) / panel_count as f32).max(0.0);
let terminal_width = (panel_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
.floor()
.clamp(2.0, f32::from(u16::MAX));
let columns = (terminal_width / TERMINAL_CELL_WIDTH)
.floor()
.clamp(20.0, f32::from(u16::MAX));
TerminalDimensions::with_cell_size(
rows as u16,
columns as u16,
TERMINAL_CELL_WIDTH.round() as u16,
TERMINAL_CELL_HEIGHT.round() as u16,
)
.expect("geometry clamps terminal dimensions above zero")
}
fn visible_panel_count(window_size: Size<Pixels>) -> usize {
let workspace_width = (f32::from(window_size.width) - SIDEBAR_WIDTH).max(0.0);
if workspace_width >= 2_800.0 {
5
} else if workspace_width >= 1_100.0 {
3
} else {
1
}
}
fn visible_panel_range(
total: usize,
selected: usize,
panel_count: usize,
) -> std::ops::Range<usize> {
let count = panel_count.clamp(1, total.max(1)).min(total);
let start = selected
.saturating_sub(count / 2)
.min(total.saturating_sub(count));
start..start + count
}
impl RenderTiming {
fn mark_dispatch(&mut self) {
self.pending_dispatch = Some(Instant::now());
}
fn observe_element_build(&mut self) {
let Some(started) = self.pending_dispatch.take() else {
return;
};
if self.dispatch_to_element_micros.len() == TIMING_SAMPLE_LIMIT {
self.dispatch_to_element_micros.pop_front();
}
self.dispatch_to_element_micros
.push_back(started.elapsed().as_micros());
}
fn summary(&self) -> String {
if self.dispatch_to_element_micros.is_empty() {
return "dispatch→element collecting…".to_owned();
}
let mut samples = self
.dispatch_to_element_micros
.iter()
.copied()
.collect::<Vec<_>>();
samples.sort_unstable();
let percentile = |percent: usize| {
let index = ((samples.len() - 1) * percent) / 100;
samples[index]
};
format!(
"dispatch→element p50 {}µs · p95 {}µs · n{}",
percentile(50),
percentile(95),
samples.len()
)
}
}
impl LumbridgeShell {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let root_focus = cx.focus_handle();
window.focus(&root_focus);
cx.spawn(async move |this, cx| {
loop {
cx.background_executor()
.timer(Duration::from_millis(650))
.await;
if this
.update(cx, |shell, cx| {
shell.dispatch(ShellAction::SyntheticStreamTick);
cx.notify();
})
.is_err()
{
break;
}
}
})
.detach();
cx.spawn(async move |this, cx| {
loop {
cx.background_executor().timer(RUNTIME_POLL_INTERVAL).await;
if this
.update(cx, |shell, cx| {
if shell.drain_runtime_events() {
cx.notify();
}
})
.is_err()
{
break;
}
}
})
.detach();
let terminal_dimensions =
terminal_dimensions_for_window(window.bounds().size, INITIAL_ATTACHED_PANEL_COUNT);
let mut runtimes = RuntimeRegistry::new();
let mut live_terminals = BTreeMap::new();
for pane in LIVE_PANES {
let mut terminal = LiveTerminalState::new(terminal_dimensions);
if let Err(error) = spawn_live_runtime(&mut runtimes, pane, terminal_dimensions) {
terminal.status = LiveRuntimeStatus::Fault(error.to_string());
}
live_terminals.insert(pane, terminal);
}
cx.observe_window_bounds(window, |shell, window, cx| {
let dimensions = terminal_dimensions_for_window(
window.bounds().size,
shell.model.attached_panel_count(),
);
shell.resize_attached_terminals(dimensions.rows(), dimensions.columns(), cx);
})
.detach();
Self {
model: ShellModel::with_external_outputs(LIVE_PANES)
.expect("all live comparison panes are terminals"),
timing: RenderTiming::default(),
runtimes,
live_terminals,
root_focus,
}
}
fn dispatch(&mut self, action: ShellAction) -> ActionOutcome {
self.timing.mark_dispatch();
self.model.dispatch(action)
}
fn drain_runtime_events(&mut self) -> bool {
let mut changed = false;
for pane in LIVE_PANES {
if self
.live_terminals
.get(&pane)
.is_none_or(|terminal| terminal.status.is_terminal())
{
continue;
}
for _ in 0..RUNTIME_DRAIN_LIMIT {
let event = match self.runtimes.try_recv(&pane) {
Ok(Some(event)) => event,
Ok(None) => break,
Err(RuntimeRegistryError::Actor(RuntimeActorError::Disconnected)) => {
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status =
LiveRuntimeStatus::Fault("runtime actor disconnected".to_owned());
changed = true;
break;
}
Err(error) => {
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status = LiveRuntimeStatus::Fault(error.to_string());
changed = true;
break;
}
};
match event {
RuntimeEvent::Started {
session_id,
process_id,
} => {
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status = LiveRuntimeStatus::Running {
session_id: session_id.get(),
process_id,
};
changed = true;
}
RuntimeEvent::Output {
sequence, bytes, ..
} => {
let responses = {
let terminal = self
.live_terminals
.get_mut(&pane)
.expect("live terminal state exists");
if sequence <= terminal.last_runtime_sequence {
terminal.status = LiveRuntimeStatus::Fault(format!(
"non-monotonic PTY output sequence {sequence}"
));
changed = true;
break;
}
terminal.last_runtime_sequence = sequence;
terminal.terminal.process(&bytes).outbound
};
for response in responses {
if let Err(error) =
self.send_runtime_command(pane, RuntimeCommand::Input(response))
{
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status = LiveRuntimeStatus::Fault(error.to_string());
return true;
}
}
self.publish_terminal_snapshot(pane);
changed = true;
}
RuntimeEvent::InputClosed { .. } => {}
RuntimeEvent::Exited { status, .. } => {
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status = LiveRuntimeStatus::Exited(format!(
"PTY exited with code {}",
status.code
));
changed = true;
break;
}
RuntimeEvent::Fault {
operation, message, ..
} => {
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status = LiveRuntimeStatus::Fault(format!("{operation:?}: {message}"));
changed = true;
break;
}
}
}
}
changed
}
fn send_runtime_command(
&self,
pane: PaneId,
command: RuntimeCommand,
) -> Result<(), RuntimeRegistryError> {
self.runtimes.try_send(&pane, command)
}
fn publish_terminal_snapshot(&mut self, pane: PaneId) {
let lines = {
let terminal = self
.live_terminals
.get_mut(&pane)
.expect("live terminal state exists");
let snapshot = terminal.terminal.snapshot();
let lines = snapshot.plain_rows();
terminal.snapshot = snapshot;
lines
};
self.dispatch(ShellAction::ReplaceExternalOutput { pane, lines });
}
fn resize_terminal(&mut self, pane: PaneId, rows: u16, columns: u16) -> bool {
let dimensions = TerminalDimensions::new(rows, columns)
.expect("resize actions always retain non-zero dimensions");
let terminal = self
.live_terminals
.get_mut(&pane)
.expect("live terminal state exists");
if !terminal.terminal.resize(dimensions) {
return false;
}
let pty_size = TerminalSize::new(rows, columns)
.expect("terminal engine dimensions are valid PTY dimensions");
if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Resize(pty_size)) {
self.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.status = LiveRuntimeStatus::Fault(error.to_string());
}
self.publish_terminal_snapshot(pane);
true
}
fn resize_attached_terminals(&mut self, rows: u16, columns: u16, cx: &mut Context<Self>) {
let mut changed = false;
for pane in LIVE_PANES {
if self.model.is_panel_attached(pane) {
changed |= self.resize_terminal(pane, rows, columns);
}
}
if changed {
cx.notify();
}
}
fn scroll_terminal(&mut self, pane: PaneId, scroll: TerminalScroll, cx: &mut Context<Self>) {
if !self
.live_terminals
.get_mut(&pane)
.expect("live terminal state exists")
.terminal
.scroll_display(scroll)
{
return;
}
self.publish_terminal_snapshot(pane);
cx.notify();
}
fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
self.dispatch(ShellAction::SelectPane(pane));
window.focus(&self.root_focus);
cx.notify();
}
fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context<Self>) {
let dimensions =
terminal_dimensions_for_window(window.bounds().size, self.model.attached_panel_count());
self.resize_attached_terminals(dimensions.rows(), dimensions.columns(), cx);
}
fn attach_panel(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
let outcome = self.dispatch(ShellAction::AttachPanel(pane));
if outcome.changed {
self.resize_terminal_for_workspace(window, cx);
}
window.focus(&self.root_focus);
cx.notify();
}
fn add_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(pane) = self.model.next_detached_panel() else {
return;
};
self.attach_panel(pane, window, cx);
}
fn detach_panel(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
let outcome = self.dispatch(ShellAction::DetachPanel(pane));
if outcome.changed {
self.resize_terminal_for_workspace(window, cx);
}
window.focus(&self.root_focus);
cx.notify();
}
fn add_panel_action(&mut self, _: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
self.add_panel(window, cx);
}
fn detach_selected_panel(
&mut self,
_: &DetachSelectedPanel,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.detach_panel(self.model.selected_pane(), window, cx);
}
fn move_focus(
&mut self,
direction: FocusDirection,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.dispatch(ShellAction::MoveFocus(direction));
window.focus(&self.root_focus);
cx.notify();
}
fn focus_left(&mut self, _: &FocusLeft, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Left, window, cx);
}
fn focus_right(&mut self, _: &FocusRight, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Right, window, cx);
}
fn focus_up(&mut self, _: &FocusUp, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Up, window, cx);
}
fn focus_down(&mut self, _: &FocusDown, window: &mut Window, cx: &mut Context<Self>) {
self.move_focus(FocusDirection::Down, window, cx);
}
fn open_palette(&mut self, _: &OpenPalette, _: &mut Window, cx: &mut Context<Self>) {
self.dispatch(ShellAction::OpenCommandPalette);
cx.notify();
}
fn close_palette(&mut self, _: &ClosePalette, _: &mut Window, cx: &mut Context<Self>) {
self.dispatch(ShellAction::CloseCommandPalette);
cx.notify();
}
fn selected_terminal_dimensions(&self) -> Option<(PaneId, TerminalDimensions)> {
let pane = self.model.selected_pane();
self.live_terminals
.get(&pane)
.map(|terminal| (pane, terminal.terminal.dimensions()))
}
fn terminal_taller(&mut self, _: &TerminalTaller, _: &mut Window, cx: &mut Context<Self>) {
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
return;
};
if self.resize_terminal(
pane,
dimensions.rows().saturating_add(TERMINAL_ROW_STEP),
dimensions.columns(),
) {
cx.notify();
}
}
fn terminal_shorter(&mut self, _: &TerminalShorter, _: &mut Window, cx: &mut Context<Self>) {
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
return;
};
if self.resize_terminal(
pane,
dimensions.rows().saturating_sub(TERMINAL_ROW_STEP).max(2),
dimensions.columns(),
) {
cx.notify();
}
}
fn terminal_wider(&mut self, _: &TerminalWider, _: &mut Window, cx: &mut Context<Self>) {
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
return;
};
if self.resize_terminal(
pane,
dimensions.rows(),
dimensions.columns().saturating_add(TERMINAL_COLUMN_STEP),
) {
cx.notify();
}
}
fn terminal_narrower(&mut self, _: &TerminalNarrower, _: &mut Window, cx: &mut Context<Self>) {
let Some((pane, dimensions)) = self.selected_terminal_dimensions() else {
return;
};
if self.resize_terminal(
pane,
dimensions.rows(),
dimensions
.columns()
.saturating_sub(TERMINAL_COLUMN_STEP)
.max(20),
) {
cx.notify();
}
}
fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
if !self.model.command_palette().is_open() {
let pane = self.model.selected_pane();
if self
.live_terminals
.get(&pane)
.is_none_or(|terminal| terminal.status.is_terminal())
{
return;
}
let modifiers = key_modifiers(event);
if let Some(scroll) =
terminal_scroll_from_parts(event.keystroke.key.as_str(), modifiers)
{
self.scroll_terminal(pane, scroll, cx);
return;
}
let Some(event) = terminal_key_from_parts(
event.keystroke.key.as_str(),
event.keystroke.key_char.as_deref(),
modifiers,
) else {
return;
};
let bytes = self
.live_terminals
.get(&pane)
.expect("selected live terminal exists")
.terminal
.encode_key(&event);
if bytes.is_empty() {
return;
}
if let Err(error) = self.send_runtime_command(pane, RuntimeCommand::Input(bytes)) {
self.live_terminals
.get_mut(&pane)
.expect("selected live terminal exists")
.status = LiveRuntimeStatus::Fault(error.to_string());
}
let moved_to_bottom = {
let terminal = self
.live_terminals
.get_mut(&pane)
.expect("selected live terminal exists");
terminal.terminal.display_offset() > 0
&& terminal.terminal.scroll_display(TerminalScroll::Bottom)
};
if moved_to_bottom {
self.publish_terminal_snapshot(pane);
}
cx.notify();
return;
}
let mut query = self.model.command_palette().query().to_owned();
match event.keystroke.key.as_str() {
"backspace" => {
query.pop();
}
"enter" => {
self.dispatch(ShellAction::CloseCommandPalette);
cx.notify();
return;
}
"escape" => {
self.dispatch(ShellAction::CloseCommandPalette);
cx.notify();
return;
}
_ => {
if let Some(character) = event.keystroke.key_char.as_deref()
&& !event.keystroke.modifiers.control
&& !event.keystroke.modifiers.platform
{
query.push_str(character);
} else {
return;
}
}
}
self.dispatch(ShellAction::SetCommandPaletteQuery(query));
cx.notify();
}
fn terminal_run(run: TerminalPaintRun) -> gpui::AnyElement {
let cursor = run.cursor;
div()
.flex_none()
.h(px(TERMINAL_CELL_HEIGHT))
.w(px(f32::from(run.columns) * TERMINAL_CELL_WIDTH))
.overflow_hidden()
.whitespace_nowrap()
.text_color(rgb(run.foreground))
.bg(rgb(run.background))
.when(run.style.contains(TerminalCellStyle::BOLD), |view| {
view.font_weight(FontWeight::BOLD)
})
.when(run.style.contains(TerminalCellStyle::ITALIC), |view| {
view.italic()
})
.when(
run.style.contains(TerminalCellStyle::UNDERLINE)
|| run.style.contains(TerminalCellStyle::DOUBLE_UNDERLINE)
|| run.style.contains(TerminalCellStyle::UNDERCURL)
|| run.style.contains(TerminalCellStyle::DOTTED_UNDERLINE)
|| run.style.contains(TerminalCellStyle::DASHED_UNDERLINE)
|| run.hyperlink,
|view| view.underline(),
)
.when(run.style.contains(TerminalCellStyle::STRIKEOUT), |view| {
view.line_through()
})
.when(cursor == Some(TerminalCursorShape::Block), |view| {
view.bg(rgb(TEXT)).text_color(rgb(BG))
})
.when(cursor == Some(TerminalCursorShape::HollowBlock), |view| {
view.border_1().border_color(rgb(TEXT))
})
.when(cursor == Some(TerminalCursorShape::Underline), |view| {
view.border_b_2().border_color(rgb(TEXT))
})
.when(cursor == Some(TerminalCursorShape::Beam), |view| {
view.border_l_2().border_color(rgb(TEXT))
})
.child(run.text)
.into_any_element()
}
fn terminal_view(&self, pane: PaneId) -> gpui::AnyElement {
let terminal = self
.live_terminals
.get(&pane)
.expect("external terminal pane has live state");
let rows = terminal_paint_rows(&terminal.snapshot)
.into_iter()
.map(|runs| {
div()
.flex()
.h(px(TERMINAL_CELL_HEIGHT))
.flex_none()
.children(runs.into_iter().map(Self::terminal_run))
})
.collect::<Vec<_>>();
div()
.flex()
.flex_col()
.size_full()
.overflow_hidden()
.bg(rgb(BG))
.font_family("monospace")
.text_size(px(13.0))
.children(rows)
.into_any_element()
}
fn pane_context(
&self,
pane: &PaneState,
selected: bool,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let pane_id = pane.id();
let can_detach = self.model.attached_panel_count() > 1;
let external = pane.output_source() == OutputSource::External;
let live_terminal = external.then(|| {
self.live_terminals
.get(&pane_id)
.expect("external terminal pane has live state")
});
let status = live_terminal.map_or(pane.fixture().badge, |terminal| terminal.status.badge());
let detail = live_terminal.map_or_else(
|| pane.fixture().target.to_owned(),
|terminal| terminal.status.detail(),
);
let surface_status = live_terminal.map_or_else(
|| status.to_owned(),
|terminal| {
let dimensions = terminal.terminal.dimensions();
if terminal.snapshot.display_offset > 0 {
format!(
"{} · ↑{} · {}×{}",
status,
terminal.snapshot.display_offset,
dimensions.columns(),
dimensions.rows()
)
} else {
format!(
"{} · {}×{}",
status,
dimensions.columns(),
dimensions.rows()
)
}
},
);
let surface = match pane.kind() {
SurfaceKind::Terminal => "TERMINAL",
SurfaceKind::Markdown => "CONTEXT",
SurfaceKind::Browser => "BROWSER",
SurfaceKind::Review => "REVIEW",
};
let tabs = ["TERMINAL", "BROWSER", "TOOLS", "CONTEXT", "GOAL", "REVIEW"]
.into_iter()
.map(|label| {
div()
.h_full()
.flex()
.items_center()
.px_3()
.text_xs()
.text_color(rgb(if label == surface { ACCENT } else { MUTED }))
.when(label == surface, |view| {
view.border_b_2().border_color(rgb(ACCENT))
})
.child(label)
})
.collect::<Vec<_>>();
div()
.flex()
.flex_col()
.size_full()
.min_h_0()
.overflow_hidden()
.px_3()
.py_2()
.bg(rgb(if selected { PANEL_ACTIVE } else { PANEL_ALT }))
.border_b_1()
.border_color(rgb(BORDER))
.child(
div()
.flex()
.items_center()
.justify_between()
.gap_2()
.child(
div()
.min_w_0()
.child(
div()
.truncate()
.text_sm()
.text_color(rgb(TEXT))
.child(pane.fixture().title),
)
.child(
div()
.mt_1()
.truncate()
.text_xs()
.text_color(rgb(MUTED))
.child(detail),
),
)
.child(
div()
.flex()
.flex_col()
.items_end()
.flex_none()
.gap_1()
.child(
div()
.text_xs()
.text_color(rgb(if pane.needs_input() {
ATTENTION
} else if selected {
SUCCESS
} else {
MUTED
}))
.child(surface_status),
)
.child(
div()
.id(("detach-panel", pane_id.index()))
.cursor_pointer()
.px_2()
.py_1()
.rounded(px(4.0))
.border_1()
.border_color(rgb(BORDER))
.text_xs()
.text_color(rgb(if can_detach { MUTED } else { BORDER }))
.child(if can_detach {
" DETACH"
} else {
"LAST PANEL"
})
.on_click(cx.listener(move |shell, _, window, cx| {
cx.stop_propagation();
shell.detach_panel(pane_id, window, cx);
})),
),
),
)
.child(
div()
.flex()
.h(px(28.0))
.flex_none()
.mt_2()
.overflow_hidden()
.border_t_1()
.border_color(rgb(BORDER_QUIET))
.children(tabs),
)
.child(
div()
.flex()
.items_center()
.justify_between()
.mt_2()
.text_xs()
.text_color(rgb(MUTED))
.child("TOOLS · CONTEXT · GOAL")
.child(if pane.needs_input() {
"NEEDS INPUT"
} else if selected {
"KEYBOARD OWNER"
} else {
"RUNNING"
}),
)
.into_any_element()
}
fn pane_work_surface(&self, pane: &PaneState) -> gpui::AnyElement {
let external = pane.output_source() == OutputSource::External;
let content = if external {
self.terminal_view(pane.id())
} else {
let start = pane.lines().len().saturating_sub(18);
div()
.flex()
.flex_col()
.gap_1()
.size_full()
.overflow_hidden()
.font_family("monospace")
.text_sm()
.text_color(rgb(TEXT))
.children(pane.lines()[start..].iter().cloned())
.into_any_element()
};
div()
.flex()
.flex_col()
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
.bg(rgb(BG))
.border_y_1()
.border_color(rgb(BORDER))
.child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.p_3()
.child(content),
)
.into_any_element()
}
fn pane_decision_region(&self, pane: &PaneState) -> gpui::AnyElement {
let choice = |label: &'static str, detail: &'static str, attention: bool| {
div()
.flex()
.items_center()
.justify_between()
.min_w_0()
.px_2()
.py_1()
.rounded(px(4.0))
.border_1()
.border_color(rgb(if attention { ATTENTION } else { BORDER }))
.bg(rgb(PANEL_ALT))
.text_xs()
.text_color(rgb(TEXT))
.child(label)
.child(div().truncate().text_color(rgb(MUTED)).child(detail))
};
let choices = if pane.needs_input() {
[
("Review request", "inspect scope", true),
("Steer…", "edit response", false),
("Dismiss", "leave inert", false),
]
} else {
[
("Continue", "keep moving", false),
("Review plan", "inspect commands", false),
("Ask…", "refine prompt", false),
]
};
div()
.flex()
.flex_col()
.size_full()
.min_h_0()
.overflow_hidden()
.px_2()
.py_2()
.bg(rgb(PANEL))
.border_t_1()
.border_color(rgb(BORDER))
.child(
div()
.flex()
.items_center()
.justify_between()
.text_xs()
.child(div().text_color(rgb(MUTED)).child("DECISION SHELF"))
.child(
div()
.text_color(rgb(if pane.needs_input() { ATTENTION } else { MUTED }))
.child(if pane.needs_input() {
"REVIEW REQUIRED"
} else {
"SUGGESTIONS INERT"
}),
),
)
.child(div().flex().flex_col().gap_1().mt_2().children(
choices.map(|(label, detail, attention)| choice(label, detail, attention)),
))
.into_any_element()
}
fn workspace_panel(
&self,
pane: &PaneState,
selected: bool,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let pane_id = pane.id();
div()
.id(("workspace-panel", pane_id.index()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(pane_id, window, cx);
}))
.flex()
.flex_col()
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
.border_1()
.border_color(rgb(if selected { ACCENT } else { BORDER }))
.child(
div()
.h(relative(0.20))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.pane_context(pane, selected, cx)),
)
.child(
div()
.h(relative(0.60))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.pane_work_surface(pane)),
)
.child(
div()
.h(relative(0.20))
.flex_none()
.min_h_0()
.overflow_hidden()
.child(self.pane_decision_region(pane)),
)
.into_any_element()
}
fn command_palette(&self) -> impl IntoElement {
let query = self.model.command_palette().query();
let prompt = if query.is_empty() {
"Type a command…".to_owned()
} else {
query.to_owned()
};
div()
.absolute()
.inset_0()
.flex()
.justify_center()
.items_start()
.pt(px(92.0))
.bg(gpui::black().opacity(0.72))
.child(
div()
.w(px(620.0))
.overflow_hidden()
.bg(rgb(PANEL_ALT))
.border_1()
.border_color(rgb(ACCENT))
.rounded(px(8.0))
.child(
div()
.px_4()
.py_3()
.border_b_1()
.border_color(rgb(BORDER))
.text_color(rgb(if query.is_empty() { MUTED } else { TEXT }))
.child(prompt),
)
.child(
div()
.flex()
.flex_col()
.p_2()
.text_sm()
.child(
div()
.px_3()
.py_2()
.rounded(px(5.0))
.bg(rgb(PANEL_ACTIVE))
.child("Focus next pane")
.child(
div()
.mt_1()
.text_xs()
.text_color(rgb(MUTED))
.child("Workspace · navigation"),
),
)
.child(
div()
.px_3()
.py_2()
.text_color(rgb(MUTED))
.child("Add workspace panel")
.child(
div()
.mt_1()
.text_xs()
.child("Alt+Shift+N · reattach if detached"),
),
)
.child(
div()
.px_3()
.py_2()
.text_color(rgb(MUTED))
.child("Detach selected panel")
.child(
div()
.mt_1()
.text_xs()
.child("Alt+Shift+W · session keeps running"),
),
)
.child(
div()
.px_3()
.py_2()
.text_color(rgb(MUTED))
.child("Share selected pane to Buzz…"),
),
)
.child(
div()
.flex()
.justify_between()
.px_4()
.py_2()
.border_t_1()
.border_color(rgb(BORDER))
.text_xs()
.text_color(rgb(MUTED))
.child("Enter to run")
.child("Esc to close"),
),
)
}
}
impl Render for LumbridgeShell {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let attention = self.model.pane(PaneId::ClaudeUi);
let panel_capacity = visible_panel_count(window.bounds().size);
let attached_panes = self.model.attached_pane_ids();
let attached_count = attached_panes.len();
let detached_entries = self
.model
.detached_pane_ids()
.into_iter()
.map(|pane| (pane, self.model.pane(pane).fixture().title))
.collect::<Vec<_>>();
let detached_count = detached_entries.len();
let visible_count = panel_capacity.min(attached_count).max(1);
let running_runtime_count = self
.live_terminals
.values()
.filter(|terminal| matches!(terminal.status, LiveRuntimeStatus::Running { .. }))
.count();
let runtime_summary = format!(
"{running_runtime_count}/{} LIVE PTYS",
self.live_terminals.len()
);
let sidebar = div()
.flex()
.flex_col()
.w(px(248.0))
.flex_none()
.bg(rgb(PANEL))
.border_r_1()
.border_color(rgb(BORDER_QUIET))
.child(
div()
.px_4()
.pt_4()
.pb_2()
.text_xs()
.text_color(rgb(ATTENTION))
.child("ATTENTION · 1"),
)
.child(
div()
.mx_2()
.mb_3()
.px_3()
.py_2()
.rounded(px(5.0))
.bg(rgb(PANEL_ACTIVE))
.border_1()
.border_color(rgb(ATTENTION))
.child(
div()
.text_sm()
.text_color(rgb(TEXT))
.child(attention.fixture().title),
)
.child(
div()
.mt_1()
.text_xs()
.text_color(rgb(MUTED))
.child("Waiting for a split decision"),
),
)
.child(
div()
.px_4()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child("WORKTREES"),
)
.children(WORKSPACES.into_iter().enumerate().map(|(index, name)| {
div()
.mx_2()
.mb_1()
.px_3()
.py_2()
.rounded(px(5.0))
.when(index == 0, |view| {
view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT))
})
.when(index != 0, |view| view.text_color(rgb(MUTED)))
.child(name)
.when(index == 0, |view| {
view.child(
div()
.mt_1()
.text_xs()
.text_color(rgb(MUTED))
.child("main · metal"),
)
})
}))
.child(
div()
.mt_3()
.px_4()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child("RUNTIMES"),
)
.child(
div()
.px_4()
.py_1()
.text_sm()
.text_color(rgb(SUCCESS))
.child(format!("metal · {runtime_summary}")),
)
.child(
div()
.px_4()
.py_1()
.text_sm()
.text_color(rgb(MUTED))
.child("amd-server · connected"),
)
.child(
div()
.px_4()
.py_1()
.text_sm()
.text_color(rgb(MUTED))
.child("spark-1 · sleeping"),
)
.child(
div()
.mt_3()
.px_4()
.py_2()
.text_xs()
.text_color(rgb(MUTED))
.child(format!("DETACHED SESSIONS · {detached_count}")),
)
.children(detached_entries.into_iter().map(|(pane, title)| {
div()
.id(("detached-session", pane.index()))
.cursor_pointer()
.mx_2()
.mb_1()
.px_3()
.py_2()
.rounded(px(4.0))
.border_1()
.border_color(rgb(BORDER_QUIET))
.text_xs()
.text_color(rgb(MUTED))
.child(format!("↪ {title}"))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.attach_panel(pane, window, cx);
}))
}))
.child(div().flex_1())
.child(
div()
.m_3()
.p_3()
.rounded(px(5.0))
.bg(rgb(PANEL_ALT))
.text_xs()
.text_color(rgb(MUTED))
.child("Buzz · lumbridgecode")
.child(
div()
.mt_1()
.text_color(rgb(SUCCESS))
.child("connected · signed identity"),
),
);
let tabs = div()
.flex()
.items_center()
.h(px(38.0))
.flex_none()
.px_2()
.gap_1()
.bg(rgb(PANEL))
.border_b_1()
.border_color(rgb(BORDER_QUIET))
.child(
div()
.h_full()
.flex()
.items_center()
.px_3()
.border_b_2()
.border_color(rgb(ACCENT))
.text_sm()
.child("Agent workspace"),
)
.child(
div()
.px_3()
.text_sm()
.text_color(rgb(MUTED))
.child("Architecture.md"),
)
.child(
div()
.px_3()
.text_sm()
.text_color(rgb(MUTED))
.child("Review"),
)
.child(div().flex_1())
.child(div().px_3().text_xs().text_color(rgb(MUTED)).child(format!(
"{visible_count} shown · {attached_count} attached · {detached_count} detached"
)))
.child(
div()
.id("add-workspace-panel")
.cursor_pointer()
.ml_2()
.px_3()
.py_1()
.rounded(px(4.0))
.border_1()
.border_color(rgb(if detached_count > 0 { ACCENT } else { BORDER }))
.text_xs()
.text_color(rgb(if detached_count > 0 { ACCENT } else { MUTED }))
.child(if detached_count > 0 {
"+ ADD PANEL"
} else {
"ALL PANELS ATTACHED"
})
.on_click(cx.listener(|shell, _, window, cx| {
shell.add_panel(window, cx);
})),
);
let selected_position = attached_panes
.iter()
.position(|pane| *pane == self.model.selected_pane())
.expect("the selected pane must remain attached");
let panel_range = visible_panel_range(attached_count, selected_position, visible_count);
let workspace_panels = panel_range
.map(|index| {
let pane = self.model.pane(attached_panes[index]);
div()
.flex_1()
.min_w_0()
.min_h_0()
.child(self.workspace_panel(pane, pane.id() == self.model.selected_pane(), cx))
})
.collect::<Vec<_>>();
let workspace_row = div()
.flex()
.gap_1()
.flex_1()
.min_w_0()
.min_h_0()
.overflow_hidden()
.children(workspace_panels);
let counters = self.model.counters();
let terminal_revision = self
.live_terminals
.values()
.map(|terminal| terminal.terminal.revision())
.sum::<u64>();
let footer_left = format!(
"rev {} · {} PTYs · VT rev {} · {} focus · {} VT snapshots · {} synthetic lines",
self.model.revision(),
self.runtimes.len(),
terminal_revision,
counters.focus_moves,
counters.external_snapshot_updates,
counters.terminal_lines_appended
);
let timing = self.timing.summary();
let root = div()
.id("lumbridge-shell")
.relative()
.track_focus(&self.root_focus)
.key_context("LumbridgeShell")
.on_click(cx.listener(|shell, _, window, _| {
window.focus(&shell.root_focus);
}))
.on_action(cx.listener(Self::focus_left))
.on_action(cx.listener(Self::focus_right))
.on_action(cx.listener(Self::focus_up))
.on_action(cx.listener(Self::focus_down))
.on_action(cx.listener(Self::open_palette))
.on_action(cx.listener(Self::close_palette))
.on_action(cx.listener(Self::add_panel_action))
.on_action(cx.listener(Self::detach_selected_panel))
.on_action(cx.listener(Self::terminal_taller))
.on_action(cx.listener(Self::terminal_shorter))
.on_action(cx.listener(Self::terminal_wider))
.on_action(cx.listener(Self::terminal_narrower))
.on_action(cx.listener(|shell, _: &SelectPane1, window, cx| {
shell.select_pane(PaneId::CodexRuntime, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane2, window, cx| {
shell.select_pane(PaneId::ClaudeUi, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane3, window, cx| {
shell.select_pane(PaneId::PiDocs, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane4, window, cx| {
shell.select_pane(PaneId::Architecture, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane5, window, cx| {
shell.select_pane(PaneId::AcpPreview, window, cx);
}))
.on_action(cx.listener(|shell, _: &SelectPane6, window, cx| {
shell.select_pane(PaneId::RuntimeReview, window, cx);
}))
.on_key_down(cx.listener(Self::on_key_down))
.flex()
.flex_col()
.size_full()
.bg(rgb(BG))
.text_color(rgb(TEXT))
.child(
div()
.flex()
.items_center()
.h(px(48.0))
.flex_none()
.px_4()
.bg(rgb(PANEL_ALT))
.border_b_1()
.border_color(rgb(BORDER_QUIET))
.child(div().text_lg().child("Lumbridge"))
.child(
div()
.ml_3()
.text_sm()
.text_color(rgb(MUTED))
.child("Lumbridge Code / main"),
)
.child(div().flex_1())
.child(
div()
.mr_4()
.text_xs()
.text_color(rgb(SUCCESS))
.child(format!("metal · {runtime_summary}")),
)
.child(
div()
.text_sm()
.text_color(rgb(ACCENT))
.child("Ctrl/⌘ K · Commands"),
),
)
.child(
div().flex().flex_1().min_h_0().child(sidebar).child(
div()
.flex()
.flex_col()
.flex_1()
.min_w_0()
.min_h_0()
.overflow_hidden()
.child(tabs)
.child(workspace_row),
),
)
.child(
div()
.flex()
.items_center()
.justify_between()
.h(px(32.0))
.flex_none()
.px_3()
.bg(rgb(PANEL_ALT))
.border_t_1()
.border_color(rgb(BORDER_QUIET))
.text_xs()
.text_color(rgb(MUTED))
.child(footer_left)
.child(timing)
.child(FOOTER_RIGHT),
)
.when(self.model.command_palette().is_open(), |view| {
view.child(self.command_palette())
});
self.timing.observe_element_build();
root
}
}
fn key_modifiers(event: &KeyDownEvent) -> KeyModifiers {
let mut modifiers = KeyModifiers::default();
if event.keystroke.modifiers.shift {
modifiers = modifiers.union(KeyModifiers::SHIFT);
}
if event.keystroke.modifiers.alt {
modifiers = modifiers.union(KeyModifiers::ALT);
}
if event.keystroke.modifiers.control {
modifiers = modifiers.union(KeyModifiers::CONTROL);
}
if event.keystroke.modifiers.platform {
modifiers = modifiers.union(KeyModifiers::PLATFORM);
}
modifiers
}
fn terminal_key_from_parts(
key: &str,
key_char: Option<&str>,
modifiers: KeyModifiers,
) -> Option<TerminalKeyEvent> {
let key = match key {
"enter" => TerminalKey::Enter,
"backspace" => TerminalKey::Backspace,
"tab" => TerminalKey::Tab,
"escape" => TerminalKey::Escape,
"up" => TerminalKey::Up,
"down" => TerminalKey::Down,
"left" => TerminalKey::Left,
"right" => TerminalKey::Right,
"home" => TerminalKey::Home,
"end" => TerminalKey::End,
"insert" => TerminalKey::Insert,
"delete" => TerminalKey::Delete,
"pageup" => TerminalKey::PageUp,
"pagedown" => TerminalKey::PageDown,
function
if function.strip_prefix('f').is_some_and(|suffix| {
!suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit())
}) =>
{
let number = function.strip_prefix('f')?.parse::<u8>().ok()?;
TerminalKey::Function(number)
}
_ if !modifiers.contains(KeyModifiers::PLATFORM) => TerminalKey::Text(key_char?.to_owned()),
_ => return None,
};
Some(TerminalKeyEvent { key, modifiers })
}
fn terminal_scroll_from_parts(key: &str, modifiers: KeyModifiers) -> Option<TerminalScroll> {
if !modifiers.contains(KeyModifiers::SHIFT) {
return None;
}
match key {
"pageup" => Some(TerminalScroll::PageUp),
"pagedown" => Some(TerminalScroll::PageDown),
"home" => Some(TerminalScroll::Top),
"end" => Some(TerminalScroll::Bottom),
_ => None,
}
}
fn live_pty_script(pane: PaneId) -> &'static str {
match pane {
PaneId::CodexRuntime => {
"printf 'Codex runtime · independent Lumbridge PTY\\n'; exec /bin/sh -i"
}
PaneId::ClaudeUi => {
"printf 'Claude workspace · independent Lumbridge PTY\\n'; exec /bin/sh -i"
}
PaneId::PiDocs => "printf 'Pi docs · independent Lumbridge PTY\\n'; exec /bin/sh -i",
PaneId::Architecture | PaneId::AcpPreview | PaneId::RuntimeReview => {
unreachable!("only terminal fixtures own live PTYs")
}
}
}
fn spawn_live_runtime(
runtimes: &mut RuntimeRegistry<PaneId>,
pane: PaneId,
dimensions: TerminalDimensions,
) -> Result<(), RuntimeRegistryError> {
let command = CommandConfig::new("/bin/sh")
.map_err(RuntimeActorError::Start)?
.args(["-c", live_pty_script(pane)]);
let pty_size = TerminalSize::new(dimensions.rows(), dimensions.columns())
.map_err(RuntimeActorError::Start)?;
runtimes.spawn(
pane,
command,
PtyOptions::new(pty_size),
RuntimeActorOptions::default(),
)?;
Ok(())
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("alt-left", FocusLeft, Some("LumbridgeShell")),
KeyBinding::new("alt-right", FocusRight, Some("LumbridgeShell")),
KeyBinding::new("alt-up", FocusUp, Some("LumbridgeShell")),
KeyBinding::new("alt-down", FocusDown, Some("LumbridgeShell")),
KeyBinding::new("secondary-k", OpenPalette, Some("LumbridgeShell")),
KeyBinding::new("alt-1", SelectPane1, Some("LumbridgeShell")),
KeyBinding::new("alt-2", SelectPane2, Some("LumbridgeShell")),
KeyBinding::new("alt-3", SelectPane3, Some("LumbridgeShell")),
KeyBinding::new("alt-4", SelectPane4, Some("LumbridgeShell")),
KeyBinding::new("alt-5", SelectPane5, Some("LumbridgeShell")),
KeyBinding::new("alt-6", SelectPane6, Some("LumbridgeShell")),
KeyBinding::new("alt-shift-n", AddPanel, Some("LumbridgeShell")),
KeyBinding::new("alt-shift-w", DetachSelectedPanel, Some("LumbridgeShell")),
KeyBinding::new("alt-shift-up", TerminalTaller, Some("LumbridgeShell")),
KeyBinding::new("alt-shift-down", TerminalShorter, Some("LumbridgeShell")),
KeyBinding::new("alt-shift-right", TerminalWider, Some("LumbridgeShell")),
KeyBinding::new("alt-shift-left", TerminalNarrower, Some("LumbridgeShell")),
]);
let bounds = Bounds::centered(None, size(px(1500.0), px(960.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some("Lumbridge · GPUI workspace".into()),
..Default::default()
}),
..Default::default()
},
|window, cx| cx.new(|cx| LumbridgeShell::new(window, cx)),
)
.expect("GPUI workspace window should open");
cx.activate(true);
});
}
#[cfg(test)]
mod tests {
use super::{
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts,
terminal_paint_rows, terminal_scroll_from_parts, visible_panel_count, visible_panel_range,
};
use gpui::{px, size};
#[test]
fn maps_named_and_composed_gpui_keys_to_terminal_input() {
let enter = terminal_key_from_parts("enter", None, KeyModifiers::default()).unwrap();
assert_eq!(enter.key, TerminalKey::Enter);
let literal_f = terminal_key_from_parts("f", Some("f"), KeyModifiers::default()).unwrap();
assert_eq!(literal_f.key, TerminalKey::Text("f".into()));
let function = terminal_key_from_parts("f12", None, KeyModifiers::default()).unwrap();
assert_eq!(function.key, TerminalKey::Function(12));
let composed = terminal_key_from_parts("é", Some("é"), KeyModifiers::default()).unwrap();
assert_eq!(composed.key, TerminalKey::Text("é".into()));
}
#[test]
fn reserves_platform_text_shortcuts_for_the_workspace() {
assert!(terminal_key_from_parts("c", Some("c"), KeyModifiers::PLATFORM).is_none());
let control = terminal_key_from_parts("c", Some("c"), KeyModifiers::CONTROL).unwrap();
assert_eq!(control.key, TerminalKey::Text("c".into()));
}
#[test]
fn terminal_geometry_tracks_middle_sixty_percent_per_panel() {
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)), 5);
assert_eq!(dimensions.rows(), 26);
assert_eq!(dimensions.columns(), 45);
let ultrawide = size(px(3440.0), px(1440.0));
assert_eq!(visible_panel_count(ultrawide), 5);
let dimensions = terminal_dimensions_for_window(ultrawide, 5);
assert_eq!(dimensions.rows(), 42);
assert_eq!(dimensions.columns(), 71);
let two_panels = terminal_dimensions_for_window(ultrawide, 2);
assert_eq!(two_panels.rows(), 42);
assert_eq!(two_panels.columns(), 185);
}
#[test]
fn panel_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_panel_range(6, 0, 5), 0..5);
assert_eq!(visible_panel_range(6, 5, 5), 1..6);
assert_eq!(visible_panel_range(6, 3, 3), 2..5);
assert_eq!(visible_panel_range(6, 4, 1), 4..5);
}
#[test]
fn shift_navigation_controls_scrollback_without_reaching_the_pty() {
assert_eq!(
terminal_scroll_from_parts("pageup", KeyModifiers::SHIFT),
Some(TerminalScroll::PageUp)
);
assert_eq!(
terminal_scroll_from_parts("end", KeyModifiers::SHIFT),
Some(TerminalScroll::Bottom)
);
assert_eq!(
terminal_scroll_from_parts("pageup", KeyModifiers::default()),
None
);
}
#[test]
fn terminal_paint_rows_merge_runs_and_preserve_cursor_boundary() {
let mut terminal = TerminalEngine::new(TerminalEngineOptions::default());
terminal.process(b"\x1b[31mAB\x1b[0m");
let rows = terminal_paint_rows(&terminal.snapshot());
assert!(
rows[0]
.iter()
.any(|run| run.text == "AB" && run.columns == 2)
);
assert!(rows[0].iter().any(|run| run.cursor.is_some()));
}
#[test]
fn xterm_color_cube_and_grayscale_are_deterministic() {
assert_eq!(indexed_terminal_color(16), 0x000000);
assert_eq!(indexed_terminal_color(231), 0xffffff);
assert_eq!(indexed_terminal_color(232), 0x080808);
assert_eq!(indexed_terminal_color(255), 0xeeeeee);
}
}