parent
e9bcea9c0d
commit
b7015f17a4
|
|
@ -353,6 +353,7 @@ pub struct CellStyle {
|
|||
pub invisible: bool,
|
||||
pub strikethrough: bool,
|
||||
pub overline: bool,
|
||||
pub underline: u8,
|
||||
pub underlined: bool,
|
||||
}
|
||||
|
||||
|
|
@ -370,11 +371,19 @@ impl From<ffi::GhosttyStyle> for CellStyle {
|
|||
invisible: value.invisible,
|
||||
strikethrough: value.strikethrough,
|
||||
overline: value.overline,
|
||||
underline: normalize_underline_style(value.underline),
|
||||
underlined: value.underline != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_underline_style(value: std::os::raw::c_int) -> u8 {
|
||||
match value {
|
||||
0..=5 => value as u8,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_color_from_style_color(color: ffi::GhosttyStyleColor) -> Option<CellColor> {
|
||||
match color.tag {
|
||||
ffi::GhosttyStyleColorTag_GHOSTTY_STYLE_COLOR_PALETTE => {
|
||||
|
|
|
|||
|
|
@ -2010,6 +2010,7 @@ fn ghostty_cell_style(
|
|||
if basic.style.strikethrough {
|
||||
modifiers |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
modifiers = crate::protocol::modifier_with_underline_style(modifiers, basic.style.underline);
|
||||
style.add_modifier(modifiers)
|
||||
}
|
||||
|
||||
|
|
@ -3783,8 +3784,7 @@ mod tests {
|
|||
let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap();
|
||||
let pane_id = PaneId::from_raw(1);
|
||||
|
||||
let result =
|
||||
pane.process_pty_bytes(pane_id, 0, b"\x1bP+q6E6F7065;536D756C78;4D7\x1b\\", &tx);
|
||||
let result = pane.process_pty_bytes(pane_id, 0, b"\x1bP+q6E6F7065;4D7\x1b\\", &tx);
|
||||
|
||||
assert!(result.terminal_responses.is_empty());
|
||||
assert!(rx.try_recv().is_err());
|
||||
|
|
@ -3797,12 +3797,18 @@ mod tests {
|
|||
let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap();
|
||||
let pane_id = PaneId::from_raw(1);
|
||||
|
||||
let result = pane.process_pty_bytes(pane_id, 0, b"\x1bP+q5375;536574756C63\x1b\\", &tx);
|
||||
let result = pane.process_pty_bytes(
|
||||
pane_id,
|
||||
0,
|
||||
b"\x1bP+q5375;536D756C78;536574756C63\x1b\\",
|
||||
&tx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result.terminal_responses,
|
||||
vec![
|
||||
expected_xtgettcap_response("5375", None),
|
||||
expected_xtgettcap_response("536D756C78", Some(b"\\E[4:%p1%dm")),
|
||||
expected_xtgettcap_response(
|
||||
"536574756C63",
|
||||
Some(b"\\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m")
|
||||
|
|
@ -3833,6 +3839,59 @@ mod tests {
|
|||
assert_eq!(style.underline_color, Some(Color::Rgb(17, 34, 51)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_patch_preserves_curly_underline_style() {
|
||||
let (tx, _rx) = mpsc::channel(4);
|
||||
let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();
|
||||
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
|
||||
let backend = ratatui::backend::TestBackend::new(20, 5);
|
||||
let mut terminal = ratatui::Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 5), false))
|
||||
.unwrap();
|
||||
{
|
||||
let mut core = pane.core.lock().unwrap();
|
||||
core.terminal.write(b"\x1b[4:3mU");
|
||||
}
|
||||
|
||||
let patch = match pane.collect_dirty_patch(20, 5) {
|
||||
TerminalDirtyPatchOutcome::Patch(patch) => patch,
|
||||
other => panic!("expected dirty patch, got {other:?}"),
|
||||
};
|
||||
|
||||
let cell = &patch.rows[0].1[0];
|
||||
assert_eq!(cell.symbol, "U");
|
||||
assert_eq!(
|
||||
crate::protocol::underline_style_from_modifier(cell.modifier),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_frame_preserves_curly_underline_style() {
|
||||
let (tx, _rx) = mpsc::channel(4);
|
||||
let terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap();
|
||||
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
|
||||
{
|
||||
let mut core = pane.core.lock().unwrap();
|
||||
core.terminal.write(b"\x1b[4:3mU");
|
||||
}
|
||||
|
||||
let backend = ratatui::backend::TestBackend::new(20, 5);
|
||||
let mut terminal = ratatui::Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|frame| pane.render(frame, Rect::new(0, 0, 20, 5), false))
|
||||
.unwrap();
|
||||
|
||||
let frame =
|
||||
crate::protocol::FrameData::from_ratatui_buffer(terminal.backend().buffer(), None);
|
||||
assert_eq!(frame.cells[0].symbol, "U");
|
||||
assert_eq!(
|
||||
crate::protocol::underline_style_from_modifier(frame.cells[0].modifier),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_pty_bytes_orders_default_color_reply_before_following_device_attribute_reply() {
|
||||
let (tx, mut rx) = mpsc::channel(4);
|
||||
|
|
|
|||
|
|
@ -174,8 +174,7 @@ fn xtgettcap_response(cap_hex: &[u8]) -> Option<Bytes> {
|
|||
}
|
||||
|
||||
fn xtgettcap_value(cap_hex: &[u8]) -> Option<Option<&'static [u8]>> {
|
||||
// Mirror only the Ghostty terminfo capabilities that this pane path can
|
||||
// stand behind. Smulx is intentionally absent until underline shapes render.
|
||||
// Mirror only the Ghostty terminfo capabilities that this pane path can stand behind.
|
||||
match cap_hex {
|
||||
b"5463" => Some(None),
|
||||
b"524742" => Some(Some(b"8")),
|
||||
|
|
@ -183,6 +182,7 @@ fn xtgettcap_value(cap_hex: &[u8]) -> Option<Option<&'static [u8]>> {
|
|||
b"73657472676262" => Some(Some(b"\\E[48:2:%p1%d:%p2%d:%p3%dm")),
|
||||
b"4D73" => Some(Some(b"\\E]52;%p1%s;%p2%s\\007")),
|
||||
b"5375" => Some(None),
|
||||
b"536D756C78" => Some(Some(b"\\E[4:%p1%dm")),
|
||||
b"536574756C63" => Some(Some(
|
||||
b"\\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m",
|
||||
)),
|
||||
|
|
@ -255,11 +255,25 @@ mod tests {
|
|||
fn tracker_ignores_unsupported_capabilities() {
|
||||
let mut tracker = XtgettcapQueryTracker::default();
|
||||
|
||||
tracker.observe(b"\x1bP+q536D756C78;6E6F7065\x1b\\");
|
||||
tracker.observe(b"\x1bP+q6E6F7065\x1b\\");
|
||||
|
||||
assert!(response_bytes(tracker.drain_pending()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_returns_underline_style_capability() {
|
||||
let mut tracker = XtgettcapQueryTracker::default();
|
||||
|
||||
tracker.observe(b"\x1bP+q536D756C78\x1b\\");
|
||||
|
||||
assert_eq!(
|
||||
response_bytes(tracker.drain_pending()),
|
||||
vec![Bytes::from_static(
|
||||
b"\x1bP1+r536D756C78=5C455B343A25703125646D\x1b\\"
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_keeps_split_query_until_string_terminator() {
|
||||
let mut tracker = XtgettcapQueryTracker::default();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use std::io::Write;
|
|||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::protocol::{CellData, FrameData};
|
||||
use crate::protocol::{underline_style_from_modifier, CellData, FrameData};
|
||||
|
||||
/// Bytes produced by a [`BlitEncoder`] for one terminal frame.
|
||||
pub(crate) struct EncodedBlit {
|
||||
|
|
@ -294,7 +294,13 @@ fn modifier_to_sgr_parts(val: u16) -> Vec<&'static str> {
|
|||
parts.push("3");
|
||||
}
|
||||
if val & UNDERLINED != 0 {
|
||||
parts.push("4");
|
||||
parts.push(match underline_style_from_modifier(val) {
|
||||
2 => "4:2",
|
||||
3 => "4:3",
|
||||
4 => "4:4",
|
||||
5 => "4:5",
|
||||
_ => "4",
|
||||
});
|
||||
}
|
||||
if val & SLOW_BLINK != 0 {
|
||||
parts.push("5");
|
||||
|
|
@ -842,6 +848,18 @@ mod tests {
|
|||
assert_eq!(build_sgr(0x00_00_00_00, 0x00_00_00_00, 0), "\x1b[0;39;49m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_sgr_preserves_curly_underline_style() {
|
||||
let modifier = crate::protocol::modifier_to_u16(
|
||||
crate::protocol::modifier_with_underline_style(ratatui::style::Modifier::UNDERLINED, 3),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_sgr(0x00_00_00_00, 0x00_00_00_00, modifier),
|
||||
"\x1b[0;4:3;39;49m"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells_equal_identical() {
|
||||
let a = make_cell("A", 2, 1, 0);
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ pub struct CellData {
|
|||
pub fg: u32,
|
||||
/// Background color as a packed u32.
|
||||
pub bg: u32,
|
||||
/// Bitmask of style modifiers (bold, italic, etc.).
|
||||
/// Bitmask of style modifiers (bold, italic, etc.) plus Herdr extension bits.
|
||||
pub modifier: u16,
|
||||
/// Whether this cell should be skipped during diff-based rendering.
|
||||
pub skip: bool,
|
||||
|
|
@ -729,15 +729,30 @@ fn u32_to_color(val: u32) -> ratatui::style::Color {
|
|||
}
|
||||
}
|
||||
|
||||
const UNDERLINE_STYLE_SHIFT: u16 = 12;
|
||||
const UNDERLINE_STYLE_MASK: u16 = 0xF000;
|
||||
|
||||
/// Converts a ratatui `Modifier` bitmask to a u16 for wire transport.
|
||||
pub(crate) fn modifier_to_u16(modifier: ratatui::style::Modifier) -> u16 {
|
||||
modifier.bits()
|
||||
}
|
||||
|
||||
pub(crate) fn underline_style_from_modifier(modifier: u16) -> u8 {
|
||||
((modifier & UNDERLINE_STYLE_MASK) >> UNDERLINE_STYLE_SHIFT) as u8
|
||||
}
|
||||
|
||||
pub(crate) fn modifier_with_underline_style(
|
||||
modifier: ratatui::style::Modifier,
|
||||
underline_style: u8,
|
||||
) -> ratatui::style::Modifier {
|
||||
let bits = modifier.bits() | ((u16::from(underline_style) & 0x0F) << UNDERLINE_STYLE_SHIFT);
|
||||
ratatui::style::Modifier::from_bits_retain(bits)
|
||||
}
|
||||
|
||||
/// Converts a u16 back to a ratatui `Modifier`.
|
||||
#[cfg(test)]
|
||||
fn u16_to_modifier(val: u16) -> ratatui::style::Modifier {
|
||||
ratatui::style::Modifier::from_bits_truncate(val)
|
||||
ratatui::style::Modifier::from_bits_truncate(val & !UNDERLINE_STYLE_MASK)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue