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

WIP: Terminal
This commit is contained in:
Mikayla Maki 2022-06-30 11:30:28 -07:00 committed by GitHub
commit 2ee57c1512
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1250 additions and 8 deletions

189
Cargo.lock generated
View file

@ -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",

View file

@ -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"
}
}
]

View file

@ -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(),

View file

@ -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"] }

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

@ -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(),
};
}

View file

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

View file

@ -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,
};
}

View file

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