diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 13:46:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 6d7b506c8a380e4dd97f337590ca079d810a360a (patch) | |
| tree | 71075c8c4383df25c6ecd5140ef75063a897b444 /src/pardes.zig | |
| parent | 13d0d107a06d7ac5fb5f8e86c747e54603f51b4e (diff) | |
| download | pardes-6d7b506c8a380e4dd97f337590ca079d810a360a.tar.gz pardes-6d7b506c8a380e4dd97f337590ca079d810a360a.zip | |
Follow embedded PDF links through Look
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 148 |
1 files changed, 127 insertions, 21 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index dd7fc5ed..6e88974d 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3492,6 +3492,8 @@ test "pixel attachment fit and pan do not invalidate cached pixels" { /// The canonical interface: what a frame of pardes IS. The tty shell writes /// these cells to vaxis one-to-one; the SDL shells rasterize them. pub const Surface = struct { + pub const PointerShape = enum(u8) { arrow, link }; + pointer_shape: PointerShape = .arrow, cols: u16 = 0, rows: u16 = 0, cells: []Cell = &.{}, @@ -11434,6 +11436,10 @@ pub const Pardes = struct { s.chorded, ); defer release.deinit(p.pdf_gpa); + if (release.probe) |*probe| if (probe.link != null) { + p.followPdfLink(s.id, probe); + return; + }; // A native PDF page has no byte offsets of its own to // report, so a scripted pane cannot intercept this one. if (release.action) |action| p.dispatchPointerBuiltin( @@ -11805,38 +11811,34 @@ pub const Pardes = struct { pane.setRange(text, 0, r, true); } - fn focusPaneByPath(p: *Pardes, path: []const u8, at: look.Spot) bool { + fn openPaneTarget(p: *const Pardes, path: []const u8, at: look.Spot) ?look.Target { for (p.panes, 0..) |slot, i| { const tt = slot orelse continue; if (tt.image) |iv| if (std.mem.eql(u8, iv.path, path)) { - p.active = i; - return true; + return .{ .pane = .{ .id = i, .at = .{} } }; }; if (comptime pdf_enabled) if (tt.pdf) |pv| if (std.mem.eql(u8, pv.path, path)) { - p.focusPaneLine(i, at, .center); - return true; + return .{ .pane = .{ .id = i, .at = at } }; }; const ff = if (tt.file) |*f| f else continue; if (!std.mem.eql(u8, ff.path, path)) continue; - p.focusPaneLine(i, at, .center); - return true; + return .{ .pane = .{ .id = i, .at = at } }; } - return false; + return null; } - pub fn lookAt(p: *Pardes, id: usize, txt: []const u8) void { - const pane = p.panes[id] orelse return; - p.noteHaptic(.look); - p.noteLookSource(id); + fn focusPaneByPath(p: *Pardes, path: []const u8, at: look.Spot) bool { + const target = p.openPaneTarget(path, at) orelse return false; + p.focusPaneLine(target.pane.id, target.pane.at, .center); + return true; + } + + /// Resolve without changing focus or falling back to search. PDF links use + /// the same location rules as an ordinary Look before offering a choice. + fn resolveLookTarget(p: *Pardes, id: usize, txt: []const u8, realbuf: *[4096]u8) look.Target { const trimmed = std.mem.trim(u8, txt, " \t\r\n"); - if (config.commandWord(trimmed)) |cmd| { - if (p.execute(id, cmd)) |dst| p.active = dst; - return; - } - var realbuf: [4096]u8 = undefined; const pl = look.parsePathLine(trimmed); - if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return; - if (pl.path.len > 0 and p.focusPaneByPath(pl.path, pl.at)) return; + if (pl.path.len > 0) if (p.openPaneTarget(pl.path, pl.at)) |target| return target; var found: look.Target = .none; var seen: [MAX_PANES][]const u8 = undefined; var nseen: usize = 0; @@ -11852,11 +11854,106 @@ pub const Pardes = struct { if (pl.path.len > 0 and pl.path[0] != '/') { var joinbuf: [2048]u8 = undefined; if (std.fmt.bufPrint(&joinbuf, "{s}/{s}", .{ dir, pl.path }) catch null) |j| - if (p.focusPaneByPath(j, pl.at)) return; + if (p.openPaneTarget(j, pl.at)) |target| return target; } - found = look.resolve(p, txt, dir, &realbuf); + found = look.resolve(p, txt, dir, realbuf); if (found != .none or pl.path.len == 0 or pl.path[0] == '/') break; } + return found; + } + + fn locationText(p: *Pardes, path: []const u8, at: look.Spot) ?[]const u8 { + if (std.mem.indexOfAny(u8, path, "\r\n") != null) return null; + const arena = p.scratch.allocator(); + if (at.line == 0) return arena.dupe(u8, path) catch null; + if (at.end_line != 0) { + if (at.col == 0 and at.end_col == 0) + return std.fmt.allocPrint(arena, "{s}:{d}-{d}", .{ path, at.line, at.end_line }) catch null; + return std.fmt.allocPrint(arena, "{s}:{d}:{d}-{d}:{d}", .{ path, at.line, at.col, at.end_line, at.end_col }) catch null; + } + if (at.col != 0) return std.fmt.allocPrint(arena, "{s}:{d}:{d}", .{ path, at.line, at.col }) catch null; + return std.fmt.allocPrint(arena, "{s}:{d}", .{ path, at.line }) catch null; + } + + fn canonicalLookLocation(p: *Pardes, id: usize, text: []const u8) ?[]const u8 { + const trimmed = std.mem.trim(u8, text, " \t\r\n"); + if (trimmed.len == 0) return null; + var realbuf: [4096]u8 = undefined; + switch (p.resolveLookTarget(id, trimmed, &realbuf)) { + .none => return null, + .url => |url| { + if (url.len > 256) return null; + for (url) |c| if (c <= 0x20 or c == 0x7f) return null; + return p.scratch.allocator().dupe(u8, url) catch null; + }, + .file => |file| return p.locationText(file.path, file.at), + .image => |iv| return p.locationText(iv.path, .{}), + .dir => |dir| return p.locationText(dir, .{}), + .pane => |target| { + if (target.id >= p.panes.len) return null; + const pane = p.panes[target.id] orelse return null; + if (pane.file) |file| return p.locationText(file.path, target.at); + if (comptime pdf_enabled) if (pane.pdf) |doc| return p.locationText(doc.path, target.at); + if (pane.image) |iv| return p.locationText(iv.path, target.at); + var path: [32]u8 = undefined; + return p.locationText(std.fmt.bufPrint(&path, config.pane_addr ++ "{d}", .{target.id}) catch return null, target.at); + }, + } + } + + pub fn pdfLinkLocation(p: *Pardes, pane: *const Pane, link: if (pdf_enabled) pdf.Link else void) ?[]const u8 { + if (comptime !pdf_enabled) return null; + const state = pane.pdf orelse return null; + const id = p.paneIdOf(pane) orelse return null; + return switch (link.destination) { + .none => null, + .internal => |dest| if (dest.page < state.document.pages) + p.locationText(state.path, .{ .line = dest.page + 1 }) + else + null, + .external => |uri| p.canonicalLookLocation(id, uri), + }; + } + + fn followPdfLink(p: *Pardes, id: usize, probe: *const panes.Pdf.WordProbe) void { + if (comptime !pdf_enabled) return; + const pane = p.panes[id] orelse return; + const link = probe.link orelse return; + const target = p.pdfLinkLocation(pane, link) orelse return; + if (p.canonicalLookLocation(id, probe.text)) |visible| { + if (!std.mem.eql(u8, visible, target)) { + const content = std.fmt.allocPrint(p.gpa, "{s}\n{s}\n", .{ visible, target }) catch |err| + return p.reportError(id, "PDF link choices", err); + panes.Output.fillResults(p, id, std.fs.path.dirname(pane.pdf.?.path) orelse ".", .pdf_links, "", content, null) catch |err| + return p.reportError(id, "PDF link choices", err); + return; + } + } + switch (link.destination) { + .internal => |dest| { + p.noteHaptic(.look); + p.noteLookSource(id); + p.active = id; + panes.Pdf.revealOutlineDestination(p, pane, dest); + }, + .external => p.lookAt(id, target), + .none => {}, + } + } + + pub fn lookAt(p: *Pardes, id: usize, txt: []const u8) void { + const pane = p.panes[id] orelse return; + p.noteHaptic(.look); + p.noteLookSource(id); + const trimmed = std.mem.trim(u8, txt, " \t\r\n"); + if (config.commandWord(trimmed)) |cmd| { + if (p.execute(id, cmd)) |dst| p.active = dst; + return; + } + const pl = look.parsePathLine(trimmed); + if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return; + var realbuf: [4096]u8 = undefined; + const found = p.resolveLookTarget(id, txt, &realbuf); switch (found) { .none => { const bmode = if (pane.tag_edit) pane.tag_mode else pane.mode; @@ -12619,6 +12716,15 @@ pub const Pardes = struct { s.cols = p.screen_w; s.rows = p.screen_h; s.cursor = null; + s.pointer_shape = .arrow; + if (comptime pdf_enabled) if (p.pdf_hover_preview) |preview| { + if (preview.probe.link != null and preview.pane < p.panes.len) { + if (p.panes[preview.pane]) |pane| { + if (pane.serial == preview.serial and panes.Pdf.paneNativeReady(p, pane)) + s.pointer_shape = .link; + } + } + }; s.nimages = 0; s.npanel_tracks = 0; s.previous_cells = &.{}; |
