aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-24 04:00:13 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-24 04:00:13 +0300
commitc0fb777ed7c7950c6a2f397113c1842c2e679306 (patch)
tree56a1b58314e292433946c1eed5c025abb832595f /src/app
parente608a75159fc5cca70485d83ffb8ee681c9da385 (diff)
fix: report truncated pane reads
refs #1717
Diffstat (limited to 'src/app')
-rw-r--r--src/app/api/agents.rs6
-rw-r--r--src/app/api/panes.rs28
-rw-r--r--src/app/api_helpers.rs83
3 files changed, 83 insertions, 34 deletions
diff --git a/src/app/api/agents.rs b/src/app/api/agents.rs
index ffb9cb63..f1977ce4 100644
--- a/src/app/api/agents.rs
+++ b/src/app/api/agents.rs
@@ -117,7 +117,7 @@ impl App {
else {
return agent_not_found(id, &params.target);
};
- let text = crate::app::api_helpers::read_terminal_snapshot(
+ let snapshot = crate::app::api_helpers::read_terminal_snapshot(
pane,
params.source,
params.format,
@@ -137,9 +137,9 @@ impl App {
.unwrap(),
source: params.source,
format: params.format,
- text,
+ text: snapshot.text,
revision: 0,
- truncated: false,
+ truncated: snapshot.truncated,
},
},
)
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index 43206d1e..253fd627 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -1179,7 +1179,7 @@ impl App {
else {
return pane_not_found(id, &params.pane_id);
};
- let text = crate::app::api_helpers::read_terminal_snapshot(
+ let snapshot = crate::app::api_helpers::read_terminal_snapshot(
pane,
params.source,
params.format,
@@ -1195,9 +1195,9 @@ impl App {
tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(),
source: params.source,
format: params.format,
- text,
+ text: snapshot.text,
revision: 0,
- truncated: false,
+ truncated: snapshot.truncated,
},
},
)
@@ -2002,6 +2002,28 @@ mod tests {
}
#[tokio::test]
+ async fn api_pane_read_reports_when_older_rows_are_omitted() {
+ let (mut app, public_pane_id, _pane_id) = app_with_scrollback_runtime();
+
+ let response = app.handle_pane_read(
+ "req".into(),
+ PaneReadParams {
+ pane_id: public_pane_id,
+ source: crate::api::schema::ReadSource::Recent,
+ lines: Some(2),
+ format: crate::api::schema::ReadFormat::Text,
+ strip_ansi: true,
+ },
+ );
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ let ResponseResult::PaneRead { read } = success.result else {
+ panic!("expected pane read response");
+ };
+ assert!(read.text.contains("line 19"));
+ assert!(read.truncated);
+ }
+
+ #[tokio::test]
async fn api_pane_send_keys_preserves_legacy_control_c_aliases() {
let (mut app, pane_id, mut rx) = app_with_send_key_runtime(3);
diff --git a/src/app/api_helpers.rs b/src/app/api_helpers.rs
index 6c393dac..e0091b39 100644
--- a/src/app/api_helpers.rs
+++ b/src/app/api_helpers.rs
@@ -106,50 +106,77 @@ pub(super) fn read_terminal_snapshot(
source: crate::api::schema::ReadSource,
format: crate::api::schema::ReadFormat,
lines: Option<u32>,
-) -> String {
+) -> crate::pane::TerminalReadSnapshot {
use crate::api::schema::{ReadFormat, ReadSource};
- let requested_lines = lines.unwrap_or(80).min(1000) as usize;
- let text = match (format, source) {
- (ReadFormat::Text, ReadSource::Visible) => terminal.visible_text(),
- (ReadFormat::Text, ReadSource::Recent) => terminal.recent_text(requested_lines),
+ let line_limit = lines.map(|lines| lines.min(1000) as usize);
+ let recent_lines = line_limit.unwrap_or(80);
+ match (format, source) {
+ (ReadFormat::Text, ReadSource::Visible) => {
+ limit_snapshot_lines(terminal.visible_text(), line_limit)
+ }
+ (ReadFormat::Text, ReadSource::Recent) => terminal.recent_text_snapshot(recent_lines),
(ReadFormat::Text, ReadSource::RecentUnwrapped) => {
- terminal.recent_unwrapped_text(requested_lines)
+ terminal.recent_unwrapped_text_snapshot(recent_lines)
+ }
+ (ReadFormat::Text, ReadSource::Detection) => {
+ limit_snapshot_lines(terminal.detection_text(), line_limit)
+ }
+ (ReadFormat::Ansi, ReadSource::Visible) => {
+ limit_snapshot_lines(terminal.visible_ansi(), line_limit)
}
- (ReadFormat::Text, ReadSource::Detection) => terminal.detection_text(),
- (ReadFormat::Ansi, ReadSource::Visible) => terminal.visible_ansi(),
- (ReadFormat::Ansi, ReadSource::Recent) => terminal.recent_ansi(requested_lines),
+ (ReadFormat::Ansi, ReadSource::Recent) => terminal.recent_ansi_snapshot(recent_lines),
(ReadFormat::Ansi, ReadSource::RecentUnwrapped) => {
- terminal.recent_unwrapped_ansi(requested_lines)
+ terminal.recent_unwrapped_ansi_snapshot(recent_lines)
+ }
+ (ReadFormat::Ansi, ReadSource::Detection) => {
+ limit_snapshot_lines(terminal.detection_text(), line_limit)
}
- (ReadFormat::Ansi, ReadSource::Detection) => terminal.detection_text(),
- };
-
- if lines.is_some() && matches!(source, ReadSource::Visible | ReadSource::Detection) {
- last_lines(&text, requested_lines)
- } else {
- text
}
}
-fn last_lines(text: &str, count: usize) -> String {
- if text.is_empty() || count == 0 {
- return String::new();
- }
+fn limit_snapshot_lines(text: String, limit: Option<usize>) -> crate::pane::TerminalReadSnapshot {
+ let Some(limit) = limit else {
+ return crate::pane::TerminalReadSnapshot {
+ text,
+ truncated: false,
+ };
+ };
let lines: Vec<_> = text.split_inclusive('\n').collect();
- lines[lines.len().saturating_sub(count)..].concat()
+ crate::pane::TerminalReadSnapshot {
+ text: lines[lines.len().saturating_sub(limit)..].concat(),
+ truncated: lines.len() > limit,
+ }
}
#[cfg(test)]
mod read_snapshot_tests {
- use super::last_lines;
+ use super::limit_snapshot_lines;
+
+ #[test]
+ fn line_limit_preserves_endings_and_reports_omitted_lines() {
+ let snapshot = limit_snapshot_lines("one\ntwø\n三\n".into(), Some(2));
+ assert_eq!(snapshot.text, "twø\n三\n");
+ assert!(snapshot.truncated);
+
+ let snapshot = limit_snapshot_lines("one\ntwo\nthree".into(), Some(1));
+ assert_eq!(snapshot.text, "three");
+ assert!(snapshot.truncated);
+
+ let snapshot = limit_snapshot_lines("one\ntwo".into(), Some(0));
+ assert_eq!(snapshot.text, "");
+ assert!(snapshot.truncated);
+
+ let snapshot = limit_snapshot_lines("".into(), Some(2));
+ assert_eq!(snapshot.text, "");
+ assert!(!snapshot.truncated);
+ }
#[test]
- fn last_lines_preserves_real_line_endings_and_unicode() {
- assert_eq!(last_lines("one\ntwø\n三\n", 2), "twø\n三\n");
- assert_eq!(last_lines("one\ntwo\nthree", 1), "three");
- assert_eq!(last_lines("one\ntwo", 0), "");
- assert_eq!(last_lines("", 2), "");
+ fn omitted_line_limit_returns_the_complete_snapshot() {
+ let snapshot = limit_snapshot_lines("one\ntwo\n".into(), None);
+ assert_eq!(snapshot.text, "one\ntwo\n");
+ assert!(!snapshot.truncated);
}
}