Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:15:46.025Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

alacritty.rs

1105 lines · 34.6 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.