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