Detailed changes
@@ -102,6 +102,19 @@
//
//
"working_directory": "current_project_directory",
+ //Set the cursor blinking behavior in the terminal.
+ //May take 4 values:
+ // 1. Never blink the cursor, ignoring the terminal mode
+ // "blinking": "never",
+ // 2. Default the cursor blink to off, but allow the terminal to
+ // turn blinking on
+ // "blinking": "off",
+ // 3. Default the cursor blink to on, but allow the terminal to
+ // turn blinking off
+ // "blinking": "on",
+ // 4. Always blink the cursor, ignoring the terminal mode
+ // "blinking": "always",
+ "blinking": "on",
//Any key-value pairs added to this list will be added to the terminal's
//enviroment. Use `:` to seperate multiple values.
"env": {
@@ -1753,6 +1753,7 @@ pub enum CursorShape {
Bar,
Block,
Underscore,
+ Hollow,
}
impl Default for CursorShape {
@@ -1808,8 +1809,19 @@ impl Cursor {
self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0),
vec2f(self.block_width, 2.0),
),
+ CursorShape::Hollow => RectF::new(
+ self.origin + origin + Vector2F::new(0.0, self.line_height - 1.0),
+ vec2f(self.block_width, 1.0),
+ ),
};
+ //Draw text under the hollow block if need be
+ if matches!(self.shape, CursorShape::Hollow) {
+ if let Some(block_text) = &self.block_text {
+ block_text.paint(self.origin + origin, bounds, self.line_height, cx);
+ }
+ }
+
cx.scene.push_quad(Quad {
bounds,
background: Some(self.color),
@@ -1817,8 +1829,38 @@ impl Cursor {
corner_radius: 0.,
});
- if let Some(block_text) = &self.block_text {
- block_text.paint(self.origin + origin, bounds, self.line_height, cx);
+ if matches!(self.shape, CursorShape::Hollow) {
+ //Top
+ cx.scene.push_quad(Quad {
+ bounds: RectF::new(
+ self.origin + origin + Vector2F::new(0.0, -1.0),
+ vec2f(self.block_width + 1., 1.0),
+ ),
+ background: Some(self.color),
+ border: Border::new(0., Color::black()),
+ corner_radius: 0.,
+ });
+ //Left
+ cx.scene.push_quad(Quad {
+ bounds: RectF::new(self.origin + origin, vec2f(1.0, self.line_height)),
+ background: Some(self.color),
+ border: Border::new(0., Color::black()),
+ corner_radius: 0.,
+ });
+ //Right
+ cx.scene.push_quad(Quad {
+ bounds: RectF::new(
+ self.origin + origin + vec2f(self.block_width, 0.),
+ vec2f(1.0, self.line_height),
+ ),
+ background: Some(self.color),
+ border: Border::new(0., Color::black()),
+ corner_radius: 0.,
+ });
+ } else {
+ if let Some(block_text) = &self.block_text {
+ block_text.paint(self.origin + origin, bounds, self.line_height, cx);
+ }
}
}
}
@@ -83,6 +83,22 @@ pub struct TerminalSettings {
pub font_size: Option<f32>,
pub font_family: Option<String>,
pub env: Option<HashMap<String, String>>,
+ pub blinking: Option<TerminalBlink>,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalBlink {
+ Never,
+ On,
+ Off,
+ Always,
+}
+
+impl Default for TerminalBlink {
+ fn default() -> Self {
+ TerminalBlink::On
+ }
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -21,7 +21,7 @@ use gpui::{
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
-use settings::Settings;
+use settings::{Settings, TerminalBlink};
use theme::TerminalStyle;
use util::ResultExt;
@@ -201,6 +201,7 @@ pub struct TerminalEl {
view: WeakViewHandle<ConnectedView>,
modal: bool,
focused: bool,
+ blink_state: bool,
}
impl TerminalEl {
@@ -209,12 +210,14 @@ impl TerminalEl {
terminal: WeakModelHandle<Terminal>,
modal: bool,
focused: bool,
+ blink_state: bool,
) -> TerminalEl {
TerminalEl {
view,
terminal,
modal,
focused,
+ blink_state,
}
}
@@ -568,6 +571,33 @@ impl TerminalEl {
(point, side)
}
+
+ pub fn should_show_cursor(
+ settings: Option<TerminalBlink>,
+ blinking_on: bool,
+ focused: bool,
+ blink_show: bool,
+ ) -> bool {
+ if !focused {
+ true
+ } else {
+ match settings {
+ Some(setting) => match setting {
+ TerminalBlink::Never => true,
+ TerminalBlink::On | TerminalBlink::Off if blinking_on => blink_show,
+ TerminalBlink::On | TerminalBlink::Off /*if !blinking_on */ => true,
+ TerminalBlink::Always => focused && blink_show,
+ },
+ None => {
+ if blinking_on {
+ blink_show
+ } else {
+ false
+ }
+ }
+ }
+ }
+ }
}
impl Element for TerminalEl {
@@ -580,6 +610,7 @@ impl Element for TerminalEl {
cx: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
let settings = cx.global::<Settings>();
+ let blink_settings = settings.terminal_overrides.blinking.clone();
let font_cache = cx.font_cache();
//Setup layout information
@@ -598,13 +629,13 @@ impl Element for TerminalEl {
terminal_theme.colors.background
};
- let (cells, selection, cursor, display_offset, cursor_text) = self
+ let (cells, selection, cursor, display_offset, cursor_text, blink_mode) = self
.terminal
.upgrade(cx)
.unwrap()
.update(cx.app, |terminal, mcx| {
terminal.set_size(dimensions);
- terminal.render_lock(mcx, |content, cursor_text| {
+ terminal.render_lock(mcx, |content, cursor_text, blink_mode| {
let mut cells = vec![];
cells.extend(
content
@@ -628,6 +659,7 @@ impl Element for TerminalEl {
content.cursor,
content.display_offset,
cursor_text,
+ blink_mode,
)
})
});
@@ -644,48 +676,57 @@ impl Element for TerminalEl {
//Layout cursor
let cursor = {
- let cursor_point = DisplayCursor::from(cursor.point, display_offset);
- let cursor_text = {
- let str_trxt = cursor_text.to_string();
-
- let color = if self.focused {
- terminal_theme.colors.background
- } else {
- terminal_theme.colors.foreground
- };
-
- cx.text_layout_cache.layout_str(
- &str_trxt,
- text_style.font_size,
- &[(
- str_trxt.len(),
- RunStyle {
- font_id: text_style.font_id,
- color,
- underline: Default::default(),
- },
- )],
- )
- };
+ if !TerminalEl::should_show_cursor(
+ blink_settings,
+ blink_mode,
+ self.focused,
+ self.blink_state,
+ ) {
+ None
+ } else {
+ let cursor_point = DisplayCursor::from(cursor.point, display_offset);
+ let cursor_text = {
+ let str_trxt = cursor_text.to_string();
- TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
- move |(cursor_position, block_width)| {
- let (shape, color) = if self.focused {
- (CursorShape::Block, terminal_theme.colors.cursor)
+ let color = if self.focused {
+ terminal_theme.colors.background
} else {
- (CursorShape::Underscore, terminal_theme.colors.foreground)
+ terminal_theme.colors.foreground
};
- Cursor::new(
- cursor_position,
- block_width,
- dimensions.line_height,
- color,
- shape,
- Some(cursor_text),
+ cx.text_layout_cache.layout_str(
+ &str_trxt,
+ text_style.font_size,
+ &[(
+ str_trxt.len(),
+ RunStyle {
+ font_id: text_style.font_id,
+ color,
+ underline: Default::default(),
+ },
+ )],
)
- },
- )
+ };
+
+ TerminalEl::shape_cursor(cursor_point, dimensions, &cursor_text).map(
+ move |(cursor_position, block_width)| {
+ let (shape, color) = if self.focused {
+ (CursorShape::Block, terminal_theme.colors.cursor)
+ } else {
+ (CursorShape::Hollow, terminal_theme.colors.foreground)
+ };
+
+ Cursor::new(
+ cursor_position,
+ block_width,
+ dimensions.line_height,
+ color,
+ shape,
+ Some(cursor_text),
+ )
+ },
+ )
+ }
};
//Done!
@@ -818,7 +859,10 @@ impl Element for TerminalEl {
//TODO Talk to keith about how to catch events emitted from an element.
if let Some(view) = self.view.upgrade(cx.app) {
- view.update(cx.app, |view, cx| view.clear_bel(cx))
+ view.update(cx.app, |view, cx| {
+ view.clear_bel(cx);
+ view.pause_cursor_blinking(cx);
+ })
}
self.terminal
@@ -1,3 +1,5 @@
+use std::time::Duration;
+
use alacritty_terminal::term::TermMode;
use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
@@ -9,10 +11,13 @@ use gpui::{
AnyViewHandle, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View,
ViewContext, ViewHandle,
};
+use smol::Timer;
use workspace::pane;
use crate::{connected_el::TerminalEl, Event, Terminal};
+const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
+
///Event to transmit the scroll from the element to the view
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
@@ -51,6 +56,9 @@ pub struct ConnectedView {
// Only for styling purposes. Doesn't effect behavior
modal: bool,
context_menu: ViewHandle<ContextMenu>,
+ show_cursor: bool,
+ blinking_paused: bool,
+ blink_epoch: usize,
}
impl ConnectedView {
@@ -83,6 +91,9 @@ impl ConnectedView {
has_bell: false,
modal,
context_menu: cx.add_view(ContextMenu::new),
+ show_cursor: true,
+ blinking_paused: false,
+ blink_epoch: 0,
}
}
@@ -120,6 +131,59 @@ impl ConnectedView {
cx.notify();
}
+ //Following code copied from editor cursor
+ pub fn blink_show(&self) -> bool {
+ self.blinking_paused || self.show_cursor
+ }
+
+ fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+ if epoch == self.blink_epoch && !self.blinking_paused {
+ self.show_cursor = !self.show_cursor;
+ cx.notify();
+
+ let epoch = self.next_blink_epoch();
+ cx.spawn(|this, mut cx| {
+ let this = this.downgrade();
+ async move {
+ Timer::after(CURSOR_BLINK_INTERVAL).await;
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
+ }
+ }
+ })
+ .detach();
+ }
+ }
+
+ pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
+ self.show_cursor = true;
+ cx.notify();
+
+ let epoch = self.next_blink_epoch();
+ cx.spawn(|this, mut cx| {
+ let this = this.downgrade();
+ async move {
+ Timer::after(CURSOR_BLINK_INTERVAL).await;
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+ }
+ }
+ })
+ .detach();
+ }
+
+ fn next_blink_epoch(&mut self) -> usize {
+ self.blink_epoch += 1;
+ self.blink_epoch
+ }
+
+ fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+ if epoch == self.blink_epoch {
+ self.blinking_paused = false;
+ self.blink_cursors(epoch, cx);
+ }
+ }
+
///Attempt to paste the clipboard into the terminal
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |term, _| term.copy())
@@ -189,9 +253,15 @@ impl View for ConnectedView {
Stack::new()
.with_child(
- TerminalEl::new(cx.handle(), terminal_handle, self.modal, focused)
- .contained()
- .boxed(),
+ TerminalEl::new(
+ cx.handle(),
+ terminal_handle,
+ self.modal,
+ focused,
+ self.blink_show(),
+ )
+ .contained()
+ .boxed(),
)
.with_child(ChildView::new(&self.context_menu).boxed())
.boxed()
@@ -200,6 +270,7 @@ impl View for ConnectedView {
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_new_content = false;
self.terminal.read(cx).focus_in();
+ self.blink_cursors(self.blink_epoch, cx);
cx.notify();
}
@@ -208,6 +279,7 @@ impl View for ConnectedView {
cx.notify();
}
+ //IME stuff
fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
if self
.terminal
@@ -25,7 +25,7 @@ use futures::{
};
use modal::deploy_modal;
-use settings::{Settings, Shell};
+use settings::{Settings, Shell, TerminalBlink};
use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration};
use thiserror::Error;
@@ -254,6 +254,7 @@ impl TerminalBuilder {
shell: Option<Shell>,
env: Option<HashMap<String, String>>,
initial_size: TerminalSize,
+ blink_settings: Option<TerminalBlink>,
) -> Result<TerminalBuilder> {
let pty_config = {
let alac_shell = shell.clone().and_then(|shell| match shell {
@@ -287,9 +288,21 @@ impl TerminalBuilder {
setup_env(&config);
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
+ //TODO: Remove with a bounded sender which can be dispatched on &self
let (events_tx, events_rx) = unbounded();
//Set up the terminal...
- let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
+ let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
+
+ //Start off blinking if we need to
+ match blink_settings {
+ Some(setting) => match setting {
+ TerminalBlink::On | TerminalBlink::Always => {
+ term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
+ }
+ _ => {}
+ },
+ None => term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor),
+ }
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
@@ -321,7 +334,7 @@ impl TerminalBuilder {
//And connect them together
let event_loop = EventLoop::new(
term.clone(),
- ZedListener(events_tx),
+ ZedListener(events_tx.clone()),
pty,
pty_config.hold,
false,
@@ -582,7 +595,7 @@ impl Terminal {
pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
where
- F: FnOnce(RenderableContent, char) -> T,
+ F: FnOnce(RenderableContent, char, bool) -> T,
{
let m = self.term.clone(); //Arc clone
let mut term = m.lock();
@@ -598,7 +611,7 @@ impl Terminal {
let cursor_text = term.grid()[content.cursor.point].c;
- f(content, cursor_text)
+ f(content, cursor_text, term.cursor_style().blinking)
}
///Scroll the terminal
@@ -94,8 +94,13 @@ impl TerminalView {
let shell = settings.terminal_overrides.shell.clone();
let envs = settings.terminal_overrides.env.clone(); //Should be short and cheap.
- let content = match TerminalBuilder::new(working_directory.clone(), shell, envs, size_info)
- {
+ let content = match TerminalBuilder::new(
+ working_directory.clone(),
+ shell,
+ envs,
+ size_info,
+ settings.terminal_overrides.blinking.clone(),
+ ) {
Ok(terminal) => {
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
let view = cx.add_view(|cx| ConnectedView::from_terminal(terminal, modal, cx));