Skip to repository content1105 lines · 34.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:15:46.025Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
alacritty.rs
1#[cfg(target_os = "windows")]
2use std::num::NonZeroU32;
3#[cfg(unix)]
4use std::os::fd::AsRawFd;
5use std::{borrow::Cow, io, ops::RangeInclusive, path::PathBuf, sync::Arc};
6
7mod hyperlinks;
8
9use alacritty_terminal::{
10 event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
11 event_loop::{EventLoop, Msg, Notifier},
12 grid::{Dimensions, Grid, GridIterator, Row, Scroll as AlacScroll},
13 index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
14 selection::{
15 Selection as AlacSelection, SelectionRange as AlacSelectionRange,
16 SelectionType as AlacSelectionType,
17 },
18 sync::FairMutex,
19 term::{
20 Config, Osc52, RenderableCursor, Term, TermMode,
21 cell::{Cell as AlacCell, Flags, Hyperlink as AlacHyperlink},
22 search::{Match, RegexIter, RegexSearch},
23 },
24 tty,
25 vi_mode::{ViModeCursor, ViMotion as AlacViMotion},
26 vte::ansi::{
27 ClearMode, CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
28 NamedPrivateMode, PrivateMode,
29 },
30};
31use anyhow::{Context as _, Result};
32use futures::channel::mpsc::UnboundedSender;
33use util::paths::PathStyle;
34use vte::ansi::Handler;
35#[cfg(target_os = "windows")]
36use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
37
38use crate::{
39 Cell, Color, Content, Cursor, CursorShape, Hyperlink, HyperlinkData, IndexedCell, Modes, Point,
40 PtyEvent, Range, RenderableCells, Scroll, Search, Selection, SelectionRange, SelectionSide,
41 SelectionType, TerminalBackendEvent, TerminalBounds, ViMotion,
42 pty_info::ProcessIdGetter,
43 terminal_settings::{AlternateScroll, CursorShape as SettingsCursorShape},
44};
45
46pub(super) use hyperlinks::{HyperlinkMatch, RegexSearches};
47
48pub(super) type AlacrittyPty = tty::Pty;
49pub(super) type AlacrittyTerm = Term<ZedListener>;
50pub(super) type AlacrittyTermConfig = Config;
51pub(super) type AlacrittyTermLock = FairMutex<AlacrittyTerm>;
52pub(super) type AlacrittyCell = AlacCell;
53pub(super) type AlacrittyGridIterator<'a> = GridIterator<'a, AlacCell>;
54pub(super) type AlacrittyHyperlink = AlacHyperlink;
55
56#[derive(Clone)]
57pub(super) struct ZedListener(UnboundedSender<PtyEvent>);
58
59#[derive(Clone, Debug)]
60pub(super) struct AlacrittySearch {
61 search: RegexSearch,
62}
63
64#[cfg(unix)]
65impl From<&AlacrittyPty> for ProcessIdGetter {
66 fn from(pty: &AlacrittyPty) -> Self {
67 Self::new(pty.file().as_raw_fd(), pty.child().id())
68 }
69}
70
71#[cfg(windows)]
72impl From<&AlacrittyPty> for ProcessIdGetter {
73 fn from(pty: &AlacrittyPty) -> Self {
74 let child = pty.child_watcher();
75 let handle = child.raw_handle();
76 let fallback_pid = child.pid().unwrap_or_else(|| unsafe {
77 NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _)))
78 });
79
80 Self::new(handle as i32, u32::from(fallback_pid))
81 }
82}
83
84pub(super) struct PtySender {
85 notifier: Notifier,
86}
87
88impl PtySender {
89 pub(super) fn notify(&self, input: impl Into<Cow<'static, [u8]>>) {
90 self.notifier.notify(input);
91 }
92
93 pub(super) fn resize(&self, bounds: TerminalBounds) {
94 if let Err(error) = self
95 .notifier
96 .0
97 .send(Msg::Resize(window_size_from_terminal_bounds(bounds)))
98 {
99 log::error!("failed to resize alacritty pty: {error}");
100 }
101 }
102
103 pub(super) fn shutdown(&self) {
104 if let Err(error) = self.notifier.0.send(Msg::Shutdown) {
105 log::debug!("failed to shut down alacritty pty loop: {error}");
106 }
107 }
108}
109
110fn window_size_from_terminal_bounds(bounds: TerminalBounds) -> WindowSize {
111 WindowSize {
112 num_lines: bounds.num_lines() as u16,
113 num_cols: bounds.num_columns() as u16,
114 cell_width: f32::from(bounds.cell_width()) as u16,
115 cell_height: f32::from(bounds.line_height()) as u16,
116 }
117}
118
119pub(super) fn display_only_term_config(
120 scrolling_history: usize,
121 cursor_shape: SettingsCursorShape,
122) -> AlacrittyTermConfig {
123 Config {
124 scrolling_history,
125 default_cursor_style: alacritty_cursor_style(cursor_shape),
126 osc52: Osc52::Disabled,
127 ..Config::default()
128 }
129}
130
131pub(super) fn pty_term_config(
132 scrolling_history: usize,
133 cursor_shape: SettingsCursorShape,
134) -> AlacrittyTermConfig {
135 Config {
136 scrolling_history,
137 default_cursor_style: alacritty_cursor_style(cursor_shape),
138 ..Config::default()
139 }
140}
141
142pub(super) fn set_default_cursor_style(
143 config: &mut AlacrittyTermConfig,
144 cursor_shape: SettingsCursorShape,
145) {
146 config.default_cursor_style = alacritty_cursor_style(cursor_shape);
147}
148
149pub(super) fn apply_config(term: &AlacrittyTermLock, config: &AlacrittyTermConfig) {
150 term.lock().set_options(config.clone());
151}
152
153#[cfg(not(windows))]
154pub(super) fn current_child_signal_mask() -> io::Result<tty::SignalMask> {
155 tty::SignalMask::current()
156}
157
158pub(super) fn pty_options(
159 shell: Option<(String, Vec<String>)>,
160 working_directory: Option<PathBuf>,
161 env: impl IntoIterator<Item = (String, String)>,
162 #[cfg(not(windows))] child_signal_mask: Option<tty::SignalMask>,
163 #[cfg(windows)] escape_args: bool,
164) -> tty::Options {
165 tty::Options {
166 shell: shell.map(|(program, args)| tty::Shell::new(program, args)),
167 working_directory,
168 drain_on_exit: true,
169 env: env.into_iter().collect(),
170 #[cfg(not(windows))]
171 child_signal_mask,
172 #[cfg(windows)]
173 escape_args,
174 }
175}
176
177pub(super) fn open_pty(
178 options: &tty::Options,
179 bounds: TerminalBounds,
180 window_id: u64,
181) -> io::Result<AlacrittyPty> {
182 tty::new(options, window_size_from_terminal_bounds(bounds), window_id)
183}
184
185pub(super) fn new_term(
186 config: &AlacrittyTermConfig,
187 bounds: TerminalBounds,
188 events_tx: UnboundedSender<PtyEvent>,
189 alternate_scroll: AlternateScroll,
190) -> Arc<AlacrittyTermLock> {
191 let mut term = Term::new(config.clone(), &bounds, ZedListener(events_tx));
192
193 if let AlternateScroll::Off = alternate_scroll {
194 term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
195 }
196
197 Arc::new(FairMutex::new(term))
198}
199
200pub(super) fn spawn_event_loop(
201 term: Arc<AlacrittyTermLock>,
202 events_tx: UnboundedSender<PtyEvent>,
203 pty: AlacrittyPty,
204 drain_on_exit: bool,
205) -> Result<PtySender> {
206 let event_loop = EventLoop::new(term, ZedListener(events_tx), pty, drain_on_exit, false)
207 .context("failed to create event loop")?;
208 let pty_tx = event_loop.channel();
209 let _io_thread = event_loop.spawn();
210
211 Ok(PtySender {
212 notifier: Notifier(pty_tx),
213 })
214}
215
216pub(super) fn resize(term: &mut AlacrittyTerm, bounds: TerminalBounds) {
217 term.resize(bounds);
218}
219
220pub(super) fn display_offset(term: &AlacrittyTerm) -> usize {
221 term.grid().display_offset()
222}
223
224pub(super) fn scroll_display(term: &mut AlacrittyTerm, scroll: Scroll) {
225 term.scroll_display(scroll.to_alacritty());
226}
227
228pub(super) fn set_selection(term: &mut AlacrittyTerm, selection: Option<&Selection>) {
229 term.selection = selection.map(Selection::to_alacritty);
230}
231
232pub(super) fn update_selection(
233 term: &mut AlacrittyTerm,
234 point: Point,
235 side: SelectionSide,
236) -> bool {
237 let Some(mut selection) = term.selection.take() else {
238 return false;
239 };
240 selection.update(point.to_alacritty(), side.to_alacritty());
241 term.selection = Some(selection);
242 true
243}
244
245pub(super) fn selection_text(term: &AlacrittyTerm) -> Option<String> {
246 term.selection_to_string()
247}
248
249pub(super) fn scroll_to_point(term: &mut AlacrittyTerm, point: Point) {
250 term.scroll_to_point(point.to_alacritty());
251}
252
253pub(super) fn vi_goto_point(term: &mut AlacrittyTerm, point: Point) {
254 term.vi_goto_point(point.to_alacritty());
255}
256
257pub(super) fn toggle_vi_mode(term: &mut AlacrittyTerm) {
258 term.toggle_vi_mode();
259}
260
261pub(super) fn vi_motion(term: &mut AlacrittyTerm, motion: ViMotion) {
262 term.vi_motion(motion.to_alacritty());
263}
264
265fn alacritty_cursor_style(cursor_shape: SettingsCursorShape) -> AlacCursorStyle {
266 AlacCursorStyle {
267 shape: alacritty_cursor_shape(cursor_shape),
268 blinking: false,
269 }
270}
271
272fn alacritty_cursor_shape(cursor_shape: SettingsCursorShape) -> AlacCursorShape {
273 match cursor_shape {
274 SettingsCursorShape::Block => AlacCursorShape::Block,
275 SettingsCursorShape::Underline => AlacCursorShape::Underline,
276 SettingsCursorShape::Bar => AlacCursorShape::Beam,
277 SettingsCursorShape::Hollow => AlacCursorShape::HollowBlock,
278 }
279}
280
281impl Dimensions for TerminalBounds {
282 /// Note: this is supposed to be for the back buffer's length,
283 /// but we exclusively use it to resize the terminal, which does not
284 /// use this method. We still have to implement it for the trait though,
285 /// hence, this comment.
286 fn total_lines(&self) -> usize {
287 self.screen_lines()
288 }
289
290 fn screen_lines(&self) -> usize {
291 self.num_lines()
292 }
293
294 fn columns(&self) -> usize {
295 self.num_columns()
296 }
297}
298
299impl From<AlacTermEvent> for TerminalBackendEvent {
300 fn from(event: AlacTermEvent) -> Self {
301 match event {
302 AlacTermEvent::MouseCursorDirty => Self::MouseCursorDirty,
303 AlacTermEvent::Title(title) => Self::Title(title),
304 AlacTermEvent::ResetTitle => Self::ResetTitle,
305 AlacTermEvent::ClipboardStore(_, data) => Self::ClipboardStore(data),
306 AlacTermEvent::ClipboardLoad(_, format) => Self::ClipboardLoad(format),
307 AlacTermEvent::ColorRequest(index, format) => Self::ColorRequest(index, format),
308 AlacTermEvent::PtyWrite(output) => Self::PtyWrite(output),
309 AlacTermEvent::TextAreaSizeRequest(format) => {
310 Self::TextAreaSizeRequest(Arc::new(move |bounds| {
311 format(window_size_from_terminal_bounds(bounds))
312 }))
313 }
314 AlacTermEvent::CursorBlinkingChange => Self::CursorBlinkingChange,
315 AlacTermEvent::Wakeup => Self::Wakeup,
316 AlacTermEvent::Bell => Self::Bell,
317 AlacTermEvent::Exit => Self::Exit,
318 AlacTermEvent::ChildExit(status) => Self::ChildExit(status),
319 }
320 }
321}
322
323impl EventListener for ZedListener {
324 fn send_event(&self, event: AlacTermEvent) {
325 self.0.unbounded_send(PtyEvent::Event(event.into())).ok();
326 }
327}
328
329impl Scroll {
330 fn to_alacritty(self) -> AlacScroll {
331 match self {
332 Self::Delta(delta) => AlacScroll::Delta(delta),
333 Self::PageUp => AlacScroll::PageUp,
334 Self::PageDown => AlacScroll::PageDown,
335 Self::Top => AlacScroll::Top,
336 Self::Bottom => AlacScroll::Bottom,
337 }
338 }
339}
340
341impl ViMotion {
342 fn to_alacritty(self) -> AlacViMotion {
343 match self {
344 Self::Up => AlacViMotion::Up,
345 Self::Down => AlacViMotion::Down,
346 Self::Left => AlacViMotion::Left,
347 Self::Right => AlacViMotion::Right,
348 Self::First => AlacViMotion::First,
349 Self::Last => AlacViMotion::Last,
350 Self::FirstOccupied => AlacViMotion::FirstOccupied,
351 Self::High => AlacViMotion::High,
352 Self::Middle => AlacViMotion::Middle,
353 Self::Low => AlacViMotion::Low,
354 Self::WordLeft => AlacViMotion::WordLeft,
355 Self::WordRight => AlacViMotion::WordRight,
356 Self::WordRightEnd => AlacViMotion::WordRightEnd,
357 Self::Bracket => AlacViMotion::Bracket,
358 Self::ParagraphUp => AlacViMotion::ParagraphUp,
359 Self::ParagraphDown => AlacViMotion::ParagraphDown,
360 }
361 }
362}
363
364impl Search {
365 pub fn new(search: &str) -> Option<Self> {
366 Some(Self {
367 search: AlacrittySearch {
368 search: RegexSearch::new(search).ok()?,
369 },
370 })
371 }
372
373 fn into_alacritty(self) -> RegexSearch {
374 self.search.search
375 }
376}
377
378impl SelectionSide {
379 fn to_alacritty(self) -> AlacDirection {
380 match self {
381 Self::Left => AlacDirection::Left,
382 Self::Right => AlacDirection::Right,
383 }
384 }
385}
386
387impl SelectionType {
388 fn to_alacritty(self) -> AlacSelectionType {
389 match self {
390 Self::Simple => AlacSelectionType::Simple,
391 Self::Semantic => AlacSelectionType::Semantic,
392 Self::Lines => AlacSelectionType::Lines,
393 }
394 }
395}
396
397impl Selection {
398 fn to_alacritty(&self) -> AlacSelection {
399 let mut selection = AlacSelection::new(
400 self.ty.to_alacritty(),
401 self.start.point.to_alacritty(),
402 self.start.side.to_alacritty(),
403 );
404 if self.start.point != self.end.point || self.start.side != self.end.side {
405 selection.update(self.end.point.to_alacritty(), self.end.side.to_alacritty());
406 }
407 selection
408 }
409}
410
411impl Hyperlink {
412 pub fn new<T: ToString>(id: Option<T>, uri: String) -> Self {
413 Self {
414 data: HyperlinkData::Owned {
415 id: id.map(|id| Arc::from(id.to_string())),
416 uri: Arc::from(uri),
417 },
418 }
419 }
420
421 pub fn id(&self) -> Option<&str> {
422 match &self.data {
423 HyperlinkData::Alacritty(hyperlink) => Some(hyperlink.id()),
424 HyperlinkData::Owned { id, .. } => id.as_deref(),
425 }
426 }
427
428 pub fn uri(&self) -> &str {
429 match &self.data {
430 HyperlinkData::Alacritty(hyperlink) => hyperlink.uri(),
431 HyperlinkData::Owned { uri, .. } => uri,
432 }
433 }
434
435 fn from_alacritty(hyperlink: AlacHyperlink) -> Self {
436 Self {
437 data: HyperlinkData::Alacritty(hyperlink),
438 }
439 }
440}
441
442fn terminal_hyperlink_from_alacritty(hyperlink: AlacHyperlink) -> Hyperlink {
443 Hyperlink::from_alacritty(hyperlink)
444}
445
446impl From<Hyperlink> for AlacHyperlink {
447 fn from(hyperlink: Hyperlink) -> Self {
448 match hyperlink.data {
449 HyperlinkData::Alacritty(hyperlink) => hyperlink,
450 HyperlinkData::Owned { id, uri } => Self::new(id.as_deref(), uri.to_string()),
451 }
452 }
453}
454
455fn terminal_cell_from_alacritty(cell: &AlacCell) -> Cell {
456 Cell { cell: cell.clone() }
457}
458
459impl Cell {
460 #[inline]
461 pub fn character(&self) -> char {
462 self.cell.c
463 }
464
465 #[cfg(test)]
466 pub(crate) fn set_character(&mut self, character: char) {
467 self.cell.c = character;
468 }
469
470 #[inline]
471 pub fn foreground(&self) -> Color {
472 self.cell.fg
473 }
474
475 #[inline]
476 pub fn background(&self) -> Color {
477 self.cell.bg
478 }
479
480 #[inline]
481 pub fn zerowidth(&self) -> Option<&[char]> {
482 self.cell.zerowidth()
483 }
484
485 #[cfg(test)]
486 pub(crate) fn push_zerowidth(&mut self, character: char) {
487 self.cell.push_zerowidth(character);
488 }
489
490 #[inline]
491 pub fn hyperlink(&self) -> Option<Hyperlink> {
492 self.cell.hyperlink().map(terminal_hyperlink_from_alacritty)
493 }
494
495 #[inline]
496 pub fn is_inverse(&self) -> bool {
497 self.cell.flags.contains(Flags::INVERSE)
498 }
499
500 #[inline]
501 pub fn is_wide_char_spacer(&self) -> bool {
502 self.cell.flags.contains(Flags::WIDE_CHAR_SPACER)
503 }
504
505 #[inline]
506 pub fn is_dim(&self) -> bool {
507 self.cell.flags.intersects(Flags::DIM)
508 }
509
510 #[inline]
511 pub fn has_underline(&self) -> bool {
512 self.cell.flags.intersects(Flags::ALL_UNDERLINES)
513 }
514
515 #[inline]
516 pub fn has_undercurl(&self) -> bool {
517 self.cell.flags.contains(Flags::UNDERCURL)
518 }
519
520 #[inline]
521 pub fn has_strikeout(&self) -> bool {
522 self.cell.flags.intersects(Flags::STRIKEOUT)
523 }
524
525 #[inline]
526 pub fn is_bold(&self) -> bool {
527 self.cell.flags.intersects(Flags::BOLD)
528 }
529
530 #[inline]
531 pub fn is_italic(&self) -> bool {
532 self.cell.flags.intersects(Flags::ITALIC)
533 }
534
535 #[inline]
536 pub fn has_visible_style_modifier(&self) -> bool {
537 self.cell
538 .flags
539 .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
540 }
541}
542
543impl<'a> RenderableCells<'a> {
544 pub(super) fn new(cells: GridIterator<'a, AlacCell>) -> Self {
545 Self { cells }
546 }
547}
548
549impl Iterator for RenderableCells<'_> {
550 type Item = IndexedCell;
551
552 fn next(&mut self) -> Option<Self::Item> {
553 self.cells.next().map(|cell| IndexedCell {
554 point: terminal_point_from_alacritty(cell.point),
555 cell: terminal_cell_from_alacritty(cell.cell),
556 })
557 }
558
559 fn size_hint(&self) -> (usize, Option<usize>) {
560 self.cells.size_hint()
561 }
562}
563
564impl Modes {
565 #[cfg(test)]
566 fn to_alacritty(self) -> TermMode {
567 let mut mode = TermMode::empty();
568 add_alacritty_mode(&mut mode, self, Self::APP_CURSOR, TermMode::APP_CURSOR);
569 add_alacritty_mode(&mut mode, self, Self::APP_KEYPAD, TermMode::APP_KEYPAD);
570 add_alacritty_mode(&mut mode, self, Self::SHOW_CURSOR, TermMode::SHOW_CURSOR);
571 add_alacritty_mode(&mut mode, self, Self::LINE_WRAP, TermMode::LINE_WRAP);
572 add_alacritty_mode(&mut mode, self, Self::ORIGIN, TermMode::ORIGIN);
573 add_alacritty_mode(&mut mode, self, Self::INSERT, TermMode::INSERT);
574 add_alacritty_mode(
575 &mut mode,
576 self,
577 Self::LINE_FEED_NEW_LINE,
578 TermMode::LINE_FEED_NEW_LINE,
579 );
580 add_alacritty_mode(&mut mode, self, Self::FOCUS_IN_OUT, TermMode::FOCUS_IN_OUT);
581 add_alacritty_mode(
582 &mut mode,
583 self,
584 Self::ALTERNATE_SCROLL,
585 TermMode::ALTERNATE_SCROLL,
586 );
587 add_alacritty_mode(
588 &mut mode,
589 self,
590 Self::BRACKETED_PASTE,
591 TermMode::BRACKETED_PASTE,
592 );
593 add_alacritty_mode(&mut mode, self, Self::SGR_MOUSE, TermMode::SGR_MOUSE);
594 add_alacritty_mode(&mut mode, self, Self::UTF8_MOUSE, TermMode::UTF8_MOUSE);
595 add_alacritty_mode(&mut mode, self, Self::ALT_SCREEN, TermMode::ALT_SCREEN);
596 add_alacritty_mode(
597 &mut mode,
598 self,
599 Self::MOUSE_REPORT_CLICK,
600 TermMode::MOUSE_REPORT_CLICK,
601 );
602 add_alacritty_mode(&mut mode, self, Self::MOUSE_DRAG, TermMode::MOUSE_DRAG);
603 add_alacritty_mode(&mut mode, self, Self::MOUSE_MOTION, TermMode::MOUSE_MOTION);
604 add_alacritty_mode(&mut mode, self, Self::VI, TermMode::VI);
605 mode
606 }
607}
608
609fn terminal_modes_from_alacritty(mode: TermMode) -> Modes {
610 let mut terminal_modes = Modes::empty();
611 add_terminal_mode(
612 &mut terminal_modes,
613 mode,
614 TermMode::APP_CURSOR,
615 Modes::APP_CURSOR,
616 );
617 add_terminal_mode(
618 &mut terminal_modes,
619 mode,
620 TermMode::APP_KEYPAD,
621 Modes::APP_KEYPAD,
622 );
623 add_terminal_mode(
624 &mut terminal_modes,
625 mode,
626 TermMode::SHOW_CURSOR,
627 Modes::SHOW_CURSOR,
628 );
629 add_terminal_mode(
630 &mut terminal_modes,
631 mode,
632 TermMode::LINE_WRAP,
633 Modes::LINE_WRAP,
634 );
635 add_terminal_mode(&mut terminal_modes, mode, TermMode::ORIGIN, Modes::ORIGIN);
636 add_terminal_mode(&mut terminal_modes, mode, TermMode::INSERT, Modes::INSERT);
637 add_terminal_mode(
638 &mut terminal_modes,
639 mode,
640 TermMode::LINE_FEED_NEW_LINE,
641 Modes::LINE_FEED_NEW_LINE,
642 );
643 add_terminal_mode(
644 &mut terminal_modes,
645 mode,
646 TermMode::FOCUS_IN_OUT,
647 Modes::FOCUS_IN_OUT,
648 );
649 add_terminal_mode(
650 &mut terminal_modes,
651 mode,
652 TermMode::ALTERNATE_SCROLL,
653 Modes::ALTERNATE_SCROLL,
654 );
655 add_terminal_mode(
656 &mut terminal_modes,
657 mode,
658 TermMode::BRACKETED_PASTE,
659 Modes::BRACKETED_PASTE,
660 );
661 add_terminal_mode(
662 &mut terminal_modes,
663 mode,
664 TermMode::SGR_MOUSE,
665 Modes::SGR_MOUSE,
666 );
667 add_terminal_mode(
668 &mut terminal_modes,
669 mode,
670 TermMode::UTF8_MOUSE,
671 Modes::UTF8_MOUSE,
672 );
673 add_terminal_mode(
674 &mut terminal_modes,
675 mode,
676 TermMode::ALT_SCREEN,
677 Modes::ALT_SCREEN,
678 );
679 add_terminal_mode(
680 &mut terminal_modes,
681 mode,
682 TermMode::MOUSE_REPORT_CLICK,
683 Modes::MOUSE_REPORT_CLICK,
684 );
685 add_terminal_mode(
686 &mut terminal_modes,
687 mode,
688 TermMode::MOUSE_DRAG,
689 Modes::MOUSE_DRAG,
690 );
691 add_terminal_mode(
692 &mut terminal_modes,
693 mode,
694 TermMode::MOUSE_MOTION,
695 Modes::MOUSE_MOTION,
696 );
697 add_terminal_mode(&mut terminal_modes, mode, TermMode::VI, Modes::VI);
698 terminal_modes
699}
700
701fn add_terminal_mode(
702 terminal_modes: &mut Modes,
703 alacritty_modes: TermMode,
704 alacritty_mode: TermMode,
705 terminal_mode: Modes,
706) {
707 if alacritty_modes.contains(alacritty_mode) {
708 terminal_modes.insert(terminal_mode);
709 }
710}
711
712#[cfg(test)]
713fn add_alacritty_mode(
714 alacritty_modes: &mut TermMode,
715 terminal_modes: Modes,
716 terminal_mode: Modes,
717 alacritty_mode: TermMode,
718) {
719 if terminal_modes.contains(terminal_mode) {
720 alacritty_modes.insert(alacritty_mode);
721 }
722}
723
724impl Cursor {
725 fn from_alacritty(cursor: RenderableCursor) -> Self {
726 Self {
727 shape: terminal_cursor_shape_from_alacritty(cursor.shape),
728 point: terminal_point_from_alacritty(cursor.point),
729 }
730 }
731}
732
733fn terminal_cursor_shape_from_alacritty(shape: AlacCursorShape) -> CursorShape {
734 match shape {
735 AlacCursorShape::Block => CursorShape::Block,
736 AlacCursorShape::Underline => CursorShape::Underline,
737 AlacCursorShape::Beam => CursorShape::Bar,
738 AlacCursorShape::HollowBlock => CursorShape::HollowBlock,
739 AlacCursorShape::Hidden => CursorShape::Hidden,
740 }
741}
742
743impl Point {
744 fn to_alacritty(self) -> AlacPoint {
745 AlacPoint::new(Line(self.line), Column(self.column))
746 }
747}
748
749fn terminal_point_from_alacritty(point: AlacPoint) -> Point {
750 Point {
751 line: point.line.0,
752 column: point.column.0,
753 }
754}
755
756impl Range {
757 #[cfg(test)]
758 fn to_alacritty(self) -> RangeInclusive<AlacPoint> {
759 self.start.to_alacritty()..=self.end.to_alacritty()
760 }
761
762 fn from_alacritty(range: RangeInclusive<AlacPoint>) -> Self {
763 Self {
764 start: terminal_point_from_alacritty(*range.start()),
765 end: terminal_point_from_alacritty(*range.end()),
766 }
767 }
768}
769
770fn terminal_selection_range_from_alacritty(range: AlacSelectionRange) -> SelectionRange {
771 SelectionRange {
772 start: terminal_point_from_alacritty(range.start),
773 end: terminal_point_from_alacritty(range.end),
774 is_block: range.is_block,
775 }
776}
777
778pub(super) fn clear_saved_screen(term: &mut Term<ZedListener>) {
779 term.clear_screen(ClearMode::Saved);
780
781 let cursor = term.grid().cursor.point;
782
783 term.grid_mut().reset_region(..cursor.line);
784
785 let line = term.grid()[cursor.line][..Column(term.grid().columns())]
786 .iter()
787 .cloned()
788 .enumerate()
789 .collect::<Vec<(usize, AlacCell)>>();
790
791 for (index, cell) in line {
792 term.grid_mut()[Line(0)][Column(index)] = cell;
793 }
794
795 term.grid_mut().cursor.point = AlacPoint::new(Line(0), term.grid_mut().cursor.point.column);
796 let new_cursor = term.grid().cursor.point;
797
798 if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
799 term.grid_mut().reset_region((new_cursor.line + 1)..);
800 }
801}
802
803pub(super) fn shrink_to_used(term: &mut Term<ZedListener>) {
804 term.grid_mut().truncate();
805}
806
807pub(super) fn make_content(term: &Term<ZedListener>, last_content: &Content) -> Content {
808 let content = term.renderable_content();
809
810 let estimated_size = content.display_iter.size_hint().0;
811 let mut cells = Vec::with_capacity(estimated_size);
812
813 cells.extend(content.display_iter.map(|ic| IndexedCell {
814 point: terminal_point_from_alacritty(ic.point),
815 cell: terminal_cell_from_alacritty(ic.cell),
816 }));
817
818 let selection_text = if content.selection.is_some() {
819 term.selection_to_string()
820 } else {
821 None
822 };
823
824 let bottom_line = term.screen_lines() as i32 - 1 - content.display_offset as i32;
825 let bottom_row_occupied = content.cursor.point.line.0 >= bottom_line
826 || cells
827 .iter()
828 .rev()
829 .take_while(|cell| cell.point.line >= bottom_line)
830 .any(|cell| cell.cell.character() != ' ');
831
832 Content {
833 cells,
834 mode: terminal_modes_from_alacritty(content.mode),
835 display_offset: content.display_offset,
836 selection_text,
837 selection: content
838 .selection
839 .map(terminal_selection_range_from_alacritty),
840 cursor: Cursor::from_alacritty(content.cursor),
841 cursor_char: term.grid()[content.cursor.point].c,
842 terminal_bounds: last_content.terminal_bounds,
843 last_hovered_word: last_content.last_hovered_word.clone(),
844 scrolled_to_top: content.display_offset == term.history_size(),
845 scrolled_to_bottom: content.display_offset == 0,
846 bottom_row_occupied,
847 }
848}
849
850pub(super) fn content_text(term: &Term<ZedListener>) -> String {
851 let start = AlacPoint::new(term.topmost_line(), Column(0));
852 let end = AlacPoint::new(term.bottommost_line(), term.last_column());
853 term.bounds_to_string(start, end)
854}
855
856pub(super) fn total_lines(term: &Term<ZedListener>) -> usize {
857 term.total_lines()
858}
859
860pub(super) fn screen_lines(term: &Term<ZedListener>) -> usize {
861 term.screen_lines()
862}
863
864pub(super) fn full_content_range(term: &Term<ZedListener>) -> Range {
865 let start = AlacPoint::new(term.topmost_line(), Column(0));
866 let end = AlacPoint::new(term.bottommost_line(), term.last_column());
867 Range::from_alacritty(start..=end)
868}
869
870pub(super) fn last_non_empty_lines(term: &Term<ZedListener>, line_count: usize) -> Vec<String> {
871 let grid = term.grid();
872 let mut lines = Vec::new();
873
874 let mut current_line = grid.bottommost_line().0;
875 let topmost_line = grid.topmost_line().0;
876
877 while current_line >= topmost_line && lines.len() < line_count {
878 let (logical_line_start, logical_line) =
879 logical_line_for_row(grid, current_line, topmost_line);
880
881 if let Some(line) = process_line(logical_line) {
882 lines.push(line);
883 }
884
885 current_line = logical_line_start - 1;
886 }
887
888 lines.reverse();
889 lines
890}
891
892pub(super) fn update_vi_cursor_for_scroll(term: &mut Term<ZedListener>, scroll: Scroll) {
893 match scroll {
894 Scroll::Delta(delta) => {
895 term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta);
896 }
897 Scroll::PageUp => {
898 let lines = term.screen_lines() as i32;
899 term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines);
900 }
901 Scroll::PageDown => {
902 let lines = -(term.screen_lines() as i32);
903 term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines);
904 }
905 Scroll::Top => {
906 let point = AlacPoint::new(term.topmost_line(), Column(0));
907 term.vi_mode_cursor = ViModeCursor::new(point);
908 }
909 Scroll::Bottom => {
910 let point = AlacPoint::new(term.bottommost_line(), Column(0));
911 term.vi_mode_cursor = ViModeCursor::new(point);
912 }
913 }
914}
915
916pub(super) fn update_selection_to_vi_cursor(term: &mut Term<ZedListener>) -> Option<Point> {
917 let mut selection = term.selection.take()?;
918 let point = term.vi_mode_cursor.point;
919 selection.update(point, AlacDirection::Right);
920 term.selection = Some(selection);
921 Some(terminal_point_from_alacritty(point))
922}
923
924pub(super) fn find_from_terminal_point(
925 term: &AlacrittyTerm,
926 point: Point,
927 regex_searches: &mut RegexSearches,
928 path_style: PathStyle,
929) -> Option<HyperlinkMatch> {
930 let point = point.to_alacritty().grid_clamp(term, Boundary::Grid);
931 hyperlinks::find_from_grid_point(term, point, regex_searches, path_style)
932}
933
934fn logical_line_for_row(grid: &Grid<AlacCell>, current: i32, topmost: i32) -> (i32, String) {
935 let start = find_logical_line_start(grid, current, topmost);
936 let mut line = String::new();
937 for row in start..=current {
938 line.push_str(&row_to_string(&grid[Line(row)]));
939 }
940 (start, line)
941}
942
943fn find_logical_line_start(grid: &Grid<AlacCell>, current: i32, topmost: i32) -> i32 {
944 let mut line_start = current;
945 while line_start > topmost {
946 let previous_line = Line(line_start - 1);
947 let last_cell = &grid[previous_line][Column(grid.columns() - 1)];
948 if !last_cell.flags.contains(Flags::WRAPLINE) {
949 break;
950 }
951 line_start -= 1;
952 }
953 line_start
954}
955
956fn row_to_string(row: &Row<AlacCell>) -> String {
957 row[..Column(row.len())]
958 .iter()
959 .map(|cell| cell.c)
960 .collect::<String>()
961}
962
963fn process_line(line: String) -> Option<String> {
964 let trimmed = line.trim_end().to_string();
965 if !trimmed.is_empty() {
966 Some(trimmed)
967 } else {
968 None
969 }
970}
971
972/// Appends a stringified task summary to the terminal, after its output.
973///
974/// SAFETY: This function should only be called after terminal's PTY is no longer alive.
975/// New text being added to the terminal here, uses "less public" APIs,
976/// which are not maintaining the entire terminal state intact.
977///
978///
979/// The library
980///
981/// * does not increment inner grid cursor's _lines_ on `input` calls
982/// (but displaying the lines correctly and incrementing cursor's columns)
983///
984/// * ignores `\n` and \r` character input, requiring the `newline` call instead
985///
986/// * does not alter grid state after `newline` call
987/// so its `bottommost_line` is always the same additions, and
988/// the cursor's `point` is not updated to the new line and column values
989///
990/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic.
991/// Still, subsequent `append_text_to_term` invocations are possible and display the contents correctly.
992///
993/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations,
994/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone.
995/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized.
996/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts.
997pub(super) unsafe fn append_text_to_term(term: &mut Term<ZedListener>, text_lines: &[&str]) {
998 term.newline();
999 term.grid_mut().cursor.point.column = Column(0);
1000 for line in text_lines {
1001 for character in line.chars() {
1002 term.input(character);
1003 }
1004 term.newline();
1005 term.grid_mut().cursor.point.column = Column(0);
1006 }
1007}
1008
1009pub(super) fn search_matches(term: &AlacrittyTerm, searcher: Search) -> Vec<Range> {
1010 let mut searcher = searcher.into_alacritty();
1011 all_search_matches(term, &mut searcher)
1012 .map(Range::from_alacritty)
1013 .collect()
1014}
1015
1016fn all_search_matches<'a, T>(
1017 term: &'a Term<T>,
1018 regex: &'a mut RegexSearch,
1019) -> impl Iterator<Item = Match> + 'a {
1020 let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
1021 let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
1022 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use std::sync::Arc;
1028
1029 use super::*;
1030
1031 #[test]
1032 fn terminal_hyperlink_from_alacritty_keeps_alacritty_storage() {
1033 let hyperlink = AlacHyperlink::new(Some("id"), "https://example.com".to_string());
1034 let hyperlink = terminal_hyperlink_from_alacritty(hyperlink);
1035
1036 assert!(matches!(&hyperlink.data, HyperlinkData::Alacritty(_)));
1037 assert_eq!(hyperlink.id(), Some("id"));
1038 assert_eq!(hyperlink.uri(), "https://example.com");
1039 }
1040
1041 #[test]
1042 fn terminal_cell_from_alacritty_shares_extra_storage() {
1043 let mut cell = AlacCell::default();
1044 cell.push_zerowidth('a');
1045
1046 let converted = terminal_cell_from_alacritty(&cell);
1047
1048 match (&cell.extra, &converted.cell.extra) {
1049 (Some(extra), Some(converted_extra)) => assert!(Arc::ptr_eq(extra, converted_extra)),
1050 _ => panic!("expected extra storage on both cells"),
1051 }
1052 }
1053
1054 #[test]
1055 fn terminal_modes_round_trip_alacritty_flags() {
1056 let alacritty_modes = TermMode::APP_CURSOR
1057 | TermMode::BRACKETED_PASTE
1058 | TermMode::ALT_SCREEN
1059 | TermMode::MOUSE_DRAG
1060 | TermMode::SGR_MOUSE
1061 | TermMode::VI;
1062
1063 let terminal_modes = terminal_modes_from_alacritty(alacritty_modes);
1064 assert!(terminal_modes.contains(Modes::APP_CURSOR));
1065 assert!(terminal_modes.contains(Modes::BRACKETED_PASTE));
1066 assert!(terminal_modes.contains(Modes::ALT_SCREEN));
1067 assert!(terminal_modes.contains(Modes::MOUSE_DRAG));
1068 assert!(terminal_modes.intersects(Modes::MOUSE_MODE));
1069 assert!(terminal_modes.contains(Modes::SGR_MOUSE));
1070 assert!(terminal_modes.contains(Modes::VI));
1071 assert!(!terminal_modes.contains(Modes::MOUSE_REPORT_CLICK));
1072
1073 let alacritty_modes = terminal_modes.to_alacritty();
1074 assert!(alacritty_modes.contains(TermMode::APP_CURSOR));
1075 assert!(alacritty_modes.contains(TermMode::BRACKETED_PASTE));
1076 assert!(alacritty_modes.contains(TermMode::ALT_SCREEN));
1077 assert!(alacritty_modes.contains(TermMode::MOUSE_DRAG));
1078 assert!(alacritty_modes.contains(TermMode::SGR_MOUSE));
1079 assert!(alacritty_modes.contains(TermMode::VI));
1080 assert!(!alacritty_modes.contains(TermMode::MOUSE_REPORT_CLICK));
1081 }
1082
1083 #[test]
1084 fn terminal_selection_range_round_trip_alacritty_range() {
1085 let alacritty_range = AlacSelectionRange {
1086 start: AlacPoint::new(Line(-2), Column(3)),
1087 end: AlacPoint::new(Line(4), Column(8)),
1088 is_block: true,
1089 };
1090
1091 let terminal_range = terminal_selection_range_from_alacritty(alacritty_range);
1092 assert_eq!(
1093 terminal_range,
1094 SelectionRange {
1095 start: Point {
1096 line: -2,
1097 column: 3
1098 },
1099 end: Point { line: 4, column: 8 },
1100 is_block: true,
1101 }
1102 );
1103 }
1104}
1105