add terminal modal which can be displayed and dismissed while preserving the terminal state

Keith Simmons created

Change summary

assets/keymaps/default.json             |   9 +
crates/terminal/src/modal.rs            |  44 ++++++++
crates/terminal/src/terminal.rs         |  54 +++++++++-
crates/terminal/src/terminal_element.rs | 134 +++++++++++++++-----------
crates/theme/src/theme.rs               |   7 +
styles/src/styleTree/terminal.ts        |  16 +++
6 files changed, 196 insertions(+), 68 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -303,7 +303,8 @@
             "cmd-shift-P": "command_palette::Toggle",
             "cmd-shift-M": "diagnostics::Deploy",
             "cmd-shift-E": "project_panel::Toggle",
-            "cmd-alt-s": "workspace::SaveAll"
+            "cmd-alt-s": "workspace::SaveAll",
+            "shift-space t": "terminal::DeployModal"
         }
     },
     // Bindings from Sublime Text
@@ -419,5 +420,11 @@
             "tab": "terminal::Tab",
             "cmd-v": "terminal::Paste"
         }
+    },
+    {
+        "context": "ModalTerminal",
+        "bindings": {
+            "escape": "terminal::DeployModal"
+        }
     }
 ]

crates/terminal/src/modal.rs 🔗

@@ -0,0 +1,44 @@
+use gpui::{ViewContext, ViewHandle};
+use workspace::Workspace;
+
+use crate::{DeployModal, Event, Terminal};
+
+pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
+    if let Some(stored_terminal) = cx.default_global::<Option<ViewHandle<Terminal>>>().clone() {
+        workspace.toggle_modal(cx, |_, _| stored_terminal);
+    } else {
+        let project = workspace.project().read(cx);
+        let abs_path = project
+            .active_entry()
+            .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
+            .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+            .map(|wt| wt.abs_path().to_path_buf());
+
+        let displaced_modal = workspace.toggle_modal(cx, |_, cx| {
+            let this = cx.add_view(|cx| Terminal::new(cx, abs_path, true));
+            cx.subscribe(&this, on_event).detach();
+            this
+        });
+        cx.set_global(displaced_modal);
+    }
+}
+
+pub fn on_event(
+    workspace: &mut Workspace,
+    _: ViewHandle<Terminal>,
+    event: &Event,
+    cx: &mut ViewContext<Workspace>,
+) {
+    // Dismiss the modal if the terminal quit
+    if let Event::CloseTerminal = event {
+        cx.set_global::<Option<ViewHandle<Terminal>>>(None);
+        if workspace
+            .modal()
+            .cloned()
+            .and_then(|modal| modal.downcast::<Terminal>())
+            .is_some()
+        {
+            workspace.dismiss_modal(cx)
+        }
+    }
+}

crates/terminal/src/terminal.rs 🔗

@@ -1,3 +1,7 @@
+pub mod gpui_func_tools;
+mod modal;
+pub mod terminal_element;
+
 use alacritty_terminal::{
     config::{Config, Program, PtyConfig},
     event::{Event as AlacTermEvent, EventListener, Notify},
@@ -17,6 +21,7 @@ use gpui::{
     actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
     ClipboardItem, Entity, MutableAppContext, View, ViewContext,
 };
+use modal::deploy_modal;
 use project::{Project, ProjectPath};
 use settings::Settings;
 use smallvec::SmallVec;
@@ -37,9 +42,6 @@ const UP_SEQ: &str = "\x1b[A";
 const DOWN_SEQ: &str = "\x1b[B";
 const DEFAULT_TITLE: &str = "Terminal";
 
-pub mod gpui_func_tools;
-pub mod terminal_element;
-
 ///Action for carrying the input to the PTY
 #[derive(Clone, Default, Debug, PartialEq, Eq)]
 pub struct Input(pub String);
@@ -50,7 +52,22 @@ pub struct ScrollTerminal(pub i32);
 
 actions!(
     terminal,
-    [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit]
+    [
+        Sigint,
+        Escape,
+        Del,
+        Return,
+        Left,
+        Right,
+        Up,
+        Down,
+        Tab,
+        Clear,
+        Paste,
+        Deploy,
+        Quit,
+        DeployModal,
+    ]
 );
 impl_internal_actions!(terminal, [Input, ScrollTerminal]);
 
@@ -70,6 +87,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Terminal::tab);
     cx.add_action(Terminal::paste);
     cx.add_action(Terminal::scroll_terminal);
