summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig148
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 = &.{};