Merge pull request #1255 from zed-industries/terminal-fr

Mikayla Maki created

WIP: Terminal

Change summary

Cargo.lock                              | 189 ++++++++++
assets/keymaps/default.json             |  16 
crates/gpui/src/presenter.rs            |  14 
crates/terminal/Cargo.toml              |  25 +
crates/terminal/src/terminal.rs         | 475 +++++++++++++++++++++++++++
crates/terminal/src/terminal_element.rs | 437 ++++++++++++++++++++++++
crates/theme/src/theme.rs               |  34 +
crates/zed/Cargo.toml                   |   1 
crates/zed/src/main.rs                  |   2 
styles/package-lock.json                |   1 
styles/src/styleTree/app.ts             |   2 
styles/src/styleTree/terminal.ts        |  35 +
styles/src/themes/cave.ts               |   2 
styles/src/themes/common/base16.ts      |  22 
styles/src/themes/common/theme.ts       |   3 
15 files changed, 1,250 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -59,6 +59,45 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alacritty_config_derive"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "alacritty_terminal"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f"
+dependencies = [
+ "alacritty_config_derive",
+ "base64 0.13.0",
+ "bitflags",
+ "dirs 3.0.2",
+ "libc",
+ "log",
+ "mio 0.6.23",
+ "mio-anonymous-pipes",
+ "mio-extras",
+ "miow 0.3.7",
+ "nix",
+ "parking_lot 0.11.2",
+ "regex-automata",
+ "serde",
+ "serde_yaml",
+ "signal-hook",
+ "signal-hook-mio",
+ "unicode-width",
+ "vte",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -2516,6 +2555,12 @@ dependencies = [
  "safemem",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
 [[package]]
 name = "lipsum"
 version = "0.8.2"
@@ -2725,7 +2770,7 @@ dependencies = [
  "kernel32-sys",
  "libc",
  "log",
- "miow",
+ "miow 0.2.2",
  "net2",
  "slab",
  "winapi 0.2.8",
@@ -2743,6 +2788,42 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "mio-anonymous-pipes"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b"
+dependencies = [
+ "mio 0.6.23",
+ "miow 0.3.7",
+ "parking_lot 0.11.2",
+ "spsc-buffer",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "mio-extras"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
+dependencies = [
+ "lazycell",
+ "log",
+ "mio 0.6.23",
+ "slab",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio 0.6.23",
+]
+
 [[package]]
 name = "miow"
 version = "0.2.2"
@@ -2755,6 +2836,15 @@ dependencies = [
  "ws2_32-sys",
 ]
 
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "multimap"
 version = "0.8.3"
@@ -2799,6 +2889,19 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "nix"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "memoffset",
+]
+
 [[package]]
 name = "nom"
 version = "7.1.1"
@@ -4253,6 +4356,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_yaml"
+version = "0.8.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc"
+dependencies = [
+ "indexmap",
+ "ryu",
+ "serde",
+ "yaml-rust",
+]
+
 [[package]]
 name = "servo-fontconfig"
 version = "0.5.1"
@@ -4365,6 +4480,18 @@ dependencies = [
  "signal-hook-registry",
 ]
 
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio 0.6.23",
+ "mio-uds",
+ "signal-hook",
+]
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.0"
@@ -4493,6 +4620,12 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
+[[package]]
+name = "spsc-buffer"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b"
+
 [[package]]
 name = "sqlformat"
 version = "0.1.8"
@@ -4740,6 +4873,24 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "terminal"
+version = "0.1.0"
+dependencies = [
+ "alacritty_terminal",
+ "editor",
+ "futures",
+ "gpui",
+ "mio-extras",
+ "ordered-float",
+ "project",
+ "settings",
+ "smallvec",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "text"
 version = "0.1.0"
@@ -5532,6 +5683,12 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
+[[package]]
+name = "utf8parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
+
 [[package]]
 name = "util"
 version = "0.1.0"
@@ -5617,6 +5774,26 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "vte"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
+dependencies = [
+ "utf8parse",
+ "vte_generate_state_changes",
+]
+
+[[package]]
+name = "vte_generate_state_changes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
 [[package]]
 name = "waker-fn"
 version = "1.1.0"
@@ -5968,6 +6145,15 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
 
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "zed"
 version = "0.42.0"
@@ -6035,6 +6221,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempdir",
+ "terminal",
  "text",
  "theme",
  "theme_selector",

assets/keymaps/default.json 🔗

@@ -403,5 +403,21 @@
             "f2": "project_panel::Rename",
             "backspace": "project_panel::Delete"
         }
+    },
+    {
+        "context": "Terminal",
+        "bindings": {
+            "ctrl-c": "terminal::Sigint",
+            "escape": "terminal::Escape",
+            "ctrl-d": "terminal::Quit",
+            "backspace": "terminal::Del",
+            "enter": "terminal::Return",
+            "left": "terminal::Left",
+            "right": "terminal::Right",
+            "up": "terminal::Up",
+            "down": "terminal::Down",
+            "tab": "terminal::Tab",
+            "cmd-v": "terminal::Paste"
+        }
     }
 ]