+    cx.add_action(deploy_modal);
 }
 
 ///A translation struct for Alacritty to communicate with us from their event loop
@@ -90,6 +108,7 @@ pub struct Terminal {
     has_new_content: bool,
     has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
     cur_size: SizeInfo,
+    modal: bool,
 }
 
 ///Upward flowing events, for changing the title and such
@@ -105,7 +124,7 @@ impl Entity for Terminal {
 
 impl Terminal {
     ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
-    fn new(cx: &mut ViewContext<Self>, working_directory: Option<PathBuf>) -> Self {
+    fn new(cx: &mut ViewContext<Self>, working_directory: Option<PathBuf>, modal: bool) -> Self {
         //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
         let (events_tx, mut events_rx) = unbounded();
         cx.spawn_weak(|this, mut cx| async move {
@@ -172,6 +191,7 @@ impl Terminal {
             has_new_content: false,
             has_bell: false,
             cur_size: size_info,
+            modal,
         }
     }
 
@@ -218,7 +238,7 @@ impl Terminal {
             ),
             AlacTermEvent::ColorRequest(index, format) => {
                 let color = self.term.lock().colors()[index].unwrap_or_else(|| {
-                    let term_style = &cx.global::<Settings>().theme.terminal;
+                    let term_style = &cx.global::<Settings>().theme.terminal.colors;
                     match index {
                         0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)),
                         //These additional values are required to match the Alacritty Colors object's behavior
@@ -274,7 +294,10 @@ impl Terminal {
             .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
             .map(|wt| wt.abs_path().to_path_buf());
 
-        workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
+        workspace.add_item(
+            Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path, false))),
+            cx,
+        );
     }
 
     ///Send the shutdown message to Alacritty
@@ -367,13 +390,26 @@ impl View for Terminal {
     }
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
-        TerminalEl::new(cx.handle()).contained().boxed()
+        let element = TerminalEl::new(cx.handle()).contained();
+        if self.modal {
+            let settings = cx.global::<Settings>();
+            let container_style = settings.theme.terminal.modal_container;
+            element.with_style(container_style).boxed()
+        } else {
+            element.boxed()
+        }
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
         cx.emit(Event::Activate);
         self.has_new_content = false;
     }
+
+    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
+        let mut context = Self::default_keymap_context();
+        context.set.insert("ModalTerminal".into());
+        context
+    }
 }
 
 impl Item for Terminal {
@@ -488,7 +524,7 @@ mod tests {
     //and produce noticable output?
     #[gpui::test]
     async fn test_terminal(cx: &mut TestAppContext) {
-        let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
+        let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None, false));
 
         terminal.update(cx, |terminal, cx| {
             terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);

crates/terminal/src/terminal_element.rs 🔗

@@ -25,7 +25,7 @@ use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use settings::Settings;
 use std::rc::Rc;
-use theme::TerminalStyle;
+use theme::{TerminalColors, TerminalStyle};
 
 use crate::{gpui_func_tools::paint_layer, Input, ScrollTerminal, Terminal};
 
@@ -110,7 +110,8 @@ impl Element for TerminalEl {
 
         //Now that we're done with the mutable portion, grab the immutable settings and view again
         let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
-        let term = view_handle.read(cx).term.lock();
+        let view = view_handle.read(cx);
+        let term = view.term.lock();
 
         let grid = term.grid();
         let cursor_point = grid.cursor.point;
@@ -123,6 +124,7 @@ impl Element for TerminalEl {
             &text_style,
             terminal_theme,
             cx.text_layout_cache,
+            view.modal,
         );
 
         let cells = layout_cells
@@ -152,7 +154,7 @@ impl Element for TerminalEl {
                 cursor_text.len(),
                 RunStyle {
                     font_id: text_style.font_id,
-                    color: terminal_theme.background,
+                    color: terminal_theme.colors.background,
                     underline: Default::default(),
                 },
             )],
@@ -178,12 +180,18 @@ impl Element for TerminalEl {
                 cursor_position,
                 block_width,
                 line_height.0,
-                terminal_theme.cursor,
+                terminal_theme.colors.cursor,
                 CursorShape::Block,
                 Some(block_text.clone()),
             )
         });
 
+        let background_color = if view.modal {
+            terminal_theme.colors.modal_background
+        } else {
+            terminal_theme.colors.background
+        };
+
         (
             constraint.max,
             LayoutState {
@@ -193,7 +201,7 @@ impl Element for TerminalEl {
                 cursor,
                 cur_size,
                 background_rects,
-                background_color: terminal_theme.background,
+                background_color,
             },
         )
     }
@@ -348,6 +356,7 @@ fn layout_cells(
     text_style: &TextStyle,
     terminal_theme: &TerminalStyle,
     text_layout_cache: &TextLayoutCache,
+    modal: bool,
 ) -> Vec<LayoutCell> {
     let mut line_count: i32 = 0;
     let lines = grid.group_by(|i| i.point.line);
@@ -358,7 +367,7 @@ fn layout_cells(
             line.map(|indexed_cell| {
                 let cell_text = &indexed_cell.c.to_string();
 
-                let cell_style = cell_style(&indexed_cell, terminal_theme, text_style);
+                let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
 
                 let layout_cell = text_layout_cache.layout_str(
                     cell_text,
@@ -368,7 +377,7 @@ fn layout_cells(
                 LayoutCell::new(
                     Point::new(line_count - 1, indexed_cell.point.column.0 as i32),
                     layout_cell,
-                    convert_color(&indexed_cell.bg, terminal_theme),
+                    convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
                 )
             })
             .collect::<Vec<LayoutCell>>()
@@ -410,9 +419,14 @@ fn get_cursor_shape(
 }
 
 ///Convert the Alacritty cell styles to GPUI text styles and background color
-fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &TextStyle) -> RunStyle {
+fn cell_style(
+    indexed: &Indexed<&Cell>,
+    style: &TerminalStyle,
+    text_style: &TextStyle,
+    modal: bool,
+) -> RunStyle {
     let flags = indexed.cell.flags;
-    let fg = convert_color(&indexed.cell.fg, style);
+    let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
 
     let underline = flags
         .contains(Flags::UNDERLINE)
@@ -431,67 +445,73 @@ fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &Text
 }
 
 ///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
-fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
+fn convert_color(alac_color: &AnsiColor, colors: &TerminalColors, modal: bool) -> Color {
+    let background = if modal {
+        colors.modal_background
+    } else {
+        colors.background
+    };
+
     match alac_color {
         //Named and theme defined colors
         alacritty_terminal::ansi::Color::Named(n) => match n {
-            alacritty_terminal::ansi::NamedColor::Black => style.black,
-            alacritty_terminal::ansi::NamedColor::Red => style.red,
-            alacritty_terminal::ansi::NamedColor::Green => style.green,
-            alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
-            alacritty_terminal::ansi::NamedColor::Blue => style.blue,
-            alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
-            alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
-            alacritty_terminal::ansi::NamedColor::White => style.white,
-            alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
-            alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
-            alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
-            alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
-            alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
-            alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
-            alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
-            alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
-            alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
-            alacritty_terminal::ansi::NamedColor::Background => style.background,
-            alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
-            alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
-            alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
-            alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
-            alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
-            alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
-            alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
-            alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
-            alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
-            alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
-            alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
+            alacritty_terminal::ansi::NamedColor::Black => colors.black,
+            alacritty_terminal::ansi::NamedColor::Red => colors.red,
+            alacritty_terminal::ansi::NamedColor::Green => colors.green,
+            alacritty_terminal::ansi::NamedColor::Yellow => colors.yellow,
+            alacritty_terminal::ansi::NamedColor::Blue => colors.blue,
+            alacritty_terminal::ansi::NamedColor::Magenta => colors.magenta,
+            alacritty_terminal::ansi::NamedColor::Cyan => colors.cyan,
+            alacritty_terminal::ansi::NamedColor::White => colors.white,
+            alacritty_terminal::ansi::NamedColor::BrightBlack => colors.bright_black,
+            alacritty_terminal::ansi::NamedColor::BrightRed => colors.bright_red,
+            alacritty_terminal::ansi::NamedColor::BrightGreen => colors.bright_green,
+            alacritty_terminal::ansi::NamedColor::BrightYellow => colors.bright_yellow,
+            alacritty_terminal::ansi::NamedColor::BrightBlue => colors.bright_blue,
+            alacritty_terminal::ansi::NamedColor::BrightMagenta => colors.bright_magenta,
+            alacritty_terminal::ansi::NamedColor::BrightCyan => colors.bright_cyan,
+            alacritty_terminal::ansi::NamedColor::BrightWhite => colors.bright_white,
+            alacritty_terminal::ansi::NamedColor::Foreground => colors.foreground,
+            alacritty_terminal::ansi::NamedColor::Background => background,
+            alacritty_terminal::ansi::NamedColor::Cursor => colors.cursor,
+            alacritty_terminal::ansi::NamedColor::DimBlack => colors.dim_black,
+            alacritty_terminal::ansi::NamedColor::DimRed => colors.dim_red,
+            alacritty_terminal::ansi::NamedColor::DimGreen => colors.dim_green,
+            alacritty_terminal::ansi::NamedColor::DimYellow => colors.dim_yellow,
+            alacritty_terminal::ansi::NamedColor::DimBlue => colors.dim_blue,
+            alacritty_terminal::ansi::NamedColor::DimMagenta => colors.dim_magenta,
+            alacritty_terminal::ansi::NamedColor::DimCyan => colors.dim_cyan,
+            alacritty_terminal::ansi::NamedColor::DimWhite => colors.dim_white,
+            alacritty_terminal::ansi::NamedColor::BrightForeground => colors.bright_foreground,
+            alacritty_terminal::ansi::NamedColor::DimForeground => colors.dim_foreground,
         },
         //'True' colors
         alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
         //8 bit, indexed colors
-        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style),
+        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, colors),
     }
 }
 
 ///Converts an 8 bit ANSI color to it's GPUI equivalent.
-pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
+pub fn get_color_at_index(index: &u8, colors: &TerminalColors) -> Color {
     match index {
         //0-15 are the same as the named colors above
-        0 => style.black,
-        1 => style.red,
-        2 => style.green,
-        3 => style.yellow,
-        4 => style.blue,
-        5 => style.magenta,
-        6 => style.cyan,
-        7 => style.white,
-        8 => style.bright_black,
-        9 => style.bright_red,
-        10 => style.bright_green,
-        11 => style.bright_yellow,
-        12 => style.bright_blue,
-        13 => style.bright_magenta,
-        14 => style.bright_cyan,
-        15 => style.bright_white,
+        0 => colors.black,
+        1 => colors.red,
+        2 => colors.green,
+        3 => colors.yellow,
+        4 => colors.blue,
+        5 => colors.magenta,
+        6 => colors.cyan,
+        7 => colors.white,
+        8 => colors.bright_black,
+        9 => colors.bright_red,
+        10 => colors.bright_green,
+        11 => colors.bright_yellow,
+        12 => colors.bright_blue,
+        13 => colors.bright_magenta,
+        14 => colors.bright_cyan,
+        15 => colors.bright_white,
         //16-231 are mapped to their RGB colors on a 0-5 range per channel
         16..=231 => {
             let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components

crates/theme/src/theme.rs 🔗

@@ -637,6 +637,12 @@ pub struct HoverPopover {
 
 #[derive(Clone, Deserialize, Default)]
 pub struct TerminalStyle {
+    pub colors: TerminalColors,
+    pub modal_container: ContainerStyle,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct TerminalColors {
     pub black: Color,
     pub red: Color,
     pub green: Color,
@@ -655,6 +661,7 @@ pub struct TerminalStyle {
     pub bright_white: Color,
     pub foreground: Color,
     pub background: Color,
+    pub modal_background: Color,
     pub cursor: Color,
     pub dim_black: Color,
     pub dim_red: Color,

styles/src/styleTree/terminal.ts 🔗

@@ -1,7 +1,8 @@
 import Theme from "../themes/common/theme";
+import { border, modalShadow } from "./components";
 
 export default function terminal(theme: Theme) {
-  return {
+  let colors = {
     black: theme.ramps.neutral(0).hex(),
     red: theme.ramps.red(0.5).hex(),
     green: theme.ramps.green(0.5).hex(),
@@ -20,6 +21,7 @@ export default function terminal(theme: Theme) {
     brightWhite: theme.ramps.neutral(7).hex(),
     foreground: theme.ramps.neutral(7).hex(),
     background: theme.ramps.neutral(0).hex(),
+    modalBackground: theme.ramps.neutral(1).hex(),
     cursor: theme.ramps.neutral(7).hex(),
     dimBlack: theme.ramps.neutral(7).hex(),
     dimRed: theme.ramps.red(0.75).hex(),
@@ -32,4 +34,16 @@ export default function terminal(theme: Theme) {
     brightForeground: theme.ramps.neutral(7).hex(),
     dimForeground: theme.ramps.neutral(0).hex(),
   };
+
+  return {
+    colors,
+    modalContainer: {
+      background: colors.modalBackground,
+      cornerRadius: 8,
+      padding: 8,
+      margin: 25,
+      border: border(theme, "primary"),
+      shadow: modalShadow(theme),
+    }
+  };
 }