mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 02:36:22 +03:00
Merge pull request #1584 from mitchellh/paged-terminal
Low-memory terminal state implementation
This commit is contained in:
151
.github/workflows/release-pr.yml
vendored
Normal file
151
.github/workflows/release-pr.yml
vendored
Normal 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: ./
|
5
.github/workflows/release-tip.yml
vendored
5
.github/workflows/release-tip.yml
vendored
@ -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
|
||||
|
7
TODO.md
7
TODO.md
@ -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
|
||||
|
14
flake.nix
14
flake.nix
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
456
src/Surface.zig
456
src/Surface.zig
@ -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,42 +1360,46 @@ 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) {
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
.up => .up,
|
||||
.down => .down,
|
||||
.page_up => .page_up,
|
||||
.page_down => .page_down,
|
||||
.home => .home,
|
||||
.end => .end,
|
||||
else => break :adjust_selection,
|
||||
});
|
||||
};
|
||||
var screen = &self.io.terminal.screen;
|
||||
const sel = if (screen.selection) |*sel| sel else break :adjust_selection;
|
||||
|
||||
// Silently consume key releases.
|
||||
// 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,
|
||||
.down => .down,
|
||||
.page_up => .page_up,
|
||||
.page_down => .page_down,
|
||||
.home => .home,
|
||||
.end => .end,
|
||||
else => break :adjust_selection,
|
||||
});
|
||||
|
||||
// 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 pt_viewport = self.posToViewport(pos.x, pos.y);
|
||||
const pt_screen = pt_viewport.toScreen(&self.io.terminal.screen);
|
||||
const pin = pin: {
|
||||
const pt_viewport = self.posToViewport(pos.x, pos.y);
|
||||
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
16
src/bench/page-init.sh
Executable 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
78
src/bench/page-init.zig
Normal 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();
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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,11 +165,11 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
i += try std.unicode.utf8Encode(cp, buf[i..]);
|
||||
}
|
||||
|
||||
|
@ -150,4 +150,5 @@ pub const ExeEntrypoint = enum {
|
||||
bench_stream,
|
||||
bench_codepoint_width,
|
||||
bench_grapheme_break,
|
||||
bench_page_init,
|
||||
};
|
||||
|
@ -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(
|
||||
u32,
|
||||
value orelse return error.ValueRequired,
|
||||
0,
|
||||
) catch return error.InvalidValue,
|
||||
|
||||
u64 => std.fmt.parseInt(
|
||||
u64,
|
||||
inline u8,
|
||||
u16,
|
||||
u32,
|
||||
u64,
|
||||
usize,
|
||||
i8,
|
||||
i16,
|
||||
i32,
|
||||
i64,
|
||||
isize,
|
||||
=> |Int| std.fmt.parseInt(
|
||||
Int,
|
||||
value orelse return error.ValueRequired,
|
||||
0,
|
||||
) catch return error.InvalidValue,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
);
|
||||
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)");
|
||||
selected.cell.renderTable(
|
||||
self.surface.renderer_state.terminal,
|
||||
selected.col,
|
||||
selected.row,
|
||||
);
|
||||
}
|
||||
|
||||
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
193
src/inspector/cell.zig
Normal 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)");
|
||||
}
|
||||
};
|
@ -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,
|
||||
@ -108,11 +120,11 @@ pub fn renderInTable(
|
||||
|
||||
// Boolean styles
|
||||
const styles = .{
|
||||
"bold", "italic", "faint", "blink",
|
||||
"inverse", "invisible", "protected", "strikethrough",
|
||||
"bold", "italic", "faint", "blink",
|
||||
"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);
|
||||
{
|
||||
|
@ -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
169
src/inspector/page.zig
Normal 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
|
||||
}
|
@ -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"),
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
break :sel null;
|
||||
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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
break :sel null;
|
||||
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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 = .{
|
||||
|
@ -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,14 +80,25 @@ 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| {
|
||||
log.warn(
|
||||
"failed to build string map for link checking err={}",
|
||||
.{err},
|
||||
);
|
||||
continue;
|
||||
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);
|
||||
|
||||
@ -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,
|
||||
} }).?));
|
||||
}
|
||||
|
@ -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
7204
src/terminal/PageList.zig
Normal file
File diff suppressed because it is too large
Load Diff
11845
src/terminal/Screen.zig
11845
src/terminal/Screen.zig
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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
474
src/terminal/bitmap_allocator.zig
Normal file
474
src/terminal/bitmap_allocator.zig
Normal 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
1513
src/terminal/hash_map.zig
Normal file
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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{
|
||||
.x = t.screen.cursor.x,
|
||||
.y = t.screen.cursor.y,
|
||||
}).toScreen(&t.screen);
|
||||
self.deleteIntersecting(alloc, t, target, delete_images, {}, null);
|
||||
self.deleteIntersecting(
|
||||
alloc,
|
||||
t,
|
||||
.{ .active = .{
|
||||
.x = t.screen.cursor.x,
|
||||
.y = t.screen.cursor.y,
|
||||
} },
|
||||
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 {
|
||||
fn filter(ctx: i32, p: Placement) bool {
|
||||
return p.z == ctx;
|
||||
}
|
||||
}.filter);
|
||||
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,
|
||||
);
|
||||
},
|
||||
|
||||
.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(.{
|
||||
|
@ -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
2090
src/terminal/page.zig
Normal file
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
||||
break :left_up .{
|
||||
.x = current.x - 1,
|
||||
.y = current.y,
|
||||
};
|
||||
},
|
||||
/// 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,
|
||||
};
|
||||
|
||||
.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,
|
||||
};
|
||||
}
|
||||
/// 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,
|
||||
|
||||
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().?);
|
||||
}
|
||||
pub fn coord(self: Point) Coordinate {
|
||||
return switch (self) {
|
||||
.active,
|
||||
.viewport,
|
||||
.screen,
|
||||
.history,
|
||||
=> |v| v,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// 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
1
src/terminal/res/glitch.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
Ḡ̴͎͍̜͎͔͕̩̗͕͖̟̜͑̊̌̇̑͒͋͑̄̈́͐̈́́̽͌͂̎̀̔͋̓́̅̌̇͘̕͜͝͝h̶̡̞̫͉̳̬̜̱̥͕͑̾͛̒̆̒̉̒̑͂̄͘ͅǫ̷̨̥͔͔͖̭͚͙̯̟̭̘͇̫̰͚̺̳̙̳̟͚̫̱̹̱͒̂̑͒͜͠ͅş̴̖̰̜̱̹͙̅͒̀̏͆̐̋͂̓͋̃̈̔̂̈͛̐̿̔́̔̄͑̇͑̋̈́͌͋̾̃̽̈́̕͘̚͘͘͘͠͠t̵̢̜̱̦͇͉̬̮̼͖̳̗̥̝̬͇͕̥̜͕̳̱̥̮͉̮̩̘̰̪̤͉͎̲͈͍̳̟̠͈̝̫͋̊̀͐̍̅̀̄̃̈́̔̇̈́̄̃̽̂̌̅̄̋͒̃̈́̍̀̍̇̽̐͊̾̆̅̈̿̓͒̄̾͌̚͝͝͝͝͝t̴̥̼̳̗̬̬͔͎̯͉͇̮̰͖͇̝͔̳̳̗̰͇͎͉̬͇̝̺̯͎͖͔̍͆͒̊̒̔̊̈́̿̊̅͂̐͋̿͂̈̒̄͜͠͠ÿ̴̢̗̜̥͇͖̰͎̝̹̗̪̙̞̣̳͎̯̹͚̲̝̗̳̳̗̖͎̗̬͈͙̝̟͍̥̤͖͇̰͈̺͛̒̂͌̌̏̈̾̓̈́̿͐̂̓̔̓̂̈́͑͛͊͋̔̿̊͑͌̊̏͘͘̕͘͠͝
|
@ -1,5 +0,0 @@
|
||||
pub usingnamespace @import("simdvt/parser.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
199
src/terminal/size.zig
Normal file
199
src/terminal/size.zig
Normal 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
332
src/terminal/style.zig
Normal 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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 {};
|
||||
},
|
||||
}
|
||||
|
Reference in New Issue
Block a user