Detailed changes
@@ -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",
@@ -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"
+ }
}
]
@@ -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(),
@@ -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"] }
@@ -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;
+ }
+}
@@ -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);
+ }
+ }
+}
@@ -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,
+}
@@ -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" }
@@ -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();
@@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
- "name": "styles",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
@@ -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),
};
}
@@ -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(),
+ };
+}
@@ -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);
@@ -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,
};
}
@@ -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 };
}