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

This commit is contained in:
Keith Simmons 2022-07-06 13:20:54 -07:00
parent 00d1c2e56f
commit 2d126c7c5c
6 changed files with 196 additions and 68 deletions

View file

@ -303,7 +303,8 @@
"cmd-shift-P": "command_palette::Toggle", "cmd-shift-P": "command_palette::Toggle",
"cmd-shift-M": "diagnostics::Deploy", "cmd-shift-M": "diagnostics::Deploy",
"cmd-shift-E": "project_panel::Toggle", "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 // Bindings from Sublime Text
@ -419,5 +420,11 @@
"tab": "terminal::Tab", "tab": "terminal::Tab",
"cmd-v": "terminal::Paste" "cmd-v": "terminal::Paste"
} }
},
{
"context": "ModalTerminal",
"bindings": {
"escape": "terminal::DeployModal"
}
} }
] ]

View file

@ -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)
}
}
}

View file

@ -1,3 +1,7 @@
pub mod gpui_func_tools;
mod modal;
pub mod terminal_element;
use alacritty_terminal::{ use alacritty_terminal::{
config::{Config, Program, PtyConfig}, config::{Config, Program, PtyConfig},
event::{Event as AlacTermEvent, EventListener, Notify}, event::{Event as AlacTermEvent, EventListener, Notify},
@ -17,6 +21,7 @@ use gpui::{
actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle, actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
ClipboardItem, Entity, MutableAppContext, View, ViewContext, ClipboardItem, Entity, MutableAppContext, View, ViewContext,
}; };
use modal::deploy_modal;
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -37,9 +42,6 @@ const UP_SEQ: &str = "\x1b[A";
const DOWN_SEQ: &str = "\x1b[B"; const DOWN_SEQ: &str = "\x1b[B";
const DEFAULT_TITLE: &str = "Terminal"; const DEFAULT_TITLE: &str = "Terminal";
pub mod gpui_func_tools;
pub mod terminal_element;
///Action for carrying the input to the PTY ///Action for carrying the input to the PTY
#[derive(Clone, Default, Debug, PartialEq, Eq)] #[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct Input(pub String); pub struct Input(pub String);
@ -50,7 +52,22 @@ pub struct ScrollTerminal(pub i32);
actions!( actions!(
terminal, 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]); 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::tab);
cx.add_action(Terminal::paste); cx.add_action(Terminal::paste);
cx.add_action(Terminal::scroll_terminal); cx.add_action(Terminal::scroll_terminal);
cx.add_action(deploy_modal);
} }
///A translation struct for Alacritty to communicate with us from their event loop ///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_new_content: bool,
has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
cur_size: SizeInfo, cur_size: SizeInfo,
modal: bool,
} }
///Upward flowing events, for changing the title and such ///Upward flowing events, for changing the title and such
@ -105,7 +124,7 @@ impl Entity for Terminal {
impl Terminal { impl Terminal {
///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices ///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 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
let (events_tx, mut events_rx) = unbounded(); let (events_tx, mut events_rx) = unbounded();
cx.spawn_weak(|this, mut cx| async move { cx.spawn_weak(|this, mut cx| async move {
@ -172,6 +191,7 @@ impl Terminal {
has_new_content: false, has_new_content: false,
has_bell: false, has_bell: false,
cur_size: size_info, cur_size: size_info,
modal,
} }
} }
@ -218,7 +238,7 @@ impl Terminal {
), ),
AlacTermEvent::ColorRequest(index, format) => { AlacTermEvent::ColorRequest(index, format) => {
let color = self.term.lock().colors()[index].unwrap_or_else(|| { 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 { match index {
0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)), 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 //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()) .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
.map(|wt| wt.abs_path().to_path_buf()); .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 ///Send the shutdown message to Alacritty
@ -367,13 +390,26 @@ impl View for Terminal {
} }
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { 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>) { fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Activate); cx.emit(Event::Activate);
self.has_new_content = false; 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 { impl Item for Terminal {
@ -488,7 +524,7 @@ mod tests {
//and produce noticable output? //and produce noticable output?
#[gpui::test] #[gpui::test]
async fn test_terminal(cx: &mut TestAppContext) { 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.update(cx, |terminal, cx| {
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);

View file

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

View file

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

View file

@ -1,7 +1,8 @@
import Theme from "../themes/common/theme"; import Theme from "../themes/common/theme";
import { border, modalShadow } from "./components";
export default function terminal(theme: Theme) { export default function terminal(theme: Theme) {
return { let colors = {
black: theme.ramps.neutral(0).hex(), black: theme.ramps.neutral(0).hex(),
red: theme.ramps.red(0.5).hex(), red: theme.ramps.red(0.5).hex(),
green: theme.ramps.green(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(), brightWhite: theme.ramps.neutral(7).hex(),
foreground: theme.ramps.neutral(7).hex(), foreground: theme.ramps.neutral(7).hex(),
background: theme.ramps.neutral(0).hex(), background: theme.ramps.neutral(0).hex(),
modalBackground: theme.ramps.neutral(1).hex(),
cursor: theme.ramps.neutral(7).hex(), cursor: theme.ramps.neutral(7).hex(),
dimBlack: theme.ramps.neutral(7).hex(), dimBlack: theme.ramps.neutral(7).hex(),
dimRed: theme.ramps.red(0.75).hex(), dimRed: theme.ramps.red(0.75).hex(),
@ -32,4 +34,16 @@ export default function terminal(theme: Theme) {
brightForeground: theme.ramps.neutral(7).hex(), brightForeground: theme.ramps.neutral(7).hex(),
dimForeground: theme.ramps.neutral(0).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),
}
};
} }