diff --git a/Cargo.lock b/Cargo.lock index 7d42af56b..46e25fe63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5726,6 +5726,7 @@ dependencies = [ "tempfile", "termios 0.3.3", "termwiz", + "termwiz-funcs", "textwrap 0.16.0", "umask", "url", diff --git a/codec/src/lib.rs b/codec/src/lib.rs index a6f2a840c..7e36965bf 100644 --- a/codec/src/lib.rs +++ b/codec/src/lib.rs @@ -417,7 +417,7 @@ macro_rules! pdu { /// The overall version of the codec. /// This must be bumped when backwards incompatible changes /// are made to the types and protocol. -pub const CODEC_VERSION: usize = 31; +pub const CODEC_VERSION: usize = 32; // Defines the Pdu enum. // Each struct has an explicit identifying number. @@ -466,6 +466,8 @@ pdu! { MovePaneToNewTab: 48, MovePaneToNewTabResponse: 49, ActivatePaneDirection: 50, + GetPaneRenderableDimensions: 51, + GetPaneRenderableDimensionsResponse: 52, } impl Pdu { @@ -780,6 +782,18 @@ pub struct GetPaneRenderChanges { pub pane_id: PaneId, } +#[derive(Deserialize, Serialize, PartialEq, Debug)] +pub struct GetPaneRenderableDimensions { + pub pane_id: PaneId, +} + +#[derive(Deserialize, Serialize, PartialEq, Debug)] +pub struct GetPaneRenderableDimensionsResponse { + pub pane_id: PaneId, + pub cursor_position: StableCursorPosition, + pub dimensions: RenderableDimensions, +} + #[derive(Deserialize, Serialize, PartialEq, Debug)] pub struct LivenessResponse { pub pane_id: PaneId, diff --git a/docs/changelog.md b/docs/changelog.md index f7eb904ca..5a308baa8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -62,6 +62,7 @@ As features stabilize some brief notes about them will accumulate here. * [wezterm.config_builder()](config/lua/wezterm/config_builder.md) * [gui-attached](config/lua/gui-events/gui-attached.md) event provides some more flexibility at startup. +* [wezterm cli get-text](cli/cli/get-text.md) command for capturing the content of a pane. #### Fixed * X11: hanging or killing the IME could hang wezterm diff --git a/docs/cli/cli/get-text.md b/docs/cli/cli/get-text.md new file mode 100644 index 000000000..1f34536e2 --- /dev/null +++ b/docs/cli/cli/get-text.md @@ -0,0 +1,27 @@ +# `wezterm cli get-text` + +*Since: nightly builds only* + +*Run `wezterm cli get-text --help` to see more help* + +Retrieves the textual content of a pane and output it to stdout. + +For example: + +``` +$ wezterm cli get-text > /tmp/myscreen.txt +``` + +will capture the main (non-scrollback) portion of the current pane to `/tmp/myscreen.txt`. + +By default, just the raw text is captured without any color or styling escape sequences. +You may pass `--escapes` to include those: + +``` +$ wezterm cli get-text --escapes > /tmp/myscreen-with-colors.txt +``` + +The default capture region is the main terminal screen, not including the scrollback. +You may use the `--start-line` and `--end-line` parameters to set the range. +Both of these accept integer values, where `0` refers to the top of the non-scrollback +screen area, and negative numbers index backwards into the scrollback. diff --git a/lua-api-crates/termwiz-funcs/src/lib.rs b/lua-api-crates/termwiz-funcs/src/lib.rs index 46df11237..92ca1e09a 100644 --- a/lua-api-crates/termwiz-funcs/src/lib.rs +++ b/lua-api-crates/termwiz-funcs/src/lib.rs @@ -9,6 +9,7 @@ use termwiz::color::{AnsiColor, ColorAttribute, ColorSpec, SrgbaTuple}; use termwiz::input::Modifiers; use termwiz::render::terminfo::TerminfoRenderer; use termwiz::surface::change::Change; +use termwiz::surface::Line; use wezterm_dynamic::{FromDynamic, ToDynamic}; pub fn register(lua: &Lua) -> anyhow::Result<()> { @@ -262,3 +263,40 @@ lazy_static::lazy_static! { pub fn new_wezterm_terminfo_renderer() -> TerminfoRenderer { TerminfoRenderer::new(CAPS.clone()) } + +pub fn lines_to_escapes(lines: Vec) -> anyhow::Result { + let mut changes = vec![]; + let mut attr = CellAttributes::blank(); + for line in lines { + changes.append(&mut line.changes(&attr)); + changes.push(Change::Text("\r\n".to_string())); + if let Some(a) = line.visible_cells().last().map(|cell| cell.attrs().clone()) { + attr = a; + } + } + changes.push(Change::AllAttributes(CellAttributes::blank())); + let mut renderer = new_wezterm_terminfo_renderer(); + + struct Target { + target: Vec, + } + + impl std::io::Write for Target { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + std::io::Write::write(&mut self.target, buf) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + impl termwiz::render::RenderTty for Target { + fn get_size_in_cells(&mut self) -> termwiz::Result<(usize, usize)> { + Ok((80, 24)) + } + } + + let mut target = Target { target: vec![] }; + renderer.render_to(&changes, &mut target)?; + Ok(String::from_utf8(target.target)?) +} diff --git a/wezterm-client/src/client.rs b/wezterm-client/src/client.rs index e192f837c..886ad2e28 100644 --- a/wezterm-client/src/client.rs +++ b/wezterm-client/src/client.rs @@ -1204,6 +1204,11 @@ impl Client { LivenessResponse ); rpc!(get_lines, GetLines, GetLinesResponse); + rpc!( + get_dimensions, + GetPaneRenderableDimensions, + GetPaneRenderableDimensionsResponse + ); rpc!(get_codec_version, GetCodecVersion, GetCodecVersionResponse); rpc!(get_tls_creds, GetTlsCreds = (), GetTlsCredsResponse); rpc!( diff --git a/wezterm-gui/src/scripting/guiwin.rs b/wezterm-gui/src/scripting/guiwin.rs index 9361238d4..30ff76b38 100644 --- a/wezterm-gui/src/scripting/guiwin.rs +++ b/wezterm-gui/src/scripting/guiwin.rs @@ -9,9 +9,7 @@ use mux::pane::PaneId; use mux::window::WindowId as MuxWindowId; use mux::Mux; use mux_lua::MuxPane; -use termwiz::cell::CellAttributes; -use termwiz::surface::{Change, Line}; -use termwiz_funcs::new_wezterm_terminfo_renderer; +use termwiz_funcs::lines_to_escapes; use wezterm_dynamic::{FromDynamic, ToDynamic}; use wezterm_toast_notification::ToastNotification; use window::{Connection, ConnectionOps, DeadKeyStatus, WindowOps, WindowState}; @@ -312,40 +310,3 @@ impl UserData for GuiWin { ); } } - -fn lines_to_escapes(lines: Vec) -> anyhow::Result { - let mut changes = vec![]; - let mut attr = CellAttributes::blank(); - for line in lines { - changes.append(&mut line.changes(&attr)); - changes.push(Change::Text("\r\n".to_string())); - if let Some(a) = line.visible_cells().last().map(|cell| cell.attrs().clone()) { - attr = a; - } - } - changes.push(Change::AllAttributes(CellAttributes::blank())); - let mut renderer = new_wezterm_terminfo_renderer(); - - struct Target { - target: Vec, - } - - impl std::io::Write for Target { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - std::io::Write::write(&mut self.target, buf) - } - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } - } - - impl termwiz::render::RenderTty for Target { - fn get_size_in_cells(&mut self) -> termwiz::Result<(usize, usize)> { - Ok((80, 24)) - } - } - - let mut target = Target { target: vec![] }; - renderer.render_to(&changes, &mut target)?; - Ok(String::from_utf8(target.target)?) -} diff --git a/wezterm-mux-server-impl/src/sessionhandler.rs b/wezterm-mux-server-impl/src/sessionhandler.rs index 63e94519b..246da0e89 100644 --- a/wezterm-mux-server-impl/src/sessionhandler.rs +++ b/wezterm-mux-server-impl/src/sessionhandler.rs @@ -582,6 +582,30 @@ impl SessionHandler { .detach(); } + Pdu::GetPaneRenderableDimensions(GetPaneRenderableDimensions { pane_id }) => { + spawn_into_main_thread(async move { + catch( + move || { + let mux = Mux::get(); + let pane = mux + .get_pane(pane_id) + .ok_or_else(|| anyhow!("no such pane {}", pane_id))?; + let cursor_position = pane.get_cursor_position(); + let dimensions = pane.get_dimensions(); + Ok(Pdu::GetPaneRenderableDimensionsResponse( + GetPaneRenderableDimensionsResponse { + pane_id, + cursor_position, + dimensions, + }, + )) + }, + send_response, + ) + }) + .detach(); + } + Pdu::GetPaneRenderChanges(GetPaneRenderChanges { pane_id, .. }) => { let sender = self.to_write_tx.clone(); let per_pane = self.per_pane(pane_id); @@ -724,6 +748,7 @@ impl SessionHandler { | Pdu::PaneRemoved { .. } | Pdu::GetImageCellResponse { .. } | Pdu::MovePaneToNewTabResponse { .. } + | Pdu::GetPaneRenderableDimensionsResponse { .. } | Pdu::ErrorResponse { .. } => { send_response(Err(anyhow!("expected a request, got {:?}", decoded.pdu))) } diff --git a/wezterm/Cargo.toml b/wezterm/Cargo.toml index 129cd219c..9dbc13b12 100644 --- a/wezterm/Cargo.toml +++ b/wezterm/Cargo.toml @@ -32,6 +32,7 @@ smol = "1.2" tabout = { path = "../tabout" } tempfile = "3.3" termwiz = { path = "../termwiz" } +termwiz-funcs = { path = "../lua-api-crates/termwiz-funcs" } textwrap = "0.16" umask = { path = "../umask" } url = "2" diff --git a/wezterm/src/main.rs b/wezterm/src/main.rs index e49417201..a83206819 100644 --- a/wezterm/src/main.rs +++ b/wezterm/src/main.rs @@ -16,10 +16,11 @@ use std::ffi::OsString; use std::io::{Read, Write}; use std::sync::Arc; use tabout::{tabulate_output, Alignment, Column}; +use termwiz_funcs::lines_to_escapes; use umask::UmaskSaver; use wezterm_client::client::{unix_connect_with_retry, Client}; use wezterm_gui_subcommands::*; -use wezterm_term::TerminalSize; +use wezterm_term::{ScrollbackOrVisibleRowIndex, StableRowIndex, TerminalSize}; mod asciicast; @@ -369,6 +370,37 @@ Outputs the pane-id for the newly created pane on success" text: Option, }, + /// Retrieves the textual content of a pane and output it to stdout + #[command(name = "get-text", rename_all = "kebab")] + GetText { + /// Specify the target pane. + /// The default is to use the current pane based on the + /// environment variable WEZTERM_PANE. + #[arg(long)] + pane_id: Option, + + /// The starting line number. + /// 0 is the first line of terminal screen. + /// Negative numbers proceed backwards into the scrollback. + /// The default value is unspecified is 0, the first line of + /// the terminal screen. + #[arg(long)] + start_line: Option, + + /// The ending line number. + /// 0 is the first line of terminal screen. + /// Negative numbers proceed backwards into the scrollback. + /// The default value if unspecified is the bottom of the + /// the terminal screen. + #[arg(long)] + end_line: Option, + + /// Include escape sequences that color and style the text. + /// If omitted, unattributed text will be returned. + #[arg(long)] + escapes: bool, + }, + /// Activate an adjacent pane in the specified direction. #[command(name = "activate-pane-direction", rename_all = "kebab")] ActivatePaneDirection { @@ -1081,6 +1113,67 @@ async fn run_cli_async(config: config::ConfigHandle, cli: CliCommand) -> anyhow: .await?; } } + CliSubCommand::GetText { + pane_id, + start_line, + end_line, + escapes, + } => { + let pane_id = resolve_pane_id(&client, pane_id).await?; + + let info = client + .get_dimensions(codec::GetPaneRenderableDimensions { pane_id }) + .await?; + + let start_line = match start_line { + None => info.dimensions.physical_top, + Some(n) if n >= 0 => info.dimensions.physical_top + n as StableRowIndex, + Some(n) => { + let line = info.dimensions.physical_top as isize + n as isize; + if line < info.dimensions.scrollback_top as isize { + info.dimensions.scrollback_top + } else { + line as StableRowIndex + } + } + }; + + let end_line = match end_line { + None => { + info.dimensions.physical_top + info.dimensions.viewport_rows as StableRowIndex + } + Some(n) if n >= 0 => info.dimensions.physical_top + n as StableRowIndex, + Some(n) => { + let line = info.dimensions.physical_top as isize + n as isize; + if line < info.dimensions.scrollback_top as isize { + info.dimensions.scrollback_top + } else { + line as StableRowIndex + } + } + }; + + let lines = client + .get_lines(codec::GetLines { + pane_id: pane_id.into(), + lines: vec![start_line..end_line + 1], + }) + .await?; + + let lines = lines + .lines + .extract_data() + .0 + .into_iter() + .map(|(_idx, line)| line) + .collect(); + + if escapes { + println!("{}", lines_to_escapes(lines)?); + } else { + lines.iter().for_each(|line| println!("{}", line.as_str())); + } + } CliSubCommand::SpawnCommand { cwd, prog,