use std::time::Duration; use floem::{ Application, action::exec_after, event::EventPropagation, kurbo::Size, peniko::Color, prelude::*, reactive::{Effect, RwSignal, SignalGet, SignalTrack, SignalUpdate, SignalWith}, style::Position, window::WindowConfig, }; use lumbridge_ui_fixture::{ FOOTER_FIXTURE_NOTICE, FocusDirection, PaneId, PaneState, ShellAction, ShellModel, SurfaceKind, }; const BG: Color = Color::from_rgb8(9, 12, 18); const PANEL: Color = Color::from_rgb8(16, 22, 32); const PANEL_ALT: Color = Color::from_rgb8(21, 29, 41); const PANEL_ACTIVE: Color = Color::from_rgb8(24, 35, 52); const BORDER: Color = Color::from_rgb8(38, 50, 70); const BORDER_QUIET: Color = Color::from_rgb8(28, 38, 54); const TEXT: Color = Color::from_rgb8(219, 229, 244); const MUTED: Color = Color::from_rgb8(130, 144, 168); const ACCENT: Color = Color::from_rgb8(104, 181, 248); const ATTENTION: Color = Color::from_rgb8(241, 185, 106); const SUCCESS: Color = Color::from_rgb8(112, 214, 168); const OS_MOD: Modifiers = if cfg!(target_os = "macos") { Modifiers::META } else { Modifiers::CONTROL }; fn dispatch(model: RwSignal, action: ShellAction) { model.update(|model| { model.dispatch(action); }); } fn pane_card(id: PaneId, model: RwSignal) -> impl IntoView { dyn_container( move || model.with(|model| (model.pane(id).clone(), model.selected_pane() == id)), move |(pane, selected)| render_pane_card(pane, selected, model), ) .style(|s| { s.flex_basis(0) .flex_grow(1.0) .min_width(0.0) .min_height(0.0) .margin(4.0) }) } fn render_pane_card(pane: PaneState, selected: bool, model: RwSignal) -> impl IntoView { let id = pane.id(); let needs_input = pane.needs_input(); let state_color = if needs_input { ATTENTION } else if matches!(pane.kind(), SurfaceKind::Terminal) { SUCCESS } else { ACCENT }; let state_label = if matches!(pane.kind(), SurfaceKind::Terminal) { pane.status().label() } else { pane.fixture().badge }; let start = pane.lines().len().saturating_sub(12); let lines = Stack::from_iter(pane.lines()[start..].iter().cloned().map(|line| { Label::new(line).style(|s| { s.font_size(12.0) .color(TEXT) .margin_bottom(4.0) .min_width(0.0) }) })) .style(|s| s.flex_col().padding_horiz(10.0).padding_bottom(10.0)); let header = Stack::horizontal(( Stack::horizontal(( Label::new(format!("{}", id.index() + 1)) .style(|s| s.font_size(11.0).color(MUTED).margin_right(8.0)), Label::new(pane.fixture().title) .style(|s| s.flex_grow(1.0).font_size(13.0).color(TEXT)), )) .style(|s| s.items_center().min_width(0.0).flex_grow(1.0)), Label::new(state_label).style(move |s| s.font_size(10.0).color(state_color)), )) .style(move |s| { s.items_center() .height(36.0) .padding_horiz(10.0) .background(if selected { PANEL_ACTIVE } else { PANEL_ALT }) .border_bottom(1.0) .border_color(BORDER_QUIET) }); let target = Stack::horizontal(( Label::new(pane.fixture().target).style(|s| s.flex_grow(1.0).color(MUTED)), Label::new(if selected { "FOCUSED" } else { "" }) .style(|s| s.font_size(10.0).color(ACCENT)), )) .style(|s| { s.items_center() .padding_horiz(10.0) .padding_vert(8.0) .font_size(11.0) }); Stack::vertical((header, target, lines)) .style(move |s| { s.width_full() .height_full() .min_width(0.0) .min_height(0.0) .background(if selected { PANEL_ACTIVE } else { PANEL }) .border(if selected { 2.0 } else { 1.0 }) .border_color(if selected || needs_input { state_color } else { BORDER }) .border_radius(5.0) .keyboard_navigable() }) .on_event_stop(listener::Click, move |_, _| { dispatch(model, ShellAction::SelectPane(id)); }) } fn sidebar() -> impl IntoView { // The worktree list this shell used to render came from a five-entry // fixture naming this developer's machines, backed by no git integration. // The GPUI shell's copy was deleted in the same pass; this one renders the // workspace it is actually open on. let worktrees = Stack::vertical((Label::new("Lumbridge Code"),)).style(|s| { s.flex_col() .padding_horiz(8.0) .padding_vert(7.0) .color(TEXT) }); Stack::vertical(( Label::new("ATTENTION · 1").style(|s| s.font_size(10.0).color(ATTENTION).padding(12.0)), Stack::vertical(( Label::new("Claude Code · UI").style(|s| s.color(TEXT)), Label::new("Waiting for a split decision") .style(|s| s.font_size(10.0).color(MUTED).margin_top(3.0)), )) .style(|s| { s.margin_horiz(8.0) .margin_bottom(12.0) .padding(10.0) .background(PANEL_ACTIVE) .border(1.0) .border_color(ATTENTION) .border_radius(5.0) }), Label::new("WORKTREES").style(|s| s.font_size(10.0).color(MUTED).padding(12.0)), worktrees, Label::new("RUNTIMES").style(|s| { s.font_size(10.0) .color(MUTED) .padding_horiz(12.0) .padding_vert(9.0) .margin_top(8.0) }), Label::new("● metal · connected").style(|s| s.color(SUCCESS).padding_horiz(12.0)), Label::new("● amd-server · connected") .style(|s| s.color(MUTED).padding_horiz(12.0).margin_top(7.0)), Label::new("○ spark-1 · sleeping") .style(|s| s.color(MUTED).padding_horiz(12.0).margin_top(7.0)), Empty::new().style(|s| s.flex_grow(1.0)), Stack::vertical(( Label::new("Buzz · lumbridgecode"), Label::new("connected · signed identity").style(|s| s.color(SUCCESS).margin_top(3.0)), )) .style(|s| { s.margin(10.0) .padding(10.0) .font_size(10.0) .color(MUTED) .background(PANEL_ALT) .border_radius(5.0) }), )) .style(|s| { s.width(248.0) .height_full() .flex_shrink(0.0) .background(PANEL) .border_right(1.0) .border_color(BORDER_QUIET) }) } fn tabs() -> impl IntoView { Stack::horizontal(( Label::new("Agent workspace").style(|s| { s.height_full() .items_center() .padding_horiz(12.0) .border_bottom(2.0) .border_color(ACCENT) .color(TEXT) }), Label::new("Architecture.md").style(|s| s.padding_horiz(12.0).color(MUTED)), Label::new("Review").style(|s| s.padding_horiz(12.0).color(MUTED)), Empty::new().style(|s| s.flex_grow(1.0)), Label::new("6 surfaces · 3 remote · 1 waiting") .style(|s| s.font_size(10.0).padding_horiz(12.0).color(MUTED)), )) .style(|s| { s.height(38.0) .items_center() .font_size(12.0) .background(PANEL) .border_bottom(1.0) .border_color(BORDER_QUIET) }) } fn command_palette(query: RwSignal) -> impl IntoView { let input = TextInput::new(query) .placeholder("Type a command…") .style(|s| { s.width_full() .height(44.0) .padding_horiz(12.0) .color(TEXT) .background(PANEL_ALT) .border(0.0) }); let input_id = input.id(); exec_after(Duration::from_millis(1), move |_| input_id.request_focus()); let card = Stack::vertical(( input, Stack::vertical(( Stack::vertical(( Label::new("Focus next pane").style(|s| s.color(TEXT)), Label::new("Workspace · navigation") .style(|s| s.font_size(10.0).color(MUTED).margin_top(3.0)), )) .style(|s| s.padding(10.0).background(PANEL_ACTIVE).border_radius(5.0)), Label::new("Open attention request").style(|s| s.padding(10.0).color(MUTED)), Label::new("Share selected pane to Buzz…").style(|s| s.padding(10.0).color(MUTED)), )) .style(|s| { s.padding(8.0) .border_top(1.0) .border_bottom(1.0) .border_color(BORDER) }), Stack::horizontal(( Label::new("Enter to run").style(|s| s.flex_grow(1.0)), Label::new("Esc to close"), )) .style(|s| { s.padding_horiz(12.0) .padding_vert(8.0) .font_size(10.0) .color(MUTED) }), )) .style(|s| { s.width(620.0) .margin_top(92.0) .background(PANEL_ALT) .border(1.0) .border_color(ACCENT) .border_radius(8.0) }); Container::new(card).style(|s| { s.position(Position::Absolute) .inset(0.0) .items_start() .justify_center() .background(Color::from_rgba8(5, 7, 11, 220)) .z_index(50) }) } fn handle_key( model: RwSignal, query: RwSignal, KeyboardEvent { code, key, modifiers, .. }: &KeyboardEvent, ) -> EventPropagation { if *code == Code::KeyK && modifiers.contains(OS_MOD) { query.set(String::new()); dispatch(model, ShellAction::OpenCommandPalette); return EventPropagation::Stop; } if model.with(|model| model.command_palette().is_open()) { if *key == Key::Named(NamedKey::Escape) { query.set(String::new()); dispatch(model, ShellAction::CloseCommandPalette); return EventPropagation::Stop; } return EventPropagation::Continue; } let action = match key { Key::Named(NamedKey::ArrowLeft) => Some(ShellAction::MoveFocus(FocusDirection::Left)), Key::Named(NamedKey::ArrowRight) => Some(ShellAction::MoveFocus(FocusDirection::Right)), Key::Named(NamedKey::ArrowUp) => Some(ShellAction::MoveFocus(FocusDirection::Up)), Key::Named(NamedKey::ArrowDown) => Some(ShellAction::MoveFocus(FocusDirection::Down)), Key::Character(character) if modifiers.is_empty() => match character.as_str() { "h" => Some(ShellAction::MoveFocus(FocusDirection::Left)), "l" => Some(ShellAction::MoveFocus(FocusDirection::Right)), "k" => Some(ShellAction::MoveFocus(FocusDirection::Up)), "j" => Some(ShellAction::MoveFocus(FocusDirection::Down)), "1" => Some(ShellAction::SelectPane(PaneId::CodexRuntime)), "2" => Some(ShellAction::SelectPane(PaneId::ClaudeUi)), "3" => Some(ShellAction::SelectPane(PaneId::PiDocs)), "4" => Some(ShellAction::SelectPane(PaneId::Architecture)), "5" => Some(ShellAction::SelectPane(PaneId::AcpPreview)), "6" => Some(ShellAction::SelectPane(PaneId::RuntimeReview)), _ => None, }, _ => None, }; if let Some(action) = action { dispatch(model, action); EventPropagation::Stop } else { EventPropagation::Continue } } fn app_view() -> impl IntoView { let model = RwSignal::new(ShellModel::with_all_panels_attached()); let query = RwSignal::new(String::new()); let timer_pulse = RwSignal::new(()); Effect::new(move |_| { timer_pulse.track(); exec_after(Duration::from_millis(650), move |_| { dispatch(model, ShellAction::SyntheticStreamTick); timer_pulse.set(()); }); }); Effect::new(move |_| { let query = query.get(); model.update(|model| { if model.command_palette().is_open() && model.command_palette().query() != query { model.dispatch(ShellAction::SetCommandPaletteQuery(query)); } }); }); let top_row = Stack::horizontal(( pane_card(PaneId::CodexRuntime, model), pane_card(PaneId::ClaudeUi, model), pane_card(PaneId::PiDocs, model), )) .style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0).width_full()); let bottom_row = Stack::horizontal(( pane_card(PaneId::Architecture, model), pane_card(PaneId::AcpPreview, model), pane_card(PaneId::RuntimeReview, model), )) .style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0).width_full()); let grid = Stack::vertical((top_row, bottom_row)).style(|s| { s.flex_basis(0) .flex_grow(1.0) .min_width(0.0) .min_height(0.0) .padding(4.0) }); let header = Stack::horizontal(( Label::new("Lumbridge").style(|s| s.font_size(18.0).color(TEXT)), Label::new("Lumbridge Code / main") .style(|s| s.color(MUTED).margin_left(12.0).flex_grow(1.0)), Label::new("metal · runtime online") .style(|s| s.font_size(10.0).color(SUCCESS).margin_right(18.0)), Label::new("Ctrl/⌘ K · Commands") .style(|s| s.color(ACCENT).keyboard_navigable()) .on_event_stop(listener::Click, move |_, _| { query.set(String::new()); dispatch(model, ShellAction::OpenCommandPalette); }), )) .style(|s| { s.height(48.0) .padding_horiz(14.0) .items_center() .background(PANEL_ALT) .border_bottom(1.0) .border_color(BORDER_QUIET) }); let body = Stack::horizontal(( sidebar(), Stack::vertical((tabs(), grid)).style(|s| { s.flex_basis(0) .flex_grow(1.0) .min_width(0.0) .min_height(0.0) }), )) .style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0)); let footer = Stack::horizontal(( Label::derived(move || { model.with(|model| { let counters = model.counters(); format!( "rev {} · {} focus moves · {} surface updates · {} lines", model.revision(), counters.focus_moves, counters.surface_updates, counters.terminal_lines_appended ) }) }) .style(|s| s.flex_grow(1.0)), Label::new(FOOTER_FIXTURE_NOTICE).style(|s| s.flex_grow(1.0)), )) .style(|s| { s.height(32.0) .padding_horiz(10.0) .items_center() .font_size(10.0) .color(MUTED) .background(PANEL_ALT) .border_top(1.0) .border_color(BORDER_QUIET) }); let palette = dyn_container( move || model.with(|model| model.command_palette().is_open()), move |open| { if open { command_palette(query).into_any() } else { Empty::new().into_any() } }, ); let root = Stack::vertical((header, body, footer, palette)) .style(|s| { s.position(Position::Relative) .width_full() .height_full() .background(BG) .color(TEXT) .keyboard_navigable() }) .on_event(listener::KeyDown, move |_, event| { handle_key(model, query, event) }) .window_title(|| "Lumbridge · Floem workspace".to_owned()); let root_id = root.id(); exec_after(Duration::from_millis(1), move |_| root_id.request_focus()); root } fn main() { Application::new() .window( |_| app_view(), Some( WindowConfig::default() .size(Size::new(1500.0, 960.0)) .min_size(Size::new(900.0, 600.0)) .title("Lumbridge · Floem workspace"), ), ) .run(); }