use std::rc::Rc; use gpui::{ AnyElement, App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, }; use crate::{ buttons::{ButtonVariant, IconButton}, menus::{Menu, MenuItem, SplitButton}, primitives::{Icon, IconName}, theme::{ActiveTheme, ControlSize, IconSize, TextSize}, }; /// A shell a terminal can start: its name and its program. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ShellChoice { pub name: SharedString, pub program: SharedString, } type OnIndex = Rc; /// A new terminal in the default shell, with the others under its arrow, the default checked. #[derive(IntoElement)] pub struct ShellSelector { id: ElementId, shells: Vec, default: usize, on_pick: Option, } impl ShellSelector { pub fn new(id: impl Into, shells: impl IntoIterator) -> Self { let shells: Vec = shells.into_iter().collect(); assert!( !shells.is_empty(), "a shell selector offers at least one shell" ); Self { id: id.into(), shells, default: 0, on_pick: None, } } pub fn default_shell(mut self, default: usize) -> Self { assert!( default < self.shells.len(), "the default is one of the shells" ); self.default = default; self } /// Called with the shell to start: the default from the button, any from the menu. pub fn on_pick(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self { self.on_pick = Some(Rc::new(handler)); self } } impl RenderOnce for ShellSelector { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let menu = self .shells .iter() .enumerate() .fold(Menu::new(), |menu, (ix, shell)| { let pick = self.on_pick.clone(); menu.item( MenuItem::check(shell.name.clone(), ix == self.default).on_click( move |window, cx| { if let Some(pick) = &pick { pick(ix, window, cx) } }, ), ) }); let (pick, default) = (self.on_pick, self.default); SplitButton::new(self.id, format!("New {}", self.shells[default].name), menu) .variant(ButtonVariant::Ghost) .on_click(move |_, window, cx| { if let Some(pick) = &pick { pick(default, window, cx) } }) } } type Run = Rc; /// The row over a terminal: its title and how its program stands, the shell selector, and split, clear, stop and maximize. #[derive(IntoElement)] pub struct TerminalToolbar { id: ElementId, title: SharedString, exit: Option, shells: Option, actions: Vec<(&'static str, IconName, &'static str, Run)>, } impl TerminalToolbar { /// `exit` is the program's exit code once it has ended. pub fn new( id: impl Into, title: impl Into, exit: Option, ) -> Self { Self { id: id.into(), title: title.into(), exit, shells: None, actions: Vec::new(), } } pub fn shells(mut self, selector: ShellSelector) -> Self { self.shells = Some(selector.into_any_element()); self } fn action( mut self, key: &'static str, icon: IconName, words: &'static str, run: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { self.actions.push((key, icon, words, Rc::new(run))); self } pub fn on_split(self, run: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.action("split", IconName::Columns2, "Split terminal", run) } pub fn on_clear(self, run: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.action("clear", IconName::Eraser, "Clear", run) } pub fn on_stop(self, run: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.action("stop", IconName::CircleStop, "Stop the program", run) } pub fn on_maximize(self, run: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.action("maximize", IconName::Maximize2, "Maximize", run) } } impl RenderOnce for TerminalToolbar { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors.clone(); let (mark, color) = match self.exit { None => (IconName::Terminal, colors.fg_muted), Some(0) => (IconName::CircleCheck, colors.success), Some(_) => (IconName::CircleX, colors.danger), }; div() .flex() .items_center() .gap_2() .px_2() .py_1() .border_b_1() .border_color(colors.border) .text_size(theme.text_size(TextSize::Sm)) .child(Icon::new(mark).size(IconSize::Sm).color(color)) .child( div() .flex_1() .min_w_0() .font_weight(FontWeight::MEDIUM) .text_color(colors.fg) .child(self.title), ) .children(self.exit.filter(|code| *code != 0).map(|code| { div() .text_color(colors.danger) .child(format!("Exit {code}")) })) .children(self.shells) .children(self.actions.into_iter().map(|(key, icon, words, run)| { IconButton::new((self.id.clone(), key), icon) .variant(ButtonVariant::Ghost) .size(ControlSize::Sm) .tooltip(words) .on_click(move |_, window, cx| run(window, cx)) })) } }