terminal: highlights

pull/9687/head
Mitchell Hashimoto 2025-11-22 21:06:31 -08:00
parent 56b69ff0fd
commit ec5bdf1a5a
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 160 additions and 0 deletions

View File

@ -26,6 +26,7 @@ pub const point = terminal.point;
pub const color = terminal.color; pub const color = terminal.color;
pub const device_status = terminal.device_status; pub const device_status = terminal.device_status;
pub const formatter = terminal.formatter; pub const formatter = terminal.formatter;
pub const highlight = terminal.highlight;
pub const kitty = terminal.kitty; pub const kitty = terminal.kitty;
pub const modes = terminal.modes; pub const modes = terminal.modes;
pub const page = terminal.page; pub const page = terminal.page;

View File

@ -3729,7 +3729,11 @@ pub const PageIterator = struct {
pub const Chunk = struct { pub const Chunk = struct {
node: *List.Node, node: *List.Node,
/// Start y index (inclusive) of this chunk in the page.
start: size.CellCountInt, start: size.CellCountInt,
/// End y index (exclusive) of this chunk in the page.
end: size.CellCountInt, end: size.CellCountInt,
pub fn rows(self: Chunk) []Row { pub fn rows(self: Chunk) []Row {

154
src/terminal/highlight.zig Normal file
View File

@ -0,0 +1,154 @@
//! Highlights are any contiguous sequences of cells that should
//! be called out in some way, most commonly for text selection but
//! also search results or any other purpose.
//!
//! Within the terminal package, a highlight is a generic concept
//! that represents a range of cells.
// NOTE: The plan is for highlights to ultimately replace Selection
// completely. Selection is deeply tied to various parts of the Ghostty
// internals so this may take some time.
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = @import("../quirks.zig").inlineAssert;
const size = @import("size.zig");
const PageList = @import("PageList.zig");
const PageChunk = PageList.PageIterator.Chunk;
const Pin = PageList.Pin;
const Screen = @import("Screen.zig");
/// An untracked highlight is a highlight that stores its highlighted
/// area as a top-left and bottom-right screen pin. Since it is untracked,
/// the pins are only valid for the current terminal state and may not
/// be safe to use after any terminal modifications.
///
/// For rectangle highlights/selections, the downstream consumer of this
/// code is expected to interpret the pins in whatever shape they want.
/// For example, a rectangular selection would interpret the pins as
/// setting the x bounds for each row between start.y and end.y.
///
/// To simplify all operations, start MUST be before or equal to end.
pub const Untracked = struct {
start: Pin,
end: Pin,
};
/// A tracked highlight is a highlight that stores its highlighted
/// area as tracked pins within a screen.
///
/// A tracked highlight ensures that the pins remain valid even as
/// the terminal state changes. Because of this, tracked highlights
/// have more operations available to them.
///
/// There is more overhead to creating and maintaining tracked highlights.
/// If you're manipulating highlights that are untracked and you're sure
/// that the terminal state won't change, you can use the `initAssume`
/// function.
pub const Tracked = struct {
start: *Pin,
end: *Pin,
pub fn init(
screen: *Screen,
start: Pin,
end: Pin,
) Allocator.Error!Tracked {
const start_tracked = try screen.pages.trackPin(start);
errdefer screen.pages.untrackPin(start_tracked);
const end_tracked = try screen.pages.trackPin(end);
errdefer screen.pages.untrackPin(end_tracked);
return .{
.start = start_tracked,
.end = end_tracked,
};
}
/// Initializes a tracked highlight by assuming that the provided
/// pins are already tracked. This allows callers to perform tracked
/// operations without the overhead of tracking the pins, if the
/// caller can guarantee that the pins are already tracked or that
/// the terminal state will not change.
///
/// Do not call deinit on highlights created with this function.
pub fn initAssume(
start: *Pin,
end: *Pin,
) Tracked {
return .{
.start = start,
.end = end,
};
}
pub fn deinit(
self: Tracked,
screen: *Screen,
) void {
screen.pages.untrackPin(self.start);
screen.pages.untrackPin(self.end);
}
};
/// A flattened highlight is a highlight that stores its highlighted
/// area as a list of page chunks. This representation allows for
/// traversing the entire highlighted area without needing to read any
/// terminal state or dereference any page nodes (which may have been
/// pruned).
pub const Flattened = struct {
/// The page chunks that make up this highlight. This handles the
/// y bounds since chunks[0].start is the first highlighted row
/// and chunks[len - 1].end is the last highlighted row (exclsive).
chunks: std.MultiArrayList(PageChunk),
/// The x bounds of the highlight. `bot_x` may be less than `top_x`
/// for typical left-to-right highlights: can start the selection right
/// of the end on a higher row.
top_x: size.CellCountInt,
bot_x: size.CellCountInt,
/// Exposed for easier type references.
pub const Chunk = PageChunk;
pub const empty: Flattened = .{
.chunks = .empty,
.top_x = 0,
.bot_x = 0,
};
pub fn init(
alloc: Allocator,
start: Pin,
end: Pin,
) Allocator.Error!Flattened {
var result: std.MultiArrayList(PageChunk) = .empty;
errdefer result.deinit(alloc);
var it = start.pageIterator(.right_down, end);
while (it.next()) |chunk| try result.append(alloc, chunk);
return .{
.chunks = result,
.top_x = start.x,
.end_x = end.x,
};
}
/// Convert to an Untracked highlight.
pub fn untracked(self: Flattened) Untracked {
const slice = self.chunks.slice();
const nodes = slice.items(.node);
const starts = slice.items(.start);
const ends = slice.items(.end);
return .{
.start = .{
.node = nodes[0],
.x = self.top_x,
.y = starts[0],
},
.end = .{
.node = nodes[nodes.len - 1],
.x = self.bot_x,
.y = ends[ends.len - 1] - 1,
},
};
}
};

View File

@ -15,6 +15,7 @@ pub const point = @import("point.zig");
pub const color = @import("color.zig"); pub const color = @import("color.zig");
pub const device_status = @import("device_status.zig"); pub const device_status = @import("device_status.zig");
pub const formatter = @import("formatter.zig"); pub const formatter = @import("formatter.zig");
pub const highlight = @import("highlight.zig");
pub const kitty = @import("kitty.zig"); pub const kitty = @import("kitty.zig");
pub const modes = @import("modes.zig"); pub const modes = @import("modes.zig");
pub const page = @import("page.zig"); pub const page = @import("page.zig");