Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 3 additions & 48 deletions crates/tui/src/tools/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ use windows::core::PCWSTR;
#[cfg(not(target_env = "ohos"))]
use portable_pty::{CommandBuilder, PtySize, native_pty_system};

mod output;

use super::shell_output::{summarize_output, truncate_with_meta};
use crate::child_env;
use crate::sandbox::{
Expand All @@ -47,6 +49,7 @@ use crate::sandbox::{
SandboxType,
};
use crate::worker_profile::ShellPolicy;
use output::{tail_from_buffer, take_delta_from_buffer};

/// Status of a shell process
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down Expand Up @@ -1854,54 +1857,6 @@ impl ShellManager {
}
}

fn take_delta_from_buffer(buffer: &Arc<Mutex<Vec<u8>>>, cursor: &mut usize) -> (Vec<u8>, usize) {
let guard = buffer.lock().unwrap_or_else(|e| e.into_inner());
let total = guard.len();
let start = (*cursor).min(total);
// Clone only the unread portion (the delta), not the entire accumulated buffer.
// Long-running processes can produce megabytes of output; cloning the full
// buffer on every poll held the ShellManager mutex for O(total_bytes) time.
let delta = guard[start..].to_vec();
*cursor = total;
(delta, total)
}

/// Read only the tail of a byte buffer and return (total_len, tail_string).
///
/// Avoids cloning the full buffer when only a trailing excerpt is needed
/// (e.g. for the job-panel display). `max_tail_chars` is in Unicode scalar
/// values; we read at most `max_tail_chars * 4` bytes from the end to account
/// for multi-byte UTF-8 sequences.
fn tail_from_buffer(buffer: &Arc<Mutex<Vec<u8>>>, max_tail_chars: usize) -> (usize, String) {
let guard = buffer.lock().unwrap_or_else(|e| e.into_inner());
let total = guard.len();
// Over-estimate byte count (4 bytes per char worst case for UTF-8).
let mut tail_start = total.saturating_sub(max_tail_chars.saturating_mul(4));
// Snap forward to the next valid UTF-8 codepoint boundary so we don't
// pass a slice beginning with continuation bytes (0x80–0xBF) to
// from_utf8_lossy, which would emit a leading U+FFFD replacement char.
while tail_start < total && (guard[tail_start] & 0xC0) == 0x80 {
tail_start += 1;
}
let tail_str = String::from_utf8_lossy(&guard[tail_start..]).into_owned();
(total, tail_text(&tail_str, max_tail_chars))
}

fn tail_text(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
let tail = text
.chars()
.rev()
.take(max_chars)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<String>();
format!("...{tail}")
}

fn job_status_rank(status: &ShellStatus, stale: bool) -> u8 {
if stale {
return 4;
Expand Down
55 changes: 55 additions & 0 deletions crates/tui/src/tools/shell/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::sync::{Arc, Mutex};

pub(super) fn take_delta_from_buffer(
buffer: &Arc<Mutex<Vec<u8>>>,
cursor: &mut usize,
) -> (Vec<u8>, usize) {
let guard = buffer.lock().unwrap_or_else(|e| e.into_inner());
let total = guard.len();
let start = (*cursor).min(total);
// Clone only the unread portion (the delta), not the entire accumulated buffer.
// Long-running processes can produce megabytes of output; cloning the full
// buffer on every poll held the ShellManager mutex for O(total_bytes) time.
let delta = guard[start..].to_vec();
*cursor = total;
(delta, total)
}

/// Read only the tail of a byte buffer and return (total_len, tail_string).
///
/// Avoids cloning the full buffer when only a trailing excerpt is needed
/// (e.g. for the job-panel display). `max_tail_chars` is in Unicode scalar
/// values; we read at most `max_tail_chars * 4` bytes from the end to account
/// for multi-byte UTF-8 sequences.
pub(super) fn tail_from_buffer(
buffer: &Arc<Mutex<Vec<u8>>>,
max_tail_chars: usize,
) -> (usize, String) {
let guard = buffer.lock().unwrap_or_else(|e| e.into_inner());
let total = guard.len();
// Over-estimate byte count (4 bytes per char worst case for UTF-8).
let mut tail_start = total.saturating_sub(max_tail_chars.saturating_mul(4));
// Snap forward to the next valid UTF-8 codepoint boundary so we don't
// pass a slice beginning with continuation bytes (0x80-0xBF) to
// from_utf8_lossy, which would emit a leading U+FFFD replacement char.
while tail_start < total && (guard[tail_start] & 0xC0) == 0x80 {
tail_start += 1;
}
let tail_str = String::from_utf8_lossy(&guard[tail_start..]).into_owned();
(total, tail_text(&tail_str, max_tail_chars))
}

fn tail_text(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
let tail = text
.chars()
.rev()
.take(max_chars)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<String>();
format!("...{tail}")
}
Loading