feat: add ansi pane read format
This commit is contained in:
parent
c6d8d8351e
commit
3a9888cd56
|
|
@ -136,6 +136,9 @@ herdr wait agent-status 1-1 --status done
|
|||
|
||||
# read output
|
||||
herdr pane read 1-2 --source recent --lines 50
|
||||
|
||||
# read a rendered ANSI snapshot for TUI feedback loops
|
||||
herdr pane read 1-2 --source visible --ansi
|
||||
```
|
||||
|
||||
full reference: [`SOCKET_API.md`](./SOCKET_API.md) and [`SKILL.md`](./SKILL.md).
|
||||
|
|
|
|||
1
SKILL.md
1
SKILL.md
|
|
@ -287,6 +287,7 @@ herdr pane read 1-1 --source recent --lines 100
|
|||
|
||||
- `workspace list`, `workspace create`, `tab list`, `tab create`, `tab get`, `tab focus`, `tab rename`, `tab close`, `pane list`, `pane get`, `pane split`, `wait output`, and `wait agent-status` print json on success.
|
||||
- `pane read` prints text, not json.
|
||||
- `pane read --format ansi` or `pane read --ansi` returns a rendered ANSI snapshot for TUI feedback loops.
|
||||
- `pane read --source recent-unwrapped` is useful when you want to inspect the same unwrapped transcript that `wait output --source recent` matches against.
|
||||
- `pane send-text`, `pane send-keys`, and `pane run` print nothing on success.
|
||||
- parse ids from `workspace create`, `tab create`, and `pane split` responses when you need new ids. `workspace create` returns `result.workspace`, `result.tab`, and `result.root_pane`. `tab create` returns `result.tab` and `result.root_pane`. for `pane split`, the new pane id is at `result.pane.pane_id`.
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ for backward compatibility, requests also accept the older positional forms like
|
|||
"workspace_id": "w64e95948145ed1",
|
||||
"tab_id": "w64e95948145ed1:1",
|
||||
"source": "recent",
|
||||
"format": "text",
|
||||
"text": "...",
|
||||
"revision": 0,
|
||||
"truncated": false
|
||||
|
|
@ -489,21 +490,24 @@ params:
|
|||
"pane_id": "1-1",
|
||||
"source": "recent",
|
||||
"lines": 80,
|
||||
"format": "text",
|
||||
"strip_ansi": true
|
||||
}
|
||||
```
|
||||
|
||||
notes:
|
||||
|
||||
- `source` is required and must be `visible` or `recent`
|
||||
- `source` is required and must be `visible`, `recent`, or `recent_unwrapped`
|
||||
- `lines` is optional
|
||||
- current implementation defaults to `80` lines when `lines` is omitted and caps reads at `1000`
|
||||
- `strip_ansi` defaults to `true`
|
||||
- `format` defaults to `text`; use `ansi` for a rendered VT/ANSI snapshot with styles preserved
|
||||
- `strip_ansi` defaults to `true` and is kept for compatibility
|
||||
|
||||
`source` meanings:
|
||||
|
||||
- `visible` — current viewport
|
||||
- `recent` — recent scrollback text
|
||||
- `recent_unwrapped` — recent scrollback text with soft wraps joined
|
||||
|
||||
example response:
|
||||
|
||||
|
|
@ -517,6 +521,7 @@ example response:
|
|||
"workspace_id": "1",
|
||||
"tab_id": "1:1",
|
||||
"source": "recent",
|
||||
"format": "text",
|
||||
"text": "...",
|
||||
"revision": 0,
|
||||
"truncated": false
|
||||
|
|
@ -941,7 +946,7 @@ pane commands:
|
|||
```text
|
||||
herdr pane list [--workspace <workspace_id>]
|
||||
herdr pane get <pane_id>
|
||||
herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--raw]
|
||||
herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]
|
||||
herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]
|
||||
herdr pane close <pane_id>
|
||||
herdr pane send-text <pane_id> <text>
|
||||
|
|
@ -971,13 +976,14 @@ herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [
|
|||
- `tab create` returns `result.tab` and `result.root_pane`
|
||||
- `pane split` keeps focus where it is by default; pass `--focus` to switch to the new pane
|
||||
- `pane read` prints **text**, not json
|
||||
- `pane read --format ansi` and `pane read --ansi` print a rendered ANSI snapshot with colors/styles preserved
|
||||
- `pane read --source recent-unwrapped` returns recent terminal text with soft wraps joined back together
|
||||
- `pane send-text`, `pane send-keys`, and `pane run` print nothing on success
|
||||
- list/get/create/split/wait commands print json on success
|
||||
- `pane run` is a convenience wrapper for `pane.send_input` with the command text followed by a real `Enter` keypress
|
||||
- `wait agent-status` is a cli convenience built on top of event subscriptions
|
||||
- use it when you want the same `done` / `idle` distinction the UI shows
|
||||
- `--raw` disables ansi stripping for `pane read` and `wait output`
|
||||
- `--raw` is a legacy alias for ANSI formatted `pane read` output and still disables ansi stripping for `wait output`
|
||||
- `wait output --source recent` matches against unwrapped recent terminal text by default, so pane width and soft wrapping do not break matches
|
||||
|
||||
### cli examples
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@ fn wait_for_output(
|
|||
pane_id: params.pane_id.clone(),
|
||||
source: output_match_read_source(¶ms.source),
|
||||
lines: params.lines,
|
||||
format: crate::api::schema::ReadFormat::Text,
|
||||
strip_ansi: params.strip_ansi,
|
||||
}),
|
||||
};
|
||||
|
|
@ -894,6 +895,7 @@ fn pane_read(
|
|||
pane_id: pane_id.to_string(),
|
||||
source,
|
||||
lines,
|
||||
format: crate::api::schema::ReadFormat::Text,
|
||||
strip_ansi,
|
||||
}),
|
||||
},
|
||||
|
|
@ -1203,6 +1205,7 @@ mod tests {
|
|||
workspace_id: "ws_1".into(),
|
||||
tab_id: "tab_1".into(),
|
||||
source: crate::api::schema::ReadSource::RecentUnwrapped,
|
||||
format: crate::api::schema::ReadFormat::Text,
|
||||
text: String::new(),
|
||||
revision: 0,
|
||||
truncated: false,
|
||||
|
|
|
|||
|
|
@ -187,6 +187,8 @@ pub struct PaneReadParams {
|
|||
pub source: ReadSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lines: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub format: ReadFormat,
|
||||
#[serde(default = "default_true")]
|
||||
pub strip_ansi: bool,
|
||||
}
|
||||
|
|
@ -215,7 +217,7 @@ pub struct PaneReleaseAgentParams {
|
|||
pub agent: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReadSource {
|
||||
Visible,
|
||||
|
|
@ -223,6 +225,14 @@ pub enum ReadSource {
|
|||
RecentUnwrapped,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReadFormat {
|
||||
#[default]
|
||||
Text,
|
||||
Ansi,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EventsSubscribeParams {
|
||||
pub subscriptions: Vec<Subscription>,
|
||||
|
|
@ -528,6 +538,7 @@ pub struct PaneReadResult {
|
|||
pub workspace_id: String,
|
||||
pub tab_id: String,
|
||||
pub source: ReadSource,
|
||||
pub format: ReadFormat,
|
||||
pub text: String,
|
||||
pub revision: u64,
|
||||
pub truncated: bool,
|
||||
|
|
@ -686,6 +697,7 @@ mod tests {
|
|||
pane_id: "p_1".into(),
|
||||
source: ReadSource::Recent,
|
||||
lines: Some(80),
|
||||
format: ReadFormat::Text,
|
||||
strip_ansi: true,
|
||||
}),
|
||||
};
|
||||
|
|
@ -845,6 +857,26 @@ mod tests {
|
|||
assert!(params.strip_ansi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_read_defaults_to_text_format() {
|
||||
let json = r#"
|
||||
{
|
||||
"id": "req_1",
|
||||
"method": "pane.read",
|
||||
"params": {
|
||||
"pane_id": "p_1",
|
||||
"source": "visible"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let request: Request = serde_json::from_str(json).unwrap();
|
||||
let Method::PaneRead(params) = request.method else {
|
||||
panic!("wrong method parsed");
|
||||
};
|
||||
assert_eq!(params.format, ReadFormat::Text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_envelope_round_trips() {
|
||||
let event = EventEnvelope {
|
||||
|
|
@ -922,6 +954,7 @@ mod tests {
|
|||
workspace_id: "w_1".into(),
|
||||
tab_id: "t_1_1".into(),
|
||||
source: ReadSource::Recent,
|
||||
format: ReadFormat::Text,
|
||||
text: "auth: received\n".into(),
|
||||
revision: 0,
|
||||
truncated: false,
|
||||
|
|
|
|||
|
|
@ -288,8 +288,8 @@ impl App {
|
|||
|
||||
use crate::api::schema::{
|
||||
ErrorBody, ErrorResponse, IntegrationInstallResult, IntegrationUninstallResult, Method,
|
||||
PaneListParams, PaneReadResult, ReadSource, ResponseResult, SuccessResponse,
|
||||
TabListParams,
|
||||
PaneListParams, PaneReadResult, ReadFormat, ReadSource, ResponseResult,
|
||||
SuccessResponse, TabListParams,
|
||||
};
|
||||
|
||||
let response = match request.method {
|
||||
|
|
@ -964,10 +964,17 @@ impl App {
|
|||
.unwrap();
|
||||
};
|
||||
let requested_lines = params.lines.unwrap_or(80).min(1000) as usize;
|
||||
let text = match params.source {
|
||||
ReadSource::Visible => pane.visible_text(),
|
||||
ReadSource::Recent => pane.recent_text(requested_lines),
|
||||
ReadSource::RecentUnwrapped => pane.recent_unwrapped_text(requested_lines),
|
||||
let text = match params.format {
|
||||
ReadFormat::Text => match params.source {
|
||||
ReadSource::Visible => pane.visible_text(),
|
||||
ReadSource::Recent => pane.recent_text(requested_lines),
|
||||
ReadSource::RecentUnwrapped => pane.recent_unwrapped_text(requested_lines),
|
||||
},
|
||||
ReadFormat::Ansi => match params.source {
|
||||
ReadSource::Visible => pane.visible_ansi(),
|
||||
ReadSource::Recent => pane.recent_ansi(requested_lines),
|
||||
ReadSource::RecentUnwrapped => pane.recent_unwrapped_ansi(requested_lines),
|
||||
},
|
||||
};
|
||||
SuccessResponse {
|
||||
id: request.id,
|
||||
|
|
@ -977,6 +984,7 @@ impl App {
|
|||
workspace_id,
|
||||
tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(),
|
||||
source: params.source,
|
||||
format: params.format,
|
||||
text,
|
||||
revision: 0,
|
||||
truncated: false,
|
||||
|
|
|
|||
33
src/cli.rs
33
src/cli.rs
|
|
@ -8,8 +8,8 @@ use crate::api;
|
|||
use crate::api::schema::{
|
||||
AgentStatus, EmptyParams, IntegrationTarget, Method, OutputMatch, PaneListParams,
|
||||
PaneReadParams, PaneSendInputParams, PaneSendKeysParams, PaneSendTextParams, PaneSplitParams,
|
||||
PaneTarget, PaneWaitForOutputParams, PingParams, ReadSource, Request, SplitDirection,
|
||||
Subscription, TabCreateParams, TabListParams, TabRenameParams, TabTarget,
|
||||
PaneTarget, PaneWaitForOutputParams, PingParams, ReadFormat, ReadSource, Request,
|
||||
SplitDirection, Subscription, TabCreateParams, TabListParams, TabRenameParams, TabTarget,
|
||||
WorkspaceCreateParams, WorkspaceRenameParams, WorkspaceTarget,
|
||||
};
|
||||
|
||||
|
|
@ -789,13 +789,14 @@ fn pane_get(args: &[String]) -> std::io::Result<i32> {
|
|||
|
||||
fn pane_read(args: &[String]) -> std::io::Result<i32> {
|
||||
let Some(raw_pane_id) = args.first() else {
|
||||
eprintln!("usage: herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N]");
|
||||
eprintln!("usage: herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]");
|
||||
return Ok(2);
|
||||
};
|
||||
|
||||
let pane_id = normalize_pane_id(raw_pane_id);
|
||||
let mut source = ReadSource::Recent;
|
||||
let mut lines = None;
|
||||
let mut format = ReadFormat::Text;
|
||||
let mut strip_ansi = true;
|
||||
|
||||
let mut index = 1;
|
||||
|
|
@ -817,7 +818,20 @@ fn pane_read(args: &[String]) -> std::io::Result<i32> {
|
|||
lines = Some(parse_u32_flag("--lines", value)?);
|
||||
index += 2;
|
||||
}
|
||||
"--format" => {
|
||||
let Some(value) = args.get(index + 1) else {
|
||||
eprintln!("missing value for --format");
|
||||
return Ok(2);
|
||||
};
|
||||
format = parse_read_format(value)?;
|
||||
index += 2;
|
||||
}
|
||||
"--ansi" => {
|
||||
format = ReadFormat::Ansi;
|
||||
index += 1;
|
||||
}
|
||||
"--raw" => {
|
||||
format = ReadFormat::Ansi;
|
||||
strip_ansi = false;
|
||||
index += 1;
|
||||
}
|
||||
|
|
@ -834,6 +848,7 @@ fn pane_read(args: &[String]) -> std::io::Result<i32> {
|
|||
pane_id,
|
||||
source,
|
||||
lines,
|
||||
format,
|
||||
strip_ansi,
|
||||
}),
|
||||
})?;
|
||||
|
|
@ -1308,6 +1323,16 @@ fn parse_read_source(value: &str) -> std::io::Result<ReadSource> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_read_format(value: &str) -> std::io::Result<ReadFormat> {
|
||||
match value {
|
||||
"text" => Ok(ReadFormat::Text),
|
||||
"ansi" => Ok(ReadFormat::Ansi),
|
||||
_ => Err(std::io::Error::other(format!(
|
||||
"invalid read format: {value}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_agent_status(value: &str) -> std::io::Result<AgentStatus> {
|
||||
match value {
|
||||
"idle" => Ok(AgentStatus::Idle),
|
||||
|
|
@ -1438,7 +1463,7 @@ fn print_pane_help() {
|
|||
eprintln!("herdr pane commands:");
|
||||
eprintln!(" herdr pane list [--workspace <workspace_id>]");
|
||||
eprintln!(" herdr pane get <pane_id>");
|
||||
eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--raw]");
|
||||
eprintln!(" herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]");
|
||||
eprintln!(
|
||||
" herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus]"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,21 @@ pub use bindings as ffi;
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Error(ffi::GhosttyResult);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum FormatterFormat {
|
||||
Plain,
|
||||
Vt,
|
||||
}
|
||||
|
||||
impl FormatterFormat {
|
||||
fn as_raw(self) -> ffi::GhosttyFormatterFormat {
|
||||
match self {
|
||||
Self::Plain => ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_PLAIN,
|
||||
Self::Vt => ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_VT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ghostty error {}", self.0)
|
||||
|
|
@ -427,10 +442,29 @@ impl Terminal {
|
|||
end: (u16, u32),
|
||||
rectangle: bool,
|
||||
) -> Result<String, Error> {
|
||||
self.read_text_selection(
|
||||
self.read_formatted_selection(
|
||||
ghostty_viewport_point(start.0, start.1),
|
||||
ghostty_viewport_point(end.0, end.1),
|
||||
rectangle,
|
||||
FormatterFormat::Plain,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn read_ansi_viewport(
|
||||
&self,
|
||||
start: (u16, u32),
|
||||
end: (u16, u32),
|
||||
rectangle: bool,
|
||||
) -> Result<String, Error> {
|
||||
self.read_formatted_selection(
|
||||
ghostty_viewport_point(start.0, start.1),
|
||||
ghostty_viewport_point(end.0, end.1),
|
||||
rectangle,
|
||||
FormatterFormat::Vt,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -440,18 +474,41 @@ impl Terminal {
|
|||
end: (u16, u32),
|
||||
rectangle: bool,
|
||||
) -> Result<String, Error> {
|
||||
self.read_text_selection(
|
||||
self.read_formatted_selection(
|
||||
ghostty_screen_point(start.0, start.1),
|
||||
ghostty_screen_point(end.0, end.1),
|
||||
rectangle,
|
||||
FormatterFormat::Plain,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
fn read_text_selection(
|
||||
pub fn read_ansi_screen(
|
||||
&self,
|
||||
start: (u16, u32),
|
||||
end: (u16, u32),
|
||||
rectangle: bool,
|
||||
unwrap: bool,
|
||||
) -> Result<String, Error> {
|
||||
self.read_formatted_selection(
|
||||
ghostty_screen_point(start.0, start.1),
|
||||
ghostty_screen_point(end.0, end.1),
|
||||
rectangle,
|
||||
FormatterFormat::Vt,
|
||||
unwrap,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
fn read_formatted_selection(
|
||||
&self,
|
||||
start: ffi::GhosttyPoint,
|
||||
end: ffi::GhosttyPoint,
|
||||
rectangle: bool,
|
||||
format: FormatterFormat,
|
||||
unwrap: bool,
|
||||
trim: bool,
|
||||
) -> Result<String, Error> {
|
||||
let mut start_ref = ffi::GhosttyGridRef {
|
||||
size: mem::size_of::<ffi::GhosttyGridRef>(),
|
||||
|
|
@ -475,9 +532,9 @@ impl Terminal {
|
|||
let mut formatter: ffi::GhosttyFormatter_ptr = ptr::null_mut();
|
||||
let options = ffi::GhosttyFormatterTerminalOptions {
|
||||
size: mem::size_of::<ffi::GhosttyFormatterTerminalOptions>(),
|
||||
emit: ffi::GhosttyFormatterFormat_GHOSTTY_FORMATTER_FORMAT_PLAIN,
|
||||
unwrap: true,
|
||||
trim: true,
|
||||
emit: format.as_raw(),
|
||||
unwrap,
|
||||
trim,
|
||||
extra: ffi::GhosttyFormatterTerminalExtra {
|
||||
size: mem::size_of::<ffi::GhosttyFormatterTerminalExtra>(),
|
||||
screen: ffi::GhosttyFormatterScreenExtra {
|
||||
|
|
|
|||
12
src/pane.rs
12
src/pane.rs
|
|
@ -761,14 +761,26 @@ impl PaneRuntime {
|
|||
self.terminal.visible_text()
|
||||
}
|
||||
|
||||
pub fn visible_ansi(&self) -> String {
|
||||
self.terminal.visible_ansi()
|
||||
}
|
||||
|
||||
pub fn recent_text(&self, lines: usize) -> String {
|
||||
self.terminal.recent_text(lines)
|
||||
}
|
||||
|
||||
pub fn recent_ansi(&self, lines: usize) -> String {
|
||||
self.terminal.recent_ansi(lines)
|
||||
}
|
||||
|
||||
pub fn recent_unwrapped_text(&self, lines: usize) -> String {
|
||||
self.terminal.recent_unwrapped_text(lines)
|
||||
}
|
||||
|
||||
pub fn recent_unwrapped_ansi(&self, lines: usize) -> String {
|
||||
self.terminal.recent_unwrapped_ansi(lines)
|
||||
}
|
||||
|
||||
pub fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> {
|
||||
self.terminal.extract_selection(selection)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,10 @@ impl PaneTerminal {
|
|||
self.ghostty.visible_text()
|
||||
}
|
||||
|
||||
pub fn visible_ansi(&self) -> String {
|
||||
self.ghostty.visible_ansi()
|
||||
}
|
||||
|
||||
pub fn detection_text(&self) -> String {
|
||||
self.ghostty.detection_text()
|
||||
}
|
||||
|
|
@ -141,10 +145,18 @@ impl PaneTerminal {
|
|||
self.ghostty.recent_text(lines)
|
||||
}
|
||||
|
||||
pub fn recent_ansi(&self, lines: usize) -> String {
|
||||
self.ghostty.recent_ansi(lines)
|
||||
}
|
||||
|
||||
pub fn recent_unwrapped_text(&self, lines: usize) -> String {
|
||||
self.ghostty.recent_unwrapped_text(lines)
|
||||
}
|
||||
|
||||
pub fn recent_unwrapped_ansi(&self, lines: usize) -> String {
|
||||
self.ghostty.recent_unwrapped_ansi(lines)
|
||||
}
|
||||
|
||||
pub fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> {
|
||||
self.ghostty.extract_selection(selection)
|
||||
}
|
||||
|
|
@ -546,6 +558,14 @@ impl GhosttyPaneTerminal {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn visible_ansi(&self) -> String {
|
||||
self.core
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|core| ghostty_visible_ansi(&core).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn detection_text(&self) -> String {
|
||||
self.core
|
||||
.lock()
|
||||
|
|
@ -562,6 +582,14 @@ impl GhosttyPaneTerminal {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn recent_ansi(&self, lines: usize) -> String {
|
||||
self.core
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|core| ghostty_recent_ansi(&core, lines, false).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn recent_unwrapped_text(&self, lines: usize) -> String {
|
||||
self.core
|
||||
.lock()
|
||||
|
|
@ -570,6 +598,14 @@ impl GhosttyPaneTerminal {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn recent_unwrapped_ansi(&self, lines: usize) -> String {
|
||||
self.core
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|core| ghostty_recent_ansi(&core, lines, true).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> {
|
||||
self.core
|
||||
.lock()
|
||||
|
|
@ -726,6 +762,19 @@ fn ghostty_visible_text(core: &mut GhosttyPaneCore) -> Result<String, crate::gho
|
|||
Ok(lines_to_text(lines))
|
||||
}
|
||||
|
||||
fn ghostty_visible_ansi(core: &GhosttyPaneCore) -> Result<String, crate::ghostty::Error> {
|
||||
let rows = core.terminal.rows()?;
|
||||
let cols = core.terminal.cols()?;
|
||||
if rows == 0 || cols == 0 {
|
||||
return Ok(String::new());
|
||||
}
|
||||
core.terminal.read_ansi_viewport(
|
||||
(0, 0),
|
||||
(cols.saturating_sub(1), u32::from(rows.saturating_sub(1))),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn ghostty_detection_text(core: &GhosttyPaneCore) -> Result<String, crate::ghostty::Error> {
|
||||
let lines = core
|
||||
.terminal
|
||||
|
|
@ -766,6 +815,22 @@ fn ghostty_recent_text_unwrapped(
|
|||
.read_text_screen((0, start), (cols.saturating_sub(1), end), false)
|
||||
}
|
||||
|
||||
fn ghostty_recent_ansi(
|
||||
core: &GhosttyPaneCore,
|
||||
lines: usize,
|
||||
unwrap: bool,
|
||||
) -> Result<String, crate::ghostty::Error> {
|
||||
let total_rows = core.terminal.total_rows()?;
|
||||
let cols = core.terminal.cols()?;
|
||||
if total_rows == 0 || cols == 0 {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let start = total_rows.saturating_sub(lines) as u32;
|
||||
let end = (total_rows.saturating_sub(1)) as u32;
|
||||
core.terminal
|
||||
.read_ansi_screen((0, start), (cols.saturating_sub(1), end), false, unwrap)
|
||||
}
|
||||
|
||||
fn ghostty_extract_selection(
|
||||
core: &mut GhosttyPaneCore,
|
||||
selection: &crate::selection::Selection,
|
||||
|
|
@ -1456,6 +1521,32 @@ mod tests {
|
|||
assert_eq!(pane.recent_unwrapped_text(3), "ABCDEFGHIJ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visible_ansi_preserves_cell_style_sequences() {
|
||||
let (tx, _rx) = mpsc::channel(4);
|
||||
let mut terminal = crate::ghostty::Terminal::new(20, 3, 100).unwrap();
|
||||
terminal.write(b"\x1b[31;1mred\x1b[0m plain");
|
||||
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
|
||||
|
||||
let ansi = pane.visible_ansi();
|
||||
assert!(ansi.contains("red"));
|
||||
assert!(ansi.contains("plain"));
|
||||
assert!(ansi.contains("\x1b["));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_ansi_can_read_styled_scrollback() {
|
||||
let (tx, _rx) = mpsc::channel(4);
|
||||
let mut terminal = crate::ghostty::Terminal::new(20, 3, 100).unwrap();
|
||||
terminal.write(b"\x1b[34mblue\x1b[0m\r\nline2\r\nline3\r\nline4");
|
||||
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
|
||||
|
||||
let ansi = pane.recent_ansi(4);
|
||||
assert!(ansi.contains("blue"));
|
||||
assert!(ansi.contains("line4"));
|
||||
assert!(ansi.contains("\x1b["));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_reflow_keeps_scrolled_viewport_and_bottom_detection_sane() {
|
||||
let (tx, _rx) = mpsc::channel(4);
|
||||
|
|
|
|||
Loading…
Reference in New Issue