Merge pull request #1584 from mitchellh/paged-terminal

Low-memory terminal state implementation
This commit is contained in:
Mitchell Hashimoto
2024-03-26 20:00:20 -07:00
committed by GitHub
49 changed files with 26689 additions and 13415 deletions

151
.github/workflows/release-pr.yml vendored Normal file
View File

@ -0,0 +1,151 @@
on:
pull_request:
types: [opened, reopened, synchronize]
workflow_dispatch: {}
name: Release PR
jobs:
build-macos:
runs-on: namespace-profile-ghostty-macos
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v14
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
# Load Build Number
- name: Build Number
run: |
echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. Build this in release mode.
- name: Build GhosttyKit
run: nix develop -c zig build -Dstatic=true -Doptimize=ReleaseSafe
# The native app is built with native XCode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: cd macos && xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
# This will be a monotonically always increasing build number that we use.
- name: Update Info.plist
env:
SPARKLE_KEY_PUB: ${{ secrets.PROD_MACOS_SPARKLE_KEY_PUB }}
run: |
# Version Info
/usr/libexec/PlistBuddy -c "Set :GhosttyCommit $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GHOSTTY_BUILD" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist"
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: |
# Turn our base64-encoded certificate back to a regular .p12 file
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
# Codesign Sparkle. Some notes here:
# - The XPC services aren't used since we don't sandbox Ghostty,
# but since they're part of the build, they still need to be
# codesigned.
# - The binaries in the "Versions" folders need to NOT be symlinks.
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
# Codesign the app bundle
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
- name: "Notarize app bundle"
env:
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
# Store the notarization credentials so that we can prevent a UI password dialog
# from blocking the CI
echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "Creating temp notarization archive"
ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "Attach staple"
xcrun stapler staple "macos/build/Release/Ghostty.app"
# Zip up the app
- name: Zip App
run: cd macos/build/Release && zip -9 -r --symlinks ../../../ghostty-macos-universal.zip Ghostty.app
# Update Blob Storage
- name: Prep R2 Storage
run: |
mkdir blob
mkdir -p blob/${GHOSTTY_BUILD}
cp ghostty-macos-universal.zip blob/${GHOSTTY_BUILD}/ghostty-macos-universal.zip
- name: Upload to R2
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_PR_SECRET_KEY }}
r2-bucket: ghostty-pr
source-dir: blob
destination-dir: ./

View File

@ -32,11 +32,8 @@ jobs:
)
}}
runs-on: ghcr.io/cirruslabs/macos-ventura-xcode:latest
runs-on: namespace-profile-ghostty-macos
timeout-minutes: 90
env:
# Needed for macos SDK
AGREE: "true"
steps:
- name: Checkout code
uses: actions/checkout@v4

View File

@ -1,9 +1,6 @@
Performance:
- for scrollback, investigate using segmented list for sufficiently large
scrollback scenarios.
- Loading fonts on startups should probably happen in multiple threads
- `deleteLines` is very, very slow which makes scroll region benchmarks terrible
Correctness:
@ -15,10 +12,6 @@ Correctness:
- can effect a crash using `vttest` menu `3 10` since it tries to parse
ASCII as UTF-8.
Improvements:
- scrollback: configurable
Mac:
- Preferences window

View File

@ -53,10 +53,22 @@
};
packages.${system} = rec {
ghostty = pkgs-stable.callPackage ./nix/package.nix {
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix {
inherit (pkgs-zig-0-12) zig_0_12;
revision = self.shortRev or self.dirtyShortRev or "dirty";
optimize = "Debug";
};
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix {
inherit (pkgs-zig-0-12) zig_0_12;
revision = self.shortRev or self.dirtyShortRev or "dirty";
optimize = "ReleaseSafe";
};
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix {
inherit (pkgs-zig-0-12) zig_0_12;
revision = self.shortRev or self.dirtyShortRev or "dirty";
optimize = "ReleaseFast";
};
ghostty = ghostty-releasesafe;
default = ghostty;
};

View File

@ -25,6 +25,7 @@
zig_0_12,
pandoc,
revision ? "dirty",
optimize ? "Debug",
}: let
# The Zig hook has no way to select the release type without actual
# overriding of the default flags.
@ -34,7 +35,7 @@
# ultimately acted on and has made its way to a nixpkgs implementation, this
# can probably be removed in favor of that.
zig012Hook = zig_0_12.hook.overrideAttrs {
zig_default_flags = "-Dcpu=baseline -Doptimize=ReleaseFast";
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize}";
};
# This hash is the computation of the zigCache fixed-output derivation. This

View File