crates/gpui/src/presenter.rs 🔗

@@ -703,6 +703,20 @@ impl<'a> EventContext<'a> {
         self.view_stack.last().copied()
     }
 
+    pub fn is_parent_view_focused(&self) -> bool {
+        if let Some(parent_view_id) = self.view_stack.last() {
+            self.app.focused_view_id(self.window_id) == Some(*parent_view_id)
+        } else {
+            false
+        }
+    }
+
+    pub fn focus_parent_view(&mut self) {
+        if let Some(parent_view_id) = self.view_stack.last() {
+            self.app.focus(self.window_id, Some(*parent_view_id))
+        }
+    }
+
     pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
         self.dispatched_actions.push(DispatchDirective {
             dispatcher_view_id: self.view_stack.last().copied(),

crates/terminal/Cargo.toml 🔗

@@ -0,0 +1,25 @@
+[package]
+name = "terminal"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/terminal.rs"
+doctest = false
+
+[dependencies]
+alacritty_terminal = "0.16.1"
+editor = { path = "../editor" }
+util = { path = "../util" }
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+settings = { path = "../settings" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }
+smallvec = { version = "1.6", features = ["union"] }
+mio-extras = "2.0.6"
+futures = "0.3"
+ordered-float = "2.1.1"
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }

crates/terminal/src/terminal.rs 🔗

@@ -0,0 +1,475 @@
+use alacritty_terminal::{
+    config::{Config, Program, PtyConfig},
+    event::{Event as AlacTermEvent, EventListener, Notify},
+    event_loop::{EventLoop, Msg, Notifier},
+    grid::Scroll,
+    sync::FairMutex,
+    term::{color::Rgb as AlacRgb, SizeInfo},
+    tty, Term,
+};
+
+use futures::{
+    channel::mpsc::{unbounded, UnboundedSender},
+    StreamExt,
+};
+use gpui::{
+    actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
+    ClipboardItem, Entity, MutableAppContext, View, ViewContext,
+};
+use project::{Project, ProjectPath};
+use settings::Settings;
+use smallvec::SmallVec;
+use std::{path::PathBuf, sync::Arc};
+use workspace::{Item, Workspace};
+
+use crate::terminal_element::{get_color_at_index, TerminalEl};
+
+//ASCII Control characters on a keyboard
+//Consts -> Structs -> Impls -> Functions, Vaguely in order of importance
+const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
+const TAB_CHAR: char = 9_u8 as char;
+const CARRIAGE_RETURN_CHAR: char = 13_u8 as char;
+const ESC_CHAR: char = 27_u8 as char;
+const DEL_CHAR: char = 127_u8 as char;
+const LEFT_SEQ: &str = "\x1b[D";
+const RIGHT_SEQ: &str = "\x1b[C";
+const UP_SEQ: &str = "\x1b[A";
+const DOWN_SEQ: &str = "\x1b[B";
+const DEFAULT_TITLE: &str = "Terminal";
+
+pub mod terminal_element;
+
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
+pub struct Input(pub String);
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ScrollTerminal(pub i32);
+
+actions!(
+    terminal,
+    [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit]
+);
+impl_internal_actions!(terminal, [Input, ScrollTerminal]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(Terminal::deploy);
+    cx.add_action(Terminal::write_to_pty);
+    cx.add_action(Terminal::send_sigint);
+    cx.add_action(Terminal::escape);
+    cx.add_action(Terminal::quit);
+    cx.add_action(Terminal::del);
+    cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode?
+    cx.add_action(Terminal::left);
+    cx.add_action(Terminal::right);
+    cx.add_action(Terminal::up);
+    cx.add_action(Terminal::down);
+    cx.add_action(Terminal::tab);
+    cx.add_action(Terminal::paste);
+    cx.add_action(Terminal::scroll_terminal);
+}
+
+#[derive(Clone)]
+pub struct ZedListener(UnboundedSender<AlacTermEvent>);
+
+impl EventListener for ZedListener {
+    fn send_event(&self, event: AlacTermEvent) {
+        self.0.unbounded_send(event).ok();
+    }
+}
+
+///A terminal renderer.
+pub struct Terminal {
+    pty_tx: Notifier,
+    term: Arc<FairMutex<Term<ZedListener>>>,
+    title: String,
+    has_new_content: bool,
+    has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
+    cur_size: SizeInfo,
+}
+
+pub enum Event {
+    TitleChanged,
+    CloseTerminal,
+    Activate,
+}
+
+impl Entity for Terminal {
+    type Event = Event;
+}
+
+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 {
+        //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 {
+            while let Some(event) = events_rx.next().await {
+                match this.upgrade(&cx) {
+                    Some(handle) => {
+                        handle.update(&mut cx, |this, cx| {
+                            this.process_terminal_event(event, cx);
+                            cx.notify();
+                        });
+                    }
+                    None => break,
+                }
+            }
+        })
+        .detach();
+
+        let pty_config = PtyConfig {
+            shell: Some(Program::Just("zsh".to_string())),
+            working_directory,
+            hold: false,
+        };
+
+        let config = Config {
+            pty_config: pty_config.clone(),
+            ..Default::default()
+        };
+
+        //The details here don't matter, the terminal will be resized on layout
+        let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false);
+
+        //Set up the terminal...
+        let term = Term::new(&config, size_info, ZedListener(events_tx.clone()));
+        let term = Arc::new(FairMutex::new(term));
+
+        //Setup the pty...
+        let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty");
+
+        //And connect them together
+        let event_loop = EventLoop::new(
+            term.clone(),
+            ZedListener(events_tx.clone()),
+            pty,
+            pty_config.hold,
+            false,
+        );
+
+        //Kick things off
+        let pty_tx = Notifier(event_loop.channel());
+        let _io_thread = event_loop.spawn();
+        Terminal {
+            title: DEFAULT_TITLE.to_string(),
+            term,
+            pty_tx,
+            has_new_content: false,
+            has_bell: false,
+            cur_size: size_info,
+        }
+    }
+
+    ///Takes events from Alacritty and translates them to behavior on this view
+    fn process_terminal_event(
+        &mut self,
+        event: alacritty_terminal::event::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            AlacTermEvent::Wakeup => {
+                if !cx.is_self_focused() {
+                    //Need to figure out how to trigger a redraw when not in focus
+                    self.has_new_content = true; //Change tab content
+                    cx.emit(Event::TitleChanged);
+                } else {
+                    cx.notify()
+                }
+            }
+            AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx),
+            AlacTermEvent::MouseCursorDirty => {
+                //Calculate new cursor style.
+                //TODO
+                //Check on correctly handling mouse events for terminals
+                cx.platform().set_cursor_style(CursorStyle::Arrow); //???
+            }
+            AlacTermEvent::Title(title) => {
+                self.title = title;
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::ResetTitle => {
+                self.title = DEFAULT_TITLE.to_string();
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::ClipboardStore(_, data) => {
+                cx.write_to_clipboard(ClipboardItem::new(data))
+            }
+            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(
+                &Input(format(
+                    &cx.read_from_clipboard()
+                        .map(|ci| ci.text().to_string())
+                        .unwrap_or("".to_string()),
+                )),
+                cx,
+            ),
+            AlacTermEvent::ColorRequest(index, format) => {
+                let color = self.term.lock().colors()[index].unwrap_or_else(|| {
+                    let term_style = &cx.global::<Settings>().theme.terminal;
+                    match index {
+                        0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)),
+                        256 => to_alac_rgb(term_style.foreground),
+                        257 => to_alac_rgb(term_style.background),
+                        258 => to_alac_rgb(term_style.cursor),
+                        259 => to_alac_rgb(term_style.dim_black),
+                        260 => to_alac_rgb(term_style.dim_red),
+                        261 => to_alac_rgb(term_style.dim_green),
+                        262 => to_alac_rgb(term_style.dim_yellow),
+                        263 => to_alac_rgb(term_style.dim_blue),
+                        264 => to_alac_rgb(term_style.dim_magenta),
+                        265 => to_alac_rgb(term_style.dim_cyan),
+                        266 => to_alac_rgb(term_style.dim_white),
+                        267 => to_alac_rgb(term_style.bright_foreground),
+                        268 => to_alac_rgb(term_style.black), //Dim Background, non-standard
+                        _ => AlacRgb { r: 0, g: 0, b: 0 },
+                    }
+                });
+                self.write_to_pty(&Input(format(color)), cx)
+            }
+            AlacTermEvent::CursorBlinkingChange => {
+                //So, it's our job to set a timer and cause the cursor to blink here
+                //Which means that I'm going to put this off until someone @ Zed looks at it
+            }
+            AlacTermEvent::Bell => {
+                self.has_bell = true;
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::Exit => self.quit(&Quit, cx),
+        }
+    }
+
+    fn set_size(&mut self, new_size: SizeInfo) {
+        if new_size != self.cur_size {
+            self.pty_tx.0.send(Msg::Resize(new_size)).ok();
+            self.term.lock().resize(new_size);
+            self.cur_size = new_size;
+        }
+    }
+
+    fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext<Self>) {
+        self.term.lock().scroll_display(Scroll::Delta(scroll.0));
+    }
+
+    ///Create a new Terminal
+    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
+        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());
+
+        workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
+    }
+
+    ///Send the shutdown message to Alacritty
+    fn shutdown_pty(&mut self) {
+        self.pty_tx.0.send(Msg::Shutdown).ok();
+    }
+
+    fn quit(&mut self, _: &Quit, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::CloseTerminal);
+    }
+
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        if let Some(item) = cx.read_from_clipboard() {
+            self.write_to_pty(&Input(item.text().to_owned()), cx);
+        }
+    }
+
+    fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext<Self>) {
+        //iTerm bell behavior, bell stays until terminal is interacted with
+        self.has_bell = false;
+        self.term.lock().scroll_display(Scroll::Bottom);
+        cx.emit(Event::TitleChanged);
+        self.pty_tx.notify(input.0.clone().into_bytes());
+    }
+
+    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(UP_SEQ.to_string()), cx);
+    }
+
+    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx);
+    }
+
+    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(TAB_CHAR.to_string()), cx);
+    }
+
+    fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(ETX_CHAR.to_string()), cx);
+    }
+
+    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(ESC_CHAR.to_string()), cx);
+    }
+
+    fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(DEL_CHAR.to_string()), cx);
+    }
+
+    fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx);
+    }
+
+    fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx);
+    }
+
+    fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx);
+    }
+}
+
+impl Drop for Terminal {
+    fn drop(&mut self) {
+        self.shutdown_pty();
+    }
+}
+
+impl View for Terminal {
+    fn ui_name() -> &'static str {
+        "Terminal"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        TerminalEl::new(cx.handle())
+            .contained()
+            // .with_style(theme.terminal.container)
+            .boxed()
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Activate);
+        self.has_new_content = false;
+    }
+}
+
+impl Item for Terminal {
+    fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
+        let settings = cx.global::<Settings>();
+        let search_theme = &settings.theme.search; //TODO properly integrate themes
+
+        let mut flex = Flex::row();
+
+        if self.has_bell {
+            flex.add_child(
+                Svg::new("icons/zap.svg")
+                    .with_color(tab_theme.label.text.color)
+                    .constrained()
+                    .with_width(search_theme.tab_icon_width)
+                    .aligned()
+                    .boxed(),
+            );
+        };
+
+        flex.with_child(
+            Label::new(self.title.clone(), tab_theme.label.clone())
+                .aligned()
+                .contained()
+                .with_margin_left(if self.has_bell {
+                    search_theme.tab_icon_spacing
+                } else {
+                    0.
+                })
+                .boxed(),
+        )
+        .boxed()
+    }
+
+    fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
+        None
+    }
+
+    fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
+        SmallVec::new()
+    }
+
+    fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
+
+    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save should not have been called");
+    }
+
+    fn save_as(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _abs_path: std::path::PathBuf,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save_as should not have been called");
+    }
+
+    fn reload(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        gpui::Task::ready(Ok(()))
+    }
+
+    fn is_dirty(&self, _: &gpui::AppContext) -> bool {
+        self.has_new_content
+    }
+
+    fn should_update_tab_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::TitleChanged)
+    }
+
+    fn should_close_item_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::CloseTerminal)
+    }
+
+    fn should_activate_item_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::Activate)
+    }
+}
+
+fn to_alac_rgb(color: Color) -> AlacRgb {
+    AlacRgb {
+        r: color.r,
+        g: color.g,
+        b: color.g,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::terminal_element::build_chunks;
+    use gpui::TestAppContext;
+
+    #[gpui::test]
+    async fn test_terminal(cx: &mut TestAppContext) {
+        let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
+
+        terminal.update(cx, |terminal, cx| {
+            terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
+            terminal.carriage_return(&Return, cx);
+        });
+
+        terminal
+            .condition(cx, |terminal, _cx| {
+                let term = terminal.term.clone();
+                let (chunks, _) = build_chunks(
+                    term.lock().renderable_content().display_iter,
+                    &Default::default(),
+                );
+                let content = chunks.iter().map(|e| e.0.trim()).collect::<String>();
+                content.contains("7")
+            })
+            .await;
+    }
+}

crates/terminal/src/terminal_element.rs 🔗

@@ -0,0 +1,437 @@
+use alacritty_terminal::{
+    ansi::Color as AnsiColor,
+    grid::{GridIterator, Indexed},
+    index::Point,
+    term::{
+        cell::{Cell, Flags},
+        SizeInfo,
+    },
+};
+use gpui::{
+    color::Color,
+    elements::*,
+    fonts::{HighlightStyle, TextStyle, Underline},
+    geometry::{rect::RectF, vector::vec2f},
+    json::json,
+    text_layout::Line,
+    Event, MouseRegion, PaintContext, Quad, WeakViewHandle,
+};
+use ordered_float::OrderedFloat;
+use settings::Settings;
+use std::rc::Rc;
+use theme::TerminalStyle;
+
+use crate::{Input, ScrollTerminal, Terminal};
+
+const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
+
+#[cfg(debug_assertions)]
+const DEBUG_GRID: bool = false;
+
+pub struct TerminalEl {
+    view: WeakViewHandle<Terminal>,
+}
+
+impl TerminalEl {
+    pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
+        TerminalEl { view }
+    }
+}
+
+pub struct LayoutState {
+    lines: Vec<Line>,
+    line_height: f32,
+    em_width: f32,
+    cursor: Option<(RectF, Color)>,
+    cur_size: SizeInfo,
+    background_color: Color,
+}
+
+impl Element for TerminalEl {
+    type LayoutState = LayoutState;
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        cx: &mut gpui::LayoutContext,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        let view = self.view.upgrade(cx).unwrap();
+        let size = constraint.max;
+        let settings = cx.global::<Settings>();
+        let editor_theme = &settings.theme.editor;
+        let font_cache = cx.font_cache();
+
+        //Set up text rendering
+        let text_style = TextStyle {
+            color: editor_theme.text_color,
+            font_family_id: settings.buffer_font_family,
+            font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
+            font_id: font_cache
+                .select_font(settings.buffer_font_family, &Default::default())
+                .unwrap(),
+            font_size: settings.buffer_font_size,
+            font_properties: Default::default(),
+            underline: Default::default(),
+        };
+
+        let line_height = font_cache.line_height(text_style.font_size);
+        let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
+
+        let new_size = SizeInfo::new(
+            size.x() - cell_width,
+            size.y(),
+            cell_width,
+            line_height,
+            0.,
+            0.,
+            false,
+        );
+        view.update(cx.app, |view, _cx| {
+            view.set_size(new_size);
+        });
+
+        let settings = cx.global::<Settings>();
+        let terminal_theme = &settings.theme.terminal;
+        let term = view.read(cx).term.lock();
+
+        let content = term.renderable_content();
+        let (chunks, line_count) = build_chunks(content.display_iter, &terminal_theme);
+
+        let shaped_lines = layout_highlighted_chunks(
+            chunks.iter().map(|(text, style)| (text.as_str(), *style)),
+            &text_style,
+            cx.text_layout_cache,
+            &cx.font_cache,
+            usize::MAX,
+            line_count,
+        );
+
+        let cursor_line = content.cursor.point.line.0 + content.display_offset as i32;
+        let mut cursor = None;
+        if let Some(layout_line) = cursor_line
+            .try_into()
+            .ok()
+            .and_then(|cursor_line: usize| shaped_lines.get(cursor_line))
+        {
+            let cursor_x = layout_line.x_for_index(content.cursor.point.column.0);
+            cursor = Some((
+                RectF::new(
+                    vec2f(cursor_x, cursor_line as f32 * line_height),
+                    vec2f(cell_width, line_height),
+                ),
+                terminal_theme.cursor,
+            ));
+        }
+
+        (
+            constraint.max,
+            LayoutState {
+                lines: shaped_lines,
+                line_height,
+                em_width: cell_width,
+                cursor,
+                cur_size: new_size,
+                background_color: terminal_theme.background,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: gpui::geometry::rect::RectF,
+        visible_bounds: gpui::geometry::rect::RectF,
+        layout: &mut Self::LayoutState,
+        cx: &mut gpui::PaintContext,
+    ) -> Self::PaintState {
+        cx.scene.push_layer(Some(visible_bounds));
+
+        cx.scene.push_mouse_region(MouseRegion {
+            view_id: self.view.id(),
+            discriminant: None,
+            bounds: visible_bounds,
+            hover: None,
+            mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
+            click: None,
+            right_mouse_down: None,
+            right_click: None,
+            drag: None,
+            mouse_down_out: None,
+            right_mouse_down_out: None,
+        });
+
+        //Background
+        cx.scene.push_quad(Quad {
+            bounds: visible_bounds,
+            background: Some(layout.background_color),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+
+        let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding
+
+        let mut line_origin = origin;
+        for line in &layout.lines {
+            let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height));
+
+            if boundaries.intersects(visible_bounds) {
+                line.paint(line_origin, visible_bounds, layout.line_height, cx);
+            }
+
+            line_origin.set_y(boundaries.max_y());
+        }
+
+        if let Some((c, color)) = layout.cursor {
+            let new_origin = origin + c.origin();
+            let new_cursor = RectF::new(new_origin, c.size());
+            cx.scene.push_quad(Quad {
+                bounds: new_cursor,
+                background: Some(color),
+                border: Default::default(),
+                corner_radius: 0.,
+            });
+        }
+
+        #[cfg(debug_assertions)]
+        if DEBUG_GRID {
+            draw_debug_grid(bounds, layout, cx);
+        }
+
+        cx.scene.pop_layer();
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &gpui::Event,
+        _bounds: gpui::geometry::rect::RectF,
+        visible_bounds: gpui::geometry::rect::RectF,
+        layout: &mut Self::LayoutState,
+        _paint: &mut Self::PaintState,
+        cx: &mut gpui::EventContext,
+    ) -> bool {
+        match event {
+            Event::ScrollWheel {
+                delta, position, ..
+            } => {
+                if visible_bounds.contains_point(*position) {
+                    let vertical_scroll =
+                        (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER;
+                    cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
+                    true
+                } else {
+                    false
+                }
+            }
+            Event::KeyDown {
+                input: Some(input), ..
+            } => {
+                if cx.is_parent_view_focused() {
+                    cx.dispatch_action(Input(input.to_string()));
+                    true
+                } else {
+                    false
+                }
+            }
+            _ => false,
+        }
+    }
+
+    fn debug(
+        &self,
+        _bounds: gpui::geometry::rect::RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        _cx: &gpui::DebugContext,
+    ) -> gpui::serde_json::Value {
+        json!({
+            "type": "TerminalElement",
+        })
+    }
+}
+
+pub(crate) fn build_chunks(
+    grid_iterator: GridIterator<Cell>,
+    theme: &TerminalStyle,
+) -> (Vec<(String, Option<HighlightStyle>)>, usize) {
+    let mut lines: Vec<(String, Option<HighlightStyle>)> = vec![];
+    let mut last_line = 0;
+    let mut line_count = 1;
+    let mut cur_chunk = String::new();
+
+    let mut cur_highlight = HighlightStyle {
+        color: Some(Color::white()),
+        ..Default::default()
+    };
+
+    for cell in grid_iterator {
+        let Indexed {
+          point: Point { line, .. },
+          cell: Cell {
+              c, fg, flags, .. // TODO: Add bg and flags
+          }, //TODO: Learn what 'CellExtra does'
+      } = cell;
+
+        let new_highlight = make_style_from_cell(fg, flags, theme);
+
+        if line != last_line {
+            line_count += 1;
+            cur_chunk.push('\n');
+            last_line = line.0;
+        }
+
+        if new_highlight != cur_highlight {
+            lines.push((cur_chunk.clone(), Some(cur_highlight.clone())));
+            cur_chunk.clear();
+            cur_highlight = new_highlight;
+        }
+        cur_chunk.push(*c)
+    }
+    lines.push((cur_chunk, Some(cur_highlight)));
+    (lines, line_count)
+}
+
+fn make_style_from_cell(fg: &AnsiColor, flags: &Flags, style: &TerminalStyle) -> HighlightStyle {
+    let fg = Some(alac_color_to_gpui_color(fg, style));
+    let underline = if flags.contains(Flags::UNDERLINE) {
+        Some(Underline {
+            color: fg,
+            squiggly: false,
+            thickness: OrderedFloat(1.),
+        })
+    } else {
+        None
+    };
+    HighlightStyle {
+        color: fg,
+        underline,
+        ..Default::default()
+    }
+}
+
+fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> Color {
+    match allac_color {
+        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,
+        }, //Theme defined
+        alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, 1),
+        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), //Color cube weirdness
+    }
+}
+
+pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
+    match index {
+        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,
+        16..=231 => {
+            let (r, g, b) = rgb_for_index(index); //Split the index into it's rgb components
+            let step = (u8::MAX as f32 / 5.).round() as u8; //Split the GPUI range into 5 chunks
+            Color::new(r * step, g * step, b * step, 1) //Map the rgb components to GPUI's range
+        }
+        //Grayscale from black to white, 0 to 24
+        232..=255 => {
+            let i = 24 - (index - 232); //Align index to 24..0
+            let step = (u8::MAX as f32 / 24.).round() as u8; //Split the 256 range grayscale into 24 chunks
+            Color::new(i * step, i * step, i * step, 1) //Map the rgb components to GPUI's range
+        }
+    }
+}
+
+///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
+///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
+///
+///Wikipedia gives a formula for calculating the index for a given color:
+///
+///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
+///
+///This function does the reverse, calculating the r, g, and b components from a given index.
+fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
+    debug_assert!(i >= &16 && i <= &231);
+    let i = i - 16;
+    let r = (i - (i % 36)) / 36;
+    let g = ((i % 36) - (i % 6)) / 6;
+    let b = (i % 36) % 6;
+    (r, g, b)
+}
+
+#[cfg(debug_assertions)]
+fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
+    let width = layout.cur_size.width();
+    let height = layout.cur_size.height();
+    //Alacritty uses 'as usize', so shall we.
+    for col in 0..(width / layout.em_width).round() as usize {
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(
+                bounds.origin() + vec2f((col + 1) as f32 * layout.em_width, 0.),
+                vec2f(1., height),
+            ),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+    }
+    for row in 0..((height / layout.line_height) + 1.0).round() as usize {
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(
+                bounds.origin() + vec2f(layout.em_width, row as f32 * layout.line_height),
+                vec2f(width, 1.),
+            ),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_rgb_for_index() {
+        //Test every possible value in the color cube
+        for i in 16..=231 {
+            let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
+            assert_eq!(i, 16 + 36 * r + 6 * g + b);
+        }
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -33,6 +33,7 @@ pub struct Theme {
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
     pub tooltip: TooltipStyle,
+    pub terminal: TerminalStyle,
 }
 
 #[derive(Deserialize, Default)]
@@ -633,3 +634,36 @@ pub struct HoverPopover {
     pub prose: TextStyle,
     pub highlight: Color,
 }
+
+#[derive(Clone, Deserialize, Default)]
+pub struct TerminalStyle {
+    pub black: Color,
+    pub red: Color,
+    pub green: Color,
+    pub yellow: Color,
+    pub blue: Color,
+    pub magenta: Color,
+    pub cyan: Color,
+    pub white: Color,
+    pub bright_black: Color,
+    pub bright_red: Color,
+    pub bright_green: Color,
+    pub bright_yellow: Color,
+    pub bright_blue: Color,
+    pub bright_magenta: Color,
+    pub bright_cyan: Color,
+    pub bright_white: Color,
+    pub foreground: Color,
+    pub background: Color,
+    pub cursor: Color,
+    pub dim_black: Color,
+    pub dim_red: Color,
+    pub dim_green: Color,
+    pub dim_yellow: Color,
+    pub dim_blue: Color,
+    pub dim_magenta: Color,
+    pub dim_cyan: Color,
+    pub dim_white: Color,
+    pub bright_foreground: Color,
+    pub dim_foreground: Color,
+}

crates/zed/Cargo.toml 🔗

@@ -46,6 +46,7 @@ rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
+terminal = { path = "../terminal" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }

crates/zed/src/main.rs 🔗

@@ -36,6 +36,7 @@ use std::{
     thread,
     time::Duration,
 };
+use terminal;
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::{ResultExt, TryFutureExt};
 use workspace::{self, AppState, NewFile, OpenPaths};
@@ -181,6 +182,7 @@ fn main() {
         diagnostics::init(cx);
         search::init(cx);
         vim::init(cx);
+        terminal::init(cx);
 
         let db = cx.background().block(db);
         let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();

styles/package-lock.json 🔗

@@ -5,7 +5,6 @@
     "requires": true,
     "packages": {
         "": {
-            "name": "styles",
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {

styles/src/styleTree/app.ts 🔗

@@ -14,6 +14,7 @@ import projectDiagnostics from "./projectDiagnostics";
 import contactNotification from "./contactNotification";
 import updateNotification from "./updateNotification";
 import tooltip from "./tooltip";
+import terminal from "./terminal";
 
 export const panel = {
   padding: { top: 12, bottom: 12 },
@@ -41,5 +42,6 @@ export default function app(theme: Theme): Object {
     contactNotification: contactNotification(theme),
     updateNotification: updateNotification(theme),
     tooltip: tooltip(theme),
+    terminal: terminal(theme),
   };
 }

styles/src/styleTree/terminal.ts 🔗

@@ -0,0 +1,35 @@
+import Theme from "../themes/common/theme";
+
+export default function terminal(theme: Theme) {
+  return {
+    black: theme.ramps.neutral(0).hex(),
+    red: theme.ramps.red(0.5).hex(),
+    green: theme.ramps.green(0.5).hex(),
+    yellow: theme.ramps.yellow(0.5).hex(),
+    blue: theme.ramps.blue(0.5).hex(),
+    magenta: theme.ramps.magenta(0.5).hex(),
+    cyan: theme.ramps.cyan(0.5).hex(),
+    white: theme.ramps.neutral(7).hex(),
+    brightBlack: theme.ramps.neutral(2).hex(),
+    brightRed: theme.ramps.red(0.25).hex(),
+    brightGreen: theme.ramps.green(0.25).hex(),
+    brightYellow: theme.ramps.yellow(0.25).hex(),
+    brightBlue: theme.ramps.blue(0.25).hex(),
+    brightMagenta: theme.ramps.magenta(0.25).hex(),
+    brightCyan: theme.ramps.cyan(0.25).hex(),
+    brightWhite: theme.ramps.neutral(7).hex(),
+    foreground: theme.ramps.neutral(7).hex(),
+    background: theme.ramps.neutral(0).hex(),
+    cursor: theme.ramps.neutral(7).hex(),
+    dimBlack: theme.ramps.neutral(7).hex(),
+    dimRed: theme.ramps.red(0.75).hex(),
+    dimGreen: theme.ramps.green(0.75).hex(),
+    dimYellow: theme.ramps.yellow(0.75).hex(),
+    dimBlue: theme.ramps.blue(0.75).hex(),
+    dimMagenta: theme.ramps.magenta(0.75).hex(),
+    dimCyan: theme.ramps.cyan(0.75).hex(),
+    dimWhite: theme.ramps.neutral(5).hex(),
+    brightForeground: theme.ramps.neutral(7).hex(),
+    dimForeground: theme.ramps.neutral(0).hex(),
+  };
+}

styles/src/themes/cave.ts 🔗

@@ -25,4 +25,4 @@ const ramps = {
 };
 
 export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/common/base16.ts 🔗

@@ -13,15 +13,25 @@ export function colorRamp(color: Color): Scale {
 export function createTheme(
   name: string,
   isLight: boolean,
-  ramps: { [rampName: string]: Scale },
+  color_ramps: { [rampName: string]: Scale },
 ): Theme {
+  let ramps: typeof color_ramps = {};
+  // Chromajs mutates the underlying ramp when you call domain. This causes problems because
+  // we now store the ramps object in the theme so that we can pull colors out of them. 
+  // So instead of calling domain and storing the result, we have to construct new ramps for each
+  // theme so that we don't modify the passed in ramps.
+  // This combined with an error in the type definitions for chroma js means we have to cast the colors
+  // function to any in order to get the colors back out from the original ramps.
   if (isLight) {
-    for (var rampName in ramps) {
-      ramps[rampName] = ramps[rampName].domain([1, 0]);
+    for (var rampName in color_ramps) {
+      ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([1, 0]);
     }
-    ramps.neutral = ramps.neutral.domain([7, 0]);
+    ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([7, 0]);
   } else {
-    ramps.neutral = ramps.neutral.domain([0, 7]);
+    for (var rampName in color_ramps) {
+      ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([0, 1]);
+    }
+    ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([0, 7]);
   }
 
   let blend = isLight ? 0.12 : 0.24;
@@ -237,6 +247,7 @@ export function createTheme(
 
   return {
     name,
+    isLight,
     backgroundColor,
     borderColor,
     textColor,
@@ -245,5 +256,6 @@ export function createTheme(
     syntax,
     player,
     shadow,
+    ramps,
   };
 }

styles/src/themes/common/theme.ts 🔗

@@ -1,3 +1,4 @@
+import { Scale } from "chroma-js";
 import { FontWeight } from "../../common";
 import { withOpacity } from "../../utils/color";
 
@@ -60,6 +61,7 @@ export interface Syntax {
 
 export default interface Theme {
   name: string;
+  isLight: boolean,
   backgroundColor: {
     // Basically just Title Bar
     // Lowest background level
@@ -155,4 +157,5 @@ export default interface Theme {
     8: Player;
   },
   shadow: string;
+  ramps: { [rampName: string]: Scale };
 }