From 88334201cbfe97053c8716f702df85dc638cefa5 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Fri, 13 Feb 2026 22:05:56 +0200 Subject: [PATCH 1/9] Fix script permissions --- crates/term-transcript-cli/package.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 crates/term-transcript-cli/package.sh diff --git a/crates/term-transcript-cli/package.sh b/crates/term-transcript-cli/package.sh old mode 100644 new mode 100755 From 3e7b612bbd6b17c6387e1eb800baaa444f73ede2 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Tue, 17 Feb 2026 16:31:48 +0200 Subject: [PATCH 2/9] Fix doc nits --- crates/term-transcript-cli/README.md | 2 +- docs/src/cli/build.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/term-transcript-cli/README.md b/crates/term-transcript-cli/README.md index 0b2cdd16..b13d761d 100644 --- a/crates/term-transcript-cli/README.md +++ b/crates/term-transcript-cli/README.md @@ -65,7 +65,7 @@ shall be dual licensed as above, without any additional terms or conditions. [`term-transcript`]: https://crates.io/crates/term-transcript [fmt-subscriber]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/index.html -[rainbow-script-link]: ../e2e-tests/rainbow/bin/rainbow +[rainbow-script-link]: ../../e2e-tests/rainbow/bin/rainbow [test-snapshot-link]: tests/snapshots/test.svg [test-color-snapshot-link]: tests/snapshots/test-fail.svg [test-link]: tests/e2e.rs diff --git a/docs/src/cli/build.md b/docs/src/cli/build.md index f9e7e86f..9181503e 100644 --- a/docs/src/cli/build.md +++ b/docs/src/cli/build.md @@ -14,12 +14,12 @@ term-transcript --help This requires a Rust toolchain locally installed. -### Minimum supported Rust version +## Minimum supported Rust version The crate supports the latest stable Rust version. It may support previous stable Rust versions, but this is not guaranteed. -### Crate feature: `portable-pty` +## Crate feature: `portable-pty` Specify `--features portable-pty` in the installation command to enable the pseudo-terminal (PTY) support (note that PTY capturing still needs @@ -29,7 +29,7 @@ which means that programs dependent on [`isatty`] checks or getting term size can produce different output than if launched in an actual shell (no coloring, no line wrapping etc.). -### Crate feature: `tracing` +## Crate feature: `tracing` Specify `--features tracing` in the installation command to enable tracing of the main performed operations. This could be useful for debugging purposes. From 1bcafdd1ee7a7ba60b580e6866b8d18f9e8d10ac Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Sun, 22 Feb 2026 10:20:38 +0200 Subject: [PATCH 3/9] Implement `start_with` / `ends_with` --- crates/styled-str/src/tests.rs | 30 ++++++++++++++++++++++++ crates/styled-str/src/types/slice.rs | 34 ++++++++++++++++++++++++++++ crates/styled-str/src/types/str.rs | 10 ++++++++ crates/styled-str/tests/proptests.rs | 24 ++++++++++++++++++++ 4 files changed, 98 insertions(+) diff --git a/crates/styled-str/src/tests.rs b/crates/styled-str/src/tests.rs index 7a2e7089..894273e8 100644 --- a/crates/styled-str/src/tests.rs +++ b/crates/styled-str/src/tests.rs @@ -483,3 +483,33 @@ fn debugging_strings() { let debug = format!("{styled:?}"); assert_eq!(debug, r#""[[red on white!]]Hello,\n\"world[[italic]]!\"""#); } + +#[test] +fn starts_with_basics() { + let styled = styled!("[[green]]Hello, [[inverted]]world[[/]]!"); + assert!(styled.starts_with(StyledStr::default())); + assert!(styled.starts_with(styled)); + + assert!(styled.starts_with(styled!("[[green]]Hell"))); + assert!(!styled.starts_with(styled!("Hell"))); + assert!(!styled.starts_with(styled!("[[red]]H"))); + assert!(styled.starts_with(styled!("[[green]]Hello, "))); + assert!(styled.starts_with(styled!("[[green]]Hello, [[inverted]]w"))); + assert!(!styled.starts_with(styled!("[[green]]Hello, w"))); + assert!(styled.starts_with(styled!("[[green]]Hello, [[inverted]]world"))); +} + +#[test] +fn ends_with_basics() { + let styled = styled!("[[green]]Hello, [[inverted]]world[[/]]!"); + assert!(styled.ends_with(StyledStr::default())); + assert!(styled.ends_with(styled)); + + assert!(styled.ends_with(styled!("!"))); + assert!(!styled.ends_with(styled!("d!"))); + assert!(!styled.ends_with(styled!("[[invert]]!"))); + assert!(styled.ends_with(styled!("[[invert]]d[[/]]!"))); + assert!(styled.ends_with(styled!("[[inverted]]world[[/]]!"))); + assert!(!styled.ends_with(styled!("[[inverted]], world[[/]]!"))); + assert!(styled.ends_with(styled!("[[green]]o, [[inverted]]world[[/]]!"))); +} diff --git a/crates/styled-str/src/types/slice.rs b/crates/styled-str/src/types/slice.rs index 2e278286..20dc9dee 100644 --- a/crates/styled-str/src/types/slice.rs +++ b/crates/styled-str/src/types/slice.rs @@ -158,6 +158,40 @@ impl<'a> SpansSlice<'a> { } } + pub(crate) fn start_with(&self, needle: &SpansSlice<'_>) -> bool { + let needle_len = needle.len(); + if needle_len > self.len() { + return false; + } + + needle + .iter() + .zip(self.iter()) + .enumerate() + .all(|(i, (needle_span, this_span))| { + this_span.style == needle_span.style && { + let cmp = this_span.len.cmp(&needle_span.len); + cmp.is_eq() || (i + 1 == needle_len && cmp.is_gt()) + } + }) + } + + pub(crate) fn end_with(&self, needle: &SpansSlice<'_>) -> bool { + let needle_len = needle.len(); + if needle_len > self.len() { + return false; + } + + needle.iter().rev().zip(self.iter().rev()).enumerate().all( + |(i, (needle_span, this_span))| { + this_span.style == needle_span.style && { + let cmp = this_span.len.cmp(&needle_span.len); + cmp.is_eq() || (i + 1 == needle_len && cmp.is_gt()) + } + }, + ) + } + pub(crate) const fn split_at(&self, mid: usize) -> (Self, Self) { assert!( mid <= self.text_end - self.text_start, diff --git a/crates/styled-str/src/types/str.rs b/crates/styled-str/src/types/str.rs index 1cc52a9a..2e47563c 100644 --- a/crates/styled-str/src/types/str.rs +++ b/crates/styled-str/src/types/str.rs @@ -133,6 +133,16 @@ impl<'a> StyledStr<'a> { (start, end) } + /// Checks whether this string starts with a `needle`, matching both its text and styling. + pub fn starts_with(&self, needle: StyledStr<'_>) -> bool { + self.text.starts_with(needle.text) && self.spans.start_with(&needle.spans) + } + + /// Checks whether this string ends with a `needle`, matching both its text and styling. + pub fn ends_with(&self, needle: StyledStr<'_>) -> bool { + self.text.ends_with(needle.text) && self.spans.end_with(&needle.spans) + } + /// Iterates over spans contained in this string. /// /// # Examples diff --git a/crates/styled-str/tests/proptests.rs b/crates/styled-str/tests/proptests.rs index 92a33f88..d325575e 100644 --- a/crates/styled-str/tests/proptests.rs +++ b/crates/styled-str/tests/proptests.rs @@ -303,4 +303,28 @@ proptest! { ) { test_string_slice(styled.as_str(), start..end)?; } + + #[test] + fn starts_with_positive(styled in styled_string(VISIBLE_ASCII, 1..=5)) { + let styled = styled.as_str(); + for end in 0..=styled.text().len() { + let prefix = styled.get(..end).unwrap(); + prop_assert!(styled.starts_with(prefix), "end={end}"); + if end < styled.text().len() { + prop_assert!(!prefix.starts_with(styled), "end={end}"); + } + } + } + + #[test] + fn ends_with_positive(styled in styled_string(VISIBLE_ASCII, 1..=5)) { + let styled = styled.as_str(); + for start in 0..=styled.text().len() { + let suffix = styled.get(start..).unwrap(); + prop_assert!(styled.ends_with(suffix), "start={start}"); + if start > 0 { + prop_assert!(!suffix.ends_with(styled), "start={start}"); + } + } + } } From 32fec17e7d5bfcdf11f496fec851e736838cbae4 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Sun, 22 Feb 2026 10:28:07 +0200 Subject: [PATCH 4/9] Implement `AddAssign` for `StyledStringBuilder` --- crates/styled-str/src/types/string.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/styled-str/src/types/string.rs b/crates/styled-str/src/types/string.rs index 13ba1f28..cd20f805 100644 --- a/crates/styled-str/src/types/string.rs +++ b/crates/styled-str/src/types/string.rs @@ -28,9 +28,10 @@ use crate::{ /// builder.push_style(AnsiColor::BrightGreen.on(AnsiColor::White).bold()); /// builder.push_text("Hello"); /// builder.push_text(","); -/// builder.push_style(Style::new()); +/// // It's possible to use `+=` as syntactic sugar for `push_str()` / `push_style()`. +/// builder += Style::new(); /// builder.push_text(" world"); -/// builder.push_str(styled!("[[it, dim]]!")); +/// builder += styled!("[[it, dim]]!"); /// /// let s = builder.build(); /// assert_eq!( @@ -123,6 +124,20 @@ impl From for StyledStringBuilder { } } +impl ops::AddAssign