Skip to content

Commit 16dd860

Browse files
committed
misc features
1 parent 0c0ff7b commit 16dd860

3 files changed

Lines changed: 527 additions & 35 deletions

File tree

src/main.rs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,8 +1347,17 @@ impl ApplicationHandler<AppEvent> for App {
13471347

13481348
// Re-sync all tab numbers when a window gains focus — this fires
13491349
// after a drag-reorder because macOS activates the moved tab.
1350-
WindowEvent::Focused(true) => {
1351-
self.sync_all_titles();
1350+
WindowEvent::Focused(focused) => {
1351+
if focused {
1352+
self.sync_all_titles();
1353+
}
1354+
// Send focus-in / focus-out to the active pane if ?1004h is on.
1355+
if let Some(tw) = self.windows.get_mut(&window_id) {
1356+
let seq: &[u8] = if focused { b"\x1b[I" } else { b"\x1b[O" };
1357+
if tw.active().terminal.state.focus_tracking {
1358+
tw.active_mut().write(seq);
1359+
}
1360+
}
13521361
}
13531362

13541363
WindowEvent::RedrawRequested => {
@@ -1514,17 +1523,29 @@ impl ApplicationHandler<AppEvent> for App {
15141523
let (mx, my) = tw.cursor_pos;
15151524
let sup = tw.modifiers.super_key();
15161525

1517-
// Cmd+click: open URL
1526+
// Cmd+click: open URL or OSC 8 link
15181527
let opened = if sup {
15191528
if let Some((pi, row, col)) = tw.pixel_to_pane_cell(mx, my) {
15201529
let rects = tw.pane_rects();
15211530
let (_, _, pw, ph) = rects.get(pi).copied().unwrap_or((0., 0., 0., 0.));
15221531
let vis_cols = (pw as usize / tw.renderer.cell_width).max(1);
15231532
let vis_rows = (ph as usize / tw.renderer.cell_height).max(1);
1524-
let url = tw.panes[pi].urls_cached(vis_rows, vis_cols)
1525-
.iter()
1526-
.find(|(r, c0, c1, _)| *r == row && col >= *c0 && col < *c1)
1527-
.map(|(_, _, _, u)| u.clone());
1533+
// Check OSC 8 link first (higher priority than raw URL detection).
1534+
let osc8_url = {
1535+
let cell = tw.panes[pi].terminal.state.visual_cell(row, col);
1536+
if cell.link_id != 0 {
1537+
let idx = (cell.link_id as usize).saturating_sub(1);
1538+
tw.panes[pi].terminal.state.links.get(idx).cloned()
1539+
} else {
1540+
None
1541+
}
1542+
};
1543+
let url = osc8_url.or_else(|| {
1544+
tw.panes[pi].urls_cached(vis_rows, vis_cols)
1545+
.iter()
1546+
.find(|(r, c0, c1, _)| *r == row && col >= *c0 && col < *c1)
1547+
.map(|(_, _, _, u)| u.clone())
1548+
});
15281549
if let Some(u) = url {
15291550
// Only open http/https URLs — defence in depth against
15301551
// file:// or custom-scheme injection via terminal output.
@@ -1675,7 +1696,13 @@ impl ApplicationHandler<AppEvent> for App {
16751696
if !pane.terminal.state.is_scrolled_back() {
16761697
pane.terminal.state.snap_to_bottom();
16771698
}
1699+
let sync_before = pane.terminal.state.sync_output;
16781700
pane.terminal.process(&data);
1701+
let sync_after = pane.terminal.state.sync_output;
1702+
// If ?2026l just cleared sync mode, force a redraw now.
1703+
if sync_before && !sync_after {
1704+
tw.window.request_redraw();
1705+
}
16791706
let responses: Vec<Vec<u8>> =
16801707
pane.terminal.state.pending_responses.drain(..).collect();
16811708
for r in responses {
@@ -1696,7 +1723,17 @@ impl ApplicationHandler<AppEvent> for App {
16961723
tw.sync_title();
16971724
}
16981725
tw.reset_blink();
1699-
tw.window.request_redraw();
1726+
// Only redraw immediately if synchronized-output mode is off.
1727+
// When ?2026h is active the application will clear it (?2026l)
1728+
// once it has finished writing, which triggers the redraw then.
1729+
if !tw.panes
1730+
.iter()
1731+
.find(|p| p.id == pane_id)
1732+
.map(|p| p.terminal.state.sync_output)
1733+
.unwrap_or(false)
1734+
{
1735+
tw.window.request_redraw();
1736+
}
17001737
}
17011738
AppEvent::PtyExit { pane_id } => {
17021739
let window_id = match self.pane_to_window.get(&pane_id).copied() {

src/renderer.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,58 @@ impl Renderer {
935935

936936
// ── Public render ─────────────────────────────────────────────────────────
937937

938+
/// Emit underline decorations for a single cell.
939+
fn emit_underline(
940+
&mut self,
941+
x: f32,
942+
y: f32,
943+
cw: f32,
944+
ch: f32,
945+
style: crate::terminal::UnderlineStyle,
946+
color: [f32; 4],
947+
) {
948+
use crate::terminal::UnderlineStyle::*;
949+
let uy = y + ch - 2.;
950+
match style {
951+
None => {}
952+
Straight => {
953+
push_rect(&mut self.fg_rects, x, uy, cw, 1., color);
954+
}
955+
Double => {
956+
push_rect(&mut self.fg_rects, x, uy - 2., cw, 1., color);
957+
push_rect(&mut self.fg_rects, x, uy, cw, 1., color);
958+
}
959+
Curly => {
960+
// Sawtooth approximation: alternating 1-px-high segments.
961+
let seg = (cw / 4.).max(1.);
962+
let n = (cw / seg) as usize;
963+
for i in 0..n {
964+
let sx = x + i as f32 * seg;
965+
let sy = if i % 2 == 0 { uy - 1. } else { uy + 1. };
966+
push_rect(&mut self.fg_rects, sx, sy, seg, 1., color);
967+
}
968+
}
969+
Dotted => {
970+
let dot = 2_f32;
971+
let gap = 2_f32;
972+
let mut sx = x;
973+
while sx + dot <= x + cw {
974+
push_rect(&mut self.fg_rects, sx, uy, dot, 1., color);
975+
sx += dot + gap;
976+
}
977+
}
978+
Dashed => {
979+
let dash = (cw * 0.45).max(2.);
980+
let gap = (cw * 0.1).max(1.);
981+
let mut sx = x;
982+
while sx + dash <= x + cw {
983+
push_rect(&mut self.fg_rects, sx, uy, dash, 1., color);
984+
sx += dash + gap;
985+
}
986+
}
987+
}
988+
}
989+
938990
/// Render all `panes` into `view`. `dividers` are solid rectangles
939991
/// (x, y, w, h) drawn between panes — typically 2 px wide/tall separator lines.
940992
pub fn render(
@@ -1028,6 +1080,19 @@ impl Renderer {
10281080
}
10291081
}
10301082
}
1083+
1084+
// ── Per-cell underlines (SGR style + OSC 8 links) ─────────
1085+
let ul_style = cell.attrs.underline_style;
1086+
if ul_style != crate::terminal::UnderlineStyle::None {
1087+
let ul_color = cell.attrs.underline_color
1088+
.map(c2f)
1089+
.unwrap_or(c2f(fg_color));
1090+
self.emit_underline(px, py, cw, ch, ul_style, ul_color);
1091+
} else if cell.link_id != 0 {
1092+
// OSC 8 hyperlink — use same blue as URL underlines.
1093+
let lc = rgb_f(0x58, 0x9a, 0xdd);
1094+
push_rect(&mut self.fg_rects, px, py + ch - 2., cw, 1., lc);
1095+
}
10311096
}
10321097
}
10331098

@@ -1063,7 +1128,20 @@ impl Renderer {
10631128
{
10641129
let px = ox + pane.state.cursor_col as f32 * cw;
10651130
let py = oy + pane.state.cursor_row as f32 * ch;
1066-
push_rect(&mut self.fg_rects, px, py, 2., ch, c2f(CURSOR_COLOR));
1131+
match pane.state.cursor_shape {
1132+
1 | 2 => {
1133+
// Block cursor: fill entire cell
1134+
push_rect(&mut self.fg_rects, px, py, cw, ch, c2f(CURSOR_COLOR));
1135+
}
1136+
3 | 4 => {
1137+
// Underline cursor: thin bar at cell bottom
1138+
push_rect(&mut self.fg_rects, px, py + ch - 2., cw, 2., c2f(CURSOR_COLOR));
1139+
}
1140+
_ => {
1141+
// Bar cursor (default, 0, 5, 6): thin vertical bar at left
1142+
push_rect(&mut self.fg_rects, px, py, 2., ch, c2f(CURSOR_COLOR));
1143+
}
1144+
}
10671145
}
10681146

10691147
// ── Scrollbar ─────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)