@ -156,7 +156,8 @@ const Mouse = struct {
/// The point at which the left mouse click happened. This is in screen
/// coordinates so that scrolling preserves the location.
left_click_point: terminal.point.ScreenPoint = .{},
left_click_pin: ?*terminal.Pin = null,
left_click_screen: terminal.ScreenType = .primary,
/// The starting xpos/ypos of the left click. Note that if scrolling occurs,
/// these will point to different "cells", but the xpos/ypos will stay
@ -171,7 +172,7 @@ const Mouse = struct {
left_click_time: std.time.Instant = undefined,
/// The last x/y sent for mouse reports.
event_point: ?terminal.point.Viewport = null,
event_point: ?terminal.point.Coordinate = null,
/// Pending scroll amounts for high-precision scrolls
pending_scroll_x: f64 = 0,
@ -185,7 +186,7 @@ const Mouse = struct {
/// The last x/y in the cursor position for links. We use this to
/// only process link hover events when the mouse actually moves cells.
link_point: ?terminal.point.Viewport = null,
link_point: ?terminal.point.Coordinate = null,
};
/// The configuration that a surface has, this is copied from the main
@ -945,7 +946,10 @@ pub fn selectionString(self: *Surface, alloc: Allocator) !?[]const u8 {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const sel = self.io.terminal.screen.selection orelse return null;
return try self.io.terminal.screen.selectionString(alloc, sel, false);
return try self.io.terminal.screen.selectionString(alloc, .{
.sel = sel,
.trim = false,
});
}
/// Returns the pwd of the terminal, if any. This is always copied because
@ -1048,9 +1052,9 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
/// Set the selection contents.
///
/// This must be called with the renderer mutex held.
fn setSelection(self: *Surface, sel_: ?terminal.Selection) void {
fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
const prev_ = self.io.terminal.screen.selection;
self.io.terminal.screen.selection = sel_;
try self.io.terminal.screen.select(sel_);
// Determine the clipboard we want to copy selection to, if it is enabled.
const clipboard: apprt.Clipboard = switch (self.config.copy_on_select) {
@ -1064,7 +1068,7 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) void {
// again if it changed, since setting the clipboard can be an expensive
// operation.
const sel = sel_ orelse return;
if (prev_) |prev| if (std.meta.eql(sel, prev)) return;
if (prev_) |prev| if (sel.eql(prev)) return;
// Check if our runtime supports the selection clipboard at all.
// We can save a lot of work if it doesn't.
@ -1074,11 +1078,10 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) void {
}
}
const buf = self.io.terminal.screen.selectionString(
self.alloc,
sel,
self.config.clipboard_trim_trailing_spaces,
) catch |err| {
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = self.config.clipboard_trim_trailing_spaces,
}) catch |err| {
log.err("error reading selection string err={}", .{err});
return;
};
@ -1357,10 +1360,14 @@ pub fn keyCallback(
if (event.mods.shift) adjust_selection: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
var screen = self.io.terminal.screen;
const sel = sel: {
const old_sel = screen.selection orelse break :adjust_selection;
break :sel old_sel.adjust(&screen, switch (event.key) {
var screen = &self.io.terminal.screen;
const sel = if (screen.selection) |*sel| sel else break :adjust_selection;
// Silently consume key releases. We only want to process selection
// adjust on press.
if (event.action != .press and event.action != .repeat) return .consumed;
sel.adjust(screen, switch (event.key) {
.left => .left,
.right => .right,
.up => .up,
@ -1371,28 +1378,28 @@ pub fn keyCallback(
.end => .end,
else => break :adjust_selection,
});
};
// Silently consume key releases.
if (event.action != .press and event.action != .repeat) return .consumed;
// If the selection endpoint is outside of the current viewpoint,
// scroll it in to view.
// scroll it in to view. Note we always specifically use sel.end
// because that is what adjust modifies.
scroll: {
const viewport_max = terminal.Screen.RowIndexTag.viewport.maxLen(&screen) - 1;
const viewport_end = screen.viewport + viewport_max;
const delta: isize = if (sel.end.y < screen.viewport)
@intCast(screen.viewport)
else if (sel.end.y > viewport_end)
@intCast(viewport_end)
else
const viewport_tl = screen.pages.getTopLeft(.viewport);
const viewport_br = screen.pages.getBottomRight(.viewport).?;
if (sel.end().isBetween(viewport_tl, viewport_br))
break :scroll;
const start_y: isize = @intCast(sel.end.y);
try self.io.terminal.scrollViewport(.{ .delta = start_y - delta });
// Our end point is not within the viewport. If the end
// point is after the br then we need to adjust the end so
// that it is at the bottom right of the viewport.
const target = if (sel.end().before(viewport_tl))
sel.end()
else
sel.end().up(screen.pages.rows - 1) orelse sel.end();
screen.scroll(.{ .pin = target });
}
// Change our selection and queue a render so its shown.
self.setSelection(sel);
// Queue a render so its shown
try self.queueRender();
return .consumed;
}
@ -1542,7 +1549,7 @@ pub fn keyCallback(
if (!event.key.modifier()) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.setSelection(null);
try self.setSelection(null);
try self.io.terminal.scrollViewport(.{ .bottom = {} });
try self.queueRender();
}
@ -1749,7 +1756,7 @@ pub fn scrollCallback(
// The selection can occur if the user uses the shift mod key to
// override mouse grabbing from the window.
if (self.io.terminal.flags.mouse_event != .none) {
self.setSelection(null);
try self.setSelection(null);
}
// If we're in alternate screen with alternate scroll enabled, then
@ -1763,7 +1770,7 @@ pub fn scrollCallback(
if (y.delta_unsigned > 0) {
// When we send mouse events as cursor keys we always
// clear the selection.
self.setSelection(null);
try self.setSelection(null);
const seq = if (self.io.terminal.modes.get(.cursor_keys)) seq: {
// cursor key: application mode
@ -2138,17 +2145,20 @@ pub fn mouseButtonCallback(
{
const pos = try self.rt_surface.getCursorPos();
const point = self.posToViewport(pos.x, pos.y);
const cell = self.renderer_state.terminal.screen.getCell(
.viewport,
point.y,
point.x,
);
const screen = &self.renderer_state.terminal.screen;
const p = screen.pages.pin(.{ .active = point }) orelse {
log.warn("failed to get pin for clicked point", .{});
return;
};
insp.cell = .{ .selected = .{
.row = point.y,
.col = point.x,
.cell = cell,
} };
insp.cell.select(
self.alloc,
p,
point.x,
point.y,
) catch |err| {
log.warn("error selecting cell for inspector err={}", .{err});
};
return;
}
}
@ -2217,7 +2227,7 @@ pub fn mouseButtonCallback(
// In any other mouse button scenario without shift pressed we
// clear the selection since the underlying application can handle
// that in any way (i.e. "scrolling").
self.setSelection(null);
try self.setSelection(null);
// We also set the left click count to 0 so that if mouse reporting
// is disabled in the middle of press (before release) we don't
@ -2245,25 +2255,44 @@ pub fn mouseButtonCallback(
}
// For left button click release we check if we are moving our cursor.
if (button == .left and action == .release and mods.alt) {
if (button == .left and action == .release and mods.alt) click_move: {
// Moving always resets the click count so that we don't highlight.
self.mouse.left_click_count = 0;
const pin = self.mouse.left_click_pin orelse break :click_move;
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
try self.clickMoveCursor(self.mouse.left_click_point);
try self.clickMoveCursor(pin.*);
return;
}
// For left button clicks we always record some information for
// selection/highlighting purposes.
if (button == .left and action == .press) {
if (button == .left and action == .press) click: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const t: *terminal.Terminal = self.renderer_state.terminal;
const screen = &self.renderer_state.terminal.screen;
const pos = try self.rt_surface.getCursorPos();
const pin = pin: {
const pt_viewport = self.posToViewport(pos.x, pos.y);
const pt_screen = pt_viewport.toScreen(&self.io.terminal.screen);
const pin = screen.pages.pin(.{
.viewport = .{
.x = pt_viewport.x,
.y = pt_viewport.y,
},
}) orelse {
// Weird... our viewport x/y that we just converted isn't
// found in our pages. This is probably a bug but we don't
// want to crash in releases because its harmless. So, we
// only assert in debug mode.
if (comptime std.debug.runtime_safety) unreachable;
break :click;
};
break :pin try screen.pages.trackPin(pin);
};
errdefer screen.pages.untrackPin(pin);
// If we move our cursor too much between clicks then we reset
// the multi-click state.
@ -2277,8 +2306,15 @@ pub fn mouseButtonCallback(
if (distance > max_distance) self.mouse.left_click_count = 0;
}
if (self.mouse.left_click_pin) |prev| {
const pin_screen = t.getScreen(self.mouse.left_click_screen);
pin_screen.pages.untrackPin(prev);
self.mouse.left_click_pin = null;
}
// Store it
self.mouse.left_click_point = pt_screen;
self.mouse.left_click_pin = pin;
self.mouse.left_click_screen = t.active_screen;
self.mouse.left_click_xpos = pos.x;
self.mouse.left_click_ypos = pos.y;
@ -2308,16 +2344,16 @@ pub fn mouseButtonCallback(
1 => {
// If we have a selection, clear it. This always happens.
if (self.io.terminal.screen.selection != null) {
self.setSelection(null);
try self.setSelection(null);
try self.queueRender();
}
},
// Double click, select the word under our mouse
2 => {
const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point);
const sel_ = self.io.terminal.screen.selectWord(pin.*);
if (sel_) |sel| {
self.setSelection(sel);
try self.setSelection(sel);
try self.queueRender();
}
},
@ -2325,11 +2361,11 @@ pub fn mouseButtonCallback(
// Triple click, select the line under our mouse
3 => {
const sel_ = if (mods.ctrl)
self.io.terminal.screen.selectOutput(self.mouse.left_click_point)
self.io.terminal.screen.selectOutput(pin.*)
else
self.io.terminal.screen.selectLine(self.mouse.left_click_point);
self.io.terminal.screen.selectLine(.{ .pin = pin.* });
if (sel_) |sel| {
self.setSelection(sel);
try self.setSelection(sel);
try self.queueRender();
}
},
@ -2360,7 +2396,7 @@ pub fn mouseButtonCallback(
/// Performs the "click-to-move" logic to move the cursor to the given
/// screen point if possible. This works by converting the path to the
/// given point into a series of arrow key inputs.
fn clickMoveCursor(self: *Surface, to: terminal.point.ScreenPoint) !void {
fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
// If click-to-move is disabled then we're done.
if (!self.config.cursor_click_to_move) return;
@ -2377,10 +2413,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.point.ScreenPoint) !void {
if (!t.flags.shell_redraws_prompt) return;
// Get our path
const from = (terminal.point.Viewport{
.x = t.screen.cursor.x,
.y = t.screen.cursor.y,
}).toScreen(&t.screen);
const from = t.screen.cursor.page_pin.*;
const path = t.screen.promptPath(from, to);
log.debug("click-to-move-cursor from={} to={} path={}", .{ from, to, path });
@ -2432,18 +2465,32 @@ fn linkAtPos(
if (self.config.links.len == 0) return null;
// Convert our cursor position to a screen point.
const mouse_pt = mouse_pt: {
const viewport_point = self.posToViewport(pos.x, pos.y);
break :mouse_pt viewport_point.toScreen(&self.io.terminal.screen);
const screen = &self.renderer_state.terminal.screen;
const mouse_pin: terminal.Pin = mouse_pin: {
const point = self.posToViewport(pos.x, pos.y);
const pin = screen.pages.pin(.{ .viewport = point }) orelse {
log.warn("failed to get pin for clicked point", .{});
return null;
};
break :mouse_pin pin;
};
// Get our comparison mods
const mouse_mods = self.mouseModsWithCapture(self.mouse.mods);
// Get the line we're hovering over.
const line = self.io.terminal.screen.getLine(mouse_pt) orelse
return null;
const strmap = try line.stringMap(self.alloc);
const line = screen.selectLine(.{
.pin = mouse_pin,
.whitespace = null,
.semantic_prompt_boundary = false,
}) orelse return null;
var strmap: terminal.StringMap = undefined;
self.alloc.free(try screen.selectionString(self.alloc, .{
.sel = line,
.trim = false,
.map = &strmap,
}));
defer strmap.deinit(self.alloc);
// Go through each link and see if we clicked it
@ -2458,7 +2505,7 @@ fn linkAtPos(
var match = (try it.next()) orelse break;
defer match.deinit();
const sel = match.selection();
if (!sel.contains(mouse_pt)) continue;
if (!sel.contains(screen, mouse_pin)) continue;
return .{ link, sel };
}
}
@ -2493,11 +2540,10 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
const link, const sel = try self.linkAtPos(pos) orelse return false;
switch (link.action) {
.open => {
const str = try self.io.terminal.screen.selectionString(
self.alloc,
sel,
false,
);
const str = try self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = false,
});
defer self.alloc.free(str);
try internal_os.open(self.alloc, str);
},
@ -2535,7 +2581,12 @@ pub fn cursorPosCallback(
if (self.inspector) |insp| {
insp.mouse.last_xpos = pos.x;
insp.mouse.last_ypos = pos.y;
insp.mouse.last_point = pos_vp.toScreen(&self.io.terminal.screen);
const screen = &self.renderer_state.terminal.screen;
insp.mouse.last_point = screen.pages.pin(.{ .viewport = .{
.x = pos_vp.x,
.y = pos_vp.y,
} });
try self.queueRender();
}
@ -2589,13 +2640,22 @@ pub fn cursorPosCallback(
}
// Convert to points
const screen_point = pos_vp.toScreen(&self.io.terminal.screen);
const screen = &self.renderer_state.terminal.screen;
const pin = screen.pages.pin(.{
.viewport = .{
.x = pos_vp.x,
.y = pos_vp.y,
},
}) orelse {
if (comptime std.debug.runtime_safety) unreachable;
return;
};
// Handle dragging depending on click count
switch (self.mouse.left_click_count) {
1 => self.dragLeftClickSingle(screen_point, pos.x),
2 => self.dragLeftClickDouble(screen_point),
3 => self.dragLeftClickTriple(screen_point),
1 => try self.dragLeftClickSingle(pin, pos.x),
2 => try self.dragLeftClickDouble(pin),
3 => try self.dragLeftClickTriple(pin),
0 => unreachable, // handled above
else => unreachable,
}
@ -2634,71 +2694,76 @@ pub fn cursorPosCallback(
/// Double-click dragging moves the selection one "word" at a time.
fn dragLeftClickDouble(
self: *Surface,
screen_point: terminal.point.ScreenPoint,
) void {
drag_pin: terminal.Pin,
) !void {
const screen = &self.io.terminal.screen;
const click_pin = self.mouse.left_click_pin.?.*;
// Get the word closest to our starting click.
const word_start = self.io.terminal.screen.selectWordBetween(
self.mouse.left_click_point,
screen_point,
) orelse {
self.setSelection(null);
const word_start = screen.selectWordBetween(click_pin, drag_pin) orelse {
try self.setSelection(null);
return;
};
// Get the word closest to our current point.
const word_current = self.io.terminal.screen.selectWordBetween(
screen_point,
self.mouse.left_click_point,
const word_current = screen.selectWordBetween(
drag_pin,
click_pin,
) orelse {
self.setSelection(null);
try self.setSelection(null);
return;
};
// If our current mouse position is before the starting position,
// then the seletion start is the word nearest our current position.
if (screen_point.before(self.mouse.left_click_point)) {
self.setSelection(.{
.start = word_current.start,
.end = word_start.end,
});
if (drag_pin.before(click_pin)) {
try self.setSelection(terminal.Selection.init(
word_current.start(),
word_start.end(),
false,
));
} else {
self.setSelection(.{
.start = word_start.start,
.end = word_current.end,
});
try self.setSelection(terminal.Selection.init(
word_start.start(),
word_current.end(),
false,
));
}
}
/// Triple-click dragging moves the selection one "line" at a time.
fn dragLeftClickTriple(
self: *Surface,
screen_point: terminal.point.ScreenPoint,
) void {
drag_pin: terminal.Pin,
) !void {
const screen = &self.io.terminal.screen;
const click_pin = self.mouse.left_click_pin.?.*;
// Get the word under our current point. If there isn't a word, do nothing.
const word = self.io.terminal.screen.selectLine(screen_point) orelse return;
const word = screen.selectLine(.{ .pin = drag_pin }) orelse return;
// Get our selection to grow it. If we don't have a selection, start it now.
// We may not have a selection if we started our dbl-click in an area
// that had no data, then we dragged our mouse into an area with data.
var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse {
self.setSelection(word);
var sel = screen.selectLine(.{ .pin = click_pin }) orelse {
try self.setSelection(word);
return;
};
// Grow our selection
if (screen_point.before(self.mouse.left_click_point)) {
sel.start = word.start;
if (drag_pin.before(click_pin)) {
sel.startPtr().* = word.start();
} else {
sel.end = word.end;
sel.endPtr().* = word.end();
}
self.setSelection(sel);
try self.setSelection(sel);
}
fn dragLeftClickSingle(
self: *Surface,
screen_point: terminal.point.ScreenPoint,
drag_pin: terminal.Pin,
xpos: f64,
) void {
) !void {
// NOTE(mitchellh): This logic super sucks. There has to be an easier way
// to calculate this, but this is good for a v1. Selection isn't THAT
// common so its not like this performance heavy code is running that
@ -2708,7 +2773,7 @@ fn dragLeftClickSingle(
// If we were selecting, and we switched directions, then we restart
// calculations because it forces us to reconsider if the first cell is
// selected.
self.checkResetSelSwitch(screen_point);
self.checkResetSelSwitch(drag_pin);
// Our logic for determining if the starting cell is selected:
//
@ -2722,19 +2787,22 @@ fn dragLeftClickSingle(
// - Inverted logic for forwards selections.
//
// Our clicking point
const click_pin = self.mouse.left_click_pin.?.*;
// the boundary point at which we consider selection or non-selection
const cell_width_f64: f64 = @floatFromInt(self.cell_size.width);
const cell_xboundary = cell_width_f64 * 0.6;
// first xpos of the clicked cell adjusted for padding
const left_padding_f64: f64 = @as(f64, @floatFromInt(self.padding.left));
const cell_xstart = @as(f64, @floatFromInt(self.mouse.left_click_point.x)) * cell_width_f64;
const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64;
const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64;
// If this is the same cell, then we only start the selection if weve
// moved past the boundary point the opposite direction from where we
// started.
if (std.meta.eql(screen_point, self.mouse.left_click_point)) {
if (click_pin.eql(drag_pin)) {
// Ensuring to adjusting the cursor position for padding
const cell_xpos = xpos - cell_xstart - left_padding_f64;
const selected: bool = if (cell_start_xpos < cell_xboundary)
@ -2742,11 +2810,11 @@ fn dragLeftClickSingle(
else
cell_xpos < cell_xboundary;
self.setSelection(if (selected) .{
.start = screen_point,
.end = screen_point,
.rectangle = self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
} else null);
try self.setSelection(if (selected) terminal.Selection.init(
drag_pin,
drag_pin,
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
) else null);
return;
}
@ -2758,42 +2826,30 @@ fn dragLeftClickSingle(
// the starting cell if we started after the boundary, else
// we start selection of the prior cell.
// - Inverse logic for a point after the start.
const click_point = self.mouse.left_click_point;
const start: terminal.point.ScreenPoint = if (dragLeftClickBefore(
screen_point,
click_point,
const start: terminal.Pin = if (dragLeftClickBefore(
drag_pin,
click_pin,
self.mouse.mods,
)) start: {
if (cell_start_xpos >= cell_xboundary) {
break :start click_point;
} else {
break :start if (click_point.x > 0) terminal.point.ScreenPoint{
.y = click_point.y,
.x = click_point.x - 1,
} else terminal.point.ScreenPoint{
.x = self.io.terminal.screen.cols - 1,
.y = click_point.y -| 1,
};
}
if (cell_start_xpos >= cell_xboundary) break :start click_pin;
if (click_pin.x > 0) break :start click_pin.left(1);
var start = click_pin.up(1) orelse click_pin;
start.x = self.io.terminal.screen.pages.cols - 1;
break :start start;
} else start: {
if (cell_start_xpos < cell_xboundary) {
break :start click_point;
} else {
break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{
.y = click_point.y,
.x = click_point.x + 1,
} else terminal.point.ScreenPoint{
.y = click_point.y + 1,
.x = 0,
};
}
if (cell_start_xpos < cell_xboundary) break :start click_pin;
if (click_pin.x < self.io.terminal.screen.pages.cols - 1)
break :start click_pin.right(1);
var start = click_pin.down(1) orelse click_pin;
start.x = 0;
break :start start;
};
self.setSelection(.{
.start = start,
.end = screen_point,
.rectangle = self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
});
try self.setSelection(terminal.Selection.init(
start,
drag_pin,
self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
));
return;
}
@ -2803,15 +2859,24 @@ fn dragLeftClickSingle(
// We moved! Set the selection end point. The start point should be
// set earlier.
assert(self.io.terminal.screen.selection != null);
var sel = self.io.terminal.screen.selection.?;
sel.end = screen_point;
self.setSelection(sel);
const sel = self.io.terminal.screen.selection.?;
try self.setSelection(terminal.Selection.init(
sel.start(),
drag_pin,
sel.rectangle,
));
}
// Resets the selection if we switched directions, depending on the select
// mode. See dragLeftClickSingle for more details.
fn checkResetSelSwitch(self: *Surface, screen_point: terminal.point.ScreenPoint) void {
const sel = self.io.terminal.screen.selection orelse return;
fn checkResetSelSwitch(
self: *Surface,
drag_pin: terminal.Pin,
) void {
const screen = &self.io.terminal.screen;
const sel = screen.selection orelse return;
const sel_start = sel.start();
const sel_end = sel.end();
var reset: bool = false;
if (sel.rectangle) {
@ -2819,26 +2884,27 @@ fn checkResetSelSwitch(self: *Surface, screen_point: terminal.point.ScreenPoint)
// the click point depending on the selection mode we're in, with
// the exception of single-column selections, which we always reset
// on if we drift.
if (sel.start.x == sel.end.x) {
reset = screen_point.x != sel.start.x;
if (sel_start.x == sel_end.x) {
reset = drag_pin.x != sel_start.x;
} else {
reset = switch (sel.order()) {
.forward => screen_point.x < sel.start.x or screen_point.y < sel.start.y,
.reverse => screen_point.x > sel.start.x or screen_point.y > sel.start.y,
.mirrored_forward => screen_point.x > sel.start.x or screen_point.y < sel.start.y,
.mirrored_reverse => screen_point.x < sel.start.x or screen_point.y > sel.start.y,
reset = switch (sel.order(screen)) {
.forward => drag_pin.x < sel_start.x or drag_pin.before(sel_start),
.reverse => drag_pin.x > sel_start.x or sel_start.before(drag_pin),
.mirrored_forward => drag_pin.x > sel_start.x or drag_pin.before(sel_start),
.mirrored_reverse => drag_pin.x < sel_start.x or sel_start.before(drag_pin),
};
}
} else {
// Normal select uses simpler logic that is just based on the
// selection start/end.
reset = if (sel.end.before(sel.start))
sel.start.before(screen_point)
reset = if (sel_end.before(sel_start))
sel_start.before(drag_pin)
else
screen_point.before(sel.start);
drag_pin.before(sel_start);
}
if (reset) self.setSelection(null);
// Nullifying a selection can't fail.
if (reset) self.setSelection(null) catch unreachable;
}
// Handles how whether or not the drag screen point is before the click point.
@ -2846,15 +2912,15 @@ fn checkResetSelSwitch(self: *Surface, screen_point: terminal.point.ScreenPoint)
// where to start the selection (before or after the click point). See
// dragLeftClickSingle for more details.
fn dragLeftClickBefore(
screen_point: terminal.point.ScreenPoint,
click_point: terminal.point.ScreenPoint,
drag_pin: terminal.Pin,
click_pin: terminal.Pin,
mods: input.Mods,
) bool {
if (mods.ctrlOrSuper() and mods.alt) {
return screen_point.x < click_point.x;
return drag_pin.x < click_pin.x;
}
return screen_point.before(click_point);
return drag_pin.before(click_pin);
}
/// Call to notify Ghostty that the color scheme for the terminal has
@ -2875,7 +2941,7 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
if (report) try self.reportColorScheme();
}
fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport {
fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate {
// xpos/ypos need to be adjusted for window padding
// (i.e. "window-padding-*" settings.
const pad = if (self.config.window_padding_balance)
@ -3034,18 +3100,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.reset => {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.terminal.fullReset(self.alloc);
self.renderer_state.terminal.fullReset();
},
.copy_to_clipboard => {
// We can read from the renderer state without holding
// the lock because only we will write to this field.
if (self.io.terminal.screen.selection) |sel| {
const buf = self.io.terminal.screen.selectionString(
self.alloc,
sel,
self.config.clipboard_trim_trailing_spaces,
) catch |err| {
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = self.config.clipboard_trim_trailing_spaces,
}) catch |err| {
log.err("error reading selection string err={}", .{err});
return true;
};
@ -3184,19 +3249,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
break :write_scrollback_file;
}
const history_max = terminal.Screen.RowIndexTag.history.maxLen(
&self.io.terminal.screen,
);
// We only dump history if we have history. We still keep
// the file and write the empty file to the pty so that this
// command always works on the primary screen.
if (history_max > 0) {
try self.io.terminal.screen.dumpString(file.writer(), .{
.start = .{ .history = 0 },
.end = .{ .history = history_max -| 1 },
.unwrap = true,
});
const pages = &self.io.terminal.screen.pages;
if (pages.getBottomRight(.history)) |br| {
const tl = pages.getTopLeft(.history);
try self.io.terminal.screen.dumpString(
file.writer(),
.{ .tl = tl, .br = br, .unwrap = true },
);
}
}
@ -3299,7 +3361,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.select_all => {
const sel = self.io.terminal.screen.selectAll();
if (sel) |s| {
self.setSelection(s);
try self.setSelection(s);
try self.queueRender();
}
},

16
src/bench/page-init.sh Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
#
# This is a trivial helper script to help run the page init benchmark.
# You probably want to tweak this script depending on what you're
# trying to measure.
# Uncomment to test with an active terminal state.
# ARGS=" --terminal"
hyperfine \
--warmup 10 \
-n alloc \
"./zig-out/bin/bench-page-init --mode=alloc${ARGS} </tmp/ghostty_bench_data" \
-n pool \
"./zig-out/bin/bench-page-init --mode=pool${ARGS} </tmp/ghostty_bench_data"

78
src/bench/page-init.zig Normal file
View File

@ -0,0 +1,78 @@
//! This benchmark tests the speed to create a terminal "page". This is
//! the internal data structure backing a terminal screen. The creation speed
//! is important because it is one of the primary bottlenecks for processing
//! large amounts of plaintext data.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const cli = @import("../cli.zig");
const terminal_new = @import("../terminal/main.zig");
const Args = struct {
mode: Mode = .alloc,
/// The number of pages to create sequentially.
count: usize = 10_000,
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
pub fn deinit(self: *Args) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
};
const Mode = enum {
/// The default allocation strategy of the structure.
alloc,
/// Use a memory pool to allocate pages from a backing buffer.
pool,
};
pub const std_options: std.Options = .{
.log_level = .debug,
};
pub fn main() !void {
// We want to use the c allocator because it is much faster than GPA.
const alloc = std.heap.c_allocator;
// Parse our args
var args: Args = .{};
defer args.deinit();
{
var iter = try std.process.argsWithAllocator(alloc);
defer iter.deinit();
try cli.args.parse(Args, alloc, &args, &iter);
}
// Handle the modes that do not depend on terminal state first.
switch (args.mode) {
.alloc => try benchAlloc(args.count),
.pool => try benchPool(alloc, args.count),
}
}
noinline fn benchAlloc(count: usize) !void {
for (0..count) |_| {
_ = try terminal_new.Page.init(terminal_new.page.std_capacity);
}
}
noinline fn benchPool(alloc: Allocator, count: usize) !void {
var list = try terminal_new.PageList.init(
alloc,
terminal_new.page.std_capacity.cols,
terminal_new.page.std_capacity.rows,
0,
);
defer list.deinit();
for (0..count) |_| {
_ = try list.grow();
}
}

View File

@ -8,7 +8,7 @@
# - "ascii", uniform random ASCII bytes
# - "utf8", uniform random unicode characters, encoded as utf8
# - "rand", pure random data, will contain many invalid code sequences.
DATA="utf8"
DATA="ascii"
SIZE="25000000"
# Uncomment to test with an active terminal state.

View File

@ -26,7 +26,7 @@ const Args = struct {
/// Process input with a real terminal. This will be MUCH slower than
/// the other modes because it has to maintain terminal state but will
/// help get more realistic numbers.
terminal: bool = false,
terminal: Terminal = .none,
@"terminal-rows": usize = 80,
@"terminal-cols": usize = 120,
@ -42,6 +42,8 @@ const Args = struct {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
const Terminal = enum { none, new };
};
const Mode = enum {
@ -91,8 +93,7 @@ pub fn main() !void {
const writer = std.io.getStdOut().writer();
const buf = try alloc.alloc(u8, args.@"buffer-size");
const seed: u64 = if (args.seed >= 0) @bitCast(args.seed)
else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())));
const seed: u64 = if (args.seed >= 0) @bitCast(args.seed) else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())));
// Handle the modes that do not depend on terminal state first.
switch (args.mode) {
@ -104,14 +105,13 @@ pub fn main() !void {
// Handle the ones that depend on terminal state next
inline .scalar,
.simd,
=> |tag| {
if (args.terminal) {
=> |tag| switch (args.terminal) {
.new => {
const TerminalStream = terminal.Stream(*TerminalHandler);
var t = try terminal.Terminal.init(
alloc,
args.@"terminal-cols",
args.@"terminal-rows",
);
var t = try terminal.Terminal.init(alloc, .{
.cols = @intCast(args.@"terminal-cols"),
.rows = @intCast(args.@"terminal-rows"),
});
var handler: TerminalHandler = .{ .t = &t };
var stream: TerminalStream = .{ .handler = &handler };
switch (tag) {
@ -119,14 +119,16 @@ pub fn main() !void {
.simd => try benchSimd(reader, &stream, buf),
else => @compileError("missing case"),
}
} else {
},
.none => {
var stream: terminal.Stream(NoopHandler) = .{ .handler = .{} };
switch (tag) {
.scalar => try benchScalar(reader, &stream, buf),
.simd => try benchSimd(reader, &stream, buf),
else => @compileError("missing case"),
}
}
},
},
}
}
@ -163,7 +165,7 @@ fn genUtf8(writer: anytype, seed: u64) !void {
while (true) {
var i: usize = 0;
while (i <= buf.len - 4) {
const cp: u18 = while(true) {
const cp: u18 = while (true) {
const cp = rnd.int(u18);
if (ziglyph.isPrint(cp)) break cp;
};

View File

@ -150,4 +150,5 @@ pub const ExeEntrypoint = enum {
bench_stream,
bench_codepoint_width,
bench_grapheme_break,
bench_page_init,
};

View File

@ -234,20 +234,18 @@ fn parseIntoField(
bool => try parseBool(value orelse "t"),
u8 => std.fmt.parseInt(
u8,
value orelse return error.ValueRequired,
0,
) catch return error.InvalidValue,
u32 => std.fmt.parseInt(
inline u8,
u16,
u32,
value orelse return error.ValueRequired,
0,
) catch return error.InvalidValue,
u64 => std.fmt.parseInt(
u64,
usize,
i8,
i16,
i32,
i64,
isize,
=> |Int| std.fmt.parseInt(
Int,
value orelse return error.ValueRequired,
0,
) catch return error.InvalidValue,

View File

@ -291,7 +291,7 @@ palette: Palette = .{},
/// a prompt, regardless of this configuration. You can disable that behavior
/// by specifying `shell-integration-features = no-cursor` or disabling shell
/// integration entirely.
@"cursor-style": terminal.Cursor.Style = .block,
@"cursor-style": terminal.CursorStyle = .block,
/// Sets the default blinking state of the cursor. This is just the default
/// state; running programs may override the cursor style using `DECSCUSR` (`CSI
@ -427,6 +427,27 @@ command: ?[]const u8 = null,
/// command.
@"abnormal-command-exit-runtime": u32 = 250,
/// The size of the scrollback buffer in bytes. This also includes the active
/// screen. No matter what this is set to, enough memory will always be
/// allocated for the visible screen and anything leftover is the limit for
/// the scrollback.
///
/// When this limit is reached, the oldest lines are removed from the
/// scrollback.
///
/// Scrollback currently exists completely in memory. This means that the
/// larger this value, the larger potential memory usage. Scrollback is
/// allocated lazily up to this limit, so if you set this to a very large
/// value, it will not immediately consume a lot of memory.
///
/// This size is per terminal surface, not for the entire application.
///
/// It is not currently possible to set an unlimited scrollback buffer.
/// This is a future planned feature.
///
/// This can be changed at runtime but will only affect new terminal surfaces.
@"scrollback-limit": u32 = 10_000_000, // 10MB
/// Match a regular expression against the terminal text and associate clicking
/// it with an action. This can be used to match URLs, file paths, etc. Actions
/// can be opening using the system opener (i.e. `open` or `xdg-open`) or

View File

@ -12,7 +12,7 @@ pub inline fn move(comptime T: type, dest: []T, source: []const T) void {
}
}
/// Same as std.mem.copyForwards but prefers libc memcpy if it is available
/// Same as @memcpy but prefers libc memcpy if it is available
/// because it is generally much faster.
pub inline fn copy(comptime T: type, dest: []T, source: []const T) void {
if (builtin.link_libc) {
@ -22,5 +22,13 @@ pub inline fn copy(comptime T: type, dest: []T, source: []const T) void {
}
}
/// Same as std.mem.rotate(T, items, 1) but more efficient by using memmove
/// and a tmp var for the single rotated item instead of 3 calls to reverse.
pub inline fn rotateOnce(comptime T: type, items: []T) void {
const tmp = items[0];
move(T, items[0..items.len - 1], items[1..items.len]);
items[items.len - 1] = tmp;
}
extern "c" fn memcpy(*anyopaque, *const anyopaque, usize) *anyopaque;
extern "c" fn memmove(*anyopaque, *const anyopaque, usize) *anyopaque;

View File

@ -84,13 +84,15 @@ pub const Shaper = struct {
pub fn runIterator(
self: *Shaper,
group: *GroupCache,
row: terminal.Screen.Row,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.group = group,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
@ -242,13 +244,19 @@ test "run iterator" {
{
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
try screen.testWriteString("ABCD");
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count);
@ -256,12 +264,18 @@ test "run iterator" {
// Spaces should be part of a run
{
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString("ABCD EFG");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count);
@ -269,13 +283,19 @@ test "run iterator" {
{
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
try screen.testWriteString("A😃D");
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |_| {
count += 1;
@ -296,30 +316,51 @@ test "run iterator: empty cells with background set" {
{
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
screen.cursor.pen.bg = .{ .rgb = try terminal.color.Name.cyan.default() };
try screen.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0 } });
try screen.testWriteString("A");
// Get our first row
const row = screen.getRow(.{ .active = 0 });
row.getCellPtr(1).* = screen.cursor.pen;
row.getCellPtr(2).* = screen.cursor.pen;
{
const list_cell = screen.pages.getCell(.{ .active = .{ .x = 1 } }).?;
const cell = list_cell.cell;
cell.* = .{
.content_tag = .bg_color_rgb,
.content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } },
};
}
{
const list_cell = screen.pages.getCell(.{ .active = .{ .x = 2 } }).?;
const cell = list_cell.cell;
cell.* = .{
.content_tag = .bg_color_rgb,
.content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } },
};
}
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
// The run should have length 3 because of the two background
// cells.
try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength());
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
{
const run = (try it.next(alloc)).?;
try testing.expectEqual(@as(u32, 1), shaper.hb_buf.getLength());
const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 3), cells.len);
try testing.expectEqual(@as(usize, 1), cells.len);
}
try testing.expectEqual(@as(usize, 1), count);
{
const run = (try it.next(alloc)).?;
try testing.expectEqual(@as(u32, 2), shaper.hb_buf.getLength());
const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 2), cells.len);
}
try testing.expect(try it.next(alloc) == null);
}
}
@ -337,13 +378,19 @@ test "shape" {
buf_idx += try std.unicode.utf8Encode(0x1F3FD, buf[buf_idx..]); // Medium skin tone
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -361,12 +408,18 @@ test "shape inconsolata ligs" {
defer testdata.deinit();
{
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
try screen.testWriteString(">=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -380,12 +433,18 @@ test "shape inconsolata ligs" {
}
{
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
try screen.testWriteString("===");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -408,12 +467,18 @@ test "shape monaspace ligs" {
defer testdata.deinit();
{
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
try screen.testWriteString("===");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -436,12 +501,18 @@ test "shape emoji width" {
defer testdata.deinit();
{
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
var screen = try terminal.Screen.init(alloc, 5, 3, 0);
defer screen.deinit();
try screen.testWriteString("👍");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -469,13 +540,19 @@ test "shape emoji width long" {
buf_idx += try std.unicode.utf8Encode(0xFE0F, buf[buf_idx..]); // emoji representation
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 30, 0);
var screen = try terminal.Screen.init(alloc, 30, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -502,13 +579,19 @@ test "shape variation selector VS15" {
buf_idx += try std.unicode.utf8Encode(0xFE0E, buf[buf_idx..]); // ZWJ to force text
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -533,13 +616,19 @@ test "shape variation selector VS16" {
buf_idx += try std.unicode.utf8Encode(0xFE0F, buf[buf_idx..]); // ZWJ to force color
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -559,23 +648,29 @@ test "shape with empty cells in between" {
defer testdata.deinit();
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 30, 0);
var screen = try terminal.Screen.init(alloc, 30, 3, 0);
defer screen.deinit();
try screen.testWriteString("A");
screen.cursor.x += 5;
screen.cursorRight(5);
try screen.testWriteString("B");
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 1), count);
try testing.expectEqual(@as(usize, 7), cells.len);
}
try testing.expectEqual(@as(usize, 1), count);
}
test "shape Chinese characters" {
@ -593,13 +688,19 @@ test "shape Chinese characters" {
buf_idx += try std.unicode.utf8Encode('a', buf[buf_idx..]);
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 30, 0);
var screen = try terminal.Screen.init(alloc, 30, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -634,13 +735,19 @@ test "shape box glyphs" {
buf_idx += try std.unicode.utf8Encode(0x2501, buf[buf_idx..]); //
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -663,7 +770,7 @@ test "shape selection boundary" {
defer testdata.deinit();
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString("a1b2c3d4e5");
@ -671,10 +778,17 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{
.start = .{ .x = 0, .y = 0 },
.end = .{ .x = screen.cols - 1, .y = 0 },
}, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
false,
),
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -687,10 +801,17 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{
.start = .{ .x = 2, .y = 0 },
.end = .{ .x = screen.cols - 1, .y = 0 },
}, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
false,
),
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -703,10 +824,17 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{
.start = .{ .x = 0, .y = 0 },
.end = .{ .x = 3, .y = 0 },
}, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false,
),
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -719,10 +847,17 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{
.start = .{ .x = 1, .y = 0 },
.end = .{ .x = 3, .y = 0 },
}, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false,
),
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -735,10 +870,17 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), .{
.start = .{ .x = 1, .y = 0 },
.end = .{ .x = 1, .y = 0 },
}, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
false,
),
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -756,7 +898,7 @@ test "shape cursor boundary" {
defer testdata.deinit();
// Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString("a1b2c3d4e5");
@ -764,7 +906,13 @@ test "shape cursor boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -777,7 +925,13 @@ test "shape cursor boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 0);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -790,7 +944,13 @@ test "shape cursor boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 1);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -803,7 +963,13 @@ test "shape cursor boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 9);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
9,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -829,7 +995,13 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -842,7 +1014,13 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 0);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -853,7 +1031,13 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, 1);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -872,12 +1056,18 @@ test "shape cell attribute change" {
// Plain >= should shape into 1 run
{
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
try screen.testWriteString(">=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -891,11 +1081,17 @@ test "shape cell attribute change" {
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
defer screen.deinit();
try screen.testWriteString(">");
screen.cursor.pen.attrs.bold = true;
try screen.setAttribute(.{ .bold = {} });
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -908,13 +1104,19 @@ test "shape cell attribute change" {
{
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
defer screen.deinit();
screen.cursor.pen.fg = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } };
try screen.setAttribute(.{ .direct_color_fg = .{ .r = 1, .g = 2, .b = 3 } });
try screen.testWriteString(">");
screen.cursor.pen.fg = .{ .rgb = .{ .r = 3, .g = 2, .b = 1 } };
try screen.setAttribute(.{ .direct_color_fg = .{ .r = 3, .g = 2, .b = 1 } });
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -927,13 +1129,19 @@ test "shape cell attribute change" {
{
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
defer screen.deinit();
screen.cursor.pen.bg = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } };
try screen.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } });
try screen.testWriteString(">");
screen.cursor.pen.bg = .{ .rgb = .{ .r = 3, .g = 2, .b = 1 } };
try screen.setAttribute(.{ .direct_color_bg = .{ .r = 3, .g = 2, .b = 1 } });
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -946,12 +1154,18 @@ test "shape cell attribute change" {
{
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
defer screen.deinit();
screen.cursor.pen.bg = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } };
try screen.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } });
try screen.testWriteString(">");
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
var it = shaper.runIterator(
testdata.cache,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;

View File

@ -26,17 +26,23 @@ pub const TextRun = struct {
pub const RunIterator = struct {
hooks: font.Shaper.RunIteratorHook,
group: *font.GroupCache,
row: terminal.Screen.Row,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection = null,
cursor_x: ?usize = null,
i: usize = 0,
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
const cells = self.row.cells(.all);
// Trim the right side of a row that might be empty
const max: usize = max: {
var j: usize = self.row.lenCells();
while (j > 0) : (j -= 1) if (!self.row.getCell(j - 1).empty()) break;
break :max j;
for (0..cells.len) |i| {
const rev_i = cells.len - i - 1;
if (!cells[rev_i].isEmpty()) break :max rev_i + 1;
}
break :max 0;
};
// We're over at the max
@ -48,67 +54,65 @@ pub const RunIterator = struct {
// Allow the hook to prepare
try self.hooks.prepare();
// Let's get our style that we'll expect for the run.
const style = self.row.style(&cells[self.i]);
// Go through cell by cell and accumulate while we build our run.
var j: usize = self.i;
while (j < max) : (j += 1) {
const cluster = j;
const cell = self.row.getCell(j);
const cell = &cells[j];
// If we have a selection and we're at a boundary point, then
// we break the run here.
if (self.selection) |unordered_sel| {
if (j > self.i) {
const sel = unordered_sel.ordered(.forward);
const sel = unordered_sel.ordered(self.screen, .forward);
const start_x = sel.start().x;
const end_x = sel.end().x;
if (sel.start.x > 0 and
j == sel.start.x and
self.row.graphemeBreak(sel.start.x)) break;
if (start_x > 0 and
j == start_x) break;
if (sel.end.x > 0 and
j == sel.end.x + 1 and
self.row.graphemeBreak(sel.end.x)) break;
if (end_x > 0 and
j == end_x + 1) break;
}
}
// If we're a spacer, then we ignore it
if (cell.attrs.wide_spacer_tail) continue;
switch (cell.wide) {
.narrow, .wide => {},
.spacer_head, .spacer_tail => continue,
}
// If our cell attributes are changing, then we split the run.
// This prevents a single glyph for ">=" to be rendered with
// one color when the two components have different styling.
if (j > self.i) {
const prev_cell = self.row.getCell(j - 1);
const Attrs = @TypeOf(cell.attrs);
const Int = @typeInfo(Attrs).Struct.backing_integer.?;
const prev_attrs: Int = @bitCast(prev_cell.attrs.styleAttrs());
const attrs: Int = @bitCast(cell.attrs.styleAttrs());
if (prev_attrs != attrs) break;
if (!cell.bg.eql(prev_cell.bg)) break;
if (!cell.fg.eql(prev_cell.fg)) break;
const prev_cell = cells[j - 1];
if (prev_cell.style_id != cell.style_id) break;
}
// Text runs break when font styles change so we need to get
// the proper style.
const style: font.Style = style: {
if (cell.attrs.bold) {
if (cell.attrs.italic) break :style .bold_italic;
const font_style: font.Style = style: {
if (style.flags.bold) {
if (style.flags.italic) break :style .bold_italic;
break :style .bold;
}
if (cell.attrs.italic) break :style .italic;
if (style.flags.italic) break :style .italic;
break :style .regular;
};
// Determine the presentation format for this glyph.
const presentation: ?font.Presentation = if (cell.attrs.grapheme) p: {
const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: {
// We only check the FIRST codepoint because I believe the
// presentation format must be directly adjacent to the codepoint.
var it = self.row.codepointIterator(j);
if (it.next()) |cp| {
if (cp == 0xFE0E) break :p .text;
if (cp == 0xFE0F) break :p .emoji;
}
const cps = self.row.grapheme(cell) orelse break :p null;
assert(cps.len > 0);
if (cps[0] == 0xFE0E) break :p .text;
if (cps[0] == 0xFE0F) break :p .emoji;
break :p null;
} else emoji: {
// If we're not a grapheme, our individual char could be
@ -128,7 +132,7 @@ pub const RunIterator = struct {
// such as a skin-tone emoji is fine, but hovering over the
// joiners will show the joiners allowing you to modify the
// emoji.
if (!cell.attrs.grapheme) {
if (!cell.hasGrapheme()) {
if (self.cursor_x) |cursor_x| {
// Exactly: self.i is the cursor and we iterated once. This
// means that we started exactly at the cursor and did at
@ -163,9 +167,8 @@ pub const RunIterator = struct {
// then we use that.
if (try self.indexForCell(
alloc,
j,
cell,
style,
font_style,
presentation,
)) |idx| break :font_info .{ .idx = idx };
@ -174,7 +177,7 @@ pub const RunIterator = struct {
if (try self.group.indexForCodepoint(
alloc,
0xFFFD, // replacement char
style,
font_style,
presentation,
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
@ -182,7 +185,7 @@ pub const RunIterator = struct {
if (try self.group.indexForCodepoint(
alloc,
' ',
style,
font_style,
presentation,
)) |idx| break :font_info .{ .idx = idx, .fallback = ' ' };
@ -206,12 +209,12 @@ pub const RunIterator = struct {
// Add all the codepoints for our grapheme
try self.hooks.addCodepoint(
if (cell.char == 0) ' ' else cell.char,
if (cell.codepoint() == 0) ' ' else cell.codepoint(),
@intCast(cluster),
);
if (cell.attrs.grapheme) {
var it = self.row.codepointIterator(j);
while (it.next()) |cp| {
if (cell.hasGrapheme()) {
const cps = self.row.grapheme(cell).?;
for (cps) |cp| {
// Do not send presentation modifiers
if (cp == 0xFE0E or cp == 0xFE0F) continue;
try self.hooks.addCodepoint(cp, @intCast(cluster));
@ -242,13 +245,12 @@ pub const RunIterator = struct {
fn indexForCell(
self: *RunIterator,
alloc: Allocator,
j: usize,
cell: terminal.Screen.Cell,
cell: *terminal.Cell,
style: font.Style,
presentation: ?font.Presentation,
) !?font.Group.FontIndex {
// Get the font index for the primary codepoint.
const primary_cp: u32 = if (cell.empty() or cell.char == 0) ' ' else cell.char;
const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint();
const primary = try self.group.indexForCodepoint(
alloc,
primary_cp,
@ -258,16 +260,16 @@ pub const RunIterator = struct {
// Easy, and common: we aren't a multi-codepoint grapheme, so
// we just return whatever index for the cell codepoint.
if (!cell.attrs.grapheme) return primary;
if (!cell.hasGrapheme()) return primary;
// If this is a grapheme, we need to find a font that supports
// all of the codepoints in the grapheme.
var it = self.row.codepointIterator(j);
var candidates = try std.ArrayList(font.Group.FontIndex).initCapacity(alloc, it.len() + 1);
const cps = self.row.grapheme(cell) orelse return primary;
var candidates = try std.ArrayList(font.Group.FontIndex).initCapacity(alloc, cps.len + 1);
defer candidates.deinit();
candidates.appendAssumeCapacity(primary);
while (it.next()) |cp| {
for (cps) |cp| {
// Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
@ -285,8 +287,7 @@ pub const RunIterator = struct {
// We need to find a candidate that has ALL of our codepoints
for (candidates.items) |idx| {
if (!self.group.group.hasCodepoint(idx, primary_cp, presentation)) continue;
it.reset();
while (it.next()) |cp| {
for (cps) |cp| {
// Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
if (!self.group.group.hasCodepoint(idx, cp, presentation)) break;

View File

@ -4,6 +4,7 @@
const Inspector = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const cimgui = @import("cimgui");
@ -35,8 +36,8 @@ mouse: struct {
last_xpos: f64 = 0,
last_ypos: f64 = 0,
/// Last hovered screen point
last_point: terminal.point.ScreenPoint = .{},
// Last hovered screen point
last_point: ?terminal.Pin = null,
} = .{},
/// A selected cell.
@ -61,17 +62,47 @@ const CellInspect = union(enum) {
selected: Selected,
const Selected = struct {
alloc: Allocator,
row: usize,
col: usize,
cell: terminal.Screen.Cell,
cell: inspector.Cell,
};
pub fn deinit(self: *CellInspect) void {
switch (self.*) {
.idle, .requested => {},
.selected => |*v| v.cell.deinit(v.alloc),
}
}
pub fn request(self: *CellInspect) void {
switch (self.*) {
.idle, .selected => self.* = .requested,
.idle => self.* = .requested,
.selected => |*v| {
v.cell.deinit(v.alloc);
self.* = .requested;
},
.requested => {},
}
}
pub fn select(
self: *CellInspect,
alloc: Allocator,
pin: terminal.Pin,
x: usize,
y: usize,
) !void {
assert(self.* == .requested);
const cell = try inspector.Cell.init(alloc, pin);
errdefer cell.deinit(alloc);
self.* = .{ .selected = .{
.alloc = alloc,
.row = y,
.col = x,
.cell = cell,
} };
}
};
/// Setup the ImGui state. This requires an ImGui context to be set.
@ -134,6 +165,8 @@ pub fn init(surface: *Surface) !Inspector {
}
pub fn deinit(self: *Inspector) void {
self.cell.deinit();
{
var it = self.key_events.iterator(.forward);
while (it.next()) |v| v.deinit(self.surface.alloc);
@ -298,9 +331,10 @@ fn renderScreenWindow(self: *Inspector) void {
0,
);
defer cimgui.c.igEndTable();
const palette = self.surface.io.terminal.color_palette.colors;
inspector.cursor.renderInTable(&screen.cursor, &palette);
inspector.cursor.renderInTable(
self.surface.renderer_state.terminal,
&screen.cursor,
);
} // table
cimgui.c.igTextDisabled("(Any styles not shown are not currently set)");
@ -457,6 +491,67 @@ fn renderScreenWindow(self: *Inspector) void {
}
} // table
} // kitty graphics
if (cimgui.c.igCollapsingHeader_TreeNodeFlags(
"Internal Terminal State",
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
)) {
const pages = &screen.pages;
{
_ = cimgui.c.igBeginTable(
"##terminal_state",
2,
cimgui.c.ImGuiTableFlags_None,
.{ .x = 0, .y = 0 },
0,
);
defer cimgui.c.igEndTable();
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Memory Usage");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d bytes", pages.page_size);
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Memory Limit");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d bytes", pages.maxSize());
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Viewport Location");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%s", @tagName(pages.viewport).ptr);
}
}
} // table
//
if (cimgui.c.igCollapsingHeader_TreeNodeFlags(
"Active Page",
cimgui.c.ImGuiTreeNodeFlags_DefaultOpen,
)) {
inspector.page.render(&pages.pages.last.?.data);
}
} // terminal state
}
/// The modes window shows the currently active terminal modes and allows
@ -664,7 +759,15 @@ fn renderSizeWindow(self: *Inspector) void {
const t = self.surface.renderer_state.terminal;
{
const hover_point = self.mouse.last_point.toViewport(&t.screen);
const hover_point: terminal.point.Coordinate = pt: {
const p = self.mouse.last_point orelse break :pt .{};
const pt = t.screen.pages.pointFromPin(
.active,
p,
) orelse break :pt .{};
break :pt pt.coord();
};
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
@ -736,7 +839,15 @@ fn renderSizeWindow(self: *Inspector) void {
}
{
const left_click_point = mouse.left_click_point.toViewport(&t.screen);
const left_click_point: terminal.point.Coordinate = pt: {
const p = mouse.left_click_pin orelse break :pt .{};
const pt = t.screen.pages.pointFromPin(
.active,
p.*,
) orelse break :pt .{};
break :pt pt.coord();
};
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
@ -825,136 +936,11 @@ fn renderCellWindow(self: *Inspector) void {
}
const selected = self.cell.selected;
{
// We have a selected cell, show information about it.
_ = cimgui.c.igBeginTable(
"table_cursor",
2,
cimgui.c.ImGuiTableFlags_None,
.{ .x = 0, .y = 0 },
0,
selected.cell.renderTable(
self.surface.renderer_state.terminal,
selected.col,
selected.row,
);
defer cimgui.c.igEndTable();
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Grid Position");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("row=%d col=%d", selected.row, selected.col);
}
}
// NOTE: we don't currently write the character itself because
// we haven't hooked up imgui to our font system. That's hard! We
// can/should instead hook up our renderer to imgui and just render
// the single glyph in an image view so it looks _identical_ to the
// terminal.
codepoint: {
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Codepoint");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
if (selected.cell.char == 0) {
cimgui.c.igTextDisabled("(empty)");
break :codepoint;
}
cimgui.c.igText("U+%X", selected.cell.char);
}
}
// If we have a color then we show the color
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Foreground Color");
_ = cimgui.c.igTableSetColumnIndex(1);
switch (selected.cell.fg) {
.none => cimgui.c.igText("default"),
else => {
const rgb = switch (selected.cell.fg) {
.none => unreachable,
.indexed => |idx| self.surface.io.terminal.color_palette.colors[idx],
.rgb => |rgb| rgb,
};
if (selected.cell.fg == .indexed) {
cimgui.c.igValue_Int("Palette", selected.cell.fg.indexed);
}
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_fg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Background Color");
_ = cimgui.c.igTableSetColumnIndex(1);
switch (selected.cell.bg) {
.none => cimgui.c.igText("default"),
else => {
const rgb = switch (selected.cell.bg) {
.none => unreachable,
.indexed => |idx| self.surface.io.terminal.color_palette.colors[idx],
.rgb => |rgb| rgb,
};
if (selected.cell.bg == .indexed) {
cimgui.c.igValue_Int("Palette", selected.cell.bg.indexed);
}
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_bg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
// Boolean styles
const styles = .{
"bold", "italic", "faint", "blink",
"inverse", "invisible", "protected", "strikethrough",
};
inline for (styles) |style| style: {
if (!@field(selected.cell.attrs, style)) break :style;
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText(style.ptr);
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("true");
}
}
} // table
cimgui.c.igTextDisabled("(Any styles not shown are not currently set)");
}
fn renderKeyboardWindow(self: *Inspector) void {
@ -1127,8 +1113,10 @@ fn renderTermioWindow(self: *Inspector) void {
0,
);
defer cimgui.c.igEndTable();
const palette = self.surface.io.terminal.color_palette.colors;
inspector.cursor.renderInTable(&ev.cursor, &palette);
inspector.cursor.renderInTable(
self.surface.renderer_state.terminal,
&ev.cursor,
);
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);

193
src/inspector/cell.zig Normal file
View File

@ -0,0 +1,193 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const cimgui = @import("cimgui");
const terminal = @import("../terminal/main.zig");
/// A cell being inspected. This duplicates much of the data in
/// the terminal data structure because we want the inspector to
/// not have a reference to the terminal state or to grab any
/// locks.
pub const Cell = struct {
/// The main codepoint for this cell.
codepoint: u21,
/// Codepoints for this cell to produce a single grapheme cluster.
/// This is only non-empty if the cell is part of a multi-codepoint
/// grapheme cluster. This does NOT include the primary codepoint.
cps: []const u21,
/// The style of this cell.
style: terminal.Style,
pub fn init(
alloc: Allocator,
pin: terminal.Pin,
) !Cell {
const cell = pin.rowAndCell().cell;
const style = pin.style(cell);
const cps: []const u21 = if (cell.hasGrapheme()) cps: {
const src = pin.grapheme(cell).?;
assert(src.len > 0);
break :cps try alloc.dupe(u21, src);
} else &.{};
errdefer if (cps.len > 0) alloc.free(cps);
return .{
.codepoint = cell.codepoint(),
.cps = cps,
.style = style,
};
}
pub fn deinit(self: *Cell, alloc: Allocator) void {
if (self.cps.len > 0) alloc.free(self.cps);
}
pub fn renderTable(
self: *const Cell,
t: *const terminal.Terminal,
x: usize,
y: usize,
) void {
// We have a selected cell, show information about it.
_ = cimgui.c.igBeginTable(
"table_cursor",
2,
cimgui.c.ImGuiTableFlags_None,
.{ .x = 0, .y = 0 },
0,
);
defer cimgui.c.igEndTable();
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Grid Position");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("row=%d col=%d", y, x);
}
}
// NOTE: we don't currently write the character itself because
// we haven't hooked up imgui to our font system. That's hard! We
// can/should instead hook up our renderer to imgui and just render
// the single glyph in an image view so it looks _identical_ to the
// terminal.
codepoint: {
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Codepoint");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
if (self.codepoint == 0) {
cimgui.c.igTextDisabled("(empty)");
break :codepoint;
}
cimgui.c.igText("U+%X", @as(u32, @intCast(self.codepoint)));
}
}
// If we have a color then we show the color
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Foreground Color");
_ = cimgui.c.igTableSetColumnIndex(1);
switch (self.style.fg_color) {
.none => cimgui.c.igText("default"),
.palette => |idx| {
const rgb = t.color_palette.colors[idx];
cimgui.c.igValue_Int("Palette", idx);
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_fg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
.rgb => |rgb| {
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_fg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Background Color");
_ = cimgui.c.igTableSetColumnIndex(1);
switch (self.style.bg_color) {
.none => cimgui.c.igText("default"),
.palette => |idx| {
const rgb = t.color_palette.colors[idx];
cimgui.c.igValue_Int("Palette", idx);
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_bg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
.rgb => |rgb| {
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_bg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
}
// Boolean styles
const styles = .{
"bold", "italic", "faint", "blink",
"inverse", "invisible", "strikethrough",
};
inline for (styles) |style| style: {
if (!@field(self.style.flags, style)) break :style;
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText(style.ptr);
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("true");
}
}
cimgui.c.igTextDisabled("(Any styles not shown are not currently set)");
}
};

View File

@ -4,8 +4,8 @@ const terminal = @import("../terminal/main.zig");
/// Render cursor information with a table already open.
pub fn renderInTable(
t: *const terminal.Terminal,
cursor: *const terminal.Screen.Cursor,
palette: *const terminal.color.Palette,
) void {
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
@ -27,7 +27,7 @@ pub fn renderInTable(
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%s", @tagName(cursor.style).ptr);
cimgui.c.igText("%s", @tagName(cursor.cursor_style).ptr);
}
}
@ -48,19 +48,25 @@ pub fn renderInTable(
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Foreground Color");
_ = cimgui.c.igTableSetColumnIndex(1);
switch (cursor.pen.fg) {
switch (cursor.style.fg_color) {
.none => cimgui.c.igText("default"),
else => {
const rgb = switch (cursor.pen.fg) {
.none => unreachable,
.indexed => |idx| palette[idx],
.rgb => |rgb| rgb,
.palette => |idx| {
const rgb = t.color_palette.colors[idx];
cimgui.c.igValue_Int("Palette", idx);
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_fg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
if (cursor.pen.fg == .indexed) {
cimgui.c.igValue_Int("Palette", cursor.pen.fg.indexed);
}
.rgb => |rgb| {
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@ -79,19 +85,25 @@ pub fn renderInTable(
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Background Color");
_ = cimgui.c.igTableSetColumnIndex(1);
switch (cursor.pen.bg) {
switch (cursor.style.bg_color) {
.none => cimgui.c.igText("default"),
else => {
const rgb = switch (cursor.pen.bg) {
.none => unreachable,
.indexed => |idx| palette[idx],
.rgb => |rgb| rgb,
.palette => |idx| {
const rgb = t.color_palette.colors[idx];
cimgui.c.igValue_Int("Palette", idx);
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@as(f32, @floatFromInt(rgb.b)) / 255,
};
_ = cimgui.c.igColorEdit3(
"color_bg",
&color,
cimgui.c.ImGuiColorEditFlags_NoPicker |
cimgui.c.ImGuiColorEditFlags_NoLabel,
);
},
if (cursor.pen.bg == .indexed) {
cimgui.c.igValue_Int("Palette", cursor.pen.bg.indexed);
}
.rgb => |rgb| {
var color: [3]f32 = .{
@as(f32, @floatFromInt(rgb.r)) / 255,
@as(f32, @floatFromInt(rgb.g)) / 255,
@ -109,10 +121,10 @@ pub fn renderInTable(
// Boolean styles
const styles = .{
"bold", "italic", "faint", "blink",
"inverse", "invisible", "protected", "strikethrough",
"inverse", "invisible", "strikethrough",
};
inline for (styles) |style| style: {
if (!@field(cursor.pen.attrs, style)) break :style;
if (!@field(cursor.style.flags, style)) break :style;
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{

View File

@ -1,7 +1,11 @@
const std = @import("std");
pub const cell = @import("cell.zig");
pub const cursor = @import("cursor.zig");
pub const key = @import("key.zig");
pub const page = @import("page.zig");
pub const termio = @import("termio.zig");
pub const Cell = cell.Cell;
pub const Inspector = @import("Inspector.zig");
test {

169
src/inspector/page.zig Normal file
View File

@ -0,0 +1,169 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const cimgui = @import("cimgui");
const terminal = @import("../terminal/main.zig");
pub fn render(page: *const terminal.Page) void {
cimgui.c.igPushID_Ptr(page);
defer cimgui.c.igPopID();
_ = cimgui.c.igBeginTable(
"##page_state",
2,
cimgui.c.ImGuiTableFlags_None,
.{ .x = 0, .y = 0 },
0,
);
defer cimgui.c.igEndTable();
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Memory Size");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d bytes", page.memory.len);
cimgui.c.igText("%d VM pages", page.memory.len / std.mem.page_size);
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Unique Styles");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", page.styles.count(page.memory));
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Grapheme Entries");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", page.graphemeCount());
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Capacity");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
_ = cimgui.c.igBeginTable(
"##capacity",
2,
cimgui.c.ImGuiTableFlags_None,
.{ .x = 0, .y = 0 },
0,
);
defer cimgui.c.igEndTable();
const cap = page.capacity;
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Columns");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", @as(u32, @intCast(cap.cols)));
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Rows");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", @as(u32, @intCast(cap.rows)));
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Unique Styles");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", @as(u32, @intCast(cap.styles)));
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Grapheme Bytes");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", cap.grapheme_bytes);
}
}
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Size");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
_ = cimgui.c.igBeginTable(
"##size",
2,
cimgui.c.ImGuiTableFlags_None,
.{ .x = 0, .y = 0 },
0,
);
defer cimgui.c.igEndTable();
const size = page.size;
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Columns");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", @as(u32, @intCast(size.cols)));
}
}
{
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
{
_ = cimgui.c.igTableSetColumnIndex(0);
cimgui.c.igText("Rows");
}
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText("%d", @as(u32, @intCast(size.rows)));
}
}
}
} // size table
}

View File

@ -10,4 +10,5 @@ pub usingnamespace switch (build_config.exe_entrypoint) {
.bench_stream => @import("bench/stream.zig"),
.bench_codepoint_width => @import("bench/codepoint-width.zig"),
.bench_grapheme_break => @import("bench/grapheme-break.zig"),
.bench_page_init => @import("bench/page-init.zig"),
};

View File

@ -623,7 +623,6 @@ pub fn updateFrame(
// Data we extract out of the critical area.
const Critical = struct {
bg: terminal.color.RGB,
selection: ?terminal.Selection,
screen: terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
@ -657,25 +656,13 @@ pub fn updateFrame(
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock while rebuilding GPU cells.
const viewport_bottom = state.terminal.screen.viewportIsBottom();
var screen_copy = if (viewport_bottom) try state.terminal.screen.clone(
var screen_copy = try state.terminal.screen.clone(
self.alloc,
.{ .active = 0 },
.{ .active = state.terminal.rows - 1 },
) else try state.terminal.screen.clone(
self.alloc,
.{ .viewport = 0 },
.{ .viewport = state.terminal.rows - 1 },
.{ .viewport = .{} },
null,
);
errdefer screen_copy.deinit();
// Convert our selection to viewport points because we copy only
// the viewport above.
const selection: ?terminal.Selection = if (state.terminal.screen.selection) |sel|
sel.toViewport(&state.terminal.screen)
else
null;
// Whether to draw our cursor or not.
const cursor_style = renderer.cursorStyle(
state,
@ -700,7 +687,6 @@ pub fn updateFrame(
break :critical .{
.bg = self.background_color,
.selection = selection,
.screen = screen_copy,
.mouse = state.mouse,
.preedit = preedit,
@ -715,7 +701,6 @@ pub fn updateFrame(
// Build our GPU cells
try self.rebuildCells(
critical.selection,
&critical.screen,
critical.mouse,
critical.preedit,
@ -1243,11 +1228,8 @@ fn prepKittyGraphics(
// The top-left and bottom-right corners of our viewport in screen
// points. This lets us determine offsets and containment of placements.
const top = (terminal.point.Viewport{}).toScreen(&t.screen);
const bot = (terminal.point.Viewport{
.x = t.screen.cols - 1,
.y = t.screen.rows - 1,
}).toScreen(&t.screen);
const top = t.screen.pages.getTopLeft(.viewport);
const bot = t.screen.pages.getBottomRight(.viewport).?;
// Go through the placements and ensure the image is loaded on the GPU.
var it = storage.placements.iterator();
@ -1264,13 +1246,15 @@ fn prepKittyGraphics(
// If the selection isn't within our viewport then skip it.
const rect = p.rect(image, t);
if (rect.top_left.y > bot.y) continue;
if (rect.bottom_right.y < top.y) continue;
if (bot.before(rect.top_left)) continue;
if (rect.bottom_right.before(top)) continue;
// If the top left is outside the viewport we need to calc an offset
// so that we render (0, 0) with some offset for the texture.
const offset_y: u32 = if (rect.top_left.y < t.screen.viewport) offset_y: {
const offset_cells = t.screen.viewport - rect.top_left.y;
const offset_y: u32 = if (rect.top_left.before(top)) offset_y: {
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
const offset_cells = vp_y - img_y;
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
break :offset_y @intCast(offset_pixels);
} else 0;
@ -1316,7 +1300,10 @@ fn prepKittyGraphics(
}
// Convert our screen point to a viewport point
const viewport = p.point.toViewport(&t.screen);
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
.viewport,
p.pin.*,
) orelse .{ .viewport = .{} };
// Calculate the source rectangle
const source_x = @min(image.width, p.source_x);
@ -1338,8 +1325,8 @@ fn prepKittyGraphics(
if (image.width > 0 and image.height > 0) {
try self.image_placements.append(self.alloc, .{
.image_id = kv.key_ptr.image_id,
.x = @intCast(p.point.x),
.y = @intCast(viewport.y),
.x = @intCast(p.pin.x),
.y = @intCast(viewport.viewport.y),
.z = p.z,
.width = dest_width,
.height = dest_height,
@ -1536,16 +1523,21 @@ pub fn setScreenSize(
/// down to the GPU yet.
fn rebuildCells(
self: *Metal,
term_selection: ?terminal.Selection,
screen: *terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette,
) !void {
const rows_usize: usize = @intCast(screen.pages.rows);
const cols_usize: usize = @intCast(screen.pages.cols);
// Bg cells at most will need space for the visible screen size
self.cells_bg.clearRetainingCapacity();
try self.cells_bg.ensureTotalCapacity(self.alloc, screen.rows * screen.cols);
try self.cells_bg.ensureTotalCapacity(
self.alloc,
rows_usize * cols_usize,
);
// Over-allocate just to ensure we don't allocate again during loops.
self.cells.clearRetainingCapacity();
@ -1554,7 +1546,7 @@ fn rebuildCells(
// * 3 for glyph + underline + strikethrough for each cell
// + 1 for cursor
(screen.rows * screen.cols * 3) + 1,
(rows_usize * cols_usize * 3) + 1,
);
// Create an arena for all our temporary allocations while rebuilding
@ -1577,7 +1569,7 @@ fn rebuildCells(
x: [2]usize,
cp_offset: usize,
} = if (preedit) |preedit_v| preedit: {
const range = preedit_v.range(screen.cursor.x, screen.cols - 1);
const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
break :preedit .{
.y = screen.cursor.y,
.x = .{ range.start, range.end },
@ -1591,9 +1583,9 @@ fn rebuildCells(
var cursor_cell: ?mtl_shaders.Cell = null;
// Build each cell
var rowIter = screen.rowIterator(.viewport);
var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null);
var y: usize = 0;
while (rowIter.next()) |row| {
while (row_it.next()) |row| {
defer y += 1;
// True if this is the row with our cursor. There are a lot of conditions
@ -1628,8 +1620,8 @@ fn rebuildCells(
defer if (cursor_row) {
// If we're on a wide spacer tail, then we want to look for
// the previous cell.
const screen_cell = row.getCell(screen.cursor.x);
const x = screen.cursor.x - @intFromBool(screen_cell.attrs.wide_spacer_tail);
const screen_cell = row.cells(.all)[screen.cursor.x];
const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail);
for (self.cells.items[start_i..]) |cell| {
if (cell.grid_pos[0] == @as(f32, @floatFromInt(x)) and
(cell.mode == .fg or cell.mode == .fg_color))
@ -1643,22 +1635,16 @@ fn rebuildCells(
// We need to get this row's selection if there is one for proper
// run splitting.
const row_selection = sel: {
if (term_selection) |sel| {
const screen_point = (terminal.point.Viewport{
.x = 0,
.y = y,
}).toScreen(screen);
if (sel.containedRow(screen, screen_point)) |row_sel| {
break :sel row_sel;
}
}
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_group,
screen,
row,
row_selection,
if (shape_cursor) screen.cursor.x else null,
@ -1679,25 +1665,19 @@ fn rebuildCells(
// It this cell is within our hint range then we need to
// underline it.
const cell: terminal.Screen.Cell = cell: {
var cell = row.getCell(shaper_cell.x);
// If our links contain this cell then we want to
// underline it.
if (link_match_set.orderedContains(.{
.x = shaper_cell.x,
.y = y,
})) {
cell.attrs.underline = .single;
}
break :cell cell;
const cell: terminal.Pin = cell: {
var copy = row;
copy.x = shaper_cell.x;
break :cell copy;
};
if (self.updateCell(
term_selection,
screen,
cell,
if (link_match_set.orderedContains(screen, cell))
.single
else
null,
color_palette,
shaper_cell,
run,
@ -1714,9 +1694,6 @@ fn rebuildCells(
}
}
}
// Set row is not dirty anymore
row.setDirty(false);
}
// Add the cursor at the end so that it overlays everything. If we have
@ -1766,9 +1743,9 @@ fn rebuildCells(
fn updateCell(
self: *Metal,
selection: ?terminal.Selection,
screen: *terminal.Screen,
cell: terminal.Screen.Cell,
screen: *const terminal.Screen,
cell_pin: terminal.Pin,
cell_underline: ?terminal.Attribute.Underline,
palette: *const terminal.color.Palette,
shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun,
@ -1788,47 +1765,30 @@ fn updateCell(
};
// True if this cell is selected
// TODO(perf): we can check in advance if selection is in
// our viewport at all and not run this on every point.
const selected: bool = if (selection) |sel| selected: {
const screen_point = (terminal.point.Viewport{
.x = x,
.y = y,
}).toScreen(screen);
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, cell_pin)
else
false;
break :selected sel.contains(screen_point);
} else false;
const rac = cell_pin.rowAndCell();
const cell = rac.cell;
const style = cell_pin.style(cell);
const underline = cell_underline orelse style.flags.underline;
// The colors for the cell.
const colors: BgFg = colors: {
// The normal cell result
const cell_res: BgFg = if (!cell.attrs.inverse) .{
const cell_res: BgFg = if (!style.flags.inverse) .{
// In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color.
.bg = switch (cell.bg) {
.none => null,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.fg = switch (cell.fg) {
.none => self.foreground_color,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.bg = style.bg(cell, palette),
.fg = style.fg(palette) orelse self.foreground_color,
} else .{
// In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background.
.bg = switch (cell.fg) {
.none => self.foreground_color,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.fg = switch (cell.bg) {
.none => self.background_color,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.bg = style.fg(palette) orelse self.foreground_color,
.fg = style.bg(cell, palette) orelse self.background_color,
};
// If we are selected, we our colors are just inverted fg/bg
@ -1846,7 +1806,7 @@ fn updateCell(
// If the cell is "invisible" then we just make fg = bg so that
// the cell is transparent but still copy-able.
const res: BgFg = selection_res orelse cell_res;
if (cell.attrs.invisible) {
if (style.flags.invisible) {
break :colors BgFg{
.bg = res.bg,
.fg = res.bg orelse self.background_color,
@ -1857,7 +1817,7 @@ fn updateCell(
};
// Alpha multiplier
const alpha: u8 = if (cell.attrs.faint) 175 else 255;
const alpha: u8 = if (style.flags.faint) 175 else 255;
// If the cell has a background, we always draw it.
const bg: [4]u8 = if (colors.bg) |rgb| bg: {
@ -1874,11 +1834,11 @@ fn updateCell(
if (selected) break :bg_alpha default;
// If we're reversed, do not apply background opacity
if (cell.attrs.inverse) break :bg_alpha default;
if (style.flags.inverse) break :bg_alpha default;
// If we have a background and its not the default background
// then we apply background opacity
if (cell.bg != .none and !rgb.eql(self.background_color)) {
if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) {
break :bg_alpha default;
}
@ -1892,7 +1852,7 @@ fn updateCell(
self.cells_bg.appendAssumeCapacity(.{
.mode = .bg,
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
.cell_width = cell.widthLegacy(),
.cell_width = cell.gridWidth(),
.color = .{ rgb.r, rgb.g, rgb.b, bg_alpha },
.bg_color = .{ 0, 0, 0, 0 },
});
@ -1906,7 +1866,7 @@ fn updateCell(
};
// If the cell has a character, draw it
if (cell.char > 0) fg: {
if (cell.hasText()) fg: {
// Render
const glyph = try self.font_group.renderGlyph(
self.alloc,
@ -1920,11 +1880,8 @@ fn updateCell(
const mode: mtl_shaders.Cell.Mode = switch (try fgMode(
&self.font_group.group,
screen,
cell,
cell_pin,
shaper_run,
x,
y,
)) {
.normal => .fg,
.color => .fg_color,
@ -1934,7 +1891,7 @@ fn updateCell(
self.cells.appendAssumeCapacity(.{
.mode = mode,
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
.cell_width = cell.widthLegacy(),
.cell_width = cell.gridWidth(),
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.bg_color = bg,
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
@ -1946,8 +1903,8 @@ fn updateCell(
});
}
if (cell.attrs.underline != .none) {
const sprite: font.Sprite = switch (cell.attrs.underline) {
if (underline != .none) {
const sprite: font.Sprite = switch (underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
@ -1961,17 +1918,17 @@ fn updateCell(
font.sprite_index,
@intFromEnum(sprite),
.{
.cell_width = if (cell.attrs.wide) 2 else 1,
.cell_width = if (cell.wide == .wide) 2 else 1,
.grid_metrics = self.grid_metrics,
},
);
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
const color = style.underlineColor(palette) orelse colors.fg;
self.cells.appendAssumeCapacity(.{
.mode = .fg,
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
.cell_width = cell.widthLegacy(),
.cell_width = cell.gridWidth(),
.color = .{ color.r, color.g, color.b, alpha },
.bg_color = bg,
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
@ -1980,11 +1937,11 @@ fn updateCell(
});
}
if (cell.attrs.strikethrough) {
if (style.flags.strikethrough) {
self.cells.appendAssumeCapacity(.{
.mode = .strikethrough,
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
.cell_width = cell.widthLegacy(),
.cell_width = cell.gridWidth(),
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.bg_color = bg,
});
@ -2002,21 +1959,14 @@ fn addCursor(
// we're on the wide characer tail.
const wide, const x = cell: {
// The cursor goes over the screen cursor position.
const cell = screen.getCell(
.active,
screen.cursor.y,
screen.cursor.x,
);
if (!cell.attrs.wide_spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.attrs.wide, screen.cursor.x };
const cell = screen.cursor.page_cell;
if (cell.wide != .spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.wide == .wide, screen.cursor.x };
// If we're part of a wide character, we move the cursor back to
// the actual character.
break :cell .{ screen.getCell(
.active,
screen.cursor.y,
screen.cursor.x - 1,
).attrs.wide, screen.cursor.x - 1 };
const prev_cell = screen.cursorCellLeft(1);
break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
};
const color = self.cursor_color orelse self.foreground_color;

View File

@ -657,7 +657,6 @@ pub fn updateFrame(
// Data we extract out of the critical area.
const Critical = struct {
gl_bg: terminal.color.RGB,
selection: ?terminal.Selection,
screen: terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
@ -691,25 +690,13 @@ pub fn updateFrame(
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock wile rebuilding GPU cells.
const viewport_bottom = state.terminal.screen.viewportIsBottom();
var screen_copy = if (viewport_bottom) try state.terminal.screen.clone(
var screen_copy = try state.terminal.screen.clone(
self.alloc,
.{ .active = 0 },
.{ .active = state.terminal.rows - 1 },
) else try state.terminal.screen.clone(
self.alloc,
.{ .viewport = 0 },
.{ .viewport = state.terminal.rows - 1 },
.{ .viewport = .{} },
null,
);
errdefer screen_copy.deinit();
// Convert our selection to viewport points because we copy only
// the viewport above.
const selection: ?terminal.Selection = if (state.terminal.screen.selection) |sel|
sel.toViewport(&state.terminal.screen)
else
null;
// Whether to draw our cursor or not.
const cursor_style = renderer.cursorStyle(
state,
@ -739,7 +726,6 @@ pub fn updateFrame(
break :critical .{
.gl_bg = self.background_color,
.selection = selection,
.screen = screen_copy,
.mouse = state.mouse,
.preedit = preedit,
@ -762,7 +748,6 @@ pub fn updateFrame(
// Build our GPU cells
try self.rebuildCells(
critical.selection,
&critical.screen,
critical.mouse,
critical.preedit,
@ -802,11 +787,8 @@ fn prepKittyGraphics(
// The top-left and bottom-right corners of our viewport in screen
// points. This lets us determine offsets and containment of placements.
const top = (terminal.point.Viewport{}).toScreen(&t.screen);
const bot = (terminal.point.Viewport{
.x = t.screen.cols - 1,
.y = t.screen.rows - 1,
}).toScreen(&t.screen);
const top = t.screen.pages.getTopLeft(.viewport);
const bot = t.screen.pages.getBottomRight(.viewport).?;
// Go through the placements and ensure the image is loaded on the GPU.
var it = storage.placements.iterator();
@ -823,13 +805,15 @@ fn prepKittyGraphics(
// If the selection isn't within our viewport then skip it.
const rect = p.rect(image, t);
if (rect.top_left.y > bot.y) continue;
if (rect.bottom_right.y < top.y) continue;
if (bot.before(rect.top_left)) continue;
if (rect.bottom_right.before(top)) continue;
// If the top left is outside the viewport we need to calc an offset
// so that we render (0, 0) with some offset for the texture.
const offset_y: u32 = if (rect.top_left.y < t.screen.viewport) offset_y: {
const offset_cells = t.screen.viewport - rect.top_left.y;
const offset_y: u32 = if (rect.top_left.before(top)) offset_y: {
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
const offset_cells = vp_y - img_y;
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
break :offset_y @intCast(offset_pixels);
} else 0;
@ -875,7 +859,10 @@ fn prepKittyGraphics(
}
// Convert our screen point to a viewport point
const viewport = p.point.toViewport(&t.screen);
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
.viewport,
p.pin.*,
) orelse .{ .viewport = .{} };
// Calculate the source rectangle
const source_x = @min(image.width, p.source_x);
@ -897,8 +884,8 @@ fn prepKittyGraphics(
if (image.width > 0 and image.height > 0) {
try self.image_placements.append(self.alloc, .{
.image_id = kv.key_ptr.image_id,
.x = @intCast(p.point.x),
.y = @intCast(viewport.y),
.x = @intCast(p.pin.x),
.y = @intCast(viewport.viewport.y),
.z = p.z,
.width = dest_width,
.height = dest_height,
@ -955,16 +942,21 @@ fn prepKittyGraphics(
/// the renderer will do this when it needs more memory space.
pub fn rebuildCells(
self: *OpenGL,
term_selection: ?terminal.Selection,
screen: *terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette,
) !void {
const rows_usize: usize = @intCast(screen.pages.rows);
const cols_usize: usize = @intCast(screen.pages.cols);
// Bg cells at most will need space for the visible screen size
self.cells_bg.clearRetainingCapacity();
try self.cells_bg.ensureTotalCapacity(self.alloc, screen.rows * screen.cols);
try self.cells_bg.ensureTotalCapacity(
self.alloc,
rows_usize * cols_usize,
);
// For now, we just ensure that we have enough cells for all the lines
// we have plus a full width. This is very likely too much but its
@ -975,7 +967,7 @@ pub fn rebuildCells(
// * 3 for glyph + underline + strikethrough for each cell
// + 1 for cursor
(screen.rows * screen.cols * 3) + 1,
(rows_usize * cols_usize * 3) + 1,
);
// Create an arena for all our temporary allocations while rebuilding
@ -1001,7 +993,7 @@ pub fn rebuildCells(
x: [2]usize,
cp_offset: usize,
} = if (preedit) |preedit_v| preedit: {
const range = preedit_v.range(screen.cursor.x, screen.cols - 1);
const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
break :preedit .{
.y = screen.cursor.y,
.x = .{ range.start, range.end },
@ -1015,9 +1007,9 @@ pub fn rebuildCells(
var cursor_cell: ?CellProgram.Cell = null;
// Build each cell
var rowIter = screen.rowIterator(.viewport);
var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null);
var y: usize = 0;
while (rowIter.next()) |row| {
while (row_it.next()) |row| {
defer y += 1;
// Our selection value is only non-null if this selection happens
@ -1025,19 +1017,10 @@ pub fn rebuildCells(
// the selection that contains this row. This way, if the selection
// changes but not for this line, we don't invalidate the cache.
const selection = sel: {
if (term_selection) |sel| {
const screen_point = (terminal.point.Viewport{
.x = 0,
.y = y,
}).toScreen(screen);
// If we are selected, we our colors are just inverted fg/bg.
if (sel.containedRow(screen, screen_point)) |row_sel| {
break :sel row_sel;
}
}
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// See Metal.zig
@ -1059,8 +1042,8 @@ pub fn rebuildCells(
defer if (cursor_row) {
// If we're on a wide spacer tail, then we want to look for
// the previous cell.
const screen_cell = row.getCell(screen.cursor.x);
const x = screen.cursor.x - @intFromBool(screen_cell.attrs.wide_spacer_tail);
const screen_cell = row.cells(.all)[screen.cursor.x];
const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail);
for (self.cells.items[start_i..]) |cell| {
if (cell.grid_col == x and
(cell.mode == .fg or cell.mode == .fg_color))
@ -1074,6 +1057,7 @@ pub fn rebuildCells(
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_group,
screen,
row,
selection,
if (shape_cursor) screen.cursor.x else null,
@ -1094,25 +1078,19 @@ pub fn rebuildCells(
// It this cell is within our hint range then we need to
// underline it.
const cell: terminal.Screen.Cell = cell: {
var cell = row.getCell(shaper_cell.x);
// If our links contain this cell then we want to
// underline it.
if (link_match_set.orderedContains(.{
.x = shaper_cell.x,
.y = y,
})) {
cell.attrs.underline = .single;
}
break :cell cell;
const cell: terminal.Pin = cell: {
var copy = row;
copy.x = shaper_cell.x;
break :cell copy;
};
if (self.updateCell(
term_selection,
screen,
cell,
if (link_match_set.orderedContains(screen, cell))
.single
else
null,
color_palette,
shaper_cell,
run,
@ -1129,9 +1107,6 @@ pub fn rebuildCells(
}
}
}
// Set row is not dirty anymore
row.setDirty(false);
}
// Add the cursor at the end so that it overlays everything. If we have
@ -1277,21 +1252,14 @@ fn addCursor(
// we're on the wide characer tail.
const wide, const x = cell: {
// The cursor goes over the screen cursor position.
const cell = screen.getCell(
.active,
screen.cursor.y,
screen.cursor.x,
);
if (!cell.attrs.wide_spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.attrs.wide, screen.cursor.x };
const cell = screen.cursor.page_cell;
if (cell.wide != .spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.wide == .wide, screen.cursor.x };
// If we're part of a wide character, we move the cursor back to
// the actual character.
break :cell .{ screen.getCell(
.active,
screen.cursor.y,
screen.cursor.x - 1,
).attrs.wide, screen.cursor.x - 1 };
const prev_cell = screen.cursorCellLeft(1);
break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
};
const color = self.cursor_color orelse self.foreground_color;
@ -1349,9 +1317,9 @@ fn addCursor(
/// needed.
fn updateCell(
self: *OpenGL,
selection: ?terminal.Selection,
screen: *terminal.Screen,
cell: terminal.Screen.Cell,
cell_pin: terminal.Pin,
cell_underline: ?terminal.Attribute.Underline,
palette: *const terminal.color.Palette,
shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun,
@ -1371,47 +1339,30 @@ fn updateCell(
};
// True if this cell is selected
// TODO(perf): we can check in advance if selection is in
// our viewport at all and not run this on every point.
const selected: bool = if (selection) |sel| selected: {
const screen_point = (terminal.point.Viewport{
.x = x,
.y = y,
}).toScreen(screen);
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, cell_pin)
else
false;
break :selected sel.contains(screen_point);
} else false;
const rac = cell_pin.rowAndCell();
const cell = rac.cell;
const style = cell_pin.style(cell);
const underline = cell_underline orelse style.flags.underline;
// The colors for the cell.
const colors: BgFg = colors: {
// The normal cell result
const cell_res: BgFg = if (!cell.attrs.inverse) .{
const cell_res: BgFg = if (!style.flags.inverse) .{
// In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color.
.bg = switch (cell.bg) {
.none => null,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.fg = switch (cell.fg) {
.none => self.foreground_color,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.bg = style.bg(cell, palette),
.fg = style.fg(palette) orelse self.foreground_color,
} else .{
// In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background.
.bg = switch (cell.fg) {
.none => self.foreground_color,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.fg = switch (cell.bg) {
.none => self.background_color,
.indexed => |i| palette[i],
.rgb => |rgb| rgb,
},
.bg = style.fg(palette) orelse self.foreground_color,
.fg = style.bg(cell, palette) orelse self.background_color,
};
// If we are selected, we our colors are just inverted fg/bg
@ -1429,7 +1380,7 @@ fn updateCell(
// If the cell is "invisible" then we just make fg = bg so that
// the cell is transparent but still copy-able.
const res: BgFg = selection_res orelse cell_res;
if (cell.attrs.invisible) {
if (style.flags.invisible) {
break :colors BgFg{
.bg = res.bg,
.fg = res.bg orelse self.background_color,
@ -1439,19 +1390,8 @@ fn updateCell(
break :colors res;
};
// Calculate the amount of space we need in the cells list.
const needed = needed: {
var i: usize = 0;
if (colors.bg != null) i += 1;
if (!cell.empty()) i += 1;
if (cell.attrs.underline != .none) i += 1;
if (cell.attrs.strikethrough) i += 1;
break :needed i;
};
if (self.cells.items.len + needed > self.cells.capacity) return false;
// Alpha multiplier
const alpha: u8 = if (cell.attrs.faint) 175 else 255;
const alpha: u8 = if (style.flags.faint) 175 else 255;
// If the cell has a background, we always draw it.
const bg: [4]u8 = if (colors.bg) |rgb| bg: {
@ -1468,11 +1408,11 @@ fn updateCell(
if (selected) break :bg_alpha default;
// If we're reversed, do not apply background opacity
if (cell.attrs.inverse) break :bg_alpha default;
if (style.flags.inverse) break :bg_alpha default;
// If we have a background and its not the default background
// then we apply background opacity
if (cell.bg != .none and !rgb.eql(self.background_color)) {
if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) {
break :bg_alpha default;
}
@ -1487,7 +1427,7 @@ fn updateCell(
.mode = .bg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.grid_width = cell.gridWidth(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
@ -1513,7 +1453,7 @@ fn updateCell(
};
// If the cell has a character, draw it
if (cell.char > 0) fg: {
if (cell.hasText()) fg: {
// Render
const glyph = try self.font_group.renderGlyph(
self.alloc,
@ -1528,11 +1468,8 @@ fn updateCell(
// If we're rendering a color font, we use the color atlas
const mode: CellProgram.CellMode = switch (try fgMode(
&self.font_group.group,
screen,
cell,
cell_pin,
shaper_run,
x,
y,
)) {
.normal => .fg,
.color => .fg_color,
@ -1543,7 +1480,7 @@ fn updateCell(
.mode = mode,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.grid_width = cell.gridWidth(),
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
@ -1561,8 +1498,8 @@ fn updateCell(
});
}
if (cell.attrs.underline != .none) {
const sprite: font.Sprite = switch (cell.attrs.underline) {
if (underline != .none) {
const sprite: font.Sprite = switch (underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
@ -1577,17 +1514,17 @@ fn updateCell(
@intFromEnum(sprite),
.{
.grid_metrics = self.grid_metrics,
.cell_width = if (cell.attrs.wide) 2 else 1,
.cell_width = if (cell.wide == .wide) 2 else 1,
},
);
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
const color = style.underlineColor(palette) orelse colors.fg;
self.cells.appendAssumeCapacity(.{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.grid_width = cell.gridWidth(),
.glyph_x = underline_glyph.atlas_x,
.glyph_y = underline_glyph.atlas_y,
.glyph_width = underline_glyph.width,
@ -1605,12 +1542,12 @@ fn updateCell(
});
}
if (cell.attrs.strikethrough) {
if (style.flags.strikethrough) {
self.cells.appendAssumeCapacity(.{
.mode = .strikethrough,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.widthLegacy(),
.grid_width = cell.gridWidth(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,

View File

@ -34,7 +34,7 @@ pub const Mouse = struct {
/// The point on the viewport where the mouse currently is. We use
/// viewport points to avoid the complexity of mapping the mouse to
/// the renderer state.
point: ?terminal.point.Viewport = null,
point: ?terminal.point.Coordinate = null,
/// The mods that are currently active for the last mouse event.
/// This could really just be mods in general and we probably will

View File

@ -21,11 +21,8 @@ pub const FgMode = enum {
/// renderer.
pub fn fgMode(
group: *font.Group,
screen: *terminal.Screen,
cell: terminal.Screen.Cell,
cell_pin: terminal.Pin,
shaper_run: font.shape.TextRun,
x: usize,
y: usize,
) !FgMode {
const presentation = try group.presentationFromIndex(shaper_run.font_index);
return switch (presentation) {
@ -41,42 +38,55 @@ pub fn fgMode(
// the subsequent character is empty, then we allow it to use
// the full glyph size. See #1071.
.text => text: {
if (!ziglyph.general_category.isPrivateUse(@intCast(cell.char)) and
!ziglyph.blocks.isDingbats(@intCast(cell.char)))
const cell = cell_pin.rowAndCell().cell;
const cp = cell.codepoint();
if (!ziglyph.general_category.isPrivateUse(cp) and
!ziglyph.blocks.isDingbats(cp))
{
break :text .normal;
}
// We exempt the Powerline range from this since they exhibit
// box-drawing behavior and should not be constrained.
if (isPowerline(cell.char)) {
if (isPowerline(cp)) {
break :text .normal;
}
// If we are at the end of the screen its definitely constrained
if (x == screen.cols - 1) break :text .constrained;
if (cell_pin.x == cell_pin.page.data.size.cols - 1) break :text .constrained;
// If we have a previous cell and it was PUA then we need to
// also constrain. This is so that multiple PUA glyphs align.
// As an exception, we ignore powerline glyphs since they are
// used for box drawing and we consider them whitespace.
if (x > 0) prev: {
const prev_cell = screen.getCell(.active, y, x - 1);
if (cell_pin.x > 0) prev: {
const prev_cp = prev_cp: {
var copy = cell_pin;
copy.x -= 1;
const prev_cell = copy.rowAndCell().cell;
break :prev_cp prev_cell.codepoint();
};
// Powerline is whitespace
if (isPowerline(prev_cell.char)) break :prev;
if (isPowerline(prev_cp)) break :prev;
if (ziglyph.general_category.isPrivateUse(@intCast(prev_cell.char))) {
if (ziglyph.general_category.isPrivateUse(prev_cp)) {
break :text .constrained;
}
}
// If the next cell is empty, then we allow it to use the
// full glyph size.
const next_cell = screen.getCell(.active, y, x + 1);
if (next_cell.char == 0 or
next_cell.char == ' ' or
isPowerline(next_cell.char))
const next_cp = next_cp: {
var copy = cell_pin;
copy.x += 1;
const next_cell = copy.rowAndCell().cell;
break :next_cp next_cell.codepoint();
};
if (next_cp == 0 or
next_cp == ' ' or
isPowerline(next_cp))
{
break :text .normal;
}
@ -88,7 +98,7 @@ pub fn fgMode(
}
// Returns true if the codepoint is a part of the Powerline range.
fn isPowerline(char: u32) bool {
fn isPowerline(char: u21) bool {
return switch (char) {
0xE0B0...0xE0C8, 0xE0CA, 0xE0CC...0xE0D2, 0xE0D4 => true,
else => false,

View File

@ -12,7 +12,7 @@ pub const CursorStyle = enum {
underline,
/// Create a cursor style from the terminal style request.
pub fn fromTerminal(style: terminal.Cursor.Style) ?CursorStyle {
pub fn fromTerminal(style: terminal.CursorStyle) ?CursorStyle {
return switch (style) {
.bar => .bar,
.block => .block,
@ -57,16 +57,16 @@ pub fn cursorStyle(
}
// Otherwise, we use whatever style the terminal wants.
return CursorStyle.fromTerminal(state.terminal.screen.cursor.style);
return CursorStyle.fromTerminal(state.terminal.screen.cursor.cursor_style);
}
test "cursor: default uses configured style" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, 10, 10);
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
term.screen.cursor.style = .bar;
term.screen.cursor.cursor_style = .bar;
term.modes.set(.cursor_blinking, true);
var state: State = .{
@ -84,10 +84,10 @@ test "cursor: default uses configured style" {
test "cursor: blinking disabled" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, 10, 10);
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
term.screen.cursor.style = .bar;
term.screen.cursor.cursor_style = .bar;
term.modes.set(.cursor_blinking, false);
var state: State = .{
@ -105,10 +105,10 @@ test "cursor: blinking disabled" {
test "cursor: explictly not visible" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, 10, 10);
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
term.screen.cursor.style = .bar;
term.screen.cursor.cursor_style = .bar;
term.modes.set(.cursor_visible, false);
term.modes.set(.cursor_blinking, false);
@ -127,7 +127,7 @@ test "cursor: explictly not visible" {
test "cursor: always block with preedit" {
const testing = std.testing;
const alloc = testing.allocator;
var term = try terminal.Terminal.init(alloc, 10, 10);
var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
defer term.deinit(alloc);
var state: State = .{

View File

@ -64,11 +64,13 @@ pub const Set = struct {
self: *const Set,
alloc: Allocator,
screen: *Screen,
mouse_vp_pt: point.Viewport,
mouse_vp_pt: point.Coordinate,
mouse_mods: inputpkg.Mods,
) !MatchSet {
// Convert the viewport point to a screen point.
const mouse_pt = mouse_vp_pt.toScreen(screen);
const mouse_pin = screen.pages.pin(.{
.viewport = mouse_vp_pt,
}) orelse return .{};
// This contains our list of matches. The matches are stored
// as selections which contain the start and end points of
@ -78,15 +80,26 @@ pub const Set = struct {
defer matches.deinit();
// Iterate over all the visible lines.
var lineIter = screen.lineIterator(.viewport);
while (lineIter.next()) |line| {
const strmap = line.stringMap(alloc) catch |err| {
var lineIter = screen.lineIterator(screen.pages.pin(.{
.viewport = .{},
}) orelse return .{});
while (lineIter.next()) |line_sel| {
const strmap: terminal.StringMap = strmap: {
var strmap: terminal.StringMap = undefined;
const str = screen.selectionString(alloc, .{
.sel = line_sel,
.trim = false,
.map = &strmap,
}) catch |err| {
log.warn(
"failed to build string map for link checking err={}",
.{err},
);
continue;
};
alloc.free(str);
break :strmap strmap;
};
defer strmap.deinit(alloc);
// Go through each link and see if we have any matches.
@ -98,7 +111,7 @@ pub const Set = struct {
.always => {},
.always_mods => |v| if (!mouse_mods.equal(v)) continue,
inline .hover, .hover_mods => |v, tag| {
if (!line.selection().contains(mouse_pt)) continue;
if (!line_sel.contains(screen, mouse_pin)) continue;
if (comptime tag == .hover_mods) {
if (!mouse_mods.equal(v)) continue;
}
@ -121,7 +134,7 @@ pub const Set = struct {
.always, .always_mods => {},
.hover,
.hover_mods,
=> if (!sel.contains(mouse_pt)) continue,
=> if (!sel.contains(screen, mouse_pin)) continue,
}
try matches.append(sel);
@ -153,19 +166,20 @@ pub const MatchSet = struct {
/// results.
pub fn orderedContains(
self: *MatchSet,
pt: point.ScreenPoint,
screen: *const Screen,
pin: terminal.Pin,
) bool {
// If we're beyond the end of our possible matches, we're done.
if (self.i >= self.matches.len) return false;
// If our selection ends before the point, then no point will ever
// again match this selection so we move on to the next one.
while (self.matches[self.i].end.before(pt)) {
while (self.matches[self.i].end().before(pin)) {
self.i += 1;
if (self.i >= self.matches.len) return false;
}
return self.matches[self.i].contains(pt);
return self.matches[self.i].contains(screen, pin);
}
};
@ -201,12 +215,30 @@ test "matchset" {
try testing.expectEqual(@as(usize, 2), match.matches.len);
// Test our matches
try testing.expect(!match.orderedContains(.{ .x = 0, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 1, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 2, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 3, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 1, .y = 1 }));
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 0,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 2,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 3,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 1,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 2,
} }).?));
}
test "matchset hover links" {
@ -242,12 +274,30 @@ test "matchset hover links" {
try testing.expectEqual(@as(usize, 1), match.matches.len);
// Test our matches
try testing.expect(!match.orderedContains(.{ .x = 0, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 2, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 3, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 1, .y = 1 }));
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 0,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 2,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 3,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 1,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 2,
} }).?));
}
// Hovering over the first link
@ -257,12 +307,30 @@ test "matchset hover links" {
try testing.expectEqual(@as(usize, 2), match.matches.len);
// Test our matches
try testing.expect(!match.orderedContains(.{ .x = 0, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 1, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 2, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 3, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 1, .y = 1 }));
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 0,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 2,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 3,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 1,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 2,
} }).?));
}
}
@ -298,10 +366,28 @@ test "matchset mods no match" {
try testing.expectEqual(@as(usize, 1), match.matches.len);
// Test our matches
try testing.expect(!match.orderedContains(.{ .x = 0, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 1, .y = 0 }));
try testing.expect(match.orderedContains(.{ .x = 2, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 3, .y = 0 }));
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 1 }));
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 0,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 0,
} }).?));
try testing.expect(match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 2,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 3,
.y = 0,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 1,
} }).?));
try testing.expect(!match.orderedContains(&s, s.pages.pin(.{ .screen = .{
.x = 1,
.y = 2,
} }).?));
}

View File

@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const font = @import("../font/main.zig");
const terminal = @import("../terminal/main.zig");
const log = std.log.scoped(.renderer_size);
@ -61,7 +62,7 @@ pub const ScreenSize = struct {
/// The dimensions of the grid itself, in rows/columns units.
pub const GridSize = struct {
const Unit = u32;
const Unit = terminal.size.CellCountInt;
columns: Unit = 0,
rows: Unit = 0,

7204
src/terminal/PageList.zig Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,11 @@ const oni = @import("oniguruma");
const point = @import("point.zig");
const Selection = @import("Selection.zig");
const Screen = @import("Screen.zig");
const Pin = @import("PageList.zig").Pin;
const Allocator = std.mem.Allocator;
string: [:0]const u8,
map: []point.ScreenPoint,
map: []Pin,
pub fn deinit(self: StringMap, alloc: Allocator) void {
alloc.free(self.string);
@ -79,11 +80,11 @@ pub const Match = struct {
const end_idx: usize = @intCast(self.region.ends()[0] - 1);
const start_pt = self.map.map[self.offset + start_idx];
const end_pt = self.map.map[self.offset + end_idx];
return .{ .start = start_pt, .end = end_pt };
return Selection.init(start_pt, end_pt, false);
}
};
test "searchIterator" {
test "StringMap searchIterator" {
const testing = std.testing;
const alloc = testing.allocator;
@ -103,8 +104,19 @@ test "searchIterator" {
defer s.deinit();
const str = "1ABCD2EFGH\n3IJKL";
try s.testWriteString(str);
const line = s.getLine(.{ .x = 2, .y = 1 }).?;
const map = try line.stringMap(alloc);
const line = s.selectLine(.{
.pin = s.pages.pin(.{ .active = .{
.x = 2,
.y = 1,
} }).?,
}).?;
var map: StringMap = undefined;
const sel_str = try s.selectionString(alloc, .{
.sel = line,
.trim = false,
.map = &map,
});
alloc.free(sel_str);
defer map.deinit(alloc);
// Get our iterator
@ -114,10 +126,14 @@ test "searchIterator" {
defer match.deinit();
const sel = match.selection();
try testing.expectEqual(Selection{
.start = .{ .x = 1, .y = 0 },
.end = .{ .x = 2, .y = 0 },
}, sel);
try testing.expectEqual(point.Point{ .screen = .{
.x = 1,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.start()).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 2,
.y = 0,
} }, s.pages.pointFromPin(.screen, sel.end()).?);
}
try testing.expect(try it.next() == null);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,474 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const size = @import("size.zig");
const getOffset = size.getOffset;
const Offset = size.Offset;
const OffsetBuf = size.OffsetBuf;
const alignForward = std.mem.alignForward;
/// A relatively naive bitmap allocator that uses memory offsets against
/// a fixed backing buffer so that the backing buffer can be easily moved
/// without having to update pointers.
///
/// The chunk size determines the size of each chunk in bytes. This is the
/// minimum distributed unit of memory. For example, if you request a
/// 1-byte allocation, you'll use a chunk of chunk_size bytes. Likewise,
/// if your chunk size is 4, and you request a 5-byte allocation, you'll
/// use 2 chunks.
///
/// The allocator is susceptible to fragmentation. If you allocate and free
/// memory in a way that leaves small holes in the memory, you may not be
/// able to allocate large chunks of memory even if there is enough free
/// memory in aggregate. To avoid fragmentation, use a chunk size that is
/// large enough to cover most of your allocations.
///
// Notes for contributors: this is highly contributor friendly part of
// the code. If you can improve this, add tests, show benchmarks, then
// please do so!
pub fn BitmapAllocator(comptime chunk_size: comptime_int) type {
return struct {
const Self = @This();
comptime {
assert(std.math.isPowerOfTwo(chunk_size));
}
pub const base_align = @alignOf(u64);
pub const bitmap_bit_size = @bitSizeOf(u64);
/// The bitmap of available chunks. Each bit represents a chunk. A
/// 1 means the chunk is free and a 0 means it's used. We use 1
/// for free since it makes it very slightly faster to find free
/// chunks.
bitmap: Offset(u64),
bitmap_count: usize,
/// The contiguous buffer of chunks.
chunks: Offset(u8),
/// Initialize the allocator map with a given buf and memory layout.
pub fn init(buf: OffsetBuf, l: Layout) Self {
assert(@intFromPtr(buf.start()) % base_align == 0);
// Initialize our bitmaps to all 1s to note that all chunks are free.
const bitmap = buf.member(u64, l.bitmap_start);
const bitmap_ptr = bitmap.ptr(buf);
@memset(bitmap_ptr[0..l.bitmap_count], std.math.maxInt(u64));
return .{
.bitmap = bitmap,
.bitmap_count = l.bitmap_count,
.chunks = buf.member(u8, l.chunks_start),
};
}
/// Allocate n elements of type T. This will return error.OutOfMemory
/// if there isn't enough space in the backing buffer.
pub fn alloc(
self: *Self,
comptime T: type,
base: anytype,
n: usize,
) Allocator.Error![]T {
// note: we don't handle alignment yet, we just require that all
// types are properly aligned. This is a limitation that should be
// fixed but we haven't needed it. Contributor friendly: add tests
// and fix this.
assert(chunk_size % @alignOf(T) == 0);
assert(n > 0);
const byte_count = std.math.mul(usize, @sizeOf(T), n) catch
return error.OutOfMemory;
const chunk_count = std.math.divCeil(usize, byte_count, chunk_size) catch
return error.OutOfMemory;
// Find the index of the free chunk. This also marks it as used.
const bitmaps = self.bitmap.ptr(base);
const idx = findFreeChunks(bitmaps[0..self.bitmap_count], chunk_count) orelse
return error.OutOfMemory;
const chunks = self.chunks.ptr(base);
const ptr: [*]T = @alignCast(@ptrCast(&chunks[idx * chunk_size]));
return ptr[0..n];
}
pub fn free(self: *Self, base: anytype, slice: anytype) void {
// Convert the slice of whatever type to a slice of bytes. We
// can then use the byte len and chunk size to determine the
// number of chunks that were allocated.
const bytes = std.mem.sliceAsBytes(slice);
const aligned_len = std.mem.alignForward(usize, bytes.len, chunk_size);
const chunk_count = @divExact(aligned_len, chunk_size);
// From the pointer, we can calculate the exact index.
const chunks = self.chunks.ptr(base);
const chunk_idx = @divExact(@intFromPtr(slice.ptr) - @intFromPtr(chunks), chunk_size);
// From the chunk index, we can find the starting bitmap index
// and the bit within the last bitmap.
var bitmap_idx = @divFloor(chunk_idx, 64);
const bitmap_bit = chunk_idx % 64;
const bitmaps = self.bitmap.ptr(base);
// If our chunk count is over 64 then we need to handle the
// case where we have to mark multiple bitmaps.
if (chunk_count > 64) {
const bitmaps_full = @divFloor(chunk_count, 64);
for (0..bitmaps_full) |i| bitmaps[bitmap_idx + i] = std.math.maxInt(u64);
bitmap_idx += bitmaps_full;
}
// Set the bitmap to mark the chunks as free. Note we have to
// do chunk_count % 64 to handle the case where our chunk count
// is using multiple bitmaps.
const bitmap = &bitmaps[bitmap_idx];
for (0..chunk_count % 64) |i| {
const mask = @as(u64, 1) << @intCast(bitmap_bit + i);
bitmap.* |= mask;
}
}
/// For debugging
fn dumpBitmaps(self: *Self, base: anytype) void {
const bitmaps = self.bitmap.ptr(base);
for (bitmaps[0..self.bitmap_count], 0..) |bitmap, idx| {
std.log.warn("bm={b} idx={}", .{ bitmap, idx });
}
}
pub const Layout = struct {
total_size: usize,
bitmap_count: usize,
bitmap_start: usize,
chunks_start: usize,
};
/// Get the layout for the given capacity. The capacity is in
/// number of bytes, not chunks. The capacity will likely be
/// rounded up to the nearest chunk size and bitmap size so
/// everything is perfectly divisible.
pub fn layout(cap: usize) Layout {
// Align the cap forward to our chunk size so we always have
// a full chunk at the end.
const aligned_cap = alignForward(usize, cap, chunk_size);
// Calculate the number of bitmaps. We need 1 bitmap per 64 chunks.
// We align the chunk count forward so our bitmaps are full so we
// don't have to handle the case where we have a partial bitmap.
const chunk_count = @divExact(aligned_cap, chunk_size);
const aligned_chunk_count = alignForward(usize, chunk_count, 64);
const bitmap_count = @divExact(aligned_chunk_count, 64);
const bitmap_start = 0;
const bitmap_end = @sizeOf(u64) * bitmap_count;
const chunks_start = alignForward(usize, bitmap_end, @alignOf(u8));
const chunks_end = chunks_start + (aligned_cap * chunk_size);
const total_size = chunks_end;
return Layout{
.total_size = total_size,
.bitmap_count = bitmap_count,
.bitmap_start = bitmap_start,
.chunks_start = chunks_start,
};
}
};
}
/// Find `n` sequential free chunks in the given bitmaps and return the index
/// of the first chunk. If no chunks are found, return `null`. This also updates
/// the bitmap to mark the chunks as used.
fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
// NOTE: This is a naive implementation that just iterates through the
// bitmaps. There is very likely a more efficient way to do this but
// I'm not a bit twiddling expert. Perhaps even SIMD could be used here
// but unsure. Contributor friendly: let's benchmark and improve this!
// Large chunks require special handling. In this case we look for
// divFloor sequential chunks that are maxInt, then look for the mod
// normally in the next bitmap.
if (n > @bitSizeOf(u64)) {
const div = @divFloor(n, @bitSizeOf(u64));
const mod = n % @bitSizeOf(u64);
var seq: usize = 0;
for (bitmaps, 0..) |*bitmap, idx| {
// If we aren't fully empty then reset the sequence
if (bitmap.* != std.math.maxInt(u64)) {
seq = 0;
continue;
}
// If we haven't reached the sequence count we're looking for
// then add one and continue, we're still accumulating blanks.
if (seq != div) {
seq += 1;
if (seq != div or mod > 0) continue;
}
// We've reached the seq count see if this has mod starting empty
// blanks.
if (mod > 0) {
const final = @as(u64, std.math.maxInt(u64)) >> @intCast(64 - mod);
if (bitmap.* & final == 0) {
// No blanks, reset.
seq = 0;
continue;
}
bitmap.* ^= final;
}
// Found! Set all in our sequence to full and mask our final.
// The "zero_mod" modifier below handles the case where we have
// a perfectly divisible number of chunks so we don't have to
// mark the trailing bitmap.
const zero_mod = @intFromBool(mod == 0);
const start_idx = idx - (seq - zero_mod);
const end_idx = idx + zero_mod;
for (start_idx..end_idx) |i| bitmaps[i] = 0;
return (start_idx * 64);
}
return null;
}
assert(n <= @bitSizeOf(u64));
for (bitmaps, 0..) |*bitmap, idx| {
// Shift the bitmap to find `n` sequential free chunks.
var shifted: u64 = bitmap.*;
for (1..n) |i| shifted &= bitmap.* >> @intCast(i);
// If we have zero then we have no matches
if (shifted == 0) continue;
// Trailing zeroes gets us the bit 1-indexed
const bit = @ctz(shifted);
// Calculate the mask so we can mark it as used
for (0..n) |i| {
const mask = @as(u64, 1) << @intCast(bit + i);
bitmap.* ^= mask;
}
return (idx * 64) + bit;
}
return null;
}
test "findFreeChunks single found" {
const testing = std.testing;
var bitmaps = [_]u64{
0b10000000_00000000_00000000_00000000_00000000_00000000_00001110_00000000,
};
const idx = findFreeChunks(&bitmaps, 2).?;
try testing.expectEqual(@as(usize, 9), idx);
try testing.expectEqual(
0b10000000_00000000_00000000_00000000_00000000_00000000_00001000_00000000,
bitmaps[0],
);
}
test "findFreeChunks single not found" {
const testing = std.testing;
var bitmaps = [_]u64{0b10000111_00000000_00000000_00000000_00000000_00000000_00000000_00000000};
const idx = findFreeChunks(&bitmaps, 4);
try testing.expect(idx == null);
}
test "findFreeChunks multiple found" {
const testing = std.testing;
var bitmaps = [_]u64{
0b10000111_00000000_00000000_00000000_00000000_00000000_00000000_01110000,
0b10000000_00111110_00000000_00000000_00000000_00000000_00111110_00000000,
};
const idx = findFreeChunks(&bitmaps, 4).?;
try testing.expectEqual(@as(usize, 73), idx);
try testing.expectEqual(
0b10000000_00111110_00000000_00000000_00000000_00000000_00100000_00000000,
bitmaps[1],
);
}
test "findFreeChunks exactly 64 chunks" {
const testing = std.testing;
var bitmaps = [_]u64{
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
};
const idx = findFreeChunks(&bitmaps, 64).?;
try testing.expectEqual(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[0],
);
try testing.expectEqual(@as(usize, 0), idx);
}
test "findFreeChunks larger than 64 chunks" {
const testing = std.testing;
var bitmaps = [_]u64{
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
};
const idx = findFreeChunks(&bitmaps, 65).?;
try testing.expectEqual(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[0],
);
try testing.expectEqual(
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111110,
bitmaps[1],
);
try testing.expectEqual(@as(usize, 0), idx);
}
test "findFreeChunks larger than 64 chunks not at beginning" {
const testing = std.testing;
var bitmaps = [_]u64{
0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
};
const idx = findFreeChunks(&bitmaps, 65).?;
try testing.expectEqual(
0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[0],
);
try testing.expectEqual(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[1],
);
try testing.expectEqual(
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111110,
bitmaps[2],
);
try testing.expectEqual(@as(usize, 64), idx);
}
test "findFreeChunks larger than 64 chunks exact" {
const testing = std.testing;
var bitmaps = [_]u64{
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
};
const idx = findFreeChunks(&bitmaps, 128).?;
try testing.expectEqual(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[0],
);
try testing.expectEqual(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[1],
);
try testing.expectEqual(@as(usize, 0), idx);
}
test "BitmapAllocator layout" {
const Alloc = BitmapAllocator(4);
const cap = 64 * 4;
const testing = std.testing;
const layout = Alloc.layout(cap);
// We expect to use one bitmap since the cap is bytes.
try testing.expectEqual(@as(usize, 1), layout.bitmap_count);
}
test "BitmapAllocator alloc sequentially" {
const Alloc = BitmapAllocator(4);
const cap = 64;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(OffsetBuf.init(buf), layout);
const ptr = try bm.alloc(u8, buf, 1);
ptr[0] = 'A';
const ptr2 = try bm.alloc(u8, buf, 1);
try testing.expect(@intFromPtr(ptr.ptr) != @intFromPtr(ptr2.ptr));
// Should grab the next chunk
try testing.expectEqual(@intFromPtr(ptr.ptr) + 4, @intFromPtr(ptr2.ptr));
// Free ptr and next allocation should be back
bm.free(buf, ptr);
const ptr3 = try bm.alloc(u8, buf, 1);
try testing.expectEqual(@intFromPtr(ptr.ptr), @intFromPtr(ptr3.ptr));
}
test "BitmapAllocator alloc non-byte" {
const Alloc = BitmapAllocator(4);
const cap = 128;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(OffsetBuf.init(buf), layout);
const ptr = try bm.alloc(u21, buf, 1);
ptr[0] = 'A';
const ptr2 = try bm.alloc(u21, buf, 1);
try testing.expect(@intFromPtr(ptr.ptr) != @intFromPtr(ptr2.ptr));
try testing.expectEqual(@intFromPtr(ptr.ptr) + 4, @intFromPtr(ptr2.ptr));
// Free ptr and next allocation should be back
bm.free(buf, ptr);
const ptr3 = try bm.alloc(u21, buf, 1);
try testing.expectEqual(@intFromPtr(ptr.ptr), @intFromPtr(ptr3.ptr));
}
test "BitmapAllocator alloc non-byte multi-chunk" {
const Alloc = BitmapAllocator(4 * @sizeOf(u21));
const cap = 128;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(OffsetBuf.init(buf), layout);
const ptr = try bm.alloc(u21, buf, 6);
try testing.expectEqual(@as(usize, 6), ptr.len);
for (ptr) |*v| v.* = 'A';
const ptr2 = try bm.alloc(u21, buf, 1);
try testing.expect(@intFromPtr(ptr.ptr) != @intFromPtr(ptr2.ptr));
try testing.expectEqual(@intFromPtr(ptr.ptr) + (@sizeOf(u21) * 4 * 2), @intFromPtr(ptr2.ptr));
// Free ptr and next allocation should be back
bm.free(buf, ptr);
const ptr3 = try bm.alloc(u21, buf, 1);
try testing.expectEqual(@intFromPtr(ptr.ptr), @intFromPtr(ptr3.ptr));
}
test "BitmapAllocator alloc large" {
const Alloc = BitmapAllocator(2);
const cap = 256;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(OffsetBuf.init(buf), layout);
const ptr = try bm.alloc(u8, buf, 129);
ptr[0] = 'A';
bm.free(buf, ptr);
}

1513
src/terminal/hash_map.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -184,15 +184,19 @@ fn display(
// Make sure our response has the image id in case we looked up by number
result.id = img.id;
// Determine the screen point for the placement.
const placement_point = (point.Viewport{
.x = terminal.screen.cursor.x,
.y = terminal.screen.cursor.y,
}).toScreen(&terminal.screen);
// Track a new pin for our cursor. The cursor is always tracked but we
// don't want this one to move with the cursor.
const placement_pin = terminal.screen.pages.trackPin(
terminal.screen.cursor.page_pin.*,
) catch |err| {
log.warn("failed to create pin for Kitty graphics err={}", .{err});
result.message = "EINVAL: failed to prepare terminal state";
return result;
};
// Add the placement
const p: ImageStorage.Placement = .{
.point = placement_point,
.pin = placement_pin,
.x_offset = d.x_offset,
.y_offset = d.y_offset,
.source_x = d.x,
@ -209,6 +213,7 @@ fn display(
result.placement_id,
p,
) catch |err| {
p.deinit(&terminal.screen);
encodeError(&result, err);
return result;
};
@ -217,19 +222,16 @@ fn display(
switch (d.cursor_movement) {
.none => {},
.after => {
const rect = p.rect(img, terminal);
// We can do better by doing this with pure internal screen state
// but this handles scroll regions.
const height = rect.bottom_right.y - rect.top_left.y;
for (0..height) |_| terminal.index() catch |err| {
// We use terminal.index to properly handle scroll regions.
const size = p.gridSize(img, terminal);
for (0..size.rows) |_| terminal.index() catch |err| {
log.warn("failed to move cursor: {}", .{err});
break;
};
terminal.setCursorPos(
terminal.screen.cursor.y,
rect.bottom_right.x + 1,
p.pin.x + size.cols + 1,
);
},
}

View File

@ -7,6 +7,7 @@ const posix = std.posix;
const command = @import("graphics_command.zig");
const point = @import("../point.zig");
const PageList = @import("../PageList.zig");
const internal_os = @import("../../os/main.zig");
const stb = @import("../../stb/main.zig");
@ -452,16 +453,8 @@ pub const Image = struct {
/// be rounded up to the nearest grid cell since we can't place images
/// in partial grid cells.
pub const Rect = struct {
top_left: point.ScreenPoint = .{},
bottom_right: point.ScreenPoint = .{},
/// True if the rect contains a given screen point.
pub fn contains(self: Rect, p: point.ScreenPoint) bool {
return p.y >= self.top_left.y and
p.y <= self.bottom_right.y and
p.x >= self.top_left.x and
p.x <= self.bottom_right.x;
}
top_left: PageList.Pin,
bottom_right: PageList.Pin,
};
/// Easy base64 encoding function.

View File

@ -6,12 +6,12 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const terminal = @import("../main.zig");
const point = @import("../point.zig");
const command = @import("graphics_command.zig");
const PageList = @import("../PageList.zig");
const Screen = @import("../Screen.zig");
const LoadingImage = @import("graphics_image.zig").LoadingImage;
const Image = @import("graphics_image.zig").Image;
const Rect = @import("graphics_image.zig").Rect;
const Command = command.Command;
const ScreenPoint = point.ScreenPoint;
const log = std.log.scoped(.kitty_gfx);
@ -53,13 +53,18 @@ pub const ImageStorage = struct {
total_bytes: usize = 0,
total_limit: usize = 320 * 1000 * 1000, // 320MB
pub fn deinit(self: *ImageStorage, alloc: Allocator) void {
pub fn deinit(
self: *ImageStorage,
alloc: Allocator,
s: *terminal.Screen,
) void {
if (self.loading) |loading| loading.destroy(alloc);
var it = self.images.iterator();
while (it.next()) |kv| kv.value_ptr.deinit(alloc);
self.images.deinit(alloc);
self.clearPlacements(s);
self.placements.deinit(alloc);
}
@ -72,10 +77,15 @@ pub const ImageStorage = struct {
/// can be loaded. If this limit is lower, this will do an eviction
/// if necessary. If the value is zero, then Kitty image protocol will
/// be disabled.
pub fn setLimit(self: *ImageStorage, alloc: Allocator, limit: usize) !void {
pub fn setLimit(
self: *ImageStorage,
alloc: Allocator,
s: *terminal.Screen,
limit: usize,
) !void {
// Special case disabling by quickly deleting all
if (limit == 0) {
self.deinit(alloc);
self.deinit(alloc, s);
self.* = .{};
}
@ -170,6 +180,12 @@ pub const ImageStorage = struct {
self.dirty = true;
}
fn clearPlacements(self: *ImageStorage, s: *terminal.Screen) void {
var it = self.placements.iterator();
while (it.next()) |entry| entry.value_ptr.deinit(s);
self.placements.clearRetainingCapacity();
}
/// Get an image by its ID. If the image doesn't exist, null is returned.
pub fn imageById(self: *const ImageStorage, image_id: u32) ?Image {
return self.images.get(image_id);
@ -197,19 +213,20 @@ pub const ImageStorage = struct {
pub fn delete(
self: *ImageStorage,
alloc: Allocator,
t: *const terminal.Terminal,
t: *terminal.Terminal,
cmd: command.Delete,
) void {
switch (cmd) {
.all => |delete_images| if (delete_images) {
// We just reset our entire state.
self.deinit(alloc);
self.deinit(alloc, &t.screen);
self.* = .{
.dirty = true,
.total_limit = self.total_limit,
};
} else {
// Delete all our placements
self.clearPlacements(&t.screen);
self.placements.deinit(alloc);
self.placements = .{};
self.dirty = true;
@ -217,6 +234,7 @@ pub const ImageStorage = struct {
.id => |v| self.deleteById(
alloc,
&t.screen,
v.image_id,
v.placement_id,
v.delete,
@ -224,29 +242,59 @@ pub const ImageStorage = struct {
.newest => |v| newest: {
const img = self.imageByNumber(v.image_number) orelse break :newest;
self.deleteById(alloc, img.id, v.placement_id, v.delete);
self.deleteById(
alloc,
&t.screen,
img.id,
v.placement_id,
v.delete,
);
},
.intersect_cursor => |delete_images| {
const target = (point.Viewport{
self.deleteIntersecting(
alloc,
t,
.{ .active = .{
.x = t.screen.cursor.x,
.y = t.screen.cursor.y,
}).toScreen(&t.screen);
self.deleteIntersecting(alloc, t, target, delete_images, {}, null);
} },
delete_images,
{},
null,
);
},
.intersect_cell => |v| {
const target = (point.Viewport{ .x = v.x, .y = v.y }).toScreen(&t.screen);
self.deleteIntersecting(alloc, t, target, v.delete, {}, null);
self.deleteIntersecting(
alloc,
t,
.{ .active = .{
.x = v.x,
.y = v.y,
} },
v.delete,
{},
null,
);
},
.intersect_cell_z => |v| {
const target = (point.Viewport{ .x = v.x, .y = v.y }).toScreen(&t.screen);
self.deleteIntersecting(alloc, t, target, v.delete, v.z, struct {
self.deleteIntersecting(
alloc,
t,
.{ .active = .{
.x = v.x,
.y = v.y,
} },
v.delete,
v.z,
struct {
fn filter(ctx: i32, p: Placement) bool {
return p.z == ctx;
}
}.filter);
}.filter,
);
},
.column => |v| {
@ -255,6 +303,7 @@ pub const ImageStorage = struct {
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
const rect = entry.value_ptr.rect(img, t);
if (rect.top_left.x <= v.x and rect.bottom_right.x >= v.x) {
entry.value_ptr.deinit(&t.screen);
self.placements.removeByPtr(entry.key_ptr);
if (v.delete) self.deleteIfUnused(alloc, img.id);
}
@ -264,15 +313,24 @@ pub const ImageStorage = struct {
self.dirty = true;
},
.row => |v| {
// Get the screenpoint y
const y = (point.Viewport{ .x = 0, .y = v.y }).toScreen(&t.screen).y;
.row => |v| row: {
// v.y is in active coords so we want to convert it to a pin
// so we can compare by page offsets.
const target_pin = t.screen.pages.pin(.{ .active = .{
.y = v.y,
} }) orelse break :row;
var it = self.placements.iterator();
while (it.next()) |entry| {
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
const rect = entry.value_ptr.rect(img, t);
if (rect.top_left.y <= y and rect.bottom_right.y >= y) {
// We need to copy our pin to ensure we are at least at
// the top-left x.
var target_pin_copy = target_pin;
target_pin_copy.x = rect.top_left.x;
if (target_pin_copy.isBetween(rect.top_left, rect.bottom_right)) {
entry.value_ptr.deinit(&t.screen);
self.placements.removeByPtr(entry.key_ptr);
if (v.delete) self.deleteIfUnused(alloc, img.id);
}
@ -287,6 +345,7 @@ pub const ImageStorage = struct {
while (it.next()) |entry| {
if (entry.value_ptr.z == v.z) {
const image_id = entry.key_ptr.image_id;
entry.value_ptr.deinit(&t.screen);
self.placements.removeByPtr(entry.key_ptr);
if (v.delete) self.deleteIfUnused(alloc, image_id);
}
@ -305,6 +364,7 @@ pub const ImageStorage = struct {
fn deleteById(
self: *ImageStorage,
alloc: Allocator,
s: *terminal.Screen,
image_id: u32,
placement_id: u32,
delete_unused: bool,
@ -314,14 +374,18 @@ pub const ImageStorage = struct {
var it = self.placements.iterator();
while (it.next()) |entry| {
if (entry.key_ptr.image_id == image_id) {
entry.value_ptr.deinit(s);
self.placements.removeByPtr(entry.key_ptr);
}
}
} else {
_ = self.placements.remove(.{
if (self.placements.getEntry(.{
.image_id = image_id,
.placement_id = .{ .tag = .external, .id = placement_id },
});
})) |entry| {
entry.value_ptr.deinit(s);
self.placements.removeByPtr(entry.key_ptr);
}
}
// If this is specified, then we also delete the image
@ -353,18 +417,22 @@ pub const ImageStorage = struct {
fn deleteIntersecting(
self: *ImageStorage,
alloc: Allocator,
t: *const terminal.Terminal,
p: point.ScreenPoint,
t: *terminal.Terminal,
p: point.Point,
delete_unused: bool,
filter_ctx: anytype,
comptime filter: ?fn (@TypeOf(filter_ctx), Placement) bool,
) void {
// Convert our target point to a pin for comparison.
const target_pin = t.screen.pages.pin(p) orelse return;
var it = self.placements.iterator();
while (it.next()) |entry| {
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
const rect = entry.value_ptr.rect(img, t);
if (rect.contains(p)) {
if (target_pin.isBetween(rect.top_left, rect.bottom_right)) {
if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue;
entry.value_ptr.deinit(&t.screen);
self.placements.removeByPtr(entry.key_ptr);
if (delete_unused) self.deleteIfUnused(alloc, img.id);
}
@ -486,8 +554,8 @@ pub const ImageStorage = struct {
};
pub const Placement = struct {
/// The location of the image on the screen.
point: ScreenPoint,
/// The tracked pin for this placement.
pin: *PageList.Pin,
/// Offset of the x/y from the top-left of the cell.
x_offset: u32 = 0,
@ -506,23 +574,26 @@ pub const ImageStorage = struct {
/// The z-index for this placement.
z: i32 = 0,
/// Returns a selection of the entire rectangle this placement
/// occupies within the screen.
pub fn rect(
pub fn deinit(
self: *const Placement,
s: *terminal.Screen,
) void {
s.pages.untrackPin(self.pin);
}
/// Returns the size in grid cells that this placement takes up.
pub fn gridSize(
self: Placement,
image: Image,
t: *const terminal.Terminal,
) Rect {
// If we have columns/rows specified we can simplify this whole thing.
if (self.columns > 0 and self.rows > 0) {
return .{
.top_left = self.point,
.bottom_right = .{
.x = @min(self.point.x + self.columns, t.cols - 1),
.y = self.point.y + self.rows,
},
) struct {
cols: u32,
rows: u32,
} {
if (self.columns > 0 and self.rows > 0) return .{
.cols = self.columns,
.rows = self.rows,
};
}
// Calculate our cell size.
const terminal_width_f64: f64 = @floatFromInt(t.width_px);
@ -543,30 +614,58 @@ pub const ImageStorage = struct {
const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64));
return .{
.top_left = self.point,
.bottom_right = .{
.x = @min(self.point.x + width_cells, t.cols - 1),
.y = self.point.y + height_cells,
},
.cols = width_cells,
.rows = height_cells,
};
}
/// Returns a selection of the entire rectangle this placement
/// occupies within the screen.
pub fn rect(
self: Placement,
image: Image,
t: *const terminal.Terminal,
) Rect {
const grid_size = self.gridSize(image, t);
var br = switch (self.pin.downOverflow(grid_size.rows)) {
.offset => |v| v,
.overflow => |v| v.end,
};
br.x = @min(self.pin.x + grid_size.cols, t.cols - 1);
return .{
.top_left = self.pin.*,
.bottom_right = br,
};
}
};
};
// Our pin for the placement
fn trackPin(
t: *terminal.Terminal,
pt: point.Coordinate,
) !*PageList.Pin {
return try t.screen.pages.trackPin(t.screen.pages.pin(.{
.active = pt,
}).?);
}
test "storage: add placement with zero placement id" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 100, 100);
var t = try terminal.Terminal.init(alloc, .{ .cols = 100, .rows = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(alloc, 1, 0, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 0, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
try testing.expectEqual(@as(usize, 2), s.placements.count());
try testing.expectEqual(@as(usize, 2), s.images.count());
@ -585,38 +684,41 @@ test "storage: add placement with zero placement id" {
test "storage: delete all placements and images" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 3, 3);
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .all = true });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 0), s.images.count());
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete all placements and images preserves limit" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 3, 3);
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
s.total_limit = 5000;
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .all = true });
@ -624,85 +726,93 @@ test "storage: delete all placements and images preserves limit" {
try testing.expectEqual(@as(usize, 0), s.images.count());
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(@as(usize, 5000), s.total_limit);
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete all placements" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 3, 3);
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .all = false });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete all placements by image id" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 3, 3);
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .id = .{ .image_id = 2 } });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins());
}
test "storage: delete all placements by image id and unused images" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 3, 3);
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .id = .{ .delete = true, .image_id = 2 } });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expectEqual(@as(usize, 2), s.images.count());
try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins());
}
test "storage: delete placement by specific id" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 3, 3);
var t = try terminal.Terminal.init(alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1 });
try s.addImage(alloc, .{ .id = 2 });
try s.addImage(alloc, .{ .id = 3 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 2, 1, .{ .point = .{ .x = 1, .y = 1 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .id = .{
@ -713,31 +823,33 @@ test "storage: delete placement by specific id" {
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 2), s.placements.count());
try testing.expectEqual(@as(usize, 3), s.images.count());
try testing.expectEqual(tracked + 2, t.screen.pages.countTrackedPins());
}
test "storage: delete intersecting cursor" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 100, 100);
var t = try terminal.Terminal.init(alloc, .{ .rows = 100, .cols = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } });
try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
t.screen.cursor.x = 12;
t.screen.cursor.y = 12;
t.screen.cursorAbsolute(12, 12);
s.dirty = false;
s.delete(alloc, &t, .{ .intersect_cursor = false });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expectEqual(@as(usize, 2), s.images.count());
try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins());
// verify the placement is what we expect
try testing.expect(s.placements.get(.{
@ -749,26 +861,27 @@ test "storage: delete intersecting cursor" {
test "storage: delete intersecting cursor plus unused" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 100, 100);
var t = try terminal.Terminal.init(alloc, .{ .rows = 100, .cols = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } });
try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
t.screen.cursor.x = 12;
t.screen.cursor.y = 12;
t.screen.cursorAbsolute(12, 12);
s.dirty = false;
s.delete(alloc, &t, .{ .intersect_cursor = true });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expectEqual(@as(usize, 2), s.images.count());
try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins());
// verify the placement is what we expect
try testing.expect(s.placements.get(.{
@ -780,42 +893,44 @@ test "storage: delete intersecting cursor plus unused" {
test "storage: delete intersecting cursor hits multiple" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 100, 100);
var t = try terminal.Terminal.init(alloc, .{ .rows = 100, .cols = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } });
try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
t.screen.cursor.x = 26;
t.screen.cursor.y = 26;
t.screen.cursorAbsolute(26, 26);
s.dirty = false;
s.delete(alloc, &t, .{ .intersect_cursor = true });
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(@as(usize, 1), s.images.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: delete by column" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 100, 100);
var t = try terminal.Terminal.init(alloc, .{ .rows = 100, .cols = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } });
try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .column = .{
@ -825,6 +940,7 @@ test "storage: delete by column" {
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expectEqual(@as(usize, 2), s.images.count());
try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins());
// verify the placement is what we expect
try testing.expect(s.placements.get(.{
@ -836,17 +952,18 @@ test "storage: delete by column" {
test "storage: delete by row" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, 100, 100);
var t = try terminal.Terminal.init(alloc, .{ .rows = 100, .cols = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
const tracked = t.screen.pages.countTrackedPins();
var s: ImageStorage = .{};
defer s.deinit(alloc);
defer s.deinit(alloc, &t.screen);
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
try s.addPlacement(alloc, 1, 1, .{ .point = .{ .x = 0, .y = 0 } });
try s.addPlacement(alloc, 1, 2, .{ .point = .{ .x = 25, .y = 25 } });
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
s.dirty = false;
s.delete(alloc, &t, .{ .row = .{
@ -856,6 +973,7 @@ test "storage: delete by row" {
try testing.expect(s.dirty);
try testing.expectEqual(@as(usize, 1), s.placements.count());
try testing.expectEqual(@as(usize, 2), s.images.count());
try testing.expectEqual(tracked + 1, t.screen.pages.countTrackedPins());
// verify the placement is what we expect
try testing.expect(s.placements.get(.{

View File

@ -7,6 +7,7 @@ const stream = @import("stream.zig");
const ansi = @import("ansi.zig");
const csi = @import("csi.zig");
const sgr = @import("sgr.zig");
const style = @import("style.zig");
pub const apc = @import("apc.zig");
pub const dcs = @import("dcs.zig");
pub const osc = @import("osc.zig");
@ -15,22 +16,31 @@ pub const color = @import("color.zig");
pub const device_status = @import("device_status.zig");
pub const kitty = @import("kitty.zig");
pub const modes = @import("modes.zig");
pub const page = @import("page.zig");
pub const parse_table = @import("parse_table.zig");
pub const size = @import("size.zig");
pub const x11_color = @import("x11_color.zig");
pub const Charset = charsets.Charset;
pub const CharsetSlot = charsets.Slots;
pub const CharsetActiveSlot = charsets.ActiveSlot;
pub const Cell = page.Cell;
pub const CSI = Parser.Action.CSI;
pub const DCS = Parser.Action.DCS;
pub const MouseShape = @import("mouse_shape.zig").MouseShape;
pub const Terminal = @import("Terminal.zig");
pub const Page = page.Page;
pub const PageList = @import("PageList.zig");
pub const Parser = @import("Parser.zig");
pub const Selection = @import("Selection.zig");
pub const Pin = PageList.Pin;
pub const Screen = @import("Screen.zig");
pub const ScreenType = Terminal.ScreenType;
pub const Selection = @import("Selection.zig");
pub const StringMap = @import("StringMap.zig");
pub const Style = style.Style;
pub const Terminal = @import("Terminal.zig");
pub const Stream = stream.Stream;
pub const Cursor = Screen.Cursor;
pub const CursorStyle = Screen.CursorStyle;
pub const CursorStyleReq = ansi.CursorStyle;
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
pub const Mode = modes.Mode;
@ -43,11 +53,11 @@ pub const EraseLine = csi.EraseLine;
pub const TabClear = csi.TabClear;
pub const Attribute = sgr.Attribute;
/// If we're targeting wasm then we export some wasm APIs.
pub usingnamespace if (builtin.target.isWasm()) struct {
pub usingnamespace @import("wasm.zig");
} else struct {};
test {
@import("std").testing.refAllDecls(@This());
// Internals
_ = @import("bitmap_allocator.zig");
_ = @import("hash_map.zig");
_ = @import("size.zig");
}

2090
src/terminal/page.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,254 +1,74 @@
const std = @import("std");
const terminal = @import("main.zig");
const Screen = terminal.Screen;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
// This file contains various types to represent x/y coordinates. We
// use different types so that we can lean on type-safety to get the
// exact expected type of point.
/// Active is a point within the active part of the screen.
pub const Active = struct {
x: usize = 0,
y: usize = 0,
pub fn toScreen(self: Active, screen: *const Screen) ScreenPoint {
return .{
.x = self.x,
.y = screen.history + self.y,
};
}
test "toScreen with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 3, 5, 3);
defer s.deinit();
const str = "1\n2\n3\n4\n5\n6\n7\n8";
try s.testWriteString(str);
try testing.expectEqual(ScreenPoint{
.x = 1,
.y = 5,
}, (Active{ .x = 1, .y = 2 }).toScreen(&s));
}
};
/// Viewport is a point within the viewport of the screen.
pub const Viewport = struct {
x: usize = 0,
y: usize = 0,
pub fn toScreen(self: Viewport, screen: *const Screen) ScreenPoint {
// x is unchanged, y we have to add the visible offset to
// get the full offset from the top.
return .{
.x = self.x,
.y = screen.viewport + self.y,
};
}
pub fn eql(self: Viewport, other: Viewport) bool {
return self.x == other.x and self.y == other.y;
}
test "toScreen with no scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 3, 5, 0);
defer s.deinit();
try testing.expectEqual(ScreenPoint{
.x = 1,
.y = 1,
}, (Viewport{ .x = 1, .y = 1 }).toScreen(&s));
}
test "toScreen with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 3, 5, 3);
defer s.deinit();
// At the bottom
try s.scroll(.{ .screen = 6 });
try testing.expectEqual(ScreenPoint{
.x = 0,
.y = 3,
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
// Move the viewport a bit up
try s.scroll(.{ .screen = -1 });
try testing.expectEqual(ScreenPoint{
.x = 0,
.y = 2,
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
// Move the viewport to top
try s.scroll(.{ .top = {} });
try testing.expectEqual(ScreenPoint{
.x = 0,
.y = 0,
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
}
};
/// A screen point. This is offset from the top of the scrollback
/// buffer. If the screen is scrolled or resized, this will have to
/// be recomputed.
pub const ScreenPoint = struct {
x: usize = 0,
y: usize = 0,
/// Returns if this point is before another point.
pub fn before(self: ScreenPoint, other: ScreenPoint) bool {
return self.y < other.y or
(self.y == other.y and self.x < other.x);
}
/// Returns if two points are equal.
pub fn eql(self: ScreenPoint, other: ScreenPoint) bool {
return self.x == other.x and self.y == other.y;
}
/// Returns true if this screen point is currently in the active viewport.
pub fn inViewport(self: ScreenPoint, screen: *const Screen) bool {
return self.y >= screen.viewport and
self.y < screen.viewport + screen.rows;
}
/// Converts this to a viewport point. If the point is above the
/// viewport this will move the point to (0, 0) and if it is below
/// the viewport it'll move it to (cols - 1, rows - 1).
pub fn toViewport(self: ScreenPoint, screen: *const Screen) Viewport {
// TODO: test
// Before viewport
if (self.y < screen.viewport) return .{ .x = 0, .y = 0 };
// After viewport
if (self.y > screen.viewport + screen.rows) return .{
.x = screen.cols - 1,
.y = screen.rows - 1,
};
return .{ .x = self.x, .y = self.y - screen.viewport };
}
/// Returns a screen point iterator. This will iterate over all of
/// of the points in a screen in a given direction one by one.
/// The possible reference locations for a point. When someone says "(42, 80)" in the context of a terminal, that could mean multiple
/// things: it is in the current visible viewport? the current active
/// area of the screen where the cursor is? the entire scrollback history?
/// etc. This tag is used to differentiate those cases.
pub const Tag = enum {
/// Top-left is part of the active area where a running program can
/// jump the cursor and make changes. The active area is the "editable"
/// part of the screen.
///
/// The iterator is only valid as long as the screen is not resized.
pub fn iterator(
self: ScreenPoint,
screen: *const Screen,
dir: Direction,
) Iterator {
return .{ .screen = screen, .current = self, .direction = dir };
}
/// The bottom-right of the active tag differs from all other tags
/// because it includes the full height (rows) of the screen, including
/// rows that may not be written yet. This is required because the active
/// area is fully "addressable" by the running program (see below) whereas
/// the other tags are used primarliy for reading/modifying past-written
/// data so they can't address unwritten rows.
///
/// Note for those less familiar with terminal functionality: there
/// are escape sequences to move the cursor to any position on
/// the screen, but it is limited to the size of the viewport and
/// the bottommost part of the screen. Terminal programs can't --
/// with sequences at the time of writing this comment -- modify
/// anything in the scrollback, visible viewport (if it differs
/// from the active area), etc.
active,
pub const Iterator = struct {
screen: *const Screen,
current: ?ScreenPoint,
direction: Direction,
/// Top-left is the visible viewport. This means that if the user
/// has scrolled in any direction, top-left changes. The bottom-right
/// is the last written row from the top-left.
viewport,
pub fn next(self: *Iterator) ?ScreenPoint {
const current = self.current orelse return null;
self.current = switch (self.direction) {
.left_up => left_up: {
if (current.x == 0) {
if (current.y == 0) break :left_up null;
break :left_up .{
.x = self.screen.cols - 1,
.y = current.y - 1,
/// Top-left is the furthest back in the scrollback history
/// supported by the screen and the bottom-right is the bottom-right
/// of the last written row. Note this last point is important: the
/// bottom right is NOT necessarilly the same as "active" because
/// "active" always allows referencing the full rows tall of the
/// screen whereas "screen" only contains written rows.
screen,
/// The top-left is the same as "screen" but the bottom-right is
/// the line just before the top of "active". This contains only
/// the scrollback history.
history,
};
/// An x/y point in the terminal for some definition of location (tag).
pub const Point = union(Tag) {
active: Coordinate,
viewport: Coordinate,
screen: Coordinate,
history: Coordinate,
pub fn coord(self: Point) Coordinate {
return switch (self) {
.active,
.viewport,
.screen,
.history,
=> |v| v,
};
}
break :left_up .{
.x = current.x - 1,
.y = current.y,
};
},
.right_down => right_down: {
if (current.x == self.screen.cols - 1) {
const max = self.screen.rows + self.screen.max_scrollback;
if (current.y == max - 1) break :right_down null;
break :right_down .{
.x = 0,
.y = current.y + 1,
};
}
break :right_down .{
.x = current.x + 1,
.y = current.y,
};
},
};
return current;
}
};
test "before" {
const testing = std.testing;
const p: ScreenPoint = .{ .x = 5, .y = 2 };
try testing.expect(p.before(.{ .x = 6, .y = 2 }));
try testing.expect(p.before(.{ .x = 3, .y = 3 }));
}
test "iterator" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 5, 5, 0);
defer s.deinit();
// Back from the first line
{
var pt: ScreenPoint = .{ .x = 1, .y = 0 };
var it = pt.iterator(&s, .left_up);
try testing.expectEqual(ScreenPoint{ .x = 1, .y = 0 }, it.next().?);
try testing.expectEqual(ScreenPoint{ .x = 0, .y = 0 }, it.next().?);
try testing.expect(it.next() == null);
}
// Back from second line
{
var pt: ScreenPoint = .{ .x = 1, .y = 1 };
var it = pt.iterator(&s, .left_up);
try testing.expectEqual(ScreenPoint{ .x = 1, .y = 1 }, it.next().?);
try testing.expectEqual(ScreenPoint{ .x = 0, .y = 1 }, it.next().?);
try testing.expectEqual(ScreenPoint{ .x = 4, .y = 0 }, it.next().?);
}
// Forward last line
{
var pt: ScreenPoint = .{ .x = 3, .y = 4 };
var it = pt.iterator(&s, .right_down);
try testing.expectEqual(ScreenPoint{ .x = 3, .y = 4 }, it.next().?);
try testing.expectEqual(ScreenPoint{ .x = 4, .y = 4 }, it.next().?);
try testing.expect(it.next() == null);
}
// Forward not last line
{
var pt: ScreenPoint = .{ .x = 3, .y = 3 };
var it = pt.iterator(&s, .right_down);
try testing.expectEqual(ScreenPoint{ .x = 3, .y = 3 }, it.next().?);
try testing.expectEqual(ScreenPoint{ .x = 4, .y = 3 }, it.next().?);
try testing.expectEqual(ScreenPoint{ .x = 0, .y = 4 }, it.next().?);
}
}
};
/// Direction that points can go.
pub const Direction = enum { left_up, right_down };
pub const Coordinate = struct {
x: usize = 0,
y: usize = 0,
test {
std.testing.refAllDecls(@This());
}
pub fn eql(self: Coordinate, other: Coordinate) bool {
return self.x == other.x and self.y == other.y;
}
};

1
src/terminal/res/glitch.txt vendored Normal file
View File

@ -0,0 +1 @@
Ḡ̴͎͍̜͎͔͕̩̗͕͖̟̜͑̊̌̇̑͒͋͑̄̈́͐̈́́̽͌͂̎̀̔͋̓́̅̌̇͘̕͜͝͝h̶̡̞̫͉̳̬̜̱̥͕͑̾͛̒̆̒̉̒̑͂̄͘ͅǫ̷̨̥͔͔͖̭͚͙̯̟̭̘͇̫̰͚̺̳̙̳̟͚̫̱̹̱͒̂̑͒͜͠ͅş̴̖̰̜̱̹͙̅͒̀̏͆̐̋͂̓͋̃̈̔̂̈͛̐̿̔́̔̄͑̇͑̋̈́͌͋̾̃̽̈́̕͘̚͘͘͘͠͠t̵̢̜̱̦͇͉̬̮̼͖̳̗̥̝̬͇͕̥̜͕̳̱̥̮͉̮̩̘̰̪̤͉͎̲͈͍̳̟̠͈̝̫͋̊̀͐̍̅̀̄̃̈́̔̇̈́̄̃̽̂̌̅̄̋͒̃̈́̍̀̍̇̽̐͊̾̆̅̈̿̓͒̄̾͌̚͝͝͝͝͝t̴̥̼̳̗̬̬͔͎̯͉͇̮̰͖͇̝͔̳̳̗̰͇͎͉̬͇̝̺̯͎͖͔̍͆͒̊̒̔̊̈́̿̊̅͂̐͋̿͂̈̒̄͜͠͠ÿ̴̢̗̜̥͇͖̰͎̝̹̗̪̙̞̣̳͎̯̹͚̲̝̗̳̳̗̖͎̗̬͈͙̝̟͍̥̤͖͇̰͈̺͛̒̂͌̌̏̈̾̓̈́̿͐̂̓̔̓̂̈́͑͛͊͋̔̿̊͑͌̊̏͘͘̕͘͠͝

View File

@ -1,5 +0,0 @@
pub usingnamespace @import("simdvt/parser.zig");
test {
@import("std").testing.refAllDecls(@This());
}

199
src/terminal/size.zig Normal file
View File

@ -0,0 +1,199 @@
const std = @import("std");
const assert = std.debug.assert;
/// The maximum size of a page in bytes. We use a u16 here because any
/// smaller bit size by Zig is upgraded anyways to a u16 on mainstream
/// CPU architectures, and because 65KB is a reasonable page size. To
/// support better configurability, we derive everything from this.
pub const max_page_size = std.math.maxInt(u32);
/// The int type that can contain the maximum memory offset in bytes,
/// derived from the maximum terminal page size.
pub const OffsetInt = std.math.IntFittingRange(0, max_page_size - 1);
/// The int type that can contain the maximum number of cells in a page.
pub const CellCountInt = u16; // TODO: derive
//
/// The offset from the base address of the page to the start of some data.
/// This is typed for ease of use.
///
/// This is a packed struct so we can attach methods to an int.
pub fn Offset(comptime T: type) type {
return packed struct(OffsetInt) {
const Self = @This();
offset: OffsetInt = 0,
/// A slice of type T that stores via a base offset and len.
pub const Slice = struct {
offset: Self,
len: usize,
};
/// Returns a pointer to the start of the data, properly typed.
pub fn ptr(self: Self, base: anytype) [*]T {
// The offset must be properly aligned for the type since
// our return type is naturally aligned. We COULD modify this
// to return arbitrary alignment, but its not something we need.
const addr = intFromBase(base) + self.offset;
assert(addr % @alignOf(T) == 0);
return @ptrFromInt(addr);
}
};
}
/// Represents a buffer that is offset from some base pointer.
/// Offset-based structures should use this as their initialization
/// parameter so that they can know what segment of memory they own
/// while at the same time initializing their offset fields to be
/// against the true base.
///
/// The term "true base" is used to describe the base address of
/// the allocation, which i.e. can include memory that you do NOT
/// own and is used by some other structures. All offsets are against
/// this "true base" so that to determine addresses structures don't
/// need to add up all the intermediary offsets.
pub const OffsetBuf = struct {
/// The true base pointer to the backing memory. This is
/// "byte zero" of the allocation. This plus the offset make
/// it easy to pass in the base pointer in all usage to this
/// structure and the offsets are correct.
base: [*]u8,
/// Offset from base where the beginning of /this/ data
/// structure is located. We use this so that we can slowly
/// build up a chain of offset-based structures but always
/// have the base pointer sent into functions be the true base.
offset: usize = 0,
/// Initialize a zero-offset buffer from a base.
pub fn init(base: anytype) OffsetBuf {
return initOffset(base, 0);
}
/// Initialize from some base pointer and offset.
pub fn initOffset(base: anytype, offset: usize) OffsetBuf {
return .{
.base = @ptrFromInt(intFromBase(base)),
.offset = offset,
};
}
/// The base address for the start of the data for the user
/// of this OffsetBuf. This is where your data structure should
/// begin; anything before this is NOT your memory.
pub fn start(self: OffsetBuf) [*]u8 {
const ptr = self.base + self.offset;
return @ptrCast(ptr);
}
/// Returns an Offset calculation for some child member of
/// your struct. The offset is against the true base pointer
/// so that future callers can pass that in as the base.
pub fn member(
self: OffsetBuf,
comptime T: type,
len: usize,
) Offset(T) {
return .{ .offset = @intCast(self.offset + len) };
}
/// Add an offset to the current offset.
pub fn add(self: OffsetBuf, offset: usize) OffsetBuf {
return .{
.base = self.base,
.offset = self.offset + offset,
};
}
/// Rebase the offset to have a zero offset by rebasing onto start.
/// This is similar to `add` but all of the offsets are merged into base.
pub fn rebase(self: OffsetBuf, offset: usize) OffsetBuf {
return .{
.base = self.start() + offset,
.offset = 0,
};
}
};
/// Get the offset for a given type from some base pointer to the
/// actual pointer to the type.
pub fn getOffset(
comptime T: type,
base: anytype,
ptr: *const T,
) Offset(T) {
const base_int = intFromBase(base);
const ptr_int = @intFromPtr(ptr);
const offset = ptr_int - base_int;
return .{ .offset = @intCast(offset) };
}
fn intFromBase(base: anytype) usize {
const T = @TypeOf(base);
return switch (@typeInfo(T)) {
.Pointer => |v| switch (v.size) {
.One,
.Many,
.C,
=> @intFromPtr(base),
.Slice => @intFromPtr(base.ptr),
},
else => switch (T) {
OffsetBuf => @intFromPtr(base.base),
else => @compileError("invalid base type"),
},
};
}
test "Offset" {
// This test is here so that if Offset changes, we can be very aware
// of this effect and think about the implications of it.
const testing = std.testing;
try testing.expect(OffsetInt == u32);
}
test "Offset ptr u8" {
const testing = std.testing;
const offset: Offset(u8) = .{ .offset = 42 };
const base_int: usize = @intFromPtr(&offset);
const actual = offset.ptr(&offset);
try testing.expectEqual(@as(usize, base_int + 42), @intFromPtr(actual));
}
test "Offset ptr structural" {
const Struct = struct { x: u32, y: u32 };
const testing = std.testing;
const offset: Offset(Struct) = .{ .offset = @alignOf(Struct) * 4 };
const base_int: usize = std.mem.alignForward(usize, @intFromPtr(&offset), @alignOf(Struct));
const base: [*]u8 = @ptrFromInt(base_int);
const actual = offset.ptr(base);
try testing.expectEqual(@as(usize, base_int + offset.offset), @intFromPtr(actual));
}
test "getOffset bytes" {
const testing = std.testing;
var widgets: []const u8 = "ABCD";
const offset = getOffset(u8, widgets.ptr, &widgets[2]);
try testing.expectEqual(@as(OffsetInt, 2), offset.offset);
}
test "getOffset structs" {
const testing = std.testing;
const Widget = struct { x: u32, y: u32 };
const widgets: []const Widget = &.{
.{ .x = 1, .y = 2 },
.{ .x = 3, .y = 4 },
.{ .x = 5, .y = 6 },
.{ .x = 7, .y = 8 },
.{ .x = 9, .y = 10 },
};
const offset = getOffset(Widget, widgets.ptr, &widgets[2]);
try testing.expectEqual(
@as(OffsetInt, @sizeOf(Widget) * 2),
offset.offset,
);
}

332
src/terminal/style.zig Normal file
View File

@ -0,0 +1,332 @@
const std = @import("std");
const assert = std.debug.assert;
const color = @import("color.zig");
const sgr = @import("sgr.zig");
const page = @import("page.zig");
const size = @import("size.zig");
const Offset = size.Offset;
const OffsetBuf = size.OffsetBuf;
const hash_map = @import("hash_map.zig");
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
/// The unique identifier for a style. This is at most the number of cells
/// that can fit into a terminal page.
pub const Id = size.CellCountInt;
/// The Id to use for default styling.
pub const default_id: Id = 0;
/// The style attributes for a cell.
pub const Style = struct {
/// Various colors, all self-explanatory.
fg_color: Color = .none,
bg_color: Color = .none,
underline_color: Color = .none,
/// On/off attributes that don't require much bit width so we use
/// a packed struct to make this take up significantly less space.
flags: packed struct {
bold: bool = false,
italic: bool = false,
faint: bool = false,
blink: bool = false,
inverse: bool = false,
invisible: bool = false,
strikethrough: bool = false,
underline: sgr.Attribute.Underline = .none,
} = .{},
/// The color for an SGR attribute. A color can come from multiple
/// sources so we use this to track the source plus color value so that
/// we can properly react to things like palette changes.
pub const Color = union(enum) {
none: void,
palette: u8,
rgb: color.RGB,
};
/// True if the style is the default style.
pub fn default(self: Style) bool {
return std.meta.eql(self, .{});
}
/// True if the style is equal to another style.
pub fn eql(self: Style, other: Style) bool {
return std.meta.eql(self, other);
}
/// Returns the bg color for a cell with this style given the cell
/// that has this style and the palette to use.
///
/// Note that generally if a cell is a color-only cell, it SHOULD
/// only have the default style, but this is meant to work with the
/// default style as well.
pub fn bg(
self: Style,
cell: *const page.Cell,
palette: *const color.Palette,
) ?color.RGB {
return switch (cell.content_tag) {
.bg_color_palette => palette[cell.content.color_palette],
.bg_color_rgb => rgb: {
const rgb = cell.content.color_rgb;
break :rgb .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
},
else => switch (self.bg_color) {
.none => null,
.palette => |idx| palette[idx],
.rgb => |rgb| rgb,
},
};
}
/// Returns the fg color for a cell with this style given the palette.
pub fn fg(
self: Style,
palette: *const color.Palette,
) ?color.RGB {
return switch (self.fg_color) {
.none => null,
.palette => |idx| palette[idx],
.rgb => |rgb| rgb,
};
}
/// Returns the underline color for this style.
pub fn underlineColor(
self: Style,
palette: *const color.Palette,
) ?color.RGB {
return switch (self.underline_color) {
.none => null,
.palette => |idx| palette[idx],
.rgb => |rgb| rgb,
};
}
/// Returns a bg-color only cell from this style, if it exists.
pub fn bgCell(self: Style) ?page.Cell {
return switch (self.bg_color) {
.none => null,
.palette => |idx| .{
.content_tag = .bg_color_palette,
.content = .{ .color_palette = idx },
},
.rgb => |rgb| .{
.content_tag = .bg_color_rgb,
.content = .{ .color_rgb = .{
.r = rgb.r,
.g = rgb.g,
.b = rgb.b,
} },
},
};
}
test {
// The size of the struct so we can be aware of changes.
const testing = std.testing;
try testing.expectEqual(@as(usize, 14), @sizeOf(Style));
}
};
/// A set of styles.
///
/// This set is created with some capacity in mind. You can determine
/// the exact memory requirement for a capacity by calling `layout`
/// and checking the total size.
///
/// When the set exceeds capacity, `error.OutOfMemory` is returned
/// from memory-using methods. The caller is responsible for determining
/// a path forward.
///
/// The general idea behind this structure is that it is optimized for
/// the scenario common in terminals where there aren't many unique
/// styles, and many cells are usually drawn with a single style before
/// changing styles.
///
/// Callers should call `upsert` when a new style is set. This will
/// return a stable pointer to metadata. You should use this metadata
/// to keep a ref count of the style usage. When it falls to zero you
/// can remove it.
pub const Set = struct {
pub const base_align = @max(MetadataMap.base_align, IdMap.base_align);
/// The mapping of a style to associated metadata. This is
/// the map that contains the actual style definitions
/// (in the form of the key).
styles: MetadataMap,
/// The mapping from ID to style.
id_map: IdMap,
/// The next ID to use for a style that isn't in the set.
/// When this overflows we'll begin returning an IdOverflow
/// error and the caller must manually compact the style
/// set.
///
/// Id zero is reserved and always is the default style. The
/// default style isn't present in the map, its dependent on
/// the terminal configuration.
next_id: Id = 1,
/// Maps a style definition to metadata about that style.
const MetadataMap = AutoOffsetHashMap(Style, Metadata);
/// Maps the unique style ID to the concrete style definition.
const IdMap = AutoOffsetHashMap(Id, Offset(Style));
/// Returns the memory layout for the given base offset and
/// desired capacity. The layout can be used by the caller to
/// determine how much memory to allocate, and the layout must
/// be used to initialize the set so that the set knows all
/// the offsets for the various buffers.
pub fn layout(cap: usize) Layout {
const md_layout = MetadataMap.layout(@intCast(cap));
const md_start = 0;
const md_end = md_start + md_layout.total_size;
const id_layout = IdMap.layout(@intCast(cap));
const id_start = std.mem.alignForward(usize, md_end, IdMap.base_align);
const id_end = id_start + id_layout.total_size;
const total_size = id_end;
return .{
.md_start = md_start,
.md_layout = md_layout,
.id_start = id_start,
.id_layout = id_layout,
.total_size = total_size,
};
}
pub const Layout = struct {
md_start: usize,
md_layout: MetadataMap.Layout,
id_start: usize,
id_layout: IdMap.Layout,
total_size: usize,
};
pub fn init(base: OffsetBuf, l: Layout) Set {
const styles_buf = base.add(l.md_start);
const id_buf = base.add(l.id_start);
return .{
.styles = MetadataMap.init(styles_buf, l.md_layout),
.id_map = IdMap.init(id_buf, l.id_layout),
};
}
/// Possible errors for upsert.
pub const UpsertError = error{
/// No more space in the backing buffer. Remove styles or
/// grow and reinitialize.
OutOfMemory,
/// No more available IDs. Perform a garbage collection
/// operation to compact ID space.
Overflow,
};
/// Upsert a style into the set and return a pointer to the metadata
/// for that style. The pointer is valid for the lifetime of the set
/// so long as the style is not removed.
///
/// The ref count for new styles is initialized to zero and
/// for existing styles remains unmodified.
pub fn upsert(self: *Set, base: anytype, style: Style) UpsertError!*Metadata {
// If we already have the style in the map, this is fast.
var map = self.styles.map(base);
const gop = try map.getOrPut(style);
if (gop.found_existing) return gop.value_ptr;
// New style, we need to setup all the metadata. First thing,
// let's get the ID we'll assign, because if we're out of space
// we need to fail early.
errdefer map.removeByPtr(gop.key_ptr);
const id = self.next_id;
self.next_id = try std.math.add(Id, self.next_id, 1);
errdefer self.next_id -= 1;
gop.value_ptr.* = .{ .id = id };
// Setup our ID mapping
var id_map = self.id_map.map(base);
const id_gop = try id_map.getOrPut(id);
errdefer id_map.removeByPtr(id_gop.key_ptr);
assert(!id_gop.found_existing);
id_gop.value_ptr.* = size.getOffset(Style, base, gop.key_ptr);
return gop.value_ptr;
}
/// Lookup a style by its unique identifier.
pub fn lookupId(self: *const Set, base: anytype, id: Id) ?*Style {
const id_map = self.id_map.map(base);
const offset = id_map.get(id) orelse return null;
return @ptrCast(offset.ptr(base));
}
/// Remove a style by its id.
pub fn remove(self: *Set, base: anytype, id: Id) void {
// Lookup by ID, if it doesn't exist then we return. We use
// getEntry so that we can make removal faster later by using
// the entry's key pointer.
var id_map = self.id_map.map(base);
const id_entry = id_map.getEntry(id) orelse return;
var style_map = self.styles.map(base);
const style_ptr: *Style = @ptrCast(id_entry.value_ptr.ptr(base));
id_map.removeByPtr(id_entry.key_ptr);
style_map.removeByPtr(style_ptr);
}
/// Return the number of styles currently in the set.
pub fn count(self: *const Set, base: anytype) usize {
return self.id_map.map(base).count();
}
};
/// Metadata about a style. This is used to track the reference count
/// and the unique identifier for a style. The unique identifier is used
/// to track the style in the full style map.
pub const Metadata = struct {
ref: size.CellCountInt = 0,
id: Id = 0,
};
test "Set basic usage" {
const testing = std.testing;
const alloc = testing.allocator;
const layout = Set.layout(16);
const buf = try alloc.alignedAlloc(u8, Set.base_align, layout.total_size);
defer alloc.free(buf);
const style: Style = .{ .flags = .{ .bold = true } };
var set = Set.init(OffsetBuf.init(buf), layout);
// Upsert
const meta = try set.upsert(buf, style);
try testing.expect(meta.id > 0);
// Second upsert should return the same metadata.
{
const meta2 = try set.upsert(buf, style);
try testing.expectEqual(meta.id, meta2.id);
}
// Look it up
{
const v = set.lookupId(buf, meta.id).?;
try testing.expect(v.flags.bold);
const v2 = set.lookupId(buf, meta.id).?;
try testing.expectEqual(v, v2);
}
// Removal
set.remove(buf, meta.id);
try testing.expect(set.lookupId(buf, meta.id) == null);
}

View File

@ -1,32 +0,0 @@
// This is the C-ABI API for the terminal package. This isn't used
// by other Zig programs but by C or WASM interfacing.
//
// NOTE: This is far, far from complete. We did a very minimal amount to
// prove that compilation works, but we haven't completed coverage yet.
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const Terminal = @import("main.zig").Terminal;
const wasm = @import("../os/wasm.zig");
const alloc = wasm.alloc;
export fn terminal_new(cols: usize, rows: usize) ?*Terminal {
const term = Terminal.init(alloc, cols, rows) catch return null;
const result = alloc.create(Terminal) catch return null;
result.* = term;
return result;
}
export fn terminal_free(ptr: ?*Terminal) void {
if (ptr) |v| {
v.deinit(alloc);
alloc.destroy(v);
}
}
export fn terminal_print(ptr: ?*Terminal, char: u32) void {
if (ptr) |t| {
t.print(@intCast(char)) catch return;
}
}

View File

@ -78,7 +78,7 @@ pub const DerivedConfig = struct {
palette: terminal.color.Palette,
image_storage_limit: usize,
cursor_style: terminal.Cursor.Style,
cursor_style: terminal.CursorStyle,
cursor_blink: ?bool,
cursor_color: ?configpkg.Config.Color,
foreground: configpkg.Config.Color,
@ -128,11 +128,11 @@ pub const DerivedConfig = struct {
/// process.
pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
// Create our terminal
var term = try terminal.Terminal.init(
alloc,
opts.grid_size.columns,
opts.grid_size.rows,
);
var term = try terminal.Terminal.init(alloc, .{
.cols = opts.grid_size.columns,
.rows = opts.grid_size.rows,
.max_scrollback = opts.full_config.@"scrollback-limit",
});
errdefer term.deinit(alloc);
term.default_palette = opts.config.palette;
term.color_palette.colors = opts.config.palette;
@ -145,8 +145,16 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
}
// Set the image size limits
try term.screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit);
try term.secondary_screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit);
try term.screen.kitty_images.setLimit(
alloc,
&term.screen,
opts.config.image_storage_limit,
);
try term.secondary_screen.kitty_images.setLimit(
alloc,
&term.secondary_screen,
opts.config.image_storage_limit,
);
// Set default cursor blink settings
term.modes.set(
@ -155,7 +163,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
);
// Set our default cursor style
term.screen.cursor.style = opts.config.cursor_style;
term.screen.cursor.cursor_style = opts.config.cursor_style;
var subprocess = try Subprocess.init(alloc, opts);
errdefer subprocess.deinit();
@ -395,10 +403,12 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void {
// Set the image size limits
try self.terminal.screen.kitty_images.setLimit(
self.alloc,
&self.terminal.screen,
config.image_storage_limit,
);
try self.terminal.secondary_screen.kitty_images.setLimit(
self.alloc,
&self.terminal.secondary_screen,
config.image_storage_limit,
);
}
@ -464,11 +474,17 @@ pub fn clearScreen(self: *Exec, history: bool) !void {
if (self.terminal.active_screen == .alternate) return;
// Clear our scrollback
if (history) self.terminal.eraseDisplay(self.alloc, .scrollback, false);
if (history) self.terminal.eraseDisplay(.scrollback, false);
// If we're not at a prompt, we just delete above the cursor.
if (!self.terminal.cursorIsAtPrompt()) {
try self.terminal.screen.clear(.above_cursor);
if (self.terminal.screen.cursor.y > 0) {
self.terminal.screen.eraseRows(
.{ .active = .{ .y = 0 } },
.{ .active = .{ .y = self.terminal.screen.cursor.y - 1 } },
);
}
return;
}
@ -478,7 +494,7 @@ pub fn clearScreen(self: *Exec, history: bool) !void {
// clear the full screen in the next eraseDisplay call.
self.terminal.markSemanticPrompt(.command);
assert(!self.terminal.cursorIsAtPrompt());
self.terminal.eraseDisplay(self.alloc, .complete, false);
self.terminal.eraseDisplay(.complete, false);
}
// If we reached here it means we're at a prompt, so we send a form-feed.
@ -494,17 +510,13 @@ pub fn scrollViewport(self: *Exec, scroll: terminal.Terminal.ScrollViewport) !vo
/// Jump the viewport to the prompt.
pub fn jumpToPrompt(self: *Exec, delta: isize) !void {
const wakeup: bool = wakeup: {
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
break :wakeup self.terminal.screen.jump(.{
.prompt_delta = delta,
});
};
if (wakeup) {
try self.renderer_wakeup.notify();
self.terminal.screen.scroll(.{ .delta_prompt = delta });
}
try self.renderer_wakeup.notify();
}
/// Called when the child process exited abnormally but before
@ -1666,7 +1678,7 @@ const StreamHandler = struct {
/// The default cursor state. This is used with CSI q. This is
/// set to true when we're currently in the default cursor state.
default_cursor: bool = true,
default_cursor_style: terminal.Cursor.Style,
default_cursor_style: terminal.CursorStyle,
default_cursor_blink: ?bool,
default_cursor_color: ?terminal.color.RGB,
@ -1843,7 +1855,7 @@ const StreamHandler = struct {
.decscusr => {
const blink = self.terminal.modes.get(.cursor_blinking);
const style: u8 = switch (self.terminal.screen.cursor.style) {
const style: u8 = switch (self.terminal.screen.cursor.cursor_style) {
.block => if (blink) 1 else 2,
.underline => if (blink) 3 else 4,
.bar => if (blink) 5 else 6,
@ -2007,7 +2019,7 @@ const StreamHandler = struct {
try self.queueRender();
}
self.terminal.eraseDisplay(self.alloc, mode, protected);
self.terminal.eraseDisplay(mode, protected);
}
pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void {
@ -2015,7 +2027,7 @@ const StreamHandler = struct {
}
pub fn deleteChars(self: *StreamHandler, count: usize) !void {
try self.terminal.deleteChars(count);
self.terminal.deleteChars(count);
}
pub fn eraseChars(self: *StreamHandler, count: usize) !void {
@ -2023,7 +2035,7 @@ const StreamHandler = struct {
}
pub fn insertLines(self: *StreamHandler, count: usize) !void {
try self.terminal.insertLines(count);
self.terminal.insertLines(count);
}
pub fn insertBlanks(self: *StreamHandler, count: usize) !void {
@ -2031,11 +2043,11 @@ const StreamHandler = struct {
}
pub fn deleteLines(self: *StreamHandler, count: usize) !void {
try self.terminal.deleteLines(count);
self.terminal.deleteLines(count);
}
pub fn reverseIndex(self: *StreamHandler) !void {
try self.terminal.reverseIndex();
self.terminal.reverseIndex();
}
pub fn index(self: *StreamHandler) !void {
@ -2183,9 +2195,9 @@ const StreamHandler = struct {
};
if (enabled)
self.terminal.alternateScreen(self.alloc, opts)
self.terminal.alternateScreen(opts)
else
self.terminal.primaryScreen(self.alloc, opts);
self.terminal.primaryScreen(opts);
// Schedule a render since we changed screens
try self.queueRender();
@ -2198,9 +2210,9 @@ const StreamHandler = struct {
};
if (enabled)
self.terminal.alternateScreen(self.alloc, opts)
self.terminal.alternateScreen(opts)
else
self.terminal.primaryScreen(self.alloc, opts);
self.terminal.primaryScreen(opts);
// Schedule a render since we changed screens
try self.queueRender();
@ -2358,7 +2370,7 @@ const StreamHandler = struct {
switch (style) {
.default => {
self.default_cursor = true;
self.terminal.screen.cursor.style = self.default_cursor_style;
self.terminal.screen.cursor.cursor_style = self.default_cursor_style;
self.terminal.modes.set(
.cursor_blinking,
self.default_cursor_blink orelse true,
@ -2366,32 +2378,32 @@ const StreamHandler = struct {
},
.blinking_block => {
self.terminal.screen.cursor.style = .block;
self.terminal.screen.cursor.cursor_style = .block;
self.terminal.modes.set(.cursor_blinking, true);
},
.steady_block => {
self.terminal.screen.cursor.style = .block;
self.terminal.screen.cursor.cursor_style = .block;
self.terminal.modes.set(.cursor_blinking, false);
},
.blinking_underline => {
self.terminal.screen.cursor.style = .underline;
self.terminal.screen.cursor.cursor_style = .underline;
self.terminal.modes.set(.cursor_blinking, true);
},
.steady_underline => {
self.terminal.screen.cursor.style = .underline;
self.terminal.screen.cursor.cursor_style = .underline;
self.terminal.modes.set(.cursor_blinking, false);
},
.blinking_bar => {
self.terminal.screen.cursor.style = .bar;
self.terminal.screen.cursor.cursor_style = .bar;
self.terminal.modes.set(.cursor_blinking, true);
},
.steady_bar => {
self.terminal.screen.cursor.style = .bar;
self.terminal.screen.cursor.cursor_style = .bar;
self.terminal.modes.set(.cursor_blinking, false);
},
@ -2424,7 +2436,7 @@ const StreamHandler = struct {
}
pub fn restoreCursor(self: *StreamHandler) !void {
self.terminal.restoreCursor();
try self.terminal.restoreCursor();
}
pub fn enquiry(self: *StreamHandler) !void {
@ -2433,11 +2445,11 @@ const StreamHandler = struct {
}
pub fn scrollDown(self: *StreamHandler, count: usize) !void {
try self.terminal.scrollDown(count);
self.terminal.scrollDown(count);
}
pub fn scrollUp(self: *StreamHandler, count: usize) !void {
try self.terminal.scrollUp(count);
self.terminal.scrollUp(count);
}
pub fn setActiveStatusDisplay(
@ -2467,7 +2479,7 @@ const StreamHandler = struct {
pub fn fullReset(
self: *StreamHandler,
) !void {
self.terminal.fullReset(self.alloc);
self.terminal.fullReset();
try self.setMouseShape(.text);
}

View File

@ -177,7 +177,7 @@ pub fn threadMain(self: *Thread) void {
\\Please free up some pty devices and try again.
;
t.eraseDisplay(alloc, .complete, false);
t.eraseDisplay(.complete, false);
t.printString(str) catch {};
},
@ -197,7 +197,7 @@ pub fn threadMain(self: *Thread) void {
\\Out of memory. This terminal is non-functional. Please close it and try again.
;
t.eraseDisplay(alloc, .complete, false);
t.eraseDisplay(.complete, false);
t.printString(str) catch {};
},
}