Skip to content
Draft
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ way.
| --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `state` | cwd, size, cursor, window title, last command + exit code, effective timeouts, text snapshot. |
| `text [--full]` | Plain text of the viewport (or scrollback). |
| `find text "T" [selector options]` | Return selected matches with zero-based row/column spans. |
| `screenshot [-o file.svg] [--full]` | Terminal text to stdout, or a crisp full-color SVG image (svg-term-style window) to a file. |
| `cells X Y [W H]` | Per-cell attributes (char, fg, bg, flags). |
| `get command\|output\|exit-code\|cwd\|cursor\|size\|title` | Structured getters. |
Expand Down Expand Up @@ -249,14 +250,22 @@ print the screen bare.

| Command | Description |
| ------------------------------------------------------------------------------- | ------------------------------------------ |
| `expect text "T" [--regex --full --no-strict --not --fg C --bg C --timeout MS]` | Visibility + optional color. |
| `expect text "T" [selector/style options]` | Visibility plus optional color and cell styles. |
| `expect title "T" [--regex --not --timeout MS]` | Window title set with OSC 0/2. |
| `expect exit-code N [--timeout MS]` | Last command's exit code. |
| `expect output "T" [--regex]` | Last command's captured output. |
| `expect snapshot NAME [-u] [--include-colors --include-title]` | Compare against `__snapshots__/NAME.snap`. `--include-title` adds the window title to the frame. |

Colors accept ANSI-256 (`9`), hex (`#ff0000`), or rgb (`255,0,0`).

Text selectors support `--after-text`, `--before-text`, `--whitespace
normalize`, `--match any|unique|first|last`, and zero-based `--nth N`.
Anchors can select their own occurrence with `--after-match` /
`--before-match` or `--after-nth` / `--before-nth`. Style assertions use
`--fg`, `--bg`, `--bold[=false]`, `--italic[=false]`, `--underline-style`,
`--underline-color`, `--inverse[=false]`, `--hidden[=false]`,
`--strikethrough[=false]`, and `--blink[=false]`.

### Screenshots

Screenshots render a snapshot of the session in the current terminal by default, but can render an SVG using the `-o` output flag. Nerd Font icons are embedded as vector paths, so SVGs remain self-contained without changing the font stack for regular text.
Expand Down
9 changes: 8 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ without parsing text:
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `state` | cwd, size, cursor, last command + exit code, timeouts, and a text snapshot. |
| `text [--full]` | Rendered viewport text, or full scrollback with `--full`. |
| `find text "T" [selector options]` | Selected matches with zero-based row/column spans. |
| `screenshot [PATH] [-o FILE] [--full]` | Terminal text to stdout, or a full-color SVG image (crisp at any zoom, svg-term-style window) when a path is given. |
| `cells X Y [W H]` | Per-cell attributes (char, fg, bg, flags) for a region. |
| `get command\|output\|exit-code\|cwd\|cursor\|size\|title` | One structured field. |
Expand Down Expand Up @@ -113,14 +114,20 @@ without parsing text:

| Command | Description |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `expect text "T" [--regex --full --no-strict --not --fg C --bg C --timeout MS]` | Visibility plus optional color. `--no-strict` relaxes a strict single-match. |
| `expect text "T" [selector/style options]` | Visibility plus optional color and cell styles. `--no-strict` selects the first match. |
| `expect title "T" [--regex --not --timeout MS]` | The window title set with `OSC 0`/`OSC 2`. An unset title matches nothing. |
| `expect exit-code N [--timeout MS]` | The last command's exit code. Waits for the command to finish first. |
| `expect output "T" [--regex]` | The last command's captured output. |
| `expect snapshot NAME [-u] [--include-colors --include-title]` | Compare the screen against `__snapshots__/NAME.snap`; `-u` writes/updates it. `--include-title` records the window title in the frame; off by default because a prompt often sets it to a host and path. |

Colors accept ansi-256 (`9`), hex (`#ff0000`), or rgb (`255,0,0`).

Selector options include `--after-text`, `--before-text`, `--whitespace
normalize`, `--match any|unique|first|last`, and zero-based `--nth`. Anchors
also accept `--after-match` / `--before-match` and `--after-nth` /
`--before-nth`. Styles include `--fg`, `--bg`, boolean SGR attributes such as
`--bold[=false]`, and underline style/color.

### Recording, monitor & self-docs

| Command | Description |
Expand Down
189 changes: 173 additions & 16 deletions crates/tui-test-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ pub enum Command {
#[command(subcommand)]
what: ExpectCmd,
},
/// Locate text and return its row/column spans.
Find {
#[command(subcommand)]
what: FindCmd,
},
/// Print the session's recording (asciinema v2 cast) to stdout.
///
/// Redirect to a `.cast` file, then `asciinema play` it or render a GIF
Expand Down Expand Up @@ -479,6 +484,59 @@ mod tests {
assert_eq!(timeout, None);
}

