summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/publish-tag.yml74
-rw-r--r--.github/workflows/release-tag.yml17
-rw-r--r--build.zig.zon2
-rw-r--r--dist/macos/update_appcast_tag.py7
-rw-r--r--nix/package.nix20
-rw-r--r--src/apprt/gtk/Surface.zig1
-rw-r--r--src/apprt/gtk/Window.zig5
-rw-r--r--src/apprt/gtk/key.zig57
-rw-r--r--src/build/Config.zig2
-rw-r--r--src/config/Config.zig40
-rw-r--r--src/input/Binding.zig56
-rw-r--r--src/renderer/Metal.zig10
-rw-r--r--src/renderer/metal/shaders.zig2
-rw-r--r--src/renderer/shaders/cell.metal95
-rw-r--r--src/terminal/Screen.zig71
15 files changed, 337 insertions, 122 deletions
diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml
new file mode 100644
index 000000000..160034a52
--- /dev/null
+++ b/.github/workflows/publish-tag.yml
@@ -0,0 +1,74 @@
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to deploy (format: vX.Y.Z)"
+ required: true
+
+name: Publish Tagged Release
+
+# We must only run one release workflow at a time to prevent corrupting
+# our release artifacts.
+concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: false
+
+jobs:
+ setup:
+ runs-on: namespace-profile-ghostty-sm
+ outputs:
+ version: ${{ steps.extract_version.outputs.version }}
+ steps:
+ - name: Validate Version Input
+ run: |
+ if [[ ! "${{ github.event.inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Error: Version must follow the format vX.Y.Z (e.g., v1.0.0)."
+ exit 1
+ fi
+
+ echo "Version is valid: ${{ github.event.inputs.version }}"
+
+ - name: Exract the Version
+ id: extract_version
+ run: |
+ VERSION=${{ github.event.inputs.version }}
+ VERSION=${VERSION#v}
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ upload:
+ needs: [setup]
+ runs-on: namespace-profile-ghostty-sm
+ env:
+ GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
+ steps:
+ - name: Validate Release Files
+ run: |
+ BASE="https://release.files.ghostty.org/${GHOSTTY_VERSION}"
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz.minisig" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal.zip" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal-dsym.zip" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/Ghostty.dmg" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/appcast-staged.xml" | grep -q "^200$" || exit 1
+
+ - name: Download Staged Appcast
+ run: |
+ curl -L https://release.files.ghostty.org/${GHOSTTY_VERSION}/appcast-staged.xml
+ mv appcast-staged.xml appcast.xml
+
+ - name: Upload Appcast
+ run: |
+ rm -rf blob
+ mkdir blob
+ mv appcast.xml blob/appcast.xml
+ - name: Upload Appcast to R2
+ uses: ryand56/r2-upload-action@latest
+ with:
+ r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
+ r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
+ r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
+ r2-bucket: ghostty-release
+ source-dir: blob
+ destination-dir: ./
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index cf94bf23e..0767152f5 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -7,6 +7,7 @@ on:
upload:
description: "Upload final artifacts to R2"
default: false
+
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
@@ -367,6 +368,7 @@ jobs:
mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip
mv ghostty-macos-universal-dsym.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal-dsym.zip
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
+ mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
- name: Upload to R2
uses: ryand56/r2-upload-action@latest
with:
@@ -376,18 +378,3 @@ jobs:
r2-bucket: ghostty-release
source-dir: blob
destination-dir: ./
-
- - name: Prep Appcast
- run: |
- rm -rf blob
- mkdir blob
- mv appcast.xml blob/appcast.xml
- - name: Upload Appcast to R2
- uses: ryand56/r2-upload-action@latest
- with:
- r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
- r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
- r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
- r2-bucket: ghostty-release
- source-dir: blob
- destination-dir: ./
diff --git a/build.zig.zon b/build.zig.zon
index a8f45e6ea..5839b090f 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,6 +1,6 @@
.{
.name = "ghostty",
- .version = "1.0.2",
+ .version = "1.1.0",
.paths = .{""},
.dependencies = .{
// Zig libs
diff --git a/dist/macos/update_appcast_tag.py b/dist/macos/update_appcast_tag.py
index 4ef526019..2cb20dd5d 100644
--- a/dist/macos/update_appcast_tag.py
+++ b/dist/macos/update_appcast_tag.py
@@ -21,6 +21,7 @@ from datetime import datetime, timezone
now = datetime.now(timezone.utc)
version = os.environ["GHOSTTY_VERSION"]
+version_dash = version.replace('.', '-')
build = os.environ["GHOSTTY_BUILD"]
commit = os.environ["GHOSTTY_COMMIT"]
commit_long = os.environ["GHOSTTY_COMMIT_LONG"]
@@ -82,6 +83,8 @@ elem = ET.SubElement(item, "sparkle:shortVersionString")
elem.text = f"{version}"
elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
elem.text = "13.0.0"
+elem = ET.SubElement(item, "sparkle:fullReleaseNotesLink")
+elem.text = f"https://ghostty.org/docs/install/release-notes/{version_dash}"
elem = ET.SubElement(item, "description")
elem.text = f"""
<h1>Ghostty v{version}</h1>
@@ -91,8 +94,8 @@ on {now.strftime('%Y-%m-%d')}.
</p>
<p>
We don't currently generate release notes for auto-updates.
-You can view the complete changelog and release notes on
-the <a href="https://ghostty.org">Ghostty website</a>.
+You can view the complete changelog and release notes
+at <a href="https://ghostty.org/docs/install/release-notes/{version_dash}">ghostty.org/docs/install/release-notes/{version_dash}</a>.
</p>
"""
elem = ET.SubElement(item, "enclosure")
diff --git a/nix/package.nix b/nix/package.nix
index 2f7825a56..dae87ed81 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -40,7 +40,7 @@
# ultimately acted on and has made its way to a nixpkgs implementation, this
# can probably be removed in favor of that.
zig_hook = zig_0_13.hook.overrideAttrs {
- zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize}";
+ zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
};
# We limit source like this to try and reduce the amount of rebuilds as possible
@@ -114,7 +114,7 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
- version = "1.0.2";
+ version = "1.1.0";
inherit src;
nativeBuildInputs =
@@ -162,13 +162,13 @@ in
dontConfigure = true;
- zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
-
- preBuild = ''
- rm -rf $ZIG_GLOBAL_CACHE_DIR
- cp -r --reflink=auto ${zigCache} $ZIG_GLOBAL_CACHE_DIR
- chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
- '';
+ zigBuildFlags = [
+ "--system"
+ "${zigCache}/p"
+ "-Dversion-string=${finalAttrs.version}-${revision}-nix"
+ "-Dgtk-x11=${lib.boolToString enableX11}"
+ "-Dgtk-wayland=${lib.boolToString enableWayland}"
+ ];
outputs = [
"out"
@@ -202,7 +202,7 @@ in
'';
meta = {
- homepage = "https://github.com/ghostty-org/ghostty";
+ homepage = "https://ghostty.org";
license = lib.licenses.mit;
platforms = [
"x86_64-linux"
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 3677c5e8d..1ca39425b 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -1757,6 +1757,7 @@ pub fn keyEvent(
event,
physical_key,
gtk_mods,
+ action,
&self.app.winproto,
);
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 28bbfe54f..3a72e1752 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -335,10 +335,7 @@ pub fn init(self: *Window, app: *App) !void {
.top,
.left,
.right,
- => c.gtk_box_prepend(
- @ptrCast(box),
- @ptrCast(@alignCast(tab_bar)),
- ),
+ => c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))),
.bottom => c.gtk_box_append(
@ptrCast(box),
diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig
index 40c9ca9a4..60f12edca 100644
--- a/src/apprt/gtk/key.zig
+++ b/src/apprt/gtk/key.zig
@@ -108,6 +108,7 @@ pub fn eventMods(
event: *c.GdkEvent,
physical_key: input.Key,
gtk_mods: c.GdkModifierType,
+ action: input.Action,
app_winproto: *winproto.App,
) input.Mods {
const device = c.gdk_event_get_device(event);
@@ -115,15 +116,55 @@ pub fn eventMods(
var mods = app_winproto.eventMods(device, gtk_mods);
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
+ // We use the physical key to determine sided modifiers. As
+ // far as I can tell there's no other way to reliably determine
+ // this.
+ //
+ // We also set the main modifier to true if either side is true,
+ // since on both X11/Wayland, GTK doesn't set the main modifier
+ // if only the modifier key is pressed, but our core logic
+ // relies on it.
switch (physical_key) {
- .left_shift => mods.sides.shift = .left,
- .right_shift => mods.sides.shift = .right,
- .left_control => mods.sides.ctrl = .left,
- .right_control => mods.sides.ctrl = .right,
- .left_alt => mods.sides.alt = .left,
- .right_alt => mods.sides.alt = .right,
- .left_super => mods.sides.super = .left,
- .right_super => mods.sides.super = .right,
+ .left_shift => {
+ mods.shift = action != .release;
+ mods.sides.shift = .left;
+ },
+
+ .right_shift => {
+ mods.shift = action != .release;
+ mods.sides.shift = .right;
+ },
+
+ .left_control => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .left;
+ },
+
+ .right_control => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .right;
+ },
+
+ .left_alt => {
+ mods.alt = action != .release;
+ mods.sides.alt = .left;
+ },
+
+ .right_alt => {
+ mods.alt = action != .release;
+ mods.sides.alt = .right;
+ },
+
+ .left_super => {
+ mods.super = action != .release;
+ mods.sides.super = .left;
+ },
+
+ .right_super => {
+ mods.super = action != .release;
+ mods.sides.super = .right;
+ },
+
else => {},
}
diff --git a/src/build/Config.zig b/src/build/Config.zig
index c6f0e6d09..c832b77ad 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
-const app_version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 2 };
+const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 0 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 839656169..0ed98bdea 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -259,7 +259,8 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// What color space to use when performing alpha blending.
///
-/// This affects how text looks for different background/foreground color pairs.
+/// This affects the appearance of text and of any images with transparency.
+/// Additionally, custom shaders will receive colors in the configured space.
///
/// Valid values:
///
@@ -273,15 +274,10 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This is also sometimes known as "gamma correction".
/// (Currently only supported on macOS. Has no effect on Linux.)
///
-/// * `linear-corrected` - Corrects the thinning/thickening effect of linear
-/// by applying a correction curve to the text alpha depending on its
-/// brightness. This compensates for the thinning and makes the weight of
-/// most text appear very similar to when it's blended non-linearly.
-///
-/// Note: This setting affects more than just text, images will also be blended
-/// in the selected color space, and custom shaders will receive colors in that
-/// color space as well.
-@"text-blending": TextBlending = .native,
+/// * `linear-corrected` - Same as `linear`, but with a correction step applied
+/// for text that makes it look nearly or completely identical to `native`,
+/// but without any of the darkening artifacts.
+@"alpha-blending": AlphaBlending = .native,
/// All of the configurations behavior adjust various metrics determined by the
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
@@ -1019,6 +1015,12 @@ class: ?[:0]const u8 = null,
/// performable (acting identically to not having a keybind set at
/// all).
///
+/// Performable keybinds will not appear as menu shortcuts in the
+/// application menu. This is because the menu shortcuts force the
+/// action to be performed regardless of the state of the terminal.
+/// Performable keybinds will still work, they just won't appear as
+/// a shortcut label in the menu.
+///
/// Keybind triggers are not unique per prefix combination. For example,
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
/// set later will overwrite the keybind set earlier. In this case, the
@@ -1221,12 +1223,16 @@ keybind: Keybinds = .{},
/// This is currently only supported on macOS and Linux.
@"window-theme": WindowTheme = .auto,
-/// The colorspace to use for the terminal window. The default is `srgb` but
-/// this can also be set to `display-p3` to use the Display P3 colorspace.
+/// The color space to use when interpreting terminal colors. "Terminal colors"
+/// refers to colors specified in your configuration and colors produced by
+/// direct-color SGR sequences.
///
-/// Changing this value at runtime will only affect new windows.
+/// Valid values:
+///
+/// * `srgb` - Interpret colors in the sRGB color space. This is the default.
+/// * `display-p3` - Interpret colors in the Display P3 color space.
///
-/// This setting is only supported on macOS.
+/// This setting is currently only supported on macOS.
@"window-colorspace": WindowColorspace = .srgb,
/// The initial window size. This size is in terminal grid cells by default.
@@ -5826,13 +5832,13 @@ pub const GraphemeWidthMethod = enum {
unicode,
};
-/// See text-blending
-pub const TextBlending = enum {
+/// See alpha-blending
+pub const AlphaBlending = enum {
native,
linear,
@"linear-corrected",
- pub fn isLinear(self: TextBlending) bool {
+ pub fn isLinear(self: AlphaBlending) bool {
return switch (self) {
.native => false,
.linear, .@"linear-corrected" => true,
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index a1e759bf8..19c103195 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -284,11 +284,11 @@ pub const Action = union(enum) {
scroll_page_fractional: f32,
scroll_page_lines: i16,
- /// Adjust the current selection in a given direction. Does nothing if no
+ /// Adjust the current selection in a given direction. Does nothing if no
/// selection exists.
///
/// Arguments:
- /// - left, right, up, down, page_up, page_down, home, end,
+ /// - left, right, up, down, page_up, page_down, home, end,
/// beginning_of_line, end_of_line
///
/// Example: Extend selection to the right
@@ -1230,6 +1230,13 @@ pub const Set = struct {
/// This is a conscious decision since the primary use case of the reverse
/// map is to support GUI toolkit keyboard accelerators and no mainstream
/// GUI toolkit supports sequences.
+ ///
+ /// Performable triggers are also not present in the reverse map. This
+ /// is so that GUI toolkits don't register performable triggers as
+ /// menu shortcuts (the primary use case of the reverse map). GUI toolkits
+ /// such as GTK handle menu shortcuts too early in the event lifecycle
+ /// for performable to work so this is a conscious decision to ease the
+ /// integration with GUI toolkits.
reverse: ReverseMap = .{},
/// The entry type for the forward mapping of trigger to action.
@@ -1494,6 +1501,11 @@ pub const Set = struct {
// unbind should never go into the set, it should be handled prior
assert(action != .unbind);
+ // This is true if we're going to track this entry as
+ // a reverse mapping. There are certain scenarios we don't.
+ // See the reverse map docs for more information.
+ const track_reverse: bool = !flags.performable;
+
const gop = try self.bindings.getOrPut(alloc, t);
if (gop.found_existing) switch (gop.value_ptr.*) {
@@ -1505,7 +1517,7 @@ pub const Set = struct {
// If we have an existing binding for this trigger, we have to
// update the reverse mapping to remove the old action.
- .leaf => {
+ .leaf => if (track_reverse) {
const t_hash = t.hash();
var it = self.reverse.iterator();
while (it.next()) |reverse_entry| it: {
@@ -1522,8 +1534,9 @@ pub const Set = struct {
.flags = flags,
} };
errdefer _ = self.bindings.remove(t);
- try self.reverse.put(alloc, action, t);
- errdefer _ = self.reverse.remove(action);
+
+ if (track_reverse) try self.reverse.put(alloc, action, t);
+ errdefer if (track_reverse) self.reverse.remove(action);
}
/// Get a binding for a given trigger.
@@ -2373,6 +2386,39 @@ test "set: maintains reverse mapping" {
}
}
+test "set: performable is not part of reverse mappings" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s: Set = .{};
+ defer s.deinit(alloc);
+
+ try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+
+ // trigger should be non-performable
+ try s.putFlags(
+ alloc,
+ .{ .key = .{ .translated = .b } },
+ .{ .new_window = {} },
+ .{ .performable = true },
+ );
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+
+ // removal of performable should do nothing
+ s.remove(alloc, .{ .key = .{ .translated = .b } });
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+}
+
test "set: overriding a mapping updates reverse" {
const testing = std.testing;
const alloc = testing.allocator;
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 52a5437c6..866f9682d 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -391,7 +391,7 @@ pub const DerivedConfig = struct {
links: link.Set,
vsync: bool,
colorspace: configpkg.Config.WindowColorspace,
- blending: configpkg.Config.TextBlending,
+ blending: configpkg.Config.AlphaBlending,
pub fn init(
alloc_gpa: Allocator,
@@ -463,7 +463,7 @@ pub const DerivedConfig = struct {
.links = links,
.vsync = config.@"window-vsync",
.colorspace = config.@"window-colorspace",
- .blending = config.@"text-blending",
+ .blending = config.@"alpha-blending",
.arena = arena,
};
}
@@ -667,7 +667,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.cursor_wide = false,
.use_display_p3 = options.config.colorspace == .@"display-p3",
.use_linear_blending = options.config.blending.isLinear(),
- .use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
+ .use_linear_correction = options.config.blending == .@"linear-corrected",
},
// Fonts
@@ -2099,7 +2099,7 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// Set our new color space and blending
self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
self.uniforms.use_linear_blending = config.blending.isLinear();
- self.uniforms.use_experimental_linear_correction = config.blending == .@"linear-corrected";
+ self.uniforms.use_linear_correction = config.blending == .@"linear-corrected";
// Set our new colors
self.default_background_color = config.background;
@@ -2242,7 +2242,7 @@ pub fn setScreenSize(
.cursor_wide = old.cursor_wide,
.use_display_p3 = old.use_display_p3,
.use_linear_blending = old.use_linear_blending,
- .use_experimental_linear_correction = old.use_experimental_linear_correction,
+ .use_linear_correction = old.use_linear_correction,
};
// Reset our cell contents if our grid size has changed.
diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig
index 62d363173..b297de809 100644
--- a/src/renderer/metal/shaders.zig
+++ b/src/renderer/metal/shaders.zig
@@ -158,7 +158,7 @@ pub const Uniforms = extern struct {
/// Enables a weight correction step that makes text rendered
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
- use_experimental_linear_correction: bool align(1) = false,
+ use_linear_correction: bool align(1) = false,
const PaddingExtend = packed struct(u8) {
left: bool = false,
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 17f811a19..3ca0f9149 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -22,7 +22,7 @@ struct Uniforms {
bool cursor_wide;
bool use_display_p3;
bool use_linear_blending;
- bool use_experimental_linear_correction;
+ bool use_linear_correction;
};
//-------------------------------------------------------------------
@@ -59,22 +59,28 @@ float3 srgb_to_display_p3(float3 srgb) {
// Converts a color from sRGB gamma encoding to linear.
float4 linearize(float4 srgb) {
- bool3 cutoff = srgb.rgb <= 0.04045;
- float3 lower = srgb.rgb / 12.92;
- float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
- srgb.rgb = mix(higher, lower, float3(cutoff));
+ bool3 cutoff = srgb.rgb <= 0.04045;
+ float3 lower = srgb.rgb / 12.92;
+ float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
+ srgb.rgb = mix(higher, lower, float3(cutoff));
- return srgb;
+ return srgb;
+}
+float linearize(float v) {
+ return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}
// Converts a color from linear to sRGB gamma encoding.
float4 unlinearize(float4 linear) {
- bool3 cutoff = linear.rgb <= 0.0031308;
- float3 lower = linear.rgb * 12.92;
- float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
- linear.rgb = mix(higher, lower, float3(cutoff));
+ bool3 cutoff = linear.rgb <= 0.0031308;
+ float3 lower = linear.rgb * 12.92;
+ float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
+ linear.rgb = mix(higher, lower, float3(cutoff));
- return linear;
+ return linear;
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
}
// Compute the luminance of the provided color.
@@ -353,8 +359,9 @@ struct CellTextVertexIn {
struct CellTextVertexOut {
float4 position [[position]];
- uint8_t mode;
- float4 color;
+ uint8_t mode [[flat]];
+ float4 color [[flat]];
+ float4 bg_color [[flat]];
float2 tex_coord;
};
@@ -445,6 +452,13 @@ vertex CellTextVertexOut cell_text_vertex(
true
);
+ // Get the BG color
+ out.bg_color = load_color(
+ bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
+ uniforms.use_display_p3,
+ true
+ );
+
// 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
// with the background.
@@ -453,14 +467,8 @@ vertex CellTextVertexOut cell_text_vertex(
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
- // Get the BG color
- float4 bg_color = load_color(
- bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
- uniforms.use_display_p3,
- true
- );
// Ensure our minimum contrast
- out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
+ out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
}
// If this cell is the cursor cell, then we need to change the color.
@@ -473,7 +481,17 @@ vertex CellTextVertexOut cell_text_vertex(
) &&
in.grid_pos.y == uniforms.cursor_pos.y
) {
- out.color = float4(uniforms.cursor_color) / 255.0f;
+ out.color = load_color(
+ uniforms.cursor_color,
+ uniforms.use_display_p3,
+ false
+ );
+ }
+
+ // Don't bother rendering if the bg and fg colors are identical, just return
+ // the same point which will be culled because it makes the quad zero sized.
+ if (all(out.color == out.bg_color)) {
+ out.position = float4(0.0);
}
return out;
@@ -514,19 +532,28 @@ fragment float4 cell_text_fragment(
// Fetch our alpha mask for this pixel.
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
- // Experimental linear blending weight correction.
- if (uniforms.use_experimental_linear_correction) {
- float l = luminance(color.rgb);
-
- // TODO: This is a dynamic dilation term that biases
- // the alpha adjustment for small font sizes;
- // it should be computed by dividing the font
- // size in `pt`s by `13.0` and using that if
- // it's less than `1.0`, but for now it's
- // hard coded at 1.0, which has no effect.
- float d = 13.0 / 13.0;
-
- a += pow(a, d + d * l) - pow(a, d + 1.0 - d * l);
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (uniforms.use_linear_correction) {
+ // Short explanation of how this works:
+ //
+ // We get the luminances of the foreground and background colors,
+ // and then unlinearize them and perform blending on them. This
+ // gives us our desired luminance, which we derive our new alpha
+ // value from by mapping the range [bg_l, fg_l] to [0, 1], since
+ // our final blend will be a linear interpolation from bg to fg.
+ //
+ // This yields virtually identical results for grayscale blending,
+ // and very similar but non-identical results for color blending.
+ float4 bg = in.bg_color;
+ float fg_l = luminance(color.rgb);
+ float bg_l = luminance(bg.rgb);
+ // To avoid numbers going haywire, we don't apply correction
+ // when the bg and fg luminances are within 0.001 of each other.
+ if (abs(fg_l - bg_l) > 0.001) {
+ float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
+ a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
+ }
}
// Multiply our whole color by the alpha mask.
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index eb70d32d0..a838e0e10 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -2591,13 +2591,36 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
const start: Pin = boundary: {
var it = pin.rowIterator(.left_up, null);
var it_prev = pin;
+
+ // First, iterate until we find the first line of command output
while (it.next()) |p| {
+ it_prev = p;
const row = p.rowAndCell().row;
switch (row.semantic_prompt) {
- .command => break :boundary p,
- else => {},
+ .command => break,
+
+ .unknown,
+ .prompt,
+ .prompt_continuation,
+ .input,
+ => {},
}
+ }
+
+ // Because the first line of command output may span multiple visual rows we must now
+ // iterate until we find the first row of anything other than command output and then
+ // yield the previous row.
+ while (it.next()) |p| {
+ const row = p.rowAndCell().row;
+ switch (row.semantic_prompt) {
+ .command => {},
+ .unknown,
+ .prompt,
+ .prompt_continuation,
+ .input,
+ => break :boundary it_prev,
+ }
it_prev = p;
}
@@ -7641,17 +7664,17 @@ 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("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.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
}
// zig fmt: on
@@ -7671,12 +7694,22 @@ test "Screen: selectOutput" {
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 = 7 } }).?;
+ const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
@@ -7701,7 +7734,7 @@ test "Screen: selectOutput" {
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 3,
- .y = 5,
+ .y = 7,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
@@ -7710,23 +7743,23 @@ test "Screen: selectOutput" {
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
- .y = 5,
+ .y = 7,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// No end marker, should select till the end
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 2,
- .y = 7,
+ .y = 10,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
- .y = 7,
+ .y = 9,
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
- .y = 10,
+ .y = 12,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// input / prompt at y = 0, pt.y = 0