Merge remote-tracking branch 'upstream/main' into titlebar-unzoom-button

This commit is contained in:
Pete Schaffner
2024-04-05 10:49:02 +02:00
20 changed files with 930 additions and 257 deletions

View File

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

View File

@ -277,6 +277,25 @@ jobs:
- name: Test Dynamic Build
run: nix develop -c zig build -Dstatic=false
test-macos:
runs-on: namespace-profile-ghostty-macos
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v14
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: test
run: nix develop -c zig build test
prettier:
runs-on: namespace-profile-ghostty-sm
timeout-minutes: 60

View File

@ -1055,11 +1055,14 @@ fn addDeps(
"fontconfig",
fontconfig_dep.module("fontconfig"),
);
if (config.font_backend.hasHarfbuzz()) step.root_module.addImport(
"harfbuzz",
harfbuzz_dep.module("harfbuzz"),
);
step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma"));
step.root_module.addImport("freetype", freetype_dep.module("freetype"));
step.root_module.addImport("glslang", glslang_dep.module("glslang"));
step.root_module.addImport("spirv_cross", spirv_cross_dep.module("spirv_cross"));
step.root_module.addImport("harfbuzz", harfbuzz_dep.module("harfbuzz"));
step.root_module.addImport("xev", libxev_dep.module("xev"));
step.root_module.addImport("opengl", opengl_dep.module("opengl"));
step.root_module.addImport("pixman", pixman_dep.module("pixman"));
@ -1110,7 +1113,6 @@ fn addDeps(
step.addIncludePath(freetype_dep.path(""));
step.linkSystemLibrary2("bzip2", dynamic_link_opts);
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
step.linkSystemLibrary2("libpng", dynamic_link_opts);
step.linkSystemLibrary2("oniguruma", dynamic_link_opts);
step.linkSystemLibrary2("pixman-1", dynamic_link_opts);
@ -1119,6 +1121,9 @@ fn addDeps(
if (config.font_backend.hasFontconfig()) {
step.linkSystemLibrary2("fontconfig", dynamic_link_opts);
}
if (config.font_backend.hasHarfbuzz()) {
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
}
}
// Other dependencies, we may dynamically link
@ -1136,14 +1141,16 @@ fn addDeps(
step.linkLibrary(freetype_dep.artifact("freetype"));
try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin());
// Harfbuzz
step.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin());
// Pixman
step.linkLibrary(pixman_dep.artifact("pixman"));
try static_libs.append(pixman_dep.artifact("pixman").getEmittedBin());
// Harfbuzz
if (config.font_backend.hasHarfbuzz()) {
step.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin());
}
// Only Linux gets fontconfig
if (config.font_backend.hasFontconfig()) {
// Fontconfig

36
flake.lock generated
View File

@ -52,11 +52,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@ -88,11 +88,11 @@
]
},
"locked": {
"lastModified": 1694102001,
"narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=",
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
@ -104,13 +104,13 @@
"langref": {
"flake": false,
"locked": {
"narHash": "sha256-mYdDCBdNEIeMbavdhSo8qXqW+3fqPC8BAich7W3umrI=",
"narHash": "sha256-94broSBethRhPJr0G9no4TPyB8ee6BQ/hHK1QnLPln0=",
"type": "file",
"url": "https://raw.githubusercontent.com/ziglang/zig/63bd2bff12992aef0ce23ae4b344e9cb5d65f05d/doc/langref.html.in"
"url": "https://raw.githubusercontent.com/ziglang/zig/54bbc73f8502fe073d385361ddb34a43d12eec39/doc/langref.html.in"
},
"original": {
"type": "file",
"url": "https://raw.githubusercontent.com/ziglang/zig/63bd2bff12992aef0ce23ae4b344e9cb5d65f05d/doc/langref.html.in"
"url": "https://raw.githubusercontent.com/ziglang/zig/54bbc73f8502fe073d385361ddb34a43d12eec39/doc/langref.html.in"
}
},
"nixpkgs-stable": {
@ -147,11 +147,11 @@
},
"nixpkgs-zig-0-12": {
"locked": {
"lastModified": 1711789504,
"narHash": "sha256-1XRwW0MD9LxtHMMlPmF3rDw/Zbv4jLnpGnJEtibO+MQ=",
"lastModified": 1712247214,
"narHash": "sha256-7PTw86NnE2nCQPf+PPI/kOKwmlbbTqUthYSz/nDnAoc=",
"owner": "vancluever",
"repo": "nixpkgs",
"rev": "c9e24149cca8215b84fc3ce5bc2bdc1ca823a588",
"rev": "6726262c930716f601345b2c9d0c42ba069991b8",
"type": "github"
},
"original": {
@ -217,11 +217,11 @@
]
},
"locked": {
"lastModified": 1701390337,
"narHash": "sha256-C+Lyio+GPl3B2IAZ6Nk5hAAE2g6a8bO9vMACUfOLC/g=",
"lastModified": 1711627798,
"narHash": "sha256-4BUZmgUFrrD5dRZbOUYRRQEDwLX/r7/ErLi+vHfB/+8=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "1815ef2d0451b1121a6a91051da84906fcb06f99",
"rev": "b01e0b81d1fa489e54362ea0a74f182eaa9a35bb",
"type": "github"
},
"original": {
@ -241,11 +241,11 @@
"zig-overlay": "zig-overlay"
},
"locked": {
"lastModified": 1703036566,
"narHash": "sha256-GKw+ON8FcUVHxDzA35piev/W/YUvXv5aZ4hmIzNFWuc=",
"lastModified": 1711925513,
"narHash": "sha256-DFgsGlEGsxLgtRrh7J+v8x4w+/cJatTCkrZP3/0Gb/o=",
"owner": "zigtools",
"repo": "zls",
"rev": "adaeabbe1ba888d74309d0a837d4abddc24cf638",
"rev": "4e01c08f558ea07462aaa7b71d2a24f86f47a855",
"type": "github"
},
"original": {

View File

@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const base = @import("base.zig");
const c = @import("c.zig");
const cftype = @import("type.zig");
const ComparisonResult = base.ComparisonResult;
const Range = base.Range;
@ -42,6 +43,14 @@ pub const Array = opaque {
};
pub const MutableArray = opaque {
pub fn create() Allocator.Error!*MutableArray {
return CFArrayCreateMutable(
null,
0,
&c.kCFTypeArrayCallBacks,
) orelse error.OutOfMemory;
}
pub fn createCopy(array: *Array) Allocator.Error!*MutableArray {
return CFArrayCreateMutableCopy(
null,
@ -54,6 +63,18 @@ pub const MutableArray = opaque {
cftype.CFRelease(self);
}
pub fn appendValue(
self: *MutableArray,
comptime Elem: type,
value: *const Elem,
) void {
CFArrayAppendValue(self, @constCast(@ptrCast(value)));
}
pub fn removeValue(self: *MutableArray, idx: usize) void {
CFArrayRemoveValueAtIndex(self, idx);
}
pub fn sortValues(
self: *MutableArray,
comptime Elem: type,
@ -73,12 +94,24 @@ pub const MutableArray = opaque {
);
}
extern "c" fn CFArrayCreateMutable(
allocator: ?*anyopaque,
capacity: usize,
callbacks: ?*const anyopaque,
) ?*MutableArray;
extern "c" fn CFArrayCreateMutableCopy(
allocator: ?*anyopaque,
capacity: usize,
array: *Array,
) ?*MutableArray;
extern "c" fn CFArrayAppendValue(
*MutableArray,
*anyopaque,
) void;
extern "c" fn CFArrayRemoveValueAtIndex(
*MutableArray,
usize,
) void;
extern "c" fn CFArraySortValues(
array: *MutableArray,
range: Range,

View File

@ -6,8 +6,8 @@ const c = @import("c.zig");
pub const Dictionary = opaque {
pub fn create(
keys: ?[]?*const anyopaque,
values: ?[]?*const anyopaque,
keys: ?[]const ?*const anyopaque,
values: ?[]const ?*const anyopaque,
) Allocator.Error!*Dictionary {
if (keys != null or values != null) {
assert(keys != null);
@ -17,8 +17,8 @@ pub const Dictionary = opaque {
return @as(?*Dictionary, @ptrFromInt(@intFromPtr(c.CFDictionaryCreate(
null,
@ptrCast(if (keys) |slice| slice.ptr else null),
@ptrCast(if (values) |slice| slice.ptr else null),
@constCast(@ptrCast(if (keys) |slice| slice.ptr else null)),
@constCast(@ptrCast(if (values) |slice| slice.ptr else null)),
@intCast(if (keys) |slice| slice.len else 0),
&c.kCFTypeDictionaryKeyCallBacks,
&c.kCFTypeDictionaryValueCallBacks,

View File

@ -2146,7 +2146,7 @@ pub fn mouseButtonCallback(
const pos = try self.rt_surface.getCursorPos();
const point = self.posToViewport(pos.x, pos.y);
const screen = &self.renderer_state.terminal.screen;
const p = screen.pages.pin(.{ .active = point }) orelse {
const p = screen.pages.pin(.{ .viewport = point }) orelse {
log.warn("failed to get pin for clicked point", .{});
return;
};

View File

@ -839,7 +839,9 @@ keybind: Keybinds = .{},
///
/// * `sudo` - Set sudo wrapper to preserve terminfo.
///
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`
/// * `title` - Set the window title via shell integration.
///
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`
@"shell-integration-features": ShellIntegrationFeatures = .{},
/// Sets the reporting format for OSC sequences that request color information.
@ -3378,6 +3380,7 @@ pub const ShellIntegration = enum {
pub const ShellIntegrationFeatures = packed struct {
cursor: bool = true,
sudo: bool = false,
title: bool = true,
};
/// OSC 4, 10, 11, and 12 default color reporting format.

View File

@ -13,8 +13,9 @@ pub const Face = struct {
/// Our font face
font: *macos.text.Font,
/// Harfbuzz font corresponding to this face.
hb_font: harfbuzz.Font,
/// Harfbuzz font corresponding to this face. We only use this
/// if we're using Harfbuzz.
hb_font: if (harfbuzz_shaper) harfbuzz.Font else void,
/// The presentation for this font.
presentation: font.Presentation,
@ -25,6 +26,10 @@ pub const Face = struct {
/// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false,
/// True if our build is using Harfbuzz. If we're not, we can avoid
/// some Harfbuzz-specific code paths.
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
/// The matrix applied to a regular font to auto-italicize it.
pub const italic_skew = macos.graphics.AffineTransform{
.a = 1,
@ -75,10 +80,6 @@ pub const Face = struct {
/// Initialize a face with a CTFont. This will take ownership over
/// the CTFont. This does NOT copy or retain the CTFont.
pub fn initFont(ct_font: *macos.text.Font, opts: font.face.Options) !Face {
var hb_font = try harfbuzz.coretext.createFont(ct_font);
errdefer hb_font.destroy();
hb_font.setScale(opts.size.pixels(), opts.size.pixels());
const traits = ct_font.getSymbolicTraits();
const metrics = metrics: {
var metrics = try calcMetrics(ct_font);
@ -86,6 +87,13 @@ pub const Face = struct {
break :metrics metrics;
};
var hb_font = if (comptime harfbuzz_shaper) font: {
var hb_font = try harfbuzz.coretext.createFont(ct_font);
hb_font.setScale(opts.size.pixels(), opts.size.pixels());
break :font hb_font;
} else {};
errdefer if (comptime harfbuzz_shaper) hb_font.destroy();
var result: Face = .{
.font = ct_font,
.hb_font = hb_font,
@ -144,7 +152,7 @@ pub const Face = struct {
pub fn deinit(self: *Face) void {
self.font.release();
self.hb_font.destroy();
if (comptime harfbuzz_shaper) self.hb_font.destroy();
self.* = undefined;
}

View File

@ -115,6 +115,19 @@ pub const Backend = enum {
=> false,
};
}
pub fn hasHarfbuzz(self: Backend) bool {
return switch (self) {
.freetype,
.fontconfig_freetype,
.coretext_freetype,
=> true,
.coretext,
.web_canvas,
=> false,
};
}
};
/// The styles that a family can take.

Binary file not shown.

View File

@ -1,7 +1,7 @@
const builtin = @import("builtin");
const options = @import("main.zig").options;
const harfbuzz = @import("shaper/harfbuzz.zig");
const coretext = @import("shaper/coretext.zig");
pub const harfbuzz = @import("shaper/harfbuzz.zig");
pub const coretext = @import("shaper/coretext.zig");
pub const web_canvas = @import("shaper/web_canvas.zig");
pub usingnamespace @import("shaper/run.zig");
@ -10,12 +10,12 @@ pub const Shaper = switch (options.backend) {
.freetype,
.fontconfig_freetype,
.coretext_freetype,
.coretext,
=> harfbuzz.Shaper,
// Has missing features, can't be used yet. See the comments in
// the coretext.zig file for more details.
//.coretext => coretext.Shaper,
// Note that coretext_freetype cannot use the coretext
// shaper because the coretext shaper requests CoreText
// font faces.
.coretext => coretext.Shaper,
.web_canvas => web_canvas.Shaper,
};

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,9 @@ pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
pub const fontVariable = @embedFile("res/Lilex-VF.ttf");
/// Font with nerd fonts embedded.
pub const fontNerdFont = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf");
/// Cozette is a unique font because it embeds some emoji characters
/// but has a text presentation.
pub const fontCozette = @embedFile("res/CozetteVector.ttf");

View File

@ -254,10 +254,19 @@ fn legacy(
self.ignore_keypad_with_numlock,
self.modify_other_keys_state_2,
)) |sequence| pc_style: {
// If we're pressing enter and have UTF-8 text, we probably are
// clearing a dead key state. This happens specifically on macOS.
// If we're pressing enter or escape and have UTF-8 text, we probably
// are clearing a dead key state. This happens specifically on macOS.
// "Clearing" a dead key state may or may not commit the dead key
// state; this differs by language:
//
// - Japanese clears and does not write the characters.
// - Korean clears and writes the characters.
//
// We have a unit test for this.
if (self.event.key == .enter and self.event.utf8.len > 0) {
if ((self.event.key == .enter or
self.event.key == .escape) and
self.event.utf8.len > 0)
{
break :pc_style;
}
@ -1649,6 +1658,20 @@ test "legacy: enter with utf8 (dead key state)" {
try testing.expectEqualStrings("A", actual);
}
test "legacy: esc with utf8 (dead key state)" {
var buf: [128]u8 = undefined;
var enc: KeyEncoder = .{
.event = .{
.key = .escape,
.utf8 = "A",
.unshifted_codepoint = 0x0D,
},
};
const actual = try enc.legacy(&buf);
try testing.expectEqualStrings("A", actual);
}
test "legacy: ctrl+shift+minus (underscore on US)" {
var buf: [128]u8 = undefined;
var enc: KeyEncoder = .{

View File

@ -256,7 +256,9 @@ pub const GlobalState = struct {
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
std.log.info("runtime={}", .{build_config.app_runtime});
std.log.info("font_backend={}", .{build_config.font_backend});
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
if (comptime build_config.font_backend.hasHarfbuzz()) {
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
}
if (comptime build_config.font_backend.hasFontconfig()) {
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
}

View File

@ -70,9 +70,11 @@ function __ghostty_precmd() {
}
fi
# Command
PS0=$PS0'$(__ghostty_get_current_command)'
PS1=$PS1'\[\e]2;$PWD\a\]'
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
# Command
PS0=$PS0'$(__ghostty_get_current_command)'
PS1=$PS1'\[\e]2;$PWD\a\]'
fi
fi
if test "$_ghostty_executing" != "0"; then

View File

@ -194,11 +194,13 @@ _ghostty_deferred_init() {
_ghostty_report_pwd"
_ghostty_report_pwd
# Enable terminal title changes.
functions[_ghostty_precmd]+="
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'"
functions[_ghostty_preexec]+="
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'"
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
# Enable terminal title changes.
functions[_ghostty_precmd]+="
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'"
functions[_ghostty_preexec]+="
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'"
fi
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; then
# Enable cursor shape changes depending on the current keymap.

View File

@ -1355,14 +1355,14 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
defer if (mapbuilder) |*b| b.deinit();
const sel_ordered = opts.sel.ordered(self, .forward);
const sel_start = start: {
var start = sel_ordered.start();
const sel_start: Pin = start: {
var start: Pin = sel_ordered.start();
const cell = start.rowAndCell().cell;
if (cell.wide == .spacer_tail) start.x -= 1;
break :start start;
};
const sel_end = end: {
var end = sel_ordered.end();
const sel_end: Pin = end: {
var end: Pin = sel_ordered.end();
const cell = end.rowAndCell().cell;
switch (cell.wide) {
.narrow, .wide => {},
@ -1433,7 +1433,9 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) !
}
}
if (row_count < rows.len - 1 and
const is_final_row = chunk.page == sel_end.page and y == sel_end.y;
if (!is_final_row and
(!row.wrap or sel_ordered.rectangle))
{
try strbuilder.append('\n');
@ -7073,6 +7075,38 @@ test "Screen: selectionString, rectangle, more complex w/breaks" {
try testing.expectEqualStrings(expected, contents);
}
test "Screen: selectionString multi-page" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 1, 3, 2048);
defer s.deinit();
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
// Lazy way to seek to the first page boundary.
for (0..first_page_size - 1) |_| {
try s.testWriteString("\n");
}
try s.testWriteString("y\ny\ny");
{
const sel = Selection.init(
s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
s.pages.pin(.{ .active = .{ .x = 0, .y = 2 } }).?,
false,
);
const contents = try s.selectionString(alloc, .{
.sel = sel,
.trim = false,
});
defer alloc.free(contents);
const expected = "y\ny\ny";
try testing.expectEqualStrings(expected, contents);
}
}
test "Screen: lineIterator" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -50,6 +50,7 @@ pub fn setup(
// Setup our feature env vars
if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1");
if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
return shell;
}