T10: focus in and out reach a program under mode 1004; the window wears the program's title; a bell is logged and silent; the scrollback setting caps history and trims it live; Cmd-F finds in the scrollback with Ely's FindWidget, matches washed, Enter older, Shift-Enter newer, new output searched again
This commit is contained in:
11 files changed
+486
-35
No files matched your search
+29
-3
@@ -4,21 +4,46 @@ use anyhow::Context as _;
|
||||
use ely_gpui_component::{primitives::FocusScope, theme::ActiveTheme};
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement,
|
||||
Render, Styled, TitlebarOptions, Window, WindowBounds, WindowHandle, WindowOptions, px, size,
|
||||
Render, Styled, Subscription, TitlebarOptions, Window, WindowBounds, WindowHandle,
|
||||
WindowOptions, px, size,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::term::mac;
|
||||
use crate::{
|
||||
settings::SettingsStore,
|
||||
term::{ICONS, Launch, MONO, TerminalView, config, input, shell::ShellIntegration, spawn},
|
||||
term::{
|
||||
ICONS, Launch, MONO, TerminalEvent, TerminalView, config, input, shell::ShellIntegration,
|
||||
spawn,
|
||||
},
|
||||
themes,
|
||||
};
|
||||
|
||||
/// The window title while the program names none.
|
||||
const TITLE: &str = "ELYTerminal";
|
||||
|
||||
/// The main window's content.
|
||||
pub struct Workspace {
|
||||
focus: FocusHandle,
|
||||
terminal: Entity<TerminalView>,
|
||||
_events: Subscription,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// The window wears the terminal's title.
|
||||
fn terminal_event(
|
||||
&mut self,
|
||||
_: &Entity<TerminalView>,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
if let TerminalEvent::Title(title) = event {
|
||||
let title = if title.is_empty() { TITLE } else { title };
|
||||
log::debug!("app: the window title is {title:?}");
|
||||
window.set_window_title(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Workspace {
|
||||
@@ -75,7 +100,7 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("ELYTerminal".into()),
|
||||
title: Some(TITLE.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -89,6 +114,7 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
|
||||
window.focus(&terminal.focus_handle(cx), cx);
|
||||
cx.new(|cx| Workspace {
|
||||
focus: cx.focus_handle(),
|
||||
_events: cx.subscribe_in(&terminal, window, Workspace::terminal_event),
|
||||
terminal,
|
||||
})
|
||||
})?;
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
use alacritty_terminal::{
|
||||
grid::Dimensions,
|
||||
index::{Column, Direction, Point},
|
||||
term::{
|
||||
Term,
|
||||
search::{Match, RegexIter, RegexSearch},
|
||||
},
|
||||
};
|
||||
use ely_gpui_component::{
|
||||
editor::{FindOptions, FindWidget},
|
||||
forms::{InputEvent, TextInput},
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, Context, Entity, Focusable, Hsla, InteractiveElement, IntoElement, ParentElement,
|
||||
SharedString, Subscription, Window, div,
|
||||
};
|
||||
|
||||
use super::{
|
||||
input::{FIND, FindNewer},
|
||||
view::TerminalView,
|
||||
};
|
||||
|
||||
/// An open find: its query and options, the matches oldest first, the one stepped to.
|
||||
pub struct Finder {
|
||||
query: Entity<TextInput>,
|
||||
options: FindOptions,
|
||||
hits: Vec<Match>,
|
||||
current: Option<usize>,
|
||||
error: Option<SharedString>,
|
||||
_input: Subscription,
|
||||
}
|
||||
|
||||
impl Finder {
|
||||
/// The matches on screen, each with its wash; the current one's is stronger.
|
||||
pub fn lit<T>(&self, term: &Term<T>, color: Hsla) -> Vec<(Match, Hsla)> {
|
||||
let offset = term.grid().display_offset() as i32;
|
||||
let (top, bottom) = (-offset, term.screen_lines() as i32 - 1 - offset);
|
||||
self.hits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, hit)| hit.end().line.0 >= top && hit.start().line.0 <= bottom)
|
||||
.map(|(at, hit)| {
|
||||
let wash = if Some(at) == self.current { 0.55 } else { 0.25 };
|
||||
(hit.clone(), color.opacity(wash))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// `query` under `options` as the grid's search reads it; word edges are ASCII, which its lazy DFA needs.
|
||||
fn pattern(query: &str, options: FindOptions) -> String {
|
||||
let body = if options.regex {
|
||||
query.to_string()
|
||||
} else {
|
||||
regex::escape(query)
|
||||
};
|
||||
let body = if options.word {
|
||||
format!(r"(?-u:\b)(?:{body})(?-u:\b)")
|
||||
} else {
|
||||
body
|
||||
};
|
||||
let case = if options.case { "(?-i)" } else { "(?i)" };
|
||||
format!("{case}{body}")
|
||||
}
|
||||
|
||||
/// Every match of `pattern` in the scrollback and the screen, oldest first.
|
||||
fn search_all<T>(term: &Term<T>, pattern: &str) -> Result<Vec<Match>, String> {
|
||||
let mut regex = RegexSearch::new(pattern).map_err(|error| error.to_string())?;
|
||||
let start = Point::new(term.topmost_line(), Column(0));
|
||||
let end = Point::new(term.bottommost_line(), term.last_column());
|
||||
Ok(RegexIter::new(start, end, Direction::Right, term, &mut regex).collect())
|
||||
}
|
||||
|
||||
/// The match one newer than `current`, or one older, round the ends.
|
||||
fn stepped(current: Option<usize>, count: usize, newer: bool) -> Option<usize> {
|
||||
let at = current?;
|
||||
Some(if newer {
|
||||
(at + 1) % count
|
||||
} else {
|
||||
(at + count - 1) % count
|
||||
})
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
/// Opens the find box, or takes the focus back to it.
|
||||
pub(super) fn find(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let query = match &self.finder {
|
||||
Some(finder) => finder.query.clone(),
|
||||
None => {
|
||||
let query = cx.new(|cx| TextInput::new(window, cx));
|
||||
let input = cx.subscribe_in(&query, window, |view, _, event, _, cx| match event {
|
||||
InputEvent::Changed => view.search(true, cx),
|
||||
InputEvent::Submit => view.step(false, cx),
|
||||
InputEvent::Focus | InputEvent::Blur => {}
|
||||
});
|
||||
log::debug!("terminal {}: find opens", self.pty.pid);
|
||||
self.finder = Some(Finder {
|
||||
query: query.clone(),
|
||||
options: FindOptions::default(),
|
||||
hits: Vec::new(),
|
||||
current: None,
|
||||
error: None,
|
||||
_input: input,
|
||||
});
|
||||
query
|
||||
}
|
||||
};
|
||||
query.update(cx, |input, cx| {
|
||||
let all = 0..input.text().len();
|
||||
input.select(all, cx);
|
||||
});
|
||||
window.focus(&query.focus_handle(cx), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn close_find(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.finder.take().is_some() {
|
||||
log::debug!("terminal {}: find closes", self.pty.pid);
|
||||
window.focus(&self.focus, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the query anew; a `fresh` query starts at the newest match, new output keeps the current one.
|
||||
pub(super) fn search(&mut self, fresh: bool, cx: &mut Context<Self>) {
|
||||
let Some(finder) = self.finder.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let query = finder.query.read(cx).text().to_string();
|
||||
let found = if query.is_empty() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
search_all(&self.pty.term.lock(), &pattern(&query, finder.options))
|
||||
};
|
||||
let (hits, error) = match found {
|
||||
Ok(hits) => (hits, None),
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"terminal {}: {query:?} is no pattern: {error}",
|
||||
self.pty.pid
|
||||
);
|
||||
(Vec::new(), Some(SharedString::from(error)))
|
||||
}
|
||||
};
|
||||
let newest = hits.len().checked_sub(1);
|
||||
let kept = finder.current.filter(|at| *at < hits.len());
|
||||
finder.current = if fresh { newest } else { kept.or(newest) };
|
||||
finder.error = error;
|
||||
finder.hits = hits;
|
||||
if fresh {
|
||||
log::debug!(
|
||||
"terminal {}: {} matches of {query:?}",
|
||||
self.pty.pid,
|
||||
finder.hits.len()
|
||||
);
|
||||
self.reveal(cx);
|
||||
} else {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Steps to a newer match, or an older one, and scrolls it into view.
|
||||
fn step(&mut self, newer: bool, cx: &mut Context<Self>) {
|
||||
let Some(finder) = self.finder.as_mut() else {
|
||||
return;
|
||||
};
|
||||
finder.current = stepped(finder.current, finder.hits.len(), newer);
|
||||
self.reveal(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn reveal(&mut self, cx: &mut Context<Self>) {
|
||||
let start = self.finder.as_ref().and_then(|finder| {
|
||||
let at = finder.current?;
|
||||
Some(*finder.hits[at].start())
|
||||
});
|
||||
if let Some(start) = start {
|
||||
log::trace!("terminal {}: match at {start:?}", self.pty.pid);
|
||||
self.pty.term.lock().scroll_to_point(start);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_options(&mut self, options: FindOptions, cx: &mut Context<Self>) {
|
||||
if let Some(finder) = self.finder.as_mut() {
|
||||
finder.options = options;
|
||||
self.search(true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// The find box over the grid; Shift-Enter steps to a newer match.
|
||||
pub(super) fn find_box(&self, cx: &mut Context<Self>) -> Option<impl IntoElement + use<>> {
|
||||
let finder = self.finder.as_ref()?;
|
||||
let (flip, step, close, back) = (cx.entity(), cx.entity(), cx.entity(), cx.entity());
|
||||
let total = finder.hits.len();
|
||||
let widget = FindWidget::new(
|
||||
("find", cx.entity_id()),
|
||||
&finder.query,
|
||||
finder.current,
|
||||
total,
|
||||
)
|
||||
.options(finder.options)
|
||||
.on_options(move |options, _, cx| flip.update(cx, |view, cx| view.set_options(options, cx)))
|
||||
.on_step(move |newer, _, cx| step.update(cx, |view, cx| view.step(newer, cx)))
|
||||
.on_close(move |window, cx| close.update(cx, |view, cx| view.close_find(window, cx)));
|
||||
let widget = match &finder.error {
|
||||
Some(error) => widget.error(error.clone()),
|
||||
None => widget,
|
||||
};
|
||||
Some(
|
||||
div()
|
||||
.key_context(FIND)
|
||||
.on_action(move |_: &FindNewer, _, cx| {
|
||||
back.update(cx, |view, cx| view.step(true, cx))
|
||||
})
|
||||
.child(widget),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alacritty_terminal::term::test::mock_term;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_takes_case_words_and_patterns() {
|
||||
let term = mock_term("one two One\r\nthree phone one");
|
||||
let count = |query: &str, options: FindOptions| {
|
||||
search_all(&term, &pattern(query, options)).map(|all| all.len())
|
||||
};
|
||||
assert_eq!(count("one", FindOptions::default()), Ok(4));
|
||||
let case = FindOptions {
|
||||
case: true,
|
||||
..FindOptions::default()
|
||||
};
|
||||
assert_eq!(count("One", case), Ok(1));
|
||||
let word = FindOptions {
|
||||
word: true,
|
||||
..FindOptions::default()
|
||||
};
|
||||
assert_eq!(count("one", word), Ok(3), "phone holds no whole word");
|
||||
let regex = FindOptions {
|
||||
regex: true,
|
||||
..FindOptions::default()
|
||||
};
|
||||
assert_eq!(count("t[wh]", regex), Ok(2));
|
||||
assert_eq!(
|
||||
count("(", FindOptions::default()),
|
||||
Ok(0),
|
||||
"plain text is escaped"
|
||||
);
|
||||
assert!(count("(", regex).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steps_go_round_the_ends() {
|
||||
assert_eq!(stepped(Some(0), 3, false), Some(2));
|
||||
assert_eq!(stepped(Some(2), 3, true), Some(0));
|
||||
assert_eq!(stepped(Some(1), 3, false), Some(0));
|
||||
assert_eq!(stepped(None, 0, true), None);
|
||||
}
|
||||
}
|
||||
+61
-12
@@ -2,7 +2,7 @@ use alacritty_terminal::{
|
||||
event::EventListener,
|
||||
grid::Dimensions,
|
||||
index::Point,
|
||||
term::{Term, cell::Cell, cell::Flags, color::Colors},
|
||||
term::{Term, cell::Cell, cell::Flags, color::Colors, search::Match},
|
||||
vte::ansi::CursorShape,
|
||||
};
|
||||
use gpui::Hsla;
|
||||
@@ -83,8 +83,8 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen as it shows now, scrolled back or not.
|
||||
pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
|
||||
/// The screen as it shows now, scrolled back or not; `lit` matches wear their washes.
|
||||
pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink, lit: &[(Match, Hsla)]) -> Frame {
|
||||
let content = term.renderable_content();
|
||||
let offset = content.display_offset as i32;
|
||||
let mut rows = vec![Row::default(); term.screen_lines()];
|
||||
@@ -94,17 +94,23 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
|
||||
let (point, cell) = (indexed.point, indexed.cell);
|
||||
let row = (point.line.0 + offset) as usize;
|
||||
let mut look = style(cell, ink, content.colors);
|
||||
let selected = content.selection.as_ref().is_some_and(|range| {
|
||||
let covers = |contains: &dyn Fn(Point) -> bool| {
|
||||
let (line, column) = (point.line, point.column);
|
||||
range.contains(point)
|
||||
|| (cell.flags.contains(Flags::WIDE_CHAR)
|
||||
&& range.contains(Point::new(line, column + 1)))
|
||||
contains(point)
|
||||
|| (cell.flags.contains(Flags::WIDE_CHAR) && contains(Point::new(line, column + 1)))
|
||||
|| (cell.flags.contains(Flags::WIDE_CHAR_SPACER)
|
||||
&& range.contains(Point::new(line, column - 1)))
|
||||
});
|
||||
if selected {
|
||||
&& contains(Point::new(line, column - 1)))
|
||||
};
|
||||
if content
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(|range| covers(&|at| range.contains(at)))
|
||||
{
|
||||
look.bg = Some(ink.selection);
|
||||
look.fg = ink.selection_text.unwrap_or(look.fg);
|
||||
} else if let Some((_, wash)) = lit.iter().find(|(hit, _)| covers(&|at| hit.contains(&at)))
|
||||
{
|
||||
look.bg = Some(*wash);
|
||||
}
|
||||
let spacer = cell
|
||||
.flags
|
||||
@@ -213,7 +219,7 @@ mod tests {
|
||||
let mut parser: Processor = Processor::new();
|
||||
parser.advance(&mut term, bytes);
|
||||
let ink = Ink::new(themes::find("dracula").expect("dracula"), term.colors());
|
||||
(frame(&term, &ink), ink)
|
||||
(frame(&term, &ink, &[]), ink)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -319,7 +325,7 @@ mod tests {
|
||||
selection.update(Point::new(Line(0), Column(3)), Side::Left);
|
||||
term.selection = Some(selection);
|
||||
let ink = Ink::new(themes::find("dracula").expect("dracula"), term.colors());
|
||||
let frame = frame(&term, &ink);
|
||||
let frame = frame(&term, &ink, &[]);
|
||||
let grounds: Vec<_> = frame.rows[0]
|
||||
.runs
|
||||
.iter()
|
||||
@@ -332,6 +338,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn found_matches_wear_their_washes_under_the_selection() {
|
||||
use alacritty_terminal::index::{Column, Line, Side};
|
||||
use alacritty_terminal::selection::{Selection, SelectionType};
|
||||
let mut term = Term::new(Config::default(), &TermSize::new(12, 3), VoidListener);
|
||||
let mut parser: Processor = Processor::new();
|
||||
parser.advance(&mut term, "ab\u{4e2d}d ab".as_bytes());
|
||||
let at = |column| Point::new(Line(0), Column(column));
|
||||
let ink = Ink::new(themes::find("dracula").expect("dracula"), term.colors());
|
||||
let (found, current) = (ink.ansi[3].opacity(0.25), ink.ansi[3].opacity(0.55));
|
||||
let lit = [(at(1)..=at(2), found), (at(6)..=at(7), current)];
|
||||
let grounds = |frame: &Frame| -> Vec<_> {
|
||||
frame.rows[0]
|
||||
.runs
|
||||
.iter()
|
||||
.map(|(bytes, style)| (*bytes, style.bg))
|
||||
.collect()
|
||||
};
|
||||
let shown = frame(&term, &ink, &lit);
|
||||
assert_eq!(
|
||||
&grounds(&shown)[..5],
|
||||
[
|
||||
(1, None),
|
||||
(5, Some(found)),
|
||||
(2, None),
|
||||
(2, Some(current)),
|
||||
(4, None)
|
||||
],
|
||||
"b and both halves of the wide cell"
|
||||
);
|
||||
term.selection = Some(Selection::new(SelectionType::Simple, at(6), Side::Left));
|
||||
term.selection
|
||||
.as_mut()
|
||||
.expect("a selection")
|
||||
.update(at(6), Side::Right);
|
||||
let shown = frame(&term, &ink, &lit);
|
||||
assert_eq!(
|
||||
grounds(&shown)[3],
|
||||
(1, Some(ink.selection)),
|
||||
"the selection wins"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_cells_show_nothing_but_their_ground() {
|
||||
let (frame, _) = shown("\x1b[8;4;9;41mSe\u{301}\x1b[0m\x1b[2D".as_bytes());
|
||||
|
||||
+14
-5
@@ -11,21 +11,30 @@ use super::{
|
||||
view::TerminalView,
|
||||
};
|
||||
|
||||
actions!(elyterminal, [Copy, Paste]);
|
||||
actions!(elyterminal, [Copy, Paste, Find, FindNewer]);
|
||||
|
||||
/// The key context a terminal's root sets.
|
||||
pub const CONTEXT: &str = "Terminal";
|
||||
|
||||
/// Copy and paste; Ely's focus keys go to the terminal's listeners instead.
|
||||
/// The key context of a terminal's find box.
|
||||
pub const FIND: &str = "TerminalFind";
|
||||
|
||||
/// Copy, paste and find; Ely's focus keys go to the terminal's listeners instead.
|
||||
pub fn bind_keys(cx: &mut App) {
|
||||
let (copy, paste) = if cfg!(target_os = "macos") {
|
||||
("cmd-c", "cmd-v")
|
||||
let (copy, paste, find) = if cfg!(target_os = "macos") {
|
||||
("cmd-c", "cmd-v", "cmd-f")
|
||||
} else {
|
||||
("ctrl-shift-c", "ctrl-shift-v")
|
||||
("ctrl-shift-c", "ctrl-shift-v", "ctrl-shift-f")
|
||||
};
|
||||
cx.bind_keys([
|
||||
KeyBinding::new(copy, Copy, Some(CONTEXT)),
|
||||
KeyBinding::new(paste, Paste, Some(CONTEXT)),
|
||||
KeyBinding::new(find, Find, Some(CONTEXT)),
|
||||
KeyBinding::new(
|
||||
"shift-enter",
|
||||
FindNewer,
|
||||
Some(&format!("{FIND} > ElyInput")),
|
||||
),
|
||||
]);
|
||||
cx.bind_keys(
|
||||
["tab", "shift-tab", "f6", "shift-f6"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod boxes;
|
||||
mod find;
|
||||
pub mod frame;
|
||||
pub mod ink;
|
||||
pub mod input;
|
||||
|
||||
@@ -377,3 +377,28 @@ mod tests {
|
||||
assert_eq!((size.num_cols, size.num_lines), (2, 1));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod scrollback {
|
||||
use alacritty_terminal::{event::VoidListener, grid::Dimensions, vte::ansi::Processor};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_setting_caps_history_and_a_change_trims_it() {
|
||||
let settings = Settings {
|
||||
scrollback: 5,
|
||||
..Settings::default()
|
||||
};
|
||||
let mut term = Term::new(config(&settings), &TermSize::new(10, 3), VoidListener);
|
||||
let mut parser: Processor = Processor::new();
|
||||
let lines: String = (1..=20).map(|line| format!("{line}\r\n")).collect();
|
||||
parser.advance(&mut term, lines.as_bytes());
|
||||
assert_eq!(term.grid().history_size(), 5);
|
||||
term.set_options(config(&Settings {
|
||||
scrollback: 2,
|
||||
..settings
|
||||
}));
|
||||
assert_eq!(term.grid().history_size(), 2);
|
||||
}
|
||||
}
|
||||
+67
-9
@@ -1,8 +1,12 @@
|
||||
use std::{collections::HashSet, path::PathBuf, process::ExitStatus, rc::Rc, time::Duration};
|
||||
|
||||
use alacritty_terminal::{
|
||||
event::Event, grid::Dimensions, term::ClipboardType, vte::ansi::CursorShape,
|
||||
event::Event,
|
||||
grid::Dimensions,
|
||||
term::{ClipboardType, TermMode},
|
||||
vte::ansi::CursorShape,
|
||||
};
|
||||
use ely_gpui_component::theme::ActiveTheme;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Bounds, Context, EventEmitter, FocusHandle, Focusable, Font, FontFeatures, FontStyle,
|
||||
@@ -11,9 +15,10 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
find::Finder,
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, Copy, Paste},
|
||||
input::{self, Copy, Find, Paste},
|
||||
mouse::MouseMode,
|
||||
paint::{self, Look},
|
||||
pointer::Pointer,
|
||||
@@ -55,13 +60,16 @@ pub struct TerminalView {
|
||||
pub(super) mouse: MouseMode,
|
||||
/// Keys whose press this terminal saw, so their releases may go out.
|
||||
pub(super) held: HashSet<String>,
|
||||
pub(super) finder: Option<Finder>,
|
||||
pub(super) size: (usize, usize),
|
||||
cell: (u16, u16),
|
||||
/// Focused in an active window, as the program last heard.
|
||||
focused: bool,
|
||||
blinking: bool,
|
||||
shown: bool,
|
||||
_events: Task<()>,
|
||||
_blink: Task<()>,
|
||||
_activation: Subscription,
|
||||
_focus: [Subscription; 3],
|
||||
_settings: Subscription,
|
||||
}
|
||||
|
||||
@@ -80,21 +88,30 @@ impl TerminalView {
|
||||
let term = pty.term.lock();
|
||||
(term.columns(), term.screen_lines())
|
||||
};
|
||||
let focus = cx.focus_handle().tab_stop(true);
|
||||
let refocus =
|
||||
|view: &mut Self, window: &mut Window, cx: &mut Context<Self>| view.refocus(window, cx);
|
||||
Self {
|
||||
pty,
|
||||
focus: cx.focus_handle().tab_stop(true),
|
||||
_focus: [
|
||||
cx.observe_window_activation(window, refocus),
|
||||
cx.on_focus(&focus, window, refocus),
|
||||
cx.on_blur(&focus, window, refocus),
|
||||
],
|
||||
focus,
|
||||
marked: None,
|
||||
caret: Bounds::default(),
|
||||
pointer: Pointer::default(),
|
||||
mouse: MouseMode::default(),
|
||||
held: HashSet::new(),
|
||||
finder: None,
|
||||
size,
|
||||
cell: (8, 16),
|
||||
focused: false,
|
||||
blinking: false,
|
||||
shown: true,
|
||||
_events: Self::pump(events, cx),
|
||||
_blink: Self::blink(cx),
|
||||
_activation: cx.observe_window_activation(window, |_, _, cx| cx.notify()),
|
||||
_settings: cx.observe_global::<SettingsStore>(|view, cx| {
|
||||
view.pty
|
||||
.term
|
||||
@@ -153,7 +170,33 @@ impl TerminalView {
|
||||
true
|
||||
}
|
||||
|
||||
/// Tells a program that asked, by mode 1004, when the terminal gains or loses focus.
|
||||
fn refocus(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let focused = self.focus.is_focused(window) && window.is_window_active();
|
||||
if focused != self.focused {
|
||||
self.focused = focused;
|
||||
let asked = self.pty.term.lock().mode().contains(TermMode::FOCUS_IN_OUT);
|
||||
log::debug!(
|
||||
"terminal {}: focus {}{}",
|
||||
self.pty.pid,
|
||||
if focused { "in" } else { "out" },
|
||||
if asked { ", reported" } else { "" }
|
||||
);
|
||||
if asked {
|
||||
self.pty.write(if focused {
|
||||
&b"\x1b[I"[..]
|
||||
} else {
|
||||
&b"\x1b[O"[..]
|
||||
});
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle(&mut self, events: Vec<PtyEvent>, cx: &mut Context<Self>) {
|
||||
let drawn = events
|
||||
.iter()
|
||||
.any(|event| matches!(event, PtyEvent::Term(Event::Wakeup)));
|
||||
for event in events {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => cx.emit(TerminalEvent::Folder(folder)),
|
||||
@@ -161,6 +204,10 @@ impl TerminalView {
|
||||
PtyEvent::Mouse(mode) => self.mouse = mode,
|
||||
}
|
||||
}
|
||||
// ponytail: a full search per output batch while find is open; search new lines alone if scrollbacks lag.
|
||||
if drawn && self.finder.is_some() {
|
||||
self.search(false, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -170,7 +217,10 @@ impl TerminalView {
|
||||
Event::MouseCursorDirty | Event::CursorBlinkingChange => {}
|
||||
Event::Title(title) => cx.emit(TerminalEvent::Title(title)),
|
||||
Event::ResetTitle => cx.emit(TerminalEvent::Title(String::new())),
|
||||
Event::Bell => cx.emit(TerminalEvent::Bell),
|
||||
Event::Bell => {
|
||||
log::debug!("terminal {}: a bell, kept silent", self.pty.pid);
|
||||
cx.emit(TerminalEvent::Bell);
|
||||
}
|
||||
Event::ClipboardStore(kind, text) => {
|
||||
log::info!("terminal {}: OSC 52 writes the {kind:?}", self.pty.pid);
|
||||
self.copy_text(text, kind == ClipboardType::Selection, cx);
|
||||
@@ -250,7 +300,10 @@ impl Render for TerminalView {
|
||||
let theme = themes::active(&settings, window.appearance());
|
||||
let term = self.pty.term.lock();
|
||||
let ink = Ink::new(theme, term.colors());
|
||||
let shown = frame(&term, &ink);
|
||||
let lit = self.finder.as_ref().map_or_else(Vec::new, |finder| {
|
||||
finder.lit(&term, cx.theme().colors.warning)
|
||||
});
|
||||
let shown = frame(&term, &ink, &lit);
|
||||
let blinks = term.cursor_style().blinking;
|
||||
drop(term);
|
||||
let focused = self.focus.is_focused(window) && window.is_window_active();
|
||||
@@ -277,6 +330,7 @@ impl Render for TerminalView {
|
||||
.on_key_up(cx.listener(Self::key_up))
|
||||
.on_action(cx.listener(|view, _: &Copy, _, cx| view.copy(cx)))
|
||||
.on_action(cx.listener(|view, _: &Paste, _, cx| view.paste(cx)))
|
||||
.on_action(cx.listener(|view, _: &Find, window, cx| view.find(window, cx)))
|
||||
.on_any_mouse_down(cx.listener(Self::mouse_down))
|
||||
.on_mouse_move(cx.listener(Self::mouse_move))
|
||||
.on_scroll_wheel(cx.listener(Self::wheel))
|
||||
@@ -291,7 +345,7 @@ impl Render for TerminalView {
|
||||
root.on_mouse_up(button, cx.listener(Self::mouse_up))
|
||||
.on_mouse_up_out(button, cx.listener(Self::mouse_up))
|
||||
});
|
||||
match look(&settings, &ink, window) {
|
||||
let root = match look(&settings, &ink, window) {
|
||||
Ok(look) => {
|
||||
self.cell = (
|
||||
f32::from(look.cell.width).round() as u16,
|
||||
@@ -310,6 +364,10 @@ impl Render for TerminalView {
|
||||
log::error!("terminal {}: {error:#}", self.pty.pid);
|
||||
root.text_color(ink.fg).child(format!("{error:#}"))
|
||||
}
|
||||
}
|
||||
};
|
||||
div().relative().size_full().child(root).children(
|
||||
self.find_box(cx)
|
||||
.map(|found| div().absolute().top_2().right_4().child(found)),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user