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