1mod block_layout_state;
17mod presentation_mutation_generation;
18mod presentation_observation_state;
19mod prompt_editor_state;
20mod redraw_sync_generation;
21mod renderer_delivery_id;
22#[cfg(test)]
23mod terminal_generation_tests;
24mod terminal_history_generation;
25mod terminal_runtime_state;
26
27use std::cell::RefCell;
28use std::collections::{HashMap, HashSet};
29use std::io::{self, BufWriter, Write};
30use std::sync::{Arc, Mutex, MutexGuard, atomic as path_std_sync_atomic};
31use std::thread::{self, JoinHandle};
32use std::time::Duration;
33use std::{sync as path_std_sync, time as path_std_time};
34
35use base64::engine as path_base64_engine;
36use crossterm::cursor as path_crossterm_cursor;
37const PROMPT_INPUT_MAX_HEIGHT_PERCENT: usize = 33;
38const INPUT_HISTORY_MAX_ENTRIES: usize = 1000;
40const INPUT_HISTORY_MAX_BYTES: usize = 16 * 1024 * 1024;
42const STALL_WARNING_INTERVAL: Duration = Duration::from_secs(5);
43static STALL_WARNING_LIMITER: Mutex<StallWarningLimiter> =
44 Mutex::new(StallWarningLimiter { last: None });
45
46#[derive(Clone, Copy)]
48struct InputHistoryLimits {
49 max_entries: usize,
51 max_bytes: usize,
53}
54
55struct StallWarningLimiter {
57 last: Option<std::time::Instant>,
59}
60
61impl StallWarningLimiter {
62 fn admit(&mut self, now: std::time::Instant) -> bool {
64 if self
65 .last
66 .is_some_and(|last| now.duration_since(last) < STALL_WARNING_INTERVAL)
67 {
68 return false;
69 }
70 self.last = Some(now);
71 true
72 }
73}
74
75fn admit_stall_warning() -> bool {
76 STALL_WARNING_LIMITER
77 .lock()
78 .expect("stall warning mutex poisoned")
79 .admit(path_std_time::Instant::now())
80}
81
82use block_layout_state::BlockLayoutState;
83use crossterm::cursor::{MoveToColumn, MoveUp, SetCursorStyle};
84use crossterm::event::{
85 self, DisableMouseCapture, Event as CtEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
86 KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
87};
88use crossterm::style::Print;
89use crossterm::{QueueableCommand, terminal};
90use presentation_observation_state::{
91 CapturedPresentationObservations, PresentationObservationState,
92};
93pub use presentation_observation_state::{
94 OpaquePresentationFact, PresentationInvalidation, PresentationObservationKey,
95};
96use prompt_editor_state::PromptEditorState;
97use redraw_sync_generation::RedrawSyncGeneration;
98pub use renderer_delivery_id::RendererDeliveryId;
99pub use tau_term_screen::{
100 Align, BlockId, Cell, CellRow, Color, PriorityLine, PriorityLineAlignment,
101 PriorityLinePriority, PriorityLineTruncation, Span, Style, StyledBlock, StyledText,
102 TwoLineElision, sanitize_hyperlink_target,
103};
104use tau_term_screen::{
105 Screen, display_width, emit_styled_cells, layout_block, layout_lines, next_grapheme_boundary,
106 previous_grapheme_boundary, truncate_to_width,
107};
108use terminal_history_generation::TerminalHistoryGeneration;
109use terminal_runtime_state::TerminalRuntimeState;
110use unicode_segmentation::UnicodeSegmentation;
111
112type NamedActionHandler = fn(&Term) -> Option<Event>;
113
114const NAMED_ACTIONS: &[(&str, NamedActionHandler)] = &[
118 ("accept-completion", Term::accept_completion_event),
119 ("backtab", Term::backtab_action),
120 ("clear-prompt", Term::clear_prompt_action),
121 (
122 "clear-or-cancel-prompt",
123 Term::clear_or_cancel_prompt_action,
124 ),
125 ("cursor-down", Term::cycle_or_move_down),
126 ("cursor-end", Term::move_cursor_end_action),
127 ("cursor-left", Term::move_cursor_left_action),
128 ("cursor-right", Term::move_cursor_right_action),
129 ("cursor-start", Term::move_cursor_start_action),
130 ("cursor-up", Term::cycle_or_move_up),
131 ("delete-backward", Term::delete_backward_action),
132 ("delete-forward", Term::delete_forward_action),
133 ("dismiss-completion", Term::dismiss_completion_event),
134 ("escape", Term::escape_action),
135 ("kill-to-start", Term::kill_to_start_action),
136 ("kill-word-left", Term::kill_word_left_action),
137 ("move-down", Term::move_cursor_down_action),
138 ("move-up", Term::move_cursor_up_action),
139 ("prompt-eof", Term::prompt_eof_action),
140 (
141 "select-completion-next",
142 Term::select_completion_next_action,
143 ),
144 (
145 "select-completion-previous",
146 Term::select_completion_previous_action,
147 ),
148];
149
150fn named_action_handler(action: &str) -> Option<NamedActionHandler> {
151 NAMED_ACTIONS
152 .iter()
153 .find_map(|(name, handler)| (*name == action).then_some(*handler))
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub enum CursorShape {
159 Bar,
161 Block,
163}
164
165impl CursorShape {
166 fn crossterm_style(self) -> crossterm::cursor::SetCursorStyle {
167 match self {
168 Self::Bar => path_crossterm_cursor::SetCursorStyle::SteadyBar,
169 Self::Block => path_crossterm_cursor::SetCursorStyle::SteadyBlock,
170 }
171 }
172}
173
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub struct TerminalOptions {
177 pub cursor_shape: CursorShape,
179 pub mouse: bool,
185}
186
187impl Default for TerminalOptions {
188 fn default() -> Self {
189 Self {
190 cursor_shape: CursorShape::Bar,
191 mouse: true,
192 }
193 }
194}
195
196#[derive(Clone, Debug)]
198pub struct Candidate {
199 pub label: String,
201 pub description: String,
203 pub replacement: String,
205 pub cursor: usize,
207 pub acceptance: Option<CompletionAcceptance>,
210}
211
212#[derive(Clone, Debug)]
214pub struct CompletionAcceptance {
215 pub replacement: String,
217 pub cursor: usize,
219}
220
221pub trait CompletionSource: Send + Sync {
227 fn candidates(&self, buffer: &str, cursor: usize) -> Vec<Candidate>;
240}
241
242impl<F> CompletionSource for F
243where
244 F: Fn(&str, usize) -> Vec<Candidate> + Send + Sync,
245{
246 fn candidates(&self, buffer: &str, cursor: usize) -> Vec<Candidate> {
247 (self)(buffer, cursor)
248 }
249}
250
251#[derive(Clone, Debug)]
253pub struct CompletionView {
254 pub candidates: Vec<Candidate>,
256 pub selected: Option<usize>,
258}
259
260#[derive(Clone)]
261struct PromptSnapshot {
262 buffer: String,
263 cursor: usize,
264}
265
266#[derive(Clone)]
267struct PromptDraft {
268 buffer: String,
269 cursor: usize,
270 undo: Vec<PromptSnapshot>,
271 redo: Vec<PromptSnapshot>,
272}
273
274impl PromptDraft {
275 fn submitted(buffer: String) -> Self {
276 let cursor = buffer.len();
277 Self {
278 buffer,
279 cursor,
280 undo: Vec::new(),
281 redo: Vec::new(),
282 }
283 }
284}
285
286struct HistoryNavEntry {
288 draft: PromptDraft,
290 source_index: Option<usize>,
293}
294
295struct HistoryNav {
299 entries: Vec<HistoryNavEntry>,
302 index: usize,
304}
305
306struct CompletionMenu {
308 candidates: Vec<Candidate>,
309 selected: Option<usize>,
312 original_buffer: String,
313 original_cursor: usize,
314}
315
316struct SharedState {
319 layout: BlockLayoutState,
321 editor: PromptEditorState,
323 terminal: TerminalRuntimeState,
325 presentation_observations: PresentationObservationState,
327 #[cfg(test)]
329 presentation_failure_test_records: Vec<(&'static str, u128, usize, u64)>,
330 #[cfg(any(test, feature = "history-retention-test-support"))]
332 input_history_limit_override: Option<InputHistoryLimits>,
333}
334
335impl SharedState {
336 fn new(width: usize, height: usize, left_prompt: StyledText) -> Self {
337 Self {
338 layout: BlockLayoutState::new(),
339 editor: PromptEditorState::new(left_prompt),
340 terminal: TerminalRuntimeState::new(width, height),
341 presentation_observations: PresentationObservationState::new(),
342 #[cfg(test)]
343 presentation_failure_test_records: Vec::new(),
344 #[cfg(any(test, feature = "history-retention-test-support"))]
345 input_history_limit_override: None,
346 }
347 }
348
349 fn advance_completion_generation(&mut self) {
352 self.editor.completion_generation = self.editor.completion_generation.wrapping_add(1);
353 }
354
355 fn alloc_id(&mut self) -> BlockId {
356 let id = BlockId(self.layout.next_id);
357 self.layout.next_id += 1;
358 id
359 }
360
361 fn mark_history_dirty_from(&mut self, entry: usize) {
362 self.layout.history_generation.advance();
363 self.layout.history_dirty_from = Some(
364 self.layout
365 .history_dirty_from
366 .map_or(entry, |dirty| dirty.min(entry)),
367 );
368 }
369
370 fn add_history_ref(&mut self, id: BlockId) {
371 *self.layout.history_refs.entry(id).or_insert(0) += 1;
372 }
373
374 fn append_history(&mut self, id: BlockId) {
375 let appended_at = self.layout.history.len();
376 self.layout.history.push(id);
377 self.add_history_ref(id);
378 self.mark_history_dirty_from(appended_at);
379 }
380
381 fn remove_history_refs(&mut self, id: BlockId, count: usize) {
382 if count == 0 {
383 return;
384 }
385 if let Some(existing) = self.layout.history_refs.get_mut(&id) {
386 if *existing <= count {
387 self.layout.history_refs.remove(&id);
388 } else {
389 *existing -= count;
390 }
391 }
392 }
393
394 fn rebuild_history_refs(&mut self) {
395 self.layout.history_refs.clear();
396 for &id in &self.layout.history {
397 *self.layout.history_refs.entry(id).or_insert(0) += 1;
398 }
399 self.mark_history_dirty_from(0);
400 }
401
402 fn block_in_history(&self, id: BlockId) -> bool {
403 self.layout.history_refs.contains_key(&id)
404 }
405
406 fn block_is_visible(&self, id: BlockId) -> bool {
408 self.block_in_history(id)
409 || self.layout.above_active.contains(&id)
410 || self.layout.above_sticky.contains(&id)
411 || self.layout.suggestions.contains(&id)
412 || self.layout.below.contains(&id)
413 }
414
415 fn remove_block(&mut self, id: BlockId, observe_delta: bool) -> (bool, Option<StyledBlock>) {
417 let presentation_changed = observe_delta && self.block_is_visible(id);
418 let removed_block = self.layout.blocks.remove(&id);
419 let existed = removed_block.is_some();
420 let debug_id = self.layout.block_debug_ids.remove(&id);
421
422 if self.block_in_history(id) {
427 #[cfg(test)]
428 {
429 self.layout.history_removal_scan_entries += self.layout.history.len();
430 }
431 let removal = remove_all_from_zone(&mut self.layout.history, id);
432 let indexed_refs = self
433 .layout
434 .history_refs
435 .get(&id)
436 .copied()
437 .expect("history membership index must contain referenced block");
438 debug_assert_eq!(
439 removal.count, indexed_refs,
440 "history membership index must exactly count duplicate references"
441 );
442 self.remove_history_refs(id, removal.count);
443 self.mark_history_dirty_from(
444 removal
445 .first_index
446 .expect("history membership index must imply one matching entry"),
447 );
448 }
449
450 remove_all_from_zone(&mut self.layout.above_active, id);
451 remove_all_from_zone(&mut self.layout.above_sticky, id);
452 remove_all_from_zone(&mut self.layout.suggestions, id);
453 remove_all_from_zone(&mut self.layout.below, id);
454 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, ?debug_id, existed, "remove block");
455 (presentation_changed, removed_block)
456 }
457
458 fn current_snapshot(&self) -> PromptSnapshot {
459 PromptSnapshot {
460 buffer: self.editor.buffer.clone(),
461 cursor: self.editor.cursor,
462 }
463 }
464
465 fn current_draft(&self) -> PromptDraft {
466 PromptDraft {
467 buffer: self.editor.buffer.clone(),
468 cursor: self.editor.cursor,
469 undo: self.editor.current_undo.clone(),
470 redo: self.editor.current_redo.clone(),
471 }
472 }
473
474 fn limit_input_history(&mut self) {
477 let limits = self.input_history_limits();
478 let recalled_source = self.editor.last_submitted_recalled_source;
479 let mut retained_source = None;
480 let mut original_entries = 0;
481 let mut retained_entries = 0;
482 self.editor.input_history.retain(|draft| {
483 let retain = !draft.buffer.is_empty() && draft.buffer.len() <= limits.max_bytes;
484 if retain && recalled_source == Some(original_entries) {
485 retained_source = Some(retained_entries);
486 }
487 original_entries += 1;
488 retained_entries += usize::from(retain);
489 retain
490 });
491 if self.editor.input_history.len() != original_entries {
492 self.editor.history_nav = None;
493 }
494 self.editor.last_submitted_recalled_source = retained_source;
495
496 let mut retained_bytes = 0;
497 let mut retained_start = self.editor.input_history.len();
498 for (retained_entries, (index, draft)) in self
499 .editor
500 .input_history
501 .iter()
502 .enumerate()
503 .rev()
504 .enumerate()
505 {
506 if retained_entries == limits.max_entries
507 || draft.buffer.len() > limits.max_bytes - retained_bytes
508 {
509 break;
510 }
511 retained_bytes += draft.buffer.len();
512 retained_start = index;
513 }
514 if retained_start == 0 {
515 return;
516 }
517
518 self.editor.input_history.drain(..retained_start);
519 self.editor.history_nav = None;
520 self.editor.last_submitted_recalled_source = self
521 .editor
522 .last_submitted_recalled_source
523 .and_then(|index| index.checked_sub(retained_start));
524 }
525
526 fn input_history_limits(&self) -> InputHistoryLimits {
528 #[cfg(any(test, feature = "history-retention-test-support"))]
529 if let Some(limits) = self.input_history_limit_override {
530 return limits;
531 }
532 InputHistoryLimits {
533 max_entries: INPUT_HISTORY_MAX_ENTRIES,
534 max_bytes: INPUT_HISTORY_MAX_BYTES,
535 }
536 }
537
538 fn take_submitted_draft(&mut self) -> PromptDraft {
544 PromptDraft {
545 buffer: self.editor.buffer.clone(),
546 cursor: self.editor.cursor,
547 undo: std::mem::take(&mut self.editor.current_undo),
548 redo: std::mem::take(&mut self.editor.current_redo),
549 }
550 }
551
552 fn load_draft(&mut self, draft: PromptDraft) {
553 self.editor.buffer = draft.buffer;
554 self.editor.current_undo = draft.undo;
555 self.editor.current_redo = draft.redo;
556 self.editor.cursor = draft.cursor.min(self.editor.buffer.len());
557 self.ensure_input_cursor_visible();
558 }
559
560 fn record_undo(&mut self) {
561 self.editor.current_undo.push(self.current_snapshot());
562 self.editor.current_redo.clear();
563 }
564
565 fn sync_buffer_to_history_nav(&mut self) {
569 let draft = self.current_draft();
570 if let Some(nav) = self.editor.history_nav.as_mut() {
571 nav.entries[nav.index].draft = draft.clone();
572 if let Some(source_index) = nav.entries[nav.index].source_index
573 && let Some(source) = self.editor.input_history.get_mut(source_index)
574 {
575 *source = draft;
576 }
577 }
578 }
579
580 fn visual_cursor_position(&self) -> (usize, usize) {
584 let width = self.terminal.width.max(1);
585 let left_cols = self.editor.left_prompt.char_count();
586 buffer_position_for_byte(&self.editor.buffer, self.editor.cursor, width, left_cols)
587 }
588
589 fn last_visual_row(&self) -> usize {
591 let width = self.terminal.width.max(1);
592 let left_cols = self.editor.left_prompt.char_count();
593 let (max_row, _) = buffer_end_position(&self.editor.buffer, width, left_cols);
594 max_row
595 }
596
597 fn cursor_byte_at(&self, target_row: usize, target_col: usize) -> usize {
601 let width = self.terminal.width.max(1);
602 let left_cols = self.editor.left_prompt.char_count();
603 byte_offset_for_buffer_position(
604 &self.editor.buffer,
605 target_row,
606 target_col,
607 width,
608 left_cols,
609 )
610 }
611
612 fn vertical_target_col(&mut self) -> usize {
616 if let Some(col) = self.editor.sticky_col {
617 return col;
618 }
619 let (_, col) = self.visual_cursor_position();
620 self.editor.sticky_col = Some(col);
621 col
622 }
623
624 fn write_cursor(&mut self, new_cursor: usize) {
630 self.editor.cursor = new_cursor;
631 self.editor.sticky_col = None;
632 self.ensure_input_cursor_visible();
633 }
634
635 fn write_cursor_keep_sticky(&mut self, new_cursor: usize) {
639 self.editor.cursor = new_cursor;
640 self.ensure_input_cursor_visible();
641 }
642
643 fn input_visible_rows(&self) -> usize {
644 let total_rows = self.last_visual_row() + 1;
645 let cap_rows = prompt_input_max_rows(self.terminal.height);
646 let indicator_rows = prompt_scroll_indicator_rows(
647 self.editor.show_prompt_scroll_indicator,
648 !self.editor.buffer.is_empty(),
649 total_rows,
650 cap_rows,
651 );
652 prompt_editable_rows(total_rows, cap_rows, indicator_rows)
653 }
654
655 fn ensure_input_cursor_visible(&mut self) {
656 let (cursor_row, _) = self.visual_cursor_position();
657 let total_rows = self.last_visual_row() + 1;
658 let visible_rows = self.input_visible_rows();
659 self.editor.input_viewport_start = viewport_start_with_cursor(
660 self.editor.input_viewport_start,
661 cursor_row,
662 total_rows,
663 visible_rows,
664 );
665 }
666
667 fn push_current_as_history_entry(&mut self, enforce_limit: bool) -> bool {
672 if self.editor.buffer.is_empty() {
673 if enforce_limit {
674 self.limit_input_history();
675 }
676 return false;
677 }
678 let draft = self.take_submitted_draft();
679 self.editor.input_history.push(draft);
680 if enforce_limit {
681 self.limit_input_history();
682 }
683 self.editor.buffer.clear();
684 self.write_cursor(0);
685 true
686 }
687
688 fn undo(&mut self) -> bool {
689 let Some(snapshot) = self.editor.current_undo.pop() else {
690 return false;
691 };
692 self.editor.current_redo.push(self.current_snapshot());
693 self.editor.buffer = snapshot.buffer;
694 self.write_cursor(snapshot.cursor.min(self.editor.buffer.len()));
695 self.sync_buffer_to_history_nav();
696 true
697 }
698
699 fn redo(&mut self) -> bool {
700 let Some(snapshot) = self.editor.current_redo.pop() else {
701 return false;
702 };
703 self.editor.current_undo.push(self.current_snapshot());
704 self.editor.buffer = snapshot.buffer;
705 self.write_cursor(snapshot.cursor.min(self.editor.buffer.len()));
706 self.sync_buffer_to_history_nav();
707 true
708 }
709
710 fn cycle_completion(&mut self, delta: isize) -> bool {
715 let (new_buffer, new_cursor) = {
716 let Some(menu) = self.editor.completion.as_mut() else {
717 return false;
718 };
719 let len = menu.candidates.len();
720 if len == 0 {
721 return false;
722 }
723 let new_selected = match menu.selected {
724 None => Some(if 0 < delta { 0 } else { len - 1 }),
725 Some(0) if delta < 0 => None,
729 Some(i) => Some((i as isize + delta).rem_euclid(len as isize) as usize),
730 };
731 menu.selected = new_selected;
732 match new_selected {
733 None => (menu.original_buffer.clone(), menu.original_cursor),
734 Some(idx) => {
735 let candidate = &menu.candidates[idx];
736 let buf = candidate.replacement.clone();
737 let cursor = candidate.cursor;
738 (buf, cursor)
739 }
740 }
741 };
742 self.editor.buffer = new_buffer;
743 self.write_cursor(new_cursor);
744 self.advance_completion_generation();
745 true
746 }
747
748 fn dismiss_completion(&mut self) -> bool {
752 let Some(menu) = self.editor.completion.take() else {
753 return false;
754 };
755 if menu.selected.is_some() {
756 self.editor.buffer = menu.original_buffer;
757 self.write_cursor(menu.original_cursor);
758 }
759 self.advance_completion_generation();
760 true
761 }
762
763 fn accept_completion(&mut self) -> bool {
768 let Some(menu) = self.editor.completion.take() else {
769 return false;
770 };
771 let Some(selected) = menu.selected else {
772 self.editor.completion = Some(menu);
773 return false;
774 };
775 if let Some(acceptance) = &menu.candidates[selected].acceptance {
776 self.editor.buffer.clone_from(&acceptance.replacement);
777 self.write_cursor(acceptance.cursor);
778 }
779 self.advance_completion_generation();
780 true
781 }
782
783 fn step_history(&mut self, delta: isize) -> bool {
795 let target_col = self.vertical_target_col();
796 if self.editor.history_nav.is_none() {
797 if 0 < delta {
798 return self.push_current_as_history_entry(true);
799 }
800 return self.enter_history_nav(target_col);
801 }
802 self.advance_history_nav(delta, target_col)
803 }
804
805 fn enter_history_nav(&mut self, target_col: usize) -> bool {
809 if self.editor.input_history.is_empty() {
810 return false;
811 }
812 let mut entries: Vec<_> = self
813 .editor
814 .input_history
815 .iter()
816 .cloned()
817 .enumerate()
818 .map(|(source_index, draft)| HistoryNavEntry {
819 draft,
820 source_index: Some(source_index),
821 })
822 .collect();
823 entries.push(HistoryNavEntry {
824 draft: self.current_draft(),
825 source_index: None,
826 });
827 let index = entries.len() - 2;
830 self.load_draft(entries[index].draft.clone());
831 let new_cursor = self.cursor_byte_at(self.last_visual_row(), target_col);
832 self.write_cursor_keep_sticky(new_cursor);
833 self.editor.history_nav = Some(HistoryNav { entries, index });
834 true
835 }
836
837 fn recall_prompt_before_current(&mut self, text: String) {
838 let previous = self.current_draft();
839 let previous_source = self.editor.history_nav.as_ref().and_then(|nav| {
840 nav.entries
841 .get(nav.index)
842 .and_then(|entry| entry.source_index)
843 });
844 let mut entries: Vec<_> = self
845 .editor
846 .input_history
847 .iter()
848 .cloned()
849 .enumerate()
850 .map(|(source_index, draft)| HistoryNavEntry {
851 draft,
852 source_index: Some(source_index),
853 })
854 .collect();
855 entries.push(HistoryNavEntry {
856 draft: PromptDraft::submitted(text),
857 source_index: None,
858 });
859 entries.push(HistoryNavEntry {
860 draft: previous,
861 source_index: previous_source,
862 });
863 let index = entries.len() - 2;
864 self.load_draft(entries[index].draft.clone());
865 self.write_cursor(self.editor.buffer.len());
866 self.editor.history_nav = Some(HistoryNav { entries, index });
867 self.editor.completion = None;
868 }
869
870 fn advance_history_nav(&mut self, delta: isize, target_col: usize) -> bool {
875 let current = self.current_draft();
876 let nav = self
877 .editor
878 .history_nav
879 .as_mut()
880 .expect("caller checked Some");
881 let new_index = nav.index as isize + delta;
882 if new_index < 0 {
883 return false;
884 }
885 if new_index >= nav.entries.len() as isize {
886 let wip = nav.entries.last().map(|entry| entry.draft.clone());
887 self.editor.history_nav = None;
888 if let Some(wip) = wip {
889 self.load_draft(wip);
890 }
891 return self.push_current_as_history_entry(true);
892 }
893 nav.entries[nav.index].draft = current.clone();
894 if let Some(source_index) = nav.entries[nav.index].source_index
895 && let Some(source) = self.editor.input_history.get_mut(source_index)
896 {
897 *source = current;
898 }
899 nav.index = new_index as usize;
900 let new_draft = nav.entries[nav.index].draft.clone();
901 self.load_draft(new_draft);
902 let target_row = if delta < 0 { self.last_visual_row() } else { 0 };
903 let new_cursor = self.cursor_byte_at(target_row, target_col);
904 self.write_cursor_keep_sticky(new_cursor);
905 true
906 }
907}
908
909#[derive(Clone, Debug, Eq, Hash, PartialEq)]
910enum KeyBinding {
911 Ctrl(char),
912 CtrlShift(char),
913 Meta(char),
914 CtrlKey(KeyCode),
915 Key(KeyCode),
916}
917
918fn parse_plain_key_code(input: &str) -> Option<KeyCode> {
919 match input.to_ascii_lowercase().as_str() {
920 "backspace" => Some(KeyCode::Backspace),
921 "backtab" | "shift-tab" => Some(KeyCode::BackTab),
922 "delete" | "del" => Some(KeyCode::Delete),
923 "down" => Some(KeyCode::Down),
924 "end" => Some(KeyCode::End),
925 "enter" => Some(KeyCode::Enter),
926 "esc" | "escape" => Some(KeyCode::Esc),
927 "home" => Some(KeyCode::Home),
928 "left" => Some(KeyCode::Left),
929 "right" => Some(KeyCode::Right),
930 "tab" => Some(KeyCode::Tab),
931 "up" => Some(KeyCode::Up),
932 _ => None,
933 }
934}
935
936fn parse_key_binding(input: &str) -> Option<KeyBinding> {
937 let input = input.trim_matches('`');
938 if let Some(code) = parse_plain_key_code(input) {
939 return Some(KeyBinding::Key(code));
940 }
941 if let Some(rest) = input.strip_prefix("M-") {
942 let mut chars = rest.chars();
943 let ch = chars.next()?;
944 return (chars.next().is_none() && ch.is_ascii()).then_some(KeyBinding::Meta(ch));
945 }
946 let rest = input
947 .strip_prefix("C-")
948 .or_else(|| input.strip_prefix("c-"))?;
949 match rest.to_ascii_lowercase().as_str() {
950 "enter" => return Some(KeyBinding::CtrlKey(KeyCode::Enter)),
951 "up" => return Some(KeyBinding::CtrlKey(KeyCode::Up)),
952 "down" => return Some(KeyBinding::CtrlKey(KeyCode::Down)),
953 _ => {}
954 }
955 let mut chars = rest.chars();
956 let ch = chars.next()?;
957 if chars.next().is_some() {
958 return None;
959 }
960 if ch.is_ascii_uppercase() {
961 Some(KeyBinding::CtrlShift(ch.to_ascii_lowercase()))
962 } else {
963 Some(KeyBinding::Ctrl(ch.to_ascii_lowercase()))
964 }
965}
966
967fn key_binding_for_event(key: KeyEvent, ctrl: bool) -> Option<KeyBinding> {
968 let modifiers = key.modifiers;
969 let plain = modifiers.is_empty();
970 let ctrl_only = modifiers == KeyModifiers::CONTROL;
971
972 match key.code {
973 KeyCode::Char(ch) if modifiers == KeyModifiers::ALT => Some(KeyBinding::Meta(ch)),
974 KeyCode::Char(ch)
975 if ctrl
976 && ch.is_ascii_alphabetic()
977 && (modifiers.contains(KeyModifiers::SHIFT) || ch.is_ascii_uppercase()) =>
978 {
979 Some(KeyBinding::CtrlShift(ch.to_ascii_lowercase()))
980 }
981 KeyCode::Char(ch) if ctrl => Some(KeyBinding::Ctrl(ch.to_ascii_lowercase())),
982 KeyCode::Char(ch @ '\u{1}'..='\u{1a}') => {
983 let letter = (b'a' + ch as u8 - 1) as char;
984 Some(KeyBinding::Ctrl(letter))
985 }
986 KeyCode::Enter if ctrl_only => Some(KeyBinding::CtrlKey(KeyCode::Enter)),
987 KeyCode::Up | KeyCode::Down if ctrl_only => Some(KeyBinding::CtrlKey(key.code)),
988 KeyCode::BackTab => Some(KeyBinding::Key(KeyCode::BackTab)),
989 KeyCode::Backspace
990 | KeyCode::Delete
991 | KeyCode::Down
992 | KeyCode::End
993 | KeyCode::Enter
994 | KeyCode::Esc
995 | KeyCode::Home
996 | KeyCode::Left
997 | KeyCode::Right
998 | KeyCode::Tab
999 | KeyCode::Up
1000 if plain =>
1001 {
1002 Some(KeyBinding::Key(key.code))
1003 }
1004 _ => None,
1005 }
1006}
1007pub enum Event {
1009 Line(String),
1012 Eof,
1014 CancelPrompt,
1016 Resize { width: u16, height: u16 },
1018 FocusChanged { focused: bool },
1020 BufferChanged,
1025 CompletionRefresh,
1028 CompletionAccept,
1034 BackTab,
1037 Escape,
1039 Binding(String),
1041 Notice(String),
1043 ExternalEditor,
1047}
1048
1049#[derive(Default)]
1051struct ZoneRemoval {
1052 count: usize,
1054 first_index: Option<usize>,
1056}
1057
1058fn remove_all_from_zone(zone: &mut Vec<BlockId>, id: BlockId) -> ZoneRemoval {
1060 let mut removal = ZoneRemoval::default();
1061 let mut index = 0;
1062 zone.retain(|&candidate| {
1063 let current_index = index;
1064 index += 1;
1065 if candidate == id {
1066 removal.count += 1;
1067 removal.first_index.get_or_insert(current_index);
1068 false
1069 } else {
1070 true
1071 }
1072 });
1073 removal
1074}
1075
1076#[derive(Clone, Debug, Default)]
1078pub struct OutputSnapshot {
1079 blocks: HashMap<BlockId, StyledBlock>,
1080 block_debug_ids: HashMap<BlockId, String>,
1081 next_id: u64,
1083 history: Vec<BlockId>,
1084 above_active: Vec<BlockId>,
1085 above_sticky: Vec<BlockId>,
1086 suggestions: Vec<BlockId>,
1087 below: Vec<BlockId>,
1088}
1089
1090impl OutputSnapshot {
1091 pub fn block_count(&self) -> usize {
1093 self.blocks.len()
1094 }
1095
1096 pub fn suggestion_ids(&self) -> &[BlockId] {
1098 &self.suggestions
1099 }
1100
1101 pub fn new_block(
1103 &mut self,
1104 debug_id: impl Into<String>,
1105 block: impl Into<StyledBlock>,
1106 ) -> BlockId {
1107 let id = BlockId(self.next_id);
1108 self.next_id = self.next_id.saturating_add(1);
1109 self.blocks.insert(id, block.into());
1110 self.block_debug_ids.insert(id, debug_id.into());
1111 id
1112 }
1113
1114 pub fn set_block(&mut self, id: BlockId, block: impl Into<StyledBlock>) {
1116 self.blocks.insert(id, block.into());
1117 self.block_debug_ids
1118 .entry(id)
1119 .or_insert_with(|| format!("set-block-{}", id.0));
1120 }
1121
1122 pub fn remove_block(&mut self, id: BlockId) {
1124 self.blocks.remove(&id);
1125 self.block_debug_ids.remove(&id);
1126 remove_all_from_zone(&mut self.history, id);
1127 remove_all_from_zone(&mut self.above_active, id);
1128 remove_all_from_zone(&mut self.above_sticky, id);
1129 remove_all_from_zone(&mut self.suggestions, id);
1130 remove_all_from_zone(&mut self.below, id);
1131 }
1132
1133 pub fn push_history(&mut self, id: BlockId) {
1135 self.history.push(id);
1136 }
1137
1138 pub fn push_above_active(&mut self, id: BlockId) {
1140 if !self.above_active.contains(&id) {
1141 self.above_active.push(id);
1142 }
1143 }
1144
1145 pub fn push_above_active_before_any<I>(&mut self, id: BlockId, anchors: I)
1147 where
1148 I: IntoIterator<Item = BlockId>,
1149 {
1150 let anchors = anchors.into_iter().collect::<HashSet<_>>();
1151 self.above_active.retain(|active_id| *active_id != id);
1152 let insert_at = self
1153 .above_active
1154 .iter()
1155 .position(|active_id| anchors.contains(active_id))
1156 .unwrap_or(self.above_active.len());
1157 self.above_active.insert(insert_at, id);
1158 }
1159
1160 pub fn push_above_sticky(&mut self, id: BlockId) {
1162 if !self.above_sticky.contains(&id) {
1163 self.above_sticky.push(id);
1164 }
1165 }
1166
1167 pub fn remove_above_sticky(&mut self, id: BlockId) {
1169 self.above_sticky.retain(|block_id| *block_id != id);
1170 }
1171
1172 pub fn push_below(&mut self, id: BlockId) {
1174 if !self.below.contains(&id) {
1175 self.below.push(id);
1176 }
1177 }
1178
1179 pub fn print_output(
1181 &mut self,
1182 debug_id: impl Into<String>,
1183 block: impl Into<StyledBlock>,
1184 ) -> BlockId {
1185 let id = self.new_block(debug_id, block);
1186 self.push_history(id);
1187 id
1188 }
1189}
1190
1191#[derive(Clone)]
1196pub struct TermHandle {
1197 state: Arc<Mutex<SharedState>>,
1198 output_transaction: Arc<Mutex<()>>,
1199 sync_condvar: Arc<std::sync::Condvar>,
1200 redraw: tau_blocking_notify_channel::Sender,
1201 input_tx: path_std_sync::mpsc::Sender<InputMessage>,
1202 output_snapshot_count: Arc<path_std_sync::atomic::AtomicU64>,
1204 output_snapshot_take_count: Arc<path_std_sync::atomic::AtomicU64>,
1206 #[cfg(feature = "redraw-test-counter")]
1208 redraw_request_count: Arc<path_std_sync::atomic::AtomicU64>,
1209 #[cfg(test)]
1211 retirement_probe_count: Arc<path_std_sync_atomic::AtomicU64>,
1212}
1213
1214thread_local! {
1215 static HELD_OUTPUT_TRANSACTIONS: RefCell<HashMap<usize, usize>> = RefCell::new(HashMap::new());
1216 #[cfg(test)]
1217 static HELD_SHARED_STATES: RefCell<HashMap<usize, usize>> = RefCell::new(HashMap::new());
1218 static RETIRED_STYLED_BLOCKS: RefCell<HashMap<usize, Vec<RetiredStyledBlocks>>> =
1219 RefCell::new(HashMap::new());
1220}
1221
1222#[cfg(test)]
1224struct SharedStateGuard<'a> {
1225 guard: MutexGuard<'a, SharedState>,
1227 key: usize,
1229}
1230
1231#[cfg(test)]
1232impl std::ops::Deref for SharedStateGuard<'_> {
1233 type Target = SharedState;
1234
1235 fn deref(&self) -> &Self::Target {
1236 &self.guard
1237 }
1238}
1239
1240#[cfg(test)]
1241impl std::ops::DerefMut for SharedStateGuard<'_> {
1242 fn deref_mut(&mut self) -> &mut Self::Target {
1243 &mut self.guard
1244 }
1245}
1246
1247#[cfg(test)]
1248impl Drop for SharedStateGuard<'_> {
1249 fn drop(&mut self) {
1250 HELD_SHARED_STATES.with(|held| {
1251 let mut held = held.borrow_mut();
1252 let depth = held
1253 .get_mut(&self.key)
1254 .expect("shared terminal state lock depth missing");
1255 *depth -= 1;
1256 if *depth == 0 {
1257 held.remove(&self.key);
1258 }
1259 });
1260 }
1261}
1262
1263#[cfg(not(test))]
1265type SharedStateGuard<'a> = MutexGuard<'a, SharedState>;
1266
1267struct OutputTransactionDepthGuard {
1268 key: usize,
1269}
1270
1271impl Drop for OutputTransactionDepthGuard {
1272 fn drop(&mut self) {
1273 HELD_OUTPUT_TRANSACTIONS.with(|held| {
1274 let mut held = held.borrow_mut();
1275 let depth = held
1276 .get_mut(&self.key)
1277 .expect("output transaction depth missing");
1278 *depth -= 1;
1279 if *depth == 0 {
1280 held.remove(&self.key);
1281 }
1282 });
1283 }
1284}
1285
1286struct OutputTransactionGuard<'a> {
1288 guard: Option<MutexGuard<'a, ()>>,
1289 depth: Option<OutputTransactionDepthGuard>,
1290 key: usize,
1291 acquired_at: std::time::Instant,
1293 #[cfg(test)]
1294 state: Arc<Mutex<SharedState>>,
1295 #[cfg(test)]
1296 retirement_probe_count: Arc<path_std_sync_atomic::AtomicU64>,
1297}
1298
1299enum RetiredStyledBlocks {
1301 One(StyledBlock),
1303 Snapshot(HashMap<BlockId, StyledBlock>),
1305}
1306
1307impl Drop for OutputTransactionGuard<'_> {
1308 fn drop(&mut self) {
1309 let held = self.acquired_at.elapsed();
1310 if Duration::from_millis(500) <= held && admit_stall_warning() {
1311 tracing::warn!(
1312 target: "tau_cli_term_raw::frontend_progress",
1313 hold_ms = held.as_millis(),
1314 "terminal output transaction stalled"
1315 );
1316 }
1317
1318 let retired = RETIRED_STYLED_BLOCKS
1319 .with(|retired| retired.borrow_mut().remove(&self.key).unwrap_or_default());
1320 drop(self.depth.take());
1321 drop(self.guard.take());
1322
1323 for retired in retired {
1324 #[cfg(test)]
1325 {
1326 assert!(
1327 !HELD_OUTPUT_TRANSACTIONS.with(|held| held.borrow().contains_key(&self.key)),
1328 "styled block retirement must follow output transaction release"
1329 );
1330 assert!(
1331 !HELD_SHARED_STATES.with(|held| {
1332 held.borrow()
1333 .contains_key(&(Arc::as_ptr(&self.state) as usize))
1334 }),
1335 "styled block retirement must follow shared terminal state release"
1336 );
1337 self.retirement_probe_count
1338 .fetch_add(1, path_std_sync_atomic::Ordering::Relaxed);
1339 }
1340 match retired {
1341 RetiredStyledBlocks::One(block) => drop(block),
1342 RetiredStyledBlocks::Snapshot(blocks) => drop(blocks),
1343 }
1344 }
1345 }
1346}
1347
1348#[must_use = "redraw suppression ends immediately when the guard is dropped"]
1353pub struct RedrawSuppressionGuard {
1354 handle: TermHandle,
1355}
1356
1357impl RedrawSuppressionGuard {
1358 fn new(handle: &TermHandle) -> Self {
1359 {
1360 let mut st = handle.lock();
1361 st.terminal.redraw_suppression = st.terminal.redraw_suppression.saturating_add(1);
1362 }
1363 Self {
1364 handle: handle.clone(),
1365 }
1366 }
1367}
1368
1369impl Drop for RedrawSuppressionGuard {
1370 fn drop(&mut self) {
1371 let notify = {
1372 let mut st = self.handle.lock();
1373 st.terminal.redraw_suppression = st.terminal.redraw_suppression.saturating_sub(1);
1374 if st.terminal.redraw_suppression == 0 && st.terminal.redraw_dirty_while_suppressed {
1375 st.terminal.redraw_dirty_while_suppressed = false;
1376 true
1377 } else {
1378 false
1379 }
1380 };
1381 if notify {
1382 self.handle.release_redraw_notification();
1383 }
1384 }
1385}
1386
1387impl TermHandle {
1388 fn lock(&self) -> SharedStateGuard<'_> {
1389 let guard = self.state.lock().expect("term state mutex poisoned");
1390 #[cfg(test)]
1391 {
1392 let key = Arc::as_ptr(&self.state) as usize;
1393 HELD_SHARED_STATES.with(|held| {
1394 *held.borrow_mut().entry(key).or_insert(0) += 1;
1395 });
1396 SharedStateGuard { guard, key }
1397 }
1398 #[cfg(not(test))]
1399 {
1400 guard
1401 }
1402 }
1403
1404 fn output_transaction_key(&self) -> usize {
1405 Arc::as_ptr(&self.output_transaction) as usize
1406 }
1407
1408 fn output_transaction_is_held(&self) -> bool {
1409 let key = self.output_transaction_key();
1410 HELD_OUTPUT_TRANSACTIONS.with(|held| held.borrow().contains_key(&key))
1411 }
1412
1413 fn mark_output_transaction_held(&self) -> OutputTransactionDepthGuard {
1414 let key = self.output_transaction_key();
1415 HELD_OUTPUT_TRANSACTIONS.with(|held| {
1416 let mut held = held.borrow_mut();
1417 *held.entry(key).or_insert(0) += 1;
1418 });
1419 OutputTransactionDepthGuard { key }
1420 }
1421
1422 fn retire_styled_blocks(&self, retired: RetiredStyledBlocks) {
1425 let key = self.output_transaction_key();
1426 debug_assert!(self.output_transaction_is_held());
1428 RETIRED_STYLED_BLOCKS.with(|retirements| {
1429 retirements
1430 .borrow_mut()
1431 .entry(key)
1432 .or_default()
1433 .push(retired);
1434 });
1435 }
1436
1437 #[cfg(test)]
1439 fn retirement_probe_count(&self) -> u64 {
1440 self.retirement_probe_count
1441 .load(path_std_sync_atomic::Ordering::Relaxed)
1442 }
1443
1444 fn output_transaction_barrier(&self) -> Option<OutputTransactionGuard<'_>> {
1445 if self.output_transaction_is_held() {
1446 return None;
1447 }
1448 let waiting_at = path_std_time::Instant::now();
1449 tracing::trace!(
1450 target: "tau_cli_term_raw::frontend_progress",
1451 "terminal output transaction acquisition started"
1452 );
1453 let guard = self
1454 .output_transaction
1455 .lock()
1456 .expect("term output transaction mutex poisoned");
1457 let waited = waiting_at.elapsed();
1458 tracing::trace!(
1459 target: "tau_cli_term_raw::frontend_progress",
1460 wait_us = waited.as_micros(),
1461 "terminal output transaction acquired"
1462 );
1463 if Duration::from_millis(500) <= waited && admit_stall_warning() {
1464 tracing::warn!(
1465 target: "tau_cli_term_raw::frontend_progress",
1466 wait_ms = waited.as_millis(),
1467 "terminal output transaction acquisition stalled"
1468 );
1469 }
1470 let depth = self.mark_output_transaction_held();
1471 Some(OutputTransactionGuard {
1472 guard: Some(guard),
1473 depth: Some(depth),
1474 key: self.output_transaction_key(),
1475 acquired_at: path_std_time::Instant::now(),
1476 #[cfg(test)]
1477 state: Arc::clone(&self.state),
1478 #[cfg(test)]
1479 retirement_probe_count: Arc::clone(&self.retirement_probe_count),
1480 })
1481 }
1482
1483 fn request_redraw_locked(st: &mut SharedState) -> bool {
1484 if st.terminal.redraw_suppression == 0 {
1485 true
1486 } else {
1487 st.terminal.redraw_dirty_while_suppressed = true;
1488 false
1489 }
1490 }
1491
1492 fn notify_redraw(&self) {
1493 let notify = {
1494 let mut st = self.lock();
1495 Self::request_redraw_locked(&mut st)
1496 };
1497 if notify {
1498 self.release_redraw_notification();
1499 }
1500 }
1501
1502 fn release_redraw_notification(&self) {
1504 #[cfg(feature = "redraw-test-counter")]
1505 self.redraw_request_count
1506 .fetch_add(1, path_std_sync_atomic::Ordering::Relaxed);
1507 self.redraw.notify();
1508 }
1509
1510 pub fn request_input_shutdown(&self) {
1518 self.lock().terminal.input_shutdown = true;
1519 let _ = self.input_tx.send(InputMessage::Shutdown);
1520 }
1521
1522 pub fn request_completion_refresh(&self) {
1527 let _ = self.input_tx.send(InputMessage::RefreshCompletion);
1528 }
1529
1530 pub fn completion_refresh_generation(&self) -> u64 {
1533 self.lock().editor.completion_generation
1534 }
1535
1536 pub fn request_completion_refresh_if_generation(&self, generation: u64) {
1539 let _ = self
1540 .input_tx
1541 .send(InputMessage::RefreshCompletionIfGeneration(generation));
1542 }
1543
1544 pub fn with_redraw_suppressed<R>(&self, f: impl FnOnce() -> R) -> R {
1551 let _guard = RedrawSuppressionGuard::new(self);
1552 f()
1553 }
1554
1555 pub fn suppress_redraws(&self) -> RedrawSuppressionGuard {
1561 RedrawSuppressionGuard::new(self)
1562 }
1563
1564 pub fn with_output_transaction<R>(&self, f: impl FnOnce() -> R) -> R {
1572 let _transaction = self.output_transaction_barrier();
1573 f()
1574 }
1575
1576 pub fn redraw(&self) {
1590 self.notify_redraw();
1591 }
1592
1593 pub fn observe_presentation_mutation(
1606 &self,
1607 delivery_id: RendererDeliveryId,
1608 fact: OpaquePresentationFact,
1609 ) -> bool {
1610 self.observe_presentation_mutation_enabled(delivery_id, fact)
1611 }
1612
1613 fn observe_presentation_mutation_enabled(
1615 &self,
1616 delivery_id: RendererDeliveryId,
1617 fact: OpaquePresentationFact,
1618 ) -> bool {
1619 let observed_at = path_std_time::Instant::now();
1620 let (notify, capture_suppressed) = {
1621 let mut st = self.lock();
1622 st.presentation_observations
1623 .register(delivery_id, fact, observed_at);
1624 (
1625 Self::request_redraw_locked(&mut st),
1626 st.terminal.redraw_suppression != 0,
1627 )
1628 };
1629 if notify {
1630 self.release_redraw_notification();
1631 }
1632 capture_suppressed
1633 }
1634
1635 #[cfg(test)]
1638 fn observe_presentation_mutation_for_test(
1639 &self,
1640 delivery_id: RendererDeliveryId,
1641 fact: OpaquePresentationFact,
1642 ) -> bool {
1643 self.observe_presentation_mutation_enabled(delivery_id, fact)
1644 }
1645
1646 #[cfg(feature = "redraw-test-counter")]
1651 pub fn redraw_request_count(&self) -> u64 {
1652 self.redraw_request_count
1653 .load(path_std_sync_atomic::Ordering::Relaxed)
1654 }
1655
1656 pub fn clear_output(&self) {
1660 self.replace_output_snapshot(OutputSnapshot::default());
1661 }
1662
1663 pub fn output_snapshot(&self) -> OutputSnapshot {
1666 self.output_snapshot_count
1667 .fetch_add(1, path_std_sync_atomic::Ordering::Relaxed);
1668 let _transaction = self.output_transaction_barrier();
1669 let st = self.lock();
1670 OutputSnapshot {
1671 blocks: st.layout.blocks.clone(),
1672 block_debug_ids: st.layout.block_debug_ids.clone(),
1673 next_id: st.layout.next_id,
1674 history: st.layout.history.clone(),
1675 above_active: st.layout.above_active.clone(),
1676 above_sticky: st.layout.above_sticky.clone(),
1677 suggestions: st.layout.suggestions.clone(),
1678 below: st.layout.below.clone(),
1679 }
1680 }
1681
1682 pub fn output_snapshot_count(&self) -> u64 {
1688 self.output_snapshot_count
1689 .load(path_std_sync_atomic::Ordering::Relaxed)
1690 }
1691
1692 pub fn take_output_snapshot(&self) -> OutputSnapshot {
1699 self.output_snapshot_take_count
1700 .fetch_add(1, path_std_sync_atomic::Ordering::Relaxed);
1701 let _transaction = self.output_transaction_barrier();
1702 let mut st = self.lock();
1703 OutputSnapshot {
1704 blocks: std::mem::take(&mut st.layout.blocks),
1705 block_debug_ids: std::mem::take(&mut st.layout.block_debug_ids),
1706 next_id: st.layout.next_id,
1707 history: std::mem::take(&mut st.layout.history),
1708 above_active: std::mem::take(&mut st.layout.above_active),
1709 above_sticky: std::mem::take(&mut st.layout.above_sticky),
1710 suggestions: std::mem::take(&mut st.layout.suggestions),
1711 below: std::mem::take(&mut st.layout.below),
1712 }
1713 }
1714
1715 pub fn output_snapshot_take_count(&self) -> u64 {
1721 self.output_snapshot_take_count
1722 .load(path_std_sync_atomic::Ordering::Relaxed)
1723 }
1724
1725 pub fn replace_output_snapshot(&self, snapshot: OutputSnapshot) {
1727 self.replace_output_snapshot_inner(snapshot, true, true);
1728 }
1729
1730 pub fn replace_output_snapshot_quiet(&self, snapshot: OutputSnapshot) {
1734 self.replace_output_snapshot_inner(snapshot, false, false);
1735 }
1736
1737 fn replace_output_snapshot_inner(
1738 &self,
1739 snapshot: OutputSnapshot,
1740 invalidate_screen: bool,
1741 notify: bool,
1742 ) {
1743 let _transaction = self.output_transaction_barrier();
1744 let mut st = self.lock();
1745 let retired_blocks = std::mem::replace(&mut st.layout.blocks, snapshot.blocks);
1746 st.layout.block_debug_ids = snapshot.block_debug_ids;
1747 st.layout.next_id = st.layout.next_id.max(snapshot.next_id);
1748 st.layout.history = snapshot.history;
1749 st.rebuild_history_refs();
1750 st.layout.above_active = snapshot.above_active;
1751 st.layout.above_sticky = snapshot.above_sticky;
1752 st.layout.suggestions = snapshot.suggestions;
1753 st.layout.below = snapshot.below;
1754 if invalidate_screen {
1755 st.terminal.invalidate_screen = true;
1756 }
1757 let notify = notify && Self::request_redraw_locked(&mut st);
1758 drop(st);
1759 if !retired_blocks.is_empty() {
1760 self.retire_styled_blocks(RetiredStyledBlocks::Snapshot(retired_blocks));
1761 }
1762 if notify {
1763 self.release_redraw_notification();
1764 }
1765 }
1766
1767 pub fn invalidate_screen(&self) {
1781 let _transaction = self.output_transaction_barrier();
1782 self.lock().terminal.invalidate_screen = true;
1783 self.notify_redraw();
1784 }
1785
1786 pub fn size(&self) -> (usize, usize) {
1788 let st = self.lock();
1789 (st.terminal.width, st.terminal.height)
1790 }
1791
1792 pub fn height(&self) -> usize {
1794 self.lock().terminal.height
1795 }
1796
1797 pub fn full_render_count(&self) -> u64 {
1800 self.lock().terminal.full_render_count
1801 }
1802
1803 pub fn redraw_history_size(&self) -> usize {
1806 self.lock().terminal.redraw_history_size
1807 }
1808
1809 pub fn set_redraw_history_size(&self, redraw_history_size: usize) {
1813 self.lock().terminal.redraw_history_size = redraw_history_size;
1814 }
1815
1816 pub fn redraw_sync(&self) {
1825 let mut st = self.lock();
1826 if st.terminal.output_failure.is_some() {
1827 return;
1828 }
1829 st.terminal.sync_requested.advance();
1830 let target = st.terminal.sync_requested;
1831 drop(st);
1832
1833 self.redraw.notify();
1834
1835 let st = self.state.lock().expect("term state mutex poisoned");
1836 let _st = self
1837 .sync_condvar
1838 .wait_while(st, |s| s.terminal.sync_completed < target)
1839 .expect("term state mutex poisoned");
1840 }
1841
1842 pub fn new_block(&self, debug_id: impl Into<String>, block: impl Into<StyledBlock>) -> BlockId {
1846 let _transaction = self.output_transaction_barrier();
1847 let mut st = self.lock();
1848 let id = st.alloc_id();
1849 let debug_id = debug_id.into();
1850 let block = block.into();
1851 let content_empty = block.is_empty();
1852 let retired_block = st.layout.blocks.insert(id, block);
1853 st.layout.block_debug_ids.insert(id, debug_id.clone());
1854 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, debug_id, content_empty, "new block");
1855 drop(st);
1856 if let Some(retired_block) = retired_block {
1857 self.retire_styled_blocks(RetiredStyledBlocks::One(retired_block));
1858 }
1859 id
1860 }
1861
1862 pub fn set_block(&self, id: BlockId, block: impl Into<StyledBlock>) {
1865 self.set_block_inner(id, block, false);
1866 }
1867
1868 pub fn set_block_with_presentation_delta(
1871 &self,
1872 id: BlockId,
1873 block: impl Into<StyledBlock>,
1874 ) -> bool {
1875 self.set_block_inner(id, block, true)
1876 }
1877
1878 fn set_block_inner(
1880 &self,
1881 id: BlockId,
1882 block: impl Into<StyledBlock>,
1883 observe_delta: bool,
1884 ) -> bool {
1885 let _transaction = self.output_transaction_barrier();
1886 let block = block.into();
1887 let content_empty = block.is_empty();
1888 let mut st = self.lock();
1889 let affects_history = st.block_in_history(id);
1890 let changed = observe_delta && st.layout.blocks.get(&id) != Some(&block);
1891 let presentation_changed = changed && st.block_is_visible(id);
1892 let retired_block = st.layout.blocks.insert(id, block);
1893 st.layout
1894 .block_debug_ids
1895 .entry(id)
1896 .or_insert_with(|| format!("set-block-{}", id.0));
1897 if affects_history {
1898 st.mark_history_dirty_from(0);
1899 }
1900 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, content_empty, "set block");
1901 drop(st);
1902 if let Some(retired_block) = retired_block {
1903 self.retire_styled_blocks(RetiredStyledBlocks::One(retired_block));
1904 }
1905 presentation_changed
1906 }
1907
1908 pub fn remove_block(&self, id: BlockId) {
1911 self.remove_block_inner(id, false);
1912 }
1913
1914 pub fn remove_block_with_presentation_delta(&self, id: BlockId) -> bool {
1916 self.remove_block_inner(id, true)
1917 }
1918
1919 fn remove_block_inner(&self, id: BlockId, observe_delta: bool) -> bool {
1921 let _transaction = self.output_transaction_barrier();
1922 let mut st = self.lock();
1923 let (presentation_changed, retired_block) = st.remove_block(id, observe_delta);
1924 drop(st);
1925 if let Some(retired_block) = retired_block {
1926 self.retire_styled_blocks(RetiredStyledBlocks::One(retired_block));
1927 }
1928 presentation_changed
1929 }
1930
1931 pub fn push_history(&self, id: BlockId) {
1935 let _transaction = self.output_transaction_barrier();
1936 let mut st = self.lock();
1937 st.append_history(id);
1938 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "history", "push block zone");
1939 }
1940
1941 pub fn push_above_active(&self, id: BlockId) {
1944 self.push_above_active_inner(id);
1945 }
1946
1947 pub fn push_above_active_with_presentation_delta(&self, id: BlockId) -> bool {
1949 self.push_above_active_inner(id)
1950 }
1951
1952 fn push_above_active_inner(&self, id: BlockId) -> bool {
1954 let _transaction = self.output_transaction_barrier();
1955 let mut st = self.lock();
1956 if !st.layout.above_active.contains(&id) {
1957 st.layout.above_active.push(id);
1958 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "above_active", "push block zone");
1959 true
1960 } else {
1961 false
1962 }
1963 }
1964
1965 pub fn push_above_active_before_any<I>(&self, id: BlockId, anchors: I)
1972 where
1973 I: IntoIterator<Item = BlockId>,
1974 {
1975 self.push_above_active_before_any_inner(id, anchors, false);
1976 }
1977
1978 pub fn push_above_active_before_any_with_presentation_delta<I>(
1980 &self,
1981 id: BlockId,
1982 anchors: I,
1983 ) -> bool
1984 where
1985 I: IntoIterator<Item = BlockId>,
1986 {
1987 self.push_above_active_before_any_inner(id, anchors, true)
1988 }
1989
1990 fn push_above_active_before_any_inner<I>(
1992 &self,
1993 id: BlockId,
1994 anchors: I,
1995 observe_delta: bool,
1996 ) -> bool
1997 where
1998 I: IntoIterator<Item = BlockId>,
1999 {
2000 let _transaction = self.output_transaction_barrier();
2001 let anchors = anchors.into_iter().collect::<HashSet<_>>();
2002 let mut st = self.lock();
2003 let previous = observe_delta.then(|| st.layout.above_active.clone());
2004 st.layout.above_active.retain(|&x| x != id);
2005 let insert_at = st
2006 .layout
2007 .above_active
2008 .iter()
2009 .position(|active_id| anchors.contains(active_id))
2010 .unwrap_or(st.layout.above_active.len());
2011 st.layout.above_active.insert(insert_at, id);
2012 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "above_active", "insert block zone");
2013 previous.is_some_and(|previous| st.layout.above_active != previous)
2014 }
2015
2016 pub fn remove_above_active(&self, id: BlockId) {
2018 let _transaction = self.output_transaction_barrier();
2019 self.lock().layout.above_active.retain(|&x| x != id);
2020 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "above_active", "remove block zone");
2021 }
2022
2023 pub fn push_above_sticky(&self, id: BlockId) {
2026 let _transaction = self.output_transaction_barrier();
2027 let mut st = self.lock();
2028 if !st.layout.above_sticky.contains(&id) {
2029 st.layout.above_sticky.push(id);
2030 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "above_sticky", "push block zone");
2031 }
2032 }
2033
2034 pub fn remove_above_sticky(&self, id: BlockId) {
2036 let _transaction = self.output_transaction_barrier();
2037 self.lock().layout.above_sticky.retain(|&x| x != id);
2038 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "above_sticky", "remove block zone");
2039 }
2040
2041 pub fn push_suggestions(&self, id: BlockId) {
2044 let _transaction = self.output_transaction_barrier();
2045 let mut st = self.lock();
2046 if !st.layout.suggestions.contains(&id) {
2047 st.layout.suggestions.push(id);
2048 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "suggestions", "push block zone");
2049 }
2050 }
2051
2052 pub fn remove_suggestions(&self, id: BlockId) {
2054 let _transaction = self.output_transaction_barrier();
2055 self.lock().layout.suggestions.retain(|&x| x != id);
2056 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "suggestions", "remove block zone");
2057 }
2058
2059 pub fn push_below(&self, id: BlockId) {
2061 self.push_below_inner(id);
2062 }
2063
2064 pub fn push_below_with_presentation_delta(&self, id: BlockId) -> bool {
2066 self.push_below_inner(id)
2067 }
2068
2069 fn push_below_inner(&self, id: BlockId) -> bool {
2071 let _transaction = self.output_transaction_barrier();
2072 let mut st = self.lock();
2073 if !st.layout.below.contains(&id) {
2074 st.layout.below.push(id);
2075 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "below", "push block zone");
2076 true
2077 } else {
2078 false
2079 }
2080 }
2081
2082 pub fn remove_below(&self, id: BlockId) {
2084 let _transaction = self.output_transaction_barrier();
2085 self.lock().layout.below.retain(|&x| x != id);
2086 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, zone = "below", "remove block zone");
2087 }
2088
2089 pub fn print_output(
2094 &self,
2095 debug_id: impl Into<String>,
2096 block: impl Into<StyledBlock>,
2097 ) -> BlockId {
2098 let _transaction = self.output_transaction_barrier();
2099 let mut st = self.lock();
2100 let id = st.alloc_id();
2101 let debug_id = debug_id.into();
2102 let block = block.into();
2103 let content_empty = block.is_empty();
2104 let retired_block = st.layout.blocks.insert(id, block);
2105 st.layout.block_debug_ids.insert(id, debug_id.clone());
2106 st.append_history(id);
2107 tracing::trace!(target: "tau_cli_term_raw::blocks", ?id, debug_id, content_empty, zone = "history", "print output");
2108 let notify = Self::request_redraw_locked(&mut st);
2109 drop(st);
2110 if let Some(retired_block) = retired_block {
2111 self.retire_styled_blocks(RetiredStyledBlocks::One(retired_block));
2112 }
2113 if notify {
2114 self.release_redraw_notification();
2115 }
2116 id
2117 }
2118
2119 pub fn set_left_prompt(&self, text: impl Into<StyledText>) {
2121 let mut st = self.lock();
2122 st.editor.left_prompt = text.into();
2123 st.ensure_input_cursor_visible();
2124 }
2125
2126 pub fn get_buffer(&self) -> String {
2128 self.lock().editor.buffer.clone()
2129 }
2130
2131 pub fn get_cursor(&self) -> usize {
2133 self.lock().editor.cursor
2134 }
2135
2136 pub fn get_buffer_revision(&self) -> u64 {
2138 self.lock().editor.revision
2139 }
2140
2141 pub fn last_submitted_buffer_revision(&self) -> Option<u64> {
2144 self.lock().editor.last_submitted_revision
2145 }
2146
2147 pub fn set_buffer(&self, text: String, cursor: usize) {
2152 let mut st = self.lock();
2153 st.editor.revision = st.editor.revision.wrapping_add(1);
2154 st.advance_completion_generation();
2155 let new_cursor = clamp_cursor_to_grapheme_boundary(&text, cursor);
2156 st.editor.buffer = text;
2157 let abandoned_history_nav = st.editor.history_nav.take().is_some();
2158 st.editor.completion = None;
2159 st.editor.current_undo.clear();
2160 st.editor.current_redo.clear();
2161 st.write_cursor(new_cursor);
2162 if abandoned_history_nav {
2163 st.limit_input_history();
2164 }
2165 }
2166
2167 pub fn set_buffer_if_revision(
2170 &self,
2171 expected_revision: u64,
2172 text: String,
2173 cursor: usize,
2174 ) -> bool {
2175 let mut st = self.lock();
2176 if st.editor.revision != expected_revision {
2177 return false;
2178 }
2179 st.editor.revision = st.editor.revision.wrapping_add(1);
2180 st.advance_completion_generation();
2181 let new_cursor = clamp_cursor_to_grapheme_boundary(&text, cursor);
2182 st.editor.buffer = text;
2183 let abandoned_history_nav = st.editor.history_nav.take().is_some();
2184 st.editor.completion = None;
2185 st.editor.current_undo.clear();
2186 st.editor.current_redo.clear();
2187 st.write_cursor(new_cursor);
2188 if abandoned_history_nav {
2189 st.limit_input_history();
2190 }
2191 true
2192 }
2193
2194 pub fn recall_prompt_before_current(&self, text: String) {
2198 let mut st = self.lock();
2199 st.editor.revision = st.editor.revision.wrapping_add(1);
2200 st.advance_completion_generation();
2201 st.recall_prompt_before_current(text);
2202 }
2203
2204 pub fn set_buffer_preserving_undo(&self, text: String, cursor: usize) {
2212 let mut st = self.lock();
2213 st.editor.revision = st.editor.revision.wrapping_add(1);
2214 st.advance_completion_generation();
2215 let new_cursor = clamp_cursor_to_grapheme_boundary(&text, cursor);
2216 st.editor.buffer = text;
2217 let abandoned_history_nav = st.editor.history_nav.take().is_some();
2218 st.editor.completion = None;
2219 st.editor.current_redo.clear();
2220 st.write_cursor(new_cursor);
2221 if abandoned_history_nav {
2222 st.limit_input_history();
2223 }
2224 }
2225
2226 pub fn completion_state(&self) -> Option<CompletionView> {
2229 let st = self.lock();
2230 st.editor.completion.as_ref().map(|c| CompletionView {
2231 candidates: c.candidates.clone(),
2232 selected: c.selected,
2233 })
2234 }
2235
2236 pub fn set_right_prompt(&self, text: impl Into<StyledText>) {
2238 self.lock().editor.right_prompt = text.into();
2239 }
2240
2241 pub fn set_input_placeholder(&self, text: impl Into<StyledText>) {
2243 self.lock().editor.input_placeholder = text.into();
2244 }
2245
2246 pub fn set_prompt_scroll_indicator(&self, enabled: bool) {
2249 let mut st = self.lock();
2250 st.editor.show_prompt_scroll_indicator = enabled;
2251 st.ensure_input_cursor_visible();
2252 }
2253
2254 pub fn print_terminal_bell(&self) {
2258 self.queue_terminal_side_effect("\x07");
2259 }
2260
2261 pub fn print_osc1337_set_user_var(&self, name: &str, value: &str, in_tmux: bool) {
2272 if let Err(error) = validate_osc1337_name(name) {
2273 tracing::warn!(
2274 target: "tau_cli_term_raw::terminal_side_effect",
2275 name_len = name.len(),
2276 error,
2277 "skipping invalid OSC 1337 SetUserVar side effect"
2278 );
2279 return;
2280 }
2281 let encoded = {
2282 use base64::Engine as _;
2283 path_base64_engine::general_purpose::STANDARD.encode(value.as_bytes())
2284 };
2285 let sequence = if in_tmux {
2286 format!("\x1bPtmux;\x1b\x1b]1337;SetUserVar={name}={encoded}\x07\x1b\\")
2287 } else {
2288 format!("\x1b]1337;SetUserVar={name}={encoded}\x07")
2289 };
2290 self.queue_terminal_side_effect(sequence);
2291 }
2292
2293 fn queue_terminal_side_effect(&self, sequence: impl Into<String>) {
2294 let notify = {
2295 let mut st = self.lock();
2296 st.terminal.pending_raw.push(sequence.into());
2297 Self::request_redraw_locked(&mut st)
2298 };
2299 if notify {
2300 self.release_redraw_notification();
2301 }
2302 }
2303}
2304
2305fn validate_osc1337_name(name: &str) -> Result<(), &'static str> {
2306 if name.is_empty() {
2307 return Err("name must not be empty");
2308 }
2309 if name.len() > 128 {
2310 return Err("name must be at most 128 bytes");
2311 }
2312 if !name
2313 .bytes()
2314 .all(|b| (0x20..=0x7e).contains(&b) && b != b'=')
2315 {
2316 return Err("name must be printable ASCII without '='");
2317 }
2318 Ok(())
2319}
2320
2321pub enum RawEvent {
2323 Key(KeyEvent),
2325 Resize(u16, u16),
2327 FocusChanged {
2329 focused: bool,
2331 },
2332 Paste(String),
2336 CompletionRefresh,
2338 CompletionRefreshIfGeneration(u64),
2341}
2342
2343enum InputMessage {
2344 Raw(RawEvent),
2346 RealRaw(RawEvent),
2348 Shutdown,
2350 RefreshCompletion,
2352 RefreshCompletionIfGeneration(u64),
2354 RealError(io::Error),
2356}
2357
2358fn spawn_real_reader_if_needed(
2361 in_flight: &Arc<path_std_sync_atomic::AtomicBool>,
2362 tx: path_std_sync::mpsc::Sender<InputMessage>,
2363 read: impl FnOnce() -> io::Result<RawEvent> + Send + 'static,
2364) -> bool {
2365 if in_flight
2366 .compare_exchange(
2367 false,
2368 true,
2369 path_std_sync_atomic::Ordering::AcqRel,
2370 path_std_sync_atomic::Ordering::Acquire,
2371 )
2372 .is_err()
2373 {
2374 return false;
2375 }
2376 thread::spawn(move || {
2377 let message = match read() {
2378 Ok(raw) => InputMessage::RealRaw(raw),
2379 Err(error) => InputMessage::RealError(error),
2380 };
2381 let _ = tx.send(message);
2382 });
2383 true
2384}
2385
2386fn finish_real_reader(in_flight: &path_std_sync_atomic::AtomicBool) {
2388 in_flight.store(false, path_std_sync_atomic::Ordering::Release);
2389}
2390
2391#[derive(Clone, Debug)]
2394struct OutputFailure {
2395 kind: io::ErrorKind,
2397 message: String,
2399}
2400
2401impl OutputFailure {
2402 fn new(error: io::Error) -> Self {
2404 Self {
2405 kind: error.kind(),
2406 message: error.to_string(),
2407 }
2408 }
2409
2410 fn io_error(&self) -> io::Error {
2412 io::Error::new(self.kind, self.clone())
2413 }
2414}
2415
2416impl std::fmt::Display for OutputFailure {
2417 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2418 write!(formatter, "terminal output failed: {}", self.message)
2419 }
2420}
2421
2422impl std::error::Error for OutputFailure {}
2423
2424#[must_use]
2426pub fn is_output_failure(error: &io::Error) -> bool {
2427 error
2428 .get_ref()
2429 .is_some_and(|source| source.is::<OutputFailure>())
2430}
2431
2432pub struct Term {
2445 handle: TermHandle,
2449 input_rx: path_std_sync::mpsc::Receiver<InputMessage>,
2451 real_read_in_flight: Arc<path_std_sync_atomic::AtomicBool>,
2453 redraw_thread: Option<JoinHandle<()>>,
2455 owns_raw_mode: bool,
2457 terminal_options: TerminalOptions,
2459 completion_source: Option<Box<dyn CompletionSource>>,
2462 bindings: HashMap<KeyBinding, String>,
2464 defer_submitted_input_history_limit: bool,
2467}
2468
2469impl std::ops::Deref for Term {
2470 type Target = TermHandle;
2471 fn deref(&self) -> &TermHandle {
2472 &self.handle
2473 }
2474}
2475
2476impl Term {
2477 pub fn new(
2488 left_prompt: impl Into<StyledText>,
2489 terminal_options: TerminalOptions,
2490 ) -> io::Result<(Self, TermHandle)> {
2491 let (width, height) = term_size();
2492 let state = Arc::new(Mutex::new(SharedState::new(
2493 width,
2494 height,
2495 left_prompt.into(),
2496 )));
2497
2498 let (redraw_tx, redraw_rx) = tau_blocking_notify_channel::channel();
2499 let sync_condvar = Arc::new(path_std_sync::Condvar::new());
2500 let (input_tx, input_rx) = path_std_sync::mpsc::channel();
2501
2502 terminal::enable_raw_mode()?;
2503 if let Err(error) = initialize_terminal_features(
2516 &mut io::stdout(),
2517 terminal_options.cursor_shape,
2518 terminal_options,
2519 ) {
2520 let _ = terminal::disable_raw_mode();
2521 return Err(error);
2522 }
2523
2524 let redraw_state = Arc::clone(&state);
2525 let redraw_writer: Box<dyn Write + Send> = Box::new(io::stdout());
2526 let redraw_sync_cv = Arc::clone(&sync_condvar);
2527 let redraw_input_tx = input_tx.clone();
2528 let redraw_thread = thread::spawn(move || {
2529 redraw_loop(
2530 redraw_state,
2531 redraw_rx,
2532 redraw_writer,
2533 redraw_input_tx,
2534 &redraw_sync_cv,
2535 );
2536 });
2537
2538 let handle = TermHandle {
2539 state,
2540 output_transaction: Arc::new(Mutex::new(())),
2541 sync_condvar,
2542 redraw: redraw_tx,
2543 input_tx,
2544 output_snapshot_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2545 output_snapshot_take_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2546 #[cfg(feature = "redraw-test-counter")]
2547 redraw_request_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2548 #[cfg(test)]
2549 retirement_probe_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2550 };
2551
2552 handle.release_redraw_notification();
2553
2554 Ok((
2555 Self {
2556 handle: handle.clone(),
2557 input_rx,
2558 real_read_in_flight: Arc::new(path_std_sync_atomic::AtomicBool::new(false)),
2559 redraw_thread: Some(redraw_thread),
2560 owns_raw_mode: true,
2561 terminal_options,
2562 completion_source: None,
2563 bindings: HashMap::new(),
2564 defer_submitted_input_history_limit: false,
2565 },
2566 handle,
2567 ))
2568 }
2569
2570 pub fn new_virtual(
2577 width: usize,
2578 height: usize,
2579 left_prompt: impl Into<StyledText>,
2580 output: Box<dyn Write + Send>,
2581 cursor_shape: CursorShape,
2582 ) -> (Self, TermHandle, path_std_sync::mpsc::Sender<RawEvent>) {
2583 let state = Arc::new(Mutex::new(SharedState::new(
2584 width,
2585 height,
2586 left_prompt.into(),
2587 )));
2588
2589 let (redraw_tx, redraw_rx) = tau_blocking_notify_channel::channel();
2590 let sync_condvar = Arc::new(path_std_sync::Condvar::new());
2591 let (input_tx, input_rx) = path_std_sync::mpsc::channel();
2592
2593 let redraw_state = Arc::clone(&state);
2594 let redraw_sync_cv = Arc::clone(&sync_condvar);
2595 let redraw_input_tx = input_tx.clone();
2596 let redraw_thread = thread::spawn(move || {
2597 redraw_loop(
2598 redraw_state,
2599 redraw_rx,
2600 output,
2601 redraw_input_tx,
2602 &redraw_sync_cv,
2603 );
2604 });
2605
2606 let (term_input_tx, term_input_rx) = path_std_sync::mpsc::channel();
2607 let virtual_input_tx = input_tx.clone();
2608 thread::spawn(move || {
2609 while let Ok(raw) = term_input_rx.recv() {
2610 if virtual_input_tx.send(InputMessage::Raw(raw)).is_err() {
2611 break;
2612 }
2613 }
2614 let _ = virtual_input_tx.send(InputMessage::Shutdown);
2615 });
2616
2617 let handle = TermHandle {
2618 state,
2619 output_transaction: Arc::new(Mutex::new(())),
2620 sync_condvar,
2621 redraw: redraw_tx,
2622 input_tx,
2623 output_snapshot_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2624 output_snapshot_take_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2625 #[cfg(feature = "redraw-test-counter")]
2626 redraw_request_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2627 #[cfg(test)]
2628 retirement_probe_count: Arc::new(path_std_sync_atomic::AtomicU64::new(0)),
2629 };
2630
2631 handle.release_redraw_notification();
2632
2633 let term = Self {
2634 handle: handle.clone(),
2635 input_rx,
2636 real_read_in_flight: Arc::new(path_std_sync_atomic::AtomicBool::new(false)),
2637 redraw_thread: Some(redraw_thread),
2638 owns_raw_mode: false,
2639 terminal_options: TerminalOptions {
2640 cursor_shape,
2641 ..TerminalOptions::default()
2642 },
2643 completion_source: None,
2644 bindings: HashMap::new(),
2645 defer_submitted_input_history_limit: false,
2646 };
2647
2648 (term, handle, term_input_tx)
2649 }
2650
2651 pub fn handle(&self) -> &TermHandle {
2655 &self.handle
2656 }
2657
2658 pub fn defer_submitted_input_history_limit(&mut self) {
2661 self.defer_submitted_input_history_limit = true;
2662 }
2663
2664 #[cfg(feature = "history-retention-test-support")]
2666 #[doc(hidden)]
2667 pub fn set_input_history_max_bytes_for_test(&mut self, max_bytes: usize) {
2668 self.handle.lock().input_history_limit_override = Some(InputHistoryLimits {
2669 max_entries: INPUT_HISTORY_MAX_ENTRIES,
2670 max_bytes,
2671 });
2672 }
2673
2674 pub fn finalize_submitted_input_history(&mut self) {
2677 let mut st = self.handle.lock();
2678 st.limit_input_history();
2679 st.editor.last_submitted_input_retained = st.editor.input_history.last().is_some();
2680 }
2681
2682 pub fn get_next_event(&self) -> io::Result<Event> {
2695 loop {
2696 let raw = match self.next_raw()? {
2697 Some(ev) => ev,
2698 None => return Ok(Event::Eof),
2699 };
2700
2701 match raw {
2702 RawEvent::Key(key) => {
2703 {
2704 let mut st = self.handle.lock();
2705 st.editor.revision = st.editor.revision.wrapping_add(1);
2706 st.advance_completion_generation();
2707 }
2708 if let Some(event) = self.handle_key(key)? {
2709 self.handle.redraw();
2710 return Ok(event);
2711 }
2712 self.handle.redraw();
2713 }
2714 RawEvent::Resize(w, h) => {
2715 let (width, height) = {
2716 let mut st = self.handle.lock();
2717 let width = effective_resize_dimension(w, st.terminal.width);
2718 let height = effective_resize_dimension(h, st.terminal.height);
2719 st.terminal.width = width;
2720 st.terminal.height = height;
2721 st.ensure_input_cursor_visible();
2722 (width, height)
2723 };
2724 self.handle.redraw();
2725 return Ok(Event::Resize {
2726 width: size_event_dimension(width),
2727 height: size_event_dimension(height),
2728 });
2729 }
2730 RawEvent::FocusChanged { focused } => {
2731 return Ok(Event::FocusChanged { focused });
2732 }
2733 RawEvent::Paste(text) => {
2734 if text.is_empty() {
2740 self.handle.redraw();
2741 continue;
2742 }
2743 let text = normalize_paste_text(text);
2744 {
2745 let mut st = self.handle.lock();
2746 st.editor.revision = st.editor.revision.wrapping_add(1);
2747 st.advance_completion_generation();
2748 st.record_undo();
2749 let cursor = st.editor.cursor;
2750 st.editor.buffer.insert_str(cursor, &text);
2751 st.write_cursor(cursor + text.len());
2752 st.sync_buffer_to_history_nav();
2753 }
2754 self.refresh_completion();
2755 self.handle.redraw();
2756 return Ok(Event::BufferChanged);
2757 }
2758 RawEvent::CompletionRefresh => {
2759 self.refresh_completion();
2760 self.handle.redraw();
2761 return Ok(Event::CompletionRefresh);
2762 }
2763 RawEvent::CompletionRefreshIfGeneration(generation) => {
2764 let eligible = {
2765 let st = self.handle.lock();
2766 st.editor.completion_generation == generation
2767 && st
2768 .editor
2769 .completion
2770 .as_ref()
2771 .is_none_or(|menu| menu.selected.is_none())
2772 };
2773 if !eligible {
2774 continue;
2775 }
2776 self.refresh_completion();
2777 self.handle.redraw();
2778 return Ok(Event::CompletionRefresh);
2779 }
2780 }
2781 }
2782 }
2783
2784 fn next_raw(&self) -> io::Result<Option<RawEvent>> {
2793 {
2794 let st = self.handle.lock();
2795 if let Some(error) = &st.terminal.output_failure {
2796 return Err(error.io_error());
2797 }
2798 if st.terminal.input_shutdown {
2799 return Ok(None);
2800 }
2801 }
2802
2803 if self.owns_raw_mode {
2804 spawn_real_reader_if_needed(
2805 &self.real_read_in_flight,
2806 self.handle.input_tx.clone(),
2807 || read_real_raw_event(event::read, raw_term_size),
2808 );
2809 }
2810
2811 let message = match self.input_rx.recv() {
2812 Ok(message) => message,
2813 Err(_) => return Ok(None),
2814 };
2815 {
2816 let st = self.handle.lock();
2817 if let Some(error) = &st.terminal.output_failure {
2818 return Err(error.io_error());
2819 }
2820 if st.terminal.input_shutdown {
2821 return Ok(None);
2822 }
2823 }
2824 match message {
2825 InputMessage::Raw(raw) => Ok(Some(raw)),
2826 InputMessage::RealRaw(raw) => {
2827 finish_real_reader(&self.real_read_in_flight);
2828 Ok(Some(raw))
2829 }
2830 InputMessage::Shutdown => {
2831 self.handle.lock().terminal.input_shutdown = true;
2832 Ok(None)
2833 }
2834 InputMessage::RefreshCompletion => Ok(Some(RawEvent::CompletionRefresh)),
2835 InputMessage::RefreshCompletionIfGeneration(generation) => {
2836 Ok(Some(RawEvent::CompletionRefreshIfGeneration(generation)))
2837 }
2838 InputMessage::RealError(error) => {
2839 finish_real_reader(&self.real_read_in_flight);
2840 Err(error)
2841 }
2842 }
2843 }
2844
2845 pub fn set_completion_source(&mut self, source: Option<Box<dyn CompletionSource>>) {
2848 self.completion_source = source;
2849 let mut st = self.handle.lock();
2850 st.editor.completion = None;
2851 }
2852
2853 pub fn set_bindings(&mut self, bindings: impl IntoIterator<Item = (String, String)>) {
2862 self.bindings = bindings
2863 .into_iter()
2864 .filter_map(|(raw_key, action)| {
2865 let parsed = parse_key_binding(&raw_key);
2866 tracing::trace!(
2867 target: "tau_cli_term_raw::input",
2868 raw_key,
2869 ?parsed,
2870 action,
2871 "configured prompt binding"
2872 );
2873 parsed.map(|key| (key, action))
2874 })
2875 .collect();
2876 }
2877
2878 pub fn seed_input_history(&mut self, history: impl IntoIterator<Item = String>) {
2883 let mut st = self.handle.lock();
2884 st.editor.input_history.extend(
2885 history
2886 .into_iter()
2887 .filter(|buffer| !buffer.is_empty())
2888 .map(PromptDraft::submitted),
2889 );
2890 st.limit_input_history();
2891 st.editor.history_nav = None;
2892 st.editor.last_submitted_input_retained = false;
2893 }
2894
2895 pub fn replace_last_submitted_input(&mut self, text: String) {
2901 let mut st = self.handle.lock();
2902 let recalled_source = st.editor.last_submitted_recalled_source;
2903 if let Some(index) = recalled_source
2904 && let Some(source) = st.editor.input_history.get_mut(index)
2905 {
2906 *source = PromptDraft::submitted(text.clone());
2907 }
2908 if st.editor.last_submitted_input_retained {
2909 if let Some(last) = st.editor.input_history.last_mut() {
2910 *last = PromptDraft::submitted(text.clone());
2911 }
2912 } else if !text.is_empty() {
2913 st.editor
2914 .input_history
2915 .push(PromptDraft::submitted(text.clone()));
2916 }
2917 if !self.defer_submitted_input_history_limit {
2918 st.limit_input_history();
2919 }
2920 st.editor.last_submitted_input_retained = st
2921 .editor
2922 .input_history
2923 .last()
2924 .is_some_and(|draft| draft.buffer == text);
2925 st.editor.history_nav = None;
2926 st.editor.completion = None;
2927 }
2928
2929 fn refresh_completion(&self) {
2936 let Some(source) = self.completion_source.as_deref() else {
2937 return;
2938 };
2939 let (buffer, cursor) = {
2940 let st = self.handle.lock();
2941 (st.editor.buffer.clone(), st.editor.cursor)
2942 };
2943 let candidates = source
2944 .candidates(&buffer, cursor)
2945 .into_iter()
2946 .filter(|candidate| {
2947 candidate.cursor
2948 == clamp_cursor_to_grapheme_boundary(&candidate.replacement, candidate.cursor)
2949 && candidate.acceptance.as_ref().is_none_or(|acceptance| {
2950 acceptance.cursor
2951 == clamp_cursor_to_grapheme_boundary(
2952 &acceptance.replacement,
2953 acceptance.cursor,
2954 )
2955 })
2956 })
2957 .collect::<Vec<_>>();
2958 let mut st = self.handle.lock();
2959 if candidates.is_empty() {
2960 st.editor.completion = None;
2961 } else {
2962 st.editor.completion = Some(CompletionMenu {
2963 candidates,
2964 selected: None,
2965 original_buffer: buffer,
2966 original_cursor: cursor,
2967 });
2968 }
2969 }
2970
2971 pub fn pause_for_external(&self) -> io::Result<()> {
2988 if !self.owns_raw_mode {
2989 return Ok(());
2990 }
2991 self.pause_for_external_with_release(|| {
2992 let mut stdout = io::stdout();
2993 write_external_pause_features(&mut stdout, self.terminal_options)?;
2994 terminal::disable_raw_mode()?;
2995 crossterm::execute!(
2996 io::stdout(),
2997 crossterm::style::ResetColor,
2998 crossterm::cursor::MoveTo(0, 0),
2999 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
3000 )?;
3001 Ok(())
3002 })
3003 }
3004
3005 fn pause_for_external_with_release(
3006 &self,
3007 release_terminal: impl FnOnce() -> io::Result<()>,
3008 ) -> io::Result<()> {
3009 {
3010 let mut st = self.handle.lock();
3011 st.terminal.external_paused = true;
3012 }
3013 self.handle.redraw_sync();
3017
3018 if let Err(error) = release_terminal() {
3019 let _ = self.resume_after_external();
3020 return Err(error);
3021 }
3022 Ok(())
3023 }
3024
3025 pub fn resume_after_external(&self) -> io::Result<()> {
3037 if !self.owns_raw_mode {
3038 self.finish_external_resume();
3039 return Ok(());
3040 }
3041 let result = (|| -> io::Result<()> {
3042 terminal::enable_raw_mode()?;
3043 let mut stdout = io::stdout();
3044 write_external_resume_features(
3045 &mut stdout,
3046 self.terminal_options.cursor_shape,
3047 self.terminal_options,
3048 )?;
3049 crossterm::execute!(
3050 io::stdout(),
3051 crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
3052 crossterm::cursor::MoveTo(0, 0)
3053 )?;
3054 Ok(())
3055 })();
3056 self.finish_external_resume();
3057 result
3058 }
3059
3060 fn finish_external_resume(&self) {
3061 let (width, height) = term_size();
3062 {
3063 let mut st = self.handle.lock();
3064 st.terminal.width = width;
3065 st.terminal.height = height;
3066 st.ensure_input_cursor_visible();
3067 st.terminal.external_paused = false;
3068 st.terminal.invalidate_screen = true;
3069 }
3070 self.handle.redraw();
3071 }
3072
3073 pub fn record_prompt_undo(&self) {
3080 let mut st = self.handle.lock();
3081 st.record_undo();
3082 }
3083
3084 pub fn trigger_insert_newline(&self) -> Event {
3089 self.insert_newline()
3090 }
3091
3092 pub fn trigger_submit_or_accept_completion(&self) -> Event {
3098 self.submit_or_accept_completion()
3099 }
3100
3101 pub fn dismiss_completion_menu(&self) -> bool {
3107 let mut st = self.handle.lock();
3108 st.dismiss_completion()
3109 }
3110
3111 pub fn trigger_history_step(&self, delta: isize) {
3116 let mut st = self.handle.lock();
3117 st.editor.completion = None;
3118 st.step_history(delta);
3119 }
3120
3121 pub fn trigger_undo(&self) -> bool {
3123 let mut st = self.handle.lock();
3124 st.editor.completion = None;
3125 st.undo()
3126 }
3127
3128 pub fn trigger_redo(&self) -> bool {
3130 let mut st = self.handle.lock();
3131 st.editor.completion = None;
3132 st.redo()
3133 }
3134
3135 fn step_history_event(&self, delta: isize) -> io::Result<Option<Event>> {
3136 self.trigger_history_step(delta);
3137 Ok(Some(Event::BufferChanged))
3138 }
3139
3140 fn binding_action(&self, binding: &Option<KeyBinding>) -> Option<String> {
3141 binding
3142 .as_ref()
3143 .and_then(|key| self.bindings.get(key))
3144 .cloned()
3145 }
3146
3147 fn handle_completion_key(
3151 &self,
3152 key: KeyEvent,
3153 ctrl: bool,
3154 shift: bool,
3155 alt: bool,
3156 ) -> Option<Event> {
3157 match key.code {
3158 KeyCode::Tab => {
3159 let mut st = self.handle.lock();
3160 st.cycle_completion(1).then_some(Event::BufferChanged)
3161 }
3162 KeyCode::BackTab | KeyCode::Up => {
3163 let mut st = self.handle.lock();
3164 st.cycle_completion(-1).then_some(Event::BufferChanged)
3165 }
3166 KeyCode::Down => {
3167 let mut st = self.handle.lock();
3168 st.cycle_completion(1).then_some(Event::BufferChanged)
3169 }
3170 KeyCode::Esc => {
3171 let mut st = self.handle.lock();
3172 st.dismiss_completion().then_some(Event::BufferChanged)
3173 }
3174 KeyCode::Enter if ctrl || (!shift && !alt) => self.accept_completion_event(),
3175 _ => None,
3176 }
3177 }
3178
3179 fn move_cursor_left(&self) -> bool {
3180 let mut st = self.handle.lock();
3181 if st.editor.cursor == 0 {
3182 return false;
3183 }
3184 let prev = prev_char_boundary(&st.editor.buffer, st.editor.cursor);
3185 st.write_cursor(prev);
3186 true
3187 }
3188
3189 fn move_cursor_right(&self) -> bool {
3190 let mut st = self.handle.lock();
3191 if st.editor.buffer.len() <= st.editor.cursor {
3192 return false;
3193 }
3194 let next = next_char_boundary(&st.editor.buffer, st.editor.cursor);
3195 st.write_cursor(next);
3196 true
3197 }
3198
3199 fn move_cursor_start(&self) -> bool {
3200 let mut st = self.handle.lock();
3201 if st.editor.cursor == 0 {
3202 return false;
3203 }
3204 st.write_cursor(0);
3205 true
3206 }
3207
3208 fn move_cursor_end(&self) -> bool {
3209 let mut st = self.handle.lock();
3210 let len = st.editor.buffer.len();
3211 if st.editor.cursor == len {
3212 return false;
3213 }
3214 st.write_cursor(len);
3215 true
3216 }
3217
3218 fn delete_backward(&self) -> bool {
3219 let changed = {
3220 let mut st = self.handle.lock();
3221 if st.editor.cursor == 0 {
3222 return false;
3223 }
3224 st.record_undo();
3225 let prev = prev_char_boundary(&st.editor.buffer, st.editor.cursor);
3226 let cursor = st.editor.cursor;
3227 st.editor.buffer.drain(prev..cursor);
3228 st.write_cursor(prev);
3229 st.sync_buffer_to_history_nav();
3230 true
3231 };
3232 self.refresh_completion();
3233 changed
3234 }
3235
3236 fn delete_forward(&self) -> bool {
3237 let changed = {
3238 let mut st = self.handle.lock();
3239 if st.editor.buffer.len() <= st.editor.cursor {
3240 return false;
3241 }
3242 st.record_undo();
3243 let cursor = st.editor.cursor;
3244 let next = next_char_boundary(&st.editor.buffer, cursor);
3245 st.editor.buffer.drain(cursor..next);
3246 st.write_cursor(cursor);
3247 st.sync_buffer_to_history_nav();
3248 true
3249 };
3250 self.refresh_completion();
3251 changed
3252 }
3253
3254 fn clear_prompt(&self) -> bool {
3255 let changed = {
3256 let mut st = self.handle.lock();
3257 if st.editor.buffer.is_empty() {
3258 return false;
3259 }
3260 st.editor.ctrl_c_cancel_armed = false;
3261 st.record_undo();
3262 st.editor.buffer.clear();
3263 let abandoned_history_nav = st.editor.history_nav.take().is_some();
3264 st.editor.completion = None;
3265 st.write_cursor(0);
3266 if abandoned_history_nav {
3267 st.limit_input_history();
3268 }
3269 true
3270 };
3271 self.refresh_completion();
3272 changed
3273 }
3274
3275 fn clear_or_cancel_prompt(&self) -> Event {
3276 let mut st = self.handle.lock();
3277 if st.editor.buffer.is_empty() {
3278 if st.editor.ctrl_c_cancel_armed {
3279 st.editor.ctrl_c_cancel_armed = false;
3280 return Event::CancelPrompt;
3281 }
3282 st.editor.ctrl_c_cancel_armed = true;
3283 return Event::Notice(
3284 "Press Ctrl-C again to cancel the current response; use Ctrl-D to exit".to_owned(),
3285 );
3286 }
3287 st.editor.ctrl_c_cancel_armed = false;
3288 st.record_undo();
3289 st.editor.buffer.clear();
3290 let abandoned_history_nav = st.editor.history_nav.take().is_some();
3291 st.editor.completion = None;
3292 st.write_cursor(0);
3293 if abandoned_history_nav {
3294 st.limit_input_history();
3295 }
3296 drop(st);
3297 self.refresh_completion();
3298 Event::BufferChanged
3299 }
3300
3301 fn kill_to_start(&self) -> bool {
3302 let changed = {
3303 let mut st = self.handle.lock();
3304 if st.editor.cursor == 0 {
3305 return false;
3306 }
3307 st.record_undo();
3308 let cursor = st.editor.cursor;
3309 st.editor.buffer.drain(..cursor);
3310 st.write_cursor(0);
3311 st.sync_buffer_to_history_nav();
3312 true
3313 };
3314 self.refresh_completion();
3315 changed
3316 }
3317
3318 fn kill_word_left(&self) -> bool {
3319 let changed = {
3320 let mut st = self.handle.lock();
3321 if st.editor.cursor == 0 {
3322 return false;
3323 }
3324 let new_end = word_left_boundary(&st.editor.buffer, st.editor.cursor);
3325 st.record_undo();
3326 let cursor = st.editor.cursor;
3327 st.editor.buffer.drain(new_end..cursor);
3328 st.write_cursor(new_end);
3329 st.sync_buffer_to_history_nav();
3330 true
3331 };
3332 self.refresh_completion();
3333 changed
3334 }
3335
3336 fn move_cursor_vertical_event(&self, delta: isize) -> Option<Event> {
3337 let mut st = self.handle.lock();
3338 let target_col = st.vertical_target_col();
3339 if let Some(new_cursor) = move_cursor_vertical(&st, delta, target_col) {
3340 st.write_cursor_keep_sticky(new_cursor);
3341 return Some(Event::BufferChanged);
3342 }
3343 None
3344 }
3345
3346 fn cycle_or_move_up(&self) -> Option<Event> {
3347 let mut st = self.handle.lock();
3348 if st.cycle_completion(-1) {
3349 return Some(Event::BufferChanged);
3350 }
3351 let target_col = st.vertical_target_col();
3352 if let Some(new_cursor) = move_cursor_vertical(&st, -1, target_col) {
3353 st.write_cursor_keep_sticky(new_cursor);
3354 return Some(Event::BufferChanged);
3355 }
3356 if st.step_history(-1) {
3357 return Some(Event::BufferChanged);
3358 }
3359 None
3360 }
3361
3362 fn cycle_or_move_down(&self) -> Option<Event> {
3363 let mut st = self.handle.lock();
3364 if st.cycle_completion(1) {
3365 return Some(Event::BufferChanged);
3366 }
3367 let target_col = st.vertical_target_col();
3368 if let Some(new_cursor) = move_cursor_vertical(&st, 1, target_col) {
3369 st.write_cursor_keep_sticky(new_cursor);
3370 return Some(Event::BufferChanged);
3371 }
3372 if st.step_history(1) {
3373 return Some(Event::BufferChanged);
3374 }
3375 None
3376 }
3377
3378 fn cycle_completion_event(&self, delta: isize) -> Option<Event> {
3379 let mut st = self.handle.lock();
3380 st.cycle_completion(delta).then_some(Event::BufferChanged)
3381 }
3382
3383 fn dismiss_completion_event(&self) -> Option<Event> {
3384 let mut st = self.handle.lock();
3385 st.dismiss_completion().then_some(Event::BufferChanged)
3386 }
3387
3388 fn accept_completion_event(&self) -> Option<Event> {
3389 let accepted = {
3390 let mut st = self.handle.lock();
3391 st.accept_completion()
3392 };
3393 if !accepted {
3394 return None;
3395 }
3396 self.refresh_completion();
3397 Some(Event::CompletionAccept)
3398 }
3399
3400 pub fn is_named_action(action: &str) -> bool {
3402 named_action_handler(action).is_some()
3403 }
3404
3405 pub fn trigger_named_action(&self, action: &str) -> Option<Event> {
3410 named_action_handler(action).and_then(|handler| handler(self))
3411 }
3412
3413 fn backtab_action(&self) -> Option<Event> {
3414 Some(Event::BackTab)
3415 }
3416
3417 fn clear_prompt_action(&self) -> Option<Event> {
3418 self.clear_prompt().then_some(Event::BufferChanged)
3419 }
3420
3421 fn clear_or_cancel_prompt_action(&self) -> Option<Event> {
3422 Some(self.clear_or_cancel_prompt())
3423 }
3424
3425 fn move_cursor_end_action(&self) -> Option<Event> {
3426 self.move_cursor_end().then_some(Event::BufferChanged)
3427 }
3428
3429 fn move_cursor_left_action(&self) -> Option<Event> {
3430 self.move_cursor_left().then_some(Event::BufferChanged)
3431 }
3432
3433 fn move_cursor_right_action(&self) -> Option<Event> {
3434 self.move_cursor_right().then_some(Event::BufferChanged)
3435 }
3436
3437 fn move_cursor_start_action(&self) -> Option<Event> {
3438 self.move_cursor_start().then_some(Event::BufferChanged)
3439 }
3440
3441 fn delete_backward_action(&self) -> Option<Event> {
3442 self.delete_backward().then_some(Event::BufferChanged)
3443 }
3444
3445 fn delete_forward_action(&self) -> Option<Event> {
3446 self.delete_forward().then_some(Event::BufferChanged)
3447 }
3448
3449 fn escape_action(&self) -> Option<Event> {
3450 Some(Event::Escape)
3451 }
3452
3453 fn kill_to_start_action(&self) -> Option<Event> {
3454 self.kill_to_start().then_some(Event::BufferChanged)
3455 }
3456
3457 fn kill_word_left_action(&self) -> Option<Event> {
3458 self.kill_word_left().then_some(Event::BufferChanged)
3459 }
3460
3461 fn move_cursor_down_action(&self) -> Option<Event> {
3462 self.move_cursor_vertical_event(1)
3463 }
3464
3465 fn move_cursor_up_action(&self) -> Option<Event> {
3466 self.move_cursor_vertical_event(-1)
3467 }
3468
3469 fn prompt_eof_action(&self) -> Option<Event> {
3470 let is_empty = self.handle.lock().editor.buffer.is_empty();
3471 is_empty.then_some(Event::Eof)
3472 }
3473
3474 fn select_completion_next_action(&self) -> Option<Event> {
3475 self.cycle_completion_event(1)
3476 }
3477
3478 fn select_completion_previous_action(&self) -> Option<Event> {
3479 self.cycle_completion_event(-1)
3480 }
3481
3482 fn insert_newline(&self) -> Event {
3483 {
3484 let mut st = self.handle.lock();
3485 st.editor.completion = None;
3486 st.record_undo();
3487 let cursor = st.editor.cursor;
3488 st.editor.buffer.insert(cursor, '\n');
3489 st.write_cursor(cursor + 1);
3490 st.sync_buffer_to_history_nav();
3491 }
3492 self.refresh_completion();
3493 Event::BufferChanged
3494 }
3495
3496 fn submit_or_accept_completion(&self) -> Event {
3497 if self.accept_completion_event().is_some() {
3500 return Event::CompletionAccept;
3501 }
3502 let started = path_std_time::Instant::now();
3503 let line = {
3504 let mut st = self.handle.lock();
3505 st.editor.completion = None;
3506 st.editor.last_submitted_recalled_source =
3507 st.editor.history_nav.as_ref().and_then(|nav| {
3508 nav.entries
3509 .get(nav.index)
3510 .and_then(|entry| entry.source_index)
3511 });
3512 st.editor.history_nav = None;
3513 let line = st.editor.buffer.clone();
3514 st.push_current_as_history_entry(!self.defer_submitted_input_history_limit);
3515 st.editor.last_submitted_input_retained = st
3516 .editor
3517 .input_history
3518 .last()
3519 .is_some_and(|draft| draft.buffer == line);
3520 st.editor.last_submitted_revision = Some(st.editor.revision);
3521 line
3522 };
3523 tracing::trace!(
3524 target: "tau_cli::prompt_submission",
3525 stage = "raw_submit_clear",
3526 prompt_bytes = line.len(),
3527 stage_us = started.elapsed().as_micros(),
3528 "content-free prompt submission stage"
3529 );
3530 Event::Line(line)
3531 }
3532
3533 fn handle_enter_key(&self, ctrl: bool, shift: bool, alt: bool) -> Event {
3534 if shift || alt {
3535 return self.insert_newline();
3547 }
3548
3549 if ctrl {
3550 self.submit_or_accept_completion()
3551 } else {
3552 self.insert_newline()
3553 }
3554 }
3555
3556 fn write_cursor_start_raw(&self) {
3557 let mut st = self.handle.lock();
3558 st.write_cursor(0);
3559 }
3560
3561 fn write_cursor_end_raw(&self) {
3562 let mut st = self.handle.lock();
3563 let len = st.editor.buffer.len();
3564 st.write_cursor(len);
3565 }
3566
3567 fn kill_to_start_raw_event(&self) -> Event {
3568 {
3569 let mut st = self.handle.lock();
3570 st.record_undo();
3571 let cursor = st.editor.cursor;
3572 st.editor.buffer.drain(..cursor);
3573 st.write_cursor(0);
3574 st.sync_buffer_to_history_nav();
3575 }
3576 self.refresh_completion();
3577 Event::BufferChanged
3578 }
3579
3580 fn handle_ctrl_c_key(&self) -> Event {
3585 let mut st = self.handle.lock();
3586 if st.editor.buffer.is_empty() {
3587 if st.editor.ctrl_c_cancel_armed {
3588 st.editor.ctrl_c_cancel_armed = false;
3589 return Event::CancelPrompt;
3590 }
3591 st.editor.ctrl_c_cancel_armed = true;
3592 return Event::Notice(
3593 "Press Ctrl-C again to cancel the current response; use Ctrl-D to exit".to_owned(),
3594 );
3595 }
3596 st.editor.ctrl_c_cancel_armed = false;
3597 st.record_undo();
3598 st.editor.buffer.clear();
3599 let abandoned_history_nav = st.editor.history_nav.take().is_some();
3600 st.editor.completion = None;
3601 st.write_cursor(0);
3602 if abandoned_history_nav {
3603 st.limit_input_history();
3604 }
3605 Event::BufferChanged
3606 }
3607
3608 fn handle_control_char_key(&self, ch: char) -> io::Result<Option<Event>> {
3609 match ch {
3610 'd' => {
3611 let is_empty = self
3612 .state
3613 .lock()
3614 .expect("term state mutex poisoned")
3615 .editor
3616 .buffer
3617 .is_empty();
3618 Ok(is_empty.then_some(Event::Eof))
3619 }
3620 'c' => Ok(Some(self.handle_ctrl_c_key())),
3621 'u' => Ok(Some(self.kill_to_start_raw_event())),
3622 'w' => Ok(self.kill_word_left().then_some(Event::BufferChanged)),
3623 'a' => {
3624 self.write_cursor_start_raw();
3625 Ok(None)
3626 }
3627 'e' => {
3628 self.write_cursor_end_raw();
3629 Ok(None)
3630 }
3631 'o' | 'g' => Ok(Some(Event::ExternalEditor)),
3632 'j' => self.step_history_event(1),
3633 'k' => self.step_history_event(-1),
3634 _ => Ok(None),
3635 }
3636 }
3637
3638 fn insert_char_event(&self, ch: char) -> Event {
3639 {
3640 let mut st = self.handle.lock();
3641 st.record_undo();
3642 let cursor = st.editor.cursor;
3643 st.editor.buffer.insert(cursor, ch);
3644 st.write_cursor(cursor + ch.len_utf8());
3645 st.sync_buffer_to_history_nav();
3646 }
3647 self.refresh_completion();
3648 Event::BufferChanged
3649 }
3650
3651 fn handle_plain_edit_key(&self, code: KeyCode) -> Option<Event> {
3652 match code {
3653 KeyCode::Backspace => self.delete_backward().then_some(Event::BufferChanged),
3654 KeyCode::Delete => self.delete_forward().then_some(Event::BufferChanged),
3655 _ => None,
3656 }
3657 }
3658
3659 fn handle_plain_cursor_key(&self, code: KeyCode) {
3660 match code {
3661 KeyCode::Left => {
3662 self.move_cursor_left();
3663 }
3664 KeyCode::Right => {
3665 self.move_cursor_right();
3666 }
3667 KeyCode::Home => {
3668 self.write_cursor_start_raw();
3669 }
3670 KeyCode::End => {
3671 self.write_cursor_end_raw();
3672 }
3673 _ => {}
3674 }
3675 }
3676
3677 fn handle_vertical_key(&self, code: KeyCode, ctrl: bool) -> io::Result<Option<Event>> {
3678 match (code, ctrl) {
3679 (KeyCode::Up, true) => self.step_history_event(-1),
3680 (KeyCode::Down, true) => self.step_history_event(1),
3681 (KeyCode::Up, false) => Ok(self.cycle_or_move_up()),
3682 (KeyCode::Down, false) => Ok(self.cycle_or_move_down()),
3683 _ => Ok(None),
3684 }
3685 }
3686
3687 fn handle_unbound_key(
3688 &self,
3689 key: KeyEvent,
3690 ctrl: bool,
3691 shift: bool,
3692 alt: bool,
3693 ) -> io::Result<Option<Event>> {
3694 match key.code {
3695 KeyCode::Enter => Ok(Some(self.handle_enter_key(ctrl, shift, alt))),
3696 KeyCode::Char(ch) if ctrl => self.handle_control_char_key(ch),
3697 KeyCode::Char(ch) => Ok(Some(self.insert_char_event(ch))),
3698 KeyCode::Backspace | KeyCode::Delete => Ok(self.handle_plain_edit_key(key.code)),
3699 KeyCode::Left | KeyCode::Right | KeyCode::Home | KeyCode::End => {
3700 self.handle_plain_cursor_key(key.code);
3701 Ok(None)
3702 }
3703 KeyCode::Up | KeyCode::Down => self.handle_vertical_key(key.code, ctrl),
3704 KeyCode::BackTab => Ok(Some(Event::BackTab)),
3705 KeyCode::Esc => Ok(Some(Event::Escape)),
3706 _ => Ok(None),
3707 }
3708 }
3709
3710 fn handle_key(&self, key: KeyEvent) -> io::Result<Option<Event>> {
3711 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
3712 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
3713 let alt = key.modifiers.contains(KeyModifiers::ALT);
3714 let binding = key_binding_for_event(key, ctrl);
3715 tracing::trace!(
3716 target: "tau_cli_term_raw::input",
3717 ?key,
3718 ctrl,
3719 shift,
3720 alt,
3721 ?binding,
3722 binding_count = self.bindings.len(),
3723 "handling key event"
3724 );
3725
3726 let ctrl_c = matches!(key.code, KeyCode::Char('c')) && ctrl;
3727 if !ctrl_c {
3728 self.handle.lock().editor.ctrl_c_cancel_armed = false;
3729 }
3730
3731 if let Some(event) = self.handle_completion_key(key, ctrl, shift, alt) {
3732 return Ok(Some(event));
3733 }
3734
3735 if let Some(action) = self.binding_action(&binding) {
3736 tracing::trace!(
3737 target: "tau_cli_term_raw::input",
3738 ?binding,
3739 action,
3740 "matched configured binding"
3741 );
3742 return Ok(Some(Event::Binding(action)));
3743 }
3744
3745 self.handle_unbound_key(key, ctrl, shift, alt)
3746 }
3747}
3748
3749impl Term {
3750 fn shutdown(&mut self) {
3753 {
3757 let mut st = self.handle.lock();
3758 st.terminal.shutdown = true;
3759 }
3760 self.handle.release_redraw_notification();
3761
3762 if let Some(handle) = self.redraw_thread.take() {
3763 let _ = handle.join();
3764 }
3765 }
3766}
3767
3768fn word_left_boundary(buffer: &str, cursor: usize) -> usize {
3769 let before_cursor = &buffer[..cursor];
3770 let trimmed_end = before_cursor.trim_end_matches(char::is_whitespace).len();
3771 before_cursor[..trimmed_end]
3772 .char_indices()
3773 .rev()
3774 .find_map(|(index, ch)| ch.is_whitespace().then_some(index + ch.len_utf8()))
3775 .unwrap_or(0)
3776}
3777
3778fn read_real_raw_event(
3779 mut read: impl FnMut() -> io::Result<CtEvent>,
3780 mut term_size: impl FnMut() -> io::Result<(u16, u16)>,
3781) -> io::Result<RawEvent> {
3782 loop {
3783 let raw = read()?;
3784 tracing::trace!(target: "tau_cli_term_raw::input", ?raw, "terminal raw input event");
3785 match raw {
3786 CtEvent::Key(key) => {
3787 if key.kind == KeyEventKind::Release {
3790 continue;
3791 }
3792 return Ok(RawEvent::Key(key));
3793 }
3794 CtEvent::Resize(w, h) => {
3795 let (actual_w, actual_h) = term_size().unwrap_or((0, 0));
3796 return Ok(RawEvent::Resize(
3797 resample_resize_dimension(w, actual_w),
3798 resample_resize_dimension(h, actual_h),
3799 ));
3800 }
3801 CtEvent::FocusGained => return Ok(RawEvent::FocusChanged { focused: true }),
3802 CtEvent::FocusLost => return Ok(RawEvent::FocusChanged { focused: false }),
3803 CtEvent::Paste(text) => return Ok(RawEvent::Paste(text)),
3804 _ => {}
3807 }
3808 }
3809}
3810
3811fn write_external_pause_features(
3812 writer: &mut impl Write,
3813 terminal_options: TerminalOptions,
3814) -> io::Result<()> {
3815 if !terminal_options.mouse {
3816 crossterm::execute!(writer, DisableMouseCapture)?;
3817 }
3818 crossterm::execute!(
3819 writer,
3820 PopKeyboardEnhancementFlags,
3821 crossterm::event::DisableFocusChange,
3822 crossterm::event::DisableBracketedPaste,
3823 SetCursorStyle::DefaultUserShape,
3824 )
3825}
3826
3827fn write_external_resume_features(
3828 writer: &mut impl Write,
3829 cursor_shape: CursorShape,
3830 terminal_options: TerminalOptions,
3831) -> io::Result<()> {
3832 if !terminal_options.mouse {
3833 crossterm::execute!(writer, DisableMouseCapture)?;
3834 }
3835 crossterm::execute!(
3836 writer,
3837 crossterm::event::EnableBracketedPaste,
3838 crossterm::event::EnableFocusChange,
3839 PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES),
3840 cursor_shape.crossterm_style()
3841 )
3842}
3843
3844fn initialize_terminal_features(
3845 writer: &mut impl Write,
3846 cursor_shape: CursorShape,
3847 terminal_options: TerminalOptions,
3848) -> io::Result<()> {
3849 if let Err(error) = write_external_resume_features(writer, cursor_shape, terminal_options) {
3850 let _ = write_external_pause_features(writer, terminal_options);
3854 return Err(error);
3855 }
3856 Ok(())
3857}
3858
3859impl Drop for Term {
3860 fn drop(&mut self) {
3861 self.shutdown();
3862 if self.should_write_drop_terminal_cleanup() {
3863 let _ = write_drop_terminal_cleanup(&mut io::stdout(), self.terminal_options);
3868 let _ = terminal::disable_raw_mode();
3869 }
3870 }
3871}
3872
3873impl Term {
3874 fn should_write_drop_terminal_cleanup(&self) -> bool {
3875 self.owns_raw_mode && !self.handle.lock().terminal.external_paused
3876 }
3877}
3878
3879fn write_drop_terminal_cleanup(
3880 writer: &mut impl Write,
3881 terminal_options: TerminalOptions,
3882) -> io::Result<()> {
3883 write_external_pause_features(writer, terminal_options)
3884}
3885
3886#[derive(Clone, Debug, PartialEq, Eq)]
3889enum LineSource {
3890 Block {
3891 id: BlockId,
3892 debug_id: String,
3893 wrapped_row: usize,
3894 },
3895 Input {
3896 wrapped_row: usize,
3897 },
3898 InputScrollIndicator,
3899}
3900
3901fn layout_id_list(
3905 ids: &[BlockId],
3906 blocks: &HashMap<BlockId, StyledBlock>,
3907 block_debug_ids: &HashMap<BlockId, String>,
3908 width: usize,
3909 out: &mut Vec<CellRow>,
3910 sources: &mut Vec<LineSource>,
3911) {
3912 for id in ids {
3913 if let Some(block) = blocks.get(id) {
3914 if block.is_empty() {
3915 continue;
3916 }
3917 let lines = layout_block(block, width);
3918 for (wrapped_row, line) in lines.into_iter().enumerate() {
3919 sources.push(LineSource::Block {
3920 id: *id,
3921 debug_id: block_debug_ids
3922 .get(id)
3923 .cloned()
3924 .unwrap_or_else(|| "<unknown>".to_owned()),
3925 wrapped_row,
3926 });
3927 out.push(line.into());
3928 }
3929 }
3930 }
3931}
3932
3933struct HistoryLayoutCache {
3935 width: usize,
3937 generation: TerminalHistoryGeneration,
3939 previous_generation: TerminalHistoryGeneration,
3941 appended_from_line: Option<usize>,
3943 entry_line_offsets: Vec<usize>,
3945 lines: Vec<CellRow>,
3947 sources: Vec<LineSource>,
3949}
3950
3951impl Default for HistoryLayoutCache {
3952 fn default() -> Self {
3953 Self {
3954 width: 0,
3955 generation: TerminalHistoryGeneration::default(),
3956 previous_generation: TerminalHistoryGeneration::default(),
3957 appended_from_line: None,
3958 entry_line_offsets: vec![0],
3959 lines: Vec::new(),
3960 sources: Vec::new(),
3961 }
3962 }
3963}
3964
3965impl HistoryLayoutCache {
3966 fn refresh(&mut self, st: &mut SharedState) -> usize {
3968 if self.width == st.terminal.width && self.generation == st.layout.history_generation {
3969 return 0;
3970 }
3971
3972 let previous_generation = self.generation;
3973 let previous_entry_count = self.entry_line_offsets.len().saturating_sub(1);
3974 let width_changed = self.width != st.terminal.width;
3975 let requested_dirty_from = st.layout.history_dirty_from.take().unwrap_or(0);
3976 let can_reuse_prefix = !width_changed
3977 && requested_dirty_from <= previous_entry_count
3978 && requested_dirty_from <= st.layout.history.len();
3979 let dirty_from = if can_reuse_prefix {
3980 requested_dirty_from
3981 } else {
3982 0
3983 };
3984 let line_start = self
3985 .entry_line_offsets
3986 .get(dirty_from)
3987 .copied()
3988 .unwrap_or(0);
3989 let append_only = can_reuse_prefix
3990 && dirty_from == previous_entry_count
3991 && previous_entry_count <= st.layout.history.len();
3992
3993 self.lines.truncate(line_start);
3994 self.sources.truncate(line_start);
3995 self.entry_line_offsets.truncate(dirty_from + 1);
3996 for id in &st.layout.history[dirty_from..] {
3997 layout_id_list(
3998 std::slice::from_ref(id),
3999 &st.layout.blocks,
4000 &st.layout.block_debug_ids,
4001 st.terminal.width,
4002 &mut self.lines,
4003 &mut self.sources,
4004 );
4005 self.entry_line_offsets.push(self.lines.len());
4006 }
4007
4008 self.width = st.terminal.width;
4009 self.previous_generation = previous_generation;
4010 self.generation = st.layout.history_generation;
4011 self.appended_from_line = append_only.then_some(line_start);
4012 st.layout.history.len().saturating_sub(dirty_from)
4013 }
4014
4015 fn rebuild(st: &SharedState) -> Self {
4017 let mut cache = Self {
4018 width: st.terminal.width,
4019 generation: st.layout.history_generation,
4020 ..Self::default()
4021 };
4022 for id in &st.layout.history {
4023 layout_id_list(
4024 std::slice::from_ref(id),
4025 &st.layout.blocks,
4026 &st.layout.block_debug_ids,
4027 st.terminal.width,
4028 &mut cache.lines,
4029 &mut cache.sources,
4030 );
4031 cache.entry_line_offsets.push(cache.lines.len());
4032 }
4033 cache
4034 }
4035}
4036
4037struct TailLayout {
4039 lines: Vec<CellRow>,
4041 sources: Vec<LineSource>,
4043 active_height: usize,
4045 cursor_row: usize,
4047 cursor_col: usize,
4049}
4050
4051impl TailLayout {
4052 fn fixed_height(&self) -> usize {
4053 self.lines.len().saturating_sub(self.active_height)
4054 }
4055}
4056
4057struct LayoutAll {
4059 all_lines: Vec<CellRow>,
4061 line_sources: Vec<LineSource>,
4063 log_end: usize,
4070 history_generation: TerminalHistoryGeneration,
4072 history_width: usize,
4074 history_height: usize,
4076 cursor_row: usize,
4078 cursor_col: usize,
4080}
4081
4082struct ViewPlan {
4083 viewport_start: usize,
4085 rubber_height: usize,
4086 render_lines: Vec<CellRow>,
4087 cursor_row: usize,
4088}
4089
4090impl ViewPlan {
4091 fn visible_start(&self, _height: usize) -> usize {
4092 self.viewport_start.min(self.render_lines.len())
4093 }
4094
4095 fn visible_lines(&self, height: usize) -> &[CellRow] {
4096 let start = self.visible_start(height);
4097 let end = (start + height).min(self.render_lines.len());
4098 &self.render_lines[start..end]
4099 }
4100
4101 fn cursor_in_visible(&self, height: usize) -> usize {
4102 self.cursor_row.saturating_sub(self.visible_start(height))
4103 }
4104}
4105
4106struct PlanMetrics {
4107 viewport_start: usize,
4108 rubber_height: usize,
4109 render_len: usize,
4110 cursor_row: usize,
4111}
4112
4113#[derive(Default)]
4122struct TerminalModel {
4123 viewport_start: usize,
4125 rubber_height: usize,
4127 history_generation: TerminalHistoryGeneration,
4129 history_width: usize,
4131 history_height: usize,
4133 active_height: usize,
4135 known_lines: Vec<CellRow>,
4137 known_sources: Vec<LineSource>,
4139}
4140
4141impl TerminalModel {
4142 fn desired_viewport_start(layout: &LayoutAll, height: usize) -> usize {
4143 layout.all_lines.len().saturating_sub(height)
4144 }
4145
4146 fn history_cache_matches(&self, history: &HistoryLayoutCache) -> bool {
4147 self.history_generation == history.generation
4148 && self.history_width == history.width
4149 && history.lines.len() <= self.known_lines.len()
4150 && history.sources.len() <= self.known_sources.len()
4151 }
4152
4153 fn history_append_matches(&self, history: &HistoryLayoutCache) -> bool {
4154 self.history_generation == history.previous_generation
4155 && self.history_width == history.width
4156 && history.appended_from_line == Some(self.history_height)
4157 && self.active_height == 0
4161 }
4162
4163 fn hidden_prefix_changed(&self, layout: &LayoutAll) -> bool {
4164 hidden_lines_changed(
4165 &self.known_lines,
4166 &layout.all_lines[..layout.log_end],
4167 self.viewport_start.min(layout.log_end),
4168 )
4169 }
4170
4171 fn changed_hidden_line(&self, layout: &LayoutAll) -> Option<usize> {
4172 changed_line_in_range(
4173 &self.known_lines,
4174 &layout.all_lines[..layout.log_end],
4175 0..self.viewport_start.min(layout.log_end),
4176 )
4177 }
4178
4179 fn build_plan(layout: &LayoutAll, viewport_start: usize, rubber_height: usize) -> ViewPlan {
4180 let mut render_lines = Vec::with_capacity(layout.all_lines.len() + rubber_height);
4181 render_lines.extend_from_slice(&layout.all_lines[..layout.log_end]);
4182 render_lines
4183 .extend(std::iter::repeat_with(|| CellRow::new(Vec::new())).take(rubber_height));
4184 render_lines.extend_from_slice(&layout.all_lines[layout.log_end..]);
4185
4186 let cursor_row = if layout.log_end <= layout.cursor_row {
4187 layout.cursor_row + rubber_height
4188 } else {
4189 layout.cursor_row
4190 };
4191
4192 ViewPlan {
4193 viewport_start,
4194 rubber_height,
4195 render_lines,
4196 cursor_row,
4197 }
4198 }
4199
4200 fn full_redraw_plan(layout: &LayoutAll, height: usize) -> ViewPlan {
4201 let plan = Self::build_plan(layout, Self::desired_viewport_start(layout, height), 0);
4202 Self::keep_cursor_visible(plan, height)
4203 }
4204
4205 #[cfg(test)]
4206 fn bottom_aligned_plan(layout: &LayoutAll, height: usize) -> ViewPlan {
4207 let mut plan = Self::build_plan(layout, Self::desired_viewport_start(layout, height), 0);
4208 plan.viewport_start = plan.visible_start(height);
4209 plan
4210 }
4211
4212 fn keep_cursor_visible(mut plan: ViewPlan, height: usize) -> ViewPlan {
4213 let height = height.max(1);
4214 let bottom_start = plan.visible_start(height);
4215 let viewport_start = viewport_start_with_cursor(
4216 bottom_start,
4217 plan.cursor_row,
4218 plan.render_lines.len(),
4219 height,
4220 );
4221
4222 if viewport_start < bottom_start {
4223 let viewport_end = (viewport_start + height).min(plan.render_lines.len());
4224 plan.render_lines.truncate(viewport_end);
4225 }
4226
4227 plan.viewport_start = plan.visible_start(height);
4228 plan
4229 }
4230
4231 fn plan_metrics(
4232 &self,
4233 log_height: usize,
4234 fixed_height: usize,
4235 cursor_row: usize,
4236 height: usize,
4237 ) -> PlanMetrics {
4238 let height = height.max(1);
4239 let viewport_start = self.viewport_start.min(log_height);
4240 let mut rubber_height = self.rubber_height;
4241
4242 if fixed_height < height {
4243 let occupied = log_height.saturating_sub(viewport_start) + rubber_height + fixed_height;
4244 if occupied < height {
4245 if 0 < self.viewport_start || 0 < rubber_height {
4249 rubber_height += height - occupied;
4250 }
4251 } else if height < occupied {
4252 let overflow = occupied - height;
4253 let consume_rubber = rubber_height.min(overflow);
4254 rubber_height -= consume_rubber;
4255 }
4256 } else {
4257 rubber_height = 0;
4258 }
4259
4260 let render_len = log_height + rubber_height + fixed_height;
4261 let cursor_row = if log_height <= cursor_row {
4262 cursor_row + rubber_height
4263 } else {
4264 cursor_row
4265 };
4266 let bottom_start = render_len.saturating_sub(height);
4267 let visible_start =
4268 viewport_start_with_cursor(bottom_start, cursor_row, render_len, height);
4269 let render_len = if visible_start < bottom_start {
4270 (visible_start + height).min(render_len)
4271 } else {
4272 render_len
4273 };
4274
4275 PlanMetrics {
4276 viewport_start: render_len.saturating_sub(height),
4277 rubber_height,
4278 render_len,
4279 cursor_row,
4280 }
4281 }
4282
4283 fn plan_view(&self, layout: &LayoutAll, height: usize) -> ViewPlan {
4284 let fixed_height = layout.all_lines.len().saturating_sub(layout.log_end);
4285 let metrics = self.plan_metrics(layout.log_end, fixed_height, layout.cursor_row, height);
4286 let mut plan = Self::build_plan(layout, metrics.viewport_start, metrics.rubber_height);
4287 plan.cursor_row = metrics.cursor_row;
4288 plan.render_lines.truncate(metrics.render_len);
4289 plan.viewport_start = metrics.viewport_start;
4290 plan
4291 }
4292
4293 fn apply_fast_plan(
4294 &mut self,
4295 history: &HistoryLayoutCache,
4296 tail: &TailLayout,
4297 metrics: &PlanMetrics,
4298 ) {
4299 self.viewport_start = metrics.viewport_start;
4300 self.rubber_height = metrics.rubber_height;
4301 self.history_generation = history.generation;
4302 self.history_width = history.width;
4303 self.known_lines.truncate(self.history_height);
4304 self.known_sources.truncate(self.history_height);
4305 self.known_lines
4306 .extend_from_slice(&history.lines[self.history_height..]);
4307 self.known_sources
4308 .extend_from_slice(&history.sources[self.history_height..]);
4309 self.history_height = history.lines.len();
4310 self.active_height = tail.active_height;
4311 self.known_lines
4312 .extend_from_slice(&tail.lines[..tail.active_height]);
4313 self.known_sources
4314 .extend_from_slice(&tail.sources[..tail.active_height]);
4315 }
4316
4317 fn reset_to_layout(&mut self, layout: &LayoutAll, viewport_start: usize, rubber_height: usize) {
4318 self.viewport_start = viewport_start;
4319 self.rubber_height = rubber_height;
4320 self.history_generation = layout.history_generation;
4321 self.history_width = layout.history_width;
4322 self.history_height = layout.history_height;
4323 self.active_height = layout.log_end.saturating_sub(layout.history_height);
4324 self.known_lines = layout.all_lines[..layout.log_end].to_vec();
4325 self.known_sources = layout.line_sources[..layout.log_end].to_vec();
4326 }
4327}
4328
4329fn prompt_input_max_rows(terminal_height: usize) -> usize {
4330 (terminal_height.max(1) * PROMPT_INPUT_MAX_HEIGHT_PERCENT / 100).max(1)
4331}
4332
4333fn prompt_scroll_indicator_rows(
4334 show_indicator: bool,
4335 buffer_non_empty: bool,
4336 total_rows: usize,
4337 cap_rows: usize,
4338) -> usize {
4339 usize::from(show_indicator && buffer_non_empty && 2 <= cap_rows && cap_rows < total_rows)
4340}
4341
4342fn prompt_editable_rows(total_rows: usize, cap_rows: usize, indicator_rows: usize) -> usize {
4343 cap_rows
4344 .saturating_sub(indicator_rows)
4345 .max(1)
4346 .min(total_rows.max(1))
4347}
4348
4349fn prompt_scroll_indicator_text(
4350 start: usize,
4351 visible_rows: usize,
4352 total_rows: usize,
4353 width: usize,
4354) -> String {
4355 let end = (start + visible_rows).min(total_rows);
4356 let hidden_above = start;
4357 let hidden_below = total_rows.saturating_sub(end);
4358 let full = format!(
4359 "↕ prompt rows {}-{}/{} ↑{} ↓{}",
4360 start + 1,
4361 end,
4362 total_rows,
4363 hidden_above,
4364 hidden_below
4365 );
4366 if display_width(&full) <= width {
4367 return full;
4368 }
4369 let compact = format!("↕ ↑{} ↓{}", hidden_above, hidden_below);
4370 if display_width(&compact) <= width {
4371 return compact;
4372 }
4373 truncate_to_width("↕", width)
4374}
4375
4376fn layout_tail(st: &SharedState, history_height: usize) -> TailLayout {
4377 let width = st.terminal.width;
4378 let mut lines: Vec<CellRow> = Vec::new();
4379 let mut sources: Vec<LineSource> = Vec::new();
4380
4381 layout_id_list(
4382 &st.layout.above_active,
4383 &st.layout.blocks,
4384 &st.layout.block_debug_ids,
4385 width,
4386 &mut lines,
4387 &mut sources,
4388 );
4389 let active_height = lines.len();
4390 layout_id_list(
4391 &st.layout.above_sticky,
4392 &st.layout.blocks,
4393 &st.layout.block_debug_ids,
4394 width,
4395 &mut lines,
4396 &mut sources,
4397 );
4398
4399 let above_end = history_height + lines.len();
4400
4401 let mut input_content = st.editor.left_prompt.clone();
4402 if st.editor.buffer.is_empty() {
4403 for span in st.editor.input_placeholder.spans() {
4404 input_content.push(span.clone());
4405 }
4406 } else {
4407 input_content.push(Span::plain(&st.editor.buffer));
4408 }
4409 let mut input_lines = layout_lines()
4414 .content(&input_content)
4415 .width(width)
4416 .preserve_last_newline(true)
4417 .call();
4418
4419 let left_cols = st.editor.left_prompt.char_count();
4420 let (buffer_cursor_row, cursor_col) =
4421 buffer_position_for_byte(&st.editor.buffer, st.editor.cursor, width, left_cols);
4422 while input_lines.len() <= buffer_cursor_row {
4430 input_lines.push(Vec::new());
4431 }
4432
4433 if !st.editor.right_prompt.is_empty() && !input_lines.is_empty() {
4434 let first_line = &input_lines[0];
4435 let right_cells = st.editor.right_prompt.to_cells();
4436 let first_cols: usize = first_line.iter().map(|c| c.col_width()).sum();
4437 let right_cols: usize = right_cells.iter().map(|c| c.col_width()).sum();
4438 let needed = first_cols + 1 + right_cols;
4439 if needed <= width && input_lines.len() == 1 {
4440 let padding = width - first_cols - right_cols;
4441 let mut padded = first_line.clone();
4442 padded.extend(std::iter::repeat_n(Cell::plain(' '), padding));
4443 padded.extend(right_cells);
4444 input_lines[0] = padded;
4445 }
4446 }
4447
4448 let input_total_rows = input_lines.len().max(1);
4449 let cap_rows = prompt_input_max_rows(st.terminal.height);
4450 let indicator_rows = prompt_scroll_indicator_rows(
4451 st.editor.show_prompt_scroll_indicator,
4452 !st.editor.buffer.is_empty(),
4453 input_total_rows,
4454 cap_rows,
4455 );
4456 let visible_input_rows = prompt_editable_rows(input_total_rows, cap_rows, indicator_rows);
4457 let viewport_start = viewport_start_with_cursor(
4458 st.editor.input_viewport_start,
4459 buffer_cursor_row,
4460 input_total_rows,
4461 visible_input_rows,
4462 );
4463 let cursor_row = above_end + indicator_rows + buffer_cursor_row.saturating_sub(viewport_start);
4464
4465 if indicator_rows == 1 {
4466 let indicator = prompt_scroll_indicator_text(
4467 viewport_start,
4468 visible_input_rows,
4469 input_total_rows,
4470 width,
4471 );
4472 sources.push(LineSource::InputScrollIndicator);
4473 lines.push(StyledText::from(indicator).to_cells().into());
4474 }
4475
4476 let viewport_end = (viewport_start + visible_input_rows).min(input_lines.len());
4477 for (wrapped_row, line) in input_lines
4478 .into_iter()
4479 .enumerate()
4480 .skip(viewport_start)
4481 .take(viewport_end.saturating_sub(viewport_start))
4482 {
4483 sources.push(LineSource::Input { wrapped_row });
4484 lines.push(line.into());
4485 }
4486 layout_id_list(
4487 &st.layout.suggestions,
4488 &st.layout.blocks,
4489 &st.layout.block_debug_ids,
4490 width,
4491 &mut lines,
4492 &mut sources,
4493 );
4494 layout_id_list(
4495 &st.layout.below,
4496 &st.layout.blocks,
4497 &st.layout.block_debug_ids,
4498 width,
4499 &mut lines,
4500 &mut sources,
4501 );
4502
4503 TailLayout {
4504 lines,
4505 sources,
4506 active_height,
4507 cursor_row,
4508 cursor_col,
4509 }
4510}
4511
4512fn layout_all_from_cached_history(history: &HistoryLayoutCache, tail: TailLayout) -> LayoutAll {
4513 let log_end = history.lines.len() + tail.active_height;
4514 let cursor_row = tail.cursor_row;
4515 let cursor_col = tail.cursor_col;
4516 let mut all_lines = Vec::with_capacity(history.lines.len() + tail.lines.len());
4517 all_lines.extend_from_slice(&history.lines);
4518 all_lines.extend(tail.lines);
4519
4520 let mut line_sources = Vec::with_capacity(history.sources.len() + tail.sources.len());
4521 line_sources.extend_from_slice(&history.sources);
4522 line_sources.extend(tail.sources);
4523
4524 LayoutAll {
4525 all_lines,
4526 line_sources,
4527 log_end,
4528 history_generation: history.generation,
4529 history_width: history.width,
4530 history_height: history.lines.len(),
4531 cursor_row,
4532 cursor_col,
4533 }
4534}
4535
4536fn layout_all(st: &SharedState) -> LayoutAll {
4538 let history = HistoryLayoutCache::rebuild(st);
4539 let tail = layout_tail(st, history.lines.len());
4540 layout_all_from_cached_history(&history, tail)
4541}
4542
4543fn visible_lines_from_parts(
4544 history_lines: &[CellRow],
4545 tail: &TailLayout,
4546 metrics: &PlanMetrics,
4547) -> Vec<CellRow> {
4548 render_rows_from(history_lines, tail, metrics, metrics.viewport_start)
4549}
4550
4551fn render_rows_from(
4553 history_lines: &[CellRow],
4554 tail: &TailLayout,
4555 metrics: &PlanMetrics,
4556 start: usize,
4557) -> Vec<CellRow> {
4558 let history_height = history_lines.len();
4559 let log_height = history_height + tail.active_height;
4560 let fixed_start = log_height + metrics.rubber_height;
4561 let mut rows = Vec::with_capacity(metrics.render_len.saturating_sub(start));
4562
4563 for idx in start..metrics.render_len {
4564 if idx < history_height {
4565 rows.push(
4566 history_lines
4567 .get(idx)
4568 .expect("requested history row should exist")
4569 .clone(),
4570 );
4571 } else if idx < log_height {
4572 rows.push(
4573 tail.lines
4574 .get(idx - history_height)
4575 .expect("requested active row should exist")
4576 .clone(),
4577 );
4578 } else if idx < fixed_start {
4579 rows.push(CellRow::new(Vec::new()));
4580 } else {
4581 rows.push(
4582 tail.lines
4583 .get(tail.active_height + idx - fixed_start)
4584 .expect("requested fixed row should exist")
4585 .clone(),
4586 );
4587 }
4588 }
4589
4590 rows
4591}
4592
4593fn scrolling_suffix(
4595 history_lines: &[CellRow],
4596 tail: &TailLayout,
4597 metrics: &PlanMetrics,
4598 terminal_model: &TerminalModel,
4599) -> Vec<CellRow> {
4600 render_rows_from(history_lines, tail, metrics, terminal_model.viewport_start)
4601}
4602
4603enum RenderFrame {
4606 Fast {
4607 tail: TailLayout,
4608 metrics: PlanMetrics,
4609 },
4610 Full {
4611 layout: LayoutAll,
4612 },
4613}
4614
4615struct RedrawPass {
4616 width: usize,
4617 height: usize,
4618 size_changed: bool,
4619 force_full: bool,
4620 sync_gen: RedrawSyncGeneration,
4621 pending_raw: Vec<String>,
4622 redraw_history_size: usize,
4623 frame: RenderFrame,
4624 presentation_observations: Option<CapturedPresentationObservations>,
4626}
4627
4628struct FullRenderMark {
4629 reason: &'static str,
4630 prev_visible_start: usize,
4631 visible_start: usize,
4632 height: usize,
4633 changed_line: Option<usize>,
4634 previous_source: Option<LineSource>,
4635}
4636
4637struct FullRenderMarkInput {
4638 reason: &'static str,
4639 changed_line: Option<usize>,
4640 previous_source: Option<LineSource>,
4641}
4642
4643fn redraw_loop(
4644 state: Arc<Mutex<SharedState>>,
4645 notify_rx: tau_blocking_notify_channel::Receiver,
4646 writer: Box<dyn Write + Send>,
4647 input_tx: path_std_sync::mpsc::Sender<InputMessage>,
4648 sync_condvar: &std::sync::Condvar,
4649) {
4650 let mut writer = BufWriter::new(writer);
4651 let (w, h) = {
4652 let st = state.lock().expect("term state mutex poisoned");
4653 (st.terminal.width, st.terminal.height)
4654 };
4655 let mut screen = Screen::new(w);
4656 let mut prev_width = w;
4657 let mut prev_height = h;
4658 let mut history_cache = HistoryLayoutCache::default();
4659 let mut terminal_model = TerminalModel::default();
4660
4661 loop {
4662 if render_shutdown_if_requested(
4663 &state,
4664 &mut writer,
4665 &mut screen,
4666 &terminal_model,
4667 prev_width,
4668 sync_condvar,
4669 ) {
4670 break;
4671 }
4672
4673 if !wait_for_redraw_or_sync(&state, ¬ify_rx) {
4674 break;
4675 }
4676
4677 tracing::trace!(
4678 target: "tau_cli_term_raw::frontend_progress",
4679 "redraw prepare started"
4680 );
4681 let pass = match prepare_redraw_pass(
4682 &state,
4683 &mut history_cache,
4684 &terminal_model,
4685 prev_width,
4686 prev_height,
4687 sync_condvar,
4688 ) {
4689 Some(pass) => pass,
4690 None => continue,
4691 };
4692 let write_started = path_std_time::Instant::now();
4693 tracing::trace!(
4694 target: "tau_cli_term_raw::frontend_progress",
4695 "terminal write started"
4696 );
4697 let render_result = render_redraw_pass(
4698 &state,
4699 &mut writer,
4700 &mut screen,
4701 &history_cache,
4702 &mut terminal_model,
4703 &pass,
4704 );
4705 let write_elapsed = write_started.elapsed();
4706 if let Err(error) = render_result {
4707 trace_failed_presentation_observations(&state, &pass, "write", write_elapsed, &error);
4708 fail_terminal_output(&state, &input_tx, sync_condvar, error);
4709 discard_failed_output(writer);
4710 return;
4711 }
4712 tracing::trace!(
4713 target: "tau_cli_term_raw::frontend_progress",
4714 write_us = write_elapsed.as_micros(),
4715 "terminal write finished; flush started"
4716 );
4717 let flush_started = path_std_time::Instant::now();
4718 let output_result = writer.flush();
4719 let flush_elapsed = flush_started.elapsed();
4720 if let Err(error) = output_result {
4721 trace_failed_presentation_observations(&state, &pass, "flush", flush_elapsed, &error);
4722 fail_terminal_output(&state, &input_tx, sync_condvar, error);
4723 discard_failed_output(writer);
4724 return;
4725 }
4726 tracing::trace!(
4727 target: "tau_cli_term_raw::frontend_progress",
4728 write_us = write_elapsed.as_micros(),
4729 flush_us = flush_elapsed.as_micros(),
4730 "terminal write and flush finished"
4731 );
4732 trace_flushed_presentation_observations(&state, &pass);
4733 if (Duration::from_millis(500) <= write_elapsed
4734 || Duration::from_millis(500) <= flush_elapsed)
4735 && admit_stall_warning()
4736 {
4737 tracing::warn!(
4738 target: "tau_cli_term_raw::frontend_progress",
4739 write_ms = write_elapsed.as_millis(),
4740 flush_ms = flush_elapsed.as_millis(),
4741 "terminal output stalled"
4742 );
4743 }
4744
4745 prev_width = pass.width;
4746 prev_height = pass.height;
4747
4748 complete_redraw_sync(&state, pass.sync_gen, sync_condvar);
4749 }
4750}
4751
4752fn discard_failed_output(writer: BufWriter<Box<dyn Write + Send>>) {
4755 let _ = writer.into_parts();
4756}
4757
4758fn render_shutdown_if_requested(
4759 state: &Arc<Mutex<SharedState>>,
4760 writer: &mut BufWriter<Box<dyn Write + Send>>,
4761 screen: &mut Screen,
4762 terminal_model: &TerminalModel,
4763 prev_width: usize,
4764 sync_condvar: &std::sync::Condvar,
4765) -> bool {
4766 let mut st = state.lock().expect("term state mutex poisoned");
4767 if !st.terminal.shutdown {
4768 return false;
4769 }
4770 if st.terminal.external_paused {
4771 st.terminal.sync_completed = st.terminal.sync_requested;
4772 drop(st);
4773 sync_condvar.notify_all();
4774 return true;
4775 }
4776
4777 let layout = layout_all(&st);
4779 let height = st.terminal.height.max(1);
4780 let plan = terminal_model.plan_view(&layout, height);
4781 let visible = plan.visible_lines(height);
4782 let cursor_in_visible = plan.cursor_in_visible(height);
4783 drop(st);
4784
4785 screen.set_width(prev_width);
4786 let _ = screen.update(writer, visible, (cursor_in_visible, layout.cursor_col));
4787 let below = plan.render_lines.len().saturating_sub(plan.cursor_row + 1);
4788 for _ in 0..=below {
4789 let _ = writer.queue(crossterm::style::Print("\r\n"));
4790 }
4791 let _ = writer.flush();
4792 {
4793 let mut st = state.lock().expect("term state mutex poisoned");
4794 st.terminal.sync_completed = st.terminal.sync_requested;
4795 }
4796 sync_condvar.notify_all();
4797 true
4798}
4799
4800fn wait_for_redraw_or_sync(
4801 state: &Arc<Mutex<SharedState>>,
4802 notify_rx: &tau_blocking_notify_channel::Receiver,
4803) -> bool {
4804 let trace_enabled = tracing::enabled!(
4807 target: "tau_cli_term_raw::frontend_progress",
4808 tracing::Level::TRACE
4809 );
4810 let lock_started = trace_enabled.then(path_std_time::Instant::now);
4811 let st = state.lock().expect("term state mutex poisoned");
4812 if let Some(lock_started) = lock_started {
4813 tracing::trace!(
4814 target: "tau_cli_term_raw::frontend_progress",
4815 lock_wait_us = lock_started.elapsed().as_micros(),
4816 stage = "notification_check",
4817 "terminal shared state acquired"
4818 );
4819 }
4820 if st.terminal.sync_completed < st.terminal.sync_requested {
4821 return true;
4822 }
4823 drop(st);
4824 let notification_started = trace_enabled.then(path_std_time::Instant::now);
4825 let result = notify_rx.recv().is_ok();
4826 if let Some(notification_started) = notification_started {
4827 tracing::trace!(
4828 target: "tau_cli_term_raw::frontend_progress",
4829 notification_wait_us = notification_started.elapsed().as_micros(),
4830 "redraw notification wait finished"
4831 );
4832 }
4833 result
4834}
4835
4836fn prepare_redraw_pass(
4837 state: &Arc<Mutex<SharedState>>,
4838 history_cache: &mut HistoryLayoutCache,
4839 terminal_model: &TerminalModel,
4840 prev_width: usize,
4841 prev_height: usize,
4842 sync_condvar: &std::sync::Condvar,
4843) -> Option<RedrawPass> {
4844 let trace_enabled = tracing::enabled!(
4845 target: "tau_cli_term_raw::frontend_progress",
4846 tracing::Level::TRACE
4847 );
4848 let lock_started = trace_enabled.then(path_std_time::Instant::now);
4849 let mut st = state.lock().expect("term state mutex poisoned");
4850 if let Some(lock_started) = lock_started {
4851 tracing::trace!(
4852 target: "tau_cli_term_raw::frontend_progress",
4853 lock_wait_us = lock_started.elapsed().as_micros(),
4854 stage = "redraw_prepare",
4855 "terminal shared state acquired"
4856 );
4857 }
4858 if st.terminal.redraw_suppression != 0 {
4859 st.terminal.redraw_dirty_while_suppressed = true;
4863 st.terminal.sync_completed = st.terminal.sync_requested;
4864 sync_condvar.notify_all();
4865 return None;
4866 }
4867 if st.terminal.external_paused {
4868 st.terminal.sync_completed = st.terminal.sync_requested;
4869 sync_condvar.notify_all();
4870 return None;
4871 }
4872 let width = st.terminal.width;
4873 let height = st.terminal.height.max(1);
4874 let size_changed = prev_width != width || prev_height != height;
4875 let force_full = std::mem::take(&mut st.terminal.invalidate_screen);
4877 let sync_gen = st.terminal.sync_requested;
4881 let pending_raw = std::mem::take(&mut st.terminal.pending_raw);
4882 let redraw_history_size = st.terminal.redraw_history_size;
4883 let presentation_observations =
4884 (!st.presentation_observations.is_empty()).then(|| st.presentation_observations.capture());
4885
4886 let preparation_started = trace_enabled.then(path_std_time::Instant::now);
4887 history_cache.refresh(&mut st);
4888 let tail = layout_tail(&st, history_cache.lines.len());
4889 let log_height = history_cache.lines.len() + tail.active_height;
4890 let fixed_height = tail.fixed_height();
4891 let metrics = terminal_model.plan_metrics(log_height, fixed_height, tail.cursor_row, height);
4892 let can_fast = !size_changed
4893 && !force_full
4894 && ((terminal_model.history_cache_matches(history_cache)
4895 && metrics.viewport_start == terminal_model.viewport_start)
4896 || (terminal_model.history_append_matches(history_cache)
4897 && terminal_model.viewport_start <= metrics.viewport_start))
4898 && metrics.viewport_start <= history_cache.lines.len();
4899 let frame = if can_fast {
4900 RenderFrame::Fast { tail, metrics }
4901 } else {
4902 RenderFrame::Full {
4903 layout: layout_all_from_cached_history(history_cache, tail),
4904 }
4905 };
4906 let pass = RedrawPass {
4907 width,
4908 height,
4909 size_changed,
4910 force_full,
4911 sync_gen,
4912 pending_raw,
4913 redraw_history_size,
4914 frame,
4915 presentation_observations,
4916 };
4917 if let Some(preparation_started) = preparation_started {
4918 tracing::trace!(
4919 target: "tau_cli_term_raw::frontend_progress",
4920 preparation_us = preparation_started.elapsed().as_micros(),
4921 "redraw layout prepared"
4922 );
4923 }
4924 Some(pass)
4925}
4926
4927fn trace_flushed_presentation_observations(_state: &Arc<Mutex<SharedState>>, pass: &RedrawPass) {
4929 let Some(observations) = &pass.presentation_observations else {
4930 return;
4931 };
4932 let flushed_at = path_std_time::Instant::now();
4933 for fact in &observations.facts {
4934 tracing::trace!(
4935 target: "tau_cli_term_raw::frontend_progress",
4936 delivery_id = fact.delivery_id.get(),
4937 fact = fact.fact,
4938 mutation_generation = fact.generation.get(),
4939 frame_generation = observations.generation.get(),
4940 mutation_to_flush_us = flushed_at.duration_since(fact.observed_at).as_micros(),
4941 "selected presentation mutation frame written and flushed"
4942 );
4943 }
4944 if observations.omitted != 0 {
4945 tracing::trace!(
4946 target: "tau_cli_term_raw::frontend_progress",
4947 frame_generation = observations.generation.get(),
4948 omitted = observations.omitted,
4949 "selected presentation flush observations omitted"
4950 );
4951 }
4952 #[cfg(test)]
4953 _state
4954 .lock()
4955 .expect("term state mutex poisoned")
4956 .presentation_observations
4957 .record_success_for_test(observations);
4958}
4959
4960fn trace_failed_presentation_observations(
4962 _state: &Arc<Mutex<SharedState>>,
4963 pass: &RedrawPass,
4964 stage: &'static str,
4965 stage_elapsed: Duration,
4966 error: &io::Error,
4967) {
4968 let Some(observations) = &pass.presentation_observations else {
4969 return;
4970 };
4971 #[cfg(test)]
4972 _state
4973 .lock()
4974 .expect("term state mutex poisoned")
4975 .presentation_failure_test_records
4976 .push((
4977 stage,
4978 stage_elapsed.as_micros(),
4979 observations.facts.len(),
4980 observations.omitted,
4981 ));
4982 tracing::trace!(
4983 target: "tau_cli_term_raw::frontend_progress",
4984 stage,
4985 stage_us = stage_elapsed.as_micros(),
4986 frame_generation = observations.generation.get(),
4987 indeterminate_facts = observations.facts.len(),
4988 omitted = observations.omitted,
4989 error_kind = ?error.kind(),
4990 "selected presentation redraw pass failed or is indeterminate"
4991 );
4992}
4993
4994fn render_redraw_pass(
4995 state: &Arc<Mutex<SharedState>>,
4996 writer: &mut BufWriter<Box<dyn Write + Send>>,
4997 screen: &mut Screen,
4998 history_cache: &HistoryLayoutCache,
4999 terminal_model: &mut TerminalModel,
5000 pass: &RedrawPass,
5001) -> io::Result<()> {
5002 for seq in &pass.pending_raw {
5008 writer.write_all(seq.as_bytes())?;
5009 }
5010 if pass.force_full {
5011 screen.invalidate();
5015 }
5016
5017 match &pass.frame {
5018 RenderFrame::Fast { tail, metrics } => {
5019 render_fast_frame(
5020 writer,
5021 screen,
5022 history_cache,
5023 terminal_model,
5024 pass,
5025 tail,
5026 metrics,
5027 )?;
5028 }
5029 RenderFrame::Full { layout } => {
5030 render_full_frame(state, writer, screen, terminal_model, pass, layout)?;
5031 }
5032 }
5033 Ok(())
5034}
5035
5036fn render_fast_frame(
5037 writer: &mut BufWriter<Box<dyn Write + Send>>,
5038 screen: &mut Screen,
5039 history_cache: &HistoryLayoutCache,
5040 terminal_model: &mut TerminalModel,
5041 pass: &RedrawPass,
5042 tail: &TailLayout,
5043 metrics: &PlanMetrics,
5044) -> io::Result<()> {
5045 screen.set_width(pass.width);
5046 if terminal_model.viewport_start < metrics.viewport_start {
5047 let previous_viewport_start = terminal_model.viewport_start;
5048 let suffix = scrolling_suffix(&history_cache.lines, tail, metrics, terminal_model);
5053 let cursor_row = metrics.cursor_row.saturating_sub(previous_viewport_start);
5054 screen.render_scrolling(
5055 writer,
5056 &suffix,
5057 0,
5058 pass.height,
5059 (cursor_row, tail.cursor_col),
5060 )?;
5061 } else {
5062 let visible = visible_lines_from_parts(&history_cache.lines, tail, metrics);
5063 let cursor_in_visible = metrics.cursor_row.saturating_sub(metrics.viewport_start);
5064 screen.update(writer, &visible, (cursor_in_visible, tail.cursor_col))?;
5065 }
5066 terminal_model.apply_fast_plan(history_cache, tail, metrics);
5067 Ok(())
5068}
5069
5070fn render_full_frame(
5071 state: &Arc<Mutex<SharedState>>,
5072 writer: &mut BufWriter<Box<dyn Write + Send>>,
5073 screen: &mut Screen,
5074 terminal_model: &mut TerminalModel,
5075 pass: &RedrawPass,
5076 layout: &LayoutAll,
5077) -> io::Result<()> {
5078 if pass.size_changed || pass.force_full {
5079 let reason = if pass.size_changed {
5080 "size_changed"
5081 } else {
5082 "force_full"
5083 };
5084 render_marked_full_frame(
5085 state,
5086 writer,
5087 screen,
5088 terminal_model,
5089 pass,
5090 layout,
5091 FullRenderMarkInput {
5092 reason,
5093 changed_line: None,
5094 previous_source: None,
5095 },
5096 )?;
5097 return Ok(());
5098 }
5099
5100 render_incremental_or_scroll_frame(state, writer, screen, terminal_model, pass, layout)
5101}
5102
5103fn render_incremental_or_scroll_frame(
5104 state: &Arc<Mutex<SharedState>>,
5105 writer: &mut BufWriter<Box<dyn Write + Send>>,
5106 screen: &mut Screen,
5107 terminal_model: &mut TerminalModel,
5108 pass: &RedrawPass,
5109 layout: &LayoutAll,
5110) -> io::Result<()> {
5111 screen.set_width(pass.width);
5112
5113 let hidden_prefix_changed = terminal_model.hidden_prefix_changed(layout);
5114 let incremental_plan = terminal_model.plan_view(layout, pass.height);
5115 let incremental_visible_start = incremental_plan.viewport_start;
5116
5117 if incremental_visible_start < terminal_model.viewport_start {
5118 render_viewport_moved_up_frame(state, writer, screen, terminal_model, pass, layout)
5119 } else if hidden_prefix_changed {
5120 render_hidden_prefix_changed_frame(state, writer, screen, terminal_model, pass, layout)
5121 } else if terminal_model.viewport_start < incremental_visible_start {
5122 render_scrolling_frame(
5123 writer,
5124 screen,
5125 terminal_model,
5126 pass,
5127 layout,
5128 incremental_plan,
5129 )
5130 } else {
5131 render_diff_frame(
5132 writer,
5133 screen,
5134 terminal_model,
5135 pass,
5136 layout,
5137 incremental_plan,
5138 )
5139 }
5140}
5141
5142fn render_marked_full_frame(
5143 state: &Arc<Mutex<SharedState>>,
5144 writer: &mut BufWriter<Box<dyn Write + Send>>,
5145 screen: &mut Screen,
5146 terminal_model: &mut TerminalModel,
5147 pass: &RedrawPass,
5148 layout: &LayoutAll,
5149 mark_input: FullRenderMarkInput,
5150) -> io::Result<()> {
5151 let plan = TerminalModel::full_redraw_plan(layout, pass.height);
5152 let mark = FullRenderMark {
5153 reason: mark_input.reason,
5154 prev_visible_start: terminal_model.viewport_start,
5155 visible_start: plan.viewport_start,
5156 height: pass.height,
5157 changed_line: mark_input.changed_line,
5158 previous_source: mark_input.previous_source,
5159 };
5160 mark_full_render(state, layout, mark);
5161 full_render(
5162 writer,
5163 screen,
5164 layout,
5165 &plan,
5166 pass.width,
5167 pass.height,
5168 pass.redraw_history_size,
5169 )?;
5170 reset_model_after_rendered_full_frame(terminal_model, pass, layout, plan);
5171 Ok(())
5172}
5173
5174fn render_viewport_moved_up_frame(
5175 state: &Arc<Mutex<SharedState>>,
5176 writer: &mut BufWriter<Box<dyn Write + Send>>,
5177 screen: &mut Screen,
5178 terminal_model: &mut TerminalModel,
5179 pass: &RedrawPass,
5180 layout: &LayoutAll,
5181) -> io::Result<()> {
5182 render_marked_full_frame(
5188 state,
5189 writer,
5190 screen,
5191 terminal_model,
5192 pass,
5193 layout,
5194 FullRenderMarkInput {
5195 reason: "viewport_moved_up",
5196 changed_line: None,
5197 previous_source: None,
5198 },
5199 )
5200}
5201
5202fn render_hidden_prefix_changed_frame(
5203 state: &Arc<Mutex<SharedState>>,
5204 writer: &mut BufWriter<Box<dyn Write + Send>>,
5205 screen: &mut Screen,
5206 terminal_model: &mut TerminalModel,
5207 pass: &RedrawPass,
5208 layout: &LayoutAll,
5209) -> io::Result<()> {
5210 let changed_line = terminal_model.changed_hidden_line(layout);
5215 let previous_source = changed_line
5216 .and_then(|idx| terminal_model.known_sources.get(idx))
5217 .cloned();
5218 render_marked_full_frame(
5219 state,
5220 writer,
5221 screen,
5222 terminal_model,
5223 pass,
5224 layout,
5225 FullRenderMarkInput {
5226 reason: "hidden_prefix_changed",
5227 changed_line,
5228 previous_source,
5229 },
5230 )
5231}
5232
5233fn render_scrolling_frame(
5234 writer: &mut BufWriter<Box<dyn Write + Send>>,
5235 screen: &mut Screen,
5236 terminal_model: &mut TerminalModel,
5237 pass: &RedrawPass,
5238 layout: &LayoutAll,
5239 plan: ViewPlan,
5240) -> io::Result<()> {
5241 screen.render_scrolling(
5245 writer,
5246 &plan.render_lines,
5247 terminal_model.viewport_start,
5248 pass.height,
5249 (plan.cursor_row, layout.cursor_col),
5250 )?;
5251 terminal_model.reset_to_layout(layout, plan.viewport_start, plan.rubber_height);
5252 Ok(())
5253}
5254
5255fn render_diff_frame(
5256 writer: &mut BufWriter<Box<dyn Write + Send>>,
5257 screen: &mut Screen,
5258 terminal_model: &mut TerminalModel,
5259 pass: &RedrawPass,
5260 layout: &LayoutAll,
5261 plan: ViewPlan,
5262) -> io::Result<()> {
5263 let visible = plan.visible_lines(pass.height);
5267 let cursor_in_visible = plan.cursor_in_visible(pass.height);
5268 screen.update(writer, visible, (cursor_in_visible, layout.cursor_col))?;
5269 terminal_model.reset_to_layout(layout, plan.viewport_start, plan.rubber_height);
5270 Ok(())
5271}
5272
5273fn reset_model_after_rendered_full_frame(
5274 terminal_model: &mut TerminalModel,
5275 pass: &RedrawPass,
5276 layout: &LayoutAll,
5277 plan: ViewPlan,
5278) {
5279 let viewport_start =
5280 full_render_effective_viewport_start(layout, &plan, pass.height, pass.redraw_history_size);
5281 terminal_model.reset_to_layout(layout, viewport_start, plan.rubber_height);
5282}
5283
5284fn complete_redraw_sync(
5285 state: &Arc<Mutex<SharedState>>,
5286 sync_gen: RedrawSyncGeneration,
5287 sync_condvar: &std::sync::Condvar,
5288) {
5289 {
5293 let mut st = state.lock().expect("term state mutex poisoned");
5294 st.terminal.sync_completed = st.terminal.sync_completed.max(sync_gen);
5295 }
5296 sync_condvar.notify_all();
5297}
5298
5299fn fail_terminal_output(
5302 state: &Arc<Mutex<SharedState>>,
5303 input_tx: &path_std_sync::mpsc::Sender<InputMessage>,
5304 sync_condvar: &std::sync::Condvar,
5305 error: io::Error,
5306) {
5307 let mut st = state.lock().expect("term state mutex poisoned");
5308 if st.terminal.output_failure.is_none() {
5309 tracing::error!(
5310 target: "tau_cli_term_raw::redraw",
5311 error = %error,
5312 "terminal output failed; stopping attachment renderer"
5313 );
5314 st.terminal.output_failure = Some(OutputFailure::new(error));
5315 }
5316 st.terminal.input_shutdown = true;
5317 st.terminal.sync_completed = st.terminal.sync_requested;
5318 drop(st);
5319 sync_condvar.notify_all();
5320 let _ = input_tx.send(InputMessage::Shutdown);
5321}
5322
5323fn changed_line_in_range(
5324 prev_all_lines: &[CellRow],
5325 all_lines: &[CellRow],
5326 range: std::ops::Range<usize>,
5327) -> Option<usize> {
5328 range
5329 .into_iter()
5330 .find(|idx| prev_all_lines.get(*idx) != all_lines.get(*idx))
5331}
5332
5333fn mark_full_render(state: &Arc<Mutex<SharedState>>, layout: &LayoutAll, mark: FullRenderMark) {
5334 let full_render_count = {
5335 let mut st = state.lock().expect("term state mutex poisoned");
5336 st.terminal.full_render_count += 1;
5337 st.terminal.full_render_count
5338 };
5339 let current_source = mark
5340 .changed_line
5341 .and_then(|idx| layout.line_sources.get(idx))
5342 .cloned();
5343 let previous = describe_line_source(mark.previous_source.as_ref());
5344 let current = describe_line_source(current_source.as_ref());
5345 tracing::info!(
5346 target: "tau_cli_term_raw::redraw",
5347 full_render_count,
5348 reason = mark.reason,
5349 prev_visible_start = mark.prev_visible_start,
5350 visible_start = mark.visible_start,
5351 height = mark.height,
5352 total_lines = layout.all_lines.len(),
5353 changed_line = mark.changed_line,
5354 previous_source = ?mark.previous_source,
5355 current_source = ?current_source,
5356 "full redraw caused by {}: {previous} -> {current}", mark.reason
5357 );
5358 tracing::trace!(
5359 target: "tau_cli_term_raw::redraw",
5360 full_render_count,
5361 reason = mark.reason,
5362 prev_visible_start = mark.prev_visible_start,
5363 visible_start = mark.visible_start,
5364 height = mark.height,
5365 total_lines = layout.all_lines.len(),
5366 changed_line = mark.changed_line,
5367 previous_source = ?mark.previous_source,
5368 current_source = ?current_source,
5369 "full render"
5370 );
5371}
5372
5373fn describe_line_source(source: Option<&LineSource>) -> String {
5374 match source {
5375 Some(LineSource::Block {
5376 id,
5377 debug_id,
5378 wrapped_row,
5379 }) => format!("block {:?} `{}` row {}", id, debug_id, wrapped_row),
5380 Some(LineSource::Input { wrapped_row }) => format!("input row {wrapped_row}"),
5381 Some(LineSource::InputScrollIndicator) => "input scroll indicator".to_owned(),
5382 None => "<missing>".to_owned(),
5383 }
5384}
5385
5386fn viewport_start_with_cursor(
5387 viewport_start: usize,
5388 cursor_row: usize,
5389 total_rows: usize,
5390 height: usize,
5391) -> usize {
5392 let height = height.max(1);
5393 let max_start = total_rows.saturating_sub(height);
5394 let mut start = viewport_start.min(max_start);
5395
5396 if cursor_row < start {
5397 start = cursor_row;
5398 } else if start + height <= cursor_row {
5399 start = (cursor_row + 1).saturating_sub(height);
5400 }
5401
5402 start.min(max_start)
5403}
5404
5405fn hidden_lines_changed(
5406 prev_all_lines: &[CellRow],
5407 all_lines: &[CellRow],
5408 prev_visible_start: usize,
5409) -> bool {
5410 (0..prev_visible_start).any(|idx| prev_all_lines.get(idx) != all_lines.get(idx))
5411}
5412
5413fn full_render_replay_start(
5414 layout: &LayoutAll,
5415 plan: &ViewPlan,
5416 redraw_history_size: usize,
5417) -> usize {
5418 let total = plan.render_lines.len();
5419 let log_end = layout.log_end.min(total);
5420 log_end.saturating_sub(redraw_history_size)
5421}
5422
5423fn full_render_effective_viewport_start(
5424 layout: &LayoutAll,
5425 plan: &ViewPlan,
5426 height: usize,
5427 redraw_history_size: usize,
5428) -> usize {
5429 let replay_start = full_render_replay_start(layout, plan, redraw_history_size);
5430 let replay_len = plan.render_lines.len().saturating_sub(replay_start);
5431 if height < replay_len {
5432 plan.render_lines.len().saturating_sub(height)
5433 } else {
5434 replay_start
5435 }
5436}
5437
5438fn full_render(
5445 stdout: &mut impl Write,
5446 screen: &mut Screen,
5447 layout: &LayoutAll,
5448 plan: &ViewPlan,
5449 width: usize,
5450 height: usize,
5451 redraw_history_size: usize,
5452) -> io::Result<()> {
5453 screen.set_width(width);
5454
5455 let all_lines = &plan.render_lines;
5456 let replay_start = full_render_replay_start(layout, plan, redraw_history_size);
5457 let replay_lines = &all_lines[replay_start..];
5458 let replay_total = replay_lines.len();
5459 let effective_viewport_start =
5460 full_render_effective_viewport_start(layout, plan, height, redraw_history_size);
5461
5462 with_synchronized_update(stdout, |stdout| {
5463 stdout.queue(Print("\x1b[2J\x1b[H\x1b[3J\x1b[?7l"))?;
5469
5470 for (i, line) in replay_lines.iter().enumerate() {
5475 if 0 < i {
5476 stdout.queue(Print("\r\n"))?;
5477 }
5478 emit_styled_cells(stdout, line)?;
5479 }
5480
5481 stdout.queue(Print("\x1b[?7h"))?;
5482
5483 let current_screen_row = if height <= replay_total {
5487 height - 1
5488 } else {
5489 replay_total.saturating_sub(1)
5490 };
5491
5492 let cursor_screen_row = plan.cursor_row.saturating_sub(effective_viewport_start);
5493
5494 let up = current_screen_row.saturating_sub(cursor_screen_row);
5495 if 0 < up {
5496 stdout.queue(MoveUp(up as u16))?;
5497 }
5498 stdout.queue(MoveToColumn(layout.cursor_col as u16))?;
5499 Ok(())
5500 })?;
5501
5502 let visible_end = (effective_viewport_start + height).min(plan.render_lines.len());
5505 let visible_lines = plan.render_lines[effective_viewport_start..visible_end].to_vec();
5506 let cursor_in_visible = plan.cursor_row.saturating_sub(effective_viewport_start);
5507 screen.reset_to(visible_lines, cursor_in_visible, layout.cursor_col);
5508
5509 Ok(())
5510}
5511
5512fn with_synchronized_update<W, F>(writer: &mut W, body: F) -> io::Result<()>
5518where
5519 W: Write,
5520 F: FnOnce(&mut W) -> io::Result<()>,
5521{
5522 writer.queue(terminal::BeginSynchronizedUpdate)?;
5523 let body_result = body(writer);
5524 let end_result = writer.queue(terminal::EndSynchronizedUpdate).map(|_| ());
5525 body_result.and(end_result)
5526}
5527
5528fn move_cursor_vertical(st: &SharedState, delta: isize, target_col: usize) -> Option<usize> {
5531 let width = st.terminal.width.max(1);
5532 let left_cols = st.editor.left_prompt.char_count();
5533 let (current_row, _) =
5534 buffer_position_for_byte(&st.editor.buffer, st.editor.cursor, width, left_cols);
5535
5536 let target_row = current_row as isize + delta;
5537 if target_row < 0 {
5538 return None;
5539 }
5540 let target_row = target_row as usize;
5541
5542 let (max_row, _) = buffer_end_position(&st.editor.buffer, width, left_cols);
5543 if max_row < target_row {
5544 return None;
5545 }
5546
5547 Some(byte_offset_for_buffer_position(
5548 &st.editor.buffer,
5549 target_row,
5550 target_col,
5551 width,
5552 left_cols,
5553 ))
5554}
5555
5556fn term_size() -> (usize, usize) {
5557 raw_term_size()
5558 .map(|(w, h)| (usize::from(w).max(1), usize::from(h).max(1)))
5559 .unwrap_or((80, 24))
5560}
5561
5562fn raw_term_size() -> io::Result<(u16, u16)> {
5563 terminal::size()
5564}
5565
5566fn resample_resize_dimension(reported: u16, actual: u16) -> u16 {
5567 if 0 < reported { reported } else { actual }
5568}
5569
5570fn effective_resize_dimension(reported: u16, fallback: usize) -> usize {
5571 let reported = usize::from(reported);
5572 if 0 < reported {
5573 reported
5574 } else {
5575 fallback.max(1)
5576 }
5577}
5578
5579fn size_event_dimension(value: usize) -> u16 {
5580 u16::try_from(value).unwrap_or(u16::MAX)
5581}
5582
5583fn normalize_paste_text(text: String) -> String {
5584 if !text.contains('\r') {
5585 return text;
5586 }
5587
5588 let mut normalized = String::with_capacity(text.len());
5589 let mut chars = text.chars().peekable();
5590 while let Some(ch) = chars.next() {
5591 if ch == '\r' {
5592 if chars.peek() == Some(&'\n') {
5593 chars.next();
5594 }
5595 normalized.push('\n');
5596 } else {
5597 normalized.push(ch);
5598 }
5599 }
5600 normalized
5601}
5602
5603fn is_prompt_line_break(grapheme: &str) -> bool {
5604 matches!(grapheme, "\n" | "\r\n" | "\r")
5605}
5606
5607fn initial_buffer_position(initial_cols: usize, width: usize) -> (usize, usize) {
5608 let width = width.max(1);
5609 (initial_cols / width, initial_cols % width)
5610}
5611
5612fn buffer_position_for_byte(
5613 s: &str,
5614 byte_pos: usize,
5615 width: usize,
5616 initial_cols: usize,
5617) -> (usize, usize) {
5618 let width = width.max(1);
5619 let mut pos = initial_buffer_position(initial_cols, width);
5620 let mut pending_exact_wrap = false;
5621
5622 for (byte, grapheme) in UnicodeSegmentation::grapheme_indices(s, true) {
5623 if byte_pos <= byte || byte_pos < byte + grapheme.len() {
5624 break;
5625 }
5626 advance_prompt_cursor_position(
5627 &mut pos.0,
5628 &mut pos.1,
5629 &mut pending_exact_wrap,
5630 grapheme,
5631 width,
5632 );
5633 }
5634
5635 pos
5636}
5637
5638fn advance_prompt_cursor_position(
5639 row: &mut usize,
5640 col: &mut usize,
5641 pending_exact_wrap: &mut bool,
5642 grapheme: &str,
5643 width: usize,
5644) {
5645 let width = width.max(1);
5646 if is_prompt_line_break(grapheme) {
5647 if *pending_exact_wrap {
5648 *pending_exact_wrap = false;
5653 } else {
5654 *row += 1;
5655 *col = 0;
5656 }
5657 return;
5658 }
5659
5660 *pending_exact_wrap = false;
5661 let grapheme_width = display_width(grapheme);
5662 if 0 < *col && width < *col + grapheme_width {
5663 *row += 1;
5664 *col = 0;
5665 }
5666 *col += grapheme_width;
5667 if width <= *col {
5668 *row += *col / width;
5669 *col %= width;
5670 *pending_exact_wrap = grapheme_width != 0 && *col == 0;
5671 }
5672}
5673
5674fn buffer_end_position(s: &str, width: usize, initial_cols: usize) -> (usize, usize) {
5675 buffer_position_for_byte(s, s.len(), width, initial_cols)
5676}
5677
5678fn byte_offset_for_buffer_position(
5679 s: &str,
5680 target_row: usize,
5681 target_col: usize,
5682 width: usize,
5683 initial_cols: usize,
5684) -> usize {
5685 let mut row_col = initial_buffer_position(initial_cols, width);
5686 let mut pending_exact_wrap = false;
5687
5688 for (byte, grapheme) in UnicodeSegmentation::grapheme_indices(s, true) {
5689 let (row, col) = row_col;
5690 if target_row < row || (target_row == row && target_col <= col) {
5691 return byte;
5692 }
5693 if is_prompt_line_break(grapheme) && !pending_exact_wrap && target_row == row {
5694 return byte;
5695 }
5696
5697 let mut next = row_col;
5698 let mut next_pending_exact_wrap = pending_exact_wrap;
5699 advance_prompt_cursor_position(
5700 &mut next.0,
5701 &mut next.1,
5702 &mut next_pending_exact_wrap,
5703 grapheme,
5704 width,
5705 );
5706 if !is_prompt_line_break(grapheme)
5707 && (target_row < next.0 || (target_row == next.0 && target_col <= next.1))
5708 {
5709 return byte + grapheme.len();
5710 }
5711 row_col = next;
5712 pending_exact_wrap = next_pending_exact_wrap;
5713 }
5714
5715 s.len()
5716}
5717
5718fn clamp_cursor_to_grapheme_boundary(s: &str, cursor: usize) -> usize {
5719 let cursor = cursor.min(s.len());
5720 if cursor == s.len() {
5721 return cursor;
5722 }
5723
5724 let mut boundary = 0;
5725 for (idx, _) in UnicodeSegmentation::grapheme_indices(s, true) {
5726 if cursor < idx {
5727 break;
5728 }
5729 boundary = idx;
5730 }
5731 boundary
5732}
5733
5734fn prev_char_boundary(s: &str, pos: usize) -> usize {
5735 previous_grapheme_boundary(s, pos)
5736}
5737
5738fn next_char_boundary(s: &str, pos: usize) -> usize {
5739 next_grapheme_boundary(s, pos)
5740}
5741
5742#[cfg(test)]
5743mod tests;