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, live_terminals: BTreeMap, 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, }, 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, dispatch_to_element_micros: VecDeque, } #[derive(Clone, Eq, PartialEq)] struct TerminalPaintRun { text: String, columns: u16, foreground: u32, background: u32, style: TerminalCellStyle, cursor: Option, 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> { (0..snapshot.dimensions.rows()) .map(|row| { let mut runs: Vec = 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, 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) -> 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 { 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::>(); 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 { 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) { 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) { 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.dispatch(ShellAction::SelectPane(pane)); window.focus(&self.root_focus); cx.notify(); } fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context) { 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) { 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) { 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) { 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.add_panel(window, cx); } fn detach_selected_panel( &mut self, _: &DetachSelectedPanel, window: &mut Window, cx: &mut Context, ) { self.detach_panel(self.model.selected_pane(), window, cx); } fn move_focus( &mut self, direction: FocusDirection, window: &mut Window, cx: &mut Context, ) { 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.move_focus(FocusDirection::Left, window, cx); } fn focus_right(&mut self, _: &FocusRight, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Right, window, cx); } fn focus_up(&mut self, _: &FocusUp, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Up, window, cx); } fn focus_down(&mut self, _: &FocusDown, window: &mut Window, cx: &mut Context) { self.move_focus(FocusDirection::Down, window, cx); } fn open_palette(&mut self, _: &OpenPalette, _: &mut Window, cx: &mut Context) { self.dispatch(ShellAction::OpenCommandPalette); cx.notify(); } fn close_palette(&mut self, _: &ClosePalette, _: &mut Window, cx: &mut Context) { 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) { 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) { 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) { 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) { 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) { 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::>(); 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, ) -> 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::>(); 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, ) -> 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) -> 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::>(); 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::>(); 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::(); 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 { 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::().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 { 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, 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); } }