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