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
2 changes: 2 additions & 0 deletions bindings/js/native/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ fn open_options(value: Option<OpenOptions>) -> std::result::Result<CoreOpenOptio
value.profile_colors.as_deref().unwrap_or_default(),
)?;
Ok(CoreOpenOptions {
backend: Default::default(),
profile,
shell: value.shell.map(Into::into),
cols: match value.cols {
Expand All @@ -509,6 +510,7 @@ fn run_options(value: RunOptions) -> std::result::Result<CoreRunOptions, TuiTest
value.profile_colors.as_deref().unwrap_or_default(),
)?;
Ok(CoreRunOptions {
backend: Default::default(),
profile,
program: value.program,
args: value.args.unwrap_or_default(),
Expand Down
2 changes: 2 additions & 0 deletions bindings/python/native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl NativeSession {
execute_open(
&name,
Operation::Open(OpenOptions {
backend: Default::default(),
profile: profile_from_parts(profile_scrollback.as_ref(), &profile_colors)?,
shell: parse_shell(shell.as_deref())?,
cols: integer_u16(&cols, "cols")?,
Expand Down Expand Up @@ -170,6 +171,7 @@ impl NativeSession {
execute_open(
&name,
Operation::Run(RunOptions {
backend: Default::default(),
profile: profile_from_parts(profile_scrollback.as_ref(), &profile_colors)?,
program,
args,
Expand Down
2 changes: 1 addition & 1 deletion crates/tui-test-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ interprocess.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tui-test.workspace = true
tui-test = { workspace = true, features = ["ghostty"] }
39 changes: 38 additions & 1 deletion crates/tui-test-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@ use clap::{Args, Parser, Subcommand};

use tui_test::config::{DEFAULT_COLS, DEFAULT_ROWS};
use tui_test::shell::Shell;
use tui_test::Timeouts;
use tui_test::{Backend, Timeouts};

#[derive(Clone, Copy, clap::ValueEnum)]
#[clap(rename_all = "lowercase")]
pub enum BackendArg {
Alacritty,
Ghostty,
}

impl From<BackendArg> for Backend {
fn from(backend: BackendArg) -> Self {
match backend {
BackendArg::Alacritty => Backend::Alacritty,
BackendArg::Ghostty => Backend::Ghostty,
}
}
}

#[derive(Clone, Copy, clap::ValueEnum)]
#[clap(rename_all = "lowercase")]
Expand Down Expand Up @@ -116,6 +132,9 @@ pub enum Command {
/// Shell to launch (defaults to the platform shell).
#[arg(long, value_enum)]
shell: Option<ShellArg>,
/// Terminal emulator to use (defaults to alacritty).
#[arg(long, value_enum)]
backend: Option<BackendArg>,
/// Terminal width in columns.
#[arg(long, default_value_t = DEFAULT_COLS)]
cols: u16,
Expand Down Expand Up @@ -147,6 +166,9 @@ pub enum Command {
/// Arguments passed to the program.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
/// Terminal emulator to use (defaults to alacritty).
#[arg(long, value_enum)]
backend: Option<BackendArg>,
/// Terminal width in columns.
#[arg(long, default_value_t = DEFAULT_COLS)]
cols: u16,
Expand Down Expand Up @@ -400,6 +422,21 @@ mod tests {
}
}

#[test]
fn open_backend_values_map_to_terminal_backends() {
let cli = Cli::try_parse_from(["tui-test", "open", "--backend", "ghostty"])
.expect("parse backend");
let Some(Command::Open {
backend: Some(backend),
..
}) = cli.command
else {
panic!("expected Open with a backend");
};
assert_eq!(Backend::from(backend), Backend::Ghostty);
assert!(Cli::try_parse_from(["tui-test", "open", "--backend", "libghostty"]).is_err());
}

#[test]
fn run_accepts_readiness_flags() {
let cli =
Expand Down
4 changes: 4 additions & 0 deletions crates/tui-test-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ fn build_request(command: Command) -> anyhow::Result<Request> {
let req = match command {
Command::Open {
shell,
backend,
cols,
rows,
cwd,
Expand All @@ -147,6 +148,7 @@ fn build_request(command: Command) -> anyhow::Result<Request> {
} => Request::Open {
shell: shell.map(Into::into),
program: None,
backend: backend.map(Into::into).unwrap_or_default(),
profile: profile.resolve()?,
cols,
rows,
Expand All @@ -158,6 +160,7 @@ fn build_request(command: Command) -> anyhow::Result<Request> {
Command::Run {
program,
args,
backend,
cols,
rows,
cwd,
Expand All @@ -172,6 +175,7 @@ fn build_request(command: Command) -> anyhow::Result<Request> {
Request::Open {
shell: None,
program: Some(prog),
backend: backend.map(Into::into).unwrap_or_default(),
profile: profile.resolve()?,
cols,
rows,
Expand Down
21 changes: 20 additions & 1 deletion crates/tui-test-cli/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize};
use serde_json::json;

use tui_test::{
Engine, OpenOptions, Operation, OperationResult, RunOptions, ScreenshotResult, TuiTestError,
Backend, Engine, OpenOptions, Operation, OperationResult, RunOptions, ScreenshotResult,
TuiTestError,
};

pub use tui_test::{ErrorKind, MouseAction, Timeouts};
Expand All @@ -14,6 +15,8 @@ pub enum Request {
Open {
shell: Option<tui_test::shell::Shell>,
program: Option<Vec<String>>,
#[serde(default)]
backend: Backend,
/// Terminal settings, already resolved from the config file by the
/// client. The daemon never reads that file: it is long-lived and
/// shared, so it has no single working directory to resolve a
Expand Down Expand Up @@ -154,6 +157,7 @@ impl Request {
Request::Open {
shell,
program,
backend,
profile,
cols,
rows,
Expand All @@ -168,6 +172,7 @@ impl Request {
.next()
.ok_or_else(|| TuiTestError::usage("empty program"))?;
Ok(Operation::Run(RunOptions {
backend,
profile,
program: executable,
args: parts.collect(),
Expand All @@ -180,6 +185,7 @@ impl Request {
}))
} else {
Ok(Operation::Open(OpenOptions {
backend,
profile,
shell,
cols,
Expand Down Expand Up @@ -393,6 +399,7 @@ mod tests {
Request::Open {
shell: None,
program: None,
backend: Backend::default(),
profile: Default::default(),
cols: 80,
rows: 30,
Expand All @@ -411,18 +418,30 @@ mod tests {
match request {
Request::Open {
wait_ready,
backend,
cols,
timeouts,
..
} => {
assert_eq!(wait_ready, None);
assert_eq!(backend, Backend::Alacritty);
assert_eq!(cols, 80);
assert_eq!(timeouts, Timeouts::default());
}
other => panic!("expected Open, got {other:?}"),
}
}

#[test]
fn open_round_trips_a_requested_backend() {
let raw = r#"{"kind":"open","shell":null,"program":null,"backend":"ghostty",
"cols":80,"rows":30,"cwd":null,"env":[]}"#;
match serde_json::from_str::<Request>(raw).expect("deserialize open") {
Request::Open { backend, .. } => assert_eq!(backend, Backend::Ghostty),
other => panic!("expected Open, got {other:?}"),
}
}

#[test]
fn waits_accept_a_concrete_timeout_from_older_clients() {
let raw = r#"{"kind":"wait_idle","timeout_ms":1234}"#;
Expand Down
40 changes: 40 additions & 0 deletions crates/tui-test-cli/tests/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,46 @@ fn sleeper() -> Vec<&'static str> {
}
}

fn blinking_program() -> Vec<&'static str> {
if cfg!(windows) {
vec![
"pwsh",
"-NoLogo",
"-NoProfile",
"-Command",
"[Console]::Write(\"`e[5mX`e[0m\"); Start-Sleep -Seconds 30",
]
} else {
vec!["sh", "-c", "printf '\\033[5mX\\033[0m'; sleep 30"]
}
}

#[test]
fn ghostty_backend_is_used_end_to_end() {
let sandbox = Sandbox::new("ghostty-backend");
let mut args = vec![
"run",
"--backend",
"ghostty",
"--cols",
"10",
"--rows",
"2",
"--",
];
args.extend(blinking_program());
sandbox.ok(&args);
sandbox.ok(&["wait", "text", "X"]);

let raw = sandbox.ok(&["--json", "cells", "0", "0"]);
let payload: serde_json::Value = serde_json::from_str(&raw).expect("cells json");
assert_eq!(
payload["data"]["cells"][0]["blink"],
serde_json::Value::Bool(true),
"Ghostty preserves SGR blink: {payload}"
);
}

fn interactive_reader() -> &'static str {
if cfg!(windows) {
"Write-Output ('reader-'+'ready'); $null = Read-Host"
Expand Down
3 changes: 3 additions & 0 deletions crates/tui-test/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ impl Timeouts {

#[derive(Debug, Clone)]
pub struct OpenOptions {
pub backend: crate::terminal::backend::Backend,
pub shell: Option<Shell>,
/// Terminal settings, already resolved from the config file by the
/// client. The daemon never reads that file: it is long-lived and shared,
Expand All @@ -46,6 +47,7 @@ pub struct OpenOptions {
impl Default for OpenOptions {
fn default() -> Self {
Self {
backend: crate::terminal::backend::Backend::default(),
shell: None,
profile: crate::profile::Profile::default(),
cols: crate::config::DEFAULT_COLS,
Expand All @@ -60,6 +62,7 @@ impl Default for OpenOptions {

#[derive(Debug, Clone)]
pub struct RunOptions {
pub backend: crate::terminal::backend::Backend,
pub program: String,
pub args: Vec<String>,
/// Terminal settings, already resolved from the config file by the
Expand Down
10 changes: 8 additions & 2 deletions crates/tui-test/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ pub struct LiveFrame {
fn operation_summary(operation: &Operation) -> String {
match operation {
Operation::Open(options) => format!(
"Open {{ shell: {:?}, scrollback: {}, {}x{}, cwd: {:?}, wait_ready: {:?}, timeouts: {:?}, env: <{} vars> }}",
"Open {{ backend: {}, shell: {:?}, scrollback: {}, {}x{}, cwd: {:?}, wait_ready: {:?}, timeouts: {:?}, env: <{} vars> }}",
options.backend.as_str(),
options.shell,
options.profile.scrollback,
options.cols,
Expand All @@ -64,7 +65,8 @@ fn operation_summary(operation: &Operation) -> String {
options.env.len()
),
Operation::Run(options) => format!(
"Run {{ program: {:?}, args: {:?}, scrollback: {}, {}x{}, cwd: {:?}, wait_ready: {:?}, timeouts: {:?}, env: <{} vars> }}",
"Run {{ backend: {}, program: {:?}, args: {:?}, scrollback: {}, {}x{}, cwd: {:?}, wait_ready: {:?}, timeouts: {:?}, env: <{} vars> }}",
options.backend.as_str(),
options.program,
options.args,
options.profile.scrollback,
Expand Down Expand Up @@ -138,6 +140,7 @@ impl Engine {
self.spawn(
options.shell,
None,
options.backend,
options.profile,
options.cols,
options.rows,
Expand All @@ -155,6 +158,7 @@ impl Engine {
self.spawn(
None,
Some(program),
options.backend,
options.profile,
options.cols,
options.rows,
Expand All @@ -170,6 +174,7 @@ impl Engine {
&self,
shell: Option<crate::shell::Shell>,
program: Option<Vec<String>>,
backend: crate::terminal::backend::Backend,
profile: crate::profile::Profile,
cols: u16,
rows: u16,
Expand All @@ -192,6 +197,7 @@ impl Engine {
let session = TerminalSession::open(
shell,
program.clone(),
backend,
profile,
cols,
rows,
Expand Down
1 change: 1 addition & 0 deletions crates/tui-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ mod session;
pub use api::*;
pub use engine::Engine;
pub use runtime::{global_registry, Session, SessionHandle, SessionRegistry};
pub use terminal::backend::Backend;
15 changes: 11 additions & 4 deletions crates/tui-test/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::time::Instant;
use crate::logger::Logger;
use crate::profile::Profile;
use crate::shell::{self, Shell};
use crate::terminal::alacritty::AlacrittyEmu;
use crate::terminal::backend::Backend;
use crate::terminal::emu::Emulator;
use crate::terminal::integration::CommandTracker;
use crate::terminal::pty::{Pty, SpawnOptions};
Expand Down Expand Up @@ -52,6 +52,7 @@ impl Session {
pub fn open(
shell: Option<Shell>,
program: Option<Vec<String>>,
backend: Backend,
profile: Profile,
cols: u16,
rows: u16,
Expand All @@ -61,6 +62,8 @@ impl Session {
logger: Arc<Logger>,
recording_path: PathBuf,
) -> anyhow::Result<Self> {
let emu = backend.build(cols, rows, &profile)?;

let (pty, reader) = if let Some(program) = &program {
let (target, args) = program
.split_first()
Expand All @@ -80,7 +83,7 @@ impl Session {
};

let state = Arc::new(Mutex::new(TermState {
emu: Box::new(AlacrittyEmu::new(cols, rows, &profile)),
emu,
tracker: CommandTracker::new(),
last_change: Instant::now(),
awaiting_start: None,
Expand Down Expand Up @@ -145,8 +148,12 @@ impl Session {
});

logger.event(&format!(
"session open shell={:?} program={:?} {}x{}",
shell, program, cols, rows
"session open shell={:?} program={:?} backend={} {}x{}",
shell,
program,
backend.as_str(),
cols,
rows
));

Ok(Session {
Expand Down
Loading
Loading