refactor(tui): extract paste-burst handlers into tui/paste.rs (P1.2)
Lifts `handle_paste_burst_key`, `handle_paste_burst_decision`, `apply_paste_burst_retro_capture`, and the local `in_command_context` helper out of `tui/ui.rs` into a sibling module. The state machine (`PasteBurst`) and its tests stay in `paste_burst.rs`; only the keymap- side wiring moves. Drops the now-unused `CharDecision` import from `ui.rs`. Workspace tests: 1011/1011 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod markdown_render;
|
|||||||
pub mod model_picker;
|
pub mod model_picker;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod pager;
|
pub mod pager;
|
||||||
|
pub mod paste;
|
||||||
pub mod paste_burst;
|
pub mod paste_burst;
|
||||||
pub mod plan_prompt;
|
pub mod plan_prompt;
|
||||||
pub mod scrolling;
|
pub mod scrolling;
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
//! Paste-burst handling — turn rapid keystrokes (terminals without bracketed
|
||||||
|
//! paste) into a single committed buffer instead of N individual chars.
|
||||||
|
//!
|
||||||
|
//! Extracted from `tui/ui.rs` (P1.2). The owning state machine lives on
|
||||||
|
//! `App.paste_burst` (`tui::paste_burst`); these helpers wire it to the key
|
||||||
|
//! event loop and the composer's text buffer.
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
use super::app::App;
|
||||||
|
use super::paste_burst::CharDecision;
|
||||||
|
|
||||||
|
/// Process a key in the context of paste-burst detection. Returns `true`
|
||||||
|
/// when the key was fully handled by the paste machinery (caller skips
|
||||||
|
/// further input handling); `false` when the key still needs the normal
|
||||||
|
/// composer path.
|
||||||
|
pub fn handle_paste_burst_key(app: &mut App, key: &KeyEvent, now: Instant) -> bool {
|
||||||
|
let has_ctrl_alt_or_super = key.modifiers.contains(KeyModifiers::CONTROL)
|
||||||
|
|| key.modifiers.contains(KeyModifiers::ALT)
|
||||||
|
|| key.modifiers.contains(KeyModifiers::SUPER);
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if !in_command_context(app) && app.paste_burst.append_newline_if_active(now) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if !in_command_context(app)
|
||||||
|
&& app.paste_burst.newline_should_insert_instead_of_submit(now)
|
||||||
|
{
|
||||||
|
app.insert_char('\n');
|
||||||
|
app.paste_burst.extend_window(now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if !has_ctrl_alt_or_super => {
|
||||||
|
if !c.is_ascii() {
|
||||||
|
if let Some(pending) = app.paste_burst.flush_before_modified_input() {
|
||||||
|
app.insert_str(&pending);
|
||||||
|
}
|
||||||
|
if app.paste_burst.try_append_char_if_active(c, now) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if let Some(decision) = app.paste_burst.on_plain_char_no_hold(now) {
|
||||||
|
return handle_paste_burst_decision(app, decision, c, now);
|
||||||
|
}
|
||||||
|
app.insert_char(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decision = app.paste_burst.on_plain_char(c, now);
|
||||||
|
return handle_paste_burst_decision(app, decision, c, now);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a paste-burst decision to the composer buffer. Some decisions
|
||||||
|
/// retroactively grab the last few chars from the input back into the
|
||||||
|
/// pending paste buffer (when the heuristic decides the recent typing was
|
||||||
|
/// actually a paste).
|
||||||
|
pub fn handle_paste_burst_decision(
|
||||||
|
app: &mut App,
|
||||||
|
decision: CharDecision,
|
||||||
|
c: char,
|
||||||
|
now: Instant,
|
||||||
|
) -> bool {
|
||||||
|
match decision {
|
||||||
|
CharDecision::RetainFirstChar => true,
|
||||||
|
CharDecision::BeginBufferFromPending | CharDecision::BufferAppend => {
|
||||||
|
app.paste_burst.append_char_to_buffer(c, now);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
CharDecision::BeginBuffer { retro_chars } => {
|
||||||
|
if apply_paste_burst_retro_capture(app, retro_chars as usize, c, now) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
app.insert_char(c);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_paste_burst_retro_capture(
|
||||||
|
app: &mut App,
|
||||||
|
retro_chars: usize,
|
||||||
|
c: char,
|
||||||
|
now: Instant,
|
||||||
|
) -> bool {
|
||||||
|
let cursor_byte = app.cursor_byte_index();
|
||||||
|
let before = &app.input[..cursor_byte];
|
||||||
|
let Some(grab) = app
|
||||||
|
.paste_burst
|
||||||
|
.decide_begin_buffer(now, before, retro_chars)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !grab.grabbed.is_empty() {
|
||||||
|
app.input.replace_range(grab.start_byte..cursor_byte, "");
|
||||||
|
let removed = grab.grabbed.chars().count();
|
||||||
|
app.cursor_position = app.cursor_position.saturating_sub(removed);
|
||||||
|
}
|
||||||
|
app.paste_burst.append_char_to_buffer(c, now);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_command_context(app: &App) -> bool {
|
||||||
|
app.input.starts_with('/')
|
||||||
|
}
|
||||||
@@ -55,7 +55,6 @@ use crate::tui::command_palette::{
|
|||||||
use crate::tui::event_broker::EventBroker;
|
use crate::tui::event_broker::EventBroker;
|
||||||
use crate::tui::onboarding;
|
use crate::tui::onboarding;
|
||||||
use crate::tui::pager::PagerView;
|
use crate::tui::pager::PagerView;
|
||||||
use crate::tui::paste_burst::CharDecision;
|
|
||||||
use crate::tui::plan_prompt::PlanPromptView;
|
use crate::tui::plan_prompt::PlanPromptView;
|
||||||
use crate::tui::scrolling::{ScrollDirection, TranscriptScroll};
|
use crate::tui::scrolling::{ScrollDirection, TranscriptScroll};
|
||||||
use crate::tui::selection::TranscriptSelectionPoint;
|
use crate::tui::selection::TranscriptSelectionPoint;
|
||||||
@@ -1119,7 +1118,7 @@ async fn run_event_loop(
|
|||||||
app.insert_str(&pending);
|
app.insert_str(&pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_plain_char || is_enter) && handle_paste_burst_key(app, &key, now) {
|
if (is_plain_char || is_enter) && super::paste::handle_paste_burst_key(app, &key, now) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1523,48 +1522,6 @@ async fn run_event_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_paste_burst_key(app: &mut App, key: &KeyEvent, now: Instant) -> bool {
|
|
||||||
let has_ctrl_alt_or_super = key.modifiers.contains(KeyModifiers::CONTROL)
|
|
||||||
|| key.modifiers.contains(KeyModifiers::ALT)
|
|
||||||
|| key.modifiers.contains(KeyModifiers::SUPER);
|
|
||||||
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if !in_command_context(app) && app.paste_burst.append_newline_if_active(now) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if !in_command_context(app)
|
|
||||||
&& app.paste_burst.newline_should_insert_instead_of_submit(now)
|
|
||||||
{
|
|
||||||
app.insert_char('\n');
|
|
||||||
app.paste_burst.extend_window(now);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) if !has_ctrl_alt_or_super => {
|
|
||||||
if !c.is_ascii() {
|
|
||||||
if let Some(pending) = app.paste_burst.flush_before_modified_input() {
|
|
||||||
app.insert_str(&pending);
|
|
||||||
}
|
|
||||||
if app.paste_burst.try_append_char_if_active(c, now) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if let Some(decision) = app.paste_burst.on_plain_char_no_hold(now) {
|
|
||||||
return handle_paste_burst_decision(app, decision, c, now);
|
|
||||||
}
|
|
||||||
app.insert_char(c);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let decision = app.paste_burst.on_plain_char(c, now);
|
|
||||||
return handle_paste_burst_decision(app, decision, c, now);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_alt_4_shortcut(app: &mut App, modifiers: KeyModifiers) {
|
fn apply_alt_4_shortcut(app: &mut App, modifiers: KeyModifiers) {
|
||||||
if modifiers.contains(KeyModifiers::CONTROL) {
|
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
app.set_sidebar_focus(SidebarFocus::Agents);
|
app.set_sidebar_focus(SidebarFocus::Agents);
|
||||||
@@ -1574,55 +1531,6 @@ fn apply_alt_4_shortcut(app: &mut App, modifiers: KeyModifiers) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_paste_burst_decision(
|
|
||||||
app: &mut App,
|
|
||||||
decision: CharDecision,
|
|
||||||
c: char,
|
|
||||||
now: Instant,
|
|
||||||
) -> bool {
|
|
||||||
match decision {
|
|
||||||
CharDecision::RetainFirstChar => true,
|
|
||||||
CharDecision::BeginBufferFromPending | CharDecision::BufferAppend => {
|
|
||||||
app.paste_burst.append_char_to_buffer(c, now);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
CharDecision::BeginBuffer { retro_chars } => {
|
|
||||||
if apply_paste_burst_retro_capture(app, retro_chars as usize, c, now) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
app.insert_char(c);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_paste_burst_retro_capture(
|
|
||||||
app: &mut App,
|
|
||||||
retro_chars: usize,
|
|
||||||
c: char,
|
|
||||||
now: Instant,
|
|
||||||
) -> bool {
|
|
||||||
let cursor_byte = app.cursor_byte_index();
|
|
||||||
let before = &app.input[..cursor_byte];
|
|
||||||
let Some(grab) = app
|
|
||||||
.paste_burst
|
|
||||||
.decide_begin_buffer(now, before, retro_chars)
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if !grab.grabbed.is_empty() {
|
|
||||||
app.input.replace_range(grab.start_byte..cursor_byte, "");
|
|
||||||
let removed = grab.grabbed.chars().count();
|
|
||||||
app.cursor_position = app.cursor_position.saturating_sub(removed);
|
|
||||||
}
|
|
||||||
app.paste_burst.append_char_to_buffer(c, now);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn in_command_context(app: &App) -> bool {
|
|
||||||
app.input.starts_with('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visible_slash_menu_entries(app: &App, limit: usize) -> Vec<String> {
|
fn visible_slash_menu_entries(app: &App, limit: usize) -> Vec<String> {
|
||||||
if app.slash_menu_hidden {
|
if app.slash_menu_hidden {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user