#[test]
fn find_text_accepts_scope_and_occurrence() {
let cli = Cli::try_parse_from([
"tui-test",
"find",
"text",
"Save",
"--after-text",
"Settings",
"--after-match",
"last",
"--whitespace",
"normalize",
"--nth",
"1",
])
.expect("parse find text");
let Some(Command::Find {
what: FindCmd::Text { selector, .. },
}) = cli.command
else {
panic!("expected Find text");
};
assert_eq!(selector.after_text.as_deref(), Some("Settings"));
assert_eq!(selector.after_match, Some(MatchArg::Last));
assert_eq!(selector.whitespace, WhitespaceArg::Normalize);
assert_eq!(selector.nth, Some(1));
}

#[test]
fn expect_text_accepts_generic_styles() {
let cli = Cli::try_parse_from([
"tui-test",
"expect",
"text",
"Warning",
"--bold",
"--italic=false",
"--underline-style",
"curly",
])
.expect("parse styled expectation");
let Some(Command::Expect {
what: ExpectCmd::Text { style, .. },
}) = cli.command
else {
panic!("expected Expect text");
};
assert_eq!(style.bold, Some(true));
assert_eq!(style.italic, Some(false));
assert_eq!(style.underline_style.as_deref(), Some("curly"));
}

#[test]
fn expect_exit_code_accepts_a_timeout() {
let cli =
Expand Down Expand Up @@ -661,6 +719,113 @@ impl ScrollDir {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "lower")]
pub enum WhitespaceArg {
Exact,
Normalize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "lower")]
pub enum MatchArg {
Any,
Unique,
First,
Last,
}

#[derive(Args)]
pub struct TextSelectorArgs {
/// Treat the target text as a regular expression.
#[arg(long)]
pub regex: bool,
/// Search the full scrollback, not just the visible viewport.
#[arg(long)]
pub full: bool,
/// Compare whitespace exactly or collapse runs and line breaks.
#[arg(long, value_enum, default_value_t = WhitespaceArg::Exact)]
pub whitespace: WhitespaceArg,
/// Search only after this literal anchor.
#[arg(long)]
pub after_text: Option<String>,
/// Treat --after-text as a regular expression.
#[arg(long, requires = "after_text")]
pub after_regex: bool,
/// Select the anchor occurrence used by --after-text.
#[arg(
long,
value_enum,
requires = "after_text",
conflicts_with = "after_nth"
)]
pub after_match: Option<MatchArg>,
/// Use the zero-based nth --after-text occurrence.
#[arg(long, requires = "after_text", conflicts_with = "after_match")]
pub after_nth: Option<usize>,
/// Search only before this literal anchor.
#[arg(long)]
pub before_text: Option<String>,
/// Treat --before-text as a regular expression.
#[arg(long, requires = "before_text")]
pub before_regex: bool,
/// Select the anchor occurrence used by --before-text.
#[arg(
long,
value_enum,
requires = "before_text",
conflicts_with = "before_nth"
)]
pub before_match: Option<MatchArg>,
/// Use the zero-based nth --before-text occurrence.
#[arg(long, requires = "before_text", conflicts_with = "before_match")]
pub before_nth: Option<usize>,
/// Select all, unique, first, or last target occurrences.
#[arg(long = "match", value_enum, conflicts_with = "nth")]
pub match_mode: Option<MatchArg>,
/// Select the zero-based nth target occurrence.
#[arg(long, conflicts_with = "match_mode")]
pub nth: Option<usize>,
}

#[derive(Args)]
pub struct TextStyleArgs {
/// Required foreground color.
#[arg(long)]
pub fg: Option<String>,
/// Required background color.
#[arg(long)]
pub bg: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub bold: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub dim: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub italic: Option<bool>,
#[arg(long)]
pub underline_style: Option<String>,
#[arg(long)]
pub underline_color: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub inverse: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub hidden: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub strikethrough: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
pub blink: Option<bool>,
}

#[derive(Subcommand)]
pub enum FindCmd {
/// Find text and return its row/column spans.
Text {
text: String,
#[command(flatten)]
selector: TextSelectorArgs,
},
}

#[derive(Subcommand)]
pub enum WaitCmd {
/// Wait until text/regex appears on screen (the most precise wait).
Expand Down Expand Up @@ -736,28 +901,20 @@ pub enum WaitCmd {
pub enum ExpectCmd {
/// Assert text is visible, optionally with a required color.
Text {
/// Text or regex to match.
text: String,
/// Treat <text> as a regular expression.
#[arg(long)]
regex: bool,
/// Search the full scrollback, not just the visible viewport.
#[arg(long)]
full: bool,
#[command(flatten)]
selector: TextSelectorArgs,
/// Allow multiple matches instead of requiring exactly one.
#[arg(long = "no-strict")]
#[arg(
long = "no-strict",
conflicts_with_all = ["match_mode", "nth"]
)]
no_strict: bool,
/// Invert: assert the text is NOT present.
#[arg(long)]
not: bool,
/// Require this foreground color on the match: `default`, an ansi256
/// index (0-255), hex (#rrggbb), or rgb (r,g,b).
#[arg(long)]
fg: Option<String>,
/// Require this background color on the match: `default`, an ansi256
/// index (0-255), hex (#rrggbb), or rgb (r,g,b).
#[arg(long)]
bg: Option<String>,
#[command(flatten)]
style: Box<TextStyleArgs>,
/// Timeout in milliseconds.
#[arg(long, value_name = "MS")]
timeout: Option<u64>,
Expand Down
Loading