duat_term/
lib.rs

1//! A terminal implementation for Duat's Ui
2//!
3//! This implementation is a sort of trial of the "preparedness" of
4//! [`RawUi`]'s API, in order to figure out what should be included
5//! and what shouldn't.
6use std::{
7    fmt::Debug,
8    io::{self, Write},
9    sync::{
10        Arc, Mutex,
11        atomic::{AtomicBool, Ordering},
12        mpsc,
13    },
14    time::Duration,
15};
16
17use crossterm::{
18    cursor,
19    event::{self, Event as CtEvent, poll as ct_poll, read as ct_read},
20    execute, queue,
21    style::ContentStyle,
22    terminal::{self, ClearType},
23};
24use duat_core::{
25    context::DuatSender,
26    form::{self, Color},
27    session::UiMouseEvent,
28    ui::{
29        self,
30        traits::{RawArea, RawUi},
31    },
32};
33
34use self::printer::Printer;
35pub use self::{
36    area::{Area, Coords},
37    layout::{Frame, FrameStyle},
38    printer::{Border, BorderStyle},
39    rules::{SepChar, VertRule, VertRuleBuilder},
40    shared_fns::*,
41};
42use crate::layout::Layouts;
43
44mod area;
45mod layout;
46mod printer;
47mod rules;
48
49mod shared_fns {
50    use std::sync::OnceLock;
51
52    use crate::FrameStyle;
53
54    /// Execution address space functions
55    #[derive(Clone, Copy)]
56    pub(super) struct ExecSpaceFns {
57        set_default_frame_style: fn(FrameStyle),
58    }
59
60    impl Default for ExecSpaceFns {
61        fn default() -> Self {
62            Self {
63                set_default_frame_style: crate::layout::set_default_frame_style,
64            }
65        }
66    }
67
68    pub(super) static FNS: OnceLock<ExecSpaceFns> = OnceLock::new();
69
70    /// Sets the default [`FrameStyle`] for all spawned [`Area`]s
71    ///
72    /// By default, it is [`FrameStyle::Regular`], which uses
73    /// characters like `─`, `│` and `┐`.
74    ///
75    /// [`Area`]: crate::Area
76    pub fn set_default_frame_style(frame_style: FrameStyle) {
77        (FNS.get().unwrap().set_default_frame_style)(frame_style)
78    }
79
80    /// Resetting functions, right before reloading.
81    pub(super) fn reset_state() {
82        set_default_frame_style(FrameStyle::Regular);
83    }
84}
85
86/// The [`RawUi`] implementation for `duat-term`
87pub struct Ui {
88    mt: Mutex<InnerUi>,
89    fns: shared_fns::ExecSpaceFns,
90}
91
92struct InnerUi {
93    windows: Vec<(Area, Arc<Printer>)>,
94    layouts: Layouts,
95    win: usize,
96    frame: Border,
97    printer_fn: fn() -> Arc<Printer>,
98    rx: Option<mpsc::Receiver<Event>>,
99    tx: mpsc::Sender<Event>,
100}
101
102// SAFETY: The Area (the part that is not Send + Sync) is only ever
103// accessed from the main thread.
104unsafe impl Send for InnerUi {}
105
106impl RawUi for Ui {
107    type Area = Area;
108
109    fn get_once() -> Option<&'static Self> {
110        static GOT: AtomicBool = AtomicBool::new(false);
111        let (tx, rx) = mpsc::channel();
112        shared_fns::FNS.set(ExecSpaceFns::default()).ok().unwrap();
113
114        (!GOT.fetch_or(true, Ordering::Relaxed)).then(|| {
115            Box::leak(Box::new(Self {
116                mt: Mutex::new(InnerUi {
117                    windows: Vec::new(),
118                    layouts: Layouts::default(),
119                    win: 0,
120                    frame: Border::default(),
121                    printer_fn: || Arc::new(Printer::new()),
122                    rx: Some(rx),
123                    tx,
124                }),
125                fns: ExecSpaceFns::default(),
126            })) as &'static Self
127        })
128    }
129
130    fn config_address_space_setup(&'static self) {
131        shared_fns::FNS.set(self.fns).ok().unwrap();
132    }
133
134    fn open(&self, duat_tx: DuatSender) {
135        use event::{KeyboardEnhancementFlags as KEF, PushKeyboardEnhancementFlags};
136
137        form::set_weak("rule.upper", "default.VertRule");
138        form::set_weak("rule.lower", "default.VertRule");
139
140        let term_rx = self.mt.lock().unwrap().rx.take().unwrap();
141        let term_tx = self.mt.lock().unwrap().tx.clone();
142
143        let print_thread = std::thread::Builder::new().name("print loop".to_string());
144        let _ = print_thread.spawn(move || {
145            // Wait for everything to be setup before doing anything to the
146            // terminal, for a less jarring effect.
147            let Ok(Event::NewPrinter(mut printer)) = term_rx.recv() else {
148                unreachable!("Failed to load the Ui");
149            };
150
151            terminal::enable_raw_mode().unwrap();
152
153            // Initial terminal setup
154            // Some key chords (like alt+shift+o for some reason) don't work
155            // without this.
156            execute!(
157                io::stdout(),
158                terminal::EnterAlternateScreen,
159                terminal::Clear(ClearType::All),
160                terminal::DisableLineWrap,
161                event::EnableFocusChange,
162                event::EnableMouseCapture
163            )
164            .unwrap();
165
166            if let Ok(true) = terminal::supports_keyboard_enhancement() {
167                execute!(
168                    io::stdout(),
169                    PushKeyboardEnhancementFlags(
170                        KEF::DISAMBIGUATE_ESCAPE_CODES | KEF::REPORT_ALTERNATE_KEYS
171                    )
172                )
173                .unwrap();
174            }
175
176            loop {
177                match term_rx.recv() {
178                    Ok(Event::Print) => printer.print(),
179                    Ok(Event::UpdatePrinter) => printer.update(true, true),
180                    Ok(Event::ClearPrinter) => printer.clear(),
181                    Ok(Event::NewPrinter(new_printer)) => printer = new_printer,
182                    Ok(Event::Quit) => break,
183                    Err(_) => {}
184                }
185            }
186        });
187
188        let _ = std::thread::Builder::new()
189            .name("crossterm".to_string())
190            .spawn(move || {
191                loop {
192                    let Ok(true) = ct_poll(Duration::from_millis(20)) else {
193                        continue;
194                    };
195
196                    match ct_read() {
197                        Ok(CtEvent::Key(key)) => {
198                            if !matches!(key.kind, event::KeyEventKind::Release) {
199                                duat_tx.send_key(key);
200                            }
201                        }
202                        Ok(CtEvent::Resize(..)) => {
203                            term_tx.send(Event::UpdatePrinter).unwrap();
204                            duat_tx.send_resize();
205                        }
206                        Ok(CtEvent::FocusGained) => duat_tx.send_focused(),
207                        Ok(CtEvent::FocusLost) => duat_tx.send_unfocused(),
208                        Ok(CtEvent::Mouse(event)) => duat_tx.send_mouse(UiMouseEvent {
209                            coord: ui::Coord {
210                                x: event.column as f32,
211                                y: event.row as f32,
212                            },
213                            kind: event.kind,
214                            modifiers: event.modifiers,
215                        }),
216                        Err(_) => {}
217                    }
218                }
219            });
220    }
221
222    fn close(&self) {
223        if self.mt.lock().unwrap().tx.send(Event::Quit).is_err() {
224            return;
225        }
226
227        if let Ok(true) = terminal::supports_keyboard_enhancement() {
228            queue!(io::stdout(), event::PopKeyboardEnhancementFlags).unwrap();
229        }
230
231        execute!(
232            io::stdout(),
233            terminal::Clear(ClearType::All),
234            terminal::LeaveAlternateScreen,
235            cursor::MoveToColumn(0),
236            terminal::Clear(ClearType::FromCursorDown),
237            terminal::EnableLineWrap,
238            event::DisableFocusChange,
239            event::DisableMouseCapture,
240            cursor::Show,
241        )
242        .unwrap();
243
244        terminal::disable_raw_mode().unwrap();
245    }
246
247    fn new_root(&self, cache: <Self::Area as RawArea>::Cache) -> Self::Area {
248        let mut ui = self.mt.lock().unwrap();
249        let printer = (ui.printer_fn)();
250
251        // SAFETY: Ui::MetaStatics is not Send + Sync, so this can't be called
252        // from another thread
253        let main_id = ui.layouts.new_layout(printer.clone(), ui.frame, cache);
254
255        let root = Area::new(main_id, ui.layouts.clone());
256        ui.windows.push((root.clone(), printer.clone()));
257        if ui.windows.len() == 1 {
258            ui.tx.send(Event::NewPrinter(printer)).unwrap();
259        }
260
261        root
262    }
263
264    fn new_dyn_spawned(
265        &self,
266        id: ui::SpawnId,
267        specs: ui::DynSpawnSpecs,
268        cache: <Self::Area as RawArea>::Cache,
269        win: usize,
270    ) -> Self::Area {
271        let ui = self.mt.lock().unwrap();
272        let id = ui.layouts.spawn_on_text(id, specs, cache, win);
273
274        Area::new(id, ui.layouts.clone())
275    }
276
277    fn new_static_spawned(
278        &self,
279        id: ui::SpawnId,
280        specs: ui::StaticSpawnSpecs,
281        cache: <Self::Area as RawArea>::Cache,
282        win: usize,
283    ) -> Self::Area {
284        let ui = self.mt.lock().unwrap();
285        let id = ui.layouts.spawn_static(id, specs, cache, win);
286
287        Area::new(id, ui.layouts.clone())
288    }
289
290    fn switch_window(&self, win: usize) {
291        let mut ui = self.mt.lock().unwrap();
292        ui.win = win;
293        let printer = ui.windows[win].1.clone();
294        printer.update(true, true);
295        ui.tx.send(Event::NewPrinter(printer)).unwrap()
296    }
297
298    fn flush_layout(&self) {
299        let ui = self.mt.lock().unwrap();
300        if let Some((_, printer)) = ui.windows.get(ui.win) {
301            printer.update(false, false);
302        }
303    }
304
305    fn print(&self) {
306        self.mt.lock().unwrap().tx.send(Event::Print).unwrap();
307    }
308
309    fn load(&'static self) {
310        // Hook for returning to regular terminal state
311        std::panic::set_hook(Box::new(|info| {
312            self.close();
313            println!("{info}");
314        }));
315    }
316
317    fn unload(&self) {
318        let mut ui = self.mt.lock().unwrap();
319        ui.windows = Vec::new();
320        // SAFETY: Ui is not Send + Sync, so this can't be called
321        // from another thread
322        ui.layouts.reset();
323        ui.win = 0;
324        ui.tx.send(Event::ClearPrinter).unwrap();
325        shared_fns::reset_state();
326    }
327
328    fn remove_window(&self, win: usize) {
329        let mut ui = self.mt.lock().unwrap();
330        ui.windows.remove(win);
331        // SAFETY: Ui is not Send + Sync, so this can't be called
332        // from another thread
333        ui.layouts.remove_window(win);
334        if ui.win > win {
335            ui.win -= 1;
336        }
337    }
338
339    fn size(&'static self) -> ui::Coord {
340        let ui = self.mt.lock().unwrap();
341        ui.windows[0].1.update(false, false);
342        let coord = ui.windows[0].1.max_value();
343        ui::Coord { x: coord.x as f32, y: coord.y as f32 }
344    }
345}
346
347#[derive(Debug)]
348pub enum Anchor {
349    TopLeft,
350    TopRight,
351    BottomLeft,
352    BottomRight,
353}
354
355enum Event {
356    Print,
357    UpdatePrinter,
358    ClearPrinter,
359    NewPrinter(Arc<Printer>),
360    Quit,
361}
362
363impl Eq for Event {}
364
365impl PartialEq for Event {
366    fn eq(&self, other: &Self) -> bool {
367        core::mem::discriminant(self) == core::mem::discriminant(other)
368    }
369}
370
371#[derive(Clone, Copy, PartialEq)]
372pub struct AreaId(usize);
373
374impl AreaId {
375    /// Generates a unique index for [`Rect`]s.
376    fn new() -> Self {
377        use std::sync::atomic::{AtomicUsize, Ordering};
378        static INDEX_COUNTER: AtomicUsize = AtomicUsize::new(0);
379
380        AreaId(INDEX_COUNTER.fetch_add(1, Ordering::Relaxed))
381    }
382}
383
384impl std::fmt::Debug for AreaId {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        f.write_fmt(format_args!("{}", self.0))
387    }
388}
389
390type Equality = kasuari::Constraint;
391
392#[rustfmt::skip]
393macro_rules! color_values {
394    ($name:ident, $p:literal, $s:literal) => {
395        macro_rules! c {
396            ($n:literal) => {
397                concat!($p, $n, $s).as_bytes()
398            }
399        }
400        
401        const $name: [&[u8]; 256] = [
402            c!(0), c!(1), c!(2), c!(3), c!(4), c!(5), c!(6), c!(7), c!(8), c!(9), c!(10), c!(11),
403            c!(12), c!(13), c!(14), c!(15), c!(16), c!(17), c!(18), c!(19), c!(20), c!(21), c!(22),
404            c!(23), c!(24), c!(25), c!(26), c!(27), c!(28), c!(29), c!(30), c!(31), c!(32), c!(33),
405            c!(34), c!(35), c!(36), c!(37), c!(38), c!(39), c!(40), c!(41), c!(42), c!(43), c!(44),
406            c!(45), c!(46), c!(47), c!(48), c!(49), c!(50), c!(51), c!(52), c!(53), c!(54), c!(55),
407            c!(56), c!(57), c!(58), c!(59), c!(60), c!(61), c!(62), c!(63), c!(64), c!(65), c!(66),
408            c!(67), c!(68), c!(69), c!(70), c!(71), c!(72), c!(73), c!(74), c!(75), c!(76), c!(77),
409            c!(78), c!(79), c!(80), c!(81), c!(82), c!(83), c!(84), c!(85), c!(86), c!(87), c!(88),
410            c!(89), c!(90), c!(91), c!(92), c!(93), c!(94), c!(95), c!(96), c!(97), c!(98), c!(99),
411            c!(100), c!(101), c!(102), c!(103), c!(104), c!(105), c!(106), c!(107), c!(108),
412            c!(109), c!(110), c!(111), c!(112), c!(113), c!(114), c!(115), c!(116), c!(117),
413            c!(118), c!(119), c!(120), c!(121), c!(122), c!(123), c!(124), c!(125), c!(126),
414            c!(127), c!(128), c!(129), c!(130), c!(131), c!(132), c!(133), c!(134), c!(135),
415            c!(136), c!(137), c!(138), c!(139), c!(140), c!(141), c!(142), c!(143), c!(144),
416            c!(145), c!(146), c!(147), c!(148), c!(149), c!(150), c!(151), c!(152), c!(153),
417            c!(154), c!(155), c!(156), c!(157), c!(158), c!(159), c!(160), c!(161), c!(162),
418            c!(163), c!(164), c!(165), c!(166), c!(167), c!(168), c!(169), c!(170), c!(171),
419            c!(172), c!(173), c!(174), c!(175), c!(176), c!(177), c!(178), c!(179), c!(180),
420            c!(181), c!(182), c!(183), c!(184), c!(185), c!(186), c!(187), c!(188), c!(189),
421            c!(190), c!(191), c!(192), c!(193), c!(194), c!(195), c!(196), c!(197), c!(198),
422            c!(199), c!(200), c!(201), c!(202), c!(203), c!(204), c!(205), c!(206), c!(207),
423            c!(208), c!(209), c!(210), c!(211), c!(212), c!(213), c!(214), c!(215), c!(216),
424            c!(217), c!(218), c!(219), c!(220), c!(221), c!(222), c!(223), c!(224), c!(225),
425            c!(226), c!(227), c!(228), c!(229), c!(230), c!(231), c!(232), c!(233), c!(234),
426            c!(235), c!(236), c!(237), c!(238), c!(239), c!(240), c!(241), c!(242), c!(243),
427            c!(244), c!(245), c!(246), c!(247), c!(248), c!(249), c!(250), c!(251), c!(252),
428            c!(253), c!(254), c!(255),
429        ];
430    }
431}
432
433fn print_style(w: &mut impl Write, style: ContentStyle) {
434    use crossterm::style::Attribute::{self, *};
435    const ATTRIBUTES: [(Attribute, &[u8]); 10] = [
436        (Reset, b"0"),
437        (Bold, b"1"),
438        (Dim, b"2"),
439        (Italic, b"3"),
440        (Underlined, b"4"),
441        (DoubleUnderlined, b"4;2"),
442        (Undercurled, b"4;3"),
443        (Underdotted, b"4;4"),
444        (Underdashed, b"4;5"),
445        (Reverse, b"7"),
446    ];
447    color_values!(U8, "", "");
448    color_values!(U8_SC, "", ";");
449    color_values!(U8_FG_RGB, "38;2;", ";");
450    color_values!(U8_FG_ANSI, "38;5;", ";");
451    color_values!(U8_BG_RGB, "48;2;", ";");
452    color_values!(U8_BG_ANSI, "48;5;", ";");
453    color_values!(U8_UL_RGB, "58;2;", ";");
454    color_values!(U8_UL_ANSI, "58;5;", ";");
455
456    w.write_all(b"\x1b[").unwrap();
457
458    let mut semicolon = false;
459    if !style.attributes.is_empty() {
460        for (attr, code) in ATTRIBUTES {
461            if style.attributes.has(attr) {
462                if semicolon {
463                    w.write_all(b";").unwrap()
464                }
465                w.write_all(code).unwrap();
466                semicolon = true;
467            }
468        }
469    }
470
471    let semicolon = if let Some(color) = style.foreground_color {
472        if semicolon {
473            w.write_all(b";").unwrap();
474        }
475        match color {
476            Color::Reset => w.write_all(b"39").unwrap(),
477            Color::Black => w.write_all(b"30").unwrap(),
478            Color::DarkRed => w.write_all(b"31").unwrap(),
479            Color::DarkGreen => w.write_all(b"32").unwrap(),
480            Color::DarkYellow => w.write_all(b"33").unwrap(),
481            Color::DarkBlue => w.write_all(b"34").unwrap(),
482            Color::DarkMagenta => w.write_all(b"35").unwrap(),
483            Color::DarkCyan => w.write_all(b"36").unwrap(),
484            Color::Grey => w.write_all(b"37").unwrap(),
485            Color::DarkGrey => w.write_all(b"90").unwrap(),
486            Color::Red => w.write_all(b"91").unwrap(),
487            Color::Green => w.write_all(b"92").unwrap(),
488            Color::Yellow => w.write_all(b"93").unwrap(),
489            Color::Blue => w.write_all(b"94").unwrap(),
490            Color::Magenta => w.write_all(b"95").unwrap(),
491            Color::Cyan => w.write_all(b"96").unwrap(),
492            Color::White => w.write_all(b"97").unwrap(),
493            Color::Rgb { r, g, b } => {
494                w.write_all(U8_FG_RGB[r as usize]).unwrap();
495                w.write_all(U8_SC[g as usize]).unwrap();
496                w.write_all(U8[b as usize]).unwrap()
497            }
498            Color::AnsiValue(val) => w.write_all(U8_FG_ANSI[val as usize]).unwrap(),
499        };
500        true
501    } else {
502        semicolon
503    };
504
505    let semicolon = if let Some(color) = style.background_color {
506        if semicolon {
507            w.write_all(b";").unwrap();
508        }
509        match color {
510            Color::Reset => w.write_all(b"49").unwrap(),
511            Color::Black => w.write_all(b"40").unwrap(),
512            Color::DarkRed => w.write_all(b"41").unwrap(),
513            Color::DarkGreen => w.write_all(b"42").unwrap(),
514            Color::DarkYellow => w.write_all(b"43").unwrap(),
515            Color::DarkBlue => w.write_all(b"44").unwrap(),
516            Color::DarkMagenta => w.write_all(b"45").unwrap(),
517            Color::DarkCyan => w.write_all(b"46").unwrap(),
518            Color::Grey => w.write_all(b"47").unwrap(),
519            Color::DarkGrey => w.write_all(b"100").unwrap(),
520            Color::Red => w.write_all(b"101").unwrap(),
521            Color::Green => w.write_all(b"102").unwrap(),
522            Color::Yellow => w.write_all(b"103").unwrap(),
523            Color::Blue => w.write_all(b"104").unwrap(),
524            Color::Magenta => w.write_all(b"105").unwrap(),
525            Color::Cyan => w.write_all(b"106").unwrap(),
526            Color::White => w.write_all(b"107").unwrap(),
527            Color::Rgb { r, g, b } => {
528                w.write_all(U8_BG_RGB[r as usize]).unwrap();
529                w.write_all(U8_SC[g as usize]).unwrap();
530                w.write_all(U8[b as usize]).unwrap()
531            }
532            Color::AnsiValue(val) => w.write_all(U8_BG_ANSI[val as usize]).unwrap(),
533        };
534        true
535    } else {
536        semicolon
537    };
538
539    if let Some(color) = style.underline_color {
540        if semicolon {
541            w.write_all(b";").unwrap();
542        }
543        match color {
544            Color::Reset => w.write_all(b"59").unwrap(),
545            Color::Black => w.write_all(b"58;0").unwrap(),
546            Color::DarkRed => w.write_all(b"58;1").unwrap(),
547            Color::DarkGreen => w.write_all(b"58;2").unwrap(),
548            Color::DarkYellow => w.write_all(b"58;3").unwrap(),
549            Color::DarkBlue => w.write_all(b"58;4").unwrap(),
550            Color::DarkMagenta => w.write_all(b"58;5").unwrap(),
551            Color::DarkCyan => w.write_all(b"58;6").unwrap(),
552            Color::Grey => w.write_all(b"58;7").unwrap(),
553            Color::DarkGrey => w.write_all(b"58;8").unwrap(),
554            Color::Red => w.write_all(b"58;9").unwrap(),
555            Color::Green => w.write_all(b"58;10").unwrap(),
556            Color::Yellow => w.write_all(b"58;11").unwrap(),
557            Color::Blue => w.write_all(b"58;12").unwrap(),
558            Color::Magenta => w.write_all(b"58;13").unwrap(),
559            Color::Cyan => w.write_all(b"58;14").unwrap(),
560            Color::White => w.write_all(b"58;15").unwrap(),
561            Color::Rgb { r, g, b } => {
562                w.write_all(U8_UL_RGB[r as usize]).unwrap();
563                w.write_all(U8_SC[g as usize]).unwrap();
564                w.write_all(U8[b as usize]).unwrap()
565            }
566            Color::AnsiValue(val) => w.write_all(U8_UL_ANSI[val as usize]).unwrap(),
567        };
568    }
569
570    w.write_all(b"m").unwrap();
571}
572
573/// The priority for edges for areas that must not overlap
574const EDGE_PRIO: kasuari::Strength = kasuari::Strength::REQUIRED;
575/// The priority for manually defined lengths
576const MANUAL_LEN_PRIO: kasuari::Strength = kasuari::Strength::new(12.0);
577/// The priority for lengths defined when creating Areas
578const LEN_PRIO: kasuari::Strength = kasuari::Strength::new(11.0);
579/// The priority for borders
580const BORDER_PRIO: kasuari::Strength = kasuari::Strength::new(10.0);
581/// The priority for hiding things
582const HIDDEN_PRIO: kasuari::Strength = kasuari::Strength::new(9.0);
583/// The priority for the length of spawned Areas
584const SPAWN_LEN_PRIO: kasuari::Strength = kasuari::Strength::new(8.0);
585/// The priority for positioning of dynamically spawned Areas
586const DYN_SPAWN_POS_PRIO: kasuari::Strength = kasuari::Strength::new(7.0);
587/// The priority for the center and len variables of spawned Areas
588const SPAWN_DIMS_PRIO: kasuari::Strength = kasuari::Strength::new(6.0);
589/// The priority for frames around spawned Areas
590const FRAME_PRIO: kasuari::Strength = kasuari::Strength::new(5.0);
591/// The priority for the length of spawned Areas
592const CONS_SPAWN_LEN_PRIO: kasuari::Strength = kasuari::Strength::new(4.0);
593/// The priority for positioning of statically spawned Areas
594const STATIC_SPAWN_POS_PRIO: kasuari::Strength = kasuari::Strength::new(3.0);
595/// The priority for the alignment of spawned Areas
596const SPAWN_ALIGN_PRIO: kasuari::Strength = kasuari::Strength::new(2.0);
597/// The priority for lengths that should try to be equal (a.k.a Files)
598const EQ_LEN_PRIO: kasuari::Strength = kasuari::Strength::new(1.0);