use super::*;
pub(crate) fn next_terminal_event(
input: &TerminalInputPump,
pending: &mut VecDeque<Event>,
timeout: Duration,
) -> io::Result<Option<Event>> {
if let Some(event) = pending.pop_front() {
return Ok(Some(event));
}
input.recv_timeout(timeout)
}
pub(crate) fn try_next_terminal_event(
input: &TerminalInputPump,
pending: &mut VecDeque<Event>,
) -> io::Result<Option<Event>> {
if let Some(event) = pending.pop_front() {
return Ok(Some(event));
}
input.try_recv()
}
pub(crate) fn drain_terminal_input_queue(
input: &TerminalInputPump,
pending: &mut VecDeque<Event>,
) -> io::Result<()> {
pending.clear();
while input.try_recv()?.is_some() {}
Ok(())
}
pub(crate) fn collect_pending_terminal_events(
input: &TerminalInputPump,
pending: &mut VecDeque<Event>,
) -> io::Result<()> {
while let Some(event) = input.try_recv()? {
pending.push_back(event);
}
Ok(())
}
pub(crate) fn require_interactive_terminal(stdin_is_tty: bool, stdout_is_tty: bool) -> Result<()> {
if stdin_is_tty && stdout_is_tty {
return Ok(());
}
Err(anyhow::anyhow!(
"Codewhale TUI requires an interactive terminal (stdin and stdout must be a TTY).\n\
Open a real terminal (Terminal.app, iTerm, Windows Terminal, …) and run `codew` \
or `codewhale` there — not from a pipe, cron job, or non-TTY launcher.\n\
For headless prompts use `codewhale exec \"…\"` instead."
))
}
pub(crate) fn raw_mode_probe_handshake(publish: &AtomicBool, check: &AtomicBool) -> bool {
publish.store(true, Ordering::SeqCst);
check.load(Ordering::SeqCst)
}
pub(crate) fn terminal_probe_timeout(config: &Config) -> Duration {
let timeout_ms = config
.tui
.as_ref()
.and_then(|tui| tui.terminal_probe_timeout_ms)
.unwrap_or(DEFAULT_TERMINAL_PROBE_TIMEOUT_MS)
.clamp(100, 5_000);
Duration::from_millis(timeout_ms)
}
pub(crate) fn subagent_terminal_verb(status: &SubAgentStatus) -> &'static str {
match status {
SubAgentStatus::Completed => "completed",
SubAgentStatus::Interrupted(_) => "interrupted",
SubAgentStatus::Failed(_) => "failed",
SubAgentStatus::Cancelled => "cancelled",
SubAgentStatus::BudgetExhausted => "exhausted its budget",
SubAgentStatus::Running => "finished",
}
}
pub(crate) fn subagent_terminal_projection_from_mailbox(
message: &MailboxMessage,
) -> Option<(&str, SubAgentStatus, Option<String>)> {
match message {
MailboxMessage::Completed { agent_id, summary } => Some((
agent_id.as_str(),
SubAgentStatus::Completed,
Some(summary.clone()),
)),
MailboxMessage::Failed { agent_id, error } => Some((
agent_id.as_str(),
SubAgentStatus::Failed(error.clone()),
Some(error.clone()),
)),
MailboxMessage::Interrupted { agent_id, reason } => Some((
agent_id.as_str(),
SubAgentStatus::Interrupted(reason.clone()),
Some(reason.clone()),
)),
MailboxMessage::Cancelled { agent_id } => Some((
agent_id.as_str(),
SubAgentStatus::Cancelled,
Some("cancelled".to_string()),
)),
_ => None,
}
}
pub(crate) fn terminal_input_recovery_relevant(app: &App, has_running_agents: bool) -> bool {
app.is_loading
|| has_running_agents
|| app.is_compacting
|| app.is_purging
|| matches!(app.runtime_turn_status.as_deref(), Some("in_progress"))
|| active_turn_has_running_tool(app)
}
pub(crate) fn pause_terminal(
terminal: &mut AppTerminal,
use_alt_screen: bool,
use_mouse_capture: bool,
use_bracketed_paste: bool,
) -> Result<()> {
pop_keyboard_enhancement_flags(terminal.backend_mut());
disable_alternate_scroll_mode(terminal.backend_mut());
execute!(terminal.backend_mut(), DisableFocusChange)?;
disable_raw_mode()?;
if use_alt_screen {
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
#[cfg(windows)]
crate::logging::restore_verbose_state();
}
if use_mouse_capture {
execute!(terminal.backend_mut(), DisableMouseCapture)?;
}
if use_bracketed_paste {
disable_bracketed_paste_mode(terminal.backend_mut());
}
Ok(())
}
pub(crate) fn resume_terminal(
terminal: &mut AppTerminal,
use_alt_screen: bool,
use_mouse_capture: bool,
use_bracketed_paste: bool,
sync_output_enabled: bool,
) -> Result<()> {
enable_raw_mode()?;
if use_alt_screen {
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
#[cfg(windows)]
crate::logging::set_verbose(false);
}
recover_terminal_modes(
terminal.backend_mut(),
use_mouse_capture,
use_bracketed_paste,
);
if let Ok((cols, rows)) = crossterm::terminal::size() {
terminal
.backend_mut()
.set_terminal_size(Size::new(cols, rows));
}
reset_terminal_viewport(terminal, sync_output_enabled)?;
Ok(())
}
pub(crate) fn reset_terminal_viewport(
terminal: &mut AppTerminal,
sync_output_enabled: bool,
) -> Result<()> {
if sync_output_enabled {
let _ = terminal.backend_mut().write_all(BEGIN_SYNC_UPDATE);
}
let result = (|| -> Result<()> {
terminal.backend_mut().write_all(TERMINAL_ORIGIN_RESET)?;
terminal.clear()?;
Ok(())
})();
if sync_output_enabled {
let _ = terminal.backend_mut().write_all(END_SYNC_UPDATE);
}
let _ = terminal.backend_mut().flush();
result
}
pub(crate) fn push_keyboard_enhancement_flags<W: Write>(writer: &mut W) {
#[cfg(windows)]
{
if let Err(err) = write!(writer, "\x1b[>0u").and_then(|()| writer.flush()) {
tracing::debug!(
target: "kitty_keyboard",
?err,
"PushKeyboardEnhancementFlags direct write failed on Windows"
);
}
}
#[cfg(not(windows))]
if let Err(err) = execute!(
writer,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
) {
tracing::debug!(
target: "kitty_keyboard",
?err,
"PushKeyboardEnhancementFlags ignored (terminal lacks support)"
);
}
}
pub(crate) fn pop_keyboard_enhancement_flags<W: Write>(writer: &mut W) {
#[cfg(windows)]
{
if let Err(err) = write!(writer, "\x1b[<1u").and_then(|()| writer.flush()) {
tracing::debug!(
target: "kitty_keyboard",
?err,
"PopKeyboardEnhancementFlags direct write failed on Windows"
);
}
}
#[cfg(not(windows))]
let _ = execute!(writer, PopKeyboardEnhancementFlags);
}
pub(crate) fn set_alternate_scroll_mode<W: Write>(writer: &mut W, enabled: bool) {
let sequence = if enabled {
ENABLE_ALT_SCROLL_MODE
} else {
DISABLE_ALT_SCROLL_MODE
};
if let Err(err) = writer.write_all(sequence).and_then(|()| writer.flush()) {
tracing::debug!(
?err,
enabled,
"alternate-scroll terminal mode change ignored"
);
}
}
pub(crate) fn disable_alternate_scroll_mode<W: Write>(writer: &mut W) {
set_alternate_scroll_mode(writer, false);
}
pub fn emergency_restore_terminal() {
let mut stdout = std::io::stdout();
pop_keyboard_enhancement_flags(&mut stdout);
disable_alternate_scroll_mode(&mut stdout);
let _ = execute!(stdout, DisableFocusChange);
disable_bracketed_paste_mode(&mut stdout);
let _ = execute!(stdout, DisableMouseCapture);
let _ = disable_raw_mode();
let _ = execute!(stdout, LeaveAlternateScreen);
}
#[cfg(target_os = "windows")]
pub(crate) fn enable_windows_ime_console_mode() {
use windows::Win32::System::Console::CONSOLE_MODE;
const ENABLE_WINDOW_INPUT: CONSOLE_MODE = CONSOLE_MODE(0x0008);
unsafe {
let Ok(handle) = GetStdHandle(windows::Win32::System::Console::STD_INPUT_HANDLE) else {
return;
};
let mut mode = CONSOLE_MODE(0);
if GetConsoleMode(handle, &mut mode).is_err() {
return;
}
if mode.0 & ENABLE_WINDOW_INPUT.0 == 0 {
let _ = SetConsoleMode(handle, mode | ENABLE_WINDOW_INPUT);
}
}
}
pub(crate) fn recover_terminal_modes<W: Write>(
writer: &mut W,
use_mouse_capture: bool,
use_bracketed_paste: bool,
) {
#[cfg(target_os = "windows")]
enable_windows_ime_console_mode();
pop_keyboard_enhancement_flags(writer);
push_keyboard_enhancement_flags(writer);
disable_alternate_scroll_mode(writer);
if use_mouse_capture && let Err(err) = execute!(writer, EnableMouseCapture) {
tracing::debug!(?err, "EnableMouseCapture ignored");
}
if use_bracketed_paste {
try_enable_bracketed_paste_mode(writer);
}
if let Err(err) = execute!(writer, EnableFocusChange) {
tracing::debug!(?err, "EnableFocusChange ignored");
}
}
pub(crate) fn try_enable_bracketed_paste_mode<W: Write>(writer: &mut W) -> bool {
match execute!(writer, EnableBracketedPaste) {
Ok(()) => true,
Err(err) => {
tracing::debug!(?err, "EnableBracketedPaste ignored");
false
}
}
}
pub(crate) fn disable_bracketed_paste_mode<W: Write>(writer: &mut W) {
if let Err(err) = execute!(writer, DisableBracketedPaste) {
tracing::debug!(?err, "DisableBracketedPaste ignored");
}
}
pub(crate) fn terminal_event_needs_viewport_recapture(evt: &Event) -> bool {
matches!(evt, Event::FocusGained)
}
pub(crate) fn terminal_pause_has_live_owner(app: &App) -> bool {
app.active_cell.as_ref().is_some_and(|active| {
active.entries().iter().any(|cell| {
matches!(
cell,
HistoryCell::Tool(ToolCell::Exec(exec)) if exec.status == ToolStatus::Running
)
})
})
}
pub(crate) fn active_poll_ms(app: &App) -> u64 {
if app.low_motion {
96
} else {
UI_ACTIVE_POLL_MS
}
}
pub(crate) fn idle_poll_ms(app: &App) -> u64 {
if app.low_motion { 120 } else { UI_IDLE_POLL_MS }
}