Coalesce wrapped OSC 8 hyperlinks in the TUI terminal (#34778)

## What changed

- Track adjacent cells with the same OSC 8 destination and emit a single
  hyperlink around their visible text.
- Close active hyperlinks before non-link content and at the end of a draw.
- Cover a production-length browser authentication URL at narrow terminal
  width, including the remote-login guidance and cancellation footer.

GitOrigin-RevId: 9b87166ce78578137dfd2bf19db359ac83a908ee
This commit is contained in:
Eric Traut
2026-07-22 15:28:46 +00:00
committed by copyberry
parent 80f3c3141e
commit ff8d521ba1
3 changed files with 146 additions and 17 deletions

View File

@@ -79,6 +79,17 @@ fn display_width(s: &str) -> usize {
visible.width()
}
fn osc8_hyperlink_parts(symbol: &str) -> Option<(&str, &str)> {
let content = symbol.strip_prefix("\x1b]8;;")?;
let destination_end = content.find('\x07')?;
let destination = &content[..destination_end];
if destination.is_empty() {
return None;
}
let visible = content[destination_end + 1..].strip_suffix("\x1b]8;;\x07")?;
Some((destination, visible))
}
pub struct Frame<'a> {
/// Where should the cursor be after drawing this frame?
///
@@ -646,17 +657,27 @@ where
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<Position> = None;
let mut active_hyperlink: Option<String> = None;
for command in commands {
let (x, y) = match command {
let (x, y) = match &command {
DrawCommand::Put { x, y, .. } => (x, y),
DrawCommand::ClearToEnd { x, y, .. } => (x, y),
};
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
queue!(writer, MoveTo(x, y))?;
let hyperlink = match &command {
DrawCommand::Put { cell, .. } => osc8_hyperlink_parts(cell.symbol()),
DrawCommand::ClearToEnd { .. } => None,
};
let destination = hyperlink.map(|(destination, _)| destination);
let hyperlink_changed = active_hyperlink.as_deref() != destination;
if hyperlink_changed && active_hyperlink.is_some() {
queue!(writer, Print("\x1b]8;;\x07"))?;
}
last_pos = Some(Position { x, y });
match command {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if *x == p.x + 1 && *y == p.y) {
queue!(writer, MoveTo(*x, *y))?;
}
last_pos = Some(Position { x: *x, y: *y });
match &command {
DrawCommand::Put { cell, .. } => {
if cell.modifier != modifier {
let diff = ModifierDiff {
@@ -675,16 +696,26 @@ where
bg = cell.bg;
}
queue!(writer, Print(cell.symbol()))?;
if hyperlink_changed && let Some(destination) = destination {
queue!(writer, Print(format!("\x1b]8;;{destination}\x07")))?;
}
let symbol = hyperlink.map_or_else(|| cell.symbol(), |(_, visible)| visible);
queue!(writer, Print(symbol))?;
}
DrawCommand::ClearToEnd { bg: clear_bg, .. } => {
queue!(writer, SetAttribute(crossterm::style::Attribute::Reset))?;
modifier = Modifier::empty();
queue!(writer, SetBackgroundColor(clear_bg.into()))?;
bg = clear_bg;
queue!(writer, SetBackgroundColor((*clear_bg).into()))?;
bg = *clear_bg;
queue!(writer, Clear(crossterm::terminal::ClearType::UntilNewLine))?;
}
}
if hyperlink_changed {
active_hyperlink = destination.map(str::to_owned);
}
}
if active_hyperlink.is_some() {
queue!(writer, Print("\x1b]8;;\x07"))?;
}
queue!(
@@ -771,6 +802,11 @@ mod tests {
use ratatui::backend::WindowSize;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::Wrap;
struct CaptureBackend {
output: Vec<u8>,
@@ -918,6 +954,41 @@ mod tests {
);
}
#[test]
fn terminal_draw_coalesces_wrapped_hyperlink_output() {
let auth_url = format!(
"https://auth.openai.com/oauth/authorize?response_type=code&state={}",
"x".repeat(/*n*/ 400)
);
let width = 44;
let height = 20;
let area = Rect::new(0, 0, width, height);
let mut terminal =
Terminal::with_options(CaptureBackend::new(width, height)).expect("terminal");
terminal.set_viewport_area(area);
terminal
.draw(|frame| {
Paragraph::new(vec![
Line::from(vec![" ".into(), auth_url.as_str().cyan().underlined()]),
"".into(),
" Press Esc to cancel".into(),
])
.wrap(Wrap { trim: false })
.render(area, frame.buffer_mut());
crate::terminal_hyperlinks::mark_url_hyperlink(frame.buffer_mut(), area, &auth_url);
})
.expect("draw");
let output = terminal.backend().output();
let open = format!("\x1b]8;;{auth_url}\x07");
let close = "\x1b]8;;\x07";
assert_eq!(output.matches(&open).count(), 1);
assert_eq!(output.matches(close).count(), 1);
let footer = output.find("Press").expect("footer");
assert!(output.find(close).expect("hyperlink close") < footer);
}
#[test]
fn terminal_draw_applies_requested_cursor_style() {
let mut output = Vec::new();

View File

@@ -1032,6 +1032,21 @@ mod tests {
use std::sync::Arc;
use tempfile::TempDir;
const PRODUCTION_LENGTH_AUTH_URL: &str = concat!(
"https://auth.openai.com/oauth/authorize?",
"response_type=code&",
"client_id=app_EMoamEEZ73f0CkXaXp7hrann&",
"redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&",
"scope=openid%20profile%20email%20offline_access%20",
"api.connectors.read%20api.connectors.invoke&",
"code_challenge=1YM3Z8QbrLbdt9C3eX3j7UQ4GmFRmKz4OeVYwD6s5xA&",
"code_challenge_method=S256&",
"id_token_add_organizations=true&",
"codex_cli_simplified_flow=true&",
"state=8cHjQ4nVx2Yp7Lm9Rk3Wf6Ta1Bs5Du0Ei4Go7Nz2PqM&",
"originator=codex_cli_rs"
);
async fn widget_forced_chatgpt() -> (AuthModeWidget, TempDir) {
let codex_home = TempDir::new().unwrap();
let codex_home_path = codex_home.path().to_path_buf();
@@ -1198,24 +1213,42 @@ mod tests {
}
#[test]
fn continue_in_browser_renders_osc8_hyperlink() {
fn continue_in_browser_preserves_long_link_and_footer_at_narrow_width() {
let runtime = tokio::runtime::Runtime::new().unwrap();
let (widget, _tmp) = runtime.block_on(widget_forced_chatgpt());
let url = "https://auth.example.com/login?state=abc123";
widget.set_animations_suppressed(/*suppressed*/ true);
*widget.sign_in_state.write().unwrap() =
SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
login_id: "login-1".to_string(),
auth_url: url.to_string(),
auth_url: PRODUCTION_LENGTH_AUTH_URL.to_string(),
});
// Render into a narrow buffer so the URL wraps across multiple rows.
let area = Rect::new(0, 0, 30, 20);
let width = 44;
let height = 30;
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
widget.render_continue_in_browser(area, &mut buf);
// Every character of the URL should be present as an OSC 8 cell.
let found = collect_osc8_chars(&buf, area, url);
assert_eq!(found, url, "OSC 8 hyperlink should cover the full URL");
let found = collect_osc8_chars(&buf, area, PRODUCTION_LENGTH_AUTH_URL);
assert_eq!(
found, PRODUCTION_LENGTH_AUTH_URL,
"OSC 8 hyperlink should cover the full URL"
);
let mut terminal = crate::custom_terminal::Terminal::with_options(
crate::test_backend::VT100Backend::new(width, height),
)
.expect("terminal");
terminal.set_viewport_area(area);
terminal
.draw(|frame| widget.render_continue_in_browser(area, frame.buffer_mut()))
.expect("draw");
let contents = terminal.backend().to_string();
insta::assert_snapshot!("continue_in_browser_narrow_long_url", contents);
assert!(contents.contains("On a remote or headless machine?"));
assert!(contents.contains("Press esc to cancel"));
}
#[test]

View File

@@ -0,0 +1,25 @@
---
source: tui/src/onboarding/auth.rs
expression: contents
---
Finish signing in via your browser
If the link doesn't open automatically,
open the following link to authenticate:
https://auth.openai.com/oauth/authorize?re
sponse_type=code&client_id=app_EMoamEEZ73f0C
kXaXp7hrann&redirect_uri=http%3A%2F%2Flocalh
ost%3A1455%2Fauth%2Fcallback&scope=openid%20
profile%20email%20offline_access%20api.conne
ctors.read%20api.connectors.invoke&code_chal
lenge=1YM3Z8QbrLbdt9C3eX3j7UQ4GmFRmKz4OeVYwD
6s5xA&code_challenge_method=S256&id_token_ad
d_organizations=true&codex_cli_simplified_fl
ow=true&state=8cHjQ4nVx2Yp7Lm9Rk3Wf6Ta1Bs5Du
0Ei4Go7Nz2PqM&originator=codex_cli_rs
On a remote or headless machine? Press esc
and choose Sign in with Device Code.
Press esc to cancel