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