Skip to content
Closed
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
35 changes: 33 additions & 2 deletions cli/src/upload_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use context::bazel_bep::common::BepParseResult;
use display::{end_output::EndOutput, message::DisplayMessage};
use pluralizer::pluralize;
use superconsole::{
Hyperlink, Line, Lines, Span,
Hyperlink, Line, Lines, Span, SuperConsole,
style::{Attribute, Color, Stylize, style},
};
use tempfile::TempDir;
Expand Down Expand Up @@ -859,9 +859,19 @@ pub fn get_bundle_upload_id_message(bundle_upload_id: &str) -> String {
format!("🏷️ Bundle Upload ID: {}", bundle_upload_id)
}

/// Plain text unless superconsole is drawing to an interactive terminal: log viewers such as
/// GitHub Actions don't support OSC 8 and drop the link text along with the escape.
fn hyperlink_span(url: String, attribute: Attribute) -> anyhow::Result<Span> {
url_span(url, attribute, SuperConsole::compatible())
}

fn url_span(url: String, attribute: Attribute, clickable: bool) -> anyhow::Result<Span> {
let span = Span::new_styled(style(url.clone()).attribute(attribute))?;
Ok(span.with_hyperlink(Some(Hyperlink::new(url))))
Ok(if clickable {
span.with_hyperlink(Some(Hyperlink::new(url)))
} else {
span
})
}

impl UploadRunResult {
Expand Down Expand Up @@ -1202,3 +1212,24 @@ impl EndOutput for UploadRunResult {
Ok(output)
}
}

#[cfg(test)]
mod tests {
use superconsole::{Line, style::Attribute};

use super::url_span;

const URL: &str = "https://app.trunk.io/my-org/flaky-tests/test/1";

fn rendered(clickable: bool) -> String {
Line::from_iter([url_span(URL.into(), Attribute::Underlined, clickable).unwrap()]).render()
}

#[test]
fn url_span_adds_osc8_only_when_clickable() {
assert!(rendered(true).contains("\x1b]8;;"));
let plain = rendered(false);
assert!(!plain.contains("\x1b]8;;"));
assert!(plain.contains(URL));
}
}
62 changes: 59 additions & 3 deletions display/src/render.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,63 @@
use std::{
io::{self, Write},
sync::mpsc::{Receiver, Sender, channel},
thread::{self, JoinHandle},
};

use superconsole::{Dimensions, SuperConsole};
use superconsole::{Builder, Dimensions, SuperConsole};
use terminal_size::{Height, Width, terminal_size};

use crate::message::DisplayMessage;

/// Cursor hide/show escapes superconsole wraps around every frame. Log viewers such as GitHub
/// Actions don't recognise them and print `25l`/`25h` at the start of each line.
const CURSOR_VISIBILITY: [&[u8]; 2] = [b"\x1b[?25l", b"\x1b[?25h"];

/// Stderr minus [`CURSOR_VISIBILITY`], for when there is no interactive terminal.
struct NonTtyStderr(io::Stderr);

impl Write for NonTtyStderr {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write_all(&strip_cursor_visibility(buf))?;
Ok(buf.len())
}

fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}

fn strip_cursor_visibility(buf: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(buf.len());
let mut rest = buf;
while !rest.is_empty() {
match CURSOR_VISIBILITY.iter().find(|seq| rest.starts_with(seq)) {
Some(seq) => rest = &rest[seq.len()..],
None => {
out.push(rest[0]);
rest = &rest[1..];
}
}
}
out
}

fn forced_console(fallback_size: Dimensions) -> SuperConsole {
let mut builder = Builder::new();
builder.write_to(Box::new(NonTtyStderr(io::stderr())));
builder
.build_forced(fallback_size)
.unwrap_or_else(|_| SuperConsole::forced_new(fallback_size))
}

fn output_render_loop(receiver: Receiver<DisplayMessage>) {
let mut superconsole = match SuperConsole::new() {
Some(console) => console,
None => {
tracing::warn!("Failed to create superconsole because of incompatible TTY");
let size = terminal_size();
if let Some((Width(width), Height(height))) = size {
SuperConsole::forced_new(Dimensions {
forced_console(Dimensions {
width: width as usize,
height: height as usize,
})
Expand All @@ -27,7 +69,7 @@ fn output_render_loop(receiver: Receiver<DisplayMessage>) {
tracing::warn!("Using default dimensions for superconsole");
tracing::warn!("This may not render correctly in some environments");
tracing::warn!("Please use a TTY compatible terminal for best results");
SuperConsole::forced_new(Dimensions {
forced_console(Dimensions {
width: 143,
height: 24,
})
Expand All @@ -46,3 +88,17 @@ pub fn spin_up_renderer() -> (JoinHandle<()>, Sender<DisplayMessage>) {
});
(render_handle, render_sender)
}

#[cfg(test)]
mod tests {
use super::strip_cursor_visibility;

#[test]
fn strips_only_cursor_visibility_escapes() {
let frame = b"\x1b[?25l\x1b[1G\x1b[1mUpload:\x1b[0m\x1b[K\n\x1b[?25h\x1b[J";
assert_eq!(
strip_cursor_visibility(frame),
b"\x1b[1G\x1b[1mUpload:\x1b[0m\x1b[K\n\x1b[J"
);
}
}
Loading