Merge branch 'main' into hu_HU_localization

Resolve conflicts in src/os/i18n.zig.
This commit is contained in:
Balázs Szücs
2025-06-30 19:34:24 +02:00
108 changed files with 5841 additions and 3355 deletions

View File

@ -81,6 +81,10 @@
# - @ghostty-org/localization/* - Anything related to localization
# for a specific locale.
#
# - @ghosty-org/localization/manager - Manage all localization tasks
# and tooling. They are not responsible for any specific locale but
# are responsible for the overall localization process and tooling.
#
# - @ghostty-org/macos - The Ghostty macOS app and any macOS-specific
# features, configurations, etc.
#
@ -161,6 +165,7 @@
/po/ca_ES.UTF-8.po @ghostty-org/ca_ES
/po/de_DE.UTF-8.po @ghostty-org/de_DE
/po/es_BO.UTF-8.po @ghostty-org/es_BO
/po/es_AR.UTF-8.po @ghostty-org/es_AR
/po/fr_FR.UTF-8.po @ghostty-org/fr_FR
/po/ga_IE.UTF-8.po @ghostty-org/ga_IE
/po/hu_HU.UTF-8.po @ghostty-org/hu_HU
@ -175,6 +180,7 @@
/po/tr_TR.UTF-8.po @ghostty-org/tr_TR
/po/uk_UA.UTF-8.po @ghostty-org/uk_UA
/po/zh_CN.UTF-8.po @ghostty-org/zh_CN
/po/ga_IE.UTF-8.po @ghostty-org/ga_IE
# Packaging - Snap
/snap/ @ghostty-org/snap

21
TODO.md
View File

@ -1,21 +0,0 @@
Performance:
- Loading fonts on startups should probably happen in multiple threads
Correctness:
- test wrap against wraptest: https://github.com/mattiase/wraptest
- automate this in some way
- Charsets: UTF-8 vs. ASCII mode
- we only support UTF-8 input right now
- need fallback glyphs if they're not supported
- can effect a crash using `vttest` menu `3 10` since it tries to parse
ASCII as UTF-8.
Mac:
- Preferences window
Major Features:
- Bell

View File

@ -8,8 +8,8 @@
.libxev = .{
// mitchellh/libxev
.url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
.hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
.url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
.hash = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw",
.lazy = true,
},
.vaxis = .{
@ -103,8 +103,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
.hash = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
.hash = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
.lazy = true,
},
},

12
build.zig.zon.json generated
View File

