Skip to content

Commit d14e4a0

Browse files
authored
fix: preserve OSC 8 hyperlinks in terminal output (#273)
1 parent e86414a commit d14e4a0

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

crates/tempo-common/src/cli/terminal.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ pub fn truncate(s: &str, max: usize) -> String {
111111
}
112112

113113
/// Print a right-aligned label/value field to stdout with a custom label width.
114+
///
115+
/// The value is sanitized to strip control characters. For pre-formatted values
116+
/// (e.g. containing OSC 8 hyperlinks), use [`print_field_raw_w`] instead.
114117
pub fn print_field_w(width: usize, label: &str, value: &str) {
115118
let safe_value = sanitize_for_terminal(value);
116119
println!("{label:>width$}: {safe_value}");
@@ -121,6 +124,17 @@ pub fn print_field(label: &str, value: &str) {
121124
print_field_w(14, label, value);
122125
}
123126

127+
/// Like [`print_field_w`] but skips sanitization — use for values that already
128+
/// contain intentional terminal sequences (e.g. OSC 8 hyperlinks from [`hyperlink`]).
129+
pub fn print_field_raw_w(width: usize, label: &str, value: &str) {
130+
println!("{label:>width$}: {value}");
131+
}
132+
133+
/// Like [`print_field`] but skips sanitization.
134+
pub fn print_field_raw(label: &str, value: &str) {
135+
print_field_raw_w(14, label, value);
136+
}
137+
124138
#[cfg(test)]
125139
mod tests {
126140
use super::*;
@@ -198,4 +212,25 @@ mod tests {
198212
fn truncate_long_adds_ellipsis() {
199213
assert_eq!(truncate("hello world", 5), "hell…");
200214
}
215+
216+
#[test]
217+
fn print_field_raw_preserves_osc8_hyperlinks() {
218+
// Simulate what hyperlink() produces when the terminal supports OSC 8.
219+
let osc8 = "\x1b]8;;https://example.com/address/0xabc\x070xabc\x1b]8;;\x07";
220+
221+
// print_field_raw_w uses the same format! — verify it preserves
222+
// the ESC (\x1b) and BEL (\x07) bytes that OSC 8 requires.
223+
let formatted = format!("{:>10}: {osc8}", "Wallet");
224+
assert!(
225+
formatted.contains('\x1b') && formatted.contains('\x07'),
226+
"print_field_raw_w must preserve intentional OSC 8 sequences"
227+
);
228+
229+
// Whereas print_field_w would strip them (the original bug).
230+
let sanitized = format!("{:>10}: {}", "Wallet", sanitize_for_terminal(osc8));
231+
assert!(
232+
!sanitized.contains('\x1b') && !sanitized.contains('\x07'),
233+
"print_field_w must sanitize control characters"
234+
);
235+
}
201236
}

crates/tempo-wallet/src/commands/keys.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tempo_common::{
1212
cli::{
1313
context::Context,
1414
output,
15-
terminal::{address_link, print_field_w},
15+
terminal::{address_link, print_field_raw_w, print_field_w},
1616
},
1717
error::TempoError,
1818
keys::Keystore,
@@ -86,7 +86,7 @@ fn render_keys(response: &KeysResponse, keystore: &Keystore, sessions: &[session
8686

8787
if let Some(wallet) = &key.wallet_address {
8888
let wallet_link = address_link(explorer.unwrap_or_default(), wallet);
89-
print_field_w(10, "Wallet", &wallet_link);
89+
print_field_raw_w(10, "Wallet", &wallet_link);
9090
}
9191
if let (Some(bal), Some(sym)) = (key.balance.as_deref(), key.symbol.as_deref()) {
9292
if let Some(bb) = balance_breakdown(bal, sym, Some(entry.chain_id), sessions) {
@@ -113,7 +113,7 @@ fn render_keys(response: &KeysResponse, keystore: &Keystore, sessions: &[session
113113
print_field_w(10, "Key", pk.as_str());
114114
} else {
115115
let key_link = address_link(explorer.unwrap_or_default(), &key.address);
116-
print_field_w(10, "Key", &key_link);
116+
print_field_raw_w(10, "Key", &key_link);
117117
}
118118
if let Some(net) = explorer {
119119
print_field_w(10, "Chain", net.as_str());

0 commit comments

Comments
 (0)