diff options
| -rw-r--r-- | .github/workflows/publish-tag.yml | 74 | ||||
| -rw-r--r-- | .github/workflows/release-tag.yml | 17 | ||||
| -rw-r--r-- | build.zig.zon | 2 | ||||
| -rw-r--r-- | dist/macos/update_appcast_tag.py | 7 | ||||
| -rw-r--r-- | nix/package.nix | 20 | ||||
| -rw-r--r-- | src/apprt/gtk/Surface.zig | 1 | ||||
| -rw-r--r-- | src/apprt/gtk/Window.zig | 5 | ||||
| -rw-r--r-- | src/apprt/gtk/key.zig | 57 | ||||
| -rw-r--r-- | src/build/Config.zig | 2 | ||||
| -rw-r--r-- | src/config/Config.zig | 40 | ||||
| -rw-r--r-- | src/input/Binding.zig | 56 | ||||
| -rw-r--r-- | src/renderer/Metal.zig | 10 | ||||
| -rw-r--r-- | src/renderer/metal/shaders.zig | 2 | ||||
| -rw-r--r-- | src/renderer/shaders/cell.metal | 95 | ||||
| -rw-r--r-- | src/terminal/Screen.zig | 71 |
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 |
