diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/nested.zig | 31 | ||||
| -rw-r--r-- | src/pardes.zig | 84 |
2 files changed, 86 insertions, 29 deletions
diff --git a/src/nested.zig b/src/nested.zig index 0db38ad9..16cea6a1 100644 --- a/src/nested.zig +++ b/src/nested.zig @@ -316,6 +316,27 @@ pub fn sendLook(pid: libc.pid_t, path: []const u8, line: usize) bool { return true; } +/// The three things ensureSocketDir has to know about a path, from whichever +/// call the platform actually offers. Darwin has fstatat and no statx; on +/// linux std.c.fstatat is `void` — glibc hides it behind a versioned symbol +/// std cannot name — so linux asks statx for the same three fields. Both +/// spellings refuse to follow a symlink, which is the point of asking. +const DirFacts = struct { mode: u32, uid: libc.uid_t }; + +fn statNoFollow(path: [:0]const u8) ?DirFacts { + if (comptime darwin) { + var st: libc.Stat = undefined; + if (libc.fstatat(libc.AT.FDCWD, path, &st, libc.AT.SYMLINK_NOFOLLOW) != 0) return null; + return .{ .mode = st.mode, .uid = st.uid }; + } else { + const linux = std.os.linux; + var stx: linux.Statx = undefined; + const want: linux.STATX = .{ .TYPE = true, .MODE = true, .UID = true }; + if (libc.statx(linux.AT.FDCWD, path, linux.AT.SYMLINK_NOFOLLOW, want, &stx) != 0) return null; + return .{ .mode = stx.mode, .uid = stx.uid }; + } +} + /// Create the socket directory if it is missing and refuse it unless it is a /// directory WE own with nothing granted to group or other. A planted path is /// the whole attack on a socket that runs commands, and $XDG_RUNTIME_DIR @@ -334,14 +355,12 @@ fn ensureSocketDir(dir: [:0]const u8) bool { partial[i] = '/'; } _ = libc.mkdir(dir, 0o700); - // fstatat rather than statx: the same three answers, on both platforms, - // and not following the symlink is the point — one where the directory - // should be is exactly the plant this guards against. - var st: libc.Stat = undefined; - if (libc.fstatat(libc.AT.FDCWD, dir, &st, libc.AT.SYMLINK_NOFOLLOW) != 0) return false; + // A symlink where the directory should be is exactly the plant this + // guards against, so the stat above it does not follow one. + const st = statNoFollow(dir) orelse return false; const IFMT: u32 = 0o170000; const IFDIR: u32 = 0o040000; - if (@as(u32, st.mode) & IFMT != IFDIR) return false; + if (st.mode & IFMT != IFDIR) return false; if (st.uid != libc.getuid()) return false; return st.mode & 0o077 == 0; } diff --git a/src/pardes.zig b/src/pardes.zig index d07fc1f1..e9cc746d 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2867,11 +2867,11 @@ test "surface print expands configured tabs and normalizes other controls" { try std.testing.expectEqualStrings("A", cells[cell_count - 1].grapheme()); } -test "tabbed file aligns syntax cursor and mouse with display cells" { +test "tabbed file aligns syntax cursor and mouse while preserving virtual columns" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 12 }); defer p.deinit(); - const pane = try p.hxOpenFileContent("\treturn x;\n"); + const pane = try p.hxOpenFileContent("\treturn x\n"); const f = &pane.file.?; f.highlights = try p.tree_sitter_gpa.alloc(u8, f.content.len); @memset(f.highlights, @intFromEnum(syntax.Syn.none)); @@ -2896,6 +2896,27 @@ test "tabbed file aligns syntax cursor and mouse with display cells" { p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = click_x, .row = body_y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = click_x, .row = body_y } }); try std.testing.expectEqual(@as(i32, 3), pane.cur_col); + + // Display-column conversion must not turn a click in the blank space + // after EOL into a click on the final byte. Besides moving the modal + // cursor, that would make a no-drag Look expand the last word instead of + // remaining inert over blank space. + const line = modal.lineSlice(f.content, 0); + const virtual: u16 = 3; + const blank_x = text_x + @as(u16, @intCast(file_pane.displayWidth(line))) + virtual; + p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = blank_x, .row = body_y } }); + p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = blank_x, .row = body_y } }); + try std.testing.expectEqual(@as(i32, @intCast(line.len + @as(usize, virtual))), pane.cur_col); + + _ = frame.reset(.retain_capacity); + const virtual_surface = try p.render(frame.allocator()); + try std.testing.expectEqual(blank_x, virtual_surface.cursor.?.x); + + const local_blank: i32 = @intCast(blank_x - rect.x - config.GUTTER); + var look_sel = Sel{ .state = .dragging, .c0 = local_blank, .c1 = local_blank, .r0 = BOX_H, .r1 = BOX_H }; + p.expandSel(pane, &look_sel); + try std.testing.expectEqual(local_blank, look_sel.c0); + try std.testing.expectEqual(local_blank, look_sel.c1); } test "plain left click clears explicit modal selection" { @@ -3618,6 +3639,20 @@ const Prompt = union(enum) { pipe: u16, }; +/// File cursor columns may live past EOL. Tabs expand before that boundary; +/// every virtual column after it is still one screen cell, as it was before +/// display-cell-aware file coordinates were introduced. +fn fileRawDisplayCol(line: []const u8, raw_col: usize) usize { + const bounded = @min(raw_col, line.len); + return file_pane.displayWidth(line[0..bounded]) +| (raw_col - bounded); +} + +fn fileRawAtDisplay(line: []const u8, display_col: usize) usize { + const width = file_pane.displayWidth(line); + if (display_col > width) return line.len +| (display_col - width); + return file_pane.byteAtDisplay(line, display_col); +} + pub const Pane = struct { vt: ghostty_vt.Terminal, stream: ghostty_vt.TerminalStream, @@ -4028,16 +4063,16 @@ pub const Pane = struct { const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, config.PREFIX_W)); const hmargin: i32 = @min(config.scroll_off, @divTrunc(w - 1, 2)); const line = modal.lineSlice(f.content, @intCast(@max(0, pane.cur_row))); - const raw_cur = @min(@as(usize, @intCast(@max(0, pane.cur_col))), line.len); - const raw_scroll = @min(@as(usize, @intCast(@max(0, pane.hscroll))), line.len); - const cur = @as(i32, @intCast(file_pane.displayWidth(line[0..raw_cur]))); - const visual_scroll = @as(i32, @intCast(file_pane.displayWidth(line[0..raw_scroll]))); + const raw_cur: usize = @intCast(@max(0, pane.cur_col)); + const raw_scroll: usize = @intCast(@max(0, pane.hscroll)); + const cur = @as(i32, @intCast(fileRawDisplayCol(line, raw_cur))); + const visual_scroll = @as(i32, @intCast(fileRawDisplayCol(line, raw_scroll))); var target = visual_scroll; if (cur < visual_scroll + hmargin) target = @max(0, cur - hmargin) else if (cur > visual_scroll + w - 1 - hmargin) target = cur - (w - 1 - hmargin); - if (target != visual_scroll) pane.hscroll = @intCast(file_pane.byteAtDisplay(line, @intCast(target))); + if (target != visual_scroll) pane.hscroll = @intCast(fileRawAtDisplay(line, @intCast(target))); } } @@ -4621,7 +4656,7 @@ pub const Pardes = struct { const pane = try p.gpa.create(Pane); errdefer p.gpa.destroy(pane); pane.* = .{ - .vt = try ghostty_vt.Terminal.init(p.gpa, .{ + .vt = try ghostty_vt.Terminal.init(std.Io.Threaded.global_single_threaded.io(), p.gpa, .{ .cols = p.screen_w, .rows = p.screen_h, .max_scrollback = 16 * 1024 * 1024, @@ -4650,7 +4685,7 @@ pub const Pardes = struct { const pane = try p.gpa.create(Pane); errdefer p.gpa.destroy(pane); pane.* = .{ - .vt = try ghostty_vt.Terminal.init(p.gpa, .{ .cols = 1, .rows = 1 }), + .vt = try ghostty_vt.Terminal.init(std.Io.Threaded.global_single_threaded.io(), p.gpa, .{ .cols = 1, .rows = 1 }), .stream = undefined, .gpa = p.gpa, .cols = p.screen_w, @@ -5360,10 +5395,12 @@ pub const Pardes = struct { fn fileDisplayOffset(pane: *const Pane, row: i32, from_raw: i32, to_raw: i32) i32 { const line = fileLine(pane, row); - const from = @min(@as(usize, @intCast(@max(0, from_raw))), line.len); - const to = @min(@as(usize, @intCast(@max(0, to_raw))), line.len); - if (to >= from) return @intCast(file_pane.displayWidth(line[from..to])); - return -@as(i32, @intCast(file_pane.displayWidth(line[to..from]))); + const from: usize = @intCast(@max(0, from_raw)); + const to: usize = @intCast(@max(0, to_raw)); + const from_display = fileRawDisplayCol(line, from); + const to_display = fileRawDisplayCol(line, to); + if (to_display >= from_display) return @intCast(to_display - from_display); + return -@as(i32, @intCast(from_display - to_display)); } fn fileDisplayEndOffset(pane: *const Pane, row: i32, from_raw: i32, at_raw: i32) i32 { @@ -5376,23 +5413,24 @@ pub const Pardes = struct { fn fileByteAtDisplay(pane: *const Pane, row: i32, from_raw: i32, display_col: i32) i32 { const line = fileLine(pane, row); - const from = @min(@as(usize, @intCast(@max(0, from_raw))), line.len); + const from: usize = @intCast(@max(0, from_raw)); const display: usize = @intCast(@max(0, display_col)); - return @intCast(from + file_pane.byteAtDisplay(line[from..], display)); + if (from >= line.len) return @intCast(from +| display); + return @intCast(from +| fileRawAtDisplay(line[from..], display)); } fn renderedLineByteCol(pane: *const Pane, row: i32, line: []const u8, display_col: usize) usize { - if (pane.file == null or row < BOX_H) return @min(display_col, line.len); + if (pane.file == null or row < BOX_H) return display_col; const prefix = @min(@as(usize, config.PREFIX_W), line.len); if (display_col <= prefix) return display_col; - return prefix + file_pane.byteAtDisplay(line[prefix..], display_col - prefix); + return prefix +| fileRawAtDisplay(line[prefix..], display_col - prefix); } fn renderedLineDisplayCol(pane: *const Pane, row: i32, line: []const u8, byte_col: usize) usize { - if (pane.file == null or row < BOX_H) return @min(byte_col, line.len); + if (pane.file == null or row < BOX_H) return byte_col; const prefix = @min(@as(usize, config.PREFIX_W), line.len); if (byte_col <= prefix) return byte_col; - return prefix + file_pane.displayWidth(line[prefix..@min(byte_col, line.len)]); + return prefix +| fileRawDisplayCol(line[prefix..], byte_col - prefix); } /// tag + '\n' + body: the full selectable pane text (row 0 = the tag) @@ -5416,7 +5454,7 @@ pub const Pardes = struct { var count_row: i32 = 0; while (count_it.next()) |line| : (count_row += 1) { if (count_row < r0 or count_row > r1) continue; - const b0 = renderedLineByteCol(pane, count_row, line, c0); + const b0 = @min(renderedLineByteCol(pane, count_row, line, c0), line.len); const b1 = @min(renderedLineByteCol(pane, count_row, line, c1) + 1, line.len); total += b1 - b0 + @intFromBool(selected > 0); selected += 1; @@ -5433,7 +5471,7 @@ pub const Pardes = struct { at += 1; } first = false; - const b0 = renderedLineByteCol(pane, v, line, c0); + const b0 = @min(renderedLineByteCol(pane, v, line, c0), line.len); const b1 = @min(renderedLineByteCol(pane, v, line, c1) + 1, line.len); @memcpy(out[at..][0 .. b1 - b0], line[b0..b1]); at += b1 - b0; @@ -12220,7 +12258,7 @@ pub const Pardes = struct { const t = src.terminal.?; const pane = try gpa.create(Pane); pane.* = .{ - .vt = try ghostty_vt.Terminal.init(gpa, .{ + .vt = try ghostty_vt.Terminal.init(std.Io.Threaded.global_single_threaded.io(), gpa, .{ .cols = @max(1, src.cols), .rows = @max(1, src.rows), .max_scrollback = 16 * 1024 * 1024, @@ -12524,7 +12562,7 @@ pub const Pardes = struct { // doc panes have no pty/emulator grid to reflow; just record // the size so bodyText renders the right number of rows if (pane.isTerminal()) { - pane.vt.resize(p.gpa, cols, rows) catch {}; + pane.vt.resize(p.gpa, .{ .cols = cols, .rows = rows }) catch {}; p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); } pane.cols = cols; |
