Skip to main content

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