Skip to main content

ui/
term.rs

1// term -- a terminal / console widget: a scrollback ring with an ANSI subset, the input line on
2// textbox_surface (IME, clipboard, selection), history, completion, IOS-style `?` help, a
3// --More-- pager, busy mode with type-ahead, raw keys, mouse selection and search
4//
5//   TermState::new(scrollback) -> st     a console (the app keeps one per console); scrollback
6//                                        = lines kept (at least 100)
7//   st.write(&[u8]) / st.write_str(s)    output: UTF-8 with an ANSI subset -- SGR 0 1 2 4 7 22
8//                                        24 27, colours 30-37 / 90-97 / 39, backgrounds 40-47 /
9//                                        100-107 / 49 (38;5 / 48;5 / 38;2 mapped to the 16);
10//                                        \r \n \t BS, BEL (the console flashes), ESC[K (0 1 2),
11//                                        ESC[2J (the screen scrolls clear), ESC[3J (the
12//                                        scrollback too), ESC[H, ESC[nC / nD / nG; OSC skipped
13//   st.clear()                           drop the scrollback and the current line
14//   st.set_prompt(s)                     the prompt before the input (SGR allowed)
15//   st.set_mode(TM_*)                    TM_LINE the input line; TM_BUSY a command runs: no
16//                                        input line, typed lines wait (type-ahead) and come
17//                                        back one Submit at a time in TM_LINE; TM_RAW every key
18//                                        goes to the app (TermEv::Raw); TM_SECRET a line typed
19//                                        without echo or history (passwords)
20//   st.input_set(s, caret)               replace the input line (caret: a byte index)
21//   st.complete_reply(line, list)        the answer to TermEv::Complete: the new line, and
22//                                        candidates listed in columns below the old one
23//   st.pager(on)                         on: the following output pages -- a screen minus one
24//                                        line (st.page_len lines when set, as "terminal
25//                                        length"), then pager_text (" --More-- "): Space a page,
26//                                        Enter a line, q / Esc / Ctrl+C TermEv::PagerQuit (later
27//                                        output is dropped until pager(false)); off: the app
28//                                        wrote everything (a held rest still pages)
29//   st.grid() -> (cols, rows)            the character grid of the last layout
30//   st.text() -> String                  scrollback and the current line as plain text
31//   ui.terminal(id, &mut st, TF_*) -> Vec<TermEv>   the console, filling the place it gets
32//                                        (natural 480 x 240 DIP; next_size(-1, -1) fills);
33//                                        ids id .. id + 7 (id + 1: the input line)
34//   ui.mono_font_set(font)               the monospace font of every console (an id from
35//                                        ui.text.load_mem; apps embed DejaVu Sans Mono)
36//   ui.mono_font(size_pt) -> (font, cell_w)   that font, else font_load("monospace"), else
37//                                        FONT_REG drawn cell by cell; the size is adjusted so
38//                                        that a cell is a whole number of pixels
39//
40// Keys while focused: Enter submits (the line is echoed after the prompt and kept in the
41// history), Tab TermEv::Complete, Up / Down the history, Home / End; TF_QHELP: a typed `?` is
42// not inserted but gives TermEv::Help (the line up to the caret); Ctrl+C copies a selection,
43// else TermEv::Interrupt (echoes ^C); TF_CTRL_SHIFT6: Ctrl+Shift+6 interrupts too; Ctrl+D on an
44// empty line TermEv::Eof, Ctrl+Z on an empty line and Ctrl+L TermEv::Chord; Ctrl+U clears the
45// line before the caret; Ctrl+Shift+C copies; Ctrl+V / Ctrl+Shift+V paste (every pasted line
46// is one Submit); PgUp / PgDn (also with Shift), Ctrl+Up / Down, Ctrl+Home / End scroll;
47// Ctrl+F the search bar (F3 / Enter: the next older match, Shift+F3 / Shift+Enter: newer, Esc
48// closes it).  While busy or paging the console holds the keyboard as a text box would: plain
49// keys are no accelerators, the undo keys stay with it (the app's undo does not run).  Mouse:
50// a drag selects in the scrollback, a double click a word, the wheel scrolls; scrolled up,
51// new output shows a "Neue Ausgabe" button that jumps down.  Every
52// visible line is a K_TEXT record (`expect text:"..." exists`), the input line the K_TEXTBOX
53// id + 1.  Colours: a 16-colour palette from the theme tokens (light / dark).  Every character
54// takes one cell (wide CJK characters overlap the next); right-to-left the rows stay LTR.
55//
56// The visible rows are drawn by two cached Draw objects (scrollback, current line + input),
57// replaced only when what they show changes, so typing redraws only the input rows.
58
59use alloc::collections::VecDeque;
60use alloc::rc::Rc;
61use alloc::string::String;
62use alloc::vec::Vec;
63
64use crate::cmd::{Draw, DrawRef, Gfx};
65use crate::consts::*;
66use crate::keys::*;
67use crate::theme::color_mix;
68use crate::{FontSel, TextSurfaceLine, Ui};
69
70/// TermState::mode: the input line
71pub const TM_LINE: u8 = 0;
72/// TermState::mode: a command runs -- no input line, typed text waits (type-ahead)
73pub const TM_BUSY: u8 = 1;
74/// TermState::mode: every key and character goes to the app (TermEv::Raw)
75pub const TM_RAW: u8 = 2;
76/// TermState::mode: a line typed without echo or history (passwords)
77pub const TM_SECRET: u8 = 3;
78
79pub const TF_QHELP: u32 = 1; // a typed `?` asks for help (TermEv::Help, IOS)
80pub const TF_CTRL_SHIFT6: u32 = 2; // Ctrl+Shift+6 interrupts too (IOS)
81pub const TF_NO_INPUT: u32 = 4; // output only: no input line
82pub const TF_NO_FRAME: u32 = 8; // no border, a small padding
83
84pub const TK_KEY: u32 = 0x8000_0000; // TermEv::Raw: a key (VK | MOD_* << 16), not a character
85
86const DEF: u8 = 255; // the default colour of Attr
87const A_BOLD: u8 = 1;
88const A_DIM: u8 = 2;
89const A_UNDER: u8 = 4;
90const A_INV: u8 = 8;
91const LINE_MAX: usize = 32768; // cells of a line before a forced break
92const INPUT_CAP: u32 = 0x1_0000; // bytes of the input line
93const HIST_MAX: usize = 500;
94const PARAMS_MAX: usize = 16;
95const UIT_TERM: u32 = 0x7FFE_0040; // timer: type-ahead goes on
96const UIT_TERM_BELL: u32 = 0x7FFE_0041; // timer: the bell flash ends
97const BELL_MS: u32 = 150;
98
99/// what ui.terminal reports
100#[derive(Clone, Debug, PartialEq)]
101#[non_exhaustive]
102pub enum TermEv {
103    /// Enter: the line (already echoed after the prompt and kept in the history)
104    Submit(String),
105    /// Tab: the input line and the caret (byte index); answer with complete_reply
106    Complete { line: String, caret: usize },
107    /// TF_QHELP: `?` was typed; the line up to the caret (echoed with the `?`)
108    Help(String),
109    /// Ctrl+C without a selection (TF_CTRL_SHIFT6: also Ctrl+Shift+6)
110    Interrupt,
111    /// Ctrl+D on an empty line
112    Eof,
113    /// Ctrl+Z on an empty line, Ctrl+L: VK | MOD_* << 16
114    Chord(u32),
115    /// the pager was quit (q, Esc, Ctrl+C)
116    PagerQuit,
117    /// the character grid changed (also at the first layout): columns, rows
118    Resize(u16, u16),
119    /// TM_RAW: characters, and keys as TK_KEY | VK | MOD_* << 16; Enter 13, Tab 9,
120    /// Backspace 127, Esc 27, Ctrl+letter 1..26
121    Raw(Vec<u32>),
122}
123
124// ------------------------------------------------------------------ the screen
125
126// SGR state of a cell: colours 0..15 or DEF, A_* flags
127#[derive(Clone, Copy, PartialEq, Eq, Debug)]
128struct Attr {
129    fg: u8,
130    bg: u8,
131    fl: u8,
132}
133
134const ATTR0: Attr = Attr { fg: DEF, bg: DEF, fl: 0 };
135
136#[derive(Clone, Copy, PartialEq)]
137struct Cell {
138    ch: char,
139    a: Attr,
140}
141
142// a finished line: its text (one char per cell), the attribute changes (column, attributes;
143// ATTR0 before the first), its cells
144struct Line {
145    text: String,
146    spans: Vec<(u32, Attr)>,
147    cells: u32,
148}
149
150impl Line {
151    fn of(cells: &[Cell]) -> Line {
152        let mut text = String::with_capacity(cells.len());
153        let mut spans = Vec::new();
154        let mut a = ATTR0;
155        for (i, c) in cells.iter().enumerate() {
156            if c.a != a {
157                spans.push((i as u32, c.a));
158                a = c.a;
159            }
160            text.push(c.ch);
161        }
162        Line { text, spans, cells: cells.len() as u32 }
163    }
164}
165
166#[derive(Clone, Copy, PartialEq)]
167enum Esc {
168    No,
169    Esc,
170    Csi,
171    Osc,
172    OscEsc,
173}
174
175// rows of a line wrapped at cols (an empty line takes one)
176fn line_rows(cells: u32, cols: usize) -> u64 {
177    if cells == 0 {
178        1
179    } else {
180        (cells as u64).div_ceil(cols.max(1) as u64)
181    }
182}
183
184// the scrollback ring, the line being written and the ANSI parser
185struct Screen {
186    lines: VecDeque<Line>,
187    row0: VecDeque<u64>, // the first wrapped row of each line (absolute)
188    row_end: u64,        // the row after the last line (the current line starts there)
189    base: u64,           // the absolute index of lines[0]
190    cap: usize,
191    cols: usize,
192    cur: Vec<Cell>,
193    col: usize,
194    attr: Attr,
195    esc: Esc,
196    params: Vec<u32>,
197    param: Option<u32>,
198    utf: u32,
199    utf_need: u8,
200    utf_min: u32,
201    floor: u64, // the screen starts at this line (ESC[2J)
202    bell: bool,
203    added: u64, // lines added so far
204    ver: u64,   // bumps with every change
205}
206
207impl Screen {
208    fn new(cap: usize) -> Screen {
209        Screen {
210            lines: VecDeque::new(),
211            row0: VecDeque::new(),
212            row_end: 0,
213            base: 0,
214            cap: cap.max(100),
215            cols: 80,
216            cur: Vec::new(),
217            col: 0,
218            attr: ATTR0,
219            esc: Esc::No,
220            params: Vec::new(),
221            param: None,
222            utf: 0,
223            utf_need: 0,
224            utf_min: 0,
225            floor: 0,
226            bell: false,
227            added: 0,
228            ver: 0,
229        }
230    }
231
232    fn first_row(&self) -> u64 {
233        self.row0.front().copied().unwrap_or(self.row_end)
234    }
235
236    // the absolute index after the last line
237    fn end(&self) -> u64 {
238        self.base + self.lines.len() as u64
239    }
240
241    fn line(&self, abs: u64) -> Option<&Line> {
242        abs.checked_sub(self.base).and_then(|i| self.lines.get(i as usize))
243    }
244
245    // the first row of line abs (lines before the ring: the first row; after: the end)
246    fn row_of(&self, abs: u64) -> u64 {
247        match abs.checked_sub(self.base) {
248            None => self.first_row(),
249            Some(i) => self.row0.get(i as usize).copied().unwrap_or(self.row_end),
250        }
251    }
252
253    // the line that shows absolute row r (None: r is past the lines)
254    fn line_at_row(&self, r: u64) -> Option<u64> {
255        if r >= self.row_end || self.lines.is_empty() {
256            return None;
257        }
258        let i = self.row0.partition_point(|&x| x <= r);
259        Some(self.base + i.max(1) as u64 - 1)
260    }
261
262    // wrap at another width: the rows are counted again from 0
263    fn set_cols(&mut self, cols: usize) {
264        let cols = cols.max(1);
265        if cols == self.cols {
266            return;
267        }
268        self.cols = cols;
269        let mut r = 0;
270        for (i, l) in self.lines.iter().enumerate() {
271            self.row0[i] = r;
272            r += line_rows(l.cells, cols);
273        }
274        self.row_end = r;
275        self.ver += 1;
276    }
277
278    fn push(&mut self, l: Line) {
279        self.row0.push_back(self.row_end);
280        self.row_end += line_rows(l.cells, self.cols);
281        self.lines.push_back(l);
282        self.added += 1;
283        while self.lines.len() > self.cap {
284            self.lines.pop_front();
285            self.row0.pop_front();
286            self.base += 1;
287        }
288    }
289
290    fn newline(&mut self) {
291        let cells = core::mem::take(&mut self.cur);
292        self.col = 0;
293        self.push(Line::of(&cells));
294    }
295
296    fn clear(&mut self) {
297        self.base = self.end();
298        self.lines.clear();
299        self.row0.clear();
300        self.cur.clear();
301        self.col = 0;
302        self.floor = self.base;
303        self.ver += 1;
304    }
305
306    fn write(&mut self, s: &[u8]) {
307        if s.is_empty() {
308            return;
309        }
310        for &b in s {
311            self.feed(b);
312        }
313        self.ver += 1;
314    }
315
316    fn put(&mut self, ch: char) {
317        if self.col >= LINE_MAX {
318            self.newline();
319        }
320        let cell = Cell { ch, a: self.attr };
321        if self.col < self.cur.len() {
322            self.cur[self.col] = cell;
323        } else {
324            while self.cur.len() < self.col {
325                self.cur.push(Cell { ch: ' ', a: ATTR0 });
326            }
327            self.cur.push(cell);
328        }
329        self.col += 1;
330    }
331
332    fn feed(&mut self, b: u8) {
333        match self.esc {
334            Esc::No => {}
335            Esc::Esc => {
336                self.esc = match b {
337                    b'[' => {
338                        self.params.clear();
339                        self.param = None;
340                        Esc::Csi
341                    }
342                    b']' => Esc::Osc,
343                    _ => Esc::No, // other escapes (charsets, keypad modes) are skipped
344                };
345                return;
346            }
347            Esc::Csi => {
348                match b {
349                    b'0'..=b'9' => {
350                        let v = self.param.unwrap_or(0) * 10 + (b - b'0') as u32;
351                        self.param = Some(v.min(65535));
352                    }
353                    b';' | b':' => {
354                        if self.params.len() < PARAMS_MAX {
355                            self.params.push(self.param.unwrap_or(0));
356                        }
357                        self.param = None;
358                    }
359                    0x40..=0x7E => {
360                        if let Some(p) = self.param.take() {
361                            if self.params.len() < PARAMS_MAX {
362                                self.params.push(p);
363                            }
364                        }
365                        self.esc = Esc::No;
366                        self.csi(b);
367                    }
368                    0x1B => self.esc = Esc::Esc,
369                    _ => {} // private markers, intermediates
370                }
371                return;
372            }
373            Esc::Osc => {
374                if b == 7 {
375                    self.esc = Esc::No;
376                } else if b == 0x1B {
377                    self.esc = Esc::OscEsc;
378                }
379                return;
380            }
381            Esc::OscEsc => {
382                self.esc = if b == b'\\' { Esc::No } else { Esc::Osc };
383                return;
384            }
385        }
386        if self.utf_need != 0 {
387            if b & 0xC0 == 0x80 {
388                self.utf = self.utf << 6 | (b & 0x3F) as u32;
389                self.utf_need -= 1;
390                if self.utf_need == 0 {
391                    let c = self.utf;
392                    let ok = c >= self.utf_min && c <= 0x10FFFF && c & 0xFFFF_F800 != 0xD800;
393                    self.put(if ok { char::from_u32(c).unwrap_or('\u{FFFD}') } else { '\u{FFFD}' });
394                }
395                return;
396            }
397            self.utf_need = 0; // a cut sequence
398            self.put('\u{FFFD}');
399        }
400        match b {
401            0x1B => self.esc = Esc::Esc,
402            b'\n' => self.newline(),
403            b'\r' => self.col = 0,
404            b'\t' => {
405                // (never past the forced break: put() would wrap and the loop never ends)
406                let to = ((self.col / 8 + 1) * 8).min(LINE_MAX);
407                while self.col < to {
408                    self.put(' ');
409                }
410            }
411            8 => self.col = self.col.saturating_sub(1),
412            7 => self.bell = true,
413            0..=0x1F | 0x7F => {}
414            0x20..=0x7E => self.put(b as char),
415            0xC2..=0xDF => self.utf_begin(b & 0x1F, 1, 0x80),
416            0xE0..=0xEF => self.utf_begin(b & 0x0F, 2, 0x800),
417            0xF0..=0xF4 => self.utf_begin(b & 7, 3, 0x10000),
418            _ => self.put('\u{FFFD}'),
419        }
420    }
421
422    fn utf_begin(&mut self, bits: u8, need: u8, min: u32) {
423        self.utf = bits as u32;
424        self.utf_need = need;
425        self.utf_min = min;
426    }
427
428    fn arg(&self, i: usize, default: u32) -> u32 {
429        match self.params.get(i) {
430            Some(&v) if v != 0 => v,
431            _ => default,
432        }
433    }
434
435    fn csi(&mut self, f: u8) {
436        match f {
437            b'm' => self.sgr(),
438            b'K' => match self.params.first().copied().unwrap_or(0) {
439                0 => self.cur.truncate(self.col),
440                1 => {
441                    let n = (self.col + 1).min(self.cur.len());
442                    for c in &mut self.cur[..n] {
443                        *c = Cell { ch: ' ', a: ATTR0 };
444                    }
445                }
446                _ => self.cur.clear(),
447            },
448            b'J' => match self.params.first().copied().unwrap_or(0) {
449                2 => {
450                    // the screen scrolls clear: what was on it stays in the scrollback
451                    if !self.cur.is_empty() {
452                        self.newline();
453                    }
454                    self.col = 0;
455                    self.floor = self.end();
456                }
457                3 => self.clear(),
458                _ => {}
459            },
460            b'H' | b'f' => self.col = self.arg(1, 1) as usize - 1,
461            b'G' => self.col = self.arg(0, 1) as usize - 1,
462            b'C' => self.col = (self.col + self.arg(0, 1) as usize).min(LINE_MAX - 1),
463            b'D' => self.col = self.col.saturating_sub(self.arg(0, 1) as usize),
464            _ => {}
465        }
466    }
467
468    fn sgr(&mut self) {
469        if self.params.is_empty() {
470            self.attr = ATTR0;
471            return;
472        }
473        let p = core::mem::take(&mut self.params);
474        let mut i = 0;
475        while i < p.len() {
476            let a = &mut self.attr;
477            match p[i] {
478                0 => *a = ATTR0,
479                1 => a.fl |= A_BOLD,
480                2 => a.fl |= A_DIM,
481                4 => a.fl |= A_UNDER,
482                7 => a.fl |= A_INV,
483                22 => a.fl &= !(A_BOLD | A_DIM),
484                24 => a.fl &= !A_UNDER,
485                27 => a.fl &= !A_INV,
486                v @ 30..=37 => a.fg = (v - 30) as u8,
487                39 => a.fg = DEF,
488                v @ 40..=47 => a.bg = (v - 40) as u8,
489                49 => a.bg = DEF,
490                v @ 90..=97 => a.fg = (v - 90 + 8) as u8,
491                v @ 100..=107 => a.bg = (v - 100 + 8) as u8,
492                v @ (38 | 48) => {
493                    // 38;5;n / 38;2;r;g;b: the nearest of the 16 colours
494                    let c = match p.get(i + 1) {
495                        Some(5) => {
496                            i += 2;
497                            p.get(i).map(|&n| ansi256(n))
498                        }
499                        Some(2) => {
500                            i += 4;
501                            match (p.get(i - 2), p.get(i - 1), p.get(i)) {
502                                (Some(&r), Some(&g), Some(&b)) => Some(rgb16(r.min(255) / 51, g.min(255) / 51, b.min(255) / 51)),
503                                _ => None,
504                            }
505                        }
506                        _ => None,
507                    };
508                    if let Some(c) = c {
509                        if v == 38 {
510                            a.fg = c;
511                        } else {
512                            a.bg = c;
513                        }
514                    }
515                }
516                _ => {}
517            }
518            i += 1;
519        }
520        self.params = p;
521    }
522}
523
524// a colour of the 6x6x6 cube (levels 0..5) as one of the 16
525fn rgb16(r: u32, g: u32, b: u32) -> u8 {
526    let bits = (r > 2) as u8 | ((g > 2) as u8) << 1 | ((b > 2) as u8) << 2;
527    let hi = r.max(g).max(b);
528    if bits == 0 {
529        return if hi >= 2 { 8 } else { 0 };
530    }
531    if hi >= 5 { bits + 8 } else { bits }
532}
533
534// xterm's 256 colours as one of the 16
535fn ansi256(n: u32) -> u8 {
536    match n {
537        0..=15 => n as u8,
538        16..=231 => {
539            let v = n - 16;
540            rgb16(v / 36, v / 6 % 6, v % 6)
541        }
542        232..=243 => 8,
543        _ => 7,
544    }
545}
546
547// ------------------------------------------------------------------ the state
548
549/// the state of one console (see the module header); the app keeps it between frames
550pub struct TermState {
551    /// a finger's way below a whole row (px, touch.rs)
552    pan_rem: i32,
553    /// the text before the input (SGR allowed)
554    pub prompt: String,
555    /// TM_LINE, TM_BUSY, TM_RAW, TM_SECRET
556    pub mode: u8,
557    /// TF_* added to the flags of ui.terminal
558    pub flags: u32,
559    /// a font id for this console (None: ui.mono_font)
560    pub font: Option<u32>,
561    /// the text size in points (default 10)
562    pub size_pt: f32,
563    /// the pager's line (default " --More-- ")
564    pub pager_text: String,
565    /// lines of output per page of the pager (0, the default: the screen minus one line)
566    pub page_len: u16,
567    scr: Screen,
568    input: String,
569    caret_want: Option<usize>,
570    history: Vec<String>,
571    hist_pos: usize,
572    hist_draft: String,
573    ta: String, // type-ahead: text typed while busy, '\n' = Enter
574    ta_at: Option<u32>, // when the last line was submitted (ms)
575    pg_open: bool,
576    pg_quit: bool,
577    pg_held: Vec<u8>,
578    pg_left: u32,
579    find_open: bool,
580    find_text: String,
581    find_hits: Vec<(u64, u32, u32)>, // line, first column, end column
582    find_idx: usize,
583    find_key: Option<(String, u64)>, // what find_hits were made for: the text, Screen::ver
584    sel: Option<((u64, u32), (u64, u32))>, // anchor, end: (line, column)
585    sel_drag: bool,
586    at_bottom: bool,
587    top: u64,
588    unseen: bool,
589    seen_added: u64,
590    cols: u16,
591    rows: u16,
592    scroll_req: i64,
593    bell_until: u32,
594    prompt_cells: Option<(String, Vec<Cell>)>,
595    body: Option<(Rc<Snap>, u32)>,
596    tail: Option<(Rc<Snap>, u32)>,
597}
598
599impl TermState {
600    /// a console keeping `scrollback` lines (at least 100)
601    pub fn new(scrollback: usize) -> TermState {
602        TermState {
603            prompt: String::new(),
604            mode: TM_LINE,
605            flags: 0,
606            font: None,
607            size_pt: 10.0,
608            pager_text: String::from(" --More-- "),
609            page_len: 0,
610            scr: Screen::new(scrollback),
611            input: String::new(),
612            caret_want: None,
613            history: Vec::new(),
614            hist_pos: 0,
615            hist_draft: String::new(),
616            ta: String::new(),
617            ta_at: None,
618            pg_open: false,
619            pg_quit: false,
620            pg_held: Vec::new(),
621            pg_left: 0,
622            find_open: false,
623            find_text: String::new(),
624            find_hits: Vec::new(),
625            find_idx: 0,
626            find_key: None,
627            sel: None,
628            sel_drag: false,
629            at_bottom: true,
630            pan_rem: 0,
631            top: 0,
632            unseen: false,
633            seen_added: 0,
634            cols: 0,
635            rows: 0,
636            scroll_req: 0,
637            bell_until: 0,
638            prompt_cells: None,
639            body: None,
640            tail: None,
641        }
642    }
643
644    /// output: UTF-8 with the ANSI subset of the module header (paged while the pager is on)
645    pub fn write(&mut self, s: &[u8]) {
646        if self.pg_open && self.pg_quit {
647            return; // the pager was quit: the rest of the paged output is dropped
648        }
649        if self.pg_open || !self.pg_held.is_empty() {
650            self.pg_feed(s);
651        } else {
652            self.scr.write(s);
653        }
654    }
655
656    /// write(s.as_bytes())
657    pub fn write_str(&mut self, s: &str) {
658        self.write(s.as_bytes());
659    }
660
661    /// drop the scrollback and the current line (the history stays)
662    pub fn clear(&mut self) {
663        self.scr.clear();
664        self.sel = None;
665        self.at_bottom = true;
666        self.find_key = None;
667    }
668
669    /// the prompt before the input (SGR allowed)
670    pub fn set_prompt(&mut self, s: &str) {
671        if self.prompt != s {
672            self.prompt = String::from(s);
673        }
674    }
675
676    /// TM_LINE, TM_BUSY, TM_RAW or TM_SECRET
677    pub fn set_mode(&mut self, mode: u8) {
678        self.mode = mode;
679    }
680
681    /// replace the input line; caret: a byte index (clamped to a character start)
682    pub fn input_set(&mut self, s: &str, caret: usize) {
683        self.input = String::from(s);
684        let mut c = caret.min(s.len());
685        while !s.is_char_boundary(c) {
686            c -= 1;
687        }
688        self.caret_want = Some(c);
689    }
690
691    /// the input line as typed so far
692    pub fn input(&self) -> &str {
693        &self.input
694    }
695
696    /// the lines submitted so far (oldest first)
697    pub fn history(&self) -> &[String] {
698        &self.history
699    }
700
701    /// the answer to TermEv::Complete: the new input line (caret at its end); a non-empty
702    /// list is shown in columns below the line as typed
703    pub fn complete_reply<S: AsRef<str>>(&mut self, line: &str, list: &[S]) {
704        if !list.is_empty() {
705            let typed = self.input.clone();
706            self.echo(&typed, "");
707            let w = list.iter().map(|s| s.as_ref().chars().count()).max().unwrap_or(0) + 2;
708            let per = (self.cols.max(1) as usize / w).max(1);
709            let mut out = String::new();
710            for (i, s) in list.iter().enumerate() {
711                let s = s.as_ref();
712                out.push_str(s);
713                if (i + 1) % per == 0 || i + 1 == list.len() {
714                    out.push('\n');
715                } else {
716                    for _ in s.chars().count()..w {
717                        out.push(' ');
718                    }
719                }
720            }
721            self.scr.write(out.as_bytes());
722        }
723        self.input_set(line, line.len());
724        self.at_bottom = true;
725    }
726
727    /// on: the following output pages (see the module header); off: the app wrote all of it
728    pub fn pager(&mut self, on: bool) {
729        if on {
730            self.pg_open = true;
731            self.pg_quit = false;
732            let held = core::mem::take(&mut self.pg_held);
733            self.scr.write(&held);
734            self.pg_left = self.page();
735        } else {
736            self.pg_open = false;
737            if self.pg_quit {
738                self.pg_held.clear();
739                self.pg_quit = false;
740            }
741        }
742    }
743
744    /// the pager holds output and waits for a key
745    pub fn pager_waiting(&self) -> bool {
746        !self.pg_held.is_empty()
747    }
748
749    /// the character grid of the last layout (columns, rows; 0 before the first)
750    pub fn grid(&self) -> (u16, u16) {
751        (self.cols, self.rows)
752    }
753
754    /// the scrollback and the current line as plain text (lines joined with '\n')
755    pub fn text(&self) -> String {
756        let mut s = String::new();
757        for l in &self.scr.lines {
758            s.push_str(&l.text);
759            s.push('\n');
760        }
761        s.extend(self.scr.cur.iter().map(|c| c.ch));
762        s
763    }
764
765    /// lines kept in the scrollback
766    pub fn line_count(&self) -> usize {
767        self.scr.lines.len()
768    }
769
770    // the lines of a page: page_len, else the screen minus one line (23 before the first layout)
771    fn page(&self) -> u32 {
772        match (self.page_len, self.rows) {
773            (0, 0) => 23,
774            (0, r) => (r.max(2) as u32 - 1).max(1),
775            (n, _) => n as u32,
776        }
777    }
778
779    // paged output: lines pass while the page has room, the rest is held
780    fn pg_feed(&mut self, s: &[u8]) {
781        if !self.pg_held.is_empty() {
782            self.pg_held.extend_from_slice(s);
783            return;
784        }
785        let mut i = 0;
786        while i < s.len() {
787            if self.pg_left == 0 {
788                self.pg_held.extend_from_slice(&s[i..]);
789                break;
790            }
791            let end = s[i..].iter().position(|&b| b == b'\n').map_or(s.len(), |p| i + p + 1);
792            self.scr.write(&s[i..end]);
793            if s[end - 1] == b'\n' {
794                self.pg_left -= 1;
795            }
796            i = end;
797        }
798    }
799
800    // the pager goes on by n lines
801    fn pg_release(&mut self, n: u32) {
802        self.pg_left = n.max(1);
803        let held = core::mem::take(&mut self.pg_held);
804        self.pg_feed(&held);
805        self.at_bottom = true;
806    }
807
808    fn pg_quit_now(&mut self, evs: &mut Vec<TermEv>) {
809        self.pg_held.clear();
810        if self.pg_open {
811            self.pg_quit = true;
812        }
813        evs.push(TermEv::PagerQuit);
814        self.at_bottom = true;
815    }
816
817    // the current line, the prompt, the line (not in TM_SECRET) and a suffix, then a line
818    // break, with the SGR state of the output kept aside
819    fn echo(&mut self, line: &str, suffix: &str) {
820        let attr = self.scr.attr;
821        self.scr.attr = ATTR0;
822        self.scr.col = self.scr.cur.len();
823        let mut s = String::with_capacity(self.prompt.len() + line.len() + suffix.len() + 1);
824        s.push_str(&self.prompt);
825        s.push_str("\x1b[0m");
826        if self.mode != TM_SECRET {
827            s.push_str(line);
828        }
829        s.push_str(suffix);
830        s.push('\n');
831        self.scr.write(s.as_bytes());
832        self.scr.attr = attr;
833    }
834
835    fn submit(&mut self, line: String, evs: &mut Vec<TermEv>) {
836        self.echo(&line, "");
837        if self.mode != TM_SECRET && !line.trim().is_empty() && self.history.last() != Some(&line) {
838            self.history.push(line.clone());
839            if self.history.len() > HIST_MAX {
840                self.history.remove(0);
841            }
842        }
843        self.hist_pos = self.history.len();
844        self.hist_draft.clear();
845        self.at_bottom = true;
846        evs.push(TermEv::Submit(line));
847    }
848
849    fn hist_move(&mut self, older: bool) {
850        let n = self.history.len();
851        if older {
852            if self.hist_pos == 0 || n == 0 {
853                return;
854            }
855            if self.hist_pos >= n {
856                self.hist_pos = n;
857                self.hist_draft = self.input.clone();
858            }
859            self.hist_pos -= 1;
860            self.input = self.history[self.hist_pos].clone();
861        } else {
862            if self.hist_pos >= n {
863                return;
864            }
865            self.hist_pos += 1;
866            self.input = if self.hist_pos == n { self.hist_draft.clone() } else { self.history[self.hist_pos].clone() };
867        }
868        self.caret_want = Some(self.input.len());
869        self.at_bottom = true;
870    }
871
872    // one waiting line of the type-ahead (TM_LINE): -> it was submitted
873    fn ta_release(&mut self, evs: &mut Vec<TermEv>) -> bool {
874        if self.ta.is_empty() {
875            return false;
876        }
877        match self.ta.find('\n') {
878            Some(i) => {
879                let mut line = core::mem::take(&mut self.input);
880                line.push_str(&self.ta[..i]);
881                self.ta.drain(..=i);
882                self.submit(line, evs);
883                true
884            }
885            None => {
886                let rest = core::mem::take(&mut self.ta);
887                self.input.push_str(&rest);
888                self.caret_want = Some(self.input.len());
889                false
890            }
891        }
892    }
893
894    // the selection, start before end
895    fn sel_range(&self) -> Option<((u64, u32), (u64, u32))> {
896        let (a, b) = self.sel?;
897        let (a, b) = if a <= b { (a, b) } else { (b, a) };
898        (a != b).then_some((a, b))
899    }
900
901    fn sel_text(&self) -> String {
902        let Some((a, b)) = self.sel_range() else { return String::new() };
903        let mut out = String::new();
904        for l in a.0..=b.0 {
905            let Some(line) = self.scr.line(l) else { continue };
906            let c0 = if l == a.0 { a.1 } else { 0 } as usize;
907            let c1 = if l == b.0 { b.1 } else { line.cells } as usize;
908            let part: String = line.text.chars().skip(c0).take(c1.saturating_sub(c0)).collect();
909            if c1 >= line.cells as usize {
910                out.push_str(part.trim_end());
911            } else {
912                out.push_str(&part);
913            }
914            if l != b.0 {
915                out.push('\n');
916            }
917        }
918        out
919    }
920
921    // the word (or run of blanks / other characters) at a position
922    fn word_at(&self, p: (u64, u32)) -> Option<((u64, u32), (u64, u32))> {
923        let line = self.scr.line(p.0)?;
924        let chars: Vec<char> = line.text.chars().collect();
925        if chars.is_empty() {
926            return None;
927        }
928        let k = (p.1 as usize).min(chars.len() - 1);
929        let class = |c: char| -> u8 {
930            if c.is_alphanumeric() || "_-./:@".contains(c) {
931                1
932            } else if c == ' ' {
933                0
934            } else {
935                2
936            }
937        };
938        let cl = class(chars[k]);
939        let mut s = k;
940        while s > 0 && class(chars[s - 1]) == cl {
941            s -= 1;
942        }
943        let mut e = k + 1;
944        while e < chars.len() && class(chars[e]) == cl {
945            e += 1;
946        }
947        Some(((p.0, s as u32), (p.0, e as u32)))
948    }
949
950    // the matches of the search text (case-insensitive), redone when text or output changed
951    fn find_update(&mut self) {
952        let key = (self.find_text.clone(), self.scr.ver);
953        if self.find_key.as_ref() == Some(&key) {
954            return;
955        }
956        let fresh = self.find_key.as_ref().is_none_or(|k| k.0 != key.0);
957        self.find_key = Some(key);
958        let cur = self.find_hits.get(self.find_idx).copied();
959        self.find_hits.clear();
960        let needle: Vec<char> = self.find_text.chars().map(fold).collect();
961        if needle.is_empty() {
962            return;
963        }
964        for (i, l) in self.scr.lines.iter().enumerate() {
965            let hay: Vec<char> = l.text.chars().map(fold).collect();
966            let mut k = 0;
967            while k + needle.len() <= hay.len() {
968                if hay[k..k + needle.len()] == needle[..] {
969                    self.find_hits.push((self.scr.base + i as u64, k as u32, (k + needle.len()) as u32));
970                    k += needle.len();
971                } else {
972                    k += 1;
973                }
974            }
975        }
976        // a new text: the newest match; new output: the same match as before
977        self.find_idx = match cur {
978            Some(c) if !fresh => self.find_hits.iter().position(|h| *h == c).unwrap_or(self.find_hits.len().saturating_sub(1)),
979            _ => self.find_hits.len().saturating_sub(1),
980        };
981        if fresh {
982            self.find_show();
983        }
984    }
985
986    // step to the next older (or newer) match
987    fn find_step(&mut self, older: bool) {
988        let n = self.find_hits.len();
989        if n == 0 {
990            return;
991        }
992        self.find_idx = if older { (self.find_idx + n - 1) % n } else { (self.find_idx + 1) % n };
993        self.find_show();
994    }
995
996    // scroll the current match into view
997    fn find_show(&mut self) {
998        let Some(&(l, c, _)) = self.find_hits.get(self.find_idx) else { return };
999        let row = self.scr.row_of(l) + c as u64 / self.scr.cols.max(1) as u64;
1000        let rows = self.rows.max(1) as u64;
1001        let top = if self.at_bottom { u64::MAX } else { self.top };
1002        if top == u64::MAX || row < top || row >= top + rows {
1003            self.at_bottom = false;
1004            self.top = row.saturating_sub(rows / 3);
1005        }
1006    }
1007}
1008
1009// a character folded for the case-insensitive search
1010fn fold(c: char) -> char {
1011    c.to_lowercase().next().unwrap_or(c)
1012}
1013
1014// ------------------------------------------------------------------ drawing
1015
1016// cells of one colour: the first column, cells, colours (bg 0: none), A_BOLD | A_UNDER
1017#[derive(Clone, PartialEq)]
1018struct Run {
1019    col: u32,
1020    n: u32,
1021    fg: u32,
1022    bg: u32,
1023    fl: u8,
1024    text: String,
1025}
1026
1027// the visible rows of one part (scrollback, or current line + input) with their font
1028#[derive(PartialEq)]
1029struct Snap {
1030    font: u32,
1031    size: i32,
1032    cw: i32,
1033    lh: i32,
1034    base: i32,
1035    ul: (i32, i32),
1036    bold_dx: i32,
1037    per_cell: bool,
1038    rows: Vec<Vec<Run>>,
1039}
1040
1041impl Draw for Snap {
1042    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
1043        g.text.set_font(self.font, self.size);
1044        for (i, row) in self.rows.iter().enumerate() {
1045            let y = r[1] + i as i32 * self.lh;
1046            for run in row.iter().filter(|run| run.bg != 0) {
1047                g.dc.set_color(run.bg);
1048                g.dc.fill(g.sf, r[0] + run.col as i32 * self.cw, y, run.n as i32 * self.cw, self.lh);
1049            }
1050            for run in row {
1051                let x0 = r[0] + run.col as i32 * self.cw;
1052                g.dc.set_color(run.fg);
1053                let passes = if run.fl & A_BOLD != 0 { 2 } else { 1 };
1054                for p in 0..passes {
1055                    let dx = p * self.bold_dx;
1056                    let b = run.text.as_bytes();
1057                    let (mut i, mut col) = (0, 0);
1058                    while i < b.len() {
1059                        let s = i;
1060                        if !self.per_cell && b[i] < 0x80 {
1061                            // ASCII runs at once (the cell width is the font's advance)
1062                            while i < b.len() && b[i] < 0x80 {
1063                                i += 1;
1064                            }
1065                        } else {
1066                            // other characters cell by cell (fallback fonts may be wider)
1067                            i += match b[i] {
1068                                0..=0x7F => 1,
1069                                0x80..=0xDF => 2,
1070                                0xE0..=0xEF => 3,
1071                                _ => 4,
1072                            };
1073                            i = i.min(b.len());
1074                        }
1075                        if b[s..i] != *b" " {
1076                            g.text.draw(&mut *g.dc, &mut *g.sf, x0 + col * self.cw + dx, y + self.base, &b[s..i]);
1077                        }
1078                        col += if i - s > 1 && b[s] < 0x80 { (i - s) as i32 } else { 1 };
1079                    }
1080                }
1081                if run.fl & A_UNDER != 0 {
1082                    g.dc.fill(g.sf, x0, y + self.ul.0, run.n as i32 * self.cw, self.ul.1);
1083                }
1084            }
1085        }
1086    }
1087}
1088
1089// builds the runs of a row cell by cell
1090#[derive(Default)]
1091struct Rb {
1092    runs: Vec<Run>,
1093}
1094
1095impl Rb {
1096    fn push(&mut self, col: u32, ch: char, (fg, bg, fl): (u32, u32, u8)) {
1097        if let Some(r) = self.runs.last_mut() {
1098            if r.fg == fg && r.bg == bg && r.fl == fl && r.col + r.n == col {
1099                r.text.push(ch);
1100                r.n += 1;
1101                return;
1102            }
1103        }
1104        self.runs.push(Run { col, n: 1, fg, bg, fl, text: String::from(ch) });
1105    }
1106}
1107
1108// the palette: 16 colours, default text and background, selection, search matches
1109struct Pal {
1110    c: [u32; 16],
1111    fg: u32,
1112    bg: u32,
1113    sel: u32,
1114    sel_fg: u32,
1115    hit: u32,
1116    hit_cur: u32,
1117}
1118
1119// the brightness of 0x..RRGGBB (0..255)
1120fn luma(c: u32) -> u32 {
1121    ((c >> 16 & 255) * 299 + (c >> 8 & 255) * 587 + (c & 255) * 114) / 1000
1122}
1123
1124// c over an opaque background
1125fn over(bg: u32, c: u32) -> u32 {
1126    let a = (c >> 24) as i32;
1127    color_mix(bg | 0xFF00_0000, c | 0xFF00_0000, a + (a >> 7))
1128}
1129
1130impl Pal {
1131    // the colours of a cell (fg, bg or 0, the flags the drawing needs)
1132    fn cell(&self, a: Attr) -> (u32, u32, u8) {
1133        let mut fg = if a.fg == DEF { self.fg } else { self.c[(a.fg & 15) as usize] };
1134        let mut bg = if a.bg == DEF { 0 } else { self.c[(a.bg & 15) as usize] };
1135        if a.fg == DEF && bg != 0 && luma(fg).abs_diff(luma(bg)) < 110 {
1136            fg = self.bg; // the default text stays readable on a background of its brightness
1137        }
1138        if a.fl & A_DIM != 0 {
1139            fg = color_mix(fg, self.bg, 110);
1140        }
1141        if a.fl & A_INV != 0 {
1142            let b = if bg == 0 { self.bg } else { bg };
1143            bg = fg;
1144            fg = b;
1145        }
1146        (fg, bg, a.fl & (A_BOLD | A_UNDER))
1147    }
1148}
1149
1150// font, size (24.8), cell width, line height, baseline; per_cell: a proportional fallback
1151#[derive(Clone, Copy)]
1152struct Metrics {
1153    font: u32,
1154    size: i32,
1155    cw: i32,
1156    lh: i32,
1157    base: i32,
1158    per_cell: bool,
1159}
1160
1161/// shared state (Ui::m.term): the monospace font of the consoles
1162#[derive(Default)]
1163pub(crate) struct S {
1164    pub(crate) mono: Option<u32>, // (svg) imgview.rs: the face of "monospace" in SVG text
1165}
1166
1167impl Ui {
1168    /// the monospace font of every console (a font id, e.g. from ui.text.load_mem)
1169    pub fn mono_font_set(&mut self, font: u32) {
1170        self.m.term.mono = Some(font);
1171    }
1172
1173    /// the consoles' monospace font at size_pt points -> (font id, cell width px): the font of
1174    /// mono_font_set, else the system's "monospace", else FONT_REG (drawn cell by cell)
1175    pub fn mono_font(&mut self, size_pt: f32) -> (u32, i32) {
1176        let m = self.term_metrics(None, size_pt);
1177        (m.font, m.cw)
1178    }
1179
1180    fn term_metrics(&mut self, font: Option<u32>, size_pt: f32) -> Metrics {
1181        let (font, per_cell) = match font.or(self.m.term.mono) {
1182            Some(f) if self.text.valid(f) => (f, false),
1183            _ => match self.font_load(&FontSel::new("monospace", size_pt as f64)) {
1184                Some(f) => (f, false),
1185                None => (text::FONT_REG, true),
1186            },
1187        };
1188        let dpi = self.wp.rt.dpi as f32;
1189        let scale = self.text_scale as f32 / 100.0;
1190        let mut size = ((size_pt.clamp(4.0, 96.0) * 4.0 / 3.0 * dpi / 96.0 * scale * 256.0) as i32).max(256);
1191        let prev = self.text.get_font();
1192        self.text.set_font(font, size);
1193        let cw = if per_cell {
1194            self.text.width("M").max(self.text.width("W")).max(1)
1195        } else {
1196            // ten cells, so that the rounding of the advance does not add up along a row
1197            let adv = self.text.measure("0000000000");
1198            let cw = ((adv + 1280) / 2560).max(1);
1199            if adv > 0 {
1200                size = (size as i64 * (cw as i64 * 2560) / adv as i64) as i32;
1201                self.text.set_font(font, size);
1202            }
1203            cw
1204        };
1205        let (asc, desc) = (self.text.ascent(), self.text.descent());
1206        let lh = self.text.line_h().max(asc + desc - 1).max(1);
1207        let base = ((lh - asc - desc) >> 1) + asc;
1208        self.text.set_font(prev.0, prev.1);
1209        Metrics { font, size, cw, lh, base, per_cell }
1210    }
1211
1212    fn term_palette(&self) -> Pal {
1213        let dark = self.theme_dark();
1214        let bg = over(self.color(T_BG), self.color(T_INPUT_ACTIVE));
1215        let fg = over(bg, self.color(T_TEXT));
1216        let red = over(bg, self.color(T_CRIT));
1217        let green = over(bg, self.color(T_OK));
1218        let yellow = over(bg, self.color(T_WARN));
1219        let blue = over(bg, self.color(T_INFO));
1220        let magenta = color_mix(red, blue, 128);
1221        let cyan = color_mix(green, blue, 140);
1222        let (black, white, gray) = if dark {
1223            (color_mix(bg, fg, 70), over(bg, self.color(T_TEXT2)), over(bg, self.color(T_TEXT3)))
1224        } else {
1225            (fg, color_mix(fg, bg, 150), over(bg, self.color(T_TEXT3)))
1226        };
1227        let lift = |c: u32| color_mix(c, if dark { 0xFFFF_FFFF } else { 0xFF00_0000 }, if dark { 70 } else { 40 });
1228        let base = [black, red, green, yellow, blue, magenta, cyan, white];
1229        let mut c = [0; 16];
1230        for i in 0..8 {
1231            c[i] = base[i];
1232            c[i + 8] = lift(base[i]);
1233        }
1234        c[8] = gray;
1235        c[15] = if dark { fg } else { color_mix(white, bg, 120) };
1236        Pal {
1237            c,
1238            fg,
1239            bg,
1240            sel: over(bg, self.color(T_ACCENT)),
1241            sel_fg: self.color(T_ON_ACCENT) | 0xFF00_0000,
1242            hit: over(bg, self.color(T_WARN_BG)),
1243            hit_cur: yellow,
1244        }
1245    }
1246
1247    // take an unconsumed key with exactly these modifiers (Shift, Ctrl, Alt), past the
1248    // reservation of the focused text box
1249    fn term_key(&mut self, vk: u32, mods: u32) -> bool {
1250        let m = MOD_SHIFT | MOD_CTRL | MOD_ALT;
1251        let q = &mut self.wp.ctx.keyq;
1252        match q.iter().position(|&k| (k as i32) >= 0 && k & 0xFFFF == vk && (k >> 16) & m == mods && k >> 16 & MOD_ALTGR == 0) {
1253            Some(i) => {
1254                q[i] |= 0x8000_0000;
1255                true
1256            }
1257            None => false,
1258        }
1259    }
1260
1261    // take the first unconsumed key: VK | MOD_* << 16
1262    fn term_key_next(&mut self) -> Option<u32> {
1263        let q = &mut self.wp.ctx.keyq;
1264        let i = q.iter().position(|&k| (k as i32) >= 0)?;
1265        let k = q[i];
1266        q[i] |= 0x8000_0000;
1267        Some(k)
1268    }
1269
1270    /// ui_terminal(id, st, flags) -> events: the console (see the module header)
1271    #[cfg_attr(jfgui_dev, track_caller)]
1272    pub fn terminal(&mut self, id: u32, st: &mut TermState, flags: u32) -> Vec<TermEv> {
1273        let _src = self.src_scope();
1274        let flags = flags | st.flags;
1275        let mut evs = Vec::new();
1276        let sid = id.wrapping_add(1); // the input line
1277        let (bar_id, find_id, more_id) = (id.wrapping_add(2), id.wrapping_add(3), id.wrapping_add(7));
1278        let r = self.place(self.dp(480), self.dp(240));
1279        let m = self.term_metrics(st.font, st.size_pt);
1280        let hair = self.hair();
1281        let style = self.style;
1282        let sbw = style.scrollbar_thickness(self).max(self.dp(10));
1283        let frame = flags & TF_NO_FRAME == 0;
1284        let pad = if frame { self.dp(6) } else { self.dp(2) };
1285        let (tx, ty) = (r[0] + pad, r[1] + pad);
1286        let tw = (r[2] - pad * 2 - sbw).max(m.cw);
1287        let th = (r[3] - pad * 2).max(m.lh);
1288        let cols = (tw / m.cw).max(1) as usize;
1289        let vrows = (th / m.lh).max(1) as usize;
1290        if (cols as u16, vrows as u16) != (st.cols, st.rows) {
1291            // keep the first visible line when the width changes
1292            let anchor = (!st.at_bottom).then(|| {
1293                let l = st.scr.line_at_row(st.top);
1294                (l, l.map_or(0, |l| st.top - st.scr.row_of(l)))
1295            });
1296            st.scr.set_cols(cols);
1297            if let Some((Some(l), sub)) = anchor {
1298                let rows = st.scr.line(l).map_or(1, |x| line_rows(x.cells, cols));
1299                st.top = st.scr.row_of(l) + sub.min(rows - 1);
1300            }
1301            st.cols = cols.min(65535) as u16;
1302            st.rows = vrows.min(65535) as u16;
1303            evs.push(TermEv::Resize(st.cols, st.rows));
1304        }
1305        // focus: the console's id stands for its input line
1306        if self.wp.ctx.focus == id {
1307            self.wp.ctx.focus = sid;
1308        }
1309        let fids = sid..=id.wrapping_add(6);
1310        let active_win = self.uiw_cur == self.m.win.active;
1311        let focused = active_win && fids.contains(&self.wp.ctx.focus);
1312        let on_input = active_win && self.wp.ctx.focus == sid;
1313        let on_find = active_win && self.wp.ctx.focus == find_id;
1314        let line_mode = matches!(st.mode, TM_LINE | TM_SECRET) && flags & TF_NO_INPUT == 0;
1315        let waiting = st.pager_waiting();
1316        let show_input = line_mode && !waiting && st.mode == TM_LINE;
1317
1318        // ---- keys
1319        let mut help_rest: Option<Vec<u32>> = None;
1320        if focused {
1321            self.term_keys_common(st, &mut evs, id, on_find);
1322        }
1323        if on_input {
1324            if waiting {
1325                self.term_keys_pager(st, &mut evs);
1326            } else if flags & TF_NO_INPUT != 0 {
1327                let _ = self.text_take(usize::MAX);
1328            } else {
1329                match st.mode {
1330                    TM_RAW => self.term_keys_raw(st, &mut evs),
1331                    TM_BUSY | TM_SECRET => self.term_keys_typed(st, &mut evs, flags),
1332                    _ => help_rest = self.term_keys_line(st, &mut evs, sid, flags),
1333                }
1334            }
1335        }
1336        // type-ahead: a waiting line every 16 ms while the app reads lines (the app may go
1337        // busy on it); the timer brings the next
1338        let due = st.ta_at.is_none_or(|t| self.now_ms.wrapping_sub(t) >= 16);
1339        if line_mode && !st.pager_waiting() && due && st.ta_release(&mut evs) {
1340            st.ta_at = Some(self.now_ms);
1341            self.repass();
1342        }
1343        if line_mode && !st.pager_waiting() && !st.ta.is_empty() {
1344            self.timer(UIT_TERM, 16);
1345        }
1346
1347        // ---- the current line, prompt and input (the tail)
1348        let waiting = st.pager_waiting();
1349        let show_input = show_input && !st.pager_waiting();
1350        let mut tail: Vec<Cell> = st.scr.cur.clone();
1351        let mut p0 = tail.len(); // the cell of the input's first character
1352        if waiting {
1353            tail.extend(st.pager_text.chars().map(|ch| Cell { ch, a: Attr { fg: DEF, bg: DEF, fl: A_INV } }));
1354        } else if line_mode {
1355            if st.prompt_cells.as_ref().is_none_or(|p| p.0 != st.prompt) {
1356                let mut s = Screen::new(100);
1357                s.write(st.prompt.as_bytes());
1358                st.prompt_cells = Some((st.prompt.clone(), s.cur));
1359            }
1360            if let Some((_, pc)) = &st.prompt_cells {
1361                tail.extend_from_slice(pc);
1362            }
1363            p0 = tail.len();
1364            if st.mode == TM_LINE {
1365                tail.extend(st.input.chars().map(|ch| Cell { ch, a: ATTR0 }));
1366            }
1367        }
1368        let n_in = if show_input { st.input.chars().count() } else { 0 };
1369        // the cursor's last possible cell: after the input / in the current line
1370        let no_cursor = waiting || flags & TF_NO_INPUT != 0;
1371        let cursor_max = if no_cursor {
1372            0
1373        } else if line_mode {
1374            p0 + n_in + 1
1375        } else {
1376            st.scr.col + 1
1377        };
1378        let tail_cells = tail.len().max(cursor_max).max(1);
1379        let tail_rows = tail_cells.div_ceil(cols) as u64;
1380
1381        // ---- scrolling
1382        let first = st.scr.first_row();
1383        let end_all = st.scr.row_end + tail_rows;
1384        let floor_row = if st.scr.floor >= st.scr.end() { st.scr.row_end } else { st.scr.row_of(st.scr.floor) };
1385        let bottom = end_all.saturating_sub(vrows as u64).max(floor_row).max(first);
1386        let clamp_top = |t: i64| -> u64 { (t.max(first as i64) as u64).min(bottom) };
1387        let mut top = if st.at_bottom { bottom } else { st.top.clamp(first, bottom) };
1388        if st.scroll_req != 0 {
1389            top = clamp_top(top as i64 + st.scroll_req);
1390            st.scroll_req = 0;
1391        }
1392
1393        // ---- pointer: selection, wheel
1394        let ev = self.hit_rect(id, r[0], r[1], r[2], r[3], CUR_IBEAM << HB_CURSOR_SHIFT);
1395        // a focused console takes the keyboard as a text box does, also while it shows no
1396        // input line (busy, the pager): no plain-key accelerators, no app undo (HB_TEXT);
1397        // output only (TF_NO_INPUT) it does not
1398        if on_input && !show_input && flags & TF_NO_INPUT == 0 {
1399            let _ = self.hit_rect(sid, r[0], r[1], 0, 0, HB_TEXT);
1400        }
1401        // (right-to-left: the rows keep their order inside the text columns)
1402        let (mx, my) = (self.rtl_map(self.wp.ctx.mx, tx, cols as i32 * m.cw), self.wp.ctx.my);
1403        let hit_pos = |st: &TermState, top: u64| -> Option<(u64, u32)> {
1404            let v = (my - ty).div_euclid(m.lh);
1405            let row = (top as i64 + v as i64).max(first as i64) as u64;
1406            match st.scr.line_at_row(row) {
1407                Some(l) => {
1408                    let line = st.scr.line(l)?;
1409                    let sub = row - st.scr.row_of(l);
1410                    let c = ((mx - tx + m.cw / 2).div_euclid(m.cw)).clamp(0, cols as i32) as u64;
1411                    Some((l, (sub * cols as u64 + c).min(line.cells as u64) as u32))
1412                }
1413                None => {
1414                    let l = st.scr.end().checked_sub(1)?;
1415                    Some((l, st.scr.line(l)?.cells))
1416                }
1417            }
1418        };
1419        let c = &self.wp.ctx;
1420        if c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id {
1421            self.wp.ctx.focus = sid; // a click takes the keyboard focus (no ring)
1422            self.wp.ctx.focus_visible = false;
1423            if let Some(p) = hit_pos(st, top) {
1424                if ev & EV_DOUBLE != 0 {
1425                    st.sel = st.word_at(p);
1426                    st.sel_drag = false;
1427                } else if self.mods & MOD_SHIFT != 0 && st.sel.is_some() {
1428                    st.sel = st.sel.map(|(a, _)| (a, p));
1429                    st.sel_drag = true;
1430                } else {
1431                    st.sel = Some((p, p));
1432                    st.sel_drag = true;
1433                }
1434            }
1435            self.repass();
1436        } else if ev & EV_HELD != 0 && st.sel_drag {
1437            if my < ty {
1438                top = clamp_top(top as i64 - 1);
1439            } else if my >= ty + vrows as i32 * m.lh {
1440                top = clamp_top(top as i64 + 1);
1441            }
1442            if let Some(p) = hit_pos(st, top) {
1443                st.sel = st.sel.map(|(a, _)| (a, p));
1444            }
1445        } else if st.sel_drag {
1446            st.sel_drag = false;
1447            if st.sel_range().is_none() {
1448                st.sel = None;
1449            }
1450        }
1451        let wy = self.wp.ctx.wheel_y;
1452        if (ev & EV_HOVER != 0 || self.wp.ctx.hot == sid) && wy != 0 {
1453            let t = clamp_top(top as i64 - (wy as i64 * 3) / 120);
1454            if t != top {
1455                top = t;
1456                self.wp.ctx.wheel_y = 0;
1457            }
1458        }
1459        // a finger may roll it, row by row (the way below a row waits); its way (touch.rs)
1460        let lh = m.lh.max(1) as i64;
1461        let (_, pan) = self.pan_take(id);
1462        if pan != 0 {
1463            let p = (top - first) as i64 * lh + st.pan_rem as i64 + pan as i64;
1464            let t = clamp_top(first as i64 + p.div_euclid(lh));
1465            st.pan_rem = if t >= bottom || t <= first && p < 0 { 0 } else { p.rem_euclid(lh) as i32 };
1466            top = t;
1467        }
1468        let hit_end = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
1469        let (pos, max) = ((top - first) as i64 * lh + st.pan_rem as i64, (bottom - first) as i64 * lh);
1470        self.pan_view(id, hit_end.saturating_sub(1), super::touch::AX_V, [0, pos.min(i32::MAX as i64) as i32], [0, max.min(i32::MAX as i64) as i32]);
1471        st.at_bottom = top >= bottom;
1472        st.top = top;
1473        if st.at_bottom {
1474            st.unseen = false;
1475            st.seen_added = st.scr.added;
1476        } else if st.scr.added != st.seen_added {
1477            st.unseen = true;
1478        }
1479
1480        // ---- frame
1481        let pal = self.term_palette();
1482        if frame {
1483            let stroke = self.color(if focused { T_ACCENT } else { T_CTRL_STROKE });
1484            self.cmd_rrect(r[0], r[1], r[2], r[3], self.dp(4), hair, pal.bg, stroke, stroke);
1485        } else {
1486            self.draw_color(pal.bg);
1487            self.draw_rect(r[0], r[1], r[2], r[3]);
1488        }
1489        self.clip_push(r[0] + hair, r[1] + hair, r[2] - hair * 2, r[3] - hair * 2);
1490        self.ltr_begin(tx, cols as i32 * m.cw);
1491        self.wp.ctx.last_state = 0;
1492        self.sem(K_CANVAS, id, None, st.scr.lines.len() as i32);
1493
1494        // ---- the scrollback rows
1495        if st.find_open {
1496            st.find_update();
1497        }
1498        let sel = st.sel_range();
1499        let body_vis = (st.scr.row_end.saturating_sub(top) as usize).min(vrows);
1500        let mut rows: Vec<Vec<Run>> = Vec::with_capacity(body_vis);
1501        let mut sems: Vec<(i32, i32, String)> = Vec::new();
1502        let mut row = top;
1503        while rows.len() < body_vis {
1504            let Some(l) = st.scr.line_at_row(row) else { break };
1505            let Some(line) = st.scr.line(l) else { break };
1506            let r0 = st.scr.row_of(l);
1507            let sub0 = (row - r0) as usize;
1508            let nrows = line_rows(line.cells, cols) as usize;
1509            let sub_end = nrows.min(sub0 + body_vis - rows.len());
1510            let mut rbs: Vec<Rb> = (sub0..sub_end).map(|_| Rb::default()).collect();
1511            let hits_from = st.find_hits.partition_point(|h| h.0 < l);
1512            let hits = st.find_hits[hits_from..].iter().take_while(|h| h.0 == l);
1513            let cur_hit = st.find_hits.get(st.find_idx).copied();
1514            let mut sp = line.spans.iter().peekable();
1515            let mut a = ATTR0;
1516            for (k, ch) in line.text.chars().enumerate() {
1517                while let Some(&&(c, at)) = sp.peek() {
1518                    if c as usize > k {
1519                        break;
1520                    }
1521                    a = at;
1522                    sp.next();
1523                }
1524                let rr = k / cols;
1525                if rr < sub0 {
1526                    continue;
1527                }
1528                if rr >= sub_end {
1529                    break;
1530                }
1531                let mut col = pal.cell(a);
1532                let kk = k as u32;
1533                if sel.is_some_and(|(s, e)| (l, kk) >= s && (l, kk) < e) {
1534                    col = (pal.sel_fg, pal.sel, col.2);
1535                } else if let Some(h) = hits.clone().find(|h| h.1 <= kk && kk < h.2) {
1536                    col = if Some(*h) == cur_hit { (pal.bg, pal.hit_cur, col.2) } else { (pal.fg, pal.hit, col.2) };
1537                }
1538                rbs[rr - sub0].push((k % cols) as u32, ch, col);
1539            }
1540            let y = ty + rows.len() as i32 * m.lh;
1541            sems.push((y, (sub_end - sub0) as i32 * m.lh, line.text.clone()));
1542            rows.extend(rbs.into_iter().map(|rb| rb.runs));
1543            row = r0 + sub_end as u64;
1544        }
1545        let snap = |rows: Vec<Vec<Run>>| Snap {
1546            font: m.font,
1547            size: m.size,
1548            cw: m.cw,
1549            lh: m.lh,
1550            base: m.base,
1551            ul: (m.base + 1 + (m.size >> 8) / 12, ((m.size >> 8) / 14).max(1)),
1552            bold_dx: (m.size >> 8) / 16 + 1,
1553            per_cell: m.per_cell,
1554            rows,
1555        };
1556        let w = cols as i32 * m.cw;
1557        if body_vis > 0 {
1558            let (d, ver) = term_cached(&mut st.body, snap(rows));
1559            self.cmd_custom(tx, ty, w, body_vis as i32 * m.lh, Some(d), 0, ver);
1560        }
1561        for (y, h, label) in sems.into_iter().filter(|s| !s.2.is_empty()) {
1562            self.wp.ctx.last_rect = [tx, y, w, h];
1563            self.sem(K_TEXT, 0, Some(&label), 0);
1564        }
1565
1566        // ---- the tail rows
1567        let tail_y0 = ty + (st.scr.row_end as i64 - top as i64) as i32 * m.lh; // its first row
1568        let t_first = top.saturating_sub(st.scr.row_end) as usize; // first visible tail row
1569        let t_end = ((top + vrows as u64).saturating_sub(st.scr.row_end) as usize).min(tail_rows as usize);
1570        if t_first < t_end {
1571            let mut rbs: Vec<Rb> = (t_first..t_end).map(|_| Rb::default()).collect();
1572            for (k, cell) in tail.iter().enumerate() {
1573                let rr = k / cols;
1574                if rr >= t_first && rr < t_end {
1575                    rbs[rr - t_first].push((k % cols) as u32, cell.ch, pal.cell(cell.a));
1576                }
1577            }
1578            let y = tail_y0 + t_first as i32 * m.lh;
1579            let (d, ver) = term_cached(&mut st.tail, snap(rbs.into_iter().map(|rb| rb.runs).collect()));
1580            self.cmd_custom(tx, y, w, (t_end - t_first) as i32 * m.lh, Some(d), 0, ver);
1581            let label: String = tail.iter().map(|c| c.ch).collect();
1582            if !label.is_empty() {
1583                self.wp.ctx.last_rect = [tx, y, w, (t_end - t_first) as i32 * m.lh];
1584                self.sem(K_TEXT, 0, Some(&label), 0);
1585            }
1586        }
1587
1588        // ---- the input line: a text surface over the input's cells
1589        if show_input {
1590            if let Some(c) = st.caret_want {
1591                if self.textbox_select(sid, &st.input, c, c) {
1592                    st.caret_want = None;
1593                }
1594            }
1595            let starts: Vec<usize> = st.input.char_indices().map(|(i, _)| i).chain(core::iter::once(st.input.len())).collect();
1596            let mut lines = Vec::new();
1597            let (r_first, r_last) = (p0 / cols, (p0 + n_in) / cols);
1598            for rr in r_first..=r_last {
1599                let i0 = (rr * cols).saturating_sub(p0).min(n_in);
1600                let i1 = ((rr + 1) * cols).saturating_sub(p0).min(n_in);
1601                let y = tail_y0 + rr as i32 * m.lh;
1602                let carets = (i0..=i1).map(|i| (starts[i], tx + ((p0 + i) - rr * cols) as i32 * m.cw)).collect();
1603                lines.push(TextSurfaceLine { start: starts[i0], end: starts[i1], rect: [tx, y, w.max(1), m.lh], carets });
1604            }
1605            let before = st.input.clone();
1606            self.textbox_surface(sid, &mut st.input, INPUT_CAP, &lines);
1607            if st.input != before {
1608                st.at_bottom = true;
1609                st.hist_pos = st.history.len();
1610            }
1611            if let Some(c) = st.caret_want.take() {
1612                self.textbox_select(sid, &st.input, c.min(st.input.len()), c.min(st.input.len()));
1613            }
1614            // a pasted line break: one Submit per line, the rest waits as type-ahead
1615            if let Some(i) = st.input.find('\n') {
1616                let rest = String::from(&st.input[i + 1..]);
1617                let line = String::from(&st.input[..i]);
1618                st.input.clear();
1619                st.ta.insert_str(0, &rest);
1620                st.submit(line, &mut evs);
1621                st.ta_at = Some(self.now_ms);
1622                self.timer(UIT_TERM, 16);
1623                self.repass();
1624            }
1625            // TF_QHELP: the `?` asks for help about the line up to the caret
1626            if let Some(rest) = help_rest {
1627                let caret = self.textbox_selection(sid).map_or(st.input.len(), |s| s.1).min(st.input.len());
1628                let line = String::from(&st.input[..caret]);
1629                st.echo(&line, "?");
1630                st.ta.insert_str(0, &rest.iter().filter_map(|&c| char::from_u32(c)).collect::<String>());
1631                st.at_bottom = true;
1632                evs.push(TermEv::Help(line));
1633            }
1634        }
1635
1636        // ---- the cursor (drawn over the text box's own caret, after the box moved it)
1637        let sel_in = self.textbox_selection(sid);
1638        let cursor = if no_cursor || sel_in.is_some_and(|s| s.0 != s.1) {
1639            None
1640        } else if show_input {
1641            let caret = sel_in.map_or(st.input.len(), |s| s.1).min(st.input.len());
1642            Some(p0 + st.input[..caret].chars().count().min(n_in))
1643        } else if line_mode {
1644            Some(p0)
1645        } else {
1646            Some(st.scr.col)
1647        };
1648        if let Some(k) = cursor {
1649            let rr = k / cols;
1650            if rr >= t_first && rr < t_end {
1651                let (x, y) = (tx + (k % cols) as i32 * m.cw, tail_y0 + rr as i32 * m.lh);
1652                if on_input {
1653                    self.draw_color(pal.fg);
1654                    self.draw_rect(x, y, m.cw, m.lh);
1655                    if let Some(cell) = tail.get(k).filter(|c| c.ch != ' ') {
1656                        let mut b = [0u8; 4];
1657                        let s = cell.ch.encode_utf8(&mut b);
1658                        self.cmd_text(x, y + m.base, s.as_bytes(), m.font, m.size, pal.bg, 0);
1659                    }
1660                } else {
1661                    self.draw_color(color_mix(pal.bg, pal.fg, 160));
1662                    self.draw_frame(x, y, m.cw, m.lh, 0, hair);
1663                }
1664            }
1665        }
1666        self.ltr_end();
1667
1668        // ---- scroll bar, "Neue Ausgabe", search bar, bell
1669        let mut pos = (top - first).min(i32::MAX as u64) as i32;
1670        // (a cleared screen may show less than a screenful at the bottom)
1671        let content = (end_all.max(bottom + vrows as u64) - first).min(i32::MAX as u64) as i32;
1672        self.next_rect_px(r[0] + r[2] - sbw - hair, r[1] + hair, sbw, r[3] - hair * 2);
1673        if self.scrollbar(bar_id, &mut pos, content, vrows as i32, true) & EV_ACT != 0 {
1674            let t = (first + pos as u64).min(bottom);
1675            st.top = t;
1676            st.at_bottom = t >= bottom;
1677        }
1678        if st.unseen && !st.at_bottom {
1679            let label = crate::i18n::tr("Neue Ausgabe");
1680            self.ts_font(TS_BODY);
1681            let bw = self.text.width(label) + self.dp(48);
1682            let bh = self.dp(30);
1683            self.next_rect_px(r[0] + (r[2] - bw) / 2, r[1] + r[3] - bh - self.dp(10), bw, bh);
1684            self.next_icon(IC_DOWN);
1685            if self.accent_button(more_id, label) & EV_ACT != 0 {
1686                st.at_bottom = true;
1687                self.wp.ctx.focus = sid;
1688                self.wp.ctx.focus_visible = false;
1689            }
1690        }
1691        if st.find_open {
1692            self.term_find_bar(id, st, r, sbw);
1693        }
1694        if st.scr.bell {
1695            st.scr.bell = false;
1696            st.bell_until = self.now_ms.wrapping_add(BELL_MS);
1697            self.timer(UIT_TERM_BELL, BELL_MS + 1);
1698        }
1699        if (st.bell_until.wrapping_sub(self.now_ms) as i32) > 0 {
1700            self.draw_color((self.color(T_ACCENT) & 0x00FF_FFFF) | 0x30 << 24);
1701            self.draw_rect(r[0], r[1], r[2], r[3]);
1702        }
1703        self.clip_pop();
1704        self.wp.ctx.last_rect = r;
1705        if !evs.is_empty() {
1706            self.repass();
1707        }
1708        evs
1709    }
1710
1711    // keys of every mode: scrolling, copying, the search bar
1712    fn term_keys_common(&mut self, st: &mut TermState, evs: &mut Vec<TermEv>, id: u32, on_find: bool) {
1713        let page = (st.rows.max(2) - 1) as i64;
1714        for (vk, mods, d) in [
1715            (VK_PRIOR, 0, -page),
1716            (VK_NEXT, 0, page),
1717            (VK_PRIOR, MOD_SHIFT, -page),
1718            (VK_NEXT, MOD_SHIFT, page),
1719            (VK_UP, MOD_CTRL, -1),
1720            (VK_DOWN, MOD_CTRL, 1),
1721            (VK_HOME, MOD_CTRL, i64::MIN / 2),
1722            (VK_END, MOD_CTRL, i64::MAX / 2),
1723        ] {
1724            if st.mode == TM_RAW && mods == 0 {
1725                continue; // raw: PgUp / PgDn go to the app
1726            }
1727            if self.term_key(vk, mods) {
1728                st.scroll_req = st.scroll_req.saturating_add(d);
1729            }
1730        }
1731        if st.sel_range().is_some() && (self.term_key(VK_C, MOD_CTRL | MOD_SHIFT) || self.term_key(VK_C, MOD_CTRL) || self.term_key(VK_INSERT, MOD_CTRL)) {
1732            let s = st.sel_text();
1733            self.clip_set(CLIP_TEXT, s.as_bytes());
1734        }
1735        if (st.mode != TM_RAW || on_find) && self.term_key(0x46, MOD_CTRL) {
1736            // Ctrl+F: the search bar (again: select its text; raw: the app's key)
1737            st.find_open = true;
1738            self.focus(id.wrapping_add(3));
1739        }
1740        if st.find_open {
1741            let older = self.term_key(VK_F3, 0) || (on_find && self.term_key(VK_RETURN, 0));
1742            let newer = self.term_key(VK_F3, MOD_SHIFT) || (on_find && self.term_key(VK_RETURN, MOD_SHIFT));
1743            if older || newer {
1744                st.find_update();
1745                st.find_step(older);
1746                self.repass();
1747            }
1748            if on_find && self.term_key(VK_ESCAPE, 0) {
1749                st.find_open = false;
1750                self.wp.ctx.focus = id.wrapping_add(1);
1751                self.wp.ctx.focus_visible = false;
1752                self.repass();
1753            }
1754        }
1755        // Esc: the search bar closes, else the selection goes, else the pager quits (else it
1756        // stays for the text box / the app)
1757        if !on_find && (st.find_open || st.sel.is_some() || st.pager_waiting()) && self.term_key(VK_ESCAPE, 0) {
1758            if st.find_open {
1759                st.find_open = false;
1760            } else if st.sel.is_some() {
1761                st.sel = None;
1762            } else {
1763                st.pg_quit_now(evs);
1764            }
1765            self.repass();
1766        }
1767    }
1768
1769    // the --More-- pager: Space a page, Enter a line, q / Ctrl+C quit (Esc: common keys)
1770    fn term_keys_pager(&mut self, st: &mut TermState, evs: &mut Vec<TermEv>) {
1771        let _ = self.text_take(usize::MAX); // the characters of these keys
1772        self.term_undo_keys();
1773        let page = st.page();
1774        if self.term_key(VK_SPACE, 0) {
1775            st.pg_release(page);
1776        } else if self.term_key(VK_RETURN, 0) {
1777            st.pg_release(1);
1778        } else if self.term_key(0x51, 0) || self.term_key(0x51, MOD_SHIFT) || self.term_key(VK_C, MOD_CTRL) {
1779            st.pg_quit_now(evs);
1780        } else {
1781            return;
1782        }
1783        self.repass();
1784    }
1785
1786    // TM_RAW: characters and keys to the app
1787    fn term_keys_raw(&mut self, st: &mut TermState, evs: &mut Vec<TermEv>) {
1788        let mut out: Vec<u32> = self.text_take(usize::MAX).into_iter().filter(|&c| c >= 32 && c != 127).collect();
1789        if self.term_key(0x56, MOD_CTRL | MOD_SHIFT) {
1790            if let Some(clip) = self.clip_get(CLIP_TEXT) {
1791                out.extend(String::from_utf8_lossy(&clip).chars().map(|c| if c == '\n' { 13 } else { c as u32 }).filter(|&c| c >= 32 || c == 13));
1792            }
1793        }
1794        while let Some(k) = self.term_key_next() {
1795            let vk = k & 0xFFFF;
1796            let mods = k >> 16 & (MOD_SHIFT | MOD_CTRL | MOD_ALT);
1797            let ctrl = mods & (MOD_CTRL | MOD_ALT) == MOD_CTRL && k >> 16 & MOD_ALTGR == 0;
1798            let typing = vk == VK_SPACE || (0x30..=0x5A).contains(&vk) || (0x60..=0x6F).contains(&vk) || (0xBA..=0xE2).contains(&vk);
1799            out.push(match vk {
1800                VK_RETURN => 13,
1801                VK_TAB if mods == 0 => 9,
1802                VK_BACK => 127,
1803                VK_ESCAPE => 27,
1804                0x41..=0x5A if ctrl => vk - 0x40, // Ctrl+letter: its control code
1805                _ if typing && mods & (MOD_CTRL | MOD_ALT) == 0 => continue, // the text follows
1806                VK_SHIFT | VK_CONTROL | VK_MENU => continue,
1807                _ => TK_KEY | k & 0x7FFF_FFFF,
1808            });
1809        }
1810        if !out.is_empty() {
1811            st.at_bottom = true;
1812            evs.push(TermEv::Raw(out));
1813        }
1814    }
1815
1816    // TM_BUSY (type-ahead) and TM_SECRET (a hidden line): typed text without a text box
1817    fn term_keys_typed(&mut self, st: &mut TermState, evs: &mut Vec<TermEv>, flags: u32) {
1818        let secret = st.mode == TM_SECRET;
1819        let buf = if secret { &mut st.input } else { &mut st.ta };
1820        for c in self.text_take(usize::MAX) {
1821            if c >= 32 && c != 127 {
1822                buf.push(char::from_u32(c).unwrap_or('\u{FFFD}'));
1823            }
1824        }
1825        if self.term_key(VK_BACK, 0) && !buf.ends_with('\n') {
1826            buf.pop();
1827        }
1828        if self.term_key(VK_RETURN, 0) || self.term_key(VK_RETURN, MOD_SHIFT) {
1829            if secret {
1830                let line = core::mem::take(&mut st.input);
1831                st.submit(line, evs);
1832            } else {
1833                st.ta.push('\n');
1834            }
1835        }
1836        let ctrl_c = self.term_key(VK_C, MOD_CTRL);
1837        if ctrl_c || (flags & TF_CTRL_SHIFT6 != 0 && (self.term_key(0x36, MOD_CTRL | MOD_SHIFT) || self.term_key(0x36, MOD_CTRL))) {
1838            st.ta.clear();
1839            if secret {
1840                st.input.clear();
1841                st.echo("", "^C");
1842            } else if ctrl_c {
1843                st.scr.write(b"^C"); // the program ends the line (Ctrl+Shift+6: no echo)
1844            }
1845            evs.push(TermEv::Interrupt);
1846            st.at_bottom = true;
1847        }
1848        let empty = if secret { st.input.is_empty() } else { st.ta.is_empty() || st.ta.ends_with('\n') };
1849        if empty && self.term_key(0x44, MOD_CTRL) {
1850            evs.push(TermEv::Eof);
1851        }
1852        if (empty || !secret) && self.term_key(0x5A, MOD_CTRL) {
1853            evs.push(TermEv::Chord(0x5A | MOD_CTRL << 16));
1854        }
1855        self.term_undo_keys();
1856    }
1857
1858    // the undo keys (Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z) belong to the console as to a text box:
1859    // without an input line it swallows them, so the app's undo does not take them
1860    fn term_undo_keys(&mut self) {
1861        for (vk, mods) in [(0x5A, MOD_CTRL), (0x59, MOD_CTRL), (0x5A, MOD_CTRL | MOD_SHIFT)] {
1862            let _ = self.term_key(vk, mods);
1863        }
1864    }
1865
1866    // TM_LINE: the keys the input line's text box must not see -> the characters typed
1867    // after a `?` (TF_QHELP), handled after the text box
1868    fn term_keys_line(&mut self, st: &mut TermState, evs: &mut Vec<TermEv>, sid: u32, flags: u32) -> Option<Vec<u32>> {
1869        let caret = self.textbox_selection(sid).map_or(st.input.len(), |s| s.1).min(st.input.len());
1870        let has_sel = self.textbox_selection(sid).is_some_and(|s| s.0 != s.1);
1871        if !self.wp.ctx.textq.is_empty() {
1872            st.at_bottom = true;
1873        }
1874        if self.term_key(VK_RETURN, 0) || self.term_key(VK_RETURN, MOD_SHIFT) || self.term_key(VK_RETURN, MOD_CTRL) {
1875            let line = core::mem::take(&mut st.input);
1876            st.submit(line, evs);
1877        }
1878        if self.term_key(VK_TAB, 0) {
1879            st.at_bottom = true;
1880            evs.push(TermEv::Complete { line: st.input.clone(), caret });
1881        }
1882        if self.term_key(VK_UP, 0) {
1883            st.hist_move(true);
1884        }
1885        if self.term_key(VK_DOWN, 0) {
1886            st.hist_move(false);
1887        }
1888        let _ = self.term_key(VK_UP, MOD_SHIFT) || self.term_key(VK_DOWN, MOD_SHIFT);
1889        if self.term_key(VK_HOME, 0) {
1890            st.caret_want = Some(0);
1891        }
1892        if self.term_key(VK_END, 0) {
1893            st.caret_want = Some(st.input.len());
1894        }
1895        if !has_sel && self.term_key(VK_C, MOD_CTRL) {
1896            let line = core::mem::take(&mut st.input);
1897            st.echo(&line, "^C");
1898            st.at_bottom = true;
1899            evs.push(TermEv::Interrupt);
1900        }
1901        if flags & TF_CTRL_SHIFT6 != 0 && (self.term_key(0x36, MOD_CTRL | MOD_SHIFT) || self.term_key(0x36, MOD_CTRL)) {
1902            evs.push(TermEv::Interrupt);
1903        }
1904        if st.input.is_empty() {
1905            if self.term_key(0x44, MOD_CTRL) {
1906                evs.push(TermEv::Eof);
1907            }
1908            if self.term_key(0x5A, MOD_CTRL) {
1909                evs.push(TermEv::Chord(0x5A | MOD_CTRL << 16));
1910            }
1911        }
1912        if self.term_key(0x4C, MOD_CTRL) {
1913            evs.push(TermEv::Chord(0x4C | MOD_CTRL << 16));
1914        }
1915        if self.term_key(0x55, MOD_CTRL) {
1916            // Ctrl+U: the line before the caret goes
1917            // (the line may have changed above: Enter, Ctrl+C, history)
1918            let mut caret = caret.min(st.input.len());
1919            while !st.input.is_char_boundary(caret) {
1920                caret -= 1;
1921            }
1922            st.input.drain(..caret);
1923            st.caret_want = Some(0);
1924        }
1925        if flags & TF_QHELP != 0 {
1926            let q = &mut self.wp.ctx.textq;
1927            if let Some(k) = q.iter().position(|&c| c == '?' as u32) {
1928                let rest = q.split_off(k);
1929                return Some(rest[1..].to_vec());
1930            }
1931        }
1932        None
1933    }
1934
1935    // the search bar at the top right: text, "n/m", older, newer, close
1936    fn term_find_bar(&mut self, id: u32, st: &mut TermState, r: [i32; 4], sbw: i32) {
1937        let (fid, up_id, down_id, close_id) = (id.wrapping_add(3), id.wrapping_add(4), id.wrapping_add(5), id.wrapping_add(6));
1938        let h = self.dp(30);
1939        let g = self.dp(4);
1940        let tb_w = self.dp(160);
1941        let count = if st.find_hits.is_empty() {
1942            String::from(if st.find_text.is_empty() { "" } else { "0/0" })
1943        } else {
1944            alloc::format!("{}/{}", st.find_idx + 1, st.find_hits.len())
1945        };
1946        self.ts_font(TS_CAPTION);
1947        let cw = self.text.width(&count).max(self.dp(36));
1948        let pw = g + tb_w + g + cw + g + h * 3 + g;
1949        let ph = h + g * 2;
1950        let (px, py) = (r[0] + r[2] - sbw - pw - self.dp(8), r[1] + self.dp(6));
1951        let (fill, stroke) = (self.color(T_FLYOUT), self.color(T_FLYOUT_STROKE));
1952        self.cmd_rrect(px, py, pw, ph, self.dp(6), self.hair(), fill | 0xFF00_0000, stroke, stroke);
1953        self.wp.ctx.last_rect = [px, py, pw, ph];
1954        self.hit(0, HB_BLOCK); // clicks between the parts do not select
1955        let mut x = px + g;
1956        self.next_rect_px(x, py + g, tb_w, h);
1957        let before = st.find_text.clone();
1958        self.textbox(fid, Some(crate::i18n::tr("Suchen")), &mut st.find_text, 256);
1959        if st.find_text != before {
1960            st.find_update();
1961            self.repass();
1962        }
1963        x += tb_w + g;
1964        let base = self.ts_base(TS_CAPTION);
1965        let line = self.ts_line(TS_CAPTION);
1966        let c = self.color(T_TEXT2);
1967        let tw = self.text.width(&count);
1968        self.emit_text(x + (cw - tw) / 2, py + g + (h - line) / 2 + base, &count, c);
1969        if !count.is_empty() {
1970            self.wp.ctx.last_rect = [x, py + g, cw, h];
1971            self.sem(K_TEXT, 0, Some(&count), 0);
1972        }
1973        x += cw + g;
1974        for (bid, icon, tip, older) in [
1975            (up_id, IC_UP, "Vorheriger Treffer", Some(true)),
1976            (down_id, IC_DOWN, "Nächster Treffer", Some(false)),
1977            (close_id, IC_CHROME_CLOSE, "Schließen", None),
1978        ] {
1979            self.next_rect_px(x, py + g, h, h);
1980            self.next_flags(ST_SUBTLE | ST_ICON_ONLY);
1981            if self.icon_button(bid, icon, crate::i18n::tr(tip)) & EV_ACT != 0 {
1982                match older {
1983                    Some(o) => {
1984                        st.find_update();
1985                        st.find_step(o);
1986                    }
1987                    None => {
1988                        st.find_open = false;
1989                        self.wp.ctx.focus = id.wrapping_add(1);
1990                        self.wp.ctx.focus_visible = false;
1991                    }
1992                }
1993                self.repass();
1994            }
1995            x += h;
1996        }
1997    }
1998}
1999
2000// the drawing object of one part: the cached one while it shows the same, else a new one
2001fn term_cached(slot: &mut Option<(Rc<Snap>, u32)>, snap: Snap) -> (DrawRef, u32) {
2002    if let Some((d, ver)) = slot {
2003        if **d == snap {
2004            let r: DrawRef = d.clone();
2005            return (r, *ver);
2006        }
2007    }
2008    let ver = slot.as_ref().map_or(1, |s| s.1.wrapping_add(1));
2009    let d = Rc::new(snap);
2010    *slot = Some((d.clone(), ver));
2011    let r: DrawRef = d;
2012    (r, ver)
2013}