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