extui/
lib.rs

1//! An opinionated, minimal terminal UI crate.
2//!
3//! # Rect based rendering and layout
4//!
5//! [Rect] is the core abstraction in extui, use builder style methods to
6//! paint to the screen using an [DoubleBuffer] for rendering to screen
7//! efficiently.
8//!
9//! ```no_run
10//! use extui::{Rect, DoubleBuffer, Style, Color};
11//!
12//! fn render_list(items: &[&str], mut area: Rect, buf: &mut DoubleBuffer) {
13//!     for item in items {
14//!         if area.h == 0 {
15//!             break;
16//!         }
17//!         area.take_top(1).with(Style::DEFAULT).text(buf, item);
18//!     }
19//! }
20//! ```
21//!
22//! # Deliberate Limitations
23//!
24//! extui intentionally omits certain features for simpler interfaces and better
25//! performance:
26//!
27//! - **8-bit color only** - No 24-bit (true color) support. The 256-color palette
28//!   covers most use cases with significantly simpler encoding.
29//! - **4-byte grapheme limit** - Characters exceeding 4 bytes are truncated. This
30//!   enables fixed-size [`Cell`] storage. Most text fits within this limit.
31//! - **Unix only** - No Windows support. This allows direct use of POSIX APIs
32//!   without abstraction overhead.
33//!
34//! # Getting Started
35//!
36//! ```no_run
37//! use extui::{Terminal, TerminalFlags, DoubleBuffer, Style, Color};
38//! use extui::event::{self, Event, KeyCode, Events};
39//!
40//! // Open terminal in raw mode with alternate screen
41//! let mut term = Terminal::open(
42//!     TerminalFlags::RAW_MODE | TerminalFlags::ALT_SCREEN | TerminalFlags::HIDE_CURSOR
43//! )?;
44//!
45//! let (w, h) = term.size()?;
46//! let mut buf = DoubleBuffer::new(w, h);
47//! let mut events = Events::default();
48//!
49//! loop {
50//!     // Render
51//!     buf.rect().with(Color::Blue1.as_bg()).fill(&mut buf);
52//!     buf.rect().with(Style::DEFAULT).text(&mut buf, "Press 'q' to quit");
53//!     buf.render(&mut term);
54//!
55//!     // Poll for events
56//!     if event::poll(&std::io::stdin(), None)?.is_readable() {
57//!         events.read_from(&std::io::stdin())?;
58//!         while let Some(ev) = events.next(term.is_raw()) {
59//!             if let Event::Key(key) = ev {
60//!                 if key.code == KeyCode::Char('q') {
61//!                     return Ok(());
62//!                 }
63//!             }
64//!         }
65//!     }
66//! }
67//! # Ok::<(), std::io::Error>(())
68//! ```
69
70use std::{
71    io::{IsTerminal, Write},
72    mem::ManuallyDrop,
73    os::fd::{AsFd, FromRawFd, RawFd},
74};
75
76use unicode_segmentation::UnicodeSegmentation;
77use unicode_width::UnicodeWidthStr;
78
79use crate::{
80    event::KeyboardEnhancementFlags,
81    vt::{BufferWrite, Modifier, MoveCursor, MoveCursorRight, ScrollBufferDown, ScrollBufferUp},
82};
83
84pub mod event;
85mod sys;
86pub mod vt;
87pub mod widget;
88
89/// Writes multiple VT escape sequences to a byte buffer.
90///
91/// Takes a mutable reference to a `Vec<u8>` and any number of expressions
92/// that implement [`BufferWrite`]. Each expression is written sequentially
93/// to the buffer.
94///
95/// # Examples
96///
97/// ```
98/// use extui::{splat, vt::{MoveCursor, CLEAR_STYLE}};
99///
100/// let mut buf = Vec::new();
101/// splat!(&mut buf, MoveCursor(0, 0), CLEAR_STYLE);
102/// ```
103///
104/// [`BufferWrite`]: crate::vt::BufferWrite
105#[macro_export]
106macro_rules! splat {
107    ($out: expr, $($expr:expr),* $(,)?) => {{
108        use $crate::vt::BufferWrite;
109        let out: &mut Vec<u8> = $out;
110        $(
111            $expr.write_to_buffer(out);
112        )*
113    }};
114}
115
116/// A single terminal cell containing styled text.
117///
118/// Stores a grapheme cluster (up to 4 bytes) along with its associated
119/// [`Style`]. Cells are the fundamental unit for terminal rendering.
120///
121#[derive(Clone, Copy, PartialEq, Eq)]
122pub struct Cell(u64);
123
124impl std::fmt::Debug for Cell {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        f.debug_struct("Cell")
127            .field("style", &self.style())
128            .field("text", &self.text())
129            .finish()
130    }
131}
132
133/// Horizontal text alignment within a region.
134#[derive(Clone, Copy, PartialEq, Eq, Debug)]
135pub enum Alignment {
136    /// Aligns text to the left edge.
137    Left,
138    /// Centers text horizontally.
139    Center,
140    /// Aligns text to the right edge.
141    Right,
142}
143
144/// A relative position within a 3x3 grid.
145///
146/// Used to specify which parts of a box border or region to render.
147#[repr(u8)]
148#[derive(Copy, Clone)]
149pub enum Rel {
150    /// Top-left corner.
151    TopLeft,
152    /// Top center edge.
153    TopCenter,
154    /// Top-right corner.
155    TopRight,
156    /// Center-left edge.
157    CenterLeft,
158    /// Center of the region.
159    CenterCenter,
160    /// Center-right edge.
161    CenterRight,
162    /// Bottom-left corner.
163    BottomLeft,
164    /// Bottom center edge.
165    BottomCenter,
166    /// Bottom-right corner.
167    BottomRight,
168}
169
170/// A set of relative positions for partial box rendering.
171///
172/// Supports bitwise OR to combine positions and check membership.
173///
174/// # Examples
175///
176/// ```
177/// use extui::{RelSet, Rel};
178///
179/// let edges = RelSet::TOP | RelSet::BOTTOM;
180/// assert!(edges.contains(Rel::TopLeft));
181/// assert!(!edges.contains(Rel::CenterLeft));
182/// ```
183#[derive(Copy, Clone)]
184pub struct RelSet(u16);
185
186impl std::ops::BitOr for RelSet {
187    type Output = RelSet;
188
189    fn bitor(self, rhs: Self) -> Self::Output {
190        RelSet(self.0 | rhs.0)
191    }
192}
193impl std::ops::BitAnd for RelSet {
194    type Output = RelSet;
195
196    fn bitand(self, rhs: Self) -> Self::Output {
197        RelSet(self.0 & rhs.0)
198    }
199}
200
201impl RelSet {
202    /// All positions along the top edge.
203    pub const TOP: RelSet = RelSet::new(&[Rel::TopLeft, Rel::TopCenter, Rel::TopRight]);
204    /// All positions along the bottom edge.
205    pub const BOTTOM: RelSet = RelSet::new(&[Rel::BottomLeft, Rel::BottomCenter, Rel::BottomRight]);
206    /// All positions along the left edge.
207    pub const LEFT: RelSet = RelSet::new(&[Rel::TopLeft, Rel::CenterLeft, Rel::BottomLeft]);
208    /// All positions along the right edge.
209    pub const RIGHT: RelSet = RelSet::new(&[Rel::TopRight, Rel::CenterRight, Rel::BottomRight]);
210    /// All border positions (excludes center).
211    pub const BOX: RelSet = RelSet(0b111_101_111);
212
213    /// Creates a new set from a slice of positions.
214    pub const fn new(mut rels: &[Rel]) -> RelSet {
215        let mut bits = 0u16;
216        while let [rel, rest @ ..] = rels {
217            bits |= 1 << (*rel as u8);
218            rels = rest;
219        }
220        RelSet(bits)
221    }
222
223    /// Returns `true` if the set contains the given position.
224    pub const fn contains(self, rel: Rel) -> bool {
225        (self.0 & (1 << (rel as u8))) != 0
226    }
227
228    /// Returns `true` if the set is empty.
229    pub const fn is_empty(self) -> bool {
230        self.0 == 0
231    }
232}
233
234/// Characters used to draw box borders.
235///
236/// Provides predefined styles like [`ASCII`](Self::ASCII) and [`LIGHT`](Self::LIGHT).
237pub struct BoxStyle {
238    /// Top-left corner character.
239    pub top_left: Cell,
240    /// Top-right corner character.
241    pub top_right: Cell,
242    /// Bottom-right corner character.
243    pub bottom_right: Cell,
244    /// Bottom-left corner character.
245    pub bottom_left: Cell,
246    /// Horizontal line character.
247    pub horizontal: Cell,
248    /// Vertical line character.
249    pub vertical: Cell,
250}
251impl BoxStyle {
252    /// ASCII box drawing characters (`+`, `-`, `|`).
253    pub const ASCII: BoxStyle = unsafe {
254        BoxStyle {
255            top_right: Cell::new_ascii(b'+', Style::DEFAULT),
256            top_left: Cell::new_ascii(b'+', Style::DEFAULT),
257            bottom_right: Cell::new_ascii(b'+', Style::DEFAULT),
258            bottom_left: Cell::new_ascii(b'+', Style::DEFAULT),
259            horizontal: Cell::new_ascii(b'-', Style::DEFAULT),
260            vertical: Cell::new_ascii(b'|', Style::DEFAULT),
261        }
262    };
263    /// Unicode light box drawing characters (`┌`, `─`, `│`, etc.).
264    pub const LIGHT: BoxStyle = BoxStyle {
265        top_right: Cell::new_const("┐", Style::DEFAULT),
266        top_left: Cell::new_const("┌", Style::DEFAULT),
267        bottom_right: Cell::new_const("┘", Style::DEFAULT),
268        bottom_left: Cell::new_const("└", Style::DEFAULT),
269        horizontal: Cell::new_const("─", Style::DEFAULT),
270        vertical: Cell::new_const("│", Style::DEFAULT),
271    };
272}
273
274impl BoxStyle {
275    /// Renders a complete box border within the given rectangle.
276    ///
277    /// Returns the inner rectangle after subtracting the border.
278    pub fn render(&self, rect: Rect, buf: &mut DoubleBuffer) -> Rect {
279        self.render_partial(rect, buf, RelSet::BOX)
280    }
281
282    /// Renders only the specified parts of a box border.
283    ///
284    /// Returns the inner rectangle after subtracting the rendered edges.
285    pub fn render_partial(&self, mut rect: Rect, buf: &mut DoubleBuffer, set: RelSet) -> Rect {
286        let Rect { x, y, w, h } = rect;
287        if w == 0 || h == 0 {
288            return rect;
289        }
290
291        if let Some(row) = buf.current.row_remaining_mut(x, y) {
292            if w >= 1 && set.contains(Rel::TopLeft) {
293                row[0] = self.top_left;
294            }
295            if set.contains(Rel::TopCenter) {
296                for cell in row
297                    .iter_mut()
298                    .take(w as usize)
299                    .skip(1)
300                    .take(w.saturating_sub(2) as usize)
301                {
302                    *cell = self.horizontal;
303                }
304            }
305            if w >= 2 && set.contains(Rel::TopRight) {
306                row[w as usize - 1] = self.top_right;
307            }
308        }
309        if let Some(row) = buf.current.row_remaining_mut(x, y + h - 1) {
310            if w >= 1 && set.contains(Rel::BottomLeft) {
311                row[0] = self.bottom_left;
312            }
313            if set.contains(Rel::BottomCenter) {
314                for cell in row
315                    .iter_mut()
316                    .take(w as usize)
317                    .skip(1)
318                    .take(w.saturating_sub(2) as usize)
319                {
320                    *cell = self.horizontal;
321                }
322            }
323            if w >= 2 && set.contains(Rel::BottomRight) {
324                row[w as usize - 1] = self.bottom_right;
325            }
326        }
327
328        for y in y + ((!(set & RelSet::TOP).is_empty()) as u16)
329            ..y + h - ((!(set & RelSet::BOTTOM).is_empty()) as u16)
330        {
331            if set.contains(Rel::CenterLeft)
332                && let Some(row) = buf.current.row_remaining_mut(x, y) {
333                    row[0] = self.vertical;
334                }
335            if set.contains(Rel::CenterRight)
336                && let Some(row) = buf.current.row_remaining_mut(x + w - 1, y) {
337                    row[0] = self.vertical;
338                }
339        }
340        // Subjects sides that have any to provide the inner vec
341        if !(set & RelSet::TOP).is_empty() {
342            rect.y += 1;
343            rect.h = rect.h.saturating_sub(1);
344        }
345        if !(set & RelSet::BOTTOM).is_empty() {
346            rect.h = rect.h.saturating_sub(1);
347        }
348        if !(set & RelSet::LEFT).is_empty() {
349            rect.x += 1;
350            rect.w = rect.w.saturating_sub(1);
351        }
352        if !(set & RelSet::RIGHT).is_empty() {
353            rect.w = rect.w.saturating_sub(1);
354        }
355        rect
356    }
357}
358
359/// Tracks a style transition for efficient escape sequence generation.
360///
361/// Computes the minimal set of escape codes needed to change from the
362/// current style to the target style.
363pub struct StyleDelta {
364    current: u32,
365    target: Style,
366}
367
368impl StyleDelta {
369    /// Sets the previous style for delta computation.
370    pub fn with_previous(self, style: Style) -> StyleDelta {
371        StyleDelta {
372            current: style.0,
373            target: self.target,
374        }
375    }
376}
377
378/// Text styling attributes including foreground color, background color, and modifiers.
379///
380/// Combines colors and text modifiers (bold, italic, etc.) into a single value.
381/// Use [`with_fg`](Self::with_fg), [`with_bg`](Self::with_bg), and
382/// [`with_modifier`](Self::with_modifier) to build styles.
383///
384/// # Examples
385///
386/// ```
387/// use extui::{Style, Color, vt::Modifier};
388///
389/// let style = Style::DEFAULT
390///     .with_fg(Color::Red1)
391///     .with_bg(Color::Black)
392///     .with_modifier(Modifier::BOLD);
393/// ```
394#[derive(Clone, Copy, PartialEq, Eq, Default)]
395pub struct Style(u32);
396
397impl std::fmt::Debug for Style {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        f.debug_struct("Style")
400            .field("fg", &self.fg())
401            .field("bg", &self.bg())
402            .field("modifiers", &self.modifiers())
403            .finish()
404    }
405}
406
407/// A 256-color palette index.
408///
409/// Provides named constants for common colors and a grayscale ramp.
410/// The inner `u8` represents the ANSI 256-color palette index.
411///
412/// # Examples
413///
414/// ```
415/// use extui::Color;
416///
417/// let red = Color::Red1;
418/// let gray = Color::Grey[15];
419/// let custom = Color(42);
420/// ```
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct Color(pub u8);
423
424#[allow(non_upper_case_globals)]
425impl Color {
426    pub const NavyBlue: Color = Color(17);
427    pub const DarkBlue: Color = Color(18);
428    pub const Blue3: Color = Color(19);
429    pub const Blue1: Color = Color(21);
430    pub const DarkGreen: Color = Color(22);
431    pub const DeepSkyBlue4: Color = Color(23);
432    pub const DodgerBlue3: Color = Color(26);
433    pub const DodgerBlue2: Color = Color(27);
434    pub const Green4: Color = Color(28);
435    pub const SpringGreen4: Color = Color(29);
436    pub const Turquoise4: Color = Color(30);
437    pub const DeepSkyBlue3: Color = Color(31);
438    pub const DodgerBlue1: Color = Color(33);
439    pub const Green3: Color = Color(34);
440    pub const DarkCyan: Color = Color(36);
441    pub const DeepSkyBlue2: Color = Color(38);
442    pub const DeepSkyBlue1: Color = Color(39);
443    pub const SpringGreen3: Color = Color(41);
444    pub const SpringGreen: Color = Color(42);
445    pub const Cyan3: Color = Color(43);
446    pub const DarkTurquoise: Color = Color(44);
447    pub const Turquoise2: Color = Color(45);
448    pub const Green1: Color = Color(46);
449    pub const SpringGreen2: Color = Color(47);
450    pub const SpringGreen1: Color = Color(48);
451    pub const MediumSpringGreen: Color = Color(49);
452    pub const Cyan2: Color = Color(50);
453    pub const Cyan1: Color = Color(51);
454    pub const DarkRed: Color = Color(52);
455    pub const DeepPink4: Color = Color(53);
456    pub const Purple3: Color = Color(56);
457    pub const BlueViolet: Color = Color(57);
458    pub const Orange4: Color = Color(58);
459    pub const MediumPurple4: Color = Color(60);
460    pub const SlateBlue3: Color = Color(61);
461    pub const RoyalBlue1: Color = Color(63);
462    pub const Chartreuse4: Color = Color(64);
463    pub const PaleTurquoise4: Color = Color(66);
464    pub const SteelBlue: Color = Color(67);
465    pub const SteelBlue3: Color = Color(68);
466    pub const CornflowerBlue: Color = Color(69);
467    pub const Chartreuse3: Color = Color(70);
468    pub const DarkSeaGreen4: Color = Color(71);
469    pub const CadetBlue: Color = Color(72);
470    pub const SkyBlue3: Color = Color(74);
471    pub const SteelBlue1: Color = Color(75);
472    pub const PaleGreen3: Color = Color(77);
473    pub const SeaGreen3: Color = Color(78);
474    pub const Aquamarine3: Color = Color(79);
475    pub const MediumTurquoise: Color = Color(80);
476    pub const Chartreuse2: Color = Color(82);
477    pub const Aquamarine1: Color = Color(86);
478    pub const DarkSlateGray2: Color = Color(87);
479    pub const DarkViolet: Color = Color(92);
480    pub const Purple: Color = Color(93);
481    pub const LightPink4: Color = Color(95);
482    pub const Plum4: Color = Color(96);
483    pub const SlateBlue1: Color = Color(99);
484    pub const Yellow4: Color = Color(100);
485    pub const Wheat4: Color = Color(101);
486    pub const LightSlateGrey: Color = Color(103);
487    pub const MediumPurple: Color = Color(104);
488    pub const LightSlateBlue: Color = Color(105);
489    pub const DarkOliveGreen3: Color = Color(107);
490    pub const DarkSeaGreen: Color = Color(108);
491    pub const Grey: [Color; 31] = [
492        Color(16),
493        Color(232),
494        Color(233),
495        Color(234),
496        Color(235),
497        Color(236),
498        Color(237),
499        Color(238),
500        Color(239),
501        Color(240),
502        Color(59),
503        Color(241),
504        Color(242),
505        Color(243),
506        Color(244),
507        Color(102),
508        Color(245),
509        Color(246),
510        Color(247),
511        Color(139),
512        Color(248),
513        Color(145),
514        Color(249),
515        Color(250),
516        Color(251),
517        Color(252),
518        Color(188),
519        Color(253),
520        Color(254),
521        Color(255),
522        Color(231),
523    ];
524    pub const SkyBlue2: Color = Color(111);
525    pub const DarkOliveGreen: Color = Color(113);
526    pub const DarkSeaGreen3: Color = Color(115);
527    pub const DarkSlateGray3: Color = Color(116);
528    pub const SkyBlue1: Color = Color(117);
529    pub const Chartreuse1: Color = Color(118);
530    pub const LightGreen: Color = Color(119);
531    pub const PaleGreen1: Color = Color(121);
532    pub const DarkSlateGray1: Color = Color(123);
533    pub const Red3: Color = Color(124);
534    pub const MediumVioletRed: Color = Color(126);
535    pub const Magenta3: Color = Color(127);
536    pub const DarkOrange3: Color = Color(130);
537    pub const IndianRed: Color = Color(131);
538    pub const HotPink3: Color = Color(132);
539    pub const MediumOrchid3: Color = Color(133);
540    pub const MediumOrchid: Color = Color(134);
541    pub const DarkGoldenrod: Color = Color(136);
542    pub const LightSalmon3: Color = Color(137);
543    pub const RosyBrown: Color = Color(138);
544    pub const Violet: Color = Color(140);
545    pub const MediumPurple1: Color = Color(141);
546    pub const Gold3: Color = Color(142);
547    pub const DarkKhaki: Color = Color(143);
548    pub const NavajoWhite3: Color = Color(144);
549    pub const LightSteelBlue3: Color = Color(146);
550    pub const LightSteelBlue: Color = Color(147);
551    pub const Yellow3: Color = Color(148);
552    pub const LightCyan3: Color = Color(152);
553    pub const LightSkyBlue1: Color = Color(153);
554    pub const GreenYellow: Color = Color(154);
555    pub const DarkOliveGreen2: Color = Color(155);
556    pub const DarkSeaGreen1: Color = Color(158);
557    pub const PaleTurquoise1: Color = Color(159);
558    pub const Magenta2: Color = Color(165);
559    pub const HotPink2: Color = Color(169);
560    pub const Orchid: Color = Color(170);
561    pub const MediumOrchid1: Color = Color(171);
562    pub const Orange3: Color = Color(172);
563    pub const LightPink3: Color = Color(174);
564    pub const Pink3: Color = Color(175);
565    pub const Plum3: Color = Color(176);
566    pub const LightGoldenrod3: Color = Color(179);
567    pub const Tan: Color = Color(180);
568    pub const MistyRose3: Color = Color(181);
569    pub const Thistle3: Color = Color(182);
570    pub const Plum2: Color = Color(183);
571    pub const Khaki3: Color = Color(185);
572    pub const LightYellow3: Color = Color(187);
573    pub const LightSteelBlue1: Color = Color(189);
574    pub const Yellow2: Color = Color(190);
575    pub const DarkOliveGreen1: Color = Color(191);
576    pub const LightSeaGreen: Color = Color(193);
577    pub const Honeydew: Color = Color(194);
578    pub const LightCyan1: Color = Color(195);
579    pub const Red1: Color = Color(196);
580    pub const DeepPink2: Color = Color(197);
581    pub const DeepPink1: Color = Color(198);
582    pub const Magenta1: Color = Color(201);
583    pub const OrangeRed1: Color = Color(202);
584    pub const NeonRed: Color = Color(203);
585    pub const HotPink: Color = Color(205);
586    pub const DarkOrange: Color = Color(208);
587    pub const Salmon: Color = Color(209);
588    pub const LightCoral: Color = Color(210);
589    pub const PaleVioletRed1: Color = Color(211);
590    pub const Orchid2: Color = Color(212);
591    pub const Orchid1: Color = Color(213);
592    pub const Orange1: Color = Color(214);
593    pub const SandyBrown: Color = Color(215);
594    pub const LightSalmon1: Color = Color(216);
595    pub const LightPink1: Color = Color(217);
596    pub const Pink1: Color = Color(218);
597    pub const Plum1: Color = Color(219);
598    pub const Gold1: Color = Color(220);
599    pub const LightGoldenrod2: Color = Color(221);
600    pub const NavajoWhite: Color = Color(223);
601    pub const MistyRose: Color = Color(224);
602    pub const Thistle: Color = Color(225);
603    pub const Yellow1: Color = Color(226);
604    pub const LightGoldenrod1: Color = Color(227);
605    pub const Khaki1: Color = Color(228);
606    pub const Wheat1: Color = Color(229);
607    pub const Cornsilk1: Color = Color(230);
608
609    pub const White: Color = Color(231);
610    pub const Black: Color = Color(16);
611}
612impl Color {
613    /// Creates a style with this color as the foreground.
614    pub fn as_fg(self) -> Style {
615        Style::DEFAULT.with_fg(self)
616    }
617
618    /// Creates a style with this color as the background.
619    pub fn as_bg(self) -> Style {
620        Style::DEFAULT.with_bg(self)
621    }
622
623    /// Creates a style with this color as background and the given foreground.
624    pub fn with_fg(self, fg: Color) -> Style {
625        Style::DEFAULT.with_fg(fg).with_bg(self)
626    }
627
628    /// Creates a style with this color as foreground and the given background.
629    pub fn with_bg(self, bg: Color) -> Style {
630        Style::DEFAULT.with_bg(bg).with_fg(self)
631    }
632}
633
634impl std::ops::BitOrAssign<Modifier> for Style {
635    fn bitor_assign(&mut self, rhs: Modifier) {
636        self.0 |= rhs.0 as u32;
637    }
638}
639
640impl std::ops::BitOr<Modifier> for Style {
641    type Output = Style;
642
643    fn bitor(self, rhs: Modifier) -> Self::Output {
644        Style(self.0 | rhs.0 as u32)
645    }
646}
647
648impl std::ops::BitOrAssign for Style {
649    fn bitor_assign(&mut self, rhs: Self) {
650        self.0 |= rhs.0;
651    }
652}
653
654impl std::ops::BitOr for Style {
655    type Output = Style;
656
657    fn bitor(self, rhs: Self) -> Self::Output {
658        Style(self.0 | rhs.0)
659    }
660}
661
662impl Style {
663    /// The default style with no colors or modifiers.
664    pub const DEFAULT: Style = Style(0);
665    pub(crate) const HAS_BG: u32 = 0b10_000;
666    pub(crate) const FG_MASK: u32 = 0xff00_0000;
667    pub(crate) const BG_MASK: u32 = 0x00ff_0000;
668    pub(crate) const MASK: u32 = 0xffff_fff8;
669    pub(crate) const HAS_FG: u32 = 0b01_000;
670
671    /// Creates a [`StyleDelta`] for transitioning to this style.
672    pub const fn delta(self) -> StyleDelta {
673        StyleDelta {
674            current: u32::MAX,
675            target: self,
676        }
677    }
678    /// Returns a new style with the given foreground color.
679    pub const fn with_fg(self, color: Color) -> Style {
680        Style((self.0 & 0x00ff_ffff) | ((color.0 as u32) << 24) | 0b1_000)
681    }
682
683    /// Returns a new style without a foreground color.
684    pub const fn without_fg(self) -> Style {
685        Style(self.0 & !(Style::FG_MASK | Style::HAS_FG))
686    }
687
688    /// Returns a new style without a background color.
689    pub const fn without_bg(self) -> Style {
690        Style(self.0 & !(Style::BG_MASK | Style::HAS_BG))
691    }
692
693    /// Returns the text modifiers applied to this style.
694    pub const fn modifiers(self) -> Modifier {
695        Modifier(self.0 as u16 & Modifier::ALL.0)
696    }
697
698    /// Returns a new style with the given modifiers added.
699    pub const fn with_modifier(self, mods: Modifier) -> Style {
700        Style(self.0 | mods.0 as u32)
701    }
702
703    /// Returns a new style with the given modifiers removed.
704    pub const fn without_modifier(self, mods: Modifier) -> Style {
705        Style(self.0 & !(mods.0 as u32))
706    }
707
708    /// Returns the foreground color if set.
709    pub const fn fg(self) -> Option<Color> {
710        if self.0 & 0b1_000 != 0 {
711            Some(Color(((self.0 >> 24) & 0xff) as u8))
712        } else {
713            None
714        }
715    }
716
717    /// Returns a new style with the given background color.
718    pub const fn with_bg(self, color: Color) -> Style {
719        Style((self.0 & 0xff00_ffff) | ((color.0 as u32) << 16) | 0b10_000)
720    }
721
722    /// Returns the background color if set.
723    pub const fn bg(self) -> Option<Color> {
724        if self.0 & 0b10_000 != 0 {
725            Some(Color(((self.0 >> 16) & 0xff) as u8))
726        } else {
727            None
728        }
729    }
730}
731
732// impl std::ops::BitOr for Style {
733//     type Output = Style;
734
735//     fn bitor(self, rhs: Self) -> Self::Output {
736//         Style(self.0 | rhs.0)
737//     }
738// }
739
740impl Cell {
741    const EMPTY: Cell = Cell(0);
742    const BLANK: Cell = unsafe { Cell::new_ascii(b' ', Style::DEFAULT) };
743
744    /// Returns `true` if the cell contains no visible content.
745    pub fn is_empty(self) -> bool {
746        self.0 <= 0xffff_ffff
747    }
748    #[inline]
749    fn text(&self) -> &str {
750        let len = (self.0 & 0b111) as usize;
751        unsafe {
752            std::str::from_utf8_unchecked(std::slice::from_raw_parts(
753                ((&self.0 as *const u64) as *const u8).add(4),
754                len,
755            ))
756        }
757    }
758    fn new(text: &str, style: Style) -> Cell {
759        if text.len() > 4 {
760            panic!();
761        }
762        let mut initial = [0u8; 8];
763        for (i, ch) in text.as_bytes().iter().enumerate() {
764            initial[i + 4] = *ch;
765        }
766        Cell(u64::from_ne_bytes(initial) | (style.0 as u64) | text.len() as u64)
767    }
768    const fn new_const(text: &str, style: Style) -> Cell {
769        if text.len() > 4 {
770            panic!();
771        }
772        let mut initial = [0u8; 8];
773        let bytes = text.as_bytes();
774        let mut i = 0;
775        while i < bytes.len() {
776            initial[i + 4] = bytes[i];
777            i += 1;
778        }
779        Cell(u64::from_ne_bytes(initial) | (style.0 as u64) | text.len() as u64)
780    }
781
782    /// Returns a new cell with the given style merged into the existing style.
783    pub fn with_style_merged(&self, style: Style) -> Cell {
784        Cell(self.0 | style.0 as u64)
785    }
786    fn style(&self) -> Style {
787        Style((self.0 & 0xfffffff8) as u32)
788    }
789    const unsafe fn new_ascii(ch: u8, style: Style) -> Cell {
790        Cell(((ch as u64) << (4 * 8)) | (style.0 as u64) | 1)
791    }
792}
793
794/// A rectangular grid of terminal cells.
795///
796/// Stores styled text content for rendering to a terminal. Use
797/// [`set_string`](Self::set_string) and [`set_style`](Self::set_style)
798/// to modify the buffer contents.
799pub struct Buffer {
800    pub(crate) cells: Box<[Cell]>,
801    pub(crate) width: u16,
802    pub(crate) height: u16,
803}
804
805/// A rectangular region defined by position and size.
806///
807/// Provides methods for splitting, containment checks, and layout calculations.
808#[derive(Clone, Copy, PartialEq, Eq, Debug)]
809pub struct Rect {
810    /// X coordinate of the top-left corner.
811    pub x: u16,
812    /// Y coordinate of the top-left corner.
813    pub y: u16,
814    /// Width of the rectangle.
815    pub w: u16,
816    /// Height of the rectangle.
817    pub h: u16,
818}
819
820impl Rect {
821    /// A zero-sized rectangle at the origin.
822    pub const EMPTY: Rect = Rect {
823        x: 0,
824        y: 0,
825        w: 0,
826        h: 0,
827    };
828}
829
830/// Defines how to split a dimension into two parts.
831///
832/// Implemented for `i32` (absolute pixel count) and `f64` (ratio).
833/// Negative values take from the end instead of the beginning.
834pub trait SplitRule: std::ops::Neg<Output = Self> {
835    /// Splits the given value into two parts according to this rule.
836    fn split_once(self, value: u16) -> (u16, u16);
837}
838
839/// Vertical alignment within a region.
840#[derive(Clone, Copy, PartialEq, Eq, Default)]
841pub enum VAlign {
842    /// Aligns content to the top.
843    #[default]
844    Top,
845    /// Centers content vertically.
846    Center,
847    /// Aligns content to the bottom.
848    Bottom,
849}
850
851/// Horizontal alignment within a region.
852#[derive(Clone, Copy, PartialEq, Eq, Default)]
853pub enum HAlign {
854    /// Aligns content to the left.
855    #[default]
856    Left,
857    /// Centers content horizontally.
858    Center,
859    /// Aligns content to the right.
860    Right,
861}
862
863/// Properties controlling how content is rendered within a region.
864#[derive(Default, Clone, Copy)]
865pub struct RenderProperties {
866    /// Text style to apply.
867    pub style: Style,
868    /// Vertical alignment.
869    pub valign: VAlign,
870    /// Horizontal alignment.
871    pub halign: HAlign,
872    /// Character offset for continued rendering.
873    pub offset: u32,
874}
875
876/// A property that can be applied to [`RenderProperties`].
877///
878/// Implemented for [`Style`], [`VAlign`], [`HAlign`], and [`RenderProperties`].
879pub trait RenderProperty {
880    /// Applies this property to the given render properties.
881    fn apply(self, properties: &mut RenderProperties);
882}
883
884impl RenderProperty for Style {
885    fn apply(self, properties: &mut RenderProperties) {
886        properties.style = self;
887    }
888}
889impl RenderProperty for VAlign {
890    fn apply(self, properties: &mut RenderProperties) {
891        properties.valign = self;
892    }
893}
894impl RenderProperty for HAlign {
895    fn apply(self, properties: &mut RenderProperties) {
896        properties.halign = self;
897    }
898}
899impl RenderProperty for RenderProperties {
900    fn apply(self, properties: &mut RenderProperties) {
901        *properties = self;
902    }
903}
904
905/// A rectangle with associated rendering properties.
906///
907/// Created from a [`Rect`] via [`display`](Rect::display) or [`with`](Rect::with).
908/// Provides a fluent API for rendering styled text.
909pub struct DisplayRect {
910    rect: Rect,
911    properties: RenderProperties,
912}
913
914impl DisplayRect {
915    /// Adds a render property to this display rectangle.
916    pub fn with(self, property: impl RenderProperty) -> DisplayRect {
917        let mut properties = self.properties;
918        property.apply(&mut properties);
919        DisplayRect {
920            rect: self.rect,
921            properties,
922        }
923    }
924
925    /// Fills the rectangle with the current style.
926    pub fn fill<'a>(self, buf: &mut DoubleBuffer) -> DisplayRect {
927        if self.rect.is_empty() {
928            return self;
929        }
930        buf.current.set_style(self.rect, self.properties.style);
931        self
932    }
933    /// Advances the horizontal offset by the given amount.
934    pub fn skip(mut self, amount: u32) -> DisplayRect {
935        self.properties.offset = self.properties.offset.wrapping_add(amount);
936        self
937    }
938    /// Renders formatted content using [`Display`](std::fmt::Display).
939    pub fn fmt<'a>(self, buf: &mut DoubleBuffer, content: impl std::fmt::Display) -> DisplayRect {
940        if self.rect.is_empty() {
941            return self;
942        }
943        let start_buf = buf.buf.len();
944        write!(buf.buf, "{content}").unwrap();
945        let text = unsafe { std::str::from_utf8_unchecked(&buf.buf[start_buf..]) };
946        let rect = self.text_inner(&mut buf.current, text);
947        unsafe {
948            buf.buf.set_len(start_buf);
949        }
950        rect
951    }
952    /// Renders the given text string.
953    pub fn text(self, buf: &mut DoubleBuffer, text: &str) -> DisplayRect {
954        self.text_inner(&mut buf.current, text)
955    }
956    fn text_inner(mut self, buf: &mut Buffer, text: &str) -> DisplayRect {
957        if self.rect.is_empty() {
958            return self;
959        }
960        match self.properties.halign {
961            HAlign::Left => {
962                let (nx, _ny) = buf.set_stringn(
963                    self.rect.x + self.properties.offset as u16,
964                    self.rect.y,
965                    text,
966                    self.rect.w as usize,
967                    self.properties.style,
968                );
969                self.properties.offset = (nx - self.rect.x) as u32;
970                self
971            }
972            HAlign::Center => todo!(),
973            HAlign::Right => {
974                let text_width = UnicodeWidthStr::width(text);
975                let start_x = if text_width as u16 >= self.rect.w {
976                    self.rect.x
977                } else {
978                    self.rect.x + self.rect.w - text_width as u16
979                };
980                let (nx, _ny) = buf.set_stringn(
981                    start_x,
982                    self.rect.y,
983                    text,
984                    self.rect.w as usize,
985                    self.properties.style,
986                );
987                self.properties.offset = (nx - self.rect.x) as u32;
988                self
989            }
990        }
991    }
992}
993
994impl Rect {
995    /// Returns `true` if the point is within this rectangle.
996    pub fn contains(&self, x: u16, y: u16) -> bool {
997        let x_delta = (x as u32).wrapping_sub(self.x as u32);
998        let y_delta = (y as u32).wrapping_sub(self.y as u32);
999        (x_delta < (self.w as u32)) & (y_delta < (self.h as u32))
1000    }
1001    /// Returns `true` if the rectangle has zero area.
1002    pub fn is_empty(self) -> bool {
1003        self.w == 0 || self.h == 0
1004    }
1005
1006    /// Creates a [`DisplayRect`] for rendering within this rectangle.
1007    #[must_use]
1008    pub fn display(self) -> DisplayRect {
1009        DisplayRect {
1010            rect: self,
1011            properties: RenderProperties::default(),
1012        }
1013    }
1014    /// Creates a [`DisplayRect`] with the given property applied.
1015    #[must_use]
1016    pub fn with(self, property: impl RenderProperty) -> DisplayRect {
1017        let properties = {
1018            let mut props = RenderProperties::default();
1019            property.apply(&mut props);
1020            props
1021        };
1022        DisplayRect {
1023            rect: self,
1024            properties,
1025        }
1026    }
1027    /// Returns the left edge X coordinate.
1028    pub fn left(self) -> u16 {
1029        self.x
1030    }
1031
1032    /// Returns the right edge X coordinate.
1033    pub fn right(self) -> u16 {
1034        self.x.saturating_add(self.w)
1035    }
1036
1037    /// Returns the top edge Y coordinate.
1038    pub fn top(self) -> u16 {
1039        self.y
1040    }
1041
1042    /// Returns the bottom edge Y coordinate.
1043    pub fn bottom(self) -> u16 {
1044        self.y.saturating_add(self.h)
1045    }
1046
1047    /// Splits the rectangle vertically according to the given rule.
1048    ///
1049    /// Returns (top, bottom) rectangles.
1050    pub fn v_split(&self, rule: impl SplitRule) -> (Self, Self) {
1051        let (h1, h2) = rule.split_once(self.h);
1052        (
1053            Rect { h: h1, ..*self },
1054            Rect {
1055                h: h2,
1056                y: self.y + h1,
1057                ..*self
1058            },
1059        )
1060    }
1061
1062    /// Splits the rectangle horizontally according to the given rule.
1063    ///
1064    /// Returns (left, right) rectangles.
1065    pub fn h_split(&self, rule: impl SplitRule) -> (Self, Self) {
1066        let (w1, w2) = rule.split_once(self.w);
1067        (
1068            Rect { w: w1, ..*self },
1069            Rect {
1070                w: w2,
1071                x: self.x + w1,
1072                ..*self
1073            },
1074        )
1075    }
1076    /// Removes and returns a portion from the right edge.
1077    pub fn take_right(&mut self, rule: impl SplitRule) -> Self {
1078        let (rest, new) = self.h_split(rule.neg());
1079        *self = rest;
1080        new
1081    }
1082
1083    /// Removes and returns a portion from the bottom edge.
1084    pub fn take_bottom(&mut self, rule: impl SplitRule) -> Self {
1085        let (rest, new) = self.v_split(rule.neg());
1086        *self = rest;
1087        new
1088    }
1089
1090    /// Removes and returns a portion from the left edge.
1091    pub fn take_left(&mut self, rule: impl SplitRule) -> Self {
1092        let (rest, new) = self.h_split(rule);
1093        *self = new;
1094        rest
1095    }
1096
1097    /// Removes and returns a portion from the top edge.
1098    pub fn take_top(&mut self, rule: impl SplitRule) -> Self {
1099        let (rest, new) = self.v_split(rule);
1100        *self = new;
1101        rest
1102    }
1103}
1104
1105impl SplitRule for i32 {
1106    fn split_once(self, value: u16) -> (u16, u16) {
1107        let value = value as u32;
1108        let c = self.unsigned_abs();
1109        let (mut a, mut b) = if value >= c {
1110            (c as u16, (value - c) as u16)
1111        } else {
1112            (value as u16, 0)
1113        };
1114        if self < 0 {
1115            std::mem::swap(&mut a, &mut b);
1116        }
1117        (a, b)
1118    }
1119}
1120
1121impl SplitRule for f64 {
1122    fn split_once(self, value: u16) -> (u16, u16) {
1123        assert!((-1.0..=1.0).contains(&self), "Invalid Split Ratio");
1124        let mut a = ((value as f64) * self.abs()) as u16;
1125        let mut b = value - a;
1126        if self < 0.0 {
1127            std::mem::swap(&mut a, &mut b);
1128        }
1129        (a, b)
1130    }
1131}
1132
1133pub(crate) fn write_style_diff_ignore_fg(
1134    current: Style,
1135    mut new: Style,
1136    buf: &mut Vec<u8>,
1137) -> Style {
1138    new.0 = (new.0 & !(Style::FG_MASK | Style::HAS_FG))
1139        | (current.0 & (Style::FG_MASK | Style::HAS_FG));
1140    write_style_diff(current, new, buf)
1141}
1142pub(crate) fn write_style_diff(current: Style, new: Style, buf: &mut Vec<u8>) -> Style {
1143    if current == new {
1144        return current;
1145    }
1146    let removed = (new.0 | current.0) ^ new.0;
1147    let clearing = removed & (Style::HAS_BG | Style::HAS_FG | Modifier::ALL.0 as u32) != 0;
1148    if clearing {
1149        vt::style(buf, new, true);
1150        return new;
1151    }
1152    let mut target = new;
1153    if current.fg() == target.fg() {
1154        target = target.without_fg();
1155    }
1156    if current.bg() == target.bg() {
1157        target = target.without_bg();
1158    }
1159    // todo only have the added modifiers
1160    vt::style(buf, target, false);
1161    // if removed & (Style::HAS_BG | Style::HAS_FG | Modifier::ALL.0 as u32) != 0 {
1162    //     vt::CLEAR_STYLE.write_to_buffer(buf);
1163    //     current = Style::DEFAULT;
1164    //     added = new.0;
1165    // }
1166
1167    // vt::write_all_modifiers(buf, Modifier(added as u16 & Modifier::ALL.0));
1168
1169    // if let Some(fg) = new.fg() {
1170    //     if current.fg() != Some(fg) {
1171    //         vt::fg_ansi(buf, fg);
1172    //     }
1173    // }
1174    // if let Some(bg) = new.bg() {
1175    //     if current.bg() != Some(bg) {
1176    //         vt::bg_ansi(buf, bg);
1177    //     }
1178    // }
1179    new
1180}
1181
1182impl Buffer {
1183    /// Scrolls content up by the given number of lines.
1184    ///
1185    /// Lines shifted off the top are discarded; new lines at the bottom
1186    /// are filled with empty cells.
1187    pub fn scroll_up(&mut self, amount: u16) {
1188        if amount == 0 || self.height == 0 {
1189            return;
1190        }
1191        let amount = amount.min(self.height);
1192        let total = self.cells.len();
1193        let shifted = (self.width as usize) * (amount as usize);
1194        self.cells.copy_within(shifted.., 0);
1195        self.cells[total - shifted..].fill(Cell::EMPTY);
1196    }
1197    /// Scrolls content down by the given number of lines.
1198    ///
1199    /// Lines shifted off the bottom are discarded; new lines at the top
1200    /// are filled with empty cells.
1201    pub fn scroll_down(&mut self, amount: u16) {
1202        if amount == 0 || self.height == 0 {
1203            return;
1204        }
1205        let amount = amount.min(self.height);
1206        let total = self.cells.len();
1207        let shifted = (self.width as usize) * (amount as usize);
1208        self.cells.copy_within(0..total - shifted, shifted);
1209        self.cells[0..shifted].fill(Cell::EMPTY);
1210    }
1211    /// Returns the buffer width in cells.
1212    pub fn width(&self) -> u16 {
1213        self.width
1214    }
1215
1216    /// Returns the buffer height in cells.
1217    pub fn height(&self) -> u16 {
1218        self.height
1219    }
1220
1221    /// Creates a new buffer with the given dimensions.
1222    pub fn new(width: u16, height: u16) -> Buffer {
1223        let cells = vec![Cell::EMPTY; width as usize * height as usize].into_boxed_slice();
1224        Buffer {
1225            cells,
1226            width,
1227            height,
1228        }
1229    }
1230    /// Returns a mutable reference to the cell at the given position.
1231    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
1232        let index = (y as u32) * (self.width as u32) + (x as u32);
1233        self.cells.get_mut(index as usize)
1234    }
1235    /// Returns a mutable slice of cells from position (x, y) to the end of the row.
1236    pub fn row_remaining_mut(&mut self, x: u16, y: u16) -> Option<&mut [Cell]> {
1237        let base = (y as u32) * (self.width as u32);
1238        let start = base + (x as u32);
1239        // todo can opt this
1240        let end = base + self.width as u32;
1241        self.cells.get_mut(start as usize..end as usize)
1242    }
1243
1244    fn render_diff(&mut self, buf: &mut Vec<u8>, old: &Buffer, y_offset: u16, blanking: bool) {
1245        if self.cells.len() != old.cells.len() {
1246            self.render(buf, y_offset);
1247            return;
1248        }
1249        vt::CLEAR_STYLE.write_to_buffer(buf);
1250        let mut current_style = Style::DEFAULT;
1251        let mut old_cells = old.cells.iter();
1252        for (y, row) in self.cells.chunks_exact(self.width as usize).enumerate() {
1253            let y = y as u16 + y_offset;
1254            let mut moved = false;
1255            let mut matching_count = 0;
1256            let mut new_cells = row.iter();
1257            let mut erased_cell: Option<Cell> = None;
1258            'next_cell: while let Some(new) = new_cells.next() {
1259                let mut new = *new;
1260                let Some(old) = erased_cell.or_else(|| old_cells.next().copied()) else {
1261                    return;
1262                };
1263                if new == old {
1264                    matching_count += 1;
1265                    continue;
1266                }
1267                if new.is_empty() {
1268                    let mut blank_overwrite = 1;
1269                    let mut blank_kind = new;
1270                    'continue_new: {
1271                        loop {
1272                            let Some(&new_k) = new_cells.next() else {
1273                                // end of row
1274                                break;
1275                            };
1276                            let Some(old_k) = erased_cell.or_else(|| old_cells.next().copied())
1277                            else {
1278                                // end of file
1279                                break;
1280                            };
1281                            if old_k == new_k {
1282                                if !moved {
1283                                    moved = true;
1284                                    MoveCursor(matching_count, y).write_to_buffer(buf);
1285                                    matching_count = 0;
1286                                }
1287                                if matching_count > 0 {
1288                                    MoveCursorRight(matching_count).write_to_buffer(buf);
1289                                    // matching_count = 0;
1290                                }
1291                                current_style = write_style_diff_ignore_fg(
1292                                    current_style,
1293                                    blank_kind.style(),
1294                                    buf,
1295                                );
1296                                if !blanking || blank_overwrite < 50 {
1297                                    buf.extend(std::iter::repeat_n(b' ', blank_overwrite as usize));
1298                                    matching_count = 1;
1299                                } else {
1300                                    buf.extend_from_slice(b"\x1b[K");
1301                                    let rem_old = new_cells.len();
1302                                    if rem_old > 0 && erased_cell.is_none() {
1303                                        old_cells.nth(rem_old - 1);
1304                                    }
1305                                    erased_cell = Some(blank_kind);
1306                                    matching_count = blank_overwrite;
1307                                    if new_k == blank_kind {
1308                                        matching_count += 1;
1309                                        continue 'next_cell;
1310                                    } else {
1311                                        new = new_k;
1312                                        break 'continue_new;
1313                                    }
1314                                }
1315                                continue 'next_cell;
1316                            }
1317                            if new_k == blank_kind {
1318                                blank_overwrite += 1;
1319                                continue;
1320                            }
1321
1322                            if !moved {
1323                                moved = true;
1324                                MoveCursor(matching_count, y).write_to_buffer(buf);
1325                                matching_count = 0;
1326                            }
1327                            if matching_count > 0 {
1328                                MoveCursorRight(matching_count).write_to_buffer(buf);
1329                                matching_count = 0;
1330                            }
1331
1332                            current_style =
1333                                write_style_diff_ignore_fg(current_style, blank_kind.style(), buf);
1334                            if !blanking || blank_overwrite < 50 {
1335                                buf.extend(std::iter::repeat_n(b' ', blank_overwrite as usize));
1336                            } else {
1337                                buf.extend_from_slice(b"\x1b[K");
1338                                let rem_old = new_cells.len();
1339                                if rem_old > 0 && erased_cell.is_none() {
1340                                    old_cells.nth(rem_old - 1);
1341                                }
1342                                erased_cell = Some(blank_kind);
1343                                matching_count = blank_overwrite;
1344                            }
1345
1346                            if new_k.is_empty() {
1347                                blank_kind = new_k;
1348                                blank_overwrite = 1;
1349                                continue;
1350                            } else {
1351                                new = new_k;
1352                                break 'continue_new;
1353                            }
1354                        }
1355
1356                        if !moved {
1357                            // moved = true;
1358                            MoveCursor(matching_count, y).write_to_buffer(buf);
1359                            matching_count = 0;
1360                        }
1361                        if matching_count > 0 {
1362                            MoveCursorRight(matching_count).write_to_buffer(buf);
1363                            // matching_count = 0;
1364                        }
1365                        current_style =
1366                            write_style_diff_ignore_fg(current_style, blank_kind.style(), buf);
1367
1368                        if blank_overwrite < 8 {
1369                            for _ in 0..blank_overwrite {
1370                                buf.push(b' ');
1371                            }
1372                        } else {
1373                            buf.extend_from_slice(b"\x1b[K");
1374                        }
1375
1376                        break 'next_cell;
1377                    }
1378                }
1379
1380                if !moved {
1381                    moved = true;
1382                    MoveCursor(matching_count, y).write_to_buffer(buf);
1383                    matching_count = 0;
1384                }
1385                if matching_count > 0 {
1386                    MoveCursorRight(matching_count).write_to_buffer(buf);
1387                    matching_count = 0;
1388                }
1389                let text = new.text();
1390                if text.is_empty() {
1391                    current_style = write_style_diff_ignore_fg(current_style, new.style(), buf);
1392                } else {
1393                    current_style = write_style_diff(current_style, new.style(), buf);
1394                }
1395                if text.is_empty() {
1396                    buf.push(b' ');
1397                } else {
1398                    buf.extend_from_slice(text.as_bytes());
1399                }
1400            }
1401        }
1402    }
1403    fn render(&mut self, buf: &mut Vec<u8>, y_offset: u16) {
1404        if y_offset == 0 {
1405            vt::MOVE_CURSOR_TO_ORIGIN.write_to_buffer(buf);
1406        } else {
1407            MoveCursor(0, y_offset).write_to_buffer(buf);
1408        }
1409        vt::CLEAR_STYLE.write_to_buffer(buf);
1410        buf.extend_from_slice(vt::CLEAR_BELOW);
1411
1412        let mut current_style = Style::DEFAULT;
1413        for (y, row) in self.cells.chunks_exact(self.width as usize).enumerate() {
1414            let y = y as u16 + y_offset;
1415            let mut moved = false;
1416            let mut blank_extension = 0;
1417            for &cell in row.iter() {
1418                if cell == Cell::BLANK || cell == Cell::EMPTY {
1419                    blank_extension += 1;
1420                    continue;
1421                }
1422                if !moved {
1423                    moved = true;
1424                    MoveCursor(blank_extension, y).write_to_buffer(buf);
1425                    blank_extension = 0;
1426                }
1427                if blank_extension > 0 {
1428                    if blank_extension < 5 && current_style == Style::DEFAULT {
1429                        for _ in 0..blank_extension {
1430                            buf.push(b' ');
1431                        }
1432                    } else {
1433                        MoveCursorRight(blank_extension).write_to_buffer(buf);
1434                    }
1435                    blank_extension = 0;
1436                }
1437                current_style = write_style_diff(current_style, cell.style(), buf);
1438                let text = cell.text();
1439                if text.is_empty() {
1440                    buf.push(b' ');
1441                } else {
1442                    buf.extend_from_slice(text.as_bytes());
1443                }
1444            }
1445        }
1446        vt::MOVE_CURSOR_TO_ORIGIN.write_to_buffer(buf);
1447    }
1448    pub fn set_style(&mut self, area: Rect, style: Style) {
1449        let Rect { x, y, w, h } = area;
1450        let mut keep_mask = !(Style::MASK as u64);
1451        let mut new_mask = style.0 as u64;
1452
1453        if style.fg().is_none() {
1454            keep_mask |= (Style::HAS_FG as u64) | Style::FG_MASK as u64;
1455        }
1456        if style.bg().is_none() {
1457            keep_mask |= (Style::HAS_BG as u64) | Style::BG_MASK as u64;
1458        }
1459        new_mask &= !keep_mask;
1460
1461        for y in y..y + h {
1462            if let Some(row) = self.row_remaining_mut(x, y) {
1463                for cell in row.iter_mut().take(w as usize) {
1464                    cell.0 = (cell.0 & keep_mask) | new_mask;
1465                }
1466            }
1467        }
1468    }
1469    /// Writes a styled string at the given position.
1470    ///
1471    /// Returns the cursor position after writing.
1472    pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: Style) -> (u16, u16) {
1473        self.set_stringn(x, y, string, usize::MAX, style)
1474    }
1475
1476    /// Writes a styled string with a maximum width.
1477    ///
1478    /// Returns the cursor position after writing.
1479    pub fn set_stringn(
1480        &mut self,
1481        x: u16,
1482        y: u16,
1483        string: &str,
1484        max_width: usize,
1485        style: Style,
1486    ) -> (u16, u16) {
1487        let mut remaining_width = (self.width.saturating_sub(x) as usize).min(max_width) as u16;
1488        let initial_remaining_width = remaining_width;
1489        let Some(target) = self.row_remaining_mut(x, y) else {
1490            return (x, y);
1491        };
1492        let graphemes = UnicodeSegmentation::graphemes(string, true)
1493            .filter(|symbol| !symbol.contains(char::is_control))
1494            .map(|symbol| (symbol, symbol.width() as u16))
1495            .filter(|(_symbol, width)| *width > 0)
1496            .map_while(|(symbol, width)| {
1497                remaining_width = remaining_width.checked_sub(width)?;
1498                Some((symbol, width))
1499            });
1500        let mut target_cells = target.iter_mut();
1501        for (symbol, width) in graphemes {
1502            if let Some(cell) = target_cells.next() {
1503                *cell = Cell::new(symbol, style);
1504            } else {
1505                return (x + initial_remaining_width, y);
1506            }
1507            // Todo: When a cell takes more space, what do we do with the pad spaces
1508            // do we these, what happens in some over rights them?
1509            for _ in 1..width {
1510                if let Some(cell) = target_cells.next() {
1511                    *cell = Cell::EMPTY;
1512                } else {
1513                    return (x + initial_remaining_width, y);
1514                }
1515            }
1516        }
1517        (x + (initial_remaining_width - remaining_width), y)
1518    }
1519}
1520
1521bitflags::bitflags! {
1522    /// Configuration flags for terminal mode and capabilities.
1523    ///
1524    /// Controls raw mode, alternate screen, mouse capture, cursor visibility,
1525    /// and extended keyboard input.
1526    #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
1527    pub struct TerminalFlags: u8 {
1528        /// Enables raw mode (disables line buffering and echo).
1529        const RAW_MODE = 0b0000_0001;
1530        /// Switches to the alternate screen buffer.
1531        const ALT_SCREEN = 0b0000_0010;
1532        /// Enables mouse event capture.
1533        const MOUSE_CAPTURE = 0b0000_0100;
1534        /// Hides the terminal cursor.
1535        const HIDE_CURSOR = 0b0000_1000;
1536        /// Enables extended keyboard input (Kitty protocol).
1537        const EXTENDED_KEYBOARD_INPUTS = 0b0001_0000;
1538    }
1539}
1540
1541/// A handle to the terminal with RAII-based mode management.
1542///
1543/// Automatically restores terminal settings when dropped. Implements
1544/// [`Write`] for direct output.
1545///
1546/// [`Write`]: std::io::Write
1547///
1548/// # Examples
1549///
1550/// ```no_run
1551/// use extui::{Terminal, TerminalFlags};
1552///
1553/// let mut term = Terminal::open(
1554///     TerminalFlags::RAW_MODE | TerminalFlags::ALT_SCREEN
1555/// )?;
1556/// # Ok::<(), std::io::Error>(())
1557/// ```
1558pub struct Terminal {
1559    fd: std::mem::ManuallyDrop<std::fs::File>,
1560    termios: sys::Termios,
1561    flags: TerminalFlags,
1562}
1563
1564fn write_enable_terminal_flags(
1565    file: &mut std::fs::File,
1566    flags: TerminalFlags,
1567) -> std::io::Result<()> {
1568    // todo use stack buffer
1569    let mut buffer = Vec::new();
1570
1571    if flags.contains(TerminalFlags::MOUSE_CAPTURE) {
1572        buffer.extend_from_slice(vt::ENABLE_NON_MOTION_MOUSE_EVENTS);
1573    }
1574    if flags.contains(TerminalFlags::HIDE_CURSOR) {
1575        buffer.extend_from_slice(vt::HIDE_CURSOR);
1576    }
1577
1578    if flags.contains(TerminalFlags::ALT_SCREEN) {
1579        buffer.extend_from_slice(vt::ENABLE_ALT_SCREEN);
1580    }
1581    if flags.contains(TerminalFlags::EXTENDED_KEYBOARD_INPUTS) {
1582        (KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
1583            | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS)
1584            .write_to_buffer(&mut buffer);
1585    }
1586
1587    file.write_all(&buffer)?;
1588    Ok(())
1589}
1590
1591fn write_disable_terminal_flags(
1592    file: &mut std::fs::File,
1593    flags: TerminalFlags,
1594) -> std::io::Result<()> {
1595    // todo use stack buffer
1596    let mut buffer = Vec::new();
1597    if flags.contains(TerminalFlags::MOUSE_CAPTURE) {
1598        buffer.extend_from_slice(vt::DISABLE_NON_MOTION_MOUSE_EVENTS);
1599    }
1600    if flags.contains(TerminalFlags::HIDE_CURSOR) {
1601        buffer.extend_from_slice(vt::SHOW_CURSOR);
1602    }
1603    if flags.contains(TerminalFlags::ALT_SCREEN) {
1604        buffer.extend_from_slice(vt::DISABLE_ALT_SCREEN);
1605    }
1606    if flags.contains(TerminalFlags::EXTENDED_KEYBOARD_INPUTS) {
1607        buffer.extend_from_slice(vt::POP_KEYBOARD_ENABLEMENT);
1608    }
1609    file.write_all(&buffer)?;
1610    Ok(())
1611}
1612impl std::io::Write for Terminal {
1613    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1614        self.fd.write(buf)
1615    }
1616
1617    fn flush(&mut self) -> std::io::Result<()> {
1618        Ok(())
1619    }
1620}
1621
1622impl Terminal {
1623    /// Returns `true` if the terminal is in raw mode.
1624    pub fn is_raw(&self) -> bool {
1625        self.flags.contains(TerminalFlags::RAW_MODE)
1626    }
1627
1628    /// Temporarily restores original terminal settings while executing a function.
1629    ///
1630    /// Useful for spawning subprocesses or showing prompts.
1631    pub fn with_bypass<T>(&mut self, mut func: impl FnMut() -> T) -> std::io::Result<T> {
1632        write_disable_terminal_flags(&mut self.fd, self.flags)?;
1633        sys::attr::set_terminal_attr(self.fd.as_fd(), &self.termios)?;
1634        //Note: if func panics then Terminal will not be restored.
1635        let t = func();
1636        if self.flags.contains(TerminalFlags::RAW_MODE) {
1637            let mut raw_term = self.termios;
1638            sys::attr::raw_terminal_attr(&mut raw_term);
1639            sys::attr::set_terminal_attr(self.fd.as_fd(), &raw_term)?;
1640        }
1641        write_enable_terminal_flags(&mut self.fd, self.flags)?;
1642        Ok(t)
1643    }
1644    /// Creates a terminal handle from a raw file descriptor.
1645    ///
1646    /// # Errors
1647    ///
1648    /// Returns an error if the file descriptor is not a terminal.
1649    pub fn new(fd: RawFd, flags: TerminalFlags) -> std::io::Result<Terminal> {
1650        let mut stdout = ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) });
1651        if !stdout.is_terminal() {
1652            return Err(std::io::Error::other(
1653                "Stdout is not a terminal",
1654            ));
1655        }
1656        let termios = sys::attr::get_terminal_attr(stdout.as_fd())?;
1657        if flags.contains(TerminalFlags::RAW_MODE) {
1658            let mut raw_term = termios;
1659            sys::attr::raw_terminal_attr(&mut raw_term);
1660            sys::attr::set_terminal_attr(stdout.as_fd(), &raw_term)?;
1661        }
1662        write_enable_terminal_flags(&mut stdout, flags)?;
1663        Ok(Terminal {
1664            fd: stdout,
1665            flags,
1666            termios,
1667        })
1668    }
1669    /// Opens the terminal using stdout.
1670    ///
1671    /// # Errors
1672    ///
1673    /// Returns an error if stdout is not a terminal.
1674    pub fn open(flags: TerminalFlags) -> std::io::Result<Terminal> {
1675        let mut stdout = ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(1) });
1676        if !stdout.is_terminal() {
1677            return Err(std::io::Error::other(
1678                "Stdout is not a terminal",
1679            ));
1680        }
1681        let termios = sys::attr::get_terminal_attr(stdout.as_fd())?;
1682        if flags.contains(TerminalFlags::RAW_MODE) {
1683            let mut raw_term = termios;
1684            sys::attr::raw_terminal_attr(&mut raw_term);
1685            sys::attr::set_terminal_attr(stdout.as_fd(), &raw_term)?;
1686        }
1687        write_enable_terminal_flags(&mut stdout, flags)?;
1688        Ok(Terminal {
1689            fd: stdout,
1690            flags,
1691            termios,
1692        })
1693    }
1694    /// Returns the terminal size as (columns, rows).
1695    pub fn size(&self) -> std::io::Result<(u16, u16)> {
1696        sys::size::terminal_size_fd(&self.fd.as_fd())
1697    }
1698}
1699
1700impl Drop for Terminal {
1701    fn drop(&mut self) {
1702        let _ = write_disable_terminal_flags(&mut self.fd, self.flags);
1703        let _ = sys::attr::set_terminal_attr(self.fd.as_fd(), &self.termios);
1704    }
1705}
1706
1707/// Double-buffered terminal output with differential rendering.
1708///
1709/// Maintains two buffers to compute minimal updates between frames,
1710/// reducing flicker and bandwidth.
1711///
1712/// # Examples
1713///
1714/// ```no_run
1715/// use extui::{DoubleBuffer, Terminal, TerminalFlags};
1716///
1717/// let mut term = Terminal::open(TerminalFlags::RAW_MODE)?;
1718/// let (w, h) = term.size()?;
1719/// let mut buf = DoubleBuffer::new(w, h);
1720///
1721/// // Draw to buf, then render
1722/// buf.render(&mut term);
1723/// # Ok::<(), std::io::Error>(())
1724/// ```
1725pub struct DoubleBuffer {
1726    current: Buffer,
1727    previous: Buffer,
1728    /// The output byte buffer containing VT escape sequences.
1729    pub buf: Vec<u8>,
1730    diffable: bool,
1731    blanking: bool,
1732    scroll: i16,
1733    #[doc(hidden)]
1734    pub y_offset: u16,
1735    epoch: u64,
1736}
1737
1738impl DoubleBuffer {
1739    /// Returns the full buffer area as a [`Rect`].
1740    pub fn rect(&self) -> Rect {
1741        Rect {
1742            x: 0,
1743            y: 0,
1744            w: self.current.width,
1745            h: self.current.height,
1746        }
1747    }
1748    /// Returns the buffer epoch, incremented on each resize or reset.
1749    pub fn epoch(&self) -> u64 {
1750        self.epoch
1751    }
1752
1753    /// Returns the buffer width in cells.
1754    pub fn width(&self) -> u16 {
1755        self.current.width
1756    }
1757
1758    /// Returns the buffer height in cells.
1759    pub fn height(&self) -> u16 {
1760        self.current.height
1761    }
1762    pub(crate) fn set_cell(&mut self, x: u16, y: u16, cell: Cell) {
1763        if let Some(target) = self.current.get_mut(x, y) {
1764            *target = cell;
1765        }
1766    }
1767    /// Applies a style to all cells within the given area.
1768    pub fn set_style(&mut self, area: Rect, style: Style) {
1769        self.current.set_style(area, style)
1770    }
1771
1772    /// Writes a styled string at the given position.
1773    ///
1774    /// Returns the cursor position after writing.
1775    pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: Style) -> (u16, u16) {
1776        self.current.set_string(x, y, string, style)
1777    }
1778
1779    /// Writes a styled string with a maximum width.
1780    ///
1781    /// Returns the cursor position after writing.
1782    pub fn set_stringn(
1783        &mut self,
1784        x: u16,
1785        y: u16,
1786        string: &str,
1787        max_width: usize,
1788        style: Style,
1789    ) -> (u16, u16) {
1790        self.current.set_stringn(x, y, string, max_width, style)
1791    }
1792    /// Creates a new double buffer with the given dimensions.
1793    pub fn new(width: u16, height: u16) -> DoubleBuffer {
1794        DoubleBuffer {
1795            current: Buffer::new(width, height),
1796            previous: Buffer::new(width, height),
1797            diffable: false,
1798            blanking: true,
1799            buf: Vec::with_capacity(16 * 1024),
1800            scroll: 0,
1801            epoch: 0,
1802            y_offset: 0,
1803        }
1804    }
1805
1806    /// Clears both buffers and increments the epoch.
1807    pub fn reset(&mut self) {
1808        self.current.cells.fill(Cell::EMPTY);
1809        self.previous.cells.fill(Cell::EMPTY);
1810        self.diffable = false;
1811        self.epoch = self.epoch.wrapping_add(1);
1812    }
1813    /// Resizes the buffer if dimensions have changed.
1814    pub fn resize(&mut self, width: u16, height: u16) {
1815        if self.current.width != width || self.current.height != height {
1816            self.current = Buffer::new(width, height);
1817            self.previous = Buffer::new(width, height);
1818            self.diffable = false;
1819            self.epoch = self.epoch.wrapping_add(1);
1820        }
1821    }
1822    /// Returns the size of the last rendered output in bytes.
1823    pub fn last_write_size(&self) -> usize {
1824        self.buf.len()
1825    }
1826
1827    /// Returns the rendered output buffer.
1828    pub fn write_buffer(&self) -> &[u8] {
1829        &self.buf
1830    }
1831
1832    /// Queues a scroll operation for the next render.
1833    ///
1834    /// Positive values scroll up, negative values scroll down.
1835    pub fn scroll(&mut self, amount: i16) {
1836        self.scroll += amount;
1837    }
1838    /// Renders the current buffer to the internal byte buffer.
1839    ///
1840    /// Use [`write_buffer`](Self::write_buffer) to access the output.
1841    pub fn render_internal(&mut self) {
1842        if self.diffable {
1843            if self.y_offset == 0 {
1844                if self.scroll < 0 {
1845                    vt::CLEAR_STYLE.write_to_buffer(&mut self.buf);
1846                    ScrollBufferDown(-self.scroll as u16).write_to_buffer(&mut self.buf);
1847                    // mutage prev buffer to match
1848                    self.previous.scroll_down(-self.scroll as u16);
1849                } else if self.scroll > 0 {
1850                    vt::CLEAR_STYLE.write_to_buffer(&mut self.buf);
1851                    ScrollBufferUp(self.scroll as u16).write_to_buffer(&mut self.buf);
1852                    self.previous.scroll_up(self.scroll as u16);
1853                }
1854            }
1855            self.scroll = 0;
1856            self.current
1857                .render_diff(&mut self.buf, &self.previous, self.y_offset, self.blanking);
1858        } else {
1859            self.scroll = 0;
1860            self.current.render(&mut self.buf, self.y_offset);
1861            self.diffable = true;
1862        }
1863        std::mem::swap(&mut self.current, &mut self.previous);
1864        self.current.cells.fill(Cell::EMPTY);
1865    }
1866    /// Renders and writes the output to the terminal.
1867    pub fn render(&mut self, term: &mut Terminal) {
1868        self.render_internal();
1869        term.write_all(&self.buf).unwrap();
1870        self.buf.clear();
1871    }
1872    /// Returns a mutable reference to the current buffer.
1873    pub fn current(&mut self) -> &mut Buffer {
1874        &mut self.current
1875    }
1876}
1877
1878/// Style of border characters for blocks.
1879#[derive(Clone, PartialEq, Eq, Debug)]
1880pub enum BorderType {
1881    /// ASCII characters (`+`, `-`, `|`).
1882    Ascii,
1883    /// Thin Unicode box-drawing characters.
1884    Thin,
1885}
1886
1887impl std::default::Default for Block<'_> {
1888    fn default() -> Self {
1889        Self {
1890            title: Default::default(),
1891            title_alignment: Alignment::Center,
1892            borders: Default::default(),
1893            border_style: Default::default(),
1894            border_type: BorderType::Thin,
1895            style: Default::default(),
1896        }
1897    }
1898}
1899
1900impl Block<'_> {
1901    /// Left border flag.
1902    pub const LEFT: u8 = 0b0001;
1903    /// Right border flag.
1904    pub const RIGHT: u8 = 0b0010;
1905    /// Top border flag.
1906    pub const TOP: u8 = 0b0100;
1907    /// Bottom border flag.
1908    pub const BOTTOM: u8 = 0b1000;
1909    /// All borders flag.
1910    pub const ALL: u8 = Self::LEFT | Self::RIGHT | Self::TOP | Self::BOTTOM;
1911}
1912
1913/// A bordered container widget with optional title.
1914///
1915/// Renders a box border around a region with configurable borders, title,
1916/// and styles.
1917///
1918/// # Examples
1919///
1920/// ```
1921/// use extui::{Block, BorderType, Alignment};
1922///
1923/// let block = Block {
1924///     title: Some("My Title"),
1925///     borders: Block::ALL,
1926///     ..Default::default()
1927/// };
1928/// ```
1929#[derive(Clone, PartialEq, Eq, Debug)]
1930pub struct Block<'a> {
1931    /// Optional title displayed on the top border.
1932    pub title: Option<&'a str>,
1933    /// Alignment of the title within the top border.
1934    pub title_alignment: Alignment,
1935    /// Bitmask of visible borders (use [`LEFT`](Self::LEFT), [`RIGHT`](Self::RIGHT), etc.).
1936    pub borders: u8,
1937    /// Style applied to border characters.
1938    pub border_style: Style,
1939    /// Type of border characters to use.
1940    pub border_type: BorderType,
1941    /// Style applied to the block background.
1942    pub style: Style,
1943}
1944
1945impl Block<'_> {
1946    /// Renders the block within the given rectangle.
1947    pub fn render(&self, rect: Rect, buf: &mut Buffer) {
1948        let Rect { x, y, w, h } = rect;
1949        if w == 0 || h == 0 {
1950            return;
1951        }
1952        let box_style = match self.border_type {
1953            BorderType::Ascii => &BoxStyle::ASCII,
1954            BorderType::Thin => &BoxStyle::LIGHT,
1955        };
1956        if self.borders & Self::TOP != 0
1957            && let Some(row) = buf.row_remaining_mut(x, y) {
1958                if w >= 1 {
1959                    row[0] = box_style.top_left;
1960                }
1961                for cell in row
1962                    .iter_mut()
1963                    .take(w as usize)
1964                    .skip(1)
1965                    .take(w.saturating_sub(2) as usize)
1966                {
1967                    *cell = box_style.horizontal;
1968                }
1969                if w >= 2 {
1970                    row[w as usize - 1] = box_style.top_right;
1971                }
1972            }
1973        if self.borders & Self::BOTTOM != 0
1974            && let Some(row) = buf.row_remaining_mut(x, y + h - 1) {
1975                if w >= 1 {
1976                    row[0] = box_style.bottom_left;
1977                }
1978                for cell in row
1979                    .iter_mut()
1980                    .take(w as usize)
1981                    .skip(1)
1982                    .take(w.saturating_sub(2) as usize)
1983                {
1984                    *cell = box_style.horizontal;
1985                }
1986                if w >= 2 {
1987                    row[w as usize - 1] = box_style.bottom_right;
1988                }
1989            }
1990        for y in y + (self.borders & Self::TOP != 0) as u16
1991            ..y + h - (self.borders & Self::BOTTOM != 0) as u16
1992        {
1993            if self.borders & Self::LEFT != 0
1994                && let Some(row) = buf.row_remaining_mut(x, y) {
1995                    row[0] = box_style.vertical;
1996                }
1997            if self.borders & Self::RIGHT != 0
1998                && let Some(row) = buf.row_remaining_mut(x + w - 1, y) {
1999                    row[0] = box_style.vertical;
2000                }
2001        }
2002
2003        buf.set_style(rect, self.style);
2004        if let Some(title) = self.title {
2005            let title_len = title.width() as u16;
2006            if title_len + 2 < w {
2007                let title_x = match self.title_alignment {
2008                    Alignment::Left => x + 1,
2009                    Alignment::Center => x + (w - title_len) / 2,
2010                    Alignment::Right => x + w - title_len - 1,
2011                };
2012                buf.set_stringn(title_x, y, title, (w - 2) as usize, self.border_style);
2013                if title_x > x + 1 {
2014                    buf.set_stringn(x + 1, y, " ", 1, self.border_style);
2015                }
2016                if title_x + title_len < x + w - 1 {
2017                    buf.set_stringn(title_x + title_len, y, " ", 1, self.border_style);
2018                }
2019            }
2020        }
2021    }
2022    pub fn inner(&self, rect: Rect) -> Rect {
2023        let mut x = rect.x;
2024        let mut y = rect.y;
2025        let mut w = rect.w;
2026        let mut h = rect.h;
2027        if self.borders & Self::LEFT != 0 {
2028            x = x.saturating_add(1);
2029            w = w.saturating_sub(1);
2030        }
2031        if self.borders & Self::RIGHT != 0 {
2032            w = w.saturating_sub(1);
2033        }
2034        if self.borders & Self::TOP != 0 {
2035            y = y.saturating_add(1);
2036            h = h.saturating_sub(1);
2037        }
2038        if self.borders & Self::BOTTOM != 0 {
2039            h = h.saturating_sub(1);
2040        }
2041        Rect { x, y, w, h }
2042    }
2043}
2044
2045#[cfg(test)]
2046mod test {
2047    use super::*;
2048
2049    #[test]
2050    fn style_encoding() {
2051        for fg in 0..=255u8 {
2052            let fg = Color(fg);
2053            let colored = Style::DEFAULT.with_fg(fg);
2054            assert_eq!(colored.fg().unwrap(), fg);
2055            for bg in 0..=255u8 {
2056                let bg = Color(bg);
2057                let colored = colored.with_bg(bg);
2058                assert_eq!(colored.bg().unwrap(), bg);
2059                assert_eq!(colored.fg().unwrap(), fg);
2060            }
2061        }
2062    }
2063
2064    pub struct BufferDiffCheck {
2065        db_a: DoubleBuffer,
2066        term_1: vt100::Parser,
2067        db_b: DoubleBuffer,
2068        term_2: vt100::Parser,
2069        step: u32,
2070    }
2071
2072    impl BufferDiffCheck {
2073        pub fn new(width: u16, height: u16) -> BufferDiffCheck {
2074            BufferDiffCheck {
2075                db_a: DoubleBuffer::new(width, height),
2076                term_1: vt100::Parser::new(height, width, 0),
2077                db_b: DoubleBuffer::new(width, height),
2078
2079                term_2: vt100::Parser::new(height, width, 0),
2080                step: 0,
2081            }
2082        }
2083        pub fn step(&mut self, fnx: impl Fn(Rect, &mut DoubleBuffer)) {
2084            self.step += 1;
2085            let rect = Rect {
2086                x: 0,
2087                y: 0,
2088                w: self.db_a.width(),
2089                h: self.db_a.height(),
2090            };
2091            fnx(rect, &mut self.db_a);
2092            self.db_a.render_internal();
2093            self.term_1.process(&self.db_a.buf);
2094
2095            fnx(rect, &mut self.db_b);
2096            self.db_b.render_internal();
2097            self.term_2.process(&self.db_b.buf);
2098
2099            let _ = std::fs::write(format!("/tmp/term_{}.bin", self.step), &self.db_b.buf);
2100
2101            let a = self.term_2.screen().contents();
2102            let b = self.term_1.screen().contents();
2103            for (a, b) in a.lines().zip(b.lines()) {
2104                assert_eq!(a, b);
2105            }
2106        }
2107    }
2108
2109    #[test]
2110    fn blanking_optimization() {
2111        let mut checker = BufferDiffCheck::new(200, 4);
2112        checker.db_b.blanking = false;
2113        checker.step(|mut rect, out| {
2114            for i in 0..4 {
2115                let (mut a, mut b) = rect.take_top(1).h_split(0.5);
2116                a.take_left(6)
2117                    .with(Color::LightGoldenrod2.with_fg(Color::Black))
2118                    .text(out, " Done ");
2119
2120                a.with(if i == 0 {
2121                    Color::Blue1.with_fg(Color::Black)
2122                } else {
2123                    Color(248).as_fg()
2124                })
2125                .fill(out)
2126                .skip(1)
2127                .text(out, "die R:0 S:0");
2128
2129                b.take_left(6)
2130                    .with(Color::Aquamarine1.with_bg(Color::Grey[4]))
2131                    .text(out, " Fail ");
2132                b.with(Color(248).as_fg())
2133                    .fill(out)
2134                    .skip(1)
2135                    .text(out, "cargo run -- die")
2136                    .skip(1)
2137                    .with(HAlign::Right)
2138                    .text(out, "100s ");
2139            }
2140        });
2141        checker.step(|mut rect, out| {
2142            for i in 0..4 {
2143                let (mut a, mut b) = rect.take_top(1).h_split(0.5);
2144                a.take_left(6)
2145                    .with(Color::LightGoldenrod2.with_fg(Color::Black))
2146                    .text(out, " Done ");
2147
2148                a.with(if i == 1 {
2149                    Color::Blue1.with_fg(Color::Black)
2150                } else {
2151                    Color(248).as_fg()
2152                })
2153                .fill(out)
2154                .skip(1)
2155                .text(out, "die R:0 S:0");
2156
2157                b.take_left(6)
2158                    .with(Color::BlueViolet.with_bg(Color::Grey[4]))
2159                    .text(out, " Done ");
2160                b.with(Color(248).as_fg())
2161                    .fill(out)
2162                    .skip(1)
2163                    .text(out, "cargo info")
2164                    .skip(1)
2165                    .with(HAlign::Right)
2166                    .text(out, "101s ");
2167            }
2168        });
2169    }
2170
2171    #[test]
2172    fn buffer_set_style() {
2173        let mut buffer = Buffer::new(8, 1);
2174        buffer.set_string(1, 0, "hello", Style::DEFAULT);
2175        buffer.set_style(
2176            Rect {
2177                x: 0,
2178                y: 0,
2179                w: 5,
2180                h: 1,
2181            },
2182            Color(1).as_bg(),
2183        );
2184        buffer.set_style(
2185            Rect {
2186                x: 2,
2187                y: 0,
2188                w: 6,
2189                h: 1,
2190            },
2191            Color(2).as_fg(),
2192        );
2193        buffer.set_style(
2194            Rect {
2195                x: 3,
2196                y: 0,
2197                w: 1,
2198                h: 1,
2199            },
2200            Style::DEFAULT.with_modifier(Modifier::BOLD),
2201        );
2202        let expected = [
2203            /* 0 */ Cell::new("", Color(1).as_bg()),
2204            /* 1 */ Cell::new("h", Color(1).as_bg()),
2205            /* 2 */ Cell::new("e", Color(1).as_bg() | Color(2).as_fg()),
2206            /* 3 */ Cell::new("l", Color(1).as_bg() | Color(2).as_fg() | Modifier::BOLD),
2207            /* 4 */ Cell::new("l", Color(1).as_bg() | Color(2).as_fg()),
2208            /* 5 */ Cell::new("o", Color(2).as_fg()),
2209            /* 6 */ Cell::new("", Color(2).as_fg()),
2210            /* 7 */ Cell::new("", Color(2).as_fg()),
2211        ];
2212        for (i, (got, expected)) in buffer.cells.iter().zip(expected.iter()).enumerate() {
2213            assert_eq!(got, expected, "Mismatch at cell {}", i);
2214        }
2215    }
2216}