diff --git a/Cargo.lock b/Cargo.lock index 88e9dc731a..1f67be5e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "editor", "futures", "gpui", + "itertools", "mio-extras", "ordered-float", "project", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 175c741421..0bbc056922 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -20,6 +20,8 @@ smallvec = { version = "1.6", features = ["union"] } mio-extras = "2.0.6" futures = "0.3" ordered-float = "2.1.1" +itertools = "0.10" + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/print256color.sh b/crates/terminal/print256color.sh new file mode 100755 index 0000000000..99e3d8c9f9 --- /dev/null +++ b/crates/terminal/print256color.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Tom Hale, 2016. MIT Licence. +# Print out 256 colours, with each number printed in its corresponding colour +# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163 + +set -eu # Fail on errors or undeclared variables + +printable_colours=256 + +# Return a colour that contrasts with the given colour +# Bash only does integer division, so keep it integral +function contrast_colour { + local r g b luminance + colour="$1" + + if (( colour < 16 )); then # Initial 16 ANSI colours + (( colour == 0 )) && printf "15" || printf "0" + return + fi + + # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8 + if (( colour > 231 )); then # Greyscale ramp + (( colour < 244 )) && printf "15" || printf "0" + return + fi + + # All other colours: + # 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5] + # See http://stackoverflow.com/a/27165165/5353461 + + # r=$(( (colour-16) / 36 )) + g=$(( ((colour-16) % 36) / 6 )) + # b=$(( (colour-16) % 6 )) + + # If luminance is bright, print number in black, white otherwise. + # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601 + (( g > 2)) && printf "0" || printf "15" + return + + # Uncomment the below for more precise luminance calculations + + # # Calculate percieved brightness + # # See https://www.w3.org/TR/AERT#color-contrast + # # and http://www.itu.int/rec/R-REC-BT.601 + # # Luminance is in range 0..5000 as each value is 0..5 + # luminance=$(( (r * 299) + (g * 587) + (b * 114) )) + # (( $luminance > 2500 )) && printf "0" || printf "15" +} + +# Print a coloured block with the number of that colour +function print_colour { + local colour="$1" contrast + contrast=$(contrast_colour "$1") + printf "\e[48;5;%sm" "$colour" # Start block of colour + printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number + printf "\e[0m " # Reset colour +} + +# Starting at $1, print a run of $2 colours +function print_run { + local i + for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do + print_colour "$i" + done + printf " " +} + +# Print blocks of colours +function print_blocks { + local start="$1" i + local end="$2" # inclusive + local block_cols="$3" + local block_rows="$4" + local blocks_per_line="$5" + local block_length=$((block_cols * block_rows)) + + # Print sets of blocks + for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do + printf "\n" # Space before each set of blocks + # For each block row + for (( row = 0; row < block_rows; row++ )) do + # Print block columns for all blocks on the line + for (( block = 0; block < blocks_per_line; block++ )) do + print_run $(( i + (block * block_length) )) "$block_cols" + done + (( i += block_cols )) # Prepare to print the next row + printf "\n" + done + done +} + +print_run 0 16 # The first 16 colours are spread over the whole spectrum +printf "\n" +print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive +print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6e62ce2a9f..984f883746 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -25,7 +25,6 @@ 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; @@ -39,9 +38,11 @@ const DEFAULT_TITLE: &str = "Terminal"; pub mod terminal_element; +///Action for carrying the input to the PTY #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Input(pub String); +///Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] pub struct ScrollTerminal(pub i32); @@ -51,6 +52,7 @@ actions!( ); impl_internal_actions!(terminal, [Input, ScrollTerminal]); +///Initialize and register all of our action handlers pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::deploy); cx.add_action(Terminal::write_to_pty); @@ -68,6 +70,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Terminal::scroll_terminal); } +///A translation struct for Alacritty to communicate with us from their event loop #[derive(Clone)] pub struct ZedListener(UnboundedSender); @@ -77,7 +80,7 @@ impl EventListener for ZedListener { } } -///A terminal renderer. +///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct Terminal { pty_tx: Notifier, term: Arc>>, @@ -87,6 +90,7 @@ pub struct Terminal { cur_size: SizeInfo, } +///Upward flowing events, for changing the title and such pub enum Event { TitleChanged, CloseTerminal, @@ -128,7 +132,8 @@ impl Terminal { ..Default::default() }; - //The details here don't matter, the terminal will be resized on layout + //The details here don't matter, the terminal will be resized on the first layout + //Set to something small for easier debugging let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false); //Set up the terminal... @@ -169,7 +174,6 @@ impl Terminal { 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 { @@ -207,6 +211,7 @@ impl Terminal { let term_style = &cx.global::().theme.terminal; match index { 0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)), + //These additional values are required to match the Alacritty Colors object's behavior 256 => to_alac_rgb(term_style.foreground), 257 => to_alac_rgb(term_style.background), 258 => to_alac_rgb(term_style.cursor), @@ -226,8 +231,7 @@ impl Terminal { 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 + //TODO: Set a timer to blink the cursor on and off } AlacTermEvent::Bell => { self.has_bell = true; @@ -237,6 +241,7 @@ impl Terminal { } } + ///Resize the terminal and the PTY. This locks the terminal. fn set_size(&mut self, new_size: SizeInfo) { if new_size != self.cur_size { self.pty_tx.0.send(Msg::Resize(new_size)).ok(); @@ -245,18 +250,20 @@ impl Terminal { } } + ///Scroll the terminal. This locks the terminal fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext) { self.term.lock().scroll_display(Scroll::Delta(scroll.0)); } - ///Create a new Terminal + ///Create a new Terminal in the current working directory or the user's home directory fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { 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()); + .map(|wt| wt.abs_path().to_path_buf()) + .or_else(|| Some("~".into())); workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx); } @@ -266,16 +273,19 @@ impl Terminal { self.pty_tx.0.send(Msg::Shutdown).ok(); } + ///Tell Zed to close us fn quit(&mut self, _: &Quit, cx: &mut ViewContext) { cx.emit(Event::CloseTerminal); } + ///Attempt to paste the clipboard into the terminal fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { if let Some(item) = cx.read_from_clipboard() { self.write_to_pty(&Input(item.text().to_owned()), cx); } } + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext) { //iTerm bell behavior, bell stays until terminal is interacted with self.has_bell = false; @@ -284,38 +294,47 @@ impl Terminal { self.pty_tx.notify(input.0.clone().into_bytes()); } + ///Send the `up` key fn up(&mut self, _: &Up, cx: &mut ViewContext) { self.write_to_pty(&Input(UP_SEQ.to_string()), cx); } + ///Send the `down` key fn down(&mut self, _: &Down, cx: &mut ViewContext) { self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); } + ///Send the `tab` key fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); } + ///Send `SIGINT` (`ctrl-c`) fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext) { self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); } + ///Send the `escape` key fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { self.write_to_pty(&Input(ESC_CHAR.to_string()), cx); } + ///Send the `delete` key. TODO: Difference between this and backspace? fn del(&mut self, _: &Del, cx: &mut ViewContext) { self.write_to_pty(&Input(DEL_CHAR.to_string()), cx); } + ///Send a carriage return. TODO: May need to check the terminal mode. fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext) { self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx); } + //Send the `left` key fn left(&mut self, _: &Left, cx: &mut ViewContext) { self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx); } + //Send the `right` key fn right(&mut self, _: &Right, cx: &mut ViewContext) { self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); } @@ -333,10 +352,7 @@ impl View for Terminal { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - TerminalEl::new(cx.handle()) - .contained() - // .with_style(theme.terminal.container) - .boxed() + TerminalEl::new(cx.handle()).contained().boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -354,7 +370,7 @@ impl Item for Terminal { if self.has_bell { flex.add_child( - Svg::new("icons/zap.svg") + Svg::new("icons/zap.svg") //TODO: Swap out for a better icon, or at least resize this .with_color(tab_theme.label.text.color) .constrained() .with_width(search_theme.tab_icon_width) @@ -437,6 +453,7 @@ impl Item for Terminal { } } +//Convenience method for less lines fn to_alac_rgb(color: Color) -> AlacRgb { AlacRgb { r: color.r, @@ -451,6 +468,8 @@ mod tests { use crate::terminal_element::build_chunks; use gpui::TestAppContext; + ///Basic integration test, can we get the terminal to show up, execute a command, + //and produce noticable output? #[gpui::test] async fn test_terminal(cx: &mut TestAppContext) { let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None)); diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index d81292d0c2..ac964dca05 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -14,39 +14,78 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, json::json, text_layout::Line, - Event, MouseRegion, PaintContext, Quad, WeakViewHandle, + Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, }; +use itertools::Itertools; use ordered_float::OrderedFloat; use settings::Settings; -use std::rc::Rc; +use std::{iter, rc::Rc}; use theme::TerminalStyle; use crate::{Input, ScrollTerminal, Terminal}; +///Scrolling is unbearably sluggish by default. Alacritty supports a configurable +///Scroll multiplier that is set to 3 by default. This will be removed when I +///Implement scroll bars. const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; +///Used to display the grid as passed to Alacritty and the TTY. +///Useful for debugging inconsistencies between behavior and display #[cfg(debug_assertions)] const DEBUG_GRID: bool = false; +///The GPUI element that paints the terminal. pub struct TerminalEl { view: WeakViewHandle, } +///Represents a span of cells in a single line in the terminal's grid. +///This is used for drawing background rectangles +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct RectSpan { + start: i32, + end: i32, + line: usize, + color: Color, +} + +///A background color span +impl RectSpan { + ///Creates a new LineSpan. `start` must be <= `end`. + ///If `start` == `end`, then this span is considered to be over a + /// single cell + fn new(start: i32, end: i32, line: usize, color: Color) -> RectSpan { + debug_assert!(start <= end); + RectSpan { + start, + end, + line, + color, + } + } +} + +///Helper types so I don't mix these two up +struct CellWidth(f32); +struct LineHeight(f32); + +///The information generated during layout that is nescessary for painting +pub struct LayoutState { + lines: Vec, + line_height: LineHeight, + em_width: CellWidth, + cursor: Option<(RectF, Color)>, + cur_size: SizeInfo, + background_color: Color, + background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan +} + impl TerminalEl { pub fn new(view: WeakViewHandle) -> TerminalEl { TerminalEl { view } } } -pub struct LayoutState { - lines: Vec, - 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 = (); @@ -56,73 +95,57 @@ impl Element for TerminalEl { 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::(); - 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, + //Settings immutably borrows cx here for the settings and font cache + //and we need to modify the cx to resize the terminal. So instead of + //storing Settings or the font_cache(), we toss them ASAP and then reborrow later + let text_style = make_text_style(cx.font_cache(), cx.global::()); + let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size)); + let cell_width = CellWidth( + cx.font_cache() + .em_advance(text_style.font_id, text_style.font_size), ); - view.update(cx.app, |view, _cx| { - view.set_size(new_size); - }); + let view_handle = self.view.upgrade(cx).unwrap(); - let settings = cx.global::(); - let terminal_theme = &settings.theme.terminal; - let term = view.read(cx).term.lock(); + //Tell the view our new size. Requires a mutable borrow of cx and the view + let cur_size = make_new_size(constraint, &cell_width, &line_height); + //Note that set_size locks and mutates the terminal. + //TODO: Would be nice to lock once for the whole of layout + view_handle.update(cx.app, |view, _cx| view.set_size(cur_size)); + //Now that we're done with the mutable portion, grab the immutable settings and view again + let terminal_theme = &(cx.global::()).theme.terminal; + let term = view_handle.read(cx).term.lock(); let content = term.renderable_content(); + + //And we're off! Begin layouting 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)), + chunks + .iter() + .map(|(text, style, _)| (text.as_str(), *style)), &text_style, cx.text_layout_cache, - &cx.font_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, - )); - } + let backgrounds = chunks + .iter() + .filter(|(_, _, line_span)| line_span != &RectSpan::default()) + .map(|(_, _, line_span)| *line_span) + .collect(); + let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); + + let cursor = make_cursor_rect( + content.cursor.point, + &shaped_lines, + content.display_offset, + &line_height, + &cell_width, + ) + .map(|cursor_rect| (cursor_rect, terminal_theme.cursor)); ( constraint.max, @@ -131,7 +154,8 @@ impl Element for TerminalEl { line_height, em_width: cell_width, cursor, - cur_size: new_size, + cur_size, + background_rects, background_color: terminal_theme.background, }, ) @@ -144,48 +168,53 @@ impl Element for TerminalEl { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { + //Setup element stuff cx.scene.push_layer(Some(visible_bounds)); + //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse 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, + bounds: visible_bounds, + ..Default::default() }); - //Background + let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + + //Start us off with a nice simple background color cx.scene.push_quad(Quad { - bounds: visible_bounds, + bounds: RectF::new(bounds.origin(), bounds.size()), background: Some(layout.background_color), border: Default::default(), corner_radius: 0., }); - let origin = bounds.origin() + vec2f(layout.em_width, 0.); //Padding + //Draw cell backgrounds + for background_rect in &layout.background_rects { + let new_origin = origin + background_rect.0.origin(); + cx.scene.push_quad(Quad { + bounds: RectF::new(new_origin, background_rect.0.size()), + background: Some(background_rect.1), + border: Default::default(), + corner_radius: 0., + }) + } - let mut line_origin = origin; + //Draw text + let mut line_origin = origin.clone(); for line in &layout.lines { - let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height)); - + let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0)); if boundaries.intersects(visible_bounds) { - line.paint(line_origin, visible_bounds, layout.line_height, cx); + line.paint(line_origin, visible_bounds, layout.line_height.0, cx); } - line_origin.set_y(boundaries.max_y()); } + //Draw cursor 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, + bounds: RectF::new(new_origin, c.size()), background: Some(color), border: Default::default(), corner_radius: 0., @@ -212,26 +241,22 @@ impl Element for TerminalEl { match event { Event::ScrollWheel { delta, position, .. - } => { - if visible_bounds.contains_point(*position) { + } => visible_bounds + .contains_point(*position) + .then(|| { let vertical_scroll = - (delta.y() / layout.line_height) * ALACRITTY_SCROLL_MULTIPLIER; + (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); - true - } else { - false - } - } + }) + .is_some(), Event::KeyDown { input: Some(input), .. - } => { - if cx.is_parent_view_focused() { + } => cx + .is_parent_view_focused() + .then(|| { cx.dispatch_action(Input(input.to_string())); - true - } else { - false - } - } + }) + .is_some(), _ => false, } } @@ -249,67 +274,149 @@ impl Element for TerminalEl { } } +fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + TextStyle { + color: settings.theme.editor.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(), + } +} + +fn make_new_size( + constraint: SizeConstraint, + cell_width: &CellWidth, + line_height: &LineHeight, +) -> SizeInfo { + SizeInfo::new( + constraint.max.x() - cell_width.0, + constraint.max.y(), + cell_width.0, + line_height.0, + 0., + 0., + false, + ) +} + +///In a single pass, this function generates the background and foreground color info for every item in the grid. pub(crate) fn build_chunks( grid_iterator: GridIterator, theme: &TerminalStyle, -) -> (Vec<(String, Option)>, usize) { - let mut lines: Vec<(String, Option)> = 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 { +) -> (Vec<(String, Option, RectSpan)>, usize) { + let mut line_count: usize = 0; + //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so + //rust knows where to put everything. + //Start by grouping by lines + let lines = grid_iterator.group_by(|i| i.point.line.0); + let result = lines + .into_iter() + .map(|(_, line)| { line_count += 1; - cur_chunk.push('\n'); - last_line = line.0; - } + let mut col_index = 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.), + //Then group by style + let chunks = line.group_by(|i| cell_style(&i, theme)); + chunks + .into_iter() + .map(|(style, fragment)| { + //And assemble the styled fragment into it's background and foreground information + let str_fragment = fragment.map(|indexed| indexed.c).collect::(); + let start = col_index; + let end = start + str_fragment.len() as i32; + col_index = end; + ( + str_fragment, + Some(style.0), + RectSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index + ) + }) + //Add a \n to the end, as we're using text layouting rather than grid layouts + .chain(iter::once(("\n".to_string(), None, Default::default()))) + .collect::, RectSpan)>>() }) - } else { - None - }; - HighlightStyle { - color: fg, - underline, - ..Default::default() - } + //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) + .flatten() + .collect::, RectSpan)>>(); + (result, line_count) } -fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> Color { - match allac_color { +///Convert a RectSpan in terms of character offsets, into RectFs of exact offsets +fn make_background_rects( + backgrounds: Vec, + shaped_lines: &Vec, + line_height: &LineHeight, +) -> Vec<(RectF, Color)> { + backgrounds + .into_iter() + .map(|line_span| { + //This should always be safe, as the shaped lines and backgrounds where derived + //At the same time earlier + let line = shaped_lines + .get(line_span.line) + .expect("Background line_num did not correspond to a line number"); + let x = line.x_for_index(line_span.start as usize); + let width = line.x_for_index(line_span.end as usize) - x; + ( + RectF::new( + vec2f(x, line_span.line as f32 * line_height.0), + vec2f(width, line_height.0), + ), + line_span.color, + ) + }) + .collect::>() +} + +///Create the rectangle for a cursor, exactly positioned according to the text +fn make_cursor_rect( + cursor_point: Point, + shaped_lines: &Vec, + display_offset: usize, + line_height: &LineHeight, + cell_width: &CellWidth, +) -> Option { + let cursor_line = cursor_point.line.0 as usize + display_offset; + shaped_lines.get(cursor_line).map(|layout_line| { + let cursor_x = layout_line.x_for_index(cursor_point.column.0); + RectF::new( + vec2f(cursor_x, cursor_line as f32 * line_height.0), + vec2f(cell_width.0, line_height.0), + ) + }) +} + +///Convert the Alacritty cell styles to GPUI text styles and background color +fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) { + let flags = indexed.cell.flags; + let fg = Some(convert_color(&indexed.cell.fg, style)); + let bg = convert_color(&indexed.cell.bg, style); + + let underline = flags.contains(Flags::UNDERLINE).then(|| Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }); + + ( + HighlightStyle { + color: fg, + underline, + ..Default::default() + }, + bg, + ) +} + +///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { + match alac_color { + //Named and theme defined colors alacritty_terminal::ansi::Color::Named(n) => match n { alacritty_terminal::ansi::NamedColor::Black => style.black, alacritty_terminal::ansi::NamedColor::Red => style.red, @@ -340,14 +447,18 @@ fn alac_color_to_gpui_color(allac_color: &AnsiColor, style: &TerminalStyle) -> C 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 + }, + //'True' colors + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), + //8 bit, indexed colors + alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), } } +///Converts an 8 bit ANSI color to it's GPUI equivalent. pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { match index { + //0-15 are the same as the named colors above 0 => style.black, 1 => style.red, 2 => style.green, @@ -364,16 +475,17 @@ pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { 13 => style.bright_magenta, 14 => style.bright_cyan, 15 => style.bright_white, + //16-231 are mapped to their RGB colors on a 0-5 range per channel 16..=231 => { - let (r, g, b) = rgb_for_index(index); //Split the index into it's 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 + let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow + Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color } - //Grayscale from black to white, 0 to 24 + //232-255 are a 24 step grayscale from black to white 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 + let i = index - 232; //Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks + Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale } } } @@ -395,15 +507,17 @@ fn rgb_for_index(i: &u8) -> (u8, u8, u8) { (r, g, b) } +///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between +///Display and conceptual grid. #[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 { + for col in 0..(width / layout.em_width.0).round() as usize { cx.scene.push_quad(Quad { bounds: RectF::new( - bounds.origin() + vec2f((col + 1) as f32 * layout.em_width, 0.), + bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.), vec2f(1., height), ), background: Some(Color::green()), @@ -411,10 +525,10 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex corner_radius: 0., }); } - for row in 0..((height / layout.line_height) + 1.0).round() as usize { + for row in 0..((height / layout.line_height.0) + 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), + bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0), vec2f(width, 1.), ), background: Some(Color::green()), diff --git a/crates/terminal/truecolor.sh b/crates/terminal/truecolor.sh new file mode 100755 index 0000000000..14e5d81308 --- /dev/null +++ b/crates/terminal/truecolor.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copied from: https://unix.stackexchange.com/a/696756 +# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213 + +awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{ + s="/\\"; + total_cols=term_cols*term_lines; + for (colnum = 0; colnum255) g = 510-g; + printf "\033[48;2;%d;%d;%dm", r,g,b; + printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b; + printf "%s\033[0m", substr(s,colnum%2+1,1); + if (colnum%term_cols==term_cols) printf "\n"; + } + printf "\n"; +}' \ No newline at end of file