mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-09 00:02:16 +00:00
feat(terminal): an OSC 8 link rests under a faint dotted underline (#1078)
* feat(terminal): an OSC 8 link rests under a faint dotted underline A hyperlink a program emits (OSC 8, which panes ask for via FORCE_HYPERLINK) looked like plain text until the pointer found it. It now carries iTerm2's faint dotted underline, drawn by the SGR 4:4 dotted painter, and swaps to the hovered link's solid line under the pointer. A cell the program underlined or gave an underline colour keeps its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(terminal): an OSC 8 link's dots stay faint under the block cursor --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
l0ng-ai
parent
02620cfa48
commit
85b739650c
+10
-2
@@ -19,6 +19,8 @@ use gpui_component::ActiveTheme as _;
|
||||
use super::view::{TerminalView, should_show_context_menu};
|
||||
use crate::core::config::Config;
|
||||
|
||||
mod osc8_underline;
|
||||
|
||||
const DIM_OPACITY: f32 = 0.66;
|
||||
|
||||
/// How much of the text's own colour a link's underline keeps before the
|
||||
@@ -54,6 +56,7 @@ pub(super) struct RenderCell {
|
||||
match_hit: bool,
|
||||
match_current: bool,
|
||||
link_hover: bool,
|
||||
osc8_dots: bool,
|
||||
}
|
||||
|
||||
impl Default for RenderCell {
|
||||
@@ -74,6 +77,7 @@ impl Default for RenderCell {
|
||||
match_hit: false,
|
||||
match_current: false,
|
||||
link_hover: false,
|
||||
osc8_dots: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,6 +256,7 @@ fn snapshot_cell(
|
||||
if flags.contains(Flags::DIM) {
|
||||
rc.fg = dim_fg(rc.fg, bgc, colors.legible_dim);
|
||||
}
|
||||
osc8_underline::mark(&mut rc, cell);
|
||||
rc
|
||||
}
|
||||
|
||||
@@ -1708,8 +1713,10 @@ fn invert_cursor_cell(
|
||||
cell.bg = colors.caret;
|
||||
cell.draw_bg = true;
|
||||
cell.fg = ink;
|
||||
// Keep the underline's own alpha: an OSC 8 link's held-back dots stay
|
||||
// faint under the caret rather than turning into a full-strength line.
|
||||
if let Some(under) = cell.underline_color.as_mut() {
|
||||
*under = ink;
|
||||
*under = ink.opacity(under.a);
|
||||
}
|
||||
// A wide character's trailing spacer is absorbed into the lead cell's
|
||||
// background run by `paint_backgrounds`, so the fill already covers both
|
||||
@@ -2004,6 +2011,7 @@ impl TerminalElement {
|
||||
let row = grid_row as usize * cols;
|
||||
for col in start.column.0..=end.column.0.min(cols.saturating_sub(1)) {
|
||||
let cell = &mut buf[row + col];
|
||||
osc8_underline::unmark(cell);
|
||||
cell.link_hover = true;
|
||||
cell.underline_color = link_underline_color(cell, link.armed);
|
||||
}
|
||||
@@ -3882,7 +3890,7 @@ mod tests {
|
||||
assert_eq!(a.as_ref(), "界");
|
||||
}
|
||||
|
||||
fn test_colors() -> PaintColors {
|
||||
pub(super) fn test_colors() -> PaintColors {
|
||||
let fg = Rgb {
|
||||
r: 10,
|
||||
g: 10,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! An OSC 8 link's resting decoration: iTerm2's faint dotted underline, so a
|
||||
//! `#1234` that is a link reads as one before the pointer finds it. It rides
|
||||
//! the SGR 4:4 dotted painter, so it costs what an app's own dotted run does.
|
||||
|
||||
use alacritty_terminal::term::cell::Cell;
|
||||
|
||||
use super::{HELD_BACK_LINK_ALPHA, RenderCell, UnderlineKind};
|
||||
|
||||
/// A cell the application underlined, or gave an underline colour, keeps
|
||||
/// its own decoration.
|
||||
pub(super) fn mark(rc: &mut RenderCell, cell: &Cell) {
|
||||
if rc.underline == UnderlineKind::None
|
||||
&& rc.underline_color.is_none()
|
||||
&& cell.hyperlink().is_some()
|
||||
{
|
||||
rc.underline = UnderlineKind::Dotted;
|
||||
rc.underline_color = Some(rc.fg.opacity(HELD_BACK_LINK_ALPHA));
|
||||
rc.osc8_dots = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Under the pointer the dots give way to the hovered link's solid line.
|
||||
pub(super) fn unmark(rc: &mut RenderCell) {
|
||||
if std::mem::take(&mut rc.osc8_dots) {
|
||||
rc.underline = UnderlineKind::None;
|
||||
rc.underline_color = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alacritty_terminal::index::{Column, Line, Point};
|
||||
use alacritty_terminal::term::cell::{Flags, Hyperlink};
|
||||
|
||||
use alacritty_terminal::vte::ansi::Rgb;
|
||||
|
||||
use super::super::{
|
||||
GlyphStyle, PaintColors, invert_cursor_cell, snapshot_cell, tests::test_colors,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
fn linked(flags: Flags) -> Cell {
|
||||
let mut cell = Cell {
|
||||
c: '#',
|
||||
flags,
|
||||
..Cell::default()
|
||||
};
|
||||
cell.set_hyperlink(Some(Hyperlink::new(
|
||||
None::<&str>,
|
||||
"https://example.com/3328".to_string(),
|
||||
)));
|
||||
cell
|
||||
}
|
||||
|
||||
fn paint_fixture() -> ([Rgb; 256], PaintColors) {
|
||||
([Rgb { r: 0, g: 0, b: 0 }; 256], test_colors())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hyperlink_rests_under_faint_dots() {
|
||||
let (palette, colors) = paint_fixture();
|
||||
let rc = snapshot_cell(
|
||||
&linked(Flags::empty()),
|
||||
Point::new(Line(0), Column(0)),
|
||||
&palette,
|
||||
&colors,
|
||||
None,
|
||||
);
|
||||
assert_eq!(rc.underline, UnderlineKind::Dotted);
|
||||
assert_eq!(
|
||||
rc.underline_color,
|
||||
Some(rc.fg.opacity(HELD_BACK_LINK_ALPHA))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_sgr_underlined_link_keeps_its_own_line_only() {
|
||||
let (palette, colors) = paint_fixture();
|
||||
let rc = snapshot_cell(
|
||||
&linked(Flags::UNDERLINE),
|
||||
Point::new(Line(0), Column(0)),
|
||||
&palette,
|
||||
&colors,
|
||||
None,
|
||||
);
|
||||
assert_eq!(rc.underline, UnderlineKind::Single);
|
||||
assert_eq!(rc.underline_color, None);
|
||||
assert!(!rc.osc8_dots);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hovering_swaps_the_dots_for_a_solid_line() {
|
||||
let (palette, colors) = paint_fixture();
|
||||
let mut rc = snapshot_cell(
|
||||
&linked(Flags::empty()),
|
||||
Point::new(Line(0), Column(0)),
|
||||
&palette,
|
||||
&colors,
|
||||
None,
|
||||
);
|
||||
unmark(&mut rc);
|
||||
rc.link_hover = true;
|
||||
assert_eq!(rc.underline, UnderlineKind::None);
|
||||
let solid = GlyphStyle::of(&rc)
|
||||
.underline_style()
|
||||
.expect("a solid underline");
|
||||
assert!(!solid.wavy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dots_stay_faint_under_a_block_cursor() {
|
||||
let (palette, colors) = paint_fixture();
|
||||
let rc = snapshot_cell(
|
||||
&linked(Flags::empty()),
|
||||
Point::new(Line(0), Column(0)),
|
||||
&palette,
|
||||
&colors,
|
||||
None,
|
||||
);
|
||||
let mut buf = vec![rc];
|
||||
invert_cursor_cell(&mut buf, 1, 0, 0, &colors);
|
||||
let under = buf[0].underline_color.expect("the dots keep a colour");
|
||||
assert_eq!(under.a, buf[0].fg.a * HELD_BACK_LINK_ALPHA);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user