@ -54,20 +54,20 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX": {
"N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
"hash": "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
"hash": "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4="
},
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
"name": "libpng",
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
},
"libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": {
"libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw": {
"name": "libxev",
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
"hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="
"url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
"hash": "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4="
},
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
"name": "libxml2",

12
build.zig.zon.nix generated
View File

@ -170,11 +170,11 @@ in
};
}
{
name = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX";
name = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz";
hash = "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz";
hash = "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4=";
};
}
{
@ -186,11 +186,11 @@ in
};
}
{
name = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3";
name = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw";
path = fetchZigArtifact {
name = "libxev";
url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz";
hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=";
url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz";
hash = "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4=";
};
}
{

4
build.zig.zon.txt generated
View File

@ -27,8 +27,8 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz
https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz
https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz

View File

@ -1,13 +1,15 @@
[Desktop Entry]
Name=Ghostty
Version=1.0
Name=@NAME@
Type=Application
Comment=A terminal emulator
Exec=ghostty
TryExec=@GHOSTTY@
Exec=@GHOSTTY@ --launched-from=desktop
Icon=com.mitchellh.ghostty
Categories=System;TerminalEmulator;
Keywords=terminal;tty;pty;
StartupNotify=true
StartupWMClass=com.mitchellh.ghostty
StartupWMClass=@APPID@
Terminal=false
Actions=new-window;
X-GNOME-UsesNotifications=true
@ -16,7 +18,8 @@ X-TerminalArgTitle=--title=
X-TerminalArgAppId=--class=
X-TerminalArgDir=--working-directory=
X-TerminalArgHold=--wait-after-command
DBusActivatable=true
[Desktop Action new-window]
Name=New Window
Exec=ghostty
Exec=@GHOSTTY@ --launched-from=desktop

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.mitchellh.ghostty</id>
<launchable type="desktop-id">com.mitchellh.ghostty.desktop</launchable>
<name>Ghostty</name>
<id>@APPID@</id>
<launchable type="desktop-id">@APPID@.desktop</launchable>
<name>@NAME@</name>
<url type="homepage">https://ghostty.org</url>
<url type="help">https://ghostty.org/docs</url>
<url type="bugtracker">https://github.com/ghostty-org/ghostty/discussions</url>

3
dist/linux/dbus.service.flatpak.in vendored Normal file
View File

@ -0,0 +1,3 @@
[D-BUS Service]
Name=@APPID@
Exec=@GHOSTTY@ --launched-from=dbus

4
dist/linux/dbus.service.in vendored Normal file
View File

@ -0,0 +1,4 @@
[D-BUS Service]
Name=@APPID@
SystemdService=@APPID@.service
Exec=@GHOSTTY@ --launched-from=dbus

7
dist/linux/systemd.service.in vendored Normal file
View File

@ -0,0 +1,7 @@
[Unit]
Description=@NAME@
[Service]
Type=dbus
BusName=@APPID@
ExecStart=@GHOSTTY@ --launched-from=systemd

View File

@ -6,11 +6,7 @@ sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang
default-branch: tip
command: ghostty
# Integrate the rename into zig build, maybe?
rename-desktop-file: com.mitchellh.ghostty.desktop
rename-appdata-file: com.mitchellh.ghostty.metainfo.xml
rename-icon: com.mitchellh.ghostty
desktop-file-name-suffix: " (Debug)"
finish-args:
# 3D rendering
- --device=dri

View File

@ -67,9 +67,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
"dest": "vendor/p/N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
"sha256": "0bddcc4b2360c81fae86fccc4044c35ebefcdfd845c97ecd7d3329e0750ab375"
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
"dest": "vendor/p/N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
"sha256": "83da3608873f4df598a144bf97f1cfe4a644083eea36c75052516bb9a2a4573e"
},
{
"type": "archive",
@ -79,9 +79,9 @@
},
{
"type": "archive",
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
"dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
"sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085"
"url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
"dest": "vendor/p/libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw",
"sha256": "fc248a4ae6597e7d1a2109556743bc721e4ee2009a8d80534eea1eb51768d27e"
},
{
"type": "archive",

View File

@ -610,14 +610,18 @@ extension SplitTree.Node {
return (self, 1)
case .split(let split):
// Recursively equalize children
let (leftNode, leftWeight) = split.left.equalizeWithWeight()
let (rightNode, rightWeight) = split.right.equalizeWithWeight()
// Calculate weights based on split direction
let leftWeight = split.left.weightForDirection(split.direction)
let rightWeight = split.right.weightForDirection(split.direction)
// Calculate new ratio based on relative weights
let totalWeight = leftWeight + rightWeight
let newRatio = Double(leftWeight) / Double(totalWeight)
// Recursively equalize children
let (leftNode, _) = split.left.equalizeWithWeight()
let (rightNode, _) = split.right.equalizeWithWeight()
// Create new split with equalized ratio
let newSplit = Split(
direction: split.direction,
@ -630,6 +634,23 @@ extension SplitTree.Node {
}
}
/// Calculate weight for equalization based on split direction.
/// Children with the same direction contribute their full weight,
/// children with different directions count as 1.
private func weightForDirection(_ direction: SplitTree.Direction) -> Int {
switch self {
case .leaf:
return 1
case .split(let split):
if split.direction == direction {
return split.left.weightForDirection(direction) + split.right.weightForDirection(direction)
} else {
return 1
}
}
}
/// Calculate the bounds of all views in this subtree based on split ratios
func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] {
switch self {

View File

@ -1,3 +1,5 @@
const std = @import("std");
const c = @cImport({
@cInclude("gtk4-layer-shell.h");
});
@ -31,6 +33,14 @@ pub fn getProtocolVersion() c_uint {
return c.gtk_layer_get_protocol_version();
}
pub fn getLibraryVersion() std.SemanticVersion {
return .{
.major = c.gtk_layer_get_major_version(),
.minor = c.gtk_layer_get_minor_version(),
.patch = c.gtk_layer_get_micro_version(),
};
}
pub fn initForWindow(window: *gtk.Window) void {
c.gtk_layer_init_for_window(@ptrCast(window));
}

View File

@ -74,6 +74,9 @@ pub const InternalFormat = enum(c_int) {
srgb = c.GL_SRGB8,
srgba = c.GL_SRGB8_ALPHA8,
rgba_compressed = c.GL_COMPRESSED_RGBA_BPTC_UNORM,
srgba_compressed = c.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM,
// There are so many more that I haven't filled in.
_,
};
@ -126,7 +129,6 @@ pub const Binding = struct {
internal_format: InternalFormat,
width: c.GLsizei,
height: c.GLsizei,
border: c.GLint,
format: Format,
typ: DataType,
data: ?*const anyopaque,
@ -137,7 +139,7 @@ pub const Binding = struct {
@intFromEnum(internal_format),
width,
height,
border,
0,
@intFromEnum(format),
@intFromEnum(typ),
data,

View File

@ -3,11 +3,12 @@ const std = @import("std");
pub const png = @import("png.zig");
pub const jpeg = @import("jpeg.zig");
pub const swizzle = @import("swizzle.zig");
pub const Error = @import("error.zig").Error;
pub const ImageData = struct {
width: u32,
height: u32,
data: []const u8,
data: []u8,
};
test {

View File

@ -33,6 +33,24 @@ pub fn rgbToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
);
}
pub fn bgrToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
return swizzle(
alloc,
src,
c.WUFFS_BASE__PIXEL_FORMAT__BGR,
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
);
}
pub fn bgraToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
return swizzle(
alloc,
src,
c.WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL,
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
);
}
fn swizzle(
alloc: Allocator,
src: []const u8,

View File

@ -1,5 +1,5 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Mitchell Hashimoto, Ghostty contributors
# Copyright (C) YEAR "Mitchell Hashimoto, Ghostty contributors"
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-04-23 16:58+0800\n"
"POT-Creation-Date: 2025-06-28 17:01+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title."
msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
#: src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr ""
@ -35,22 +36,26 @@ msgid "OK"
msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
msgid "Ignore"
msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
msgid "Reload Configuration"
msgstr ""
@ -89,7 +94,7 @@ msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
msgid "Paste"
msgstr ""
@ -119,7 +124,7 @@ msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:255
#: src/apprt/gtk/Window.zig:263
msgid "New Tab"
msgstr ""
@ -160,7 +165,7 @@ msgid "Terminal Inspector"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1024
#: src/apprt/gtk/Window.zig:1036
msgid "About Ghostty"
msgstr ""
@ -170,10 +175,13 @@ msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
@ -181,52 +189,67 @@ msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
#: src/apprt/gtk/Window.zig:208
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
msgstr ""
#: src/apprt/gtk/Window.zig:229
#: src/apprt/gtk/Window.zig:238
msgid "View Open Tabs"
msgstr ""
#: src/apprt/gtk/Window.zig:256
#: src/apprt/gtk/Window.zig:264
msgid "New Split"
msgstr ""
#: src/apprt/gtk/Window.zig:319
#: src/apprt/gtk/Window.zig:327
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
#: src/apprt/gtk/Window.zig:765
#: src/apprt/gtk/Window.zig:773
msgid "Reloaded the configuration"
msgstr ""
#: src/apprt/gtk/Window.zig:1005
#: src/apprt/gtk/Window.zig:1017
msgid "Ghostty Developers"
msgstr ""
@ -270,6 +293,6 @@ msgstr ""
msgid "The currently running process in this split will be terminated."
msgstr ""
#: src/apprt/gtk/Surface.zig:1243
#: src/apprt/gtk/Surface.zig:1257
msgid "Copied to clipboard"
msgstr ""

285
po/es_AR.UTF-8.po Normal file
View File

@ -0,0 +1,285 @@
# Spanish translations for com.mitchellh.ghostty package.
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Alan Moyano <alanmoyano203@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-04-23 16:58+0800\n"
"PO-Revision-Date: 2025-05-19 20:17-0300\n"
"Last-Translator: Alan Moyano <alanmoyano203@gmail.com>\n"
"Language-Team: Argentinian <es@tp.org.es>\n"
"Language: es_AR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "Cambiar el título de la terminal"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Dejar en blanco para restaurar el título predeterminado."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "Cancelar"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
msgstr "Aceptar"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Errores de configuración"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Se encontraron uno o más errores de configuración. Por favor revisá los "
"errores a continuación, y recargá tu configuración o ignorá estos errores."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
msgid "Ignore"
msgstr "Ignorar"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
msgid "Reload Configuration"
msgstr "Recargar configuración"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr "Dividir arriba"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr "Dividir abajo"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr "Dividir a la izquierda"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr "Dividir a la derecha"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Ejecutar un comando…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
msgid "Copy"
msgstr "Copiar"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
msgid "Paste"
msgstr "Pegar"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear"
msgstr "Limpiar"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
msgid "Reset"
msgstr "Reiniciar"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr "Dividir"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
msgid "Change Title…"
msgstr "Cambiar título…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr "Pestaña"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:255
msgid "New Tab"
msgstr "Nueva pestaña"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35
msgid "Close Tab"
msgstr "Cerrar pestaña"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
msgid "Window"
msgstr "Ventana"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18
msgid "New Window"
msgstr "Nueva ventana"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23
msgid "Close Window"
msgstr "Cerrar ventana"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Configuración"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Open Configuration"
msgstr "Abrir configuración"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Paleta de comandos"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
msgstr "Inspector de la terminal"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1024
msgid "About Ghostty"
msgstr "Acerca de Ghostty"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112
msgid "Quit"
msgstr "Salir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "Autorizar acceso al portapapeles"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Una aplicación está intentando leer desde el portapapeles. El contenido "
"actual del portapapeles se muestra a continuación."
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "Denegar"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Una aplicación está intentando escribir en el portapapeles. El contenido "
"actual del portapapeles se muestra a continuación."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr "Advertencia: Pegado potencialmente inseguro"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Pegar este texto en la terminal puede ser peligroso ya que parece que "
"algunos comandos podrían ejecutarse."
#: src/apprt/gtk/Window.zig:208
msgid "Main Menu"
msgstr "Menú principal"
#: src/apprt/gtk/Window.zig:229
msgid "View Open Tabs"
msgstr "Ver pestañas abiertas"
#: src/apprt/gtk/Window.zig:256
msgid "New Split"
msgstr "Nueva división"
#: src/apprt/gtk/Window.zig:319
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
"⚠️ Estás ejecutando una versión de depuración de Ghostty. El rendimiento no "
"será óptimo."
#: src/apprt/gtk/Window.zig:765
msgid "Reloaded the configuration"
msgstr "Configuración recargada"
#: src/apprt/gtk/Window.zig:1005
msgid "Ghostty Developers"
msgstr "Desarrolladores de Ghostty"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Inspector de la terminal"
#: src/apprt/gtk/CloseDialog.zig:47
msgid "Close"
msgstr "Cerrar"
#: src/apprt/gtk/CloseDialog.zig:87
msgid "Quit Ghostty?"
msgstr "¿Salir de Ghostty?"
#: src/apprt/gtk/CloseDialog.zig:88
msgid "Close Window?"
msgstr "¿Cerrar ventana?"
#: src/apprt/gtk/CloseDialog.zig:89
msgid "Close Tab?"
msgstr "¿Cerrar pestaña?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "¿Cerrar división?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
msgstr "Todas las sesiones de terminal serán terminadas."
#: src/apprt/gtk/CloseDialog.zig:97
msgid "All terminal sessions in this window will be terminated."
msgstr "Todas las sesiones de terminal en esta ventana serán terminadas."
#: src/apprt/gtk/CloseDialog.zig:98
msgid "All terminal sessions in this tab will be terminated."
msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas."
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr "El proceso actualmente en ejecución en esta división será terminado."
#: src/apprt/gtk/Surface.zig:1243
msgid "Copied to clipboard"
msgstr "Copiado al portapapeles"

286
po/ga_IE.UTF-8.po Normal file
View File

@ -0,0 +1,286 @@
# Irish translations for com.mitchellh.ghostty package.
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Aindriú Mac Giolla Eoin <aindriu80@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-04-23 16:58+0800\n"
"PO-Revision-Date: 2025-06-29 21:15+0100\n"
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
"Language-Team: Irish <gaeilge-gnulinux@lists.sourceforge.net>\n"
"Language: ga\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n"
"X-Generator: Poedit 3.4.4\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "Athraigh teideal teirminéil"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Fág bán chun an teideal réamhshocraithe a athbhunú."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "Cealaigh"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
msgstr "Ceart go leor"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Earráidí cumraíochta"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Fuarthas earráid chumraíochta amháin nó níos mó. Athbhreithnigh na hearráidí "
"thíos, agus athlódáil do chumraíocht nó déan neamhaird de na hearráidí seo."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
msgid "Ignore"
msgstr "Déan neamhaird de"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
msgid "Reload Configuration"
msgstr "Athlódáil cumraíocht"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr "Scoilt suas"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr "Scoilt síos"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr "Scoilt ar chlé"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr "Scoilt ar dheis"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Rith ordú…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
msgid "Copy"
msgstr "Cóipeáil"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
msgid "Paste"
msgstr "Greamaigh"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear"
msgstr "Glan"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
msgid "Reset"
msgstr "Athshocraigh"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr "Scoilt"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
msgid "Change Title…"
msgstr "Athraigh teideal…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr "Táb"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:255
msgid "New Tab"
msgstr "Táb nua"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35
msgid "Close Tab"
msgstr "Dún táb"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
msgid "Window"
msgstr "Fuinneog"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18
msgid "New Window"
msgstr "Fuinneog nua"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23
msgid "Close Window"
msgstr "Dún fuinneog"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Cumraíocht"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Open Configuration"
msgstr "Oscail cumraíocht"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Pailéad ordaithe"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
msgstr "Cigire teirminéil"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1024
msgid "About Ghostty"
msgstr "Maidir le Ghostty"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112
msgid "Quit"
msgstr "Scoir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "Údarú rochtain ar an ngearrthaisce"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Tá feidhmchlár ag iarraidh léamh ón ngearrthaisce. Taispeántar ábhar reatha "
"an ghearrthaisce thíos."
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "Diúltaigh"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr "Ceadaigh"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Tá feidhmchlár ag iarraidh scríobh chuig an ngearrthaisce. Taispeántar ábhar "
"reatha an ghearrthaisce thíos."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr "Rabhadh: Greamaigh a d'fhéadfadh a bheith neamhshábháilte"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Dfhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa "
"teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú."
#: src/apprt/gtk/Window.zig:208
msgid "Main Menu"
msgstr "Príomh-Roghchlár"
#: src/apprt/gtk/Window.zig:229
msgid "View Open Tabs"
msgstr "Féach ar na táib oscailte"
#: src/apprt/gtk/Window.zig:256
msgid "New Split"
msgstr "Scoilt nua"
#: src/apprt/gtk/Window.zig:319
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
"⚠️ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht."
#: src/apprt/gtk/Window.zig:765
msgid "Reloaded the configuration"
msgstr "Tá an chumraíocht athlódáilte"
#: src/apprt/gtk/Window.zig:1005
msgid "Ghostty Developers"
msgstr "Forbróirí Ghostty"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Cigire teirminéil"
#: src/apprt/gtk/CloseDialog.zig:47
msgid "Close"
msgstr "Dún"
#: src/apprt/gtk/CloseDialog.zig:87
msgid "Quit Ghostty?"
msgstr "Scoir Ghostty?"
#: src/apprt/gtk/CloseDialog.zig:88
msgid "Close Window?"
msgstr "Dún fuinneog?"
#: src/apprt/gtk/CloseDialog.zig:89
msgid "Close Tab?"
msgstr "Dún táb?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "Dún an scoilt?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
msgstr "Cuirfear deireadh le gach seisiún teirminéil."
#: src/apprt/gtk/CloseDialog.zig:97
msgid "All terminal sessions in this window will be terminated."
msgstr "Cuirfear deireadh le gach seisiún teirminéil san fhuinneog seo."
#: src/apprt/gtk/CloseDialog.zig:98
msgid "All terminal sessions in this tab will be terminated."
msgstr "Cuirfear deireadh le gach seisiún teirminéil sa táb seo."
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr ""
"Cuirfear deireadh leis an bpróiseas atá ar siúl faoi láthair sa scoilt seo."
#: src/apprt/gtk/Surface.zig:1243
msgid "Copied to clipboard"
msgstr "Cóipeáilte chuig an ghearrthaisce"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-04-23 16:58+0800\n"
"POT-Creation-Date: 2025-06-28 17:01+0200\n"
"PO-Revision-Date: 2025-02-27 09:16+0100\n"
"Last-Translator: Leah <hi@pluie.me>\n"
"Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\n"
@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title."
msgstr "留空以重置至默认标题。"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
#: src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "取消"
@ -35,10 +36,12 @@ msgid "OK"
msgstr "确认"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "配置错误"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
@ -46,12 +49,14 @@ msgstr ""
"加载配置时发现了以下错误。请仔细阅读错误信息,并选择忽略或重新加载配置文件。"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
msgid "Ignore"
msgstr "忽略"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
msgid "Reload Configuration"
msgstr "重新加载配置"
@ -90,7 +95,7 @@ msgstr "复制"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
msgid "Paste"
msgstr "粘贴"
@ -120,7 +125,7 @@ msgstr "标签页"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:255
#: src/apprt/gtk/Window.zig:263
msgid "New Tab"
msgstr "新建标签页"
@ -161,7 +166,7 @@ msgid "Terminal Inspector"
msgstr "终端调试器"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1024
#: src/apprt/gtk/Window.zig:1036
msgid "About Ghostty"
msgstr "关于 Ghostty"
@ -171,10 +176,13 @@ msgstr "退出"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "剪贴板访问授权"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
@ -182,52 +190,67 @@ msgstr "一个应用正在试图从剪贴板读取内容。剪贴板目前的内
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "拒绝"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr "允许"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "为本分屏记住当前选择"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "本提示将在重载配置后再次出现"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr "一个应用正在试图向剪贴板写入内容。剪贴板目前的内容如下:"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr "警告:粘贴内容可能不安全"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr "将以下内容粘贴至终端内将可能执行有害命令。"
#: src/apprt/gtk/Window.zig:208
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
msgstr "主菜单"
#: src/apprt/gtk/Window.zig:229
#: src/apprt/gtk/Window.zig:238
msgid "View Open Tabs"
msgstr "浏览标签页"
#: src/apprt/gtk/Window.zig:256
#: src/apprt/gtk/Window.zig:264
msgid "New Split"
msgstr "新建分屏"
#: src/apprt/gtk/Window.zig:319
#: src/apprt/gtk/Window.zig:327
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。"
#: src/apprt/gtk/Window.zig:765
#: src/apprt/gtk/Window.zig:773
msgid "Reloaded the configuration"
msgstr "已重新加载配置"
#: src/apprt/gtk/Window.zig:1005
#: src/apprt/gtk/Window.zig:1017
msgid "Ghostty Developers"
msgstr "Ghostty 开发团队"
@ -271,6 +294,6 @@ msgstr "标签页内所有运行中的进程将被终止。"
msgid "The currently running process in this split will be terminated."
msgstr "分屏内正在运行中的进程将被终止。"
#: src/apprt/gtk/Surface.zig:1243
#: src/apprt/gtk/Surface.zig:1257
msgid "Copied to clipboard"
msgstr "已复制至剪贴板"

View File

@ -76,13 +76,11 @@ parts:
- git
- patchelf
- gettext
# TODO: Remove -fno-sys=gtk4-layer-shell when we upgrade to a version that packages it Ubuntu 24.10+
override-build: |
craftctl set version=$(cat VERSION)
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline -fno-sys=gtk4-layer-shell
cp -rp zig-out/* $CRAFT_PART_INSTALL/
# Install libgtk4-layer-shell.so
mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR
cp .zig-cache/*/*/libgtk4-layer-shell.so $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/
sed -i 's|Icon=com.mitchellh.ghostty|Icon=${SNAP}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
libs:

View File

@ -76,34 +76,38 @@ first: bool = true,
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
/// Create a new app instance. This returns a stable pointer to the app
/// instance which is required for callbacks.
pub fn create(alloc: Allocator) CreateError!*App {
var app = try alloc.create(App);
errdefer alloc.destroy(app);
try app.init(alloc);
return app;
}
/// Initialize the main app instance. This creates the main window, sets
/// up the renderer state, compiles the shaders, etc. This is the primary
/// "startup" logic.
///
/// After calling this function, well behaved apprts should then call
/// `focusEvent` to set the initial focus state of the app.
pub fn create(
pub fn init(
self: *App,
alloc: Allocator,
) CreateError!*App {
var app = try alloc.create(App);
errdefer alloc.destroy(app);
) CreateError!void {
var font_grid_set = try font.SharedGridSet.init(alloc);
errdefer font_grid_set.deinit();
app.* = .{
self.* = .{
.alloc = alloc,
.surfaces = .{},
.mailbox = .{},
.font_grid_set = font_grid_set,
.config_conditional_state = .{},
};
errdefer app.surfaces.deinit(alloc);
return app;
}
pub fn destroy(self: *App) void {
pub fn deinit(self: *App) void {
// Clean up all our surfaces
for (self.surfaces.items) |surface| surface.deinit();
self.surfaces.deinit(self.alloc);
@ -114,7 +118,13 @@ pub fn destroy(self: *App) void {
// should gracefully close all surfaces.
assert(self.font_grid_set.count() == 0);
self.font_grid_set.deinit();
}
pub fn destroy(self: *App) void {
// Deinitialize the app
self.deinit();
// Free the app memory
self.alloc.destroy(self);
}

View File

@ -138,6 +138,9 @@ child_exited: bool = false,
/// to let us know.
focused: bool = true,
/// Used to determine whether to continuously scroll.
selection_scroll_active: bool = false,
/// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it
@ -237,6 +240,7 @@ const DerivedConfig = struct {
/// For docs for these, see the associated config they are derived from.
original_font_size: f32,
keybind: configpkg.Keybinds,
abnormal_command_exit_runtime_ms: u32,
clipboard_read: configpkg.ClipboardAccess,
clipboard_write: configpkg.ClipboardAccess,
clipboard_trim_trailing_spaces: bool,
@ -255,6 +259,7 @@ const DerivedConfig = struct {
macos_option_as_alt: ?configpkg.OptionAsAlt,
selection_clear_on_typing: bool,
vt_kam_allowed: bool,
wait_after_command: bool,
window_padding_top: u32,
window_padding_bottom: u32,
window_padding_left: u32,
@ -301,6 +306,7 @@ const DerivedConfig = struct {
return .{
.original_font_size = config.@"font-size",
.keybind = try config.keybind.clone(alloc),
.abnormal_command_exit_runtime_ms = config.@"abnormal-command-exit-runtime",
.clipboard_read = config.@"clipboard-read",
.clipboard_write = config.@"clipboard-write",
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
@ -319,6 +325,7 @@ const DerivedConfig = struct {
.macos_option_as_alt = config.@"macos-option-as-alt",
.selection_clear_on_typing = config.@"selection-clear-on-typing",
.vt_kam_allowed = config.@"vt-kam-allowed",
.wait_after_command = config.@"wait-after-command",
.window_padding_top = config.@"window-padding-y".top_left,
.window_padding_bottom = config.@"window-padding-y".bottom_right,
.window_padding_left = config.@"window-padding-x".top_left,
@ -546,7 +553,7 @@ pub fn init(
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory",
.resources_dir = global_state.resources_dir,
.resources_dir = global_state.resources_dir.host(),
.term = config.term,
// Get the cgroup if we're on linux and have the decl. I'd love
@ -911,11 +918,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.close => self.close(),
// Close without confirmation.
.child_exited => {
self.child_exited = true;
self.close();
},
.child_exited => |v| self.childExited(v),
.desktop_notification => |notification| {
if (!self.config.desktop_notifications) {
@ -945,9 +948,181 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
log.warn("apprt failed to ring bell={}", .{err});
};
},
.selection_scroll_tick => |active| {
self.selection_scroll_active = active;
try self.selectionScrollTick();
},
}
}
fn selectionScrollTick(self: *Surface) !void {
// If we're no longer active then we don't do anything.
if (!self.selection_scroll_active) return;
// If we don't have a left mouse button down then we
// don't do anything.
if (self.mouse.left_click_count == 0) return;
const pos = try self.rt_surface.getCursorPos();
const pos_vp = self.posToViewport(pos.x, pos.y);
const delta: isize = if (pos.y < 0) -1 else 1;
// We need our locked state for the remainder
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const t: *terminal.Terminal = self.renderer_state.terminal;
// Scroll the viewport as required
try t.scrollViewport(.{ .delta = delta });
// Next, trigger our drag behavior
const pin = t.screen.pages.pin(.{
.viewport = .{
.x = pos_vp.x,
.y = pos_vp.y,
},
}) orelse {
if (comptime std.debug.runtime_safety) unreachable;
return;
};
try self.dragLeftClickSingle(pin, pos.x);
// We modified our viewport and selection so we need to queue
// a render.
try self.queueRender();
}
fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
// Mark our flag that we exited immediately
self.child_exited = true;
// If our runtime was below some threshold then we assume that this
// was an abnormal exit and we show an error message.
if (info.runtime_ms <= self.config.abnormal_command_exit_runtime_ms) runtime: {
// On macOS, our exit code detection doesn't work, possibly
// because of our `login` wrapper. More investigation required.
if (comptime builtin.target.os.tag.isDarwin()) break :runtime;
// If the exit code is 0 then we it was a good exit.
if (info.exit_code == 0) break :runtime;
log.warn("abnormal process exit detected, showing error message", .{});
// Update our terminal to note the abnormal exit. In the future we
// may want the apprt to handle this to show some native GUI element.
self.childExitedAbnormally(info) catch |err| {
log.err("error handling abnormal child exit err={}", .{err});
return;
};
return;
}
// We output a message so that the user knows whats going on and
// doesn't think their terminal just froze. We show this unconditionally
// on close even if `wait_after_command` is false and the surface closes
// immediately because if a user does an `undo` to restore a closed
// surface then they will see this message and know the process has
// completed.
terminal: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const t: *terminal.Terminal = self.renderer_state.terminal;
t.carriageReturn();
t.linefeed() catch break :terminal;
t.printString("Process exited. Press any key to close the terminal.") catch
break :terminal;
t.modes.set(.cursor_visible, false);
}
// Waiting after command we stop here. The terminal is updated, our
// state is updated, and now its up to the user to decide what to do.
if (self.config.wait_after_command) return;
// If we aren't waiting after the command, then we exit immediately
// with no confirmation.
self.close();
}
/// Called when the child process exited abnormally.
fn childExitedAbnormally(
self: *Surface,
info: apprt.surface.Message.ChildExited,
) !void {
var arena = ArenaAllocator.init(self.alloc);
defer arena.deinit();
const alloc = arena.allocator();
// Build up our command for the error message
const command = try std.mem.join(alloc, " ", switch (self.io.backend) {
.exec => |*exec| exec.subprocess.args,
});
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const t: *terminal.Terminal = self.renderer_state.terminal;
// No matter what move the cursor back to the column 0.
t.carriageReturn();
// Reset styles
try t.setAttribute(.{ .unset = {} });
// If there is data in the viewport, we want to scroll down
// a little bit and write a horizontal rule before writing
// our message. This lets the use see the error message the
// command may have output.
const viewport_str = try t.plainString(alloc);
if (viewport_str.len > 0) {
try t.linefeed();
for (0..t.cols) |_| try t.print(0x2501);
t.carriageReturn();
try t.linefeed();
try t.linefeed();
}
// Output our error message
try t.setAttribute(.{ .@"8_fg" = .bright_red });
try t.setAttribute(.{ .bold = {} });
try t.printString("Ghostty failed to launch the requested command:");
try t.setAttribute(.{ .unset = {} });
t.carriageReturn();
try t.linefeed();
try t.linefeed();
try t.printString(command);
try t.setAttribute(.{ .unset = {} });
t.carriageReturn();
try t.linefeed();
try t.linefeed();
try t.printString("Runtime: ");
try t.setAttribute(.{ .@"8_fg" = .red });
try t.printString(runtime_str);
try t.setAttribute(.{ .unset = {} });
// We don't print this on macOS because the exit code is always 0
// due to the way we launch the process.
if (comptime !builtin.target.os.tag.isDarwin()) {
const exit_code_str = try std.fmt.allocPrint(alloc, "{d}", .{info.exit_code});
t.carriageReturn();
try t.linefeed();
try t.printString("Exit Code: ");
try t.setAttribute(.{ .@"8_fg" = .red });
try t.printString(exit_code_str);
try t.setAttribute(.{ .unset = {} });
}
t.carriageReturn();
try t.linefeed();
try t.linefeed();
try t.printString("Press any key to close the window.");
// Hide the cursor
t.modes.set(.cursor_visible, false);
}
/// Called when the terminal detects there is a password input prompt.
fn passwordInput(self: *Surface, v: bool) !void {
{
@ -1953,6 +2128,14 @@ pub fn keyCallback(
if (self.io.terminal.modes.get(.disable_keyboard)) return .consumed;
}
// If our process is exited and we press a key then we close the
// surface. We may want to eventually move this to the apprt rather
// than in core.
if (self.child_exited and event.action == .press) {
self.close();
return .closed;
}
// If this input event has text, then we hide the mouse if configured.
// We only do this on pressed events to avoid hiding the mouse when we
// change focus due to a keybinding (i.e. switching tabs).
@ -3094,15 +3277,42 @@ pub fn mouseButtonCallback(
}
}
// Handle link clicking. We want to do this before we do mouse
// reporting or any other mouse handling because a successfully
// clicked link will swallow the event.
if (button == .left and action == .release and self.mouse.over_link) {
const pos = try self.rt_surface.getCursorPos();
if (self.processLinks(pos)) |processed| {
if (processed) return true;
} else |err| {
log.warn("error processing links err={}", .{err});
if (button == .left and action == .release) {
// Stop selection scrolling when releasing the left mouse button
// but only when selection scrolling is active.
if (self.selection_scroll_active) {
self.io.queueMessage(
.{ .selection_scroll = false },
.unlocked,
);
}
// The selection clipboard is only updated for left-click drag when
// the left button is released. This is to avoid the clipboard
// being updated on every mouse move which would be noisy.
if (self.config.copy_on_select != .false) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const prev_ = self.io.terminal.screen.selection;
if (prev_) |prev| {
try self.setSelection(terminal.Selection.init(
prev.start(),
prev.end(),
false,
));
}
}
// Handle link clicking. We want to do this before we do mouse
// reporting or any other mouse handling because a successfully
// clicked link will swallow the event.
if (self.mouse.over_link) {
const pos = try self.rt_surface.getCursorPos();
if (self.processLinks(pos)) |processed| {
if (processed) return true;
} else |err| {
log.warn("error processing links err={}", .{err});
}
}
}
@ -3238,12 +3448,16 @@ pub fn mouseButtonCallback(
log.err("error reading time, mouse multi-click won't work err={}", .{err});
}
// In all cases below, we set the selection directly rather than use
// `setSelection` because we want to avoid copying the selection
// to the selection clipboard. For left mouse clicks we only set
// the clipboard on release.
switch (self.mouse.left_click_count) {
// Single click
1 => {
// If we have a selection, clear it. This always happens.
if (self.io.terminal.screen.selection != null) {
try self.setSelection(null);
try self.io.terminal.screen.select(null);
try self.queueRender();
}
},
@ -3252,7 +3466,7 @@ pub fn mouseButtonCallback(
2 => {
const sel_ = self.io.terminal.screen.selectWord(pin.*);
if (sel_) |sel| {
try self.setSelection(sel);
try self.io.terminal.screen.select(sel);
try self.queueRender();
}
},
@ -3264,7 +3478,7 @@ pub fn mouseButtonCallback(
else
self.io.terminal.screen.selectLine(.{ .pin = pin.* });
if (sel_) |sel| {
try self.setSelection(sel);
try self.io.terminal.screen.select(sel);
try self.queueRender();
}
},
@ -3549,7 +3763,7 @@ pub fn mousePressureCallback(
// to handle state inconsistency here.
const pin = self.mouse.left_click_pin orelse break :select;
const sel = self.io.terminal.screen.selectWord(pin.*) orelse break :select;
try self.setSelection(sel);
try self.io.terminal.screen.select(sel);
try self.queueRender();
}
}
@ -3626,6 +3840,15 @@ pub fn cursorPosCallback(
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
// Stop selection scrolling when inside the viewport within a 1px buffer
// for fullscreen windows, but only when selection scrolling is active.
if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) {
self.io.queueMessage(
.{ .selection_scroll = false },
.locked,
);
}
// Update our mouse state. We set this to null initially because we only
// want to set it when we're not selecting or doing any other mouse
// event.
@ -3708,13 +3931,16 @@ pub fn cursorPosCallback(
// Note: one day, we can change this from distance to time based if we want.
//log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen });
const max_y: f32 = @floatFromInt(self.size.screen.height);
if (pos.y <= 1 or pos.y > max_y - 1) {
const delta: isize = if (pos.y < 0) -1 else 1;
try self.io.terminal.scrollViewport(.{ .delta = delta });
// TODO: We want a timer or something to repeat while we're still
// at this cursor position. Right now, the user has to jiggle their
// mouse in order to scroll.
// If the mouse is outside the viewport and we have the left
// mouse button pressed then we need to start the scroll timer.
if ((pos.y <= 1 or pos.y > max_y - 1) and
!self.selection_scroll_active)
{
self.io.queueMessage(
.{ .selection_scroll = true },
.locked,
);
}
// Convert to points
@ -3768,13 +3994,13 @@ fn dragLeftClickDouble(
// If our current mouse position is before the starting position,
// then the selection start is the word nearest our current position.
if (drag_pin.before(click_pin)) {
try self.setSelection(terminal.Selection.init(
try self.io.terminal.screen.select(.init(
word_current.start(),
word_start.end(),
false,
));
} else {
try self.setSelection(terminal.Selection.init(
try self.io.terminal.screen.select(.init(
word_start.start(),
word_current.end(),
false,
@ -3806,7 +4032,7 @@ fn dragLeftClickTriple(
} else {
sel.endPtr().* = line.end();
}
try self.setSelection(sel);
try self.io.terminal.screen.select(sel);
}
fn dragLeftClickSingle(
@ -3815,7 +4041,7 @@ fn dragLeftClickSingle(
drag_x: f64,
) !void {
// This logic is in a separate function so that it can be unit tested.
try self.setSelection(mouseSelection(
try self.io.terminal.screen.select(mouseSelection(
self.mouse.left_click_pin.?.*,
drag_pin,
@intFromFloat(@max(0.0, self.mouse.left_click_xpos)),
@ -4685,6 +4911,11 @@ fn writeScreenFile(
const path = try tmp_dir.dir.realpath(filename, &path_buf);
switch (write_action) {
.copy => {
const pathZ = try self.alloc.dupeZ(u8, path);
defer self.alloc.free(pathZ);
try self.rt_surface.setClipboardString(pathZ, .standard, false);
},
.open => try internal_os.open(self.alloc, .text, path),
.paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc,

View File

@ -1,2 +1,4 @@
const internal_os = @import("../os/main.zig");
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {};
pub const Window = struct {};

View File

@ -23,6 +23,8 @@ const Config = configpkg.Config;
const log = std.log.scoped(.embedded_window);
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {
/// Because we only expect the embedding API to be used in embedded
/// environments, the options are extern so that we can expose it
@ -115,10 +117,11 @@ pub const App = struct {
config: Config,
pub fn init(
self: *App,
core_app: *CoreApp,
config: *const Config,
opts: Options,
) !App {
) !void {
// We have to clone the config.
const alloc = core_app.alloc;
var config_clone = try config.clone(alloc);
@ -127,7 +130,7 @@ pub const App = struct {
var keymap = try input.Keymap.init();
errdefer keymap.deinit();
return .{
self.* = .{
.core_app = core_app,
.config = config_clone,
.opts = opts,
@ -1314,13 +1317,13 @@ pub const CAPI = struct {
opts: *const apprt.runtime.App.Options,
config: *const Config,
) !*App {
var core_app = try CoreApp.create(global.alloc);
const core_app = try CoreApp.create(global.alloc);
errdefer core_app.destroy();
// Create our runtime app
var app = try global.alloc.create(App);
errdefer global.alloc.destroy(app);
app.* = try .init(core_app, config, opts.*);
try app.init(core_app, config, opts.*);
errdefer app.terminate();
return app;

View File

@ -35,6 +35,8 @@ const darwin_enabled = builtin.target.os.tag.isDarwin() and
const log = std.log.scoped(.glfw);
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {
app: *CoreApp,
config: Config,
@ -48,7 +50,7 @@ pub const App = struct {
pub const Options = struct {};
pub fn init(core_app: *CoreApp, _: Options) !App {
pub fn init(self: *App, core_app: *CoreApp, _: Options) !void {
if (comptime builtin.target.os.tag.isDarwin()) {
log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{});
log.warn("You should use the AppKit-based app instead. The official download", .{});
@ -105,7 +107,7 @@ pub const App = struct {
// We want the event loop to wake up instantly so we can process our tick.
glfw.postEmptyEvent();
return .{
self.* = .{
.app = core_app,
.config = config,
.darwin = darwin,

View File

@ -2,6 +2,7 @@
pub const App = @import("gtk/App.zig");
pub const Surface = @import("gtk/Surface.zig");
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -110,7 +110,7 @@ quit_timer: union(enum) {
expired: void,
} = .{ .off = {} },
pub fn init(core_app: *CoreApp, opts: Options) !App {
pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
_ = opts;
// Log our GTK version
@ -405,11 +405,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// This just calls the `activate` signal but its part of the normal startup
// routine so we just call it, but only if the config allows it (this allows
// for launching Ghostty in the "background" without immediately opening
// a window)
// a window). An initial window will not be immediately created if we were
// launched by D-Bus activation or systemd. D-Bus activation will send it's
// own `activate` or `new-window` signal later.
//
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
if (config.@"initial-window")
gio_app.activate();
if (config.@"initial-window") switch (config.@"launched-from".?) {
.desktop, .cli => gio_app.activate(),
.dbus, .systemd => {},
};
// Internally, GTK ensures that only one instance of this provider exists in the provider list
// for the display.
@ -420,7 +424,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
);
return .{
self.* = .{
.core_app = core_app,
.app = adw_app,
.config = config,
@ -1683,6 +1687,17 @@ fn gtkActionShowGTKInspector(
};
}
fn gtkActionNewWindow(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
log.info("received new window action", .{});
_ = self.core_app.mailbox.push(.{
.new_window = .{},
}, .{ .forever = {} });
}
/// This is called to setup the action map that this application supports.
/// This should be called only once on startup.
fn initActions(self: *App) void {
@ -1702,7 +1717,9 @@ fn initActions(self: *App) void {
.{ "reload-config", gtkActionReloadConfig, null },
.{ "present-surface", gtkActionPresentSurface, t },
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
.{ "new-window", gtkActionNewWindow, null },
};
inline for (actions) |entry| {
const action = gio.SimpleAction.new(entry[0], entry[2]);
defer action.unref();

View File

@ -17,7 +17,7 @@ const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk);
const DialogType = if (adw_version.atLeast(1, 5, 0)) adw.AlertDialog else adw.MessageDialog;
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
app: *App,
dialog: *DialogType,
@ -28,6 +28,7 @@ text_view: *gtk.TextView,
text_view_scroll: *gtk.ScrolledWindow,
reveal_button: *gtk.Button,
hide_button: *gtk.Button,
remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
pub fn create(
app: *App,
@ -89,6 +90,10 @@ fn init(
const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
const hide_button = builder.getObject(gtk.Button, "hide_button").?;
const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?;
const remember_choice = if (adw_version.supportsSwitchRow())
builder.getObject(adw.SwitchRow, "remember_choice")
else
null;
const copy = try app.core_app.alloc.dupeZ(u8, data);
errdefer app.core_app.alloc.free(copy);
@ -102,6 +107,7 @@ fn init(
.text_view_scroll = text_view_scroll,
.reveal_button = reveal_button,
.hide_button = hide_button,
.remember_choice = remember_choice,
};
const buffer = gtk.TextBuffer.new(null);
@ -152,8 +158,10 @@ fn init(
}
}
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
if (std.mem.orderZ(u8, response, "ok") == .eq) {
fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
if (is_ok) {
self.core_surface.completeClipboardRequest(
self.pending_req,
self.data,
@ -162,8 +170,30 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation)
log.err("Failed to requeue clipboard request: {}", .{err});
};
}
if (self.remember_choice) |remember| remember: {
if (!adw_version.supportsSwitchRow()) break :remember;
if (remember.getActive() == 0) break :remember;
switch (self.pending_req) {
.osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny,
.osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny,
.paste => {},
}
}
self.destroy();
}
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
const response = dialog.chooseFinish(result);
self.handleResponse(response);
}
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void {
self.handleResponse(response);
}
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));

View File

@ -94,9 +94,8 @@ pub fn deinit(self: *CommandPalette) void {
pub fn toggle(self: *CommandPalette) void {
self.dialog.present(self.window.window.as(gtk.Widget));
// Focus on the search bar when opening the dialog
self.dialog.setFocus(self.search.as(gtk.Widget));
_ = self.search.as(gtk.Widget).grabFocus();
}
pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void {
@ -104,13 +103,17 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi
self.source.removeAll();
_ = self.arena.reset(.retain_capacity);
// TODO: Allow user-configured palette entries
for (inputpkg.command.defaults) |command| {
for (config.@"command-palette-entry".value.items) |command| {
// Filter out actions that are not implemented
// or don't make sense for GTK
switch (command.action) {
.close_all_windows,
.toggle_secure_input,
.check_for_updates,
.redo,
.undo,
.reset_window_size,
.toggle_window_float_on_top,
=> continue,
else => {},

View File

@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
// Remove some environment variables that are set when Ghostty is launched
// from a `.desktop` file, by D-Bus activation, or systemd.
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
env.remove("DBUS_STARTER_ADDRESS");
env.remove("DBUS_STARTER_BUS_TYPE");
env.remove("INVOCATION_ID");
env.remove("JOURNAL_STREAM");
// Unset environment varies set by snaps if we're running in a snap.
// This allows Ghostty to further launch additional snaps.
if (env.get("SNAP")) |_| {

View File

@ -55,6 +55,9 @@ window: *adw.ApplicationWindow,
/// The header bar for the window.
headerbar: HeaderBar,
/// The tab bar for the window.
tab_bar: *adw.TabBar,
/// The tab overview for the window. This is possibly null since there is no
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
tab_overview: ?*adw.TabOverview,
@ -86,6 +89,7 @@ pub const DerivedConfig = struct {
gtk_tabs_location: configpkg.Config.GtkTabsLocation,
gtk_wide_tabs: bool,
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
window_show_tab_bar: configpkg.Config.WindowShowTabBar,
quick_terminal_position: configpkg.Config.QuickTerminalPosition,
quick_terminal_size: configpkg.Config.QuickTerminalSize,
@ -106,6 +110,7 @@ pub const DerivedConfig = struct {
.gtk_tabs_location = config.@"gtk-tabs-location",
.gtk_wide_tabs = config.@"gtk-wide-tabs",
.gtk_toolbar_style = config.@"gtk-toolbar-style",
.window_show_tab_bar = config.@"window-show-tab-bar",
.quick_terminal_position = config.@"quick-terminal-position",
.quick_terminal_size = config.@"quick-terminal-size",
@ -141,6 +146,7 @@ pub fn init(self: *Window, app: *App) !void {
.config = .init(&app.config),
.window = undefined,
.headerbar = undefined,
.tab_bar = undefined,
.tab_overview = null,
.notebook = undefined,
.titlebar_menu = undefined,
@ -225,8 +231,9 @@ pub fn init(self: *Window, app: *App) !void {
// If we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| {
if (!adw_version.supportsTabOverview()) unreachable;
const btn = switch (self.config.gtk_tabs_location) {
.top, .bottom => btn: {
const btn = switch (self.config.window_show_tab_bar) {
.always, .auto => btn: {
const btn = gtk.ToggleButton.new();
btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs"));
btn.as(gtk.Button).setIconName("view-grid-symbolic");
@ -238,8 +245,7 @@ pub fn init(self: *Window, app: *App) !void {
);
break :btn btn.as(gtk.Widget);
},
.hidden => btn: {
.never => btn: {
const btn = adw.TabButton.new();
btn.setView(self.notebook.tab_view);
btn.as(gtk.Actionable).setActionName("overview.open");
@ -385,21 +391,16 @@ pub fn init(self: *Window, app: *App) !void {
// Our actions for the menu
initActions(self);
self.tab_bar = adw.TabBar.new();
self.tab_bar.setView(self.notebook.tab_view);
if (adw_version.supportsToolbarView()) {
const toolbar_view = adw.ToolbarView.new();
toolbar_view.addTopBar(self.headerbar.asWidget());
if (self.config.gtk_tabs_location != .hidden) {
const tab_bar = adw.TabBar.new();
tab_bar.setView(self.notebook.tab_view);
if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0);
switch (self.config.gtk_tabs_location) {
.top => toolbar_view.addTopBar(tab_bar.as(gtk.Widget)),
.bottom => toolbar_view.addBottomBar(tab_bar.as(gtk.Widget)),
.hidden => unreachable,
}
switch (self.config.gtk_tabs_location) {
.top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)),
.bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)),
}
toolbar_view.setContent(box.as(gtk.Widget));
@ -414,23 +415,18 @@ pub fn init(self: *Window, app: *App) !void {
// Set our application window content.
self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget));
self.window.setContent(self.tab_overview.?.as(gtk.Widget));
} else tab_bar: {
if (self.config.gtk_tabs_location == .hidden) break :tab_bar;
} else {
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView.
const tab_bar = adw.TabBar.new();
tab_bar.as(gtk.Widget).addCssClass("inline");
self.tab_bar.as(gtk.Widget).addCssClass("inline");
switch (self.config.gtk_tabs_location) {
.top => box.insertChildAfter(
tab_bar.as(gtk.Widget),
self.tab_bar.as(gtk.Widget),
self.headerbar.asWidget(),
),
.bottom => box.append(tab_bar.as(gtk.Widget)),
.hidden => unreachable,
.bottom => box.append(self.tab_bar.as(gtk.Widget)),
}
tab_bar.setView(self.notebook.tab_view);
if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0);
}
// If we want the window to be maximized, we do that here.
@ -555,6 +551,16 @@ pub fn syncAppearance(self: *Window) !void {
}
}
self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs));
self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) {
.auto, .never => @intFromBool(true),
.always => @intFromBool(false),
});
self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) {
.always, .auto => @intFromBool(true),
.never => @intFromBool(false),
});
self.winproto.syncAppearance() catch |err| {
log.warn("failed to sync winproto appearance error={}", .{err});
};

View File

@ -109,6 +109,10 @@ pub inline fn supportsTabOverview() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsSwitchRow() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsToolbarView() bool {
return atLeast(1, 4, 0);
}

29
src/apprt/gtk/flatpak.zig Normal file
View File

@ -0,0 +1,29 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const internal_os = @import("../../os/main.zig");
const glib = @import("glib");
pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
if (comptime build_config.flatpak) {
// Only consult Flatpak runtime data for host case.
if (internal_os.isFlatpak()) {
var result: internal_os.ResourcesDir = .{
.app_path = try alloc.dupe(u8, "/app/share/ghostty"),
};
errdefer alloc.free(result.app_path.?);
const keyfile = glib.KeyFile.new();
defer keyfile.unref();
if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
defer glib.free(app_dir.ptr);
result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
return result;
}
}
return try internal_os.resourcesDir(alloc);
}

View File

@ -64,14 +64,18 @@ window.ssd.no-border-radius {
padding: 0;
}
.clipboard-overlay {
border-radius: 10px;
}
.clipboard-content-view {
filter: blur(0px);
transition: filter 0.3s ease;
border-radius: 10px;
}
.clipboard-content-view.blurred {
filter: blur(5px);
transition: filter 0.3s ease;
}
.command-palette-search {

View File

@ -14,58 +14,72 @@ Adw.AlertDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: Overlay {
extra-child: ListBox {
selection-mode: none;
styles [
"osd",
"boxed-list-separate",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Overlay {
styles [
"opaque",
"osd",
"clipboard-overlay",
]
Image {
icon-name: "view-conceal-symbolic";
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 200;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
}
}
Adw.SwitchRow remember_choice {
title: _("Remember choice for this split");
subtitle: _("Reload configuration to show this prompt again");
}
};
}

View File

@ -14,58 +14,68 @@ Adw.AlertDialog clipboard_confirmation_window {
default-response: "cancel";
close-response: "cancel";
extra-child: Overlay {
extra-child: ListBox {
selection-mode: none;
styles [
"osd",
"boxed-list-separate",
]
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 250;
Overlay {
styles [
"osd",
"clipboard-overlay",
]
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
ScrolledWindow text_view_scroll {
width-request: 500;
height-request: 200;
TextView text_view {
cursor-visible: false;
editable: false;
monospace: true;
top-margin: 8;
left-margin: 8;
bottom-margin: 8;
right-margin: 8;
styles [
"clipboard-content-view",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"clipboard-content-view",
"opaque",
]
}
}
[overlay]
Button reveal_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
Image {
icon-name: "view-reveal-symbolic";
}
}
[overlay]
Button hide_button {
visible: false;
halign: end;
valign: start;
margin-end: 12;
margin-top: 12;
styles [
"opaque",
]
Image {
icon-name: "view-conceal-symbolic";
}
Adw.SwitchRow remember_choice {
title: _("Remember choice for this split");
subtitle: _("Reload configuration to show this prompt again");
}
};
}

View File

@ -37,6 +37,19 @@ pub const App = struct {
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
xdg_activation: ?*xdg.ActivationV1 = null,
/// Whether the xdg_wm_dialog_v1 protocol is present.
///
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
/// creates a quick terminal, and we need to ensure this fails
/// gracefully if this situation occurs.
///
/// FIXME: This is a temporary workaround - we should remove this when
/// all of our supported distros drop support for affected old
/// gtk4-layer-shell versions.
///
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
xdg_wm_dialog_present: bool = false,
};
pub fn init(
@ -95,11 +108,21 @@ pub const App = struct {
return null;
}
pub fn supportsQuickTerminal(_: App) bool {
pub fn supportsQuickTerminal(self: App) bool {
if (!layer_shell.isSupported()) {
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
return false;
}
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
.major = 1,
.minor = 0,
.patch = 4,
}) == .lt) {
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
return false;
}
return true;
}
@ -111,26 +134,38 @@ pub const App = struct {
layer_shell.setNamespace(window, "ghostty-quick-terminal");
}
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
// Globals should be optional pointers
const T = switch (@typeInfo(field.type)) {
.optional => |o| switch (@typeInfo(o.child)) {
.pointer => |v| v.child,
else => return null,
},
else => return null,
};
// Only process Wayland interfaces
if (!@hasDecl(T, "interface")) return null;
return T;
}
fn registryListener(
registry: *wl.Registry,
event: wl.Registry.Event,
context: *Context,
) void {
inline for (@typeInfo(Context).@"struct".fields) |field| {
// Globals should be optional pointers
const T = switch (@typeInfo(field.type)) {
.optional => |o| switch (@typeInfo(o.child)) {
.pointer => |v| v.child,
else => continue,
},
else => continue,
};
const ctx_fields = @typeInfo(Context).@"struct".fields;
// Only process Wayland interfaces
if (!@hasDecl(T, "interface")) continue;
switch (event) {
.global => |v| global: {
// We don't actually do anything with this other than checking
// for its existence, so we process this separately.
if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq)
context.xdg_wm_dialog_present = true;
inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue;
switch (event) {
.global => |v| global: {
if (std.mem.orderZ(
u8,
v.interface,
@ -148,19 +183,22 @@ pub const App = struct {
);
return;
};
},
}
},
// This should be a rare occurrence, but in case a global
// is suddenly no longer available, we destroy and unset it
// as the protocol mandates.
.global_remove => |v| remove: {
// This should be a rare occurrence, but in case a global
// is suddenly no longer available, we destroy and unset it
// as the protocol mandates.
.global_remove => |v| remove: {
inline for (ctx_fields) |field| {
if (getInterfaceType(field) == null) continue;
const global = @field(context, field.name) orelse break :remove;
if (global.getId() == v.name) {
global.destroy();
@field(context, field.name) = null;
}
},
}
}
},
}
}

View File

@ -1,2 +1,4 @@
const internal_os = @import("../os/main.zig");
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {};
pub const Surface = struct {};

View File

@ -43,8 +43,9 @@ pub const Message = union(enum) {
close: void,
/// The child process running in the surface has exited. This may trigger
/// a surface close, it may not.
child_exited: void,
/// a surface close, it may not. Additional details about the child
/// command are given in the `ChildExited` struct.
child_exited: ChildExited,
/// Show a desktop notification.
desktop_notification: struct {
@ -78,6 +79,13 @@ pub const Message = union(enum) {
color: terminal.color.RGB,
},
/// Notifies the surface that a tick of the timer that is timing
/// out selection scrolling has occurred. "selection scrolling"
/// is when the user has clicked and dragged the mouse outside
/// the viewport of the terminal and the terminal is scrolling
/// the viewport to follow the mouse cursor.
selection_scroll_tick: bool,
/// The terminal has reported a change in the working directory.
pwd_change: WriteReq,
@ -89,6 +97,11 @@ pub const Message = union(enum) {
// This enum is a placeholder for future title styles.
};
pub const ChildExited = struct {
exit_code: u32,
runtime_ms: u64,
};
};
/// A surface mailbox.

View File

@ -87,7 +87,7 @@ pub fn init(b: *std.Build) !Config {
// This is set to true when we're building a system package. For now
// this is trivially detected using the "system_package_mode" bool
// but we may want to make this more sophisticated in the future.
const system_package: bool = b.graph.system_package_mode;
const system_package = b.graph.system_package_mode;
// This specifies our target wasm runtime. For now only one semi-usable
// one exists so this is hardcoded.
@ -361,7 +361,6 @@ pub fn init(b: *std.Build) !Config {
"libpng",
"zlib",
"oniguruma",
"gtk4-layer-shell",
}) |dep| {
_ = b.systemIntegrationOption(
dep,
@ -387,6 +386,15 @@ pub fn init(b: *std.Build) !Config {
}) |dep| {
_ = b.systemIntegrationOption(dep, .{ .default = false });
}
// These are dynamic libraries we default to true, preferring
// to use system packages over building and installing libs
// as they require additional ldconfig of library paths or
// patching the rpath of the program to discover the dynamic library
// at runtime
for (&[_][]const u8{"gtk4-layer-shell"}) |dep| {
_ = b.systemIntegrationOption(dep, .{ .default = true });
}
}
return config;

View File

@ -2,6 +2,7 @@ const GhosttyResources = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const buildpkg = @import("main.zig");
const Config = @import("Config.zig");
const config_vim = @import("../config/vim.zig");
@ -220,83 +221,178 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
}
// App (Linux)
if (cfg.target.result.os.tag == .linux) {
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
if (cfg.target.result.os.tag == .linux) try addLinuxAppResources(
b,
cfg,
&steps,
);
return .{ .steps = steps.items };
}
/// Add the resource files needed to make Ghostty a proper
/// Linux desktop application (for various desktop environments).
fn addLinuxAppResources(
b: *std.Build,
cfg: *const Config,
steps: *std.ArrayList(*std.Build.Step),
) !void {
assert(cfg.target.result.os.tag == .linux);
// Background:
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
const name = b.fmt("Ghostty{s}", .{
switch (cfg.optimize) {
.Debug, .ReleaseSafe => " (Debug)",
.ReleaseFast, .ReleaseSmall => "",
},
});
const app_id = b.fmt("com.mitchellh.ghostty{s}", .{
switch (cfg.optimize) {
.Debug, .ReleaseSafe => "-debug",
.ReleaseFast, .ReleaseSmall => "",
},
});
const exe_abs_path = b.fmt(
"{s}/bin/ghostty",
.{b.install_prefix},
);
// The templates that we will process. The templates are in
// cmake format and will be processed and saved to the
// second element of the tuple.
const Template = struct { std.Build.LazyPath, []const u8 };
const templates: []const Template = templates: {
var ts: std.ArrayList(Template) = .init(b.allocator);
// Desktop file so that we have an icon and other metadata
try steps.append(&b.addInstallFile(
b.path("dist/linux/app.desktop"),
"share/applications/com.mitchellh.ghostty.desktop",
).step);
try ts.append(.{
b.path("dist/linux/app.desktop.in"),
b.fmt("share/applications/{s}.desktop", .{app_id}),
});
// AppStream metainfo so that application has rich metadata within app stores
try steps.append(&b.addInstallFile(
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"),
"share/metainfo/com.mitchellh.ghostty.metainfo.xml",
).step);
// Service for DBus activation.
try ts.append(.{
if (cfg.flatpak)
b.path("dist/linux/dbus.service.flatpak.in")
else
b.path("dist/linux/dbus.service.in"),
b.fmt("share/dbus-1/services/{s}.service", .{app_id}),
});
// Right click menu action for Plasma desktop
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_dolphin.desktop"),
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
// systemd user service. This is kind of nasty but systemd
// looks for user services in different paths depending on
// if we are installed as a system package or not (lib vs.
// share) so we have to handle that here. We might be able
// to get away with always installing to both because it
// only ever searches in one... but I don't want to do that hack
// until we have to.
if (!cfg.flatpak) try ts.append(.{
b.path("dist/linux/systemd.service.in"),
b.fmt(
"{s}/systemd/user/{s}.service",
.{
if (b.graph.system_package_mode) "lib" else "share",
app_id,
},
),
});
// Right click menu action for Nautilus. Note that this _must_ be named
// `ghostty.py`. Using the full app id causes problems (see #5468).
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_nautilus.py"),
"share/nautilus-python/extensions/ghostty.py",
).step);
// AppStream metainfo so that application has rich metadata
// within app stores
try ts.append(.{
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"),
b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}),
});
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16.png"),
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32.png"),
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128.png"),
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256.png"),
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_512.png"),
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
).step);
// Flatpaks only support icons up to 512x512.
if (!cfg.flatpak) {
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_1024.png"),
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
).step);
}
break :templates ts.items;
};
// Process all our templates
for (templates) |template| {
const tpl = b.addConfigHeader(.{
.style = .{ .cmake = template[0] },
}, .{
.NAME = name,
.APPID = app_id,
.GHOSTTY = exe_abs_path,
});
// Template output has a single header line we want to remove.
// We use `tail` to do it since its part of the POSIX standard.
const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" });
tail.setStdIn(.{ .lazy_path = tpl.getOutput() });
const copy = b.addInstallFile(
tail.captureStdOut(),
template[1],
);
try steps.append(&copy.step);
}
// Right click menu action for Plasma desktop
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_dolphin.desktop"),
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
// Right click menu action for Nautilus. Note that this _must_ be named
// `ghostty.py`. Using the full app id causes problems (see #5468).
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_nautilus.py"),
"share/nautilus-python/extensions/ghostty.py",
).step);
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16.png"),
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32.png"),
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128.png"),
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256.png"),
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_512.png"),
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
).step);
// Flatpaks only support icons up to 512x512.
if (!cfg.flatpak) {
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16@2x.png"),
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32@2x.png"),
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128@2x.png"),
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256@2x.png"),
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
b.path("images/icons/icon_1024.png"),
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
).step);
}
return .{ .steps = steps.items };
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16@2x.png"),
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_32@2x.png"),
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_128@2x.png"),
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
).step);
try steps.append(&b.addInstallFile(
b.path("images/icons/icon_256@2x.png"),
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
).step);
}
pub fn install(self: *const GhosttyResources) void {

View File

@ -75,7 +75,7 @@ fn initTarget(
self.metallib = .create(b, .{
.name = "Ghostty",
.target = target,
.sources = &.{b.path("src/renderer/shaders/cell.metal")},
.sources = &.{b.path("src/renderer/shaders/shaders.metal")},
});
// Change our config
@ -652,14 +652,13 @@ fn addGTK(
// IMPORTANT: gtk4-layer-shell must be linked BEFORE
// wayland-client, as it relies on shimming libwayland's APIs.
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
step.linkSystemLibrary2(
"gtk4-layer-shell-0",
dynamic_link_opts,
);
step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
} else {
// gtk4-layer-shell *must* be dynamically linked,
// so we don't add it as a static library
step.linkLibrary(gtk4_layer_shell.artifact("gtk4-layer-shell"));
const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
b.installArtifact(shared_lib);
step.linkLibrary(shared_lib);
}
}

View File

@ -2,6 +2,8 @@ const diags = @import("cli/diagnostics.zig");
pub const args = @import("cli/args.zig");
pub const Action = @import("cli/action.zig").Action;
pub const CompatibilityHandler = args.CompatibilityHandler;
pub const compatibilityRenamed = args.compatibilityRenamed;
pub const DiagnosticList = diags.DiagnosticList;
pub const Diagnostic = diags.Diagnostic;
pub const Location = diags.Location;

View File

@ -9,6 +9,7 @@ const list_keybinds = @import("list_keybinds.zig");
const list_themes = @import("list_themes.zig");
const list_colors = @import("list_colors.zig");
const list_actions = @import("list_actions.zig");
const edit_config = @import("edit_config.zig");
const show_config = @import("show_config.zig");
const validate_config = @import("validate_config.zig");
const crash_report = @import("crash_report.zig");
@ -40,6 +41,9 @@ pub const Action = enum {
/// List keybind actions
@"list-actions",
/// Edit the config file in the configured terminal editor.
@"edit-config",
/// Dump the config to stdout
@"show-config",
@ -151,6 +155,7 @@ pub const Action = enum {
.@"list-themes" => try list_themes.run(alloc),
.@"list-colors" => try list_colors.run(alloc),
.@"list-actions" => try list_actions.run(alloc),
.@"edit-config" => try edit_config.run(alloc),
.@"show-config" => try show_config.run(alloc),
.@"validate-config" => try validate_config.run(alloc),
.@"crash-report" => try crash_report.run(alloc),
@ -187,6 +192,7 @@ pub const Action = enum {
.@"list-themes" => list_themes.Options,
.@"list-colors" => list_colors.Options,
.@"list-actions" => list_actions.Options,
.@"edit-config" => edit_config.Options,
.@"show-config" => show_config.Options,
.@"validate-config" => validate_config.Options,
.@"crash-report" => crash_report.Options,

View File

@ -40,11 +40,14 @@ pub const Error = error{
/// "DiagnosticList" and any diagnostic messages will be added to that list.
/// When diagnostics are present, only allocation errors will be returned.
///
/// If the destination type has a decl "renamed", it must be of type
/// std.StaticStringMap([]const u8) and contains a mapping from the old
/// field name to the new field name. This is used to allow renaming fields
/// while still supporting the old name. If a renamed field is set, parsing
/// will automatically set the new field name.
/// If the destination type has a decl "compatibility", it must be of type
/// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to
/// handle backwards compatibility for fields with the given name. The
/// field name doesn't need to exist (so you can setup compatibility for
/// removed fields). The value is a function that will be called when
/// all other parsing fails for that field. If a field changes such that
/// the old values would NOT error, then the caller should handle that
/// downstream after parsing is done, not through this method.
///
/// Note: If the arena is already non-null, then it will be used. In this
/// case, in the case of an error some memory might be leaked into the arena.
@ -57,24 +60,6 @@ pub fn parse(
const info = @typeInfo(T);
assert(info == .@"struct");
comptime {
// Verify all renamed fields are valid (source does not exist,
// destination does exist).
if (@hasDecl(T, "renamed")) {
for (T.renamed.keys(), T.renamed.values()) |key, value| {
if (@hasField(T, key)) {
@compileLog(key);
@compileError("renamed field source exists");
}
if (!@hasField(T, value)) {
@compileLog(value);
@compileError("renamed field destination does not exist");
}
}
}
}
// Make an arena for all our allocations if we support it. Otherwise,
// use an allocator that always fails. If the arena is already set on
// the config, then we reuse that. See memory note in parse docs.
@ -147,7 +132,23 @@ pub fn parse(
break :value null;
};
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
parseIntoField(T, arena_alloc, dst, key, value) catch |err| err: {
// If we get an error parsing a field, then we try to fall
// back to compatibility handlers if able.
if (@hasDecl(T, "compatibility")) {
// If we have a compatibility handler for this key, then
// we call it and see if it handles the error.
if (T.compatibility.get(key)) |handler| {
if (handler(dst, arena_alloc, key, value)) {
log.info(
"compatibility handler for {s} handled error, you may be using a deprecated field: {}",
.{ key, err },
);
break :err;
}
}
}
if (comptime !canTrackDiags(T)) return err;
// The error set is dependent on comptime T, so we always add
@ -177,6 +178,58 @@ pub fn parse(
}
}
/// The function type for a compatibility handler. The compatibility
/// handler is documented in the `parse` function documentation.
///
/// The function type should return bool if the compatibility was
/// handled, and false otherwise. If false is returned then the
/// naturally occurring error will continue to be processed as if
/// this compatibility handler was not present.
///
/// Compatibility handlers aren't allowed to return errors because
/// they're generally only called in error cases, so we already have
/// an error message to show users. If there is an error in handling
/// the compatibility, then the handler should return false.
pub fn CompatibilityHandler(comptime T: type) type {
return *const fn (
dst: *T,
alloc: Allocator,
key: []const u8,
value: ?[]const u8,
) bool;
}
/// Convenience function to create a compatibility handler that
/// renames a field from `from` to `to`.
pub fn compatibilityRenamed(
comptime T: type,
comptime to: []const u8,
) CompatibilityHandler(T) {
comptime assert(@hasField(T, to));
return (struct {
fn compat(
dst: *T,
alloc: Allocator,
key: []const u8,
value: ?[]const u8,
) bool {
_ = key;
parseIntoField(T, alloc, dst, to, value) catch |err| {
log.warn("error parsing renamed field {s}: {}", .{
to,
err,
});
return false;
};
return true;
}
}).compat;
}
fn formatValueRequired(
comptime T: type,
arena_alloc: std.mem.Allocator,
@ -401,16 +454,6 @@ pub fn parseIntoField(
}
}
// Unknown field, is the field renamed?
if (@hasDecl(T, "renamed")) {
for (T.renamed.keys(), T.renamed.values()) |old, new| {
if (mem.eql(u8, old, key)) {
try parseIntoField(T, alloc, dst, new, value);
return;
}
}
}
return error.InvalidField;
}
@ -752,6 +795,77 @@ test "parse: diagnostic location" {
}
}
test "parse: compatibility handler" {
const testing = std.testing;
var data: struct {
a: bool = false,
_arena: ?ArenaAllocator = null,
pub const compatibility: std.StaticStringMap(
CompatibilityHandler(@This()),
) = .initComptime(&.{
.{ "a", compat },
});
fn compat(
self: *@This(),
alloc: Allocator,
key: []const u8,
value: ?[]const u8,
) bool {
_ = alloc;
if (std.mem.eql(u8, key, "a")) {
if (value) |v| {
if (mem.eql(u8, v, "yuh")) {
self.a = true;
return true;
}
}
}
return false;
}
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
testing.allocator,
"--a=yuh",
);
defer iter.deinit();
try parse(@TypeOf(data), testing.allocator, &data, &iter);
try testing.expect(data._arena != null);
try testing.expect(data.a);
}
test "parse: compatibility renamed" {
const testing = std.testing;
var data: struct {
a: bool = false,
b: bool = false,
_arena: ?ArenaAllocator = null,
pub const compatibility: std.StaticStringMap(
CompatibilityHandler(@This()),
) = .initComptime(&.{
.{ "old", compatibilityRenamed(@This(), "a") },
});
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
testing.allocator,
"--old=true --b=true",
);
defer iter.deinit();
try parse(@TypeOf(data), testing.allocator, &data, &iter);
try testing.expect(data._arena != null);
try testing.expect(data.a);
try testing.expect(data.b);
}
test "parseIntoField: ignore underscore-prefixed fields" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
@ -1176,24 +1290,6 @@ test "parseIntoField: tagged union missing tag" {
);
}
test "parseIntoField: renamed field" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct {
a: []const u8,
const renamed = std.StaticStringMap([]const u8).initComptime(&.{
.{ "old", "a" },
});
} = undefined;
try parseIntoField(@TypeOf(data), alloc, &data, "old", "42");
try testing.expectEqualStrings("42", data.a);
}
/// An iterator that considers its location to be CLI args. It
/// iterates through an underlying iterator and increments a counter
/// to track the current CLI arg index.

View File

@ -176,7 +176,7 @@ const Boo = struct {
pub fn run(gpa: Allocator) !u8 {
// Disable on non-desktop systems.
switch (builtin.os.tag) {
.windows, .macos, .linux => {},
.windows, .macos, .linux, .freebsd => {},
else => return 1,
}

159
src/cli/edit_config.zig Normal file
View File

@ -0,0 +1,159 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const args = @import("args.zig");
const Allocator = std.mem.Allocator;
const Action = @import("action.zig").Action;
const configpkg = @import("../config.zig");
const internal_os = @import("../os/main.zig");
const Config = configpkg.Config;
pub const Options = struct {
pub fn deinit(self: Options) void {
_ = self;
}
/// Enables `-h` and `--help` to work.
pub fn help(self: Options) !void {
_ = self;
return Action.help_error;
}
};
/// The `edit-config` command opens the Ghostty configuration file in the
/// editor specified by the `$VISUAL` or `$EDITOR` environment variables.
///
/// IMPORTANT: This command will not reload the configuration after
/// editing. You will need to manually reload the configuration using the
/// application menu, configured keybind, or by restarting Ghostty. We
/// plan to auto-reload in the future, but Ghostty isn't capable of
/// this yet.
///
/// The filepath opened is the default user-specific configuration
/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`.
/// On macOS, this may also be located at
/// `~/Library/Application Support/com.mitchellh.ghostty/config`.
/// On macOS, whichever path exists and is non-empty will be prioritized,
/// prioritizing the Application Support directory if neither are
/// non-empty.
///
/// This command prefers the `$VISUAL` environment variable over `$EDITOR`,
/// if both are set. If neither are set, it will print an error
/// and exit.
pub fn run(alloc: Allocator) !u8 {
// Implementation note (by @mitchellh): I do proper memory cleanup
// throughout this command, even though we plan on doing `exec`.
// I do this out of good hygiene in case we ever change this to
// not using `exec` anymore and because this command isn't performance
// critical where setting up the defer cleanup is a problem.
const stderr = std.io.getStdErr().writer();
var opts: Options = .{};
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
// We load the configuration once because that will write our
// default configuration files to disk. We don't use the config.
var config = try Config.load(alloc);
defer config.deinit();
// Find the preferred path.
const path = try Config.preferredDefaultFilePath(alloc);
defer alloc.free(path);
// We don't currently support Windows because we use the exec syscall.
if (comptime builtin.os.tag == .windows) {
try stderr.print(
\\The `ghostty +edit-config` command is not supported on Windows.
\\Please edit the configuration file manually at the following path:
\\
\\{s}
\\
,
.{path},
);
return 1;
}
// Get our editor
const get_env_: ?internal_os.GetEnvResult = env: {
// VISUAL vs. EDITOR: https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference
if (try internal_os.getenv(alloc, "VISUAL")) |v| {
if (v.value.len > 0) break :env v;
v.deinit(alloc);
}
if (try internal_os.getenv(alloc, "EDITOR")) |v| {
if (v.value.len > 0) break :env v;
v.deinit(alloc);
}
break :env null;
};
defer if (get_env_) |v| v.deinit(alloc);
const editor: []const u8 = if (get_env_) |v| v.value else "";
// If we don't have `$EDITOR` set then we can't do anything
// but we can still print a helpful message.
if (editor.len == 0) {
try stderr.print(
\\The $EDITOR or $VISUAL environment variable is not set or is empty.
\\This environment variable is required to edit the Ghostty configuration
\\via this CLI command.
\\
\\Please set the environment variable to your preferred terminal
\\text editor and try again.
\\
\\If you prefer to edit the configuration file another way,
\\you can find the configuration file at the following path:
\\
\\
,
.{},
);
// Output the path using the OSC8 sequence so that it is linked.
try stderr.print(
"\x1b]8;;file://{s}\x1b\\{s}\x1b]8;;\x1b\\\n",
.{ path, path },
);
return 1;
}
// We require libc because we want to use std.c.environ for envp
// and not have to build that ourselves. We can remove this
// limitation later but Ghostty already heavily requires libc
// so this is not a big deal.
comptime assert(builtin.link_libc);
const editorZ = try alloc.dupeZ(u8, editor);
defer alloc.free(editorZ);
const pathZ = try alloc.dupeZ(u8, path);
defer alloc.free(pathZ);
const err = std.posix.execvpeZ(
editorZ,
&.{ editorZ, pathZ },
std.c.environ,
);
// If we reached this point then exec failed.
try stderr.print(
\\Failed to execute the editor. Error code={}.
\\
\\This is usually due to the executable path not existing, invalid
\\permissions, or the shell environment not being set up
\\correctly.
\\
\\Editor: {s}
\\Path: {s}
\\
, .{ err, editor, path });
return 1;
}

View File

@ -115,7 +115,8 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
const stderr = std.io.getStdErr().writer();
const stdout = std.io.getStdOut().writer();
if (global_state.resources_dir == null)
const resources_dir = global_state.resources_dir.app();
if (resources_dir == null)
try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
"that Ghostty is installed correctly.\n", .{});

View File

@ -20,6 +20,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
pub const FontShapingBreak = Config.FontShapingBreak;
pub const FontStyle = Config.FontStyle;
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
pub const Keybinds = Config.Keybinds;
@ -31,8 +32,11 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation;
pub const RepeatableString = Config.RepeatableString;
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
pub const RepeatablePath = Config.RepeatablePath;
pub const Path = Config.Path;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowPaddingColor = Config.WindowPaddingColor;
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
pub const BackgroundImageFit = Config.BackgroundImageFit;
// Alternate APIs
pub const CAPI = @import("config/CAPI.zig");

View File

@ -46,14 +46,22 @@ const c = @cImport({
@cInclude("unistd.h");
});
/// Renamed fields, used by cli.parse
pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
pub const compatibility = std.StaticStringMap(
cli.CompatibilityHandler(Config),
).initComptime(&.{
// Ghostty 1.1 introduced background-blur support for Linux which
// doesn't support a specific radius value. The renaming is to let
// one field be used for both platforms (macOS retained the ability
// to set a radius).
.{ "background-blur-radius", "background-blur" },
.{ "adw-toolbar-style", "gtk-toolbar-style" },
.{ "background-blur-radius", cli.compatibilityRenamed(Config, "background-blur") },
// Ghostty 1.2 renamed all our adw options to gtk because we now have
// a hard dependency on libadwaita.
.{ "adw-toolbar-style", cli.compatibilityRenamed(Config, "gtk-toolbar-style") },
// Ghostty 1.2 removed the `hidden` value from `gtk-tabs-location` and
// moved it to `window-show-tab-bar`.
.{ "gtk-tabs-location", compatGtkTabsLocation },
});
/// The font families to use.
@ -262,6 +270,32 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This is currently only supported on macOS.
@"font-thicken-strength": u8 = 255,
/// Locations to break font shaping into multiple runs.
///
/// A "run" is a contiguous segment of text that is shaped together. "Shaping"
/// is the process of converting text (codepoints) into glyphs (renderable
/// characters). This is how ligatures are formed, among other things.
/// For example, if a coding font turns "!=" into a single glyph, then it
/// must see "!" and "=" next to each other in a single run. When a run
/// is broken, the text is shaped separately. To continue our example, if
/// "!" is at the end of one run and "=" is at the start of the next run,
/// then the ligature will not be formed.
///
/// Ghostty breaks runs at certain points to improve readability or usability.
/// For example, Ghostty by default will break runs under the cursor so that
/// text editing can see the individual characters rather than a ligature.
/// This configuration lets you configure this behavior.
///
/// Combine values with a comma to set multiple options. Prefix an
/// option with "no-" to disable it. Enabling and disabling options
/// can be done at the same time.
///
/// Available options:
///
/// * `cursor` - Break runs under the cursor.
///
@"font-shaping-break": FontShapingBreak = .{},
/// What color space to use when performing alpha blending.
///
/// This affects the appearance of text and of any images with transparency.
@ -466,6 +500,93 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// Background image for the terminal.
///
/// This should be a path to a PNG or JPEG file, other image formats are
/// not yet supported.
///
/// The background image is currently per-terminal, not per-window. If
/// you are a heavy split user, the background image will be repeated across
/// splits. A future improvement to Ghostty will address this.
///
/// WARNING: Background images are currently duplicated in VRAM per-terminal.
/// For sufficiently large images, this could lead to a large increase in
/// memory usage (specifically VRAM usage). A future Ghostty improvement
/// will resolve this by sharing image textures across terminals.
@"background-image": ?Path = null,
/// Background image opacity.
///
/// This is relative to the value of `background-opacity`.
///
/// A value of `1.0` (the default) will result in the background image being
/// placed on top of the general background color, and then the combined result
/// will be adjusted to the opacity specified by `background-opacity`.
///
/// A value less than `1.0` will result in the background image being mixed
/// with the general background color before the combined result is adjusted
/// to the configured `background-opacity`.
///
/// A value greater than `1.0` will result in the background image having a
/// higher opacity than the general background color. For instance, if the
/// configured `background-opacity` is `0.5` and `background-image-opacity`
/// is set to `1.5`, then the final opacity of the background image will be
/// `0.5 * 1.5 = 0.75`.
@"background-image-opacity": f32 = 1.0,
/// Background image position.
///
/// Valid values are:
/// * `top-left`
/// * `top-center`
/// * `top-right`
/// * `center-left`
/// * `center`
/// * `center-right`
/// * `bottom-left`
/// * `bottom-center`
/// * `bottom-right`
///
/// The default value is `center`.
@"background-image-position": BackgroundImagePosition = .center,
/// Background image fit.
///
/// Valid values are:
///
/// * `contain`
///
/// Preserving the aspect ratio, scale the background image to the largest
/// size that can still be contained within the terminal, so that the whole
/// image is visible.
///
/// * `cover`
///
/// Preserving the aspect ratio, scale the background image to the smallest
/// size that can completely cover the terminal. This may result in one or
/// more edges of the image being clipped by the edge of the terminal.
///
/// * `stretch`
///
/// Stretch the background image to the full size of the terminal, without
/// preserving the aspect ratio.
///
/// * `none`
///
/// Don't scale the background image.
///
/// The default value is `contain`.
@"background-image-fit": BackgroundImageFit = .contain,
/// Whether to repeat the background image or not.
///
/// If this is set to true, the background image will be repeated if there
/// would otherwise be blank space around it because it doesn't completely
/// fill the terminal area.
///
/// The default value is `false`.
@"background-image-repeat": bool = false,
/// The foreground and background color for selection. If this is not set, then
/// the selection color is just the inverted window background and foreground
/// (note: not to be confused with the cell bg/fg).
@ -942,12 +1063,17 @@ title: ?[:0]const u8 = null,
/// The setting that will change the application class value.
///
/// This controls the class field of the `WM_CLASS` X11 property (when running
/// under X11), and the Wayland application ID (when running under Wayland).
/// under X11), the Wayland application ID (when running under Wayland), and the
/// bus name that Ghostty uses to connect to DBus.
///
/// Note that changing this value between invocations will create new, separate
/// instances, of Ghostty when running with `gtk-single-instance=true`. See that
/// option for more details.
///
/// Changing this value may break launching Ghostty from `.desktop` files, via
/// DBus activation, or systemd user services as the system is expecting Ghostty
/// to connect to DBus using the default `class` when it is launched.
///
/// The class name must follow the requirements defined [in the GTK
/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html).
///
@ -1494,6 +1620,27 @@ keybind: Keybinds = .{},
/// * `end` - Insert the new tab at the end of the tab list.
@"window-new-tab-position": WindowNewTabPosition = .current,
/// Whether to show the tab bar.
///
/// Valid values:
///
/// - `always`
///
/// Always display the tab bar, even when there's only one tab.
///
/// - `auto` *(default)*
///
/// Automatically show and hide the tab bar. The tab bar is only
/// shown when there are two or more tabs present.
///
/// - `never`
///
/// Never show the tab bar. Tabs are only accessible via the tab
/// overview or by keybind actions.
///
/// Currently only supported on Linux (GTK).
@"window-show-tab-bar": WindowShowTabBar = .auto,
/// Background color for the window titlebar. This only takes effect if
/// window-theme is set to ghostty. Currently only supported in the GTK app
/// runtime.
@ -1975,6 +2122,28 @@ keybind: Keybinds = .{},
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`
@"shell-integration-features": ShellIntegrationFeatures = .{},
/// Custom entries into the command palette.
///
/// Each entry requires the title, the corresponding action, and an optional
/// description. Each field should be prefixed with the field name, a colon
/// (`:`), and then the specified value. The syntax for actions is identical
/// to the one for keybind actions. Whitespace in between fields is ignored.
///
/// ```ini
/// command-palette-entry = title:Reset Font Style, action:csi:0m
/// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main
/// ```
///
/// By default, the command palette is preloaded with most actions that might
/// be useful in an interactive setting yet do not have easily accessible or
/// memorizable shortcuts. The default entries can be cleared by setting this
/// setting to an empty value:
///
/// ```ini
/// command-palette-entry =
/// ```
@"command-palette-entry": RepeatableCommand = .{},
/// Sets the reporting format for OSC sequences that request color information.
/// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and
/// OSC 4 (256 color palette) queries, and by default the reported values
@ -2009,9 +2178,59 @@ keybind: Keybinds = .{},
/// causing the window to be completely black. If this happens, you can
/// unset this configuration to disable the shader.
///
/// The shader API is identical to the Shadertoy API: you specify a `mainImage`
/// function and the available uniforms match Shadertoy. The iChannel0 uniform
/// is a texture containing the rendered terminal screen.
/// Custom shader support is based on and compatible with the Shadertoy shaders.
/// Shaders should specify a `mainImage` function and the available uniforms
/// largely match Shadertoy, with some caveats and Ghostty-specific extensions.
///
/// The uniform values available to shaders are as follows:
///
/// * `sampler2D iChannel0` - Input texture.
///
/// A texture containing the current terminal screen. If multiple custom
/// shaders are specified, the output of previous shaders is written to
/// this texture, to allow combining multiple effects.
///
/// * `vec3 iResolution` - Output texture size, `[width, height, 1]` (in px).
///
/// * `float iTime` - Time in seconds since first frame was rendered.
///
/// * `float iTimeDelta` - Time in seconds since previous frame was rendered.
///
/// * `float iFrameRate` - Average framerate. (NOT CURRENTLY SUPPORTED)
///
/// * `int iFrame` - Number of frames that have been rendered so far.
///
/// * `float iChannelTime[4]` - Current time for video or sound input. (N/A)
///
/// * `vec3 iChannelResolution[4]` - Resolutions of the 4 input samplers.
///
/// Currently only `iChannel0` exists, and `iChannelResolution[0]` is
/// identical to `iResolution`.
///
/// * `vec4 iMouse` - Mouse input info. (NOT CURRENTLY SUPPORTED)
///
/// * `vec4 iDate` - Date/time info. (NOT CURRENTLY SUPPORTED)
///
/// * `float iSampleRate` - Sample rate for audio. (N/A)
///
/// Ghostty-specific extensions:
///
/// * `vec4 iCurrentCursor` - Info about the terminal cursor.
///
/// - `iCurrentCursor.xy` is the -X, +Y corner of the current cursor.
/// - `iCurrentCursor.zw` is the width and height of the current cursor.
///
/// * `vec4 iPreviousCursor` - Info about the previous terminal cursor.
///
/// * `vec4 iCurrentCursorColor` - Color of the terminal cursor.
///
/// * `vec4 iPreviousCursorColor` - Color of the previous terminal cursor.
///
/// * `float iTimeCursorChange` - Timestamp of terminal cursor change.
///
/// When the terminal cursor changes position or color, this is set to
/// the same time as the `iTime` uniform, allowing you to compute the
/// time since the change by subtracting this from `iTime`.
///
/// If the shader fails to compile, the shader will be ignored. Any errors
/// related to shader compilation will not show up as configuration errors
@ -2734,6 +2953,9 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Add our default keybindings
try result.keybind.init(alloc);
// Add our default command palette entries
try result.@"command-palette-entry".init(alloc);
// Add our default link for URL detection
try result.link.links.append(alloc, .{
.regex = url.regex,
@ -2759,24 +2981,20 @@ pub fn loadIter(
/// `path` must be resolved and absolute.
pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
assert(std.fs.path.isAbsolute(path));
var file = try std.fs.openFileAbsolute(path, .{});
defer file.close();
const stat = try file.stat();
switch (stat.kind) {
.file => {},
else => |kind| {
log.warn("config-file {s}: not reading because file type is {s}", .{
path,
@tagName(kind),
});
var file = openFile(path) catch |err| switch (err) {
error.NotAFile => {
log.warn(
"config-file {s}: not reading because it is not a file",
.{path},
);
return;
},
}
else => return err,
};
defer file.close();
std.log.info("reading configuration file path={s}", .{path});
var buf_reader = std.io.bufferedReader(file.reader());
const reader = buf_reader.reader();
const Iter = cli.args.LineIterator(@TypeOf(reader));
@ -2831,13 +3049,13 @@ fn writeConfigTemplate(path: []const u8) !void {
/// is also loaded.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
// Load XDG first
const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
const xdg_path = try defaultXdgPath(alloc);
defer alloc.free(xdg_path);
const xdg_action = self.loadOptionalFile(alloc, xdg_path);
// On macOS load the app support directory as well
if (comptime builtin.os.tag == .macos) {
const app_support_path = try internal_os.macos.appSupportDir(alloc, "config");
const app_support_path = try defaultAppSupportPath(alloc);
defer alloc.free(app_support_path);
const app_support_action = self.loadOptionalFile(alloc, app_support_path);
@ -2857,6 +3075,102 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
}
}
/// Default path for the XDG home configuration file. Returned value
/// must be freed by the caller.
fn defaultXdgPath(alloc: Allocator) ![]const u8 {
return try internal_os.xdg.config(
alloc,
.{ .subdir = "ghostty/config" },
);
}
/// Default path for the macOS Application Support configuration file.
/// Returned value must be freed by the caller.
fn defaultAppSupportPath(alloc: Allocator) ![]const u8 {
return try internal_os.macos.appSupportDir(alloc, "config");
}
/// Returns the path to the preferred default configuration file.
/// This is the file where users should place their configuration.
///
/// This doesn't create or populate the file with any default
/// contents; downstream callers must handle this.
///
/// The returned value must be freed by the caller.
pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 {
switch (builtin.os.tag) {
.macos => {
// macOS prefers the Application Support directory
// if it exists.
const app_support_path = try defaultAppSupportPath(alloc);
if (openFile(app_support_path)) |f| {
f.close();
return app_support_path;
} else |_| {}
// Try the XDG path if it exists
const xdg_path = try defaultXdgPath(alloc);
if (openFile(xdg_path)) |f| {
f.close();
alloc.free(app_support_path);
return xdg_path;
} else |_| {}
defer alloc.free(xdg_path);
// Neither exist, use app support
return app_support_path;
},
// All other platforms use XDG only
else => return try defaultXdgPath(alloc),
}
}
const OpenFileError = error{
FileNotFound,
FileIsEmpty,
FileOpenFailed,
NotAFile,
};
/// Opens the file at the given path and returns the file handle
/// if it exists and is non-empty. This also constrains the possible
/// errors to a smaller set that we can explicitly handle.
fn openFile(path: []const u8) OpenFileError!std.fs.File {
assert(std.fs.path.isAbsolute(path));
var file = std.fs.openFileAbsolute(
path,
.{},
) catch |err| switch (err) {
error.FileNotFound => return OpenFileError.FileNotFound,
else => {
log.warn("unexpected file open error path={s} err={}", .{
path,
err,
});
return OpenFileError.FileOpenFailed;
},
};
errdefer file.close();
const stat = file.stat() catch |err| {
log.warn("error getting file stat path={s} err={}", .{
path,
err,
});
return OpenFileError.FileOpenFailed;
};
switch (stat.kind) {
.file => {},
else => return OpenFileError.NotAFile,
}
if (stat.size == 0) return OpenFileError.FileIsEmpty;
return file;
}
/// Load and parse the CLI args.
pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
switch (builtin.os.tag) {
@ -3156,6 +3470,15 @@ fn expandPaths(self: *Config, base: []const u8) !void {
&self._diagnostics,
);
},
?RepeatablePath, ?Path => {
if (@field(self, field.name)) |*path| {
try path.expand(
arena_alloc,
base,
&self._diagnostics,
);
}
},
else => {},
}
}
@ -3503,6 +3826,27 @@ pub fn parseManuallyHook(
return true;
}
/// parseFieldManuallyFallback is a fallback called only when
/// parsing the field directly failed. It can be used to implement
/// backward compatibility. Since this is only called when parsing
/// fails, it doesn't impact happy-path performance.
fn compatGtkTabsLocation(
self: *Config,
alloc: Allocator,
key: []const u8,
value: ?[]const u8,
) bool {
_ = alloc;
assert(std.mem.eql(u8, key, "gtk-tabs-location"));
if (std.mem.eql(u8, value orelse "", "hidden")) {
self.@"window-show-tab-bar" = .never;
return true;
}
return false;
}
/// Create a shallow copy of this config. This will share all the memory
/// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit`
@ -4714,6 +5058,12 @@ pub const Keybinds = struct {
.{ .reset_font_size = {} },
);
try self.set.put(
alloc,
.{ .key = .{ .unicode = 'j' }, .mods = .{ .shift = true, .ctrl = true, .super = true } },
.{ .write_screen_file = .copy },
);
try self.set.put(
alloc,
.{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
@ -4820,25 +5170,29 @@ pub const Keybinds = struct {
.{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } },
.{ .close_tab = {} },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } },
.{ .previous_tab = {} },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } },
.{ .next_tab = {} },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } },
.{ .previous_tab = {} },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } },
.{ .next_tab = {} },
.{ .performable = true },
);
try self.set.put(
alloc,
@ -4850,57 +5204,67 @@ pub const Keybinds = struct {
.{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_split = .down },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } },
.{ .goto_split = .previous },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } },
.{ .goto_split = .next },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .up },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .down },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .left },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .right },
.{ .performable = true },
);
// Resizing splits
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .up, 10 } },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .down, 10 } },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .left, 10 } },
.{ .performable = true },
);
try self.set.put(
try self.set.putFlags(
alloc,
.{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .right, 10 } },
.{ .performable = true },
);
// Viewport scrolling
@ -4971,22 +5335,24 @@ pub const Keybinds = struct {
const end: u21 = '8';
var i: u21 = start;
while (i <= end) : (i += 1) {
try self.set.put(
try self.set.putFlags(
alloc,
.{
.key = .{ .unicode = i },
.mods = mods,
},
.{ .goto_tab = (i - start) + 1 },
.{ .performable = true },
);
}
try self.set.put(
try self.set.putFlags(
alloc,
.{
.key = .{ .unicode = '9' },
.mods = mods,
},
.{ .last_tab = {} },
.{ .performable = true },
);
}
@ -5874,6 +6240,11 @@ pub const FontSyntheticStyle = packed struct {
@"bold-italic": bool = true,
};
/// See "font-shaping-break" for documentation
pub const FontShapingBreak = packed struct {
cursor: bool = true,
};
/// See "link" for documentation.
pub const RepeatableLink = struct {
const Self = @This();
@ -5956,6 +6327,150 @@ pub const ShellIntegrationFeatures = packed struct {
title: bool = true,
};
pub const RepeatableCommand = struct {
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
pub fn init(self: *RepeatableCommand, alloc: Allocator) !void {
self.value = .empty;
try self.value.appendSlice(alloc, inputpkg.command.defaults);
}
pub fn parseCLI(
self: *RepeatableCommand,
alloc: Allocator,
input_: ?[]const u8,
) !void {
// Unset or empty input clears the list
const input = input_ orelse "";
if (input.len == 0) {
self.value.clearRetainingCapacity();
return;
}
const cmd = try cli.args.parseAutoStruct(
inputpkg.Command,
alloc,
input,
);
try self.value.append(alloc, cmd);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand {
const value = try self.value.clone(alloc);
for (value.items) |*item| {
item.* = try item.clone(alloc);
}
return .{ .value = value };
}
/// Compare if two of our value are equal. Required by Config.
pub fn equal(self: RepeatableCommand, other: RepeatableCommand) bool {
if (self.value.items.len != other.value.items.len) return false;
for (self.value.items, other.value.items) |a, b| {
if (!a.equal(b)) return false;
}
return true;
}
/// Used by Formatter
pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void {
if (self.value.items.len == 0) {
try formatter.formatEntry(void, {});
return;
}
var buf: [4096]u8 = undefined;
for (self.value.items) |item| {
const str = if (item.description.len > 0) std.fmt.bufPrint(
&buf,
"title:{s},description:{s},action:{}",
.{ item.title, item.description, item.action },
) else std.fmt.bufPrint(
&buf,
"title:{s},action:{}",
.{ item.title, item.action },
);
try formatter.formatEntry([]const u8, str catch return error.OutOfMemory);
}
}
test "RepeatableCommand parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Foo,action:ignore");
try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5");
try testing.expectEqual(@as(usize, 3), list.value.items.len);
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
try testing.expectEqualStrings("Foo", list.value.items[0].title);
try testing.expect(list.value.items[1].action == .text);
try testing.expectEqualStrings("ale bydle", list.value.items[1].action.text);
try testing.expectEqualStrings("Bar", list.value.items[1].title);
try testing.expectEqualStrings("bobr", list.value.items[1].description);
try testing.expectEqual(
inputpkg.Binding.Action{ .increase_font_size = 2.5 },
list.value.items[2].action,
);
try testing.expectEqualStrings("Quux", list.value.items[2].title);
try testing.expectEqualStrings("boo", list.value.items[2].description);
try list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.value.items.len);
}
test "RepeatableCommand formatConfig empty" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var list: RepeatableCommand = .{};
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = \n", buf.items);
}
test "RepeatableCommand formatConfig single item" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:Bober");
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:Bober\n", buf.items);
}
test "RepeatableCommand formatConfig multiple items" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:kurwa");
try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle");
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:kurwa\na = title:Ja,description:pierdole,action:text:jakie bydle\n", buf.items);
}
};
/// OSC 4, 10, 11, and 12 default color reporting format.
pub const OSCColorReportFormat = enum {
none,
@ -6048,7 +6563,6 @@ pub const GtkSingleInstance = enum {
pub const GtkTabsLocation = enum {
top,
bottom,
hidden,
};
/// See gtk-toolbar-style
@ -6099,6 +6613,13 @@ pub const WindowNewTabPosition = enum {
end,
};
/// See window-show-tab-bar
pub const WindowShowTabBar = enum {
always,
auto,
never,
};
/// See resize-overlay
pub const ResizeOverlay = enum {
always,
@ -6411,6 +6932,28 @@ pub const AlphaBlending = enum {
}
};
/// See background-image-position
pub const BackgroundImagePosition = enum {
@"top-left",
@"top-center",
@"top-right",
@"center-left",
@"center-center",
@"center-right",
@"bottom-left",
@"bottom-center",
@"bottom-right",
center,
};
/// See background-image-fit
pub const BackgroundImageFit = enum {
contain,
cover,
stretch,
none,
};
/// See freetype-load-flag
pub const FreetypeLoadFlags = packed struct {
// The defaults here at the time of writing this match the defaults

View File

@ -20,10 +20,10 @@ pub fn open(alloc_gpa: Allocator) !void {
// Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
const alloc = arena.allocator();
const alloc_arena = arena.allocator();
// Get the path we should open
const config_path = try configPath(alloc);
const config_path = try configPath(alloc_arena);
// Create config directory recursively.
if (std.fs.path.dirname(config_path)) |config_dir| {
@ -41,7 +41,7 @@ pub fn open(alloc_gpa: Allocator) !void {
}
};
try internal_os.open(alloc, .text, config_path);
try internal_os.open(alloc_gpa, .text, config_path);
}
/// Returns the config path to use for open for the current OS.

View File

@ -56,7 +56,7 @@ pub const Location = enum {
},
.resources => try std.fs.path.join(arena_alloc, &.{
global_state.resources_dir orelse return null,
global_state.resources_dir.app() orelse return null,
"themes",
}),
};

View File

@ -0,0 +1,44 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
/// A collection of ArrayLists with methods for bulk operations.
pub fn ArrayListCollection(comptime T: type) type {
return struct {
const Self = ArrayListCollection(T);
const ArrayListT = std.ArrayListUnmanaged(T);
// An array containing the lists that belong to this collection.
lists: []ArrayListT,
// The collection will be initialized with empty ArrayLists.
pub fn init(
alloc: Allocator,
list_count: usize,
initial_capacity: usize,
) Allocator.Error!Self {
const self: Self = .{
.lists = try alloc.alloc(ArrayListT, list_count),
};
for (self.lists) |*list| {
list.* = try .initCapacity(alloc, initial_capacity);
}
return self;
}
pub fn deinit(self: *Self, alloc: Allocator) void {
for (self.lists) |*list| {
list.deinit(alloc);
}
alloc.free(self.lists);
}
/// Clear all lists in the collection, retaining capacity.
pub fn reset(self: *Self) void {
for (self.lists) |*list| {
list.clearRetainingCapacity();
}
}
};
}

102
src/file_type.zig Normal file
View File

@ -0,0 +1,102 @@
const std = @import("std");
const type_details: []const struct {
typ: FileType,
sigs: []const []const ?u8,
exts: []const []const u8,
} = &.{
.{
.typ = .jpeg,
.sigs = &.{
&.{ 0xFF, 0xD8, 0xFF, 0xDB },
&.{ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01 },
&.{ 0xFF, 0xD8, 0xFF, 0xEE },
&.{ 0xFF, 0xD8, 0xFF, 0xE1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 },
&.{ 0xFF, 0xD8, 0xFF, 0xE0 },
},
.exts = &.{ ".jpg", ".jpeg", ".jfif" },
},
.{
.typ = .png,
.sigs = &.{&.{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }},
.exts = &.{".png"},
},
.{
.typ = .gif,
.sigs = &.{
&.{ 'G', 'I', 'F', '8', '7', 'a' },
&.{ 'G', 'I', 'F', '8', '9', 'a' },
},
.exts = &.{".gif"},
},
.{
.typ = .bmp,
.sigs = &.{&.{ 'B', 'M' }},
.exts = &.{".bmp"},
},
.{
.typ = .qoi,
.sigs = &.{&.{ 'q', 'o', 'i', 'f' }},
.exts = &.{".qoi"},
},
.{
.typ = .webp,
.sigs = &.{
&.{ 0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50 },
},
.exts = &.{".webp"},
},
};
/// This is a helper for detecting file types based on magic bytes.
///
/// Ref: https://en.wikipedia.org/wiki/List_of_file_signatures
pub const FileType = enum {
/// JPEG image file.
jpeg,
/// PNG image file.
png,
/// GIF image file.
gif,
/// BMP image file.
bmp,
/// QOI image file.
qoi,
/// WebP image file.
webp,
/// Unknown file format.
unknown,
/// Detect file type based on the magic bytes
/// at the start of the provided file contents.
pub fn detect(contents: []const u8) FileType {
inline for (type_details) |typ| {
inline for (typ.sigs) |signature| {
if (contents.len >= signature.len) {
for (contents[0..signature.len], signature) |f, sig| {
if (sig) |s| if (f != s) break;
} else {
return typ.typ;
}
}
}
}
return .unknown;
}
/// Guess file type from its extension.
pub fn guessFromExtension(extension: []const u8) FileType {
inline for (type_details) |typ| {
inline for (typ.exts) |ext| {
if (std.ascii.eqlIgnoreCase(extension, ext)) return typ.typ;
}
}
return .unknown;
}
};

View File

@ -50,15 +50,18 @@ modified: std.atomic.Value(usize) = .{ .raw = 0 },
resized: std.atomic.Value(usize) = .{ .raw = 0 },
pub const Format = enum(u8) {
/// 1 byte per pixel grayscale.
grayscale = 0,
rgb = 1,
rgba = 2,
/// 3 bytes per pixel BGR.
bgr = 1,
/// 4 bytes per pixel BGRA.
bgra = 2,
pub fn depth(self: Format) u8 {
return switch (self) {
.grayscale => 1,
.rgb => 3,
.rgba => 4,
.bgr => 3,
.bgra => 4,
};
}
};
@ -303,7 +306,12 @@ pub fn clear(self: *Atlas) void {
}
/// Dump the atlas as a PPM to a writer, for debug purposes.
/// Only supports grayscale and rgb atlases.
/// Only supports grayscale and bgr atlases.
///
/// NOTE: BGR atlases will have the red and blue channels
/// swapped because PPM expects RGB. This would be
/// easy enough to fix so next time someone needs
/// to debug a color atlas they should fix it.
pub fn dump(self: Atlas, writer: anytype) !void {
try writer.print(
\\P{c}
@ -313,7 +321,7 @@ pub fn dump(self: Atlas, writer: anytype) !void {
, .{
@as(u8, switch (self.format) {
.grayscale => '5',
.rgb => '6',
.bgr => '6',
else => {
log.err("Unsupported format for dump: {}", .{self.format});
@panic("Cannot dump this atlas format.");
@ -418,8 +426,16 @@ pub const Wasm = struct {
// We need to draw pixels so this is format dependent.
const buf: []u8 = switch (self.format) {
// RGBA is the native ImageData format
.rgba => self.data,
.bgra => buf: {
// Convert from BGRA to RGBA by swapping every R and B.
var buf: []u8 = try alloc.dupe(u8, self.data);
errdefer alloc.free(buf);
var i: usize = 0;
while (i < self.data.len) : (i += 4) {
std.mem.swap(u8, &buf[i], &buf[i + 2]);
}
break :buf buf;
},
.grayscale => buf: {
// Convert from A8 to RGBA so every 4th byte is set to a value.
@ -572,12 +588,12 @@ test "grow" {
try testing.expectEqual(@as(u8, 4), atlas.data[atlas.size * 2 + 2]);
}
test "writing RGB data" {
test "writing BGR data" {
const alloc = testing.allocator;
var atlas = try init(alloc, 32, .rgb);
var atlas = try init(alloc, 32, .bgr);
defer atlas.deinit(alloc);
// This is RGB so its 3 bpp
// This is BGR so its 3 bpp
const reg = try atlas.reserve(alloc, 1, 2);
atlas.set(reg, &[_]u8{
1, 2, 3,
@ -594,18 +610,18 @@ test "writing RGB data" {
try testing.expectEqual(@as(u8, 6), atlas.data[65 * depth + 2]);
}
test "grow RGB" {
test "grow BGR" {
const alloc = testing.allocator;
// Atlas is 4x4 so its a 1px border meaning we only have 2x2 available
var atlas = try init(alloc, 4, .rgb);
var atlas = try init(alloc, 4, .bgr);
defer atlas.deinit(alloc);
// Get our 2x2, which should be ALL our usable space
const reg = try atlas.reserve(alloc, 2, 2);
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
// This is RGB so its 3 bpp
// This is BGR so its 3 bpp
atlas.set(reg, &[_]u8{
10, 11, 12, // (0, 0) (x, y) from top-left
13, 14, 15, // (1, 0)

View File

@ -40,7 +40,7 @@ const log = std.log.scoped(.font_shared_grid);
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{},
/// Cache for glyph renders into the atlas.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Render) = .{},
glyphs: std.HashMapUnmanaged(GlyphKey, Render, GlyphKey.Context, 80) = .{},
/// The texture atlas to store renders in. The Glyph data in the glyphs
/// cache is dependent on the atlas matching.
@ -79,7 +79,7 @@ pub fn init(
var atlas_grayscale = try Atlas.init(alloc, 512, .grayscale);
errdefer atlas_grayscale.deinit(alloc);
var atlas_color = try Atlas.init(alloc, 512, .rgba);
var atlas_color = try Atlas.init(alloc, 512, .bgra);
errdefer atlas_color.deinit(alloc);
var result: SharedGrid = .{
@ -307,6 +307,39 @@ const GlyphKey = struct {
index: Collection.Index,
glyph: u32,
opts: RenderOptions,
const Context = struct {
pub fn hash(_: Context, key: GlyphKey) u64 {
return @bitCast(Packed.from(key));
}
pub fn eql(_: Context, a: GlyphKey, b: GlyphKey) bool {
return Packed.from(a) == Packed.from(b);
}
};
const Packed = packed struct(u64) {
index: Collection.Index,
glyph: u32,
opts: packed struct(u16) {
cell_width: u2,
thicken: bool,
thicken_strength: u8,
_padding: u5 = 0,
},
inline fn from(key: GlyphKey) Packed {
return .{
.index = key.index,
.glyph = key.glyph,
.opts = .{
.cell_width = key.opts.cell_width orelse 0,
.thicken = key.opts.thicken,
.thicken_strength = key.opts.thicken_strength,
},
};
}
};
};
const TestMode = enum { normal };

View File

@ -391,7 +391,7 @@ pub const Face = struct {
const format: ?font.Atlas.Format = switch (bitmap_ft.pixel_mode) {
freetype.c.FT_PIXEL_MODE_MONO => null,
freetype.c.FT_PIXEL_MODE_GRAY => .grayscale,
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
freetype.c.FT_PIXEL_MODE_BGRA => .bgra,
else => {
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
@panic("unsupported pixel mode");
@ -925,7 +925,7 @@ test "color emoji" {
var lib = try Library.init(alloc);
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .rgba);
var atlas = try font.Atlas.init(alloc, 512, .bgra);
defer atlas.deinit(alloc);
var ft_font = try Face.init(
@ -973,14 +973,14 @@ test "color emoji" {
}
}
test "mono to rgba" {
test "mono to bgra" {
const alloc = testing.allocator;
const testFont = font.embedded.emoji;
var lib = try Library.init(alloc);
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .rgba);
var atlas = try font.Atlas.init(alloc, 512, .bgra);
defer atlas.deinit(alloc);
var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });

View File

@ -2,6 +2,9 @@ const builtin = @import("builtin");
const options = @import("main.zig").options;
const run = @import("shaper/run.zig");
const feature = @import("shaper/feature.zig");
const configpkg = @import("../config.zig");
const terminal = @import("../terminal/main.zig");
const SharedGrid = @import("main.zig").SharedGrid;
pub const noop = @import("shaper/noop.zig");
pub const harfbuzz = @import("shaper/harfbuzz.zig");
pub const coretext = @import("shaper/coretext.zig");
@ -61,6 +64,38 @@ pub const Options = struct {
features: []const []const u8 = &.{},
};
/// Options for runIterator.
pub const RunOptions = struct {
/// The font state for the terminal screen. This is mutable because
/// cached values may be updated during shaping.
grid: *SharedGrid,
/// The terminal screen to shape.
screen: *const terminal.Screen,
/// The row within the screen to shape. This row must exist within
/// screen; it is not validated.
row: terminal.Pin,
/// The selection boundaries. This is used to break shaping on
/// selection boundaries. This can be disabled by setting this to
/// null.
selection: ?terminal.Selection = null,
/// The cursor position within this row. This is used to break shaping
/// on cursor boundaries. This can be disabled by setting this to
/// null.
cursor_x: ?usize = null,
/// Apply the font break configuration to the run.
pub fn applyBreakConfig(
self: *RunOptions,
config: configpkg.FontShapingBreak,
) void {
if (!config.cursor) self.cursor_x = null;
}
};
test {
_ = Cache;
_ = Shaper;

View File

@ -7,6 +7,7 @@ const trace = @import("tracy").trace;
const font = @import("../main.zig");
const os = @import("../../os/main.zig");
const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const Feature = font.shape.Feature;
const FeatureList = font.shape.FeatureList;
const default_features = font.shape.default_features;
@ -288,19 +289,11 @@ pub const Shaper = struct {
pub fn runIterator(
self: *Shaper,
grid: *SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
opts: font.shape.RunOptions,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.grid = grid,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
.opts = opts,
};
}
@ -594,13 +587,11 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count);
@ -613,13 +604,11 @@ test "run iterator" {
try screen.testWriteString("ABCD EFG");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count);
@ -633,13 +622,11 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 3), count);
@ -654,13 +641,11 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 2), count);
@ -701,13 +686,11 @@ test "run iterator: empty cells with background set" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
{
const run = (try it.next(alloc)).?;
const cells = try shaper.shape(run);
@ -737,13 +720,11 @@ test "shape" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -772,13 +753,11 @@ test "shape nerd fonts" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -800,13 +779,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString(">=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -825,13 +802,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString("===");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -858,13 +833,11 @@ test "shape monaspace ligs" {
try screen.testWriteString("===");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -892,13 +865,11 @@ test "shape left-replaced lig in last run" {
try screen.testWriteString("!==");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -926,13 +897,11 @@ test "shape left-replaced lig in early run" {
try screen.testWriteString("!==X");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
const run = (try it.next(alloc)).?;
@ -957,13 +926,11 @@ test "shape U+3C9 with JB Mono" {
try screen.testWriteString("\u{03C9} foo");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var run_count: usize = 0;
var cell_count: usize = 0;
@ -990,13 +957,11 @@ test "shape emoji width" {
try screen.testWriteString("👍");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1040,13 +1005,11 @@ test "shape emoji width long" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1076,13 +1039,11 @@ test "shape variation selector VS15" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1111,13 +1072,11 @@ test "shape variation selector VS16" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1143,13 +1102,11 @@ test "shape with empty cells in between" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1181,13 +1138,11 @@ test "shape Chinese characters" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1221,13 +1176,11 @@ test "shape box glyphs" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1257,17 +1210,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -1280,17 +1232,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -1303,17 +1254,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -1326,17 +1276,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -1349,17 +1298,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -1385,13 +1333,11 @@ test "shape cursor boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1400,61 +1346,111 @@ test "shape cursor boundary" {
try testing.expectEqual(@as(usize, 1), count);
}
// Cursor at index 0 is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
// Cursor at index 0 is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 0,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
try testing.expectEqual(@as(usize, 2), count);
}
// Cursor at index 1 is three runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
// Cursor at index 1 is three runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 1,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 3), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
try testing.expectEqual(@as(usize, 3), count);
}
// Cursor at last col is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
9,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
// Cursor at last col is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 9,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
try testing.expectEqual(@as(usize, 2), count);
}
}
@ -1474,13 +1470,11 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1493,13 +1487,12 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 0,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1510,13 +1503,42 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 1,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1540,13 +1562,11 @@ test "shape cell attribute change" {
try screen.testWriteString(">=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1564,13 +1584,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1589,13 +1607,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1614,13 +1630,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1638,13 +1652,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1678,13 +1690,11 @@ test "shape high plane sprite font codepoint" {
try screen.testWriteString("\u{1FB70}");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
// We should get one run
const run = (try it.next(alloc)).?;
// The run state should have the UTF-16 encoding of the character.

View File

@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig");
const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const Feature = font.shape.Feature;
const FeatureList = font.shape.FeatureList;
const default_features = font.shape.default_features;
@ -89,19 +90,11 @@ pub const Shaper = struct {
/// and assume the y value matches.
pub fn runIterator(
self: *Shaper,
grid: *SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
opts: font.shape.RunOptions,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.grid = grid,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
.opts = opts,
};
}
@ -225,13 +218,11 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count);
@ -244,13 +235,11 @@ test "run iterator" {
try screen.testWriteString("ABCD EFG");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count);
@ -264,13 +253,11 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |_| {
count += 1;
@ -316,13 +303,11 @@ test "run iterator: empty cells with background set" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
{
const run = (try it.next(alloc)).?;
try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength());
@ -353,13 +338,11 @@ test "shape" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -382,13 +365,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString(">=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -407,13 +388,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString("===");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -440,13 +419,11 @@ test "shape monaspace ligs" {
try screen.testWriteString("===");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -476,13 +453,11 @@ test "shape arabic forced LTR" {
try screen.testWriteString(@embedFile("testdata/arabic.txt"));
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -513,13 +488,11 @@ test "shape emoji width" {
try screen.testWriteString("👍");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -565,13 +538,11 @@ test "shape emoji width long" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -603,13 +574,11 @@ test "shape variation selector VS15" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -640,13 +609,11 @@ test "shape variation selector VS16" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -674,13 +641,11 @@ test "shape with empty cells in between" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -712,13 +677,11 @@ test "shape Chinese characters" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -752,13 +715,11 @@ test "shape box glyphs" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -789,17 +750,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -812,17 +772,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -835,17 +794,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -858,17 +816,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -881,17 +838,16 @@ test "shape selection boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.selection = 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;
@ -917,13 +873,11 @@ test "shape cursor boundary" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -932,61 +886,111 @@ test "shape cursor boundary" {
try testing.expectEqual(@as(usize, 1), count);
}
// Cursor at index 0 is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
// Cursor at index 0 is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 0,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
try testing.expectEqual(@as(usize, 2), count);
}
// Cursor at index 1 is three runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
// Cursor at index 1 is three runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 1,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 3), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
try testing.expectEqual(@as(usize, 3), count);
}
// Cursor at last col is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
9,
);
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
// Cursor at last col is two runs
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 9,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 2), count);
}
// And without cursor splitting remains one
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
try testing.expectEqual(@as(usize, 2), count);
}
}
@ -1006,13 +1010,11 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1025,13 +1027,12 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
0,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 0,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1042,13 +1043,42 @@ test "shape cursor boundary and colored emoji" {
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
1,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
.cursor_x = 1,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1072,13 +1102,11 @@ test "shape cell attribute change" {
try screen.testWriteString(">=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1096,13 +1124,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1121,13 +1147,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1146,13 +1170,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;
@ -1170,13 +1192,11 @@ test "shape cell attribute change" {
try screen.testWriteString("=");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
var it = shaper.runIterator(.{
.grid = testdata.grid,
.screen = &screen,
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
});
var count: usize = 0;
while (try it.next(alloc)) |run| {
count += 1;

View File

@ -70,19 +70,11 @@ pub const Shaper = struct {
pub fn runIterator(
self: *Shaper,
grid: *SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
opts: font.shape.RunOptions,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.grid = grid,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
.opts = opts,
};
}

View File

@ -35,15 +35,11 @@ pub const TextRun = struct {
/// RunIterator is an iterator that yields text runs.
pub const RunIterator = struct {
hooks: font.Shaper.RunIteratorHook,
grid: *font.SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection = null,
cursor_x: ?usize = null,
opts: shape.RunOptions,
i: usize = 0,
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
const cells = self.row.cells(.all);
const cells = self.opts.row.cells(.all);
// Trim the right side of a row that might be empty
const max: usize = max: {
@ -58,7 +54,7 @@ pub const RunIterator = struct {
// Invisible cells don't have any glyphs rendered,
// so we explicitly skip them in the shaping process.
while (self.i < max and
self.row.style(&cells[self.i]).flags.invisible)
self.opts.row.style(&cells[self.i]).flags.invisible)
{
self.i += 1;
}
@ -76,7 +72,7 @@ pub const RunIterator = struct {
var hasher = Hasher.init(0);
// Let's get our style that we'll expect for the run.
const style = self.row.style(&cells[self.i]);
const style = self.opts.row.style(&cells[self.i]);
// Go through cell by cell and accumulate while we build our run.
var j: usize = self.i;
@ -86,9 +82,9 @@ pub const RunIterator = struct {
// If we have a selection and we're at a boundary point, then
// we break the run here.
if (self.selection) |unordered_sel| {
if (self.opts.selection) |unordered_sel| {
if (j > self.i) {
const sel = unordered_sel.ordered(self.screen, .forward);
const sel = unordered_sel.ordered(self.opts.screen, .forward);
const start_x = sel.start().x;
const end_x = sel.end().x;
@ -142,7 +138,7 @@ pub const RunIterator = struct {
// The style is different. We allow differing background
// styles but any other change results in a new run.
const c1 = comparableStyle(style);
const c2 = comparableStyle(self.row.style(&cells[j]));
const c2 = comparableStyle(self.opts.row.style(&cells[j]));
if (!c1.eql(c2)) break;
}
@ -162,7 +158,7 @@ pub const RunIterator = struct {
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.
const cps = self.row.grapheme(cell) orelse break :p null;
const cps = self.opts.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;
@ -186,7 +182,7 @@ pub const RunIterator = struct {
// joiners will show the joiners allowing you to modify the
// emoji.
if (!cell.hasGrapheme()) {
if (self.cursor_x) |cursor_x| {
if (self.opts.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
// exactly one iteration. Why exactly one? Because we may
@ -227,7 +223,7 @@ pub const RunIterator = struct {
// Otherwise we need a fallback character. Prefer the
// official replacement character.
if (try self.grid.getIndex(
if (try self.opts.grid.getIndex(
alloc,
0xFFFD, // replacement char
font_style,
@ -235,7 +231,7 @@ pub const RunIterator = struct {
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
// Fallback to space
if (try self.grid.getIndex(
if (try self.opts.grid.getIndex(
alloc,
' ',
font_style,
@ -273,7 +269,7 @@ pub const RunIterator = struct {
@intCast(cluster),
);
if (cell.hasGrapheme()) {
const cps = self.row.grapheme(cell).?;
const cps = self.opts.row.grapheme(cell).?;
for (cps) |cp| {
// Do not send presentation modifiers
if (cp == 0xFE0E or cp == 0xFE0F) continue;
@ -298,7 +294,7 @@ pub const RunIterator = struct {
.hash = hasher.final(),
.offset = @intCast(self.i),
.cells = @intCast(j - self.i),
.grid = self.grid,
.grid = self.opts.grid,
.font_index = current_font,
};
}
@ -326,7 +322,7 @@ pub const RunIterator = struct {
cell.codepoint() == 0 or
cell.codepoint() == terminal.kitty.graphics.unicode.placeholder)
{
return try self.grid.getIndex(
return try self.opts.grid.getIndex(
alloc,
' ',
style,
@ -336,7 +332,7 @@ pub const RunIterator = struct {
// Get the font index for the primary codepoint.
const primary_cp: u32 = cell.codepoint();
const primary = try self.grid.getIndex(
const primary = try self.opts.grid.getIndex(
alloc,
primary_cp,
style,
@ -349,7 +345,7 @@ pub const RunIterator = struct {
// If this is a grapheme, we need to find a font that supports
// all of the codepoints in the grapheme.
const cps = self.row.grapheme(cell) orelse return primary;
const cps = self.opts.row.grapheme(cell) orelse return primary;
var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1);
defer candidates.deinit();
candidates.appendAssumeCapacity(primary);
@ -365,7 +361,7 @@ pub const RunIterator = struct {
// to support the base presentation, since it is common for emoji
// fonts to support the base emoji with emoji presentation but not
// certain ZWJ-combined characters like the male and female signs.
const idx = try self.grid.getIndex(
const idx = try self.opts.grid.getIndex(
alloc,
cp,
style,
@ -376,11 +372,11 @@ pub const RunIterator = struct {
// We need to find a candidate that has ALL of our codepoints
for (candidates.items) |idx| {
if (!self.grid.hasCodepoint(idx, primary_cp, presentation)) continue;
if (!self.opts.grid.hasCodepoint(idx, primary_cp, presentation)) continue;
for (cps) |cp| {
// Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
if (!self.grid.hasCodepoint(idx, cp, null)) break;
if (!self.opts.grid.hasCodepoint(idx, cp, null)) break;
} else {
// If the while completed, then we have a candidate that
// supports all of our codepoints.

View File

@ -61,17 +61,11 @@ pub const Shaper = struct {
/// for a Shaper struct since they share state.
pub fn runIterator(
self: *Shaper,
group: *font.GroupCache,
row: terminal.Screen.Row,
selection: ?terminal.Selection,
cursor_x: ?usize,
opts: font.shape.RunOptions,
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.group = group,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
.opts = opts,
};
}

View File

@ -9,6 +9,7 @@ const harfbuzz = @import("harfbuzz");
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const apprt = @import("apprt.zig");
/// We export the xev backend we want to use so that the rest of
/// Ghostty can import this once and have access to the proper
@ -35,7 +36,7 @@ pub const GlobalState = struct {
/// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it.
resources_dir: ?[]const u8,
resources_dir: internal_os.ResourcesDir,
/// Where logging should go
pub const Logging = union(enum) {
@ -62,7 +63,7 @@ pub const GlobalState = struct {
.action = null,
.logging = .{ .stderr = {} },
.rlimits = .{},
.resources_dir = null,
.resources_dir = .{},
};
errdefer self.deinit();
@ -170,11 +171,11 @@ pub const GlobalState = struct {
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
self.resources_dir = try internal_os.resourcesDir(self.alloc);
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
self.resources_dir = try apprt.runtime.resourcesDir(self.alloc);
errdefer self.resources_dir.deinit(self.alloc);
// Setup i18n
if (self.resources_dir) |v| internal_os.i18n.init(v) catch |err| {
if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| {
std.log.warn("failed to init i18n, translations will not be available err={}", .{err});
};
}
@ -182,7 +183,7 @@ pub const GlobalState = struct {
/// Cleans up the global state. This doesn't _need_ to be called but
/// doing so in dev modes will check for memory leaks.
pub fn deinit(self: *GlobalState) void {
if (self.resources_dir) |dir| self.alloc.free(dir);
self.resources_dir.deinit(self.alloc);
// Flush our crash logs
crash.deinit();

View File

@ -379,6 +379,10 @@ pub const Action = union(enum) {
///
/// Valid actions are:
///
/// - `copy`
///
/// Copy the file path into the clipboard.
///
/// - `paste`
///
/// Paste the file path into the terminal.
@ -813,6 +817,7 @@ pub const Action = union(enum) {
};
pub const WriteScreenAction = enum {
copy,
paste,
open,
};

View File

@ -18,7 +18,7 @@ const Action = @import("Binding.zig").Action;
pub const Command = struct {
action: Action,
title: [:0]const u8,
description: [:0]const u8,
description: [:0]const u8 = "",
/// ghostty_command_s
pub const C = extern struct {
@ -28,6 +28,21 @@ pub const Command = struct {
description: [*:0]const u8,
};
pub fn clone(self: *const Command, alloc: Allocator) Allocator.Error!Command {
return .{
.action = try self.action.clone(alloc),
.title = try alloc.dupeZ(u8, self.title),
.description = try alloc.dupeZ(u8, self.description),
};
}
pub fn equal(self: Command, other: Command) bool {
if (self.action.hash() != other.action.hash()) return false;
if (!std.mem.eql(u8, self.title, other.title)) return false;
if (!std.mem.eql(u8, self.description, other.description)) return false;
return true;
}
/// Convert this command to a C struct.
pub fn comptimeCval(self: Command) C {
assert(@inComptime());
@ -189,6 +204,11 @@ fn actionCommands(action: Action.Key) []const Command {
}},
.write_screen_file => comptime &.{
.{
.action = .{ .write_screen_file = .copy },
.title = "Copy Screen to Temporary File and Copy Path",
.description = "Copy the screen contents to a temporary file and copy the path to the clipboard.",
},
.{
.action = .{ .write_screen_file = .paste },
.title = "Copy Screen to Temporary File and Paste Path",
@ -202,6 +222,11 @@ fn actionCommands(action: Action.Key) []const Command {
},
.write_selection_file => comptime &.{
.{
.action = .{ .write_selection_file = .copy },
.title = "Copy Selection to Temporary File and Copy Path",
.description = "Copy the selection contents to a temporary file and copy the path to the clipboard.",
},
.{
.action = .{ .write_selection_file = .paste },
.title = "Copy Selection to Temporary File and Paste Path",

View File

@ -98,11 +98,12 @@ pub fn main() !MainReturn {
}
// Create our app state
var app = try App.create(alloc);
const app: *App = try App.create(alloc);
defer app.destroy();
// Create our runtime app
var app_runtime = try apprt.App.init(app, .{});
var app_runtime: apprt.App = undefined;
try app_runtime.init(app, .{});
defer app_runtime.terminate();
// Since - by definition - there are no surfaces when first started, the

View File

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
pub const HostnameParsingError = error{
@ -6,6 +7,96 @@ pub const HostnameParsingError = error{
NoSpaceLeft,
};
pub const UrlParsingError = std.Uri.ParseError || error{
HostnameIsNotMacAddress,
NoSchemeProvided,
};
const mac_address_length = 17;
fn isUriPathSeparator(c: u8) bool {
return switch (c) {
'?', '#' => true,
else => false,
};
}
fn isValidMacAddress(mac_address: []const u8) bool {
// A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef.
if (mac_address.len != 17) {
return false;
}
for (mac_address, 0..) |c, i| {
if ((i + 1) % 3 == 0) {
if (c != ':') {
return false;
}
} else if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) {
return false;
}
}
return true;
}
/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and
/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS
/// the url passed to this function might have a mac address as its hostname and parses it
/// correctly.
pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri {
return std.Uri.parse(url) catch |e| {
// The mac-address-as-hostname issue is specific to macOS so we just return an error if we
// hit it on other platforms.
if (comptime builtin.os.tag != .macos) return e;
// It's possible this is a mac address on macOS where the last 2 characters in the
// address are non-digits, e.g. 'ff', and thus an invalid port.
//
// Example: file://12:34:56:78:90:12/path/to/file
if (e != error.InvalidPort) return e;
const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse {
return error.NoSchemeProvided;
};
const scheme = url[0..url_without_scheme_start];
const url_without_scheme = url[url_without_scheme_start + 3 ..];
// The first '/' after the scheme marks the end of the hostname. If the first '/'
// following the end of the scheme is not at the right position this is not a
// valid mac address.
if (url_without_scheme.len != mac_address_length and
std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length)
{
return error.HostnameIsNotMacAddress;
}
// At this point we may have a mac address as the hostname.
const mac_address = url_without_scheme[0..mac_address_length];
if (!isValidMacAddress(mac_address)) {
return error.HostnameIsNotMacAddress;
}
var uri_path_end_idx: usize = mac_address_length;
while (uri_path_end_idx < url_without_scheme.len and
!isUriPathSeparator(url_without_scheme[uri_path_end_idx]))
{
uri_path_end_idx += 1;
}
// Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI
// spec.
return .{
.scheme = scheme,
.host = .{ .percent_encoded = mac_address },
.path = .{
.percent_encoded = url_without_scheme[mac_address_length..uri_path_end_idx],
},
};
};
}
/// Print the hostname from a file URI into a buffer.
pub fn bufPrintHostnameFromFileUri(
buf: []u8,
@ -70,6 +161,101 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool {
return std.mem.eql(u8, hostname, ourHostname);
}
test parseUrl {
// 1. Typical hostnames.
var uri = try parseUrl("file://personal.computer/home/test/");
try std.testing.expectEqualStrings("file", uri.scheme);
try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
try std.testing.expect(uri.port == null);
uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/");
try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
try std.testing.expect(uri.port == null);
// 2. Hostnames that are mac addresses.
// Numerical mac addresses.
uri = try parseUrl("file://12:34:56:78:90:12/home/test/");
try std.testing.expectEqualStrings("file", uri.scheme);
try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
try std.testing.expect(uri.port == 12);
uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/");
try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
try std.testing.expect(uri.port == 12);
// Alphabetical mac addresses.
uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/");
try std.testing.expectEqualStrings("file", uri.scheme);
try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
try std.testing.expect(uri.port == null);
uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/");
try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded);
try std.testing.expect(uri.port == null);
// 3. Hostnames that are mac addresses with no path.
// Numerical mac addresses.
uri = try parseUrl("file://12:34:56:78:90:12");
try std.testing.expectEqualStrings("file", uri.scheme);
try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("", uri.path.percent_encoded);
try std.testing.expect(uri.port == 12);
uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12");
try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("", uri.path.percent_encoded);
try std.testing.expect(uri.port == 12);
// Alphabetical mac addresses.
uri = try parseUrl("file://ab:cd:ef:ab:cd:ef");
try std.testing.expectEqualStrings("file", uri.scheme);
try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("", uri.path.percent_encoded);
try std.testing.expect(uri.port == null);
uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef");
try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme);
try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded);
try std.testing.expectEqualStrings("", uri.path.percent_encoded);
try std.testing.expect(uri.port == null);
}
test "parseUrl succeeds even if path component is missing" {
const uri = try parseUrl("file://12:34:56:78:90:ab");
try std.testing.expectEqualStrings("file", uri.scheme);
try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded);
try std.testing.expect(uri.path.isEmpty());
try std.testing.expect(uri.port == null);
}
test "bufPrintHostnameFromFileUri succeeds with ascii hostname" {
const uri = try std.Uri.parse("file://localhost/");
@ -86,6 +272,14 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" {
try std.testing.expectEqualStrings("12:34:56:78:90:12", actual);
}
test "bufPrintHostnameFromFileUri succeeds with hostname as mac address with the last component as ascii" {
const uri = try parseUrl("file://12:34:56:78:90:ab");
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
const actual = try bufPrintHostnameFromFileUri(&buf, uri);
try std.testing.expectEqualStrings("12:34:56:78:90:ab", actual);
}
test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" {
const uri = try std.Uri.parse("file://12:34:56:78:90:05");

View File

@ -43,6 +43,7 @@ pub const locales = [_][:0]const u8{
"tr_TR.UTF-8",
"id_ID.UTF-8",
"es_BO.UTF-8",
"es_AR.UTF-8",
"pt_BR.UTF-8",
"ca_ES.UTF-8",
"ga_IE.UTF-8",

View File

@ -29,6 +29,7 @@ pub const shell = @import("shell.zig");
// Functions and types
pub const CFReleaseThread = @import("cf_release_thread.zig");
pub const TempDir = @import("TempDir.zig");
pub const GetEnvResult = env.GetEnvResult;
pub const getEnvMap = env.getEnvMap;
pub const appendEnv = env.appendEnv;
pub const appendEnvAlways = env.appendEnvAlways;
@ -55,6 +56,7 @@ pub const open = openpkg.open;
pub const OpenType = openpkg.Type;
pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir;
pub const ResourcesDir = resourcesdir.ResourcesDir;
pub const ShellEscapeWriter = shell.ShellEscapeWriter;
test {

View File

@ -2,6 +2,8 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.@"os-open");
/// The type of the data at the URL to open. This is used as a hint
/// to potentially open the URL in a different way.
pub const Type = enum {
@ -12,68 +14,73 @@ pub const Type = enum {
/// Open a URL in the default handling application.
///
/// Any output on stderr is logged as a warning in the application logs.
/// Output on stdout is ignored.
/// Output on stdout is ignored. The allocator is used to buffer the
/// log output and may allocate from another thread.
pub fn open(
alloc: Allocator,
typ: Type,
url: []const u8,
) !void {
const cmd: OpenCommand = switch (builtin.os.tag) {
.linux, .freebsd => .{ .child = std.process.Child.init(
var exe: std.process.Child = switch (builtin.os.tag) {
.linux, .freebsd => .init(
&.{ "xdg-open", url },
alloc,
) },
),
.windows => .{ .child = std.process.Child.init(
.windows => .init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
) },
),
.macos => .{
.child = std.process.Child.init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.wait = true,
},
.macos => .init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
var exe = cmd.child;
if (cmd.wait) {
// Pipe stdout/stderr so we can collect output from the command
exe.stdout_behavior = .Pipe;
exe.stderr_behavior = .Pipe;
}
// Pipe stdout/stderr so we can collect output from the command.
// This must be set before spawning the process.
exe.stdout_behavior = .Pipe;
exe.stderr_behavior = .Pipe;
// Spawn the process on our same thread so we can detect failure
// quickly.
try exe.spawn();
if (cmd.wait) {
// 50 KiB is the default value used by std.process.Child.run
const output_max_size = 50 * 1024;
var stdout: std.ArrayListUnmanaged(u8) = .{};
var stderr: std.ArrayListUnmanaged(u8) = .{};
defer {
stdout.deinit(alloc);
stderr.deinit(alloc);
}
try exe.collectOutput(alloc, &stdout, &stderr, output_max_size);
_ = try exe.wait();
// If we have any stderr output we log it. This makes it easier for
// users to debug why some open commands may not work as expected.
if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items});
}
// Create a thread that handles collecting output and reaping
// the process. This is done in a separate thread because SOME
// open implementations block and some do not. It's easier to just
// spawn a thread to handle this so that we never block.
const thread = try std.Thread.spawn(.{}, openThread, .{ alloc, exe });
thread.detach();
}
const OpenCommand = struct {
child: std.process.Child,
wait: bool = false,
};
fn openThread(alloc: Allocator, exe_: std.process.Child) !void {
// 50 KiB is the default value used by std.process.Child.run and should
// be enough to get the output we care about.
const output_max_size = 50 * 1024;
var stdout: std.ArrayListUnmanaged(u8) = .{};
var stderr: std.ArrayListUnmanaged(u8) = .{};
defer {
stdout.deinit(alloc);
stderr.deinit(alloc);
}
// Copy the exe so it is non-const. This is necessary because wait()
// requires a mutable reference and we can't have one as a thread
// param.
var exe = exe_;
try exe.collectOutput(alloc, &stdout, &stderr, output_max_size);
_ = try exe.wait();
// If we have any stderr output we log it. This makes it easier for
// users to debug why some open commands may not work as expected.
if (stderr.items.len > 0) log.warn("wait stderr={s}", .{stderr.items});
}

View File

@ -2,13 +2,42 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
pub const ResourcesDir = struct {
/// Avoid accessing these directly, use the app() and host() methods instead.
app_path: ?[]const u8 = null,
host_path: ?[]const u8 = null,
/// Free resources held. Requires the same allocator as when resourcesDir()
/// is called.
pub fn deinit(self: *ResourcesDir, alloc: Allocator) void {
if (self.app_path) |p| alloc.free(p);
if (self.host_path) |p| alloc.free(p);
}
/// Get the directory to the bundled resources directory accessible
/// by the application.
pub fn app(self: *ResourcesDir) ?[]const u8 {
return self.app_path;
}
/// Get the directory to the bundled resources directory accessible
/// by the host environment (i.e. for sandboxed applications). The
/// returned directory might not be accessible from the application
/// itself.
///
/// In non-sandboxed environment, this should be the same as app().
pub fn host(self: *ResourcesDir) ?[]const u8 {
return self.host_path orelse self.app_path;
}
};
/// Gets the directory to the bundled resources directory, if it
/// exists (not all platforms or packages have it). The output is
/// owned by the caller.
///
/// This is highly Ghostty-specific and can likely be generalized at
/// some point but we can cross that bridge if we ever need to.
pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
pub fn resourcesDir(alloc: Allocator) !ResourcesDir {
// Use the GHOSTTY_RESOURCES_DIR environment variable in release builds.
//
// In debug builds we try using terminfo detection first instead, since
@ -20,7 +49,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// freed, do not try to use internal_os.getenv or posix getenv.
if (comptime builtin.mode != .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
if (dir.len > 0) return .{ .app_path = dir };
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
@ -38,7 +67,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return .{};
// We have an exe path! Climb the tree looking for the terminfo
// bundle as we expect it.
@ -50,7 +79,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
if (comptime builtin.target.os.tag.isDarwin()) {
inline for (sentinels) |sentinel| {
if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| {
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
}
}
}
@ -65,7 +94,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
if (builtin.target.os.tag == .freebsd) "local/share" else "share",
sentinel,
)) |v| {
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
}
}
}
@ -74,14 +103,14 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// fallback and use the provided resources dir.
if (comptime builtin.mode == .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
if (dir.len > 0) return .{ .app_path = dir };
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
}
}
return null;
return .{};
}
/// Little helper to check if the "base/sub/suffix" directory exists and

View File

@ -29,9 +29,6 @@ pub const Buffer = bufferpkg.Buffer;
pub const Texture = @import("metal/Texture.zig");
pub const shaders = @import("metal/shaders.zig");
pub const cellpkg = @import("metal/cell.zig");
pub const imagepkg = @import("metal/image.zig");
pub const custom_shader_target: shadertoy.Target = .msl;
// The fragCoord for Metal shaders is +Y = down.
pub const custom_shader_y_is_down = true;
@ -285,6 +282,7 @@ pub const uniformBufferOptions = bufferOptions;
pub const fgBufferOptions = bufferOptions;
pub const bgBufferOptions = bufferOptions;
pub const imageBufferOptions = bufferOptions;
pub const bgImageBufferOptions = bufferOptions;
/// Returns the options to use when constructing textures.
pub inline fn textureOptions(self: Metal) Texture.Options {
@ -305,6 +303,44 @@ pub inline fn textureOptions(self: Metal) Texture.Options {
};
}
/// Pixel format for image texture options.
pub const ImageTextureFormat = enum {
/// 1 byte per pixel grayscale.
gray,
/// 4 bytes per pixel RGBA.
rgba,
/// 4 bytes per pixel BGRA.
bgra,
fn toPixelFormat(
self: ImageTextureFormat,
srgb: bool,
) mtl.MTLPixelFormat {
return switch (self) {
.gray => if (srgb) .r8unorm_srgb else .r8unorm,
.rgba => if (srgb) .rgba8unorm_srgb else .rgba8unorm,
.bgra => if (srgb) .bgra8unorm_srgb else .bgra8unorm,
};
}
};
/// Returns the options to use when constructing textures for images.
pub inline fn imageTextureOptions(
self: Metal,
format: ImageTextureFormat,
srgb: bool,
) Texture.Options {
return .{
.device = self.device,
.pixel_format = format.toPixelFormat(srgb),
.resource_options = .{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = self.default_storage_mode,
},
};
}
/// Initializes a Texture suitable for the provided font atlas.
pub fn initAtlasTexture(
self: *const Metal,
@ -312,7 +348,7 @@ pub fn initAtlasTexture(
) Texture.Error!Texture {
const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) {
.grayscale => .r8unorm,
.rgba => .bgra8unorm,
.bgra => .bgra8unorm_srgb,
else => @panic("unsupported atlas format for Metal texture"),
};

View File

@ -24,9 +24,6 @@ pub const Buffer = bufferpkg.Buffer;
pub const Texture = @import("opengl/Texture.zig");
pub const shaders = @import("opengl/shaders.zig");
pub const cellpkg = @import("opengl/cell.zig");
pub const imagepkg = @import("opengl/image.zig");
pub const custom_shader_target: shadertoy.Target = .glsl;
// The fragCoord for OpenGL shaders is +Y = up.
pub const custom_shader_y_is_down = false;
@ -391,6 +388,7 @@ pub const uniformBufferOptions = bufferOptions;
pub const fgBufferOptions = bufferOptions;
pub const bgBufferOptions = bufferOptions;
pub const imageBufferOptions = bufferOptions;
pub const bgImageBufferOptions = bufferOptions;
/// Returns the options to use when constructing textures.
pub inline fn textureOptions(self: OpenGL) Texture.Options {
@ -402,6 +400,38 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
};
}
/// Pixel format for image texture options.
pub const ImageTextureFormat = enum {
/// 1 byte per pixel grayscale.
gray,
/// 4 bytes per pixel RGBA.
rgba,
/// 4 bytes per pixel BGRA.
bgra,
fn toPixelFormat(self: ImageTextureFormat) gl.Texture.Format {
return switch (self) {
.gray => .red,
.rgba => .rgba,
.bgra => .bgra,
};
}
};
/// Returns the options to use when constructing textures for images.
pub inline fn imageTextureOptions(
self: OpenGL,
format: ImageTextureFormat,
srgb: bool,
) Texture.Options {
_ = self;
return .{
.format = format.toPixelFormat(),
.internal_format = if (srgb) .srgba else .rgba,
.target = .@"2D",
};
}
/// Initializes a Texture suitable for the provided font atlas.
pub fn initAtlasTexture(
self: *const OpenGL,
@ -411,7 +441,7 @@ pub fn initAtlasTexture(
const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat =
switch (atlas.format) {
.grayscale => .{ .red, .red },
.rgba => .{ .rgba, .srgba },
.bgra => .{ .bgra, .srgba },
else => @panic("unsupported atlas format for OpenGL texture"),
};

View File

@ -1,6 +1,197 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const ziglyph = @import("ziglyph");
const font = @import("../font/main.zig");
const terminal = @import("../terminal/main.zig");
const renderer = @import("../renderer.zig");
const shaderpkg = renderer.Renderer.API.shaders;
const ArrayListCollection = @import("../datastruct/array_list_collection.zig").ArrayListCollection;
/// The possible cell content keys that exist.
pub const Key = enum {
bg,
text,
underline,
strikethrough,
overline,
/// Returns the GPU vertex type for this key.
pub fn CellType(self: Key) type {
return switch (self) {
.bg => shaderpkg.CellBg,
.text,
.underline,
.strikethrough,
.overline,
=> shaderpkg.CellText,
};
}
};
/// The contents of all the cells in the terminal.
///
/// The goal of this data structure is to allow for efficient row-wise
/// clearing of data from the GPU buffers, to allow for row-wise dirty
/// tracking to eliminate the overhead of rebuilding the GPU buffers
/// each frame.
///
/// Must be initialized by resizing before calling any operations.
pub const Contents = struct {
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
/// Flat array containing cell background colors for the terminal grid.
///
/// Indexed as `bg_cells[row * size.columns + col]`.
///
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
/// of directly indexing in order to avoid integer size bugs.
bg_cells: []shaderpkg.CellBg = undefined,
/// The ArrayListCollection which holds all of the foreground cells. When
/// sized with Contents.resize the individual ArrayLists are given enough
/// room that they can hold a single row with #cols glyphs, underlines, and
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
/// it is possible to exceed this with combining glyphs that add a glyph
/// but take up no column since they combine with the previous one, as
/// well as with fonts that perform multi-substitutions for glyphs, which
/// can result in a similar situation where multiple glyphs reside in the
/// same column.
///
/// Allocations should nevertheless be exceedingly rare since hitting the
/// initial capacity of a list would require a row filled with underlined
/// struck through characters, at least one of which is a multi-glyph
/// composite.
///
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
/// the collection is reserved for the cursor, which must be the first item
/// in the buffer.
///
/// Must be initialized by calling resize on the Contents struct before
/// calling any operations.
fg_rows: ArrayListCollection(shaderpkg.CellText) = .{ .lists = &.{} },
pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
}
/// Resize the cell contents for the given grid size. This will
/// always invalidate the entire cell contents.
pub fn resize(
self: *Contents,
alloc: Allocator,
size: renderer.GridSize,
) Allocator.Error!void {
self.size = size;
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
errdefer alloc.free(bg_cells);
@memset(bg_cells, .{ 0, 0, 0, 0 });
// The foreground lists can hold 3 types of items:
// - Glyphs
// - Underlines
// - Strikethroughs
// So we give them an initial capacity of size.columns * 3, which will
// avoid any further allocations in the vast majority of cases. Sadly
// we can not assume capacity though, since with combining glyphs that
// form a single grapheme, and multi-substitutions in fonts, the number
// of glyphs in a row is theoretically unlimited.
//
// We have size.rows + 1 lists because index 0 is used for a special
// list containing the cursor cell which needs to be first in the buffer.
var fg_rows = try ArrayListCollection(shaderpkg.CellText).init(
alloc,
size.rows + 1,
size.columns * 3,
);
errdefer fg_rows.deinit(alloc);
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
self.bg_cells = bg_cells;
self.fg_rows = fg_rows;
// We don't need 3*cols worth of cells for the cursor list, so we can
// replace it with a smaller list. This is technically a tiny bit of
// extra work but resize is not a hot function so it's worth it to not
// waste the memory.
self.fg_rows.lists[0].deinit(alloc);
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(
shaderpkg.CellText,
).initCapacity(alloc, 1);
}
/// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void {
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
self.fg_rows.reset();
}
/// Set the cursor value. If the value is null then the cursor is hidden.
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
self.fg_rows.lists[0].clearRetainingCapacity();
if (v) |cell| {
self.fg_rows.lists[0].appendAssumeCapacity(cell);
}
}
/// Access a background cell. Prefer this function over direct indexing
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
pub inline fn bgCell(
self: *Contents,
row: usize,
col: usize,
) *shaderpkg.CellBg {
return &self.bg_cells[row * self.size.columns + col];
}
/// Add a cell to the appropriate list. Adding the same cell twice will
/// result in duplication in the vertex buffer. The caller should clear
/// the corresponding row with Contents.clear to remove old cells first.
pub fn add(
self: *Contents,
alloc: Allocator,
comptime key: Key,
cell: key.CellType(),
) Allocator.Error!void {
const y = cell.grid_pos[1];
assert(y < self.size.rows);
switch (key) {
.bg => comptime unreachable,
.text,
.underline,
.strikethrough,
.overline,
// We have a special list containing the cursor cell at the start
// of our fg row collection, so we need to add 1 to the y to get
// the correct index.
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
}
}
/// Clear all of the cell contents for a given row.
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
assert(y < self.size.rows);
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
// We have a special list containing the cursor cell at the start
// of our fg row collection, so we need to add 1 to the y to get
// the correct index.
self.fg_rows.lists[y + 1].clearRetainingCapacity();
}
};
/// Returns true if a codepoint for a cell is a covering character. A covering
/// character is a character that covers the entire cell. This is used to
@ -38,7 +229,7 @@ pub const FgMode = enum {
pub fn fgMode(
presentation: font.Presentation,
cell_pin: terminal.Pin,
) !FgMode {
) FgMode {
return switch (presentation) {
// Emoji is always full size and color.
.emoji => .color,
@ -131,3 +322,141 @@ fn isPowerline(char: u21) bool {
else => false,
};
}
test Contents {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c: Contents = .{};
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// We should start off empty after resizing.
for (0..rows) |y| {
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
for (0..cols) |x| {
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
}
}
// And the cursor row should have a capacity of 1 and also be empty.
try testing.expect(c.fg_rows.lists[0].capacity == 1);
try testing.expect(c.fg_rows.lists[0].items.len == 0);
// Add some contents.
const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell: shaderpkg.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(1, 4).* = bg_cell;
try c.add(alloc, .text, fg_cell);
try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
// The fg row index is offset by 1 because of the cursor list.
try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
// And we should be able to clear it.
c.clear(1);
for (0..rows) |y| {
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
for (0..cols) |x| {
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
}
}
// Add a cursor.
const cursor_cell: shaderpkg.CellText = .{
.mode = .cursor,
.grid_pos = .{ 2, 3 },
.color = .{ 0, 0, 0, 1 },
};
c.setCursor(cursor_cell);
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
// And remove it.
c.setCursor(null);
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
}
test "Contents clear retains other content" {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c: Contents = .{};
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Set some contents
// bg and fg cells in row 1
const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: shaderpkg.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(1, 4).* = bg_cell_1;
try c.add(alloc, .text, fg_cell_1);
// bg and fg cells in row 2
const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_2: shaderpkg.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 2 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(2, 4).* = bg_cell_2;
try c.add(alloc, .text, fg_cell_2);
// Clear row 1, this should leave row 2 untouched
c.clear(1);
// Row 2 should still contain its cells.
try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
// Fg row index is +1 because of cursor list at start
try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
}
test "Contents clear last added content" {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c: Contents = .{};
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Set some contents
// bg and fg cells in row 1
const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: shaderpkg.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(1, 4).* = bg_cell_1;
try c.add(alloc, .text, fg_cell_1);
// bg and fg cells in row 2
const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_2: shaderpkg.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 2 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(2, 4).* = bg_cell_2;
try c.add(alloc, .text, fg_cell_2);
// Clear row 2, this should leave row 1 untouched
c.clear(2);
// Row 1 should still contain its cells.
try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
// Fg row index is +1 because of cursor list at start
try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
}

View File

@ -2,6 +2,7 @@ const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const xev = @import("xev");
const wuffs = @import("wuffs");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");
@ -11,8 +12,13 @@ const renderer = @import("../renderer.zig");
const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const link = @import("link.zig");
const fgMode = @import("cell.zig").fgMode;
const isCovering = @import("cell.zig").isCovering;
const cellpkg = @import("cell.zig");
const fgMode = cellpkg.fgMode;
const isCovering = cellpkg.isCovering;
const imagepkg = @import("image.zig");
const Image = imagepkg.Image;
const ImageMap = imagepkg.ImageMap;
const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
const shadertoy = @import("shadertoy.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
@ -20,6 +26,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const Terminal = terminal.Terminal;
const Health = renderer.Health;
const FileType = @import("../file_type.zig").FileType;
const macos = switch (builtin.os.tag) {
.macos => @import("macos"),
else => void,
@ -71,21 +79,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
return struct {
const Self = @This();
pub const API = GraphicsAPI;
const Target = GraphicsAPI.Target;
const Buffer = GraphicsAPI.Buffer;
const Texture = GraphicsAPI.Texture;
const RenderPass = GraphicsAPI.RenderPass;
const shaderpkg = GraphicsAPI.shaders;
const cellpkg = GraphicsAPI.cellpkg;
const imagepkg = GraphicsAPI.imagepkg;
const Image = imagepkg.Image;
const ImageMap = imagepkg.ImageMap;
const Shaders = shaderpkg.Shaders;
const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
/// Allocator that can be used
alloc: std.mem.Allocator,
@ -181,6 +184,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
image_text_end: u32 = 0,
image_virtual: bool = false,
/// Background image, if we have one.
bg_image: ?imagepkg.Image = null,
/// Set whenever the background image changes, singalling
/// that the new background image needs to be uploaded to
/// the GPU.
///
/// This is initialized as true so that we load the image
/// on renderer initialization, not just on config change.
bg_image_changed: bool = true,
/// Background image vertex buffer.
bg_image_buffer: shaderpkg.BgImage,
/// This value is used to force-update the swap chain copy
/// of the background image buffer whenever we change it.
bg_image_buffer_modified: usize = 0,
/// Graphics API state.
api: GraphicsAPI,
@ -298,13 +316,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// See property of same name on Renderer for explanation.
target_config_modified: usize = 0,
/// Buffer with the vertex data for our background image.
///
/// TODO: Make this an optional and only create it
/// if we actually have a background image.
bg_image_buffer: BgImageBuffer,
/// See property of same name on Renderer for explanation.
bg_image_buffer_modified: usize = 0,
/// Custom shader state, this is null if we have no custom shaders.
custom_shader_state: ?CustomShaderState = null,
/// A buffer containing the uniform data.
const UniformBuffer = Buffer(shaderpkg.Uniforms);
const CellBgBuffer = Buffer(shaderpkg.CellBg);
const CellTextBuffer = Buffer(shaderpkg.CellText);
const BgImageBuffer = Buffer(shaderpkg.BgImage);
pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
// Uniform buffer contains exactly 1 uniform struct. The
@ -324,6 +350,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1);
errdefer cells_bg.deinit();
// Create a GPU buffer for our background image info.
var bg_image_buffer = try BgImageBuffer.init(
api.bgImageBufferOptions(),
1,
);
errdefer bg_image_buffer.deinit();
// Initialize our textures for our font atlas.
//
// As with the buffers above, we start these off as small
@ -337,7 +370,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const color = try api.initAtlasTexture(&.{
.data = undefined,
.size = 1,
.format = .rgba,
.format = .bgra,
});
errdefer color.deinit();
@ -356,6 +389,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.uniforms = uniforms,
.cells = cells,
.cells_bg = cells_bg,
.bg_image_buffer = bg_image_buffer,
.grayscale = grayscale,
.color = color,
.target = target,
@ -369,6 +403,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.cells_bg.deinit();
self.grayscale.deinit();
self.color.deinit();
self.bg_image_buffer.deinit();
if (self.custom_shader_state) |*state| state.deinit();
}
@ -395,12 +430,20 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
front_texture: Texture,
back_texture: Texture,
uniforms: UniformBuffer,
const UniformBuffer = Buffer(shadertoy.Uniforms);
/// Swap the front and back textures.
pub fn swap(self: *CustomShaderState) void {
std.mem.swap(Texture, &self.front_texture, &self.back_texture);
}
pub fn init(api: GraphicsAPI) !CustomShaderState {
// Create a GPU buffer to hold our uniforms.
var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1);
errdefer uniforms.deinit();
// Initialize the front and back textures at 1x1 px, this
// is slightly wasteful but it's only done once so whatever.
const front_texture = try Texture.init(
@ -417,15 +460,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
null,
);
errdefer back_texture.deinit();
return .{
.front_texture = front_texture,
.back_texture = back_texture,
.uniforms = uniforms,
};
}
pub fn deinit(self: *CustomShaderState) void {
self.front_texture.deinit();
self.back_texture.deinit();
self.uniforms.deinit();
}
pub fn resize(
@ -467,6 +513,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
font_thicken_strength: u8,
font_features: std.ArrayListUnmanaged([:0]const u8),
font_styles: font.CodepointResolver.StyleStatus,
font_shaping_break: configpkg.FontShapingBreak,
cursor_color: ?terminal.color.RGB,
cursor_invert: bool,
cursor_opacity: f64,
@ -481,6 +528,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
bg_image: ?configpkg.Path,
bg_image_opacity: f32,
bg_image_position: configpkg.BackgroundImagePosition,
bg_image_fit: configpkg.BackgroundImageFit,
bg_image_repeat: bool,
links: link.Set,
vsync: bool,
colorspace: configpkg.Config.WindowColorspace,
@ -497,6 +549,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Copy our shaders
const custom_shaders = try config.@"custom-shader".clone(alloc);
// Copy our background image
const bg_image =
if (config.@"background-image") |bg|
try bg.clone(alloc)
else
null;
// Copy our font features
const font_features = try config.@"font-feature".clone(alloc);
@ -520,6 +579,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.font_thicken_strength = config.@"font-thicken-strength",
.font_features = font_features.list,
.font_styles = font_styles,
.font_shaping_break = config.@"font-shaping-break",
.cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
config.@"cursor-color".?.toTerminalRGB()
@ -553,6 +613,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
null,
.custom_shaders = custom_shaders,
.bg_image = bg_image,
.bg_image_opacity = config.@"background-image-opacity",
.bg_image_position = config.@"background-image-position",
.bg_image_fit = config.@"background-image-fit",
.bg_image_repeat = config.@"background-image-repeat",
.links = links,
.vsync = config.@"window-vsync",
.colorspace = config.@"window-colorspace",
@ -647,6 +712,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.cell_size = undefined,
.grid_size = undefined,
.grid_padding = undefined,
.screen_size = undefined,
.padding_extend = .{},
.min_contrast = options.config.min_contrast,
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
@ -670,7 +736,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.time_delta = 0,
.frame_rate = 60, // not currently updated
.frame = 0,
.channel_time = @splat(@splat(0)),
.channel_time = @splat(@splat(0)), // not currently updated
.channel_resolution = @splat(@splat(0)),
.mouse = @splat(0), // not currently updated
.date = @splat(0), // not currently updated
@ -681,6 +747,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.previous_cursor_color = @splat(0),
.cursor_change_time = 0,
},
.bg_image_buffer = undefined,
// Fonts
.font_grid = options.font_grid,
@ -701,6 +768,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Ensure our undefined values above are correctly initialized.
result.updateFontGridUniforms();
result.updateScreenSizeUniforms();
result.updateBgImageBuffer();
try result.prepBackgroundImage();
return result;
}
@ -729,6 +798,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
self.image_placements.deinit(self.alloc);
if (self.bg_image) |img| img.deinit(self.alloc);
self.deinitShaders();
self.api.deinit();
@ -1324,32 +1395,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
// Upload images to the GPU as necessary.
{
var image_it = self.images.iterator();
while (image_it.next()) |kv| {
switch (kv.value_ptr.image) {
.ready => {},
try self.uploadKittyImages();
.pending_gray,
.pending_gray_alpha,
.pending_rgb,
.pending_rgba,
.replace_gray,
.replace_gray_alpha,
.replace_rgb,
.replace_rgba,
=> try kv.value_ptr.image.upload(self.alloc, &self.api),
.unload_pending,
.unload_replace,
.unload_ready,
=> {
kv.value_ptr.image.deinit(self.alloc);
self.images.removeByPtr(kv.key_ptr);
},
}
}
}
// Upload the background image to the GPU as necessary.
try self.uploadBackgroundImage();
// Update custom shader uniforms if necessary.
try self.updateCustomShaderUniforms();
@ -1359,6 +1408,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
try frame.cells_bg.sync(self.cells.bg_cells);
const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
// If our background image buffer has changed, sync it.
if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) {
try frame.bg_image_buffer.sync(&.{self.bg_image_buffer});
frame.bg_image_buffer_modified = self.bg_image_buffer_modified;
}
// If our font atlas changed, sync the texture data
texture: {
const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
@ -1391,23 +1447,58 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}});
defer pass.complete();
// bg images
try self.drawImagePlacements(&pass, self.image_placements.items[0..self.image_bg_end]);
// bg
// First we draw our background image, if we have one.
// The bg image shader also draws the main bg color.
//
// Otherwise, if we don't have a background image, we
// draw the background color by itself in its own step.
//
// NOTE: We don't use the clear_color for this because that
// would require us to do color space conversion on the
// CPU-side. In the future when we have utilities for
// that we should remove this step and use clear_color.
if (self.bg_image) |img| switch (img) {
.ready => |texture| pass.step(.{
.pipeline = self.shaders.pipelines.bg_image,
.uniforms = frame.uniforms.buffer,
.buffers = &.{frame.bg_image_buffer.buffer},
.textures = &.{texture},
.draw = .{ .type = .triangle, .vertex_count = 3 },
}),
else => {},
} else {
pass.step(.{
.pipeline = self.shaders.pipelines.bg_color,
.uniforms = frame.uniforms.buffer,
.buffers = &.{ null, frame.cells_bg.buffer },
.draw = .{ .type = .triangle, .vertex_count = 3 },
});
}
// Then we draw any kitty images that need
// to be behind text AND cell backgrounds.
try self.drawImagePlacements(
&pass,
self.image_placements.items[0..self.image_bg_end],
);
// Then we draw any opaque cell backgrounds.
pass.step(.{
.pipeline = self.shaders.cell_bg_pipeline,
.pipeline = self.shaders.pipelines.cell_bg,
.uniforms = frame.uniforms.buffer,
.buffers = &.{ null, frame.cells_bg.buffer },
.draw = .{
.type = .triangle,
.vertex_count = 3,
},
.draw = .{ .type = .triangle, .vertex_count = 3 },
});
// mg images
try self.drawImagePlacements(&pass, self.image_placements.items[self.image_bg_end..self.image_text_end]);
// text
// Kitty images between cell backgrounds and text.
try self.drawImagePlacements(
&pass,
self.image_placements.items[self.image_bg_end..self.image_text_end],
);
// Text.
pass.step(.{
.pipeline = self.shaders.cell_text_pipeline,
.pipeline = self.shaders.pipelines.cell_text,
.uniforms = frame.uniforms.buffer,
.buffers = &.{
frame.cells.buffer,
@ -1423,20 +1514,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.instance_count = fg_count,
},
});
// fg images
try self.drawImagePlacements(&pass, self.image_placements.items[self.image_text_end..]);
// Kitty images in front of text.
try self.drawImagePlacements(
&pass,
self.image_placements.items[self.image_text_end..],
);
}
// If we have custom shaders, then we render them.
if (frame.custom_shader_state) |*state| {
// We create a buffer on the GPU for our post uniforms.
// TODO: This should be a part of the frame state tbqh.
const PostBuffer = Buffer(shadertoy.Uniforms);
const uniform_buffer = try PostBuffer.initFill(
self.api.bufferOptions(),
&.{self.custom_shader_uniforms},
);
defer uniform_buffer.deinit();
// Sync our uniforms.
try state.uniforms.sync(&.{self.custom_shader_uniforms});
for (self.shaders.post_pipelines, 0..) |pipeline, i| {
defer state.swap();
@ -1452,7 +1541,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
pass.step(.{
.pipeline = pipeline,
.uniforms = uniform_buffer.buffer,
.uniforms = state.uniforms.buffer,
.textures = &.{state.back_texture},
.draw = .{
.type = .triangle,
@ -1539,7 +1628,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
defer buf.deinit();
pass.step(.{
.pipeline = self.shaders.image_pipeline,
.pipeline = self.shaders.pipelines.image,
.buffers = &.{buf.buffer},
.textures = &.{texture},
.draw = .{
@ -1551,8 +1640,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
/// This goes through the Kitty graphic placements and accumulates the
/// placements we need to render on our viewport. It also ensures that
/// the visible images are loaded on the GPU.
/// placements we need to render on our viewport.
fn prepKittyGraphics(
self: *Self,
t: *terminal.Terminal,
@ -1589,7 +1677,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y;
// Go through the placements and ensure the image is loaded on the GPU.
// Go through the placements and ensure the image is
// on the GPU or else is ready to be sent to the GPU.
var it = storage.placements.iterator();
while (it.next()) |kv| {
const p = kv.value_ptr;
@ -1648,8 +1737,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}.lessThan,
);
// Find our indices. The values are sorted by z so we can find the
// first placement out of bounds to find the limits.
// Find our indices. The values are sorted by z so we can
// find the first placement out of bounds to find the limits.
var bg_end: ?u32 = null;
var text_end: ?u32 = null;
const bg_limit = std.math.minInt(i32) / 2;
@ -1662,8 +1751,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
self.image_bg_end = bg_end orelse 0;
self.image_text_end = text_end orelse self.image_bg_end;
// If we didn't see any images with a z > the bg limit,
// then our bg end is the end of our placement list.
self.image_bg_end =
bg_end orelse @intCast(self.image_placements.items.len);
// Same idea for the image_text_end.
self.image_text_end =
text_end orelse @intCast(self.image_placements.items.len);
}
fn prepKittyVirtualPlacement(
@ -1704,7 +1799,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
unreachable;
};
// Send our image to the GPU and store the placement for rendering.
// Prepare the image for the GPU and store the placement.
try self.prepKittyImage(&image);
try self.image_placements.append(self.alloc, .{
.image_id = image.id,
@ -1722,6 +1817,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
});
}
/// Get the viewport-relative position for this
/// placement and add it to the placements list.
fn prepKittyPlacement(
self: *Self,
t: *terminal.Terminal,
@ -1742,9 +1839,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (img_top_y > bot_y) return;
if (img_bot_y < top_y) return;
// We need to prep this image for upload if it isn't in the cache OR
// it is in the cache but the transmit time doesn't match meaning this
// image is different.
// We need to prep this image for upload if it isn't in the
// cache OR it is in the cache but the transmit time doesn't
// match meaning this image is different.
try self.prepKittyImage(image);
// Calculate the dimensions of our image, taking in to
@ -1785,6 +1882,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
/// Prepare the provided image for upload to the GPU by copying its
/// data with our allocator and setting it to the pending state.
fn prepKittyImage(
self: *Self,
image: *const terminal.kitty.graphics.Image,
@ -1806,16 +1905,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const pending: Image.Pending = .{
.width = image.width,
.height = image.height,
.pixel_format = switch (image.format) {
.gray => .gray,
.gray_alpha => .gray_alpha,
.rgb => .rgb,
.rgba => .rgba,
.png => unreachable, // should be decoded by now
},
.data = data.ptr,
};
const new_image: Image = switch (image.format) {
.gray => .{ .pending_gray = pending },
.gray_alpha => .{ .pending_gray_alpha = pending },
.rgb => .{ .pending_rgb = pending },
.rgba => .{ .pending_rgba = pending },
.png => unreachable, // should be decoded by now
};
const new_image: Image = .{ .pending = pending };
if (!gop.found_existing) {
gop.value_ptr.* = .{
@ -1829,9 +1929,122 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
}
try gop.value_ptr.image.prepForUpload(self.alloc);
gop.value_ptr.transmit_time = image.transmit_time;
}
/// Upload any images to the GPU that need to be uploaded,
/// and remove any images that are no longer needed on the GPU.
fn uploadKittyImages(self: *Self) !void {
var image_it = self.images.iterator();
while (image_it.next()) |kv| {
const img = &kv.value_ptr.image;
if (img.isUnloading()) {
img.deinit(self.alloc);
self.images.removeByPtr(kv.key_ptr);
return;
}
if (img.isPending()) try img.upload(self.alloc, &self.api);
}
}
/// Call this any time the background image path changes.
///
/// Caller must hold the draw mutex.
fn prepBackgroundImage(self: *Self) !void {
// Then we try to load the background image if we have a path.
if (self.config.bg_image) |p| load_background: {
const path = switch (p) {
.required, .optional => |slice| slice,
};
// Open the file
var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
log.warn(
"error opening background image file \"{s}\": {}",
.{ path, err },
);
break :load_background;
};
defer file.close();
// Read it
const contents = file.readToEndAlloc(
self.alloc,
std.math.maxInt(u32), // Max size of 4 GiB, for now.
) catch |err| {
log.warn(
"error reading background image file \"{s}\": {}",
.{ path, err },
);
break :load_background;
};
defer self.alloc.free(contents);
// Figure out what type it probably is.
const file_type = switch (FileType.detect(contents)) {
.unknown => FileType.guessFromExtension(
std.fs.path.extension(path),
),
else => |t| t,
};
// Decode it if we know how.
const image_data = switch (file_type) {
.png => try wuffs.png.decode(self.alloc, contents),
.jpeg => try wuffs.jpeg.decode(self.alloc, contents),
.unknown => {
log.warn(
"Cannot determine file type for background image file \"{s}\"!",
.{path},
);
break :load_background;
},
else => |f| {
log.warn(
"Unsupported file type {} for background image file \"{s}\"!",
.{ f, path },
);
break :load_background;
},
};
const image: imagepkg.Image = .{
.pending = .{
.width = image_data.width,
.height = image_data.height,
.pixel_format = .rgba,
.data = image_data.data.ptr,
},
};
// If we have an existing background image, replace it.
// Otherwise, set this as our background image directly.
if (self.bg_image) |*img| {
try img.markForReplace(self.alloc, image);
} else {
self.bg_image = image;
}
} else {
// If we don't have a background image path, mark our
// background image for unload if we currently have one.
if (self.bg_image) |*img| img.markForUnload();
}
}
fn uploadBackgroundImage(self: *Self) !void {
// Make sure our bg image is uploaded if it needs to be.
if (self.bg_image) |*bg| {
if (bg.isUnloading()) {
bg.deinit(self.alloc);
self.bg_image = null;
return;
}
if (bg.isPending()) try bg.upload(self.alloc, &self.api);
}
}
/// Update the configuration.
pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
self.draw_mutex.lock();
@ -1869,12 +2082,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
const bg_image_config_changed =
self.config.bg_image_fit != config.bg_image_fit or
self.config.bg_image_position != config.bg_image_position or
self.config.bg_image_repeat != config.bg_image_repeat or
self.config.bg_image_opacity != config.bg_image_opacity;
const bg_image_changed =
if (self.config.bg_image) |old|
if (config.bg_image) |new|
!old.equal(new)
else
true
else
config.bg_image != null;
const old_blending = self.config.blending;
const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
self.config.deinit();
self.config = config.*;
// If our background image path changed, prepare the new bg image.
if (bg_image_changed) try self.prepBackgroundImage();
// If our background image config changed, update the vertex buffer.
if (bg_image_config_changed) self.updateBgImageBuffer();
// Reset our viewport to force a rebuild, in case of a font change.
self.cells_viewport = null;
@ -1944,14 +2178,50 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
@floatFromInt(blank.bottom),
@floatFromInt(blank.left),
};
self.uniforms.screen_size = .{
@floatFromInt(self.size.screen.width),
@floatFromInt(self.size.screen.height),
};
}
/// Update the background image vertex buffer (CPU-side).
///
/// This should be called if and when configs change that
/// could affect the background image.
///
/// Caller must hold the draw mutex.
fn updateBgImageBuffer(self: *Self) void {
self.bg_image_buffer = .{
.opacity = self.config.bg_image_opacity,
.info = .{
.position = switch (self.config.bg_image_position) {
.@"top-left" => .tl,
.@"top-center" => .tc,
.@"top-right" => .tr,
.@"center-left" => .ml,
.@"center-center", .center => .mc,
.@"center-right" => .mr,
.@"bottom-left" => .bl,
.@"bottom-center" => .bc,
.@"bottom-right" => .br,
},
.fit = switch (self.config.bg_image_fit) {
.contain => .contain,
.cover => .cover,
.stretch => .stretch,
.none => .none,
},
.repeat = self.config.bg_image_repeat,
},
};
// Signal that the buffer was modified.
self.bg_image_buffer_modified +%= 1;
}
/// Update uniforms for the custom shaders, if necessary.
///
/// This should be called exactly once per frame, inside `drawFrame`.
fn updateCustomShaderUniforms(
self: *Self,
) !void {
fn updateCustomShaderUniforms(self: *Self) !void {
// We only need to do this if we have custom shaders.
if (!self.has_custom_shaders) return;
@ -2199,13 +2469,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
// Iterator of runs for shaping.
var run_iter = self.font_shaper.runIterator(
self.font_grid,
screen,
row,
row_selection,
if (shape_cursor) screen.cursor.x else null,
);
var run_iter_opts: font.shape.RunOptions = .{
.grid = self.font_grid,
.screen = screen,
.row = row,
.selection = row_selection,
.cursor_x = if (shape_cursor) screen.cursor.x else null,
};
run_iter_opts.applyBreakConfig(self.config.font_shaping_break);
var run_iter = self.font_shaper.runIterator(run_iter_opts);
var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0;
@ -2378,8 +2650,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const bg_alpha: u8 = bg_alpha: {
const default: u8 = 255;
if (self.config.background_opacity >= 1) break :bg_alpha default;
// Cells that are selected should be fully opaque.
if (selected) break :bg_alpha default;
@ -2387,12 +2657,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (style.flags.inverse) break :bg_alpha default;
// Cells that have an explicit bg color should be fully opaque.
if (bg_style != null) {
break :bg_alpha default;
}
if (bg_style != null) break :bg_alpha default;
// Otherwise, we use the configured background opacity.
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
// Otherwise, we won't draw the bg for this cell,
// we'll let the already-drawn background color
// show through.
break :bg_alpha 0;
};
self.cells.bgCell(y, x).* = .{
@ -2769,7 +3039,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
return;
}
const mode: shaderpkg.CellText.Mode = switch (try fgMode(
const mode: shaderpkg.CellText.Mode = switch (fgMode(
render.presentation,
cell_pin,
)) {

302
src/renderer/image.zig Normal file
View File

@ -0,0 +1,302 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const wuffs = @import("wuffs");
const Renderer = @import("../renderer.zig").Renderer;
const GraphicsAPI = Renderer.API;
const Texture = GraphicsAPI.Texture;
/// Represents a single image placement on the grid.
/// A placement is a request to render an instance of an image.
pub const Placement = struct {
/// The image being rendered. This MUST be in the image map.
image_id: u32,
/// The grid x/y where this placement is located.
x: i32,
y: i32,
z: i32,
/// The width/height of the placed image.
width: u32,
height: u32,
/// The offset in pixels from the top left of the cell.
/// This is clamped to the size of a cell.
cell_offset_x: u32,
cell_offset_y: u32,
/// The source rectangle of the placement.
source_x: u32,
source_y: u32,
source_width: u32,
source_height: u32,
};
/// The map used for storing images.
pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct {
image: Image,
transmit_time: std.time.Instant,
});
/// The state for a single image that is to be rendered.
pub const Image = union(enum) {
/// The image data is pending upload to the GPU.
///
/// This data is owned by this union so it must be freed once uploaded.
pending: Pending,
/// This is the same as the pending states but there is
/// a texture already allocated that we want to replace.
replace: Replace,
/// The image is uploaded and ready to be used.
ready: Texture,
/// The image isn't uploaded yet but is scheduled to be unloaded.
unload_pending: Pending,
/// The image is uploaded and is scheduled to be unloaded.
unload_ready: Texture,
/// The image is uploaded and scheduled to be replaced
/// with new data, but it's also scheduled to be unloaded.
unload_replace: Replace,
pub const Replace = struct {
texture: Texture,
pending: Pending,
};
/// Pending image data that needs to be uploaded to the GPU.
pub const Pending = struct {
height: u32,
width: u32,
pixel_format: PixelFormat,
/// Data is always expected to be (width * height * bpp).
data: [*]u8,
pub fn dataSlice(self: Pending) []u8 {
return self.data[0..self.len()];
}
pub fn len(self: Pending) usize {
return self.width * self.height * self.pixel_format.bpp();
}
pub const PixelFormat = enum {
/// 1 byte per pixel grayscale.
gray,
/// 2 bytes per pixel grayscale + alpha.
gray_alpha,
/// 3 bytes per pixel RGB.
rgb,
/// 3 bytes per pixel BGR.
bgr,
/// 4 byte per pixel RGBA.
rgba,
/// 4 byte per pixel BGRA.
bgra,
/// Get bytes per pixel for this format.
pub inline fn bpp(self: PixelFormat) usize {
return switch (self) {
.gray => 1,
.gray_alpha => 2,
.rgb => 3,
.bgr => 3,
.rgba => 4,
.bgra => 4,
};
}
};
};
pub fn deinit(self: Image, alloc: Allocator) void {
switch (self) {
.pending,
.unload_pending,
=> |p| alloc.free(p.dataSlice()),
.replace, .unload_replace => |r| {
alloc.free(r.pending.dataSlice());
r.texture.deinit();
},
.ready,
.unload_ready,
=> |t| t.deinit(),
}
}
/// Mark this image for unload whatever state it is in.
pub fn markForUnload(self: *Image) void {
self.* = switch (self.*) {
.unload_pending,
.unload_replace,
.unload_ready,
=> return,
.ready => |t| .{ .unload_ready = t },
.pending => |p| .{ .unload_pending = p },
.replace => |r| .{ .unload_replace = r },
};
}
/// Mark the current image to be replaced with a pending one. This will
/// attempt to update the existing texture if we have one, otherwise it
/// will act like a new upload.
pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void {
assert(img.isPending());
// If we have pending data right now, free it.
if (self.getPending()) |p| {
alloc.free(p.dataSlice());
}
// If we have an existing texture, use it in the replace.
if (self.getTexture()) |t| {
self.* = .{ .replace = .{
.texture = t,
.pending = img.getPending().?,
} };
return;
}
// Otherwise we just become a pending image.
self.* = .{ .pending = img.getPending().? };
}
/// Returns true if this image is pending upload.
pub fn isPending(self: Image) bool {
return self.getPending() != null;
}
/// Returns true if this image has an associated texture.
pub fn hasTexture(self: Image) bool {
return self.getTexture() != null;
}
/// Returns true if this image is marked for unload.
pub fn isUnloading(self: Image) bool {
return switch (self) {
.unload_pending,
.unload_replace,
.unload_ready,
=> true,
.pending,
.replace,
.ready,
=> false,
};
}
/// Converts the image data to a format that can be uploaded to the GPU.
/// If the data is already in a format that can be uploaded, this is a
/// no-op.
pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
const p = self.getPendingPointer().?;
// As things stand, we currently convert all images to RGBA before
// uploading to the GPU. This just makes things easier. In the future
// we may want to support other formats.
if (p.pixel_format == .rgba) return;
// If the pending data isn't RGBA we'll need to swizzle it.
const data = p.dataSlice();
const rgba = try switch (p.pixel_format) {
.gray => wuffs.swizzle.gToRgba(alloc, data),
.gray_alpha => wuffs.swizzle.gaToRgba(alloc, data),
.rgb => wuffs.swizzle.rgbToRgba(alloc, data),
.bgr => wuffs.swizzle.bgrToRgba(alloc, data),
.rgba => unreachable,
.bgra => wuffs.swizzle.bgraToRgba(alloc, data),
};
alloc.free(data);
p.data = rgba.ptr;
p.pixel_format = .rgba;
}
/// Prepare the pending image data for upload to the GPU.
/// This doesn't need GPU access so is safe to call any time.
pub fn prepForUpload(self: *Image, alloc: Allocator) !void {
assert(self.isPending());
try self.convert(alloc);
}
/// Upload the pending image to the GPU and
/// change the state of this image to ready.
pub fn upload(
self: *Image,
alloc: Allocator,
api: *const GraphicsAPI,
) !void {
assert(self.isPending());
try self.prepForUpload(alloc);
// Get our pending info
const p = self.getPending().?;
// Create our texture
const texture = try Texture.init(
api.imageTextureOptions(.rgba, true),
@intCast(p.width),
@intCast(p.height),
p.dataSlice(),
);
// Uploaded. We can now clear our data and change our state.
//
// NOTE: For the `replace` state, this will free the old texture.
// We don't currently actually replace the existing texture
// in-place but that is an optimization we can do later.
self.deinit(alloc);
self.* = .{ .ready = texture };
}
/// Returns any pending image data for this image that requires upload.
///
/// If there is no pending data to upload, returns null.
fn getPending(self: Image) ?Pending {
return switch (self) {
.pending,
.unload_pending,
=> |p| p,
.replace,
.unload_replace,
=> |r| r.pending,
else => null,
};
}
/// Returns the texture for this image.
///
/// If there is no texture for it yet, returns null.
fn getTexture(self: Image) ?Texture {
return switch (self) {
.ready,
.unload_ready,
=> |t| t,
.replace,
.unload_replace,
=> |r| r.texture,
else => null,
};
}
// Same as getPending but returns a pointer instead of a copy.
fn getPendingPointer(self: *Image) ?*Pending {
return switch (self.*) {
.pending => return &self.pending,
.unload_pending => return &self.unload_pending,
.replace => return &self.replace.pending,
.unload_replace => return &self.unload_replace.pending,
else => null,
};
}
};

View File

@ -160,6 +160,7 @@ fn autoAttribute(T: type, attrs: objc.Object) void {
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"struct" => |e| e.backing_integer.?,
.@"enum" => |e| e.tag_type,
else => field.type,
};
@ -169,13 +170,17 @@ fn autoAttribute(T: type, attrs: objc.Object) void {
[4]u8 => mtl.MTLVertexFormat.uchar4,
[2]u16 => mtl.MTLVertexFormat.ushort2,
[2]i16 => mtl.MTLVertexFormat.short2,
f32 => mtl.MTLVertexFormat.float,
[2]f32 => mtl.MTLVertexFormat.float2,
[4]f32 => mtl.MTLVertexFormat.float4,
i32 => mtl.MTLVertexFormat.int,
[2]i32 => mtl.MTLVertexFormat.int2,
[4]i32 => mtl.MTLVertexFormat.int2,
u32 => mtl.MTLVertexFormat.uint,
[2]u32 => mtl.MTLVertexFormat.uint2,
[4]u32 => mtl.MTLVertexFormat.uint4,
u8 => mtl.MTLVertexFormat.uchar,
i8 => mtl.MTLVertexFormat.char,
else => comptime unreachable,
};

View File

@ -1,358 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../../renderer.zig");
const terminal = @import("../../terminal/main.zig");
const mtl_shaders = @import("shaders.zig");
/// The possible cell content keys that exist.
pub const Key = enum {
bg,
text,
underline,
strikethrough,
overline,
/// Returns the GPU vertex type for this key.
pub fn CellType(self: Key) type {
return switch (self) {
.bg => mtl_shaders.CellBg,
.text,
.underline,
.strikethrough,
.overline,
=> mtl_shaders.CellText,
};
}
};
/// A pool of ArrayLists with methods for bulk operations.
fn ArrayListPool(comptime T: type) type {
return struct {
const Self = ArrayListPool(T);
const ArrayListT = std.ArrayListUnmanaged(T);
// An array containing the lists that belong to this pool.
lists: []ArrayListT = &[_]ArrayListT{},
// The pool will be initialized with empty ArrayLists.
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
const self: Self = .{
.lists = try alloc.alloc(ArrayListT, list_count),
};
for (self.lists) |*list| {
list.* = try .initCapacity(alloc, initial_capacity);
}
return self;
}
pub fn deinit(self: *Self, alloc: Allocator) void {
for (self.lists) |*list| {
list.deinit(alloc);
}
alloc.free(self.lists);
}
/// Clear all lists in the pool.
pub fn reset(self: *Self) void {
for (self.lists) |*list| {
list.clearRetainingCapacity();
}
}
};
}
/// The contents of all the cells in the terminal.
///
/// The goal of this data structure is to allow for efficient row-wise
/// clearing of data from the GPU buffers, to allow for row-wise dirty
/// tracking to eliminate the overhead of rebuilding the GPU buffers
/// each frame.
///
/// Must be initialized by resizing before calling any operations.
pub const Contents = struct {
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
/// Flat array containing cell background colors for the terminal grid.
///
/// Indexed as `bg_cells[row * size.columns + col]`.
///
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
/// of directly indexing in order to avoid integer size bugs.
bg_cells: []mtl_shaders.CellBg = undefined,
/// The ArrayListPool which holds all of the foreground cells. When sized
/// with Contents.resize the individual ArrayLists are given enough room
/// that they can hold a single row with #cols glyphs, underlines, and
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
/// it is possible to exceed this with combining glyphs that add a glyph
/// but take up no column since they combine with the previous one, as
/// well as with fonts that perform multi-substitutions for glyphs, which
/// can result in a similar situation where multiple glyphs reside in the
/// same column.
///
/// Allocations should nevertheless be exceedingly rare since hitting the
/// initial capacity of a list would require a row filled with underlined
/// struck through characters, at least one of which is a multi-glyph
/// composite.
///
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
/// the pool is reserved for the cursor, which must be the first item in
/// the buffer.
///
/// Must be initialized by calling resize on the Contents struct before
/// calling any operations.
fg_rows: ArrayListPool(mtl_shaders.CellText) = .{},
pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
}
/// Resize the cell contents for the given grid size. This will
/// always invalidate the entire cell contents.
pub fn resize(
self: *Contents,
alloc: Allocator,
size: renderer.GridSize,
) !void {
self.size = size;
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
const bg_cells = try alloc.alloc(mtl_shaders.CellBg, cell_count);
errdefer alloc.free(bg_cells);
@memset(bg_cells, .{ 0, 0, 0, 0 });
// The foreground lists can hold 3 types of items:
// - Glyphs
// - Underlines
// - Strikethroughs
// So we give them an initial capacity of size.columns * 3, which will
// avoid any further allocations in the vast majority of cases. Sadly
// we can not assume capacity though, since with combining glyphs that
// form a single grapheme, and multi-substitutions in fonts, the number
// of glyphs in a row is theoretically unlimited.
//
// We have size.rows + 1 lists because index 0 is used for a special
// list containing the cursor cell which needs to be first in the buffer.
var fg_rows = try ArrayListPool(mtl_shaders.CellText).init(alloc, size.rows + 1, size.columns * 3);
errdefer fg_rows.deinit(alloc);
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
self.bg_cells = bg_cells;
self.fg_rows = fg_rows;
// We don't need 3*cols worth of cells for the cursor list, so we can
// replace it with a smaller list. This is technically a tiny bit of
// extra work but resize is not a hot function so it's worth it to not
// waste the memory.
self.fg_rows.lists[0].deinit(alloc);
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(mtl_shaders.CellText).initCapacity(alloc, 1);
}
/// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void {
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
self.fg_rows.reset();
}
/// Set the cursor value. If the value is null then the cursor is hidden.
pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void {
self.fg_rows.lists[0].clearRetainingCapacity();
if (v) |cell| {
self.fg_rows.lists[0].appendAssumeCapacity(cell);
}
}
/// Access a background cell. Prefer this function over direct indexing
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
pub inline fn bgCell(self: *Contents, row: usize, col: usize) *mtl_shaders.CellBg {
return &self.bg_cells[row * self.size.columns + col];
}
/// Add a cell to the appropriate list. Adding the same cell twice will
/// result in duplication in the vertex buffer. The caller should clear
/// the corresponding row with Contents.clear to remove old cells first.
pub fn add(
self: *Contents,
alloc: Allocator,
comptime key: Key,
cell: key.CellType(),
) !void {
const y = cell.grid_pos[1];
assert(y < self.size.rows);
switch (key) {
.bg => comptime unreachable,
.text,
.underline,
.strikethrough,
.overline,
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
}
}
/// Clear all of the cell contents for a given row.
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
assert(y < self.size.rows);
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
self.fg_rows.lists[y + 1].clearRetainingCapacity();
}
};
test Contents {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c: Contents = .{};
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// We should start off empty after resizing.
for (0..rows) |y| {
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
for (0..cols) |x| {
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
}
}
// And the cursor row should have a capacity of 1 and also be empty.
try testing.expect(c.fg_rows.lists[0].capacity == 1);
try testing.expect(c.fg_rows.lists[0].items.len == 0);
// Add some contents.
const bg_cell: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(1, 4).* = bg_cell;
try c.add(alloc, .text, fg_cell);
try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
// The fg row index is offset by 1 because of the cursor list.
try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
// And we should be able to clear it.
c.clear(1);
for (0..rows) |y| {
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
for (0..cols) |x| {
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
}
}
// Add a cursor.
const cursor_cell: mtl_shaders.CellText = .{
.mode = .cursor,
.grid_pos = .{ 2, 3 },
.color = .{ 0, 0, 0, 1 },
};
c.setCursor(cursor_cell);
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
// And remove it.
c.setCursor(null);
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
}
test "Contents clear retains other content" {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c: Contents = .{};
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Set some contents
// bg and fg cells in row 1
const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(1, 4).* = bg_cell_1;
try c.add(alloc, .text, fg_cell_1);
// bg and fg cells in row 2
const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_2: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 2 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(2, 4).* = bg_cell_2;
try c.add(alloc, .text, fg_cell_2);
// Clear row 1, this should leave row 2 untouched
c.clear(1);
// Row 2 should still contain its cells.
try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
// Fg row index is +1 because of cursor list at start
try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
}
test "Contents clear last added content" {
const testing = std.testing;
const alloc = testing.allocator;
const rows = 10;
const cols = 10;
var c: Contents = .{};
try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc);
// Set some contents
// bg and fg cells in row 1
const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(1, 4).* = bg_cell_1;
try c.add(alloc, .text, fg_cell_1);
// bg and fg cells in row 2
const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_2: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 2 },
.color = .{ 0, 0, 0, 1 },
};
c.bgCell(2, 4).* = bg_cell_2;
try c.add(alloc, .text, fg_cell_2);
// Clear row 2, this should leave row 1 untouched
c.clear(2);
// Row 1 should still contain its cells.
try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
// Fg row index is +1 because of cursor list at start
try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
}

View File

@ -1,424 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const objc = @import("objc");
const wuffs = @import("wuffs");
const Metal = @import("../Metal.zig");
const Texture = Metal.Texture;
const mtl = @import("api.zig");
/// Represents a single image placement on the grid. A placement is a
/// request to render an instance of an image.
pub const Placement = struct {
/// The image being rendered. This MUST be in the image map.
image_id: u32,
/// The grid x/y where this placement is located.
x: i32,
y: i32,
z: i32,
/// The width/height of the placed image.
width: u32,
height: u32,
/// The offset in pixels from the top left of the cell.
/// This is clamped to the size of a cell.
cell_offset_x: u32,
cell_offset_y: u32,
/// The source rectangle of the placement.
source_x: u32,
source_y: u32,
source_width: u32,
source_height: u32,
};
/// The map used for storing images.
pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct {
image: Image,
transmit_time: std.time.Instant,
});
/// The state for a single image that is to be rendered. The image can be
/// pending upload or ready to use with a texture.
pub const Image = union(enum) {
/// The image is pending upload to the GPU. The different keys are
/// different formats since some formats aren't accepted by the GPU
/// and require conversion.
///
/// This data is owned by this union so it must be freed once the
/// image is uploaded.
pending_gray: Pending,
pending_gray_alpha: Pending,
pending_rgb: Pending,
pending_rgba: Pending,
/// This is the same as the pending states but there is a texture
/// already allocated that we want to replace.
replace_gray: Replace,
replace_gray_alpha: Replace,
replace_rgb: Replace,
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
unload_ready: Texture,
unload_replace: struct { []u8, Texture },
pub const Replace = struct {
texture: Texture,
pending: Pending,
};
/// Pending image data that needs to be uploaded to the GPU.
pub const Pending = struct {
height: u32,
width: u32,
/// Data is always expected to be (width * height * depth). Depth
/// is based on the union key.
data: [*]u8,
pub fn dataSlice(self: Pending, d: u32) []u8 {
return self.data[0..self.len(d)];
}
pub fn len(self: Pending, d: u32) u32 {
return self.width * self.height * d;
}
};
pub fn deinit(self: Image, alloc: Allocator) void {
switch (self) {
.pending_gray => |p| alloc.free(p.dataSlice(1)),
.pending_gray_alpha => |p| alloc.free(p.dataSlice(2)),
.pending_rgb => |p| alloc.free(p.dataSlice(3)),
.pending_rgba => |p| alloc.free(p.dataSlice(4)),
.unload_pending => |data| alloc.free(data),
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
r[1].deinit();
},
.ready,
.unload_ready,
=> |t| t.deinit(),
}
}
/// Mark this image for unload whatever state it is in.
pub fn markForUnload(self: *Image) void {
self.* = switch (self.*) {
.unload_pending,
.unload_replace,
.unload_ready,
=> return,
.ready => |obj| .{ .unload_ready = obj },
.pending_gray => |p| .{ .unload_pending = p.dataSlice(1) },
.pending_gray_alpha => |p| .{ .unload_pending = p.dataSlice(2) },
.pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) },
.pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) },
.replace_gray => |r| .{ .unload_replace = .{
r.pending.dataSlice(1), r.texture,
} },
.replace_gray_alpha => |r| .{ .unload_replace = .{
r.pending.dataSlice(2), r.texture,
} },
.replace_rgb => |r| .{ .unload_replace = .{
r.pending.dataSlice(3), r.texture,
} },
.replace_rgba => |r| .{ .unload_replace = .{
r.pending.dataSlice(4), r.texture,
} },
};
}
/// Replace the currently pending image with a new one. This will
/// attempt to update the existing texture if it is already allocated.
/// If the texture is not allocated, this will act like a new upload.
///
/// This function only marks the image for replace. The actual logic
/// to replace is done later.
pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void {
assert(img.pending() != null);
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending
// ourselves.
.pending_gray => |p| {
alloc.free(p.dataSlice(1));
self.* = img;
return;
},
.pending_gray_alpha => |p| {
alloc.free(p.dataSlice(2));
self.* = img;
return;
},
.pending_rgb => |p| {
alloc.free(p.dataSlice(3));
self.* = img;
return;
},
.pending_rgba => |p| {
alloc.free(p.dataSlice(4));
self.* = img;
return;
},
// If we're marked for unload but we just have pending data,
// this behaves the same as a normal "pending": free the data,
// become new pending.
.unload_pending => |data| {
alloc.free(data);
self.* = img;
return;
},
.unload_replace => |r| existing: {
alloc.free(r[0]);
break :existing r[1];
},
// If we were already pending a replacement, then we free our
// existing pending data and use the same texture.
.replace_gray => |r| existing: {
alloc.free(r.pending.dataSlice(1));
break :existing r.texture;
},
.replace_gray_alpha => |r| existing: {
alloc.free(r.pending.dataSlice(2));
break :existing r.texture;
},
.replace_rgb => |r| existing: {
alloc.free(r.pending.dataSlice(3));
break :existing r.texture;
},
.replace_rgba => |r| existing: {
alloc.free(r.pending.dataSlice(4));
break :existing r.texture;
},
// For both ready and unload_ready, we need to replace the
// texture. We can't do that here, so we just mark ourselves
// for replacement.
.ready, .unload_ready => |tex| tex,
};
// We now have an existing texture, so set the proper replace key.
self.* = switch (img) {
.pending_gray => |p| .{ .replace_gray = .{
.texture = existing,
.pending = p,
} },
.pending_gray_alpha => |p| .{ .replace_gray_alpha = .{
.texture = existing,
.pending = p,
} },
.pending_rgb => |p| .{ .replace_rgb = .{
.texture = existing,
.pending = p,
} },
.pending_rgba => |p| .{ .replace_rgba = .{
.texture = existing,
.pending = p,
} },
else => unreachable,
};
}
/// Returns true if this image is pending upload.
pub fn isPending(self: Image) bool {
return self.pending() != null;
}
/// Returns true if this image is pending an unload.
pub fn isUnloading(self: Image) bool {
return switch (self) {
.unload_pending,
.unload_ready,
=> true,
.ready,
.pending_rgb,
.pending_rgba,
=> false,
};
}
/// Converts the image data to a format that can be uploaded to the GPU.
/// If the data is already in a format that can be uploaded, this is a
/// no-op.
pub fn convert(self: *Image, alloc: Allocator) !void {
switch (self.*) {
.ready,
.unload_pending,
.unload_replace,
.unload_ready,
=> unreachable, // invalid
.pending_rgba,
.replace_rgba,
=> {}, // ready
// RGB needs to be converted to RGBA because Metal textures
// don't support RGB.
.pending_rgb => |*p| {
const data = p.dataSlice(3);
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
alloc.free(data);
p.data = rgba.ptr;
self.* = .{ .pending_rgba = p.* };
},
.replace_rgb => |*r| {
const data = r.pending.dataSlice(3);
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
alloc.free(data);
r.pending.data = rgba.ptr;
self.* = .{ .replace_rgba = r.* };
},
// Gray and Gray+Alpha need to be converted to RGBA, too.
.pending_gray => |*p| {
const data = p.dataSlice(1);
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
alloc.free(data);
p.data = rgba.ptr;
self.* = .{ .pending_rgba = p.* };
},
.replace_gray => |*r| {
const data = r.pending.dataSlice(2);
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
alloc.free(data);
r.pending.data = rgba.ptr;
self.* = .{ .replace_rgba = r.* };
},
.pending_gray_alpha => |*p| {
const data = p.dataSlice(2);
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
alloc.free(data);
p.data = rgba.ptr;
self.* = .{ .pending_rgba = p.* };
},
.replace_gray_alpha => |*r| {
const data = r.pending.dataSlice(2);
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
alloc.free(data);
r.pending.data = rgba.ptr;
self.* = .{ .replace_rgba = r.* };
},
}
}
/// Upload the pending image to the GPU and change the state of this
/// image to ready.
pub fn upload(
self: *Image,
alloc: Allocator,
metal: *const Metal,
) !void {
const device = metal.device;
const storage_mode = metal.default_storage_mode;
// Convert our data if we have to
try self.convert(alloc);
// Get our pending info
const p = self.pending().?;
// Create our texture
const texture = try Texture.init(
.{
.device = device,
.pixel_format = .rgba8unorm_srgb,
.resource_options = .{
// Indicate that the CPU writes to this resource but never reads it.
.cpu_cache_mode = .write_combined,
.storage_mode = storage_mode,
},
},
@intCast(p.width),
@intCast(p.height),
p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.
//
// NOTE: For "replace_*" states, this will free the old texture.
// We don't currently actually replace the existing texture in-place
// but that is an optimization we can do later.
self.deinit(alloc);
self.* = .{ .ready = texture };
}
/// Our pixel depth
fn depth(self: Image) u32 {
return switch (self) {
.pending_rgb => 3,
.pending_rgba => 4,
.replace_rgb => 3,
.replace_rgba => 4,
else => unreachable,
};
}
/// Returns true if this image is in a pending state and requires upload.
fn pending(self: Image) ?Pending {
return switch (self) {
.pending_rgb,
.pending_rgba,
=> |p| p,
.replace_rgb,
.replace_rgba,
=> |r| r.pending,
else => null,
};
}
};

View File

@ -10,20 +10,97 @@ const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.metal);
const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
&.{
.{ "bg_color", .{
.vertex_fn = "full_screen_vertex",
.fragment_fn = "bg_color_fragment",
.blending_enabled = false,
} },
.{ "cell_bg", .{
.vertex_fn = "full_screen_vertex",
.fragment_fn = "cell_bg_fragment",
.blending_enabled = true,
} },
.{ "cell_text", .{
.vertex_attributes = CellText,
.vertex_fn = "cell_text_vertex",
.fragment_fn = "cell_text_fragment",
.step_fn = .per_instance,
.blending_enabled = true,
} },
.{ "image", .{
.vertex_attributes = Image,
.vertex_fn = "image_vertex",
.fragment_fn = "image_fragment",
.step_fn = .per_instance,
.blending_enabled = true,
} },
.{ "bg_image", .{
.vertex_attributes = BgImage,
.vertex_fn = "bg_image_vertex",
.fragment_fn = "bg_image_fragment",
.step_fn = .per_instance,
.blending_enabled = true,
} },
};
/// All the comptime-known info about a pipeline, so that
/// we can define them ahead-of-time in an ergonomic way.
const PipelineDescription = struct {
vertex_attributes: ?type = null,
vertex_fn: []const u8,
fragment_fn: []const u8,
step_fn: mtl.MTLVertexStepFunction = .per_vertex,
blending_enabled: bool,
fn initPipeline(
self: PipelineDescription,
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !Pipeline {
return try .init(self.vertex_attributes, .{
.device = device,
.vertex_fn = self.vertex_fn,
.fragment_fn = self.fragment_fn,
.vertex_library = library,
.fragment_library = library,
.step_fn = self.step_fn,
.attachments = &.{.{
.pixel_format = pixel_format,
.blending_enabled = self.blending_enabled,
}},
});
}
};
/// We create a type for the pipeline collection based on our desc array.
const PipelineCollection = t: {
var fields: [pipeline_descs.len]std.builtin.Type.StructField = undefined;
for (pipeline_descs, 0..) |pipeline, i| {
fields[i] = .{
.name = pipeline[0],
.type = Pipeline,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(Pipeline),
};
}
break :t @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = false,
} });
};
/// This contains the state for the shaders used by the Metal renderer.
pub const Shaders = struct {
library: objc.Object,
/// Renders cell foreground elements (text, decorations).
cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
image_pipeline: Pipeline,
/// Collection of available render pipelines.
pipelines: PipelineCollection,
/// Custom shaders to run against the final drawable texture. This
/// can be used to apply a lot of effects. Each shader is run in sequence
@ -48,14 +125,24 @@ pub const Shaders = struct {
const library = try initLibrary(device);
errdefer library.msgSend(void, objc.sel("release"), .{});
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
errdefer cell_text_pipeline.deinit();
var pipelines: PipelineCollection = undefined;
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
errdefer cell_bg_pipeline.deinit();
var initialized_pipelines: usize = 0;
const image_pipeline = try initImagePipeline(device, library, pixel_format);
errdefer image_pipeline.deinit();
errdefer inline for (pipeline_descs, 0..) |pipeline, i| {
if (i < initialized_pipelines) {
@field(pipelines, pipeline[0]).deinit();
}
};
inline for (pipeline_descs) |pipeline| {
@field(pipelines, pipeline[0]) = try pipeline[1].initPipeline(
device,
library,
pixel_format,
);
initialized_pipelines += 1;
}
const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
@ -77,9 +164,7 @@ pub const Shaders = struct {
return .{
.library = library,
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.pipelines = pipelines,
.post_pipelines = post_pipelines,
};
}
@ -89,9 +174,9 @@ pub const Shaders = struct {
self.defunct = true;
// Release our primary shaders
self.cell_text_pipeline.deinit();
self.cell_bg_pipeline.deinit();
self.image_pipeline.deinit();
inline for (pipeline_descs) |pipeline| {
@field(self.pipelines, pipeline[0]).deinit();
}
self.library.msgSend(void, objc.sel("release"), .{});
// Release our postprocess shaders
@ -104,15 +189,7 @@ pub const Shaders = struct {
}
};
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32,
cell_offset: [2]f32,
source_rect: [4]f32,
dest_size: [2]f32,
};
/// The uniforms that are passed to the terminal cell shader.
/// The uniforms that are passed to our shaders.
pub const Uniforms = extern struct {
// Note: all of the explicit alignments are copied from the
// MSL developer reference just so that we can be sure that we got
@ -122,6 +199,9 @@ pub const Uniforms = extern struct {
/// This is calculated based on the size of the screen.
projection_matrix: math.Mat align(16),
/// Size of the screen (render target) in pixels.
screen_size: [2]f32 align(8),
/// Size of a single cell in pixels, unscaled.
cell_size: [2]f32 align(8),
@ -182,6 +262,74 @@ pub const Uniforms = extern struct {
};
};
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(1),
constraint_width: u8 align(1) = 0,
pub const Mode = enum(u8) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
};
test {
// Minimizing the size of this struct is important,
// so we test it in order to be aware of any changes.
try std.testing.expectEqual(32, @sizeOf(CellText));
}
};
/// This is a single parameter for the cell bg shader.
pub const CellBg = [4]u8;
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32,
cell_offset: [2]f32,
source_rect: [4]f32,
dest_size: [2]f32,
};
/// Single parameter for the bg image shader.
pub const BgImage = extern struct {
opacity: f32 align(4),
info: Info align(1),
pub const Info = packed struct(u8) {
position: Position,
fit: Fit,
repeat: bool,
_padding: u1 = 0,
pub const Position = enum(u4) {
tl = 0,
tc = 1,
tr = 2,
ml = 3,
mc = 4,
mr = 5,
bl = 6,
bc = 7,
br = 8,
};
pub const Fit = enum(u2) {
contain = 0,
cover = 1,
stretch = 2,
none = 3,
};
};
};
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
fn initLibrary(device: objc.Object) !objc.Object {
const start = try std.time.Instant.now();
@ -294,99 +442,6 @@ fn initPostPipeline(
});
}
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(1),
constraint_width: u8 align(1) = 0,
pub const Mode = enum(u8) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
};
test {
// Minimizing the size of this struct is important,
// so we test it in order to be aware of any changes.
try std.testing.expectEqual(32, @sizeOf(CellText));
}
};
/// Initialize the cell render pipeline for our shader library.
fn initCellTextPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !Pipeline {
return try Pipeline.init(CellText, .{
.device = device,
.vertex_fn = "cell_text_vertex",
.fragment_fn = "cell_text_fragment",
.vertex_library = library,
.fragment_library = library,
.step_fn = .per_instance,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = true,
},
},
});
}
/// This is a single parameter for the cell bg shader.
pub const CellBg = [4]u8;
/// Initialize the cell background render pipeline for our shader library.
fn initCellBgPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !Pipeline {
return try Pipeline.init(null, .{
.device = device,
.vertex_fn = "cell_bg_vertex",
.fragment_fn = "cell_bg_fragment",
.vertex_library = library,
.fragment_library = library,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = false,
},
},
});
}
/// Initialize the image render pipeline for our shader library.
fn initImagePipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
) !Pipeline {
return try Pipeline.init(Image, .{
.device = device,
.vertex_fn = "image_vertex",
.fragment_fn = "image_fragment",
.vertex_library = library,
.fragment_library = library,
.step_fn = .per_instance,
.attachments = &.{
.{
.pixel_format = pixel_format,
.blending_enabled = true,
},
},
});
}
fn checkError(err_: ?*anyopaque) !void {
const nserr = objc.Object.fromId(err_ orelse return);
const str = @as(

View File

@ -98,6 +98,7 @@ fn autoAttribute(
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"struct" => |s| s.backing_integer.?,
.@"enum" => |e| e.tag_type,
else => field.type,
};

View File

@ -57,7 +57,6 @@ pub fn init(
opts.internal_format,
@intCast(width),
@intCast(height),
0,
opts.format,
.UnsignedByte,
if (data) |d| @ptrCast(d.ptr) else null,

View File

@ -1,220 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../../renderer.zig");
const terminal = @import("../../terminal/main.zig");
const shaderpkg = @import("shaders.zig");
/// The possible cell content keys that exist.
pub const Key = enum {
bg,
text,
underline,
strikethrough,
overline,
/// Returns the GPU vertex type for this key.
pub fn CellType(self: Key) type {
return switch (self) {
.bg => shaderpkg.CellBg,
.text,
.underline,
.strikethrough,
.overline,
=> shaderpkg.CellText,
};
}
};
/// A pool of ArrayLists with methods for bulk operations.
fn ArrayListPool(comptime T: type) type {
return struct {
const Self = ArrayListPool(T);
const ArrayListT = std.ArrayListUnmanaged(T);
// An array containing the lists that belong to this pool.
lists: []ArrayListT = &[_]ArrayListT{},
// The pool will be initialized with empty ArrayLists.
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
const self: Self = .{
.lists = try alloc.alloc(ArrayListT, list_count),
};
for (self.lists) |*list| {
list.* = try ArrayListT.initCapacity(alloc, initial_capacity);
}
return self;
}
pub fn deinit(self: *Self, alloc: Allocator) void {
for (self.lists) |*list| {
list.deinit(alloc);
}
alloc.free(self.lists);
}
/// Clear all lists in the pool.
pub fn reset(self: *Self) void {
for (self.lists) |*list| {
list.clearRetainingCapacity();
}
}
};
}
/// The contents of all the cells in the terminal.
///
/// The goal of this data structure is to allow for efficient row-wise
/// clearing of data from the GPU buffers, to allow for row-wise dirty
/// tracking to eliminate the overhead of rebuilding the GPU buffers
/// each frame.
///
/// Must be initialized by resizing before calling any operations.
pub const Contents = struct {
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
/// Flat array containing cell background colors for the terminal grid.
///
/// Indexed as `bg_cells[row * size.columns + col]`.
///
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
/// of directly indexing in order to avoid integer size bugs.
bg_cells: []shaderpkg.CellBg = undefined,
/// The ArrayListPool which holds all of the foreground cells. When sized
/// with Contents.resize the individual ArrayLists are given enough room
/// that they can hold a single row with #cols glyphs, underlines, and
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
/// it is possible to exceed this with combining glyphs that add a glyph
/// but take up no column since they combine with the previous one, as
/// well as with fonts that perform multi-substitutions for glyphs, which
/// can result in a similar situation where multiple glyphs reside in the
/// same column.
///
/// Allocations should nevertheless be exceedingly rare since hitting the
/// initial capacity of a list would require a row filled with underlined
/// struck through characters, at least one of which is a multi-glyph
/// composite.
///
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
/// the pool is reserved for the cursor, which must be the first item in
/// the buffer.
///
/// Must be initialized by calling resize on the Contents struct before
/// calling any operations.
fg_rows: ArrayListPool(shaderpkg.CellText) = .{},
pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
}
/// Resize the cell contents for the given grid size. This will
/// always invalidate the entire cell contents.
pub fn resize(
self: *Contents,
alloc: Allocator,
size: renderer.GridSize,
) !void {
self.size = size;
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
errdefer alloc.free(bg_cells);
@memset(bg_cells, .{ 0, 0, 0, 0 });
// The foreground lists can hold 3 types of items:
// - Glyphs
// - Underlines
// - Strikethroughs
// So we give them an initial capacity of size.columns * 3, which will
// avoid any further allocations in the vast majority of cases. Sadly
// we can not assume capacity though, since with combining glyphs that
// form a single grapheme, and multi-substitutions in fonts, the number
// of glyphs in a row is theoretically unlimited.
//
// We have size.rows + 1 lists because index 0 is used for a special
// list containing the cursor cell which needs to be first in the buffer.
var fg_rows = try ArrayListPool(shaderpkg.CellText).init(alloc, size.rows + 1, size.columns * 3);
errdefer fg_rows.deinit(alloc);
alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc);
self.bg_cells = bg_cells;
self.fg_rows = fg_rows;
// We don't need 3*cols worth of cells for the cursor list, so we can
// replace it with a smaller list. This is technically a tiny bit of
// extra work but resize is not a hot function so it's worth it to not
// waste the memory.
self.fg_rows.lists[0].deinit(alloc);
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(shaderpkg.CellText).initCapacity(alloc, 1);
}
/// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void {
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
self.fg_rows.reset();
}
/// Set the cursor value. If the value is null then the cursor is hidden.
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
self.fg_rows.lists[0].clearRetainingCapacity();
if (v) |cell| {
self.fg_rows.lists[0].appendAssumeCapacity(cell);
}
}
/// Access a background cell. Prefer this function over direct indexing
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
pub inline fn bgCell(self: *Contents, row: usize, col: usize) *shaderpkg.CellBg {
return &self.bg_cells[row * self.size.columns + col];
}
/// Add a cell to the appropriate list. Adding the same cell twice will
/// result in duplication in the vertex buffer. The caller should clear
/// the corresponding row with Contents.clear to remove old cells first.
pub fn add(
self: *Contents,
alloc: Allocator,
comptime key: Key,
cell: key.CellType(),
) !void {
const y = cell.grid_pos[1];
assert(y < self.size.rows);
switch (key) {
.bg => comptime unreachable,
.text,
.underline,
.strikethrough,
.overline,
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
}
}
/// Clear all of the cell contents for a given row.
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
assert(y < self.size.rows);
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
// We have a special list containing the cursor cell at the start
// of our fg row pool, so we need to add 1 to the y to get the
// correct index.
self.fg_rows.lists[y + 1].clearRetainingCapacity();
}
};

View File

@ -1,423 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gl = @import("opengl");
const wuffs = @import("wuffs");
const OpenGL = @import("../OpenGL.zig");
const Texture = OpenGL.Texture;
/// Represents a single image placement on the grid. A placement is a
/// request to render an instance of an image.
pub const Placement = struct {
/// The image being rendered. This MUST be in the image map.
image_id: u32,
/// The grid x/y where this placement is located.
x: i32,
y: i32,
z: i32,
/// The width/height of the placed image.
width: u32,
height: u32,
/// The offset in pixels from the top left of the cell. This is
/// clamped to the size of a cell.
cell_offset_x: u32,
cell_offset_y: u32,
/// The source rectangle of the placement.
source_x: u32,
source_y: u32,
source_width: u32,
source_height: u32,
};
/// The map used for storing images.
pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct {
image: Image,
transmit_time: std.time.Instant,
});
/// The state for a single image that is to be rendered. The image can be
/// pending upload or ready to use with a texture.
pub const Image = union(enum) {
/// The image is pending upload to the GPU. The different keys are
/// different formats since some formats aren't accepted by the GPU
/// and require conversion.
///
/// This data is owned by this union so it must be freed once the
/// image is uploaded.
pending_gray: Pending,
pending_gray_alpha: Pending,
pending_rgb: Pending,
pending_rgba: Pending,
/// This is the same as the pending states but there is a texture
/// already allocated that we want to replace.
replace_gray: Replace,
replace_gray_alpha: Replace,
replace_rgb: Replace,
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
unload_ready: Texture,
unload_replace: struct { []u8, Texture },
pub const Replace = struct {
texture: Texture,
pending: Pending,
};
/// Pending image data that needs to be uploaded to the GPU.
pub const Pending = struct {
height: u32,
width: u32,
/// Data is always expected to be (width * height * depth). Depth
/// is based on the union key.
data: [*]u8,
pub fn dataSlice(self: Pending, d: u32) []u8 {
return self.data[0..self.len(d)];
}
pub fn len(self: Pending, d: u32) u32 {
return self.width * self.height * d;
}
};
pub fn deinit(self: Image, alloc: Allocator) void {
switch (self) {
.pending_gray => |p| alloc.free(p.dataSlice(1)),
.pending_gray_alpha => |p| alloc.free(p.dataSlice(2)),
.pending_rgb => |p| alloc.free(p.dataSlice(3)),
.pending_rgba => |p| alloc.free(p.dataSlice(4)),
.unload_pending => |data| alloc.free(data),
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
r[1].deinit();
},
.ready,
.unload_ready,
=> |tex| tex.deinit(),
}
}
/// Mark this image for unload whatever state it is in.
pub fn markForUnload(self: *Image) void {
self.* = switch (self.*) {
.unload_pending,
.unload_replace,
.unload_ready,
=> return,
.ready => |obj| .{ .unload_ready = obj },
.pending_gray => |p| .{ .unload_pending = p.dataSlice(1) },
.pending_gray_alpha => |p| .{ .unload_pending = p.dataSlice(2) },
.pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) },
.pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) },
.replace_gray => |r| .{ .unload_replace = .{
r.pending.dataSlice(1), r.texture,
} },
.replace_gray_alpha => |r| .{ .unload_replace = .{
r.pending.dataSlice(2), r.texture,
} },
.replace_rgb => |r| .{ .unload_replace = .{
r.pending.dataSlice(3), r.texture,
} },
.replace_rgba => |r| .{ .unload_replace = .{
r.pending.dataSlice(4), r.texture,
} },
};
}
/// Replace the currently pending image with a new one. This will
/// attempt to update the existing texture if it is already allocated.
/// If the texture is not allocated, this will act like a new upload.
///
/// This function only marks the image for replace. The actual logic
/// to replace is done later.
pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void {
assert(img.pending() != null);
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending ourselves.
.pending_gray => |p| {
alloc.free(p.dataSlice(1));
self.* = img;
return;
},
.pending_gray_alpha => |p| {
alloc.free(p.dataSlice(2));
self.* = img;
return;
},
.pending_rgb => |p| {
alloc.free(p.dataSlice(3));
self.* = img;
return;
},
.pending_rgba => |p| {
alloc.free(p.dataSlice(4));
self.* = img;
return;
},
// If we're marked for unload but we just have pending data,
// this behaves the same as a normal "pending": free the data,
// become new pending.
.unload_pending => |data| {
alloc.free(data);
self.* = img;
return;
},
.unload_replace => |r| existing: {
alloc.free(r[0]);
break :existing r[1];
},
// If we were already pending a replacement, then we free our
// existing pending data and use the same texture.
.replace_gray => |r| existing: {
alloc.free(r.pending.dataSlice(1));
break :existing r.texture;
},
.replace_gray_alpha => |r| existing: {
alloc.free(r.pending.dataSlice(2));
break :existing r.texture;
},
.replace_rgb => |r| existing: {
alloc.free(r.pending.dataSlice(3));
break :existing r.texture;
},
.replace_rgba => |r| existing: {
alloc.free(r.pending.dataSlice(4));
break :existing r.texture;
},
// For both ready and unload_ready, we need to replace the
// texture. We can't do that here, so we just mark ourselves
// for replacement.
.ready, .unload_ready => |tex| tex,
};
// We now have an existing texture, so set the proper replace key.
self.* = switch (img) {
.pending_gray => |p| .{ .replace_gray = .{
.texture = existing,
.pending = p,
} },
.pending_gray_alpha => |p| .{ .replace_gray_alpha = .{
.texture = existing,
.pending = p,
} },
.pending_rgb => |p| .{ .replace_rgb = .{
.texture = existing,
.pending = p,
} },
.pending_rgba => |p| .{ .replace_rgba = .{
.texture = existing,
.pending = p,
} },
else => unreachable,
};
}
/// Returns true if this image is pending upload.
pub fn isPending(self: Image) bool {
return self.pending() != null;
}
/// Returns true if this image is pending an unload.
pub fn isUnloading(self: Image) bool {
return switch (self) {
.unload_pending,
.unload_ready,
=> true,
.ready,
.pending_gray,
.pending_gray_alpha,
.pending_rgb,
.pending_rgba,
=> false,
};
}
/// Converts the image data to a format that can be uploaded to the GPU.
/// If the data is already in a format that can be uploaded, this is a
/// no-op.
pub fn convert(self: *Image, alloc: Allocator) !void {
switch (self.*) {
.ready,
.unload_pending,
.unload_replace,
.unload_ready,
=> unreachable, // invalid
.pending_rgba,
.replace_rgba,
=> {}, // ready
// RGB needs to be converted to RGBA because Metal textures
// don't support RGB.
.pending_rgb => |*p| {
const data = p.dataSlice(3);
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
alloc.free(data);
p.data = rgba.ptr;
self.* = .{ .pending_rgba = p.* };
},
.replace_rgb => |*r| {
const data = r.pending.dataSlice(3);
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
alloc.free(data);
r.pending.data = rgba.ptr;
self.* = .{ .replace_rgba = r.* };
},
// Gray and Gray+Alpha need to be converted to RGBA, too.
.pending_gray => |*p| {
const data = p.dataSlice(1);
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
alloc.free(data);
p.data = rgba.ptr;
self.* = .{ .pending_rgba = p.* };
},
.replace_gray => |*r| {
const data = r.pending.dataSlice(2);
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
alloc.free(data);
r.pending.data = rgba.ptr;
self.* = .{ .replace_rgba = r.* };
},
.pending_gray_alpha => |*p| {
const data = p.dataSlice(2);
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
alloc.free(data);
p.data = rgba.ptr;
self.* = .{ .pending_rgba = p.* };
},
.replace_gray_alpha => |*r| {
const data = r.pending.dataSlice(2);
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
alloc.free(data);
r.pending.data = rgba.ptr;
self.* = .{ .replace_rgba = r.* };
},
}
}
/// Upload the pending image to the GPU and change the state of this
/// image to ready.
pub fn upload(
self: *Image,
alloc: Allocator,
opengl: *const OpenGL,
) !void {
_ = opengl;
// Convert our data if we have to
try self.convert(alloc);
// Get our pending info
const p = self.pending().?;
// Get our format
const formats: struct {
internal: gl.Texture.InternalFormat,
format: gl.Texture.Format,
} = switch (self.*) {
.pending_rgb, .replace_rgb => .{ .internal = .srgb, .format = .rgb },
.pending_rgba, .replace_rgba => .{ .internal = .srgba, .format = .rgba },
else => unreachable,
};
// Create our texture
const tex = try Texture.init(
.{
.format = formats.format,
.internal_format = formats.internal,
.target = .Rectangle,
},
@intCast(p.width),
@intCast(p.height),
p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.
self.deinit(alloc);
self.* = .{ .ready = tex };
}
/// Our pixel depth
fn depth(self: Image) u32 {
return switch (self) {
.pending_rgb => 3,
.pending_rgba => 4,
.replace_rgb => 3,
.replace_rgba => 4,
else => unreachable,
};
}
/// Returns true if this image is in a pending state and requires upload.
fn pending(self: Image) ?Pending {
return switch (self) {
.pending_rgb,
.pending_rgba,
=> |p| p,
.replace_rgb,
.replace_rgba,
=> |r| r.pending,
else => null,
};
}
};

View File

@ -7,18 +7,84 @@ const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.opengl);
const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
&.{
.{ "bg_color", .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/bg_color.f.glsl"),
.blending_enabled = false,
} },
.{ "cell_bg", .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
.blending_enabled = true,
} },
.{ "cell_text", .{
.vertex_attributes = CellText,
.vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
.step_fn = .per_instance,
.blending_enabled = true,
} },
.{ "image", .{
.vertex_attributes = Image,
.vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
.step_fn = .per_instance,
.blending_enabled = true,
} },
.{ "bg_image", .{
.vertex_attributes = BgImage,
.vertex_fn = loadShaderCode("../shaders/glsl/bg_image.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/bg_image.f.glsl"),
.step_fn = .per_instance,
.blending_enabled = true,
} },
};
/// All the comptime-known info about a pipeline, so that
/// we can define them ahead-of-time in an ergonomic way.
const PipelineDescription = struct {
vertex_attributes: ?type = null,
vertex_fn: [:0]const u8,
fragment_fn: [:0]const u8,
step_fn: Pipeline.Options.StepFunction = .per_vertex,
blending_enabled: bool = true,
fn initPipeline(self: PipelineDescription) !Pipeline {
return try .init(self.vertex_attributes, .{
.vertex_fn = self.vertex_fn,
.fragment_fn = self.fragment_fn,
.step_fn = self.step_fn,
.blending_enabled = self.blending_enabled,
});
}
};
/// We create a type for the pipeline collection based on our desc array.
const PipelineCollection = t: {
var fields: [pipeline_descs.len]std.builtin.Type.StructField = undefined;
for (pipeline_descs, 0..) |pipeline, i| {
fields[i] = .{
.name = pipeline[0],
.type = Pipeline,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(Pipeline),
};
}
break :t @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = false,
} });
};
/// This contains the state for the shaders used by the Metal renderer.
pub const Shaders = struct {
/// Renders cell foreground elements (text, decorations).
cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
image_pipeline: Pipeline,
/// Collection of available render pipelines.
pipelines: PipelineCollection,
/// Custom shaders to run against the final drawable texture. This
/// can be used to apply a lot of effects. Each shader is run in sequence
@ -38,14 +104,20 @@ pub const Shaders = struct {
alloc: Allocator,
post_shaders: []const [:0]const u8,
) !Shaders {
const cell_text_pipeline = try initCellTextPipeline();
errdefer cell_text_pipeline.deinit();
var pipelines: PipelineCollection = undefined;
const cell_bg_pipeline = try initCellBgPipeline();
errdefer cell_bg_pipeline.deinit();
var initialized_pipelines: usize = 0;
const image_pipeline = try initImagePipeline();
errdefer image_pipeline.deinit();
errdefer inline for (pipeline_descs, 0..) |pipeline, i| {
if (i < initialized_pipelines) {
@field(pipelines, pipeline[0]).deinit();
}
};
inline for (pipeline_descs) |pipeline| {
@field(pipelines, pipeline[0]) = try pipeline[1].initPipeline();
initialized_pipelines += 1;
}
const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
@ -63,9 +135,7 @@ pub const Shaders = struct {
};
return .{
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.pipelines = pipelines,
.post_pipelines = post_pipelines,
};
}
@ -75,9 +145,9 @@ pub const Shaders = struct {
self.defunct = true;
// Release our primary shaders
self.cell_text_pipeline.deinit();
self.cell_bg_pipeline.deinit();
self.image_pipeline.deinit();
inline for (pipeline_descs) |pipeline| {
@field(self.pipelines, pipeline[0]).deinit();
}
// Release our postprocess shaders
if (self.post_pipelines.len > 0) {
@ -89,20 +159,15 @@ pub const Shaders = struct {
}
};
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32 align(8),
cell_offset: [2]f32 align(8),
source_rect: [4]f32 align(16),
dest_size: [2]f32 align(8),
};
/// The uniforms that are passed to the terminal cell shader.
/// The uniforms that are passed to our shaders.
pub const Uniforms = extern struct {
/// The projection matrix for turning world coordinates to normalized.
/// This is calculated based on the size of the screen.
projection_matrix: math.Mat align(16),
/// Size of the screen (render target) in pixels.
screen_size: [2]f32 align(8),
/// Size of a single cell in pixels, unscaled.
cell_size: [2]f32 align(8),
@ -165,6 +230,74 @@ pub const Uniforms = extern struct {
};
};
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(4),
constraint_width: u32 align(4) = 0,
pub const Mode = enum(u32) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
};
// test {
// // Minimizing the size of this struct is important,
// // so we test it in order to be aware of any changes.
// try std.testing.expectEqual(32, @sizeOf(CellText));
// }
};
/// This is a single parameter for the cell bg shader.
pub const CellBg = [4]u8;
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32 align(8),
cell_offset: [2]f32 align(8),
source_rect: [4]f32 align(16),
dest_size: [2]f32 align(8),
};
/// Single parameter for the bg image shader.
pub const BgImage = extern struct {
opacity: f32 align(4),
info: Info align(1),
pub const Info = packed struct(u8) {
position: Position,
fit: Fit,
repeat: bool,
_padding: u1 = 0,
pub const Position = enum(u4) {
tl = 0,
tc = 1,
tr = 2,
ml = 3,
mc = 4,
mr = 5,
bl = 6,
bc = 7,
br = 8,
};
pub const Fit = enum(u2) {
contain = 0,
cover = 1,
stretch = 2,
none = 3,
};
};
};
/// Initialize our custom shader pipelines. The shaders argument is a
/// set of shader source code, not file paths.
fn initPostPipelines(
@ -204,60 +337,6 @@ fn initPostPipeline(data: [:0]const u8) !Pipeline {
});
}
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(4),
constraint_width: u32 align(4) = 0,
pub const Mode = enum(u32) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
};
// test {
// // Minimizing the size of this struct is important,
// // so we test it in order to be aware of any changes.
// try std.testing.expectEqual(32, @sizeOf(CellText));
// }
};
/// Initialize the cell render pipeline.
fn initCellTextPipeline() !Pipeline {
return try Pipeline.init(CellText, .{
.vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
.step_fn = .per_instance,
});
}
/// This is a single parameter for the cell bg shader.
pub const CellBg = [4]u8;
/// Initialize the cell background render pipeline.
fn initCellBgPipeline() !Pipeline {
return try Pipeline.init(null, .{
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
});
}
/// Initialize the image render pipeline.
fn initImagePipeline() !Pipeline {
return try Pipeline.init(Image, .{
.vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
.step_fn = .per_instance,
});
}
/// Load shader code from the target path, processing `#include` directives.
///
/// Comptime only for now, this code is really sloppy and makes a bunch of

View File

@ -0,0 +1,13 @@
#include "common.glsl"
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
out_FragColor = load_color(
unpack4u8(bg_color_packed_4u8),
use_linear_blending
);
}

View File

@ -0,0 +1,63 @@
#include "common.glsl"
// Position the FragCoord origin to the upper left
// so as to align with our texture's directionality.
layout(origin_upper_left) in vec4 gl_FragCoord;
layout(binding = 0) uniform sampler2D image;
flat in vec4 bg_color;
flat in vec2 offset;
flat in vec2 scale;
flat in float opacity;
flat in uint repeat;
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
// Our texture coordinate is based on the screen position, offset by the
// dest rect origin, and scaled by the ratio between the dest rect size
// and the original texture size, which effectively scales the original
// size of the texture to the dest rect size.
vec2 tex_coord = (gl_FragCoord.xy - offset) * scale;
vec2 tex_size = textureSize(image, 0);
// If we need to repeat the texture, wrap the coordinates.
if (repeat != 0) {
tex_coord = mod(mod(tex_coord, tex_size) + tex_size, tex_size);
}
vec4 rgba;
// If we're out of bounds, we have no color,
// otherwise we sample the texture for it.
if (any(lessThan(tex_coord, vec2(0.0))) ||
any(greaterThan(tex_coord, tex_size)))
{
rgba = vec4(0.0);
} else {
// We divide by the texture size to normalize for sampling.
rgba = texture(image, tex_coord / tex_size);
if (!use_linear_blending) {
rgba = unlinearize(rgba);
}
rgba.rgb *= rgba.a;
}
// Multiply it by the configured opacity, but cap it at
// the value that will make it fully opaque relative to
// the background color alpha, so it isn't overexposed.
rgba *= min(opacity, 1.0 / bg_color.a);
// Blend it on to a fully opaque version of the background color.
rgba += max(vec4(0.0), vec4(bg_color.rgb, 1.0) * vec4(1.0 - rgba.a));
// Multiply everything by the background color alpha.
rgba *= bg_color.a;
out_FragColor = rgba;
}

View File

@ -0,0 +1,145 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2D image;
layout(location = 0) in float in_opacity;
layout(location = 1) in uint info;
// 4 bits of info.
const uint BG_IMAGE_POSITION = 15u;
const uint BG_IMAGE_TL = 0u;
const uint BG_IMAGE_TC = 1u;
const uint BG_IMAGE_TR = 2u;
const uint BG_IMAGE_ML = 3u;
const uint BG_IMAGE_MC = 4u;
const uint BG_IMAGE_MR = 5u;
const uint BG_IMAGE_BL = 6u;
const uint BG_IMAGE_BC = 7u;
const uint BG_IMAGE_BR = 8u;
// 2 bits of info shifted 4.
const uint BG_IMAGE_FIT = 3u << 4;
const uint BG_IMAGE_CONTAIN = 0u << 4;
const uint BG_IMAGE_COVER = 1u << 4;
const uint BG_IMAGE_STRETCH = 2u << 4;
const uint BG_IMAGE_NO_FIT = 3u << 4;
// 1 bit of info shifted 6.
const uint BG_IMAGE_REPEAT = 1u << 6;
flat out vec4 bg_color;
flat out vec2 offset;
flat out vec2 scale;
flat out float opacity;
// We use a uint to pass the repeat value because
// bools aren't allowed for vertex outputs in OpenGL.
flat out uint repeat;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 position;
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
position.z = 1.0;
position.w = 1.0;
// Single triangle is clipped to viewport.
//
// X <- vid == 0: (-1, -3)
// |\
// | \
// | \
// |###\
// |#+# \ `+` is (0, 0). `#`s are viewport area.
// |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
gl_Position = position;
opacity = in_opacity;
repeat = info & BG_IMAGE_REPEAT;
vec2 screen_size = screen_size;
vec2 tex_size = textureSize(image, 0);
vec2 dest_size = tex_size;
switch (info & BG_IMAGE_FIT) {
// For `contain` we scale by a factor that makes the image
// width match the screen width or makes the image height
// match the screen height, whichever is smaller.
case BG_IMAGE_CONTAIN: {
float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
dest_size = tex_size * scale;
} break;
// For `cover` we scale by a factor that makes the image
// width match the screen width or makes the image height
// match the screen height, whichever is larger.
case BG_IMAGE_COVER: {
float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
dest_size = tex_size * scale;
} break;
// For `stretch` we stretch the image to the size of
// the screen without worrying about aspect ratio.
case BG_IMAGE_STRETCH: {
dest_size = screen_size;
} break;
// For `none` we just use the original texture size.
case BG_IMAGE_NO_FIT: {
dest_size = tex_size;
} break;
}
vec2 start = vec2(0.0);
vec2 mid = (screen_size - dest_size) / vec2(2.0);
vec2 end = screen_size - dest_size;
vec2 dest_offset = mid;
switch (info & BG_IMAGE_POSITION) {
case BG_IMAGE_TL: {
dest_offset = vec2(start.x, start.y);
} break;
case BG_IMAGE_TC: {
dest_offset = vec2(mid.x, start.y);
} break;
case BG_IMAGE_TR: {
dest_offset = vec2(end.x, start.y);
} break;
case BG_IMAGE_ML: {
dest_offset = vec2(start.x, mid.y);
} break;
case BG_IMAGE_MC: {
dest_offset = vec2(mid.x, mid.y);
} break;
case BG_IMAGE_MR: {
dest_offset = vec2(end.x, mid.y);
} break;
case BG_IMAGE_BL: {
dest_offset = vec2(start.x, end.y);
} break;
case BG_IMAGE_BC: {
dest_offset = vec2(mid.x, end.y);
} break;
case BG_IMAGE_BR: {
dest_offset = vec2(end.x, end.y);
} break;
}
offset = dest_offset;
scale = tex_size / dest_size;
// We load a fully opaque version of the bg color and combine it with
// the alpha separately, because we need these as separate values in
// the framgment shader.
uvec4 u_bg_color = unpack4u8(bg_color_packed_4u8);
bg_color = vec4(load_color(
uvec4(u_bg_color.rgb, 255),
use_linear_blending
).rgb, float(u_bg_color.a) / 255.0);
}

View File

@ -15,7 +15,7 @@ vec4 cell_bg() {
ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 bg = load_color(unpack4u8(bg_color_packed_4u8), use_linear_blending);
vec4 bg = vec4(0.0);
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {

View File

@ -87,19 +87,19 @@ void main() {
case MODE_TEXT_COLOR:
{
// For now, we assume that color glyphs
// are already premultiplied sRGB colors.
// are already premultiplied linear colors.
vec4 color = texture(atlas_color, in_data.tex_coord);
// If we aren't doing linear blending, we can return this right away.
if (!use_linear_blending) {
// If we are doing linear blending, we can return this right away.
if (use_linear_blending) {
out_FragColor = color;
return;
}
// Otherwise we need to linearize the color. Since the alpha is
// premultiplied, we need to divide it out before linearizing.
// Otherwise we need to unlinearize the color. Since the alpha is
// premultiplied, we need to divide it out before unlinearizing.
color.rgb /= vec3(color.a);
color = linearize(color);
color = unlinearize(color);
color.rgb *= vec3(color.a);
out_FragColor = color;

View File

@ -139,6 +139,12 @@ void main() {
unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
true
);
// Blend it with the global bg color
vec4 global_bg = load_color(
unpack4u8(bg_color_packed_4u8),
true
);
out_data.bg_color += global_bg * vec4(1.0 - out_data.bg_color.a);
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast

View File

@ -13,6 +13,7 @@
//----------------------------------------------------------------------------//
layout(binding = 1, std140) uniform Globals {
uniform mat4 projection_matrix;
uniform vec2 screen_size;
uniform vec2 cell_size;
uniform uint grid_size_packed_2u16;
uniform vec4 grid_padding;

View File

@ -1,6 +1,6 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
layout(binding = 0) uniform sampler2D image;
in vec2 tex_coord;

View File

@ -1,6 +1,6 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
layout(binding = 0) uniform sampler2D image;
layout(location = 0) in vec2 grid_pos;
layout(location = 1) in vec2 cell_offset;
@ -32,11 +32,12 @@ void main() {
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.
//
// We don't need to normalize because we use pixel addressing for our sampler.
tex_coord = source_rect.xy;
tex_coord += source_rect.zw * corner;
// Normalize the coordinates.
tex_coord /= textureSize(image, 0);
// The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components.
vec2 image_pos = (cell_size * grid_pos) + cell_offset;

View File

@ -11,6 +11,7 @@ enum Padding : uint8_t {
struct Uniforms {
float4x4 projection_matrix;
float2 screen_size;
float2 cell_size;
ushort2 grid_size;
float4 grid_padding;
@ -216,45 +217,245 @@ vertex FullScreenVertexOut full_screen_vertex(
}
//-------------------------------------------------------------------
// Cell Background Shader
// Background Color Shader
//-------------------------------------------------------------------
#pragma mark - Cell BG Shader
#pragma mark - BG Color Shader
struct CellBgVertexOut {
float4 position [[position]];
float4 bg_color;
};
vertex CellBgVertexOut cell_bg_vertex(
uint vid [[vertex_id]],
fragment float4 bg_color_fragment(
FullScreenVertexOut in [[stage_in]],
constant Uniforms& uniforms [[buffer(1)]]
) {
CellBgVertexOut out;
return load_color(
uniforms.bg_color,
uniforms.use_display_p3,
uniforms.use_linear_blending
);
}
//-------------------------------------------------------------------
// Background Image Shader
//-------------------------------------------------------------------
#pragma mark - BG Image Shader
struct BgImageVertexIn {
float opacity [[attribute(0)]];
uint8_t info [[attribute(1)]];
};
enum BgImagePosition : uint8_t {
// 4 bits of info.
BG_IMAGE_POSITION = 15u,
BG_IMAGE_TL = 0u,
BG_IMAGE_TC = 1u,
BG_IMAGE_TR = 2u,
BG_IMAGE_ML = 3u,
BG_IMAGE_MC = 4u,
BG_IMAGE_MR = 5u,
BG_IMAGE_BL = 6u,
BG_IMAGE_BC = 7u,
BG_IMAGE_BR = 8u,
};
enum BgImageFit : uint8_t {
// 2 bits of info shifted 4.
BG_IMAGE_FIT = 3u << 4,
BG_IMAGE_CONTAIN = 0u << 4,
BG_IMAGE_COVER = 1u << 4,
BG_IMAGE_STRETCH = 2u << 4,
BG_IMAGE_NO_FIT = 3u << 4,
};
enum BgImageRepeat : uint8_t {
// 1 bit of info shifted 6.
BG_IMAGE_REPEAT = 1u << 6,
};
struct BgImageVertexOut {
float4 position [[position]];
float4 bg_color [[flat]];
float2 offset [[flat]];
float2 scale [[flat]];
float opacity [[flat]];
bool repeat [[flat]];
};
vertex BgImageVertexOut bg_image_vertex(
uint vid [[vertex_id]],
BgImageVertexIn in [[stage_in]],
texture2d<float> image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]
) {
BgImageVertexOut out;
float4 position;
position.x = (vid == 2) ? 3.0 : -1.0;
position.y = (vid == 0) ? -3.0 : 1.0;
position.zw = 1.0;
// Single triangle is clipped to viewport.
//
// X <- vid == 0: (-1, -3)
// |\
// | \
// | \
// |###\
// |#+# \ `+` is (0, 0). `#`s are viewport area.
// |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
out.position = position;
// Convert the background color to Display P3
out.bg_color = load_color(
uniforms.bg_color,
out.opacity = in.opacity;
out.repeat = (in.info & BG_IMAGE_REPEAT) == BG_IMAGE_REPEAT;
float2 screen_size = uniforms.screen_size;
float2 tex_size = float2(image.get_width(), image.get_height());
float2 dest_size = tex_size;
switch (in.info & BG_IMAGE_FIT) {
// For `contain` we scale by a factor that makes the image
// width match the screen width or makes the image height
// match the screen height, whichever is smaller.
case BG_IMAGE_CONTAIN: {
float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
dest_size = tex_size * scale;
} break;
// For `cover` we scale by a factor that makes the image
// width match the screen width or makes the image height
// match the screen height, whichever is larger.
case BG_IMAGE_COVER: {
float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
dest_size = tex_size * scale;
} break;
// For `stretch` we stretch the image to the size of
// the screen without worrying about aspect ratio.
case BG_IMAGE_STRETCH: {
dest_size = screen_size;
} break;
// For `none` we just use the original texture size.
case BG_IMAGE_NO_FIT: {
dest_size = tex_size;
} break;
}
float2 start = float2(0.0);
float2 mid = (screen_size - dest_size) / 2;
float2 end = screen_size - dest_size;
float2 dest_offset = mid;
switch (in.info & BG_IMAGE_POSITION) {
case BG_IMAGE_TL: {
dest_offset = float2(start.x, start.y);
} break;
case BG_IMAGE_TC: {
dest_offset = float2(mid.x, start.y);
} break;
case BG_IMAGE_TR: {
dest_offset = float2(end.x, start.y);
} break;
case BG_IMAGE_ML: {
dest_offset = float2(start.x, mid.y);
} break;
case BG_IMAGE_MC: {
dest_offset = float2(mid.x, mid.y);
} break;
case BG_IMAGE_MR: {
dest_offset = float2(end.x, mid.y);
} break;
case BG_IMAGE_BL: {
dest_offset = float2(start.x, end.y);
} break;
case BG_IMAGE_BC: {
dest_offset = float2(mid.x, end.y);
} break;
case BG_IMAGE_BR: {
dest_offset = float2(end.x, end.y);
} break;
}
out.offset = dest_offset;
out.scale = tex_size / dest_size;
// We load a fully opaque version of the bg color and combine it with
// the alpha separately, because we need these as separate values in
// the framgment shader.
out.bg_color = float4(load_color(
uchar4(uniforms.bg_color.rgb, 255),
uniforms.use_display_p3,
uniforms.use_linear_blending
);
).rgb, float(uniforms.bg_color.a) / 255.0);
return out;
}
fragment float4 bg_image_fragment(
BgImageVertexOut in [[stage_in]],
texture2d<float> image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]
) {
constexpr sampler textureSampler(
coord::pixel,
address::clamp_to_zero,
filter::linear
);
// Our texture coordinate is based on the screen position, offset by the
// dest rect origin, and scaled by the ratio between the dest rect size
// and the original texture size, which effectively scales the original
// size of the texture to the dest rect size.
float2 tex_coord = (in.position.xy - in.offset) * in.scale;
// If we need to repeat the texture, wrap the coordinates.
if (in.repeat) {
float2 tex_size = float2(image.get_width(), image.get_height());
tex_coord = fmod(fmod(tex_coord, tex_size) + tex_size, tex_size);
}
float4 rgba = image.sample(textureSampler, tex_coord);
if (!uniforms.use_linear_blending) {
rgba = unlinearize(rgba);
}
// Premultiply the bg image.
rgba.rgb *= rgba.a;
// Multiply it by the configured opacity, but cap it at
// the value that will make it fully opaque relative to
// the background color alpha, so it isn't overexposed.
rgba *= min(in.opacity, 1.0 / in.bg_color.a);
// Blend it on to a fully opaque version of the background color.
rgba += max(float4(0.0), float4(in.bg_color.rgb, 1.0) * (1.0 - rgba.a));
// Multiply everything by the background color alpha.
rgba *= in.bg_color.a;
return rgba;
}
//-------------------------------------------------------------------
// Cell Background Shader
//-------------------------------------------------------------------
#pragma mark - Cell BG Shader
fragment float4 cell_bg_fragment(
CellBgVertexOut in [[stage_in]],
FullScreenVertexOut in [[stage_in]],
constant Uniforms& uniforms [[buffer(1)]],
constant uchar4 *cells [[buffer(2)]]
) {
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
float4 bg = in.bg_color;
float4 bg = float4(0.0);
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
@ -289,17 +490,8 @@ fragment float4 cell_bg_fragment(
// Load the color for the cell.
uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
// We have special case handling for when the cell color matches the bg color.
if (all(cell_color == uniforms.bg_color)) {
return bg;
}
// Convert the color and return it.
//
// TODO: We may want to blend the color with the background
// color, rather than purely replacing it, this needs
// some consideration about config options though.
//
// TODO: It might be a good idea to do a pass before this
// to convert all of the bg colors, so we don't waste
// a bunch of work converting the cell color in every
@ -462,6 +654,13 @@ vertex CellTextVertexOut cell_text_vertex(
uniforms.use_display_p3,
true
);
// Blend it with the global bg color
float4 global_bg = load_color(
uniforms.bg_color,
uniforms.use_display_p3,
true
);
out.bg_color += global_bg * (1.0 - out.bg_color.a);
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
@ -566,19 +765,19 @@ fragment float4 cell_text_fragment(
}
case MODE_TEXT_COLOR: {
// For now, we assume that color glyphs are
// already premultiplied Display P3 colors.
// For now, we assume that color glyphs
// are already premultiplied linear colors.
float4 color = textureColor.sample(textureSampler, in.tex_coord);
// If we aren't doing linear blending, we can return this right away.
if (!uniforms.use_linear_blending) {
// If we're doing linear blending, we can return this right away.
if (uniforms.use_linear_blending) {
return color;
}
// Otherwise we need to linearize the color. Since the alpha is
// premultiplied, we need to divide it out before linearizing.
// Otherwise we need to unlinearize the color. Since the alpha is
// premultiplied, we need to divide it out before unlinearizing.
color.rgb /= color.a;
color = linearize(color);
color = unlinearize(color);
color.rgb *= color.a;
return color;

View File

@ -402,32 +402,47 @@ pub fn clonePool(
};
const start_pin = pin_remap.get(ordered.tl) orelse start: {
// No start means it is outside the cloned area. We change it
// to the top-left.
// No start means it is outside the cloned area.
// If we have no end pin then either
// (1) our whole selection is outside the cloned area or
// (2) our cloned area is within the selection
if (pin_remap.get(ordered.br) == null) {
// If our tl is before the cloned area and br is after
// the cloned area then the whole screen is selected.
// This detection is somewhat more expensive so we try
// to avoid it if possible so its nested in this if.
// We check if the selection bottom right pin is above
// the cloned area or if the top left pin is below the
// cloned area, in either of these cases it means that
// the selection is fully out of bounds, so we have no
// selection in the cloned area and break out now.
const clone_top = self.pages.pin(top) orelse break :sel null;
if (!sel.contains(self, clone_top)) break :sel null;
const clone_top_y = self.pages.pointFromPin(
.screen,
clone_top,
).?.screen.y;
if (self.pages.pointFromPin(
.screen,
ordered.br.*,
).?.screen.y < clone_top_y) break :sel null;
if (self.pages.pointFromPin(
.screen,
ordered.tl.*,
).?.screen.y > clone_top_y) break :sel null;
}
break :start try pages.trackPin(.{ .node = pages.pages.first.? });
// We move the top pin back in bounds to the top row.
break :start try pages.trackPin(.{
.node = pages.pages.first.?,
.x = if (sel.rectangle) ordered.tl.x else 0,
});
};
const end_pin = pin_remap.get(ordered.br) orelse end: {
// No end means it is outside the cloned area. We change it
// to the bottom-right.
break :end try pages.trackPin(pages.pin(.{ .active = .{
.x = pages.cols - 1,
.y = pages.rows - 1,
} }) orelse break :sel null);
};
// If we got to this point it means that the selection is not
// fully out of bounds, so we move the bottom right pin back
// in bounds if it isn't already.
const end_pin = pin_remap.get(ordered.br) orelse try pages.trackPin(.{
.node = pages.pages.last.?,
.x = if (sel.rectangle) ordered.br.x else pages.cols - 1,
.y = pages.pages.last.?.data.size.rows - 1,
});
break :sel .{
.bounds = .{ .tracked = .{
@ -3053,6 +3068,29 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
}
}
/// Write text that's marked as a semantic prompt.
fn testWriteSemanticString(self: *Screen, text: []const u8, semantic_prompt: Row.SemanticPrompt) !void {
// Determine the first row using the cursor position. If we know that our
// first write is going to start on the next line because of a pending
// wrap, we'll proactively start there.
const start_y = if (self.cursor.pending_wrap) self.cursor.y + 1 else self.cursor.y;
try self.testWriteString(text);
// Determine the last row that we actually wrote by inspecting the cursor's
// position. If we're in the first column, we haven't actually written any
// characters to it, so we end at the preceding row instead.
const end_y = if (self.cursor.x > 0) self.cursor.y else self.cursor.y - 1;
// Mark the full range of written rows with our semantic prompt.
var y = start_y;
while (y <= end_y) {
const pin = self.pages.pin(.{ .active = .{ .y = y } }).?;
pin.rowAndCell().row.semantic_prompt = semantic_prompt;
y += 1;
}
}
test "Screen read and write" {
const testing = std.testing;
const alloc = testing.allocator;
@ -3671,16 +3709,11 @@ test "Screen: clearPrompt" {
var s = try init(alloc, 5, 3, 0);
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
// Set one of the rows to be a prompt
{
s.cursorAbsolute(0, 1);
s.cursor.page_row.semantic_prompt = .prompt;
s.cursorAbsolute(0, 2);
s.cursor.page_row.semantic_prompt = .input;
}
try s.testWriteSemanticString("1ABCD\n", .unknown);
try s.testWriteSemanticString("2EFGH\n", .prompt);
try s.testWriteSemanticString("3IJKL", .input);
s.clearPrompt();
@ -3697,18 +3730,12 @@ test "Screen: clearPrompt continuation" {
var s = try init(alloc, 5, 4, 0);
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL\n4MNOP";
try s.testWriteString(str);
// Set one of the rows to be a prompt followed by a continuation row
{
s.cursorAbsolute(0, 1);
s.cursor.page_row.semantic_prompt = .prompt;
s.cursorAbsolute(0, 2);
s.cursor.page_row.semantic_prompt = .prompt_continuation;
s.cursorAbsolute(0, 3);
s.cursor.page_row.semantic_prompt = .input;
}
try s.testWriteSemanticString("1ABCD\n", .unknown);
try s.testWriteSemanticString("2EFGH\n", .prompt);
try s.testWriteSemanticString("3IJKL\n", .prompt_continuation);
try s.testWriteSemanticString("4MNOP", .input);
s.clearPrompt();
@ -3719,22 +3746,17 @@ test "Screen: clearPrompt continuation" {
}
}
test "Screen: clearPrompt consecutive prompts" {
test "Screen: clearPrompt consecutive inputs" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0);
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
// Set both rows to be prompts
{
s.cursorAbsolute(0, 1);
s.cursor.page_row.semantic_prompt = .input;
s.cursorAbsolute(0, 2);
s.cursor.page_row.semantic_prompt = .input;
}
// Set both rows to be inputs
try s.testWriteSemanticString("1ABCD\n", .unknown);
try s.testWriteSemanticString("2EFGH\n", .input);
try s.testWriteSemanticString("3IJKL", .input);
s.clearPrompt();
@ -5287,6 +5309,45 @@ test "Screen: clone contains subset of selection" {
}
}
test "Screen: clone contains subset of rectangle selection" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 4, 1);
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD");
// Select the full screen from x=1 to x=3
try s.select(Selection.init(
s.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
s.pages.pin(.{ .active = .{ .x = 3, .y = 3 } }).?,
true,
));
// Clone
var s2 = try s.clone(
alloc,
.{ .active = .{ .y = 1 } },
.{ .active = .{ .y = 2 } },
);
defer s2.deinit();
// Our selection should remain valid and be properly clipped
// preserving the columns of the start and end points of the
// selection.
{
const sel = s2.selection.?;
try testing.expectEqual(point.Point{ .active = .{
.x = 1,
.y = 0,
} }, s2.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 3,
.y = 3,
} }, s2.pages.pointFromPin(.active, sel.end()).?);
}
}
test "Screen: clone basic" {
const testing = std.testing;
const alloc = testing.allocator;
@ -6003,26 +6064,24 @@ test "Screen: resize more cols no reflow preserves semantic prompt" {
var s = try init(alloc, 5, 3, 0);
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
// Set one of the rows to be a prompt
{
s.cursorAbsolute(0, 1);
s.cursor.page_row.semantic_prompt = .prompt;
}
try s.testWriteSemanticString("1ABCD\n", .unknown);
try s.testWriteSemanticString("2EFGH\n", .prompt);
try s.testWriteSemanticString("3IJKL", .unknown);
try s.resize(10, 3);
const expected = "1ABCD\n2EFGH\n3IJKL";
{
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings(str, contents);
try testing.expectEqualStrings(expected, contents);
}
{
const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings(str, contents);
try testing.expectEqualStrings(expected, contents);
}
// Our one row should still be a semantic prompt, the others should not.
@ -7453,7 +7512,9 @@ test "Screen: selectLine semantic prompt boundary" {
var s = try init(alloc, 5, 10, 0);
defer s.deinit();
try s.testWriteString("ABCDE\nA > ");
try s.testWriteSemanticString("ABCDE\n", .unknown);
try s.testWriteSemanticString("A ", .prompt);
try s.testWriteSemanticString("> ", .unknown);
{
const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
@ -7461,12 +7522,6 @@ test "Screen: selectLine semantic prompt boundary" {
try testing.expectEqualStrings("ABCDE\nA \n> ", contents);
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .prompt;
}
// Selecting output stops at the prompt even if soft-wrapped
{
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
@ -7851,55 +7906,23 @@ test "Screen: selectOutput" {
// zig fmt: off
{
// line number:
try s.testWriteString("output1\n"); // 0
try s.testWriteString("output1\n"); // 1
try s.testWriteString("prompt2\n"); // 2
try s.testWriteString("input2\n"); // 3
try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow
try s.testWriteString("output2\n"); // 7
try s.testWriteString("prompt3$ input3\n"); // 8
try s.testWriteString("output3\n"); // 9
try s.testWriteString("output3\n"); // 10
try s.testWriteString("output3"); // 11
try s.testWriteSemanticString("output1\n", .command); // 0
try s.testWriteSemanticString("output1\n", .command); // 1
try s.testWriteSemanticString("prompt2\n", .prompt); // 2
try s.testWriteSemanticString("input2\n", .input); // 3
try s.testWriteSemanticString( //
"output2output2output2output2\n", // 4, 5, 6 due to overflow
.command, //
); //
try s.testWriteSemanticString("output2\n", .command); // 7
try s.testWriteSemanticString("$ ", .prompt); // 8 prompt
try s.testWriteSemanticString("input3\n", .input); // 8 input
try s.testWriteSemanticString("output3\n", .command); // 9
try s.testWriteSemanticString("output3\n", .command); // 10
try s.testWriteSemanticString("output3", .command); // 11
}
// zig fmt: on
{
const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .prompt;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 5 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 8 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
// No start marker, should select from the beginning
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
@ -7945,26 +7968,17 @@ test "Screen: selectOutput" {
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
.y = 12,
.y = 11,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// input / prompt at y = 0, pt.y = 0
{
s.deinit();
s = try init(alloc, 10, 5, 0);
try s.testWriteString("prompt1$ input1\n");
try s.testWriteString("output1\n");
try s.testWriteString("prompt2\n");
{
const pin = s.pages.pin(.{ .screen = .{ .y = 0 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
try s.testWriteSemanticString("$ ", .prompt);
try s.testWriteSemanticString("input1\n", .input);
try s.testWriteSemanticString("output1\n", .command);
try s.testWriteSemanticString("prompt2\n", .prompt);
try testing.expect(s.selectOutput(s.pages.pin(.{ .active = .{
.x = 2,
.y = 0,
@ -7981,46 +7995,21 @@ test "Screen: selectPrompt basics" {
// zig fmt: off
{
// line number:
try s.testWriteString("output1\n"); // 0
try s.testWriteString("output1\n"); // 1
try s.testWriteString("prompt2\n"); // 2
try s.testWriteString("input2\n"); // 3
try s.testWriteString("output2\n"); // 4
try s.testWriteString("output2\n"); // 5
try s.testWriteString("prompt3$ input3\n"); // 6
try s.testWriteString("output3\n"); // 7
try s.testWriteString("output3\n"); // 8
try s.testWriteString("output3"); // 9
// line number:
try s.testWriteSemanticString("output1\n", .command); // 0
try s.testWriteSemanticString("output1\n", .command); // 1
try s.testWriteSemanticString("prompt2\n", .prompt); // 2
try s.testWriteSemanticString("input2\n", .input); // 3
try s.testWriteSemanticString("output2\n", .command); // 4
try s.testWriteSemanticString("output2\n", .command); // 5
try s.testWriteSemanticString("$ ", .prompt); // 6 prompt
try s.testWriteSemanticString("input3\n", .input); // 6 input
try s.testWriteSemanticString("output3\n", .command); // 7
try s.testWriteSemanticString("output3\n", .command); // 8
try s.testWriteSemanticString("output3", .command); // 9
}
// zig fmt: on
{
const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .prompt;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
// Not at a prompt
{
const sel = s.selectPrompt(s.pages.pin(.{ .active = .{
@ -8081,30 +8070,14 @@ test "Screen: selectPrompt prompt at start" {
// zig fmt: off
{
// line number:
try s.testWriteString("prompt1\n"); // 0
try s.testWriteString("input1\n"); // 1
try s.testWriteString("output2\n"); // 2
try s.testWriteString("output2\n"); // 3
// line number:
try s.testWriteSemanticString("prompt1\n", .prompt); // 0
try s.testWriteSemanticString("input1\n", .input); // 1
try s.testWriteSemanticString("output2\n", .command); // 2
try s.testWriteSemanticString("output2\n", .command); // 3
}
// zig fmt: on
{
const pin = s.pages.pin(.{ .screen = .{ .y = 0 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .prompt;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
// Not at a prompt
{
const sel = s.selectPrompt(s.pages.pin(.{ .active = .{
@ -8141,25 +8114,14 @@ test "Screen: selectPrompt prompt at end" {
// zig fmt: off
{
// line number:
try s.testWriteString("output2\n"); // 0
try s.testWriteString("output2\n"); // 1
try s.testWriteString("prompt1\n"); // 2
try s.testWriteString("input1\n"); // 3
// line number:
try s.testWriteSemanticString("output2\n", .command); // 0
try s.testWriteSemanticString("output2\n", .command); // 1
try s.testWriteSemanticString("prompt1\n", .prompt); // 2
try s.testWriteSemanticString("input1\n", .input); // 3
}
// zig fmt: on
{
const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .prompt;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
// Not at a prompt
{
const sel = s.selectPrompt(s.pages.pin(.{ .active = .{
@ -8196,46 +8158,21 @@ test "Screen: promptPath" {
// zig fmt: off
{
// line number:
try s.testWriteString("output1\n"); // 0
try s.testWriteString("output1\n"); // 1
try s.testWriteString("prompt2\n"); // 2
try s.testWriteString("input2\n"); // 3
try s.testWriteString("output2\n"); // 4
try s.testWriteString("output2\n"); // 5
try s.testWriteString("prompt3$ input3\n"); // 6
try s.testWriteString("output3\n"); // 7
try s.testWriteString("output3\n"); // 8
try s.testWriteString("output3"); // 9
// line number:
try s.testWriteSemanticString("output1\n", .command); // 0
try s.testWriteSemanticString("output1\n", .command); // 1
try s.testWriteSemanticString("prompt2\n", .prompt); // 2
try s.testWriteSemanticString("input2\n", .input); // 3
try s.testWriteSemanticString("output2\n", .command); // 4
try s.testWriteSemanticString("output2\n", .command); // 5
try s.testWriteSemanticString("$ ", .prompt); // 6 prompt
try s.testWriteSemanticString("input3\n", .input); // 6 input
try s.testWriteSemanticString("output3\n", .command); // 7
try s.testWriteSemanticString("output3\n", .command); // 8
try s.testWriteSemanticString("output3", .command); // 9
}
// zig fmt: on
{
const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .prompt;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
// From is not in the prompt
{
const path = s.promptPath(

Some files were not shown because too many files have changed in this diff Show More