mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'mitchellh:main' into main
This commit is contained in:
@ -31,6 +31,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
|
self,
|
||||||
nixpkgs-unstable,
|
nixpkgs-unstable,
|
||||||
nixpkgs-stable,
|
nixpkgs-stable,
|
||||||
nixpkgs-zig-0-12,
|
nixpkgs-zig-0-12,
|
||||||
@ -54,6 +55,7 @@
|
|||||||
packages.${system} = rec {
|
packages.${system} = rec {
|
||||||
ghostty = pkgs-stable.callPackage ./nix/package.nix {
|
ghostty = pkgs-stable.callPackage ./nix/package.nix {
|
||||||
inherit (pkgs-zig-0-12) zig_0_12;
|
inherit (pkgs-zig-0-12) zig_0_12;
|
||||||
|
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||||
};
|
};
|
||||||
default = ghostty;
|
default = ghostty;
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
ncurses,
|
ncurses,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
zig_0_12,
|
zig_0_12,
|
||||||
|
revision ? "dirty",
|
||||||
}: let
|
}: let
|
||||||
# The Zig hook has no way to select the release type without actual
|
# The Zig hook has no way to select the release type without actual
|
||||||
# overriding of the default flags.
|
# overriding of the default flags.
|
||||||
@ -121,7 +122,7 @@ in
|
|||||||
|
|
||||||
dontConfigure = true;
|
dontConfigure = true;
|
||||||
|
|
||||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}";
|
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix";
|
||||||
|
|
||||||
preBuild = ''
|
preBuild = ''
|
||||||
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
||||||
|
@ -94,6 +94,7 @@ pub const Error = error{
|
|||||||
BbxTooBig,
|
BbxTooBig,
|
||||||
CorruptedFontHeader,
|
CorruptedFontHeader,
|
||||||
CorruptedFontGlyphs,
|
CorruptedFontGlyphs,
|
||||||
|
UnknownFreetypeError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn intToError(err: c_int) Error!void {
|
pub fn intToError(err: c_int) Error!void {
|
||||||
@ -188,7 +189,7 @@ pub fn intToError(err: c_int) Error!void {
|
|||||||
c.FT_Err_Bbx_Too_Big => Error.BbxTooBig,
|
c.FT_Err_Bbx_Too_Big => Error.BbxTooBig,
|
||||||
c.FT_Err_Corrupted_Font_Header => Error.CorruptedFontHeader,
|
c.FT_Err_Corrupted_Font_Header => Error.CorruptedFontHeader,
|
||||||
c.FT_Err_Corrupted_Font_Glyphs => Error.CorruptedFontGlyphs,
|
c.FT_Err_Corrupted_Font_Glyphs => Error.CorruptedFontGlyphs,
|
||||||
else => unreachable,
|
else => Error.UnknownFreetypeError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,6 +410,21 @@ pub fn init(
|
|||||||
_ = try group.addFace(.bold_italic, .{ .deferred = face });
|
_ = try group.addFace(.bold_italic, .{ .deferred = face });
|
||||||
} else log.warn("font-family-bold-italic not found: {s}", .{family});
|
} else log.warn("font-family-bold-italic not found: {s}", .{family});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On macOS, always search for and add the Apple Emoji font
|
||||||
|
// as our preferred emoji font for fallback. We do this in case
|
||||||
|
// people add other emoji fonts to their system, we always want to
|
||||||
|
// prefer the official one. Users can override this by explicitly
|
||||||
|
// specifying a font-family for emoji.
|
||||||
|
if (comptime builtin.os.tag == .macos) {
|
||||||
|
var disco_it = try disco.discover(alloc, .{
|
||||||
|
.family = "Apple Color Emoji",
|
||||||
|
});
|
||||||
|
defer disco_it.deinit();
|
||||||
|
if (try disco_it.next()) |face| {
|
||||||
|
_ = try group.addFace(.regular, .{ .fallback_deferred = face });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Our built-in font will be used as a backup
|
// Our built-in font will be used as a backup
|
||||||
@ -2773,6 +2788,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
},
|
},
|
||||||
|
|
||||||
.clear_screen => {
|
.clear_screen => {
|
||||||
|
// This is a duplicate of some of the logic in termio.clearScreen
|
||||||
|
// but we need to do this here so we can know the answer before
|
||||||
|
// we send the message. If the currently active screen is on the
|
||||||
|
// alternate screen then clear screen does nothing so we want to
|
||||||
|
// return false so the keybind can be unconsumed.
|
||||||
|
{
|
||||||
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
if (self.io.terminal.active_screen == .alternate) return false;
|
||||||
|
}
|
||||||
|
|
||||||
_ = self.io_thread.mailbox.push(.{
|
_ = self.io_thread.mailbox.push(.{
|
||||||
.clear_screen = .{ .history = true },
|
.clear_screen = .{ .history = true },
|
||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
|
@ -1674,16 +1674,22 @@ pub fn finalize(self: *Config) !void {
|
|||||||
// set to /bin/sh.
|
// set to /bin/sh.
|
||||||
if (self.command) |cmd|
|
if (self.command) |cmd|
|
||||||
log.info("shell src=config value={s}", .{cmd})
|
log.info("shell src=config value={s}", .{cmd})
|
||||||
else {
|
else shell_env: {
|
||||||
if (!internal_os.isFlatpak()) {
|
// Flatpak always gets its shell from outside the sandbox
|
||||||
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
|
if (internal_os.isFlatpak()) break :shell_env;
|
||||||
log.info("default shell source=env value={s}", .{value});
|
|
||||||
self.command = value;
|
|
||||||
|
|
||||||
// If we don't need the working directory, then we can exit now.
|
// If we were launched from the desktop, our SHELL env var
|
||||||
if (!wd_home) break :command;
|
// will represent our SHELL at login time. We want to use the
|
||||||
} else |_| {}
|
// latest shell from /etc/passwd or directory services.
|
||||||
}
|
if (internal_os.launchedFromDesktop()) break :shell_env;
|
||||||
|
|
||||||
|
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
|
||||||
|
log.info("default shell source=env value={s}", .{value});
|
||||||
|
self.command = value;
|
||||||
|
|
||||||
|
// If we don't need the working directory, then we can exit now.
|
||||||
|
if (!wd_home) break :command;
|
||||||
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (builtin.os.tag) {
|
switch (builtin.os.tag) {
|
||||||
|
BIN
src/font/res/MonaspaceNeon-Regular.otf
Normal file
BIN
src/font/res/MonaspaceNeon-Regular.otf
Normal file
Binary file not shown.
@ -37,7 +37,10 @@ pub const Cell = struct {
|
|||||||
/// this cell is available in the text run. This glyph index is only
|
/// this cell is available in the text run. This glyph index is only
|
||||||
/// valid for a given GroupCache and FontIndex that was used to create
|
/// valid for a given GroupCache and FontIndex that was used to create
|
||||||
/// the runs.
|
/// the runs.
|
||||||
glyph_index: u32,
|
///
|
||||||
|
/// If this is null then this is an empty cell. If there are styles
|
||||||
|
/// then those should be applied but there is no glyph to render.
|
||||||
|
glyph_index: ?u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Options for shapers.
|
/// Options for shapers.
|
||||||
|
@ -230,6 +230,10 @@ pub const Shaper = struct {
|
|||||||
cell_offset.x += advance.width;
|
cell_offset.x += advance.width;
|
||||||
cell_offset.y += advance.height;
|
cell_offset.y += advance.height;
|
||||||
|
|
||||||
|
// TODO: harfbuzz shaper has handling for inserting blank
|
||||||
|
// cells for multi-cell ligatures. Do we need to port that?
|
||||||
|
// Example: try Monaspace "===" with a background color.
|
||||||
|
|
||||||
_ = pos;
|
_ = pos;
|
||||||
// const i = self.cell_buf.items.len - 1;
|
// const i = self.cell_buf.items.len - 1;
|
||||||
// log.warn(
|
// log.warn(
|
||||||
|
@ -142,13 +142,13 @@ pub const Shaper = struct {
|
|||||||
|
|
||||||
// Convert all our info/pos to cells and set it.
|
// Convert all our info/pos to cells and set it.
|
||||||
self.cell_buf.clearRetainingCapacity();
|
self.cell_buf.clearRetainingCapacity();
|
||||||
try self.cell_buf.ensureTotalCapacity(self.alloc, info.len);
|
for (info, pos, 0..) |info_v, pos_v, i| {
|
||||||
for (info, pos) |info_v, pos_v| {
|
// If our cluster changed then we've moved to a new cell.
|
||||||
if (info_v.cluster != cell_offset.cluster) cell_offset = .{
|
if (info_v.cluster != cell_offset.cluster) cell_offset = .{
|
||||||
.cluster = info_v.cluster,
|
.cluster = info_v.cluster,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.cell_buf.appendAssumeCapacity(.{
|
try self.cell_buf.append(self.alloc, .{
|
||||||
.x = @intCast(info_v.cluster),
|
.x = @intCast(info_v.cluster),
|
||||||
.x_offset = @intCast(cell_offset.x),
|
.x_offset = @intCast(cell_offset.x),
|
||||||
.y_offset = @intCast(cell_offset.y),
|
.y_offset = @intCast(cell_offset.y),
|
||||||
@ -166,6 +166,43 @@ pub const Shaper = struct {
|
|||||||
cell_offset.y += pos_v.y_advance;
|
cell_offset.y += pos_v.y_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the width of the cell. To do this, we have to
|
||||||
|
// find the next cluster that has been shaped. This tells us how
|
||||||
|
// many cells this glyph replaced (i.e. for ligatures). For example
|
||||||
|
// in some fonts "!=" turns into a single glyph from the component
|
||||||
|
// parts "!" and "=" so this cell width would be "2" despite
|
||||||
|
// only having a single glyph.
|
||||||
|
//
|
||||||
|
// Many fonts replace ligature cells with space so that this always
|
||||||
|
// is one (e.g. Fira Code, JetBrains Mono, etc). Some do not
|
||||||
|
// (e.g. Monaspace).
|
||||||
|
const cell_width = width: {
|
||||||
|
if (i + 1 < info.len) {
|
||||||
|
// We may have to go through multiple glyphs because
|
||||||
|
// multiple can be replaced. e.g. "==="
|
||||||
|
for (info[i + 1 ..]) |next_info_v| {
|
||||||
|
if (next_info_v.cluster != info_v.cluster) {
|
||||||
|
break :width next_info_v.cluster - info_v.cluster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reached the end then our width is our max cluster
|
||||||
|
// minus this one.
|
||||||
|
const max = run.offset + run.cells;
|
||||||
|
break :width max - info_v.cluster;
|
||||||
|
};
|
||||||
|
if (cell_width > 1) {
|
||||||
|
// To make the renderer implementations simpler, we convert
|
||||||
|
// the extra spaces for width to blank cells.
|
||||||
|
for (1..cell_width) |j| {
|
||||||
|
try self.cell_buf.append(self.alloc, .{
|
||||||
|
.x = @intCast(info_v.cluster + j),
|
||||||
|
.glyph_index = null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// const i = self.cell_buf.items.len - 1;
|
// const i = self.cell_buf.items.len - 1;
|
||||||
// log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] });
|
// log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] });
|
||||||
}
|
}
|
||||||
@ -334,7 +371,9 @@ test "shape inconsolata ligs" {
|
|||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
const cells = try shaper.shape(run);
|
const cells = try shaper.shape(run);
|
||||||
try testing.expectEqual(@as(usize, 1), cells.len);
|
try testing.expectEqual(@as(usize, 2), cells.len);
|
||||||
|
try testing.expect(cells[0].glyph_index != null);
|
||||||
|
try testing.expect(cells[1].glyph_index == null);
|
||||||
}
|
}
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
@ -351,7 +390,38 @@ test "shape inconsolata ligs" {
|
|||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
const cells = try shaper.shape(run);
|
const cells = try shaper.shape(run);
|
||||||
try testing.expectEqual(@as(usize, 1), cells.len);
|
try testing.expectEqual(@as(usize, 3), cells.len);
|
||||||
|
try testing.expect(cells[0].glyph_index != null);
|
||||||
|
try testing.expect(cells[1].glyph_index == null);
|
||||||
|
try testing.expect(cells[2].glyph_index == null);
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shape monaspace ligs" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var testdata = try testShaperWithFont(alloc, .monaspace_neon);
|
||||||
|
defer testdata.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
|
defer screen.deinit();
|
||||||
|
try screen.testWriteString("===");
|
||||||
|
|
||||||
|
var shaper = &testdata.shaper;
|
||||||
|
var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null);
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |run| {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const cells = try shaper.shape(run);
|
||||||
|
try testing.expectEqual(@as(usize, 3), cells.len);
|
||||||
|
try testing.expect(cells[0].glyph_index != null);
|
||||||
|
try testing.expect(cells[1].glyph_index == null);
|
||||||
|
try testing.expect(cells[2].glyph_index == null);
|
||||||
}
|
}
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
@ -376,7 +446,7 @@ test "shape emoji width" {
|
|||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
const cells = try shaper.shape(run);
|
const cells = try shaper.shape(run);
|
||||||
try testing.expectEqual(@as(usize, 1), cells.len);
|
try testing.expectEqual(@as(usize, 2), cells.len);
|
||||||
}
|
}
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
@ -411,7 +481,9 @@ test "shape emoji width long" {
|
|||||||
try testing.expectEqual(@as(u32, 4), shaper.hb_buf.getLength());
|
try testing.expectEqual(@as(u32, 4), shaper.hb_buf.getLength());
|
||||||
|
|
||||||
const cells = try shaper.shape(run);
|
const cells = try shaper.shape(run);
|
||||||
try testing.expectEqual(@as(usize, 1), cells.len);
|
|
||||||
|
// screen.testWriteString isn't grapheme aware, otherwise this is two
|
||||||
|
try testing.expectEqual(@as(usize, 5), cells.len);
|
||||||
}
|
}
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
@ -574,9 +646,9 @@ test "shape box glyphs" {
|
|||||||
try testing.expectEqual(@as(u32, 2), shaper.hb_buf.getLength());
|
try testing.expectEqual(@as(u32, 2), shaper.hb_buf.getLength());
|
||||||
const cells = try shaper.shape(run);
|
const cells = try shaper.shape(run);
|
||||||
try testing.expectEqual(@as(usize, 2), cells.len);
|
try testing.expectEqual(@as(usize, 2), cells.len);
|
||||||
try testing.expectEqual(@as(u32, 0x2500), cells[0].glyph_index);
|
try testing.expectEqual(@as(u32, 0x2500), cells[0].glyph_index.?);
|
||||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||||
try testing.expectEqual(@as(u32, 0x2501), cells[1].glyph_index);
|
try testing.expectEqual(@as(u32, 0x2501), cells[1].glyph_index.?);
|
||||||
try testing.expectEqual(@as(u16, 1), cells[1].x);
|
try testing.expectEqual(@as(u16, 1), cells[1].x);
|
||||||
}
|
}
|
||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
@ -902,11 +974,23 @@ const TestShaper = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TestFont = enum {
|
||||||
|
inconsolata,
|
||||||
|
monaspace_neon,
|
||||||
|
};
|
||||||
|
|
||||||
/// Helper to return a fully initialized shaper.
|
/// Helper to return a fully initialized shaper.
|
||||||
fn testShaper(alloc: Allocator) !TestShaper {
|
fn testShaper(alloc: Allocator) !TestShaper {
|
||||||
const testFont = @import("../test.zig").fontRegular;
|
return try testShaperWithFont(alloc, .inconsolata);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
||||||
const testEmoji = @import("../test.zig").fontEmoji;
|
const testEmoji = @import("../test.zig").fontEmoji;
|
||||||
const testEmojiText = @import("../test.zig").fontEmojiText;
|
const testEmojiText = @import("../test.zig").fontEmojiText;
|
||||||
|
const testFont = switch (font_req) {
|
||||||
|
.inconsolata => @import("../test.zig").fontRegular,
|
||||||
|
.monaspace_neon => @import("../test.zig").fontMonaspaceNeon,
|
||||||
|
};
|
||||||
|
|
||||||
var lib = try Library.init();
|
var lib = try Library.init();
|
||||||
errdefer lib.deinit();
|
errdefer lib.deinit();
|
||||||
|
@ -15,3 +15,7 @@ pub const fontVariable = @embedFile("res/Lilex-VF.ttf");
|
|||||||
/// Cozette is a unique font because it embeds some emoji characters
|
/// Cozette is a unique font because it embeds some emoji characters
|
||||||
/// but has a text presentation.
|
/// but has a text presentation.
|
||||||
pub const fontCozette = @embedFile("res/CozetteVector.ttf");
|
pub const fontCozette = @embedFile("res/CozetteVector.ttf");
|
||||||
|
|
||||||
|
/// Monaspace has weird ligature behaviors we want to test in our shapers
|
||||||
|
/// so we embed it here.
|
||||||
|
pub const fontMonaspaceNeon = @embedFile("res/MonaspaceNeon-Regular.otf");
|
||||||
|
@ -19,7 +19,15 @@ pub fn launchedFromDesktop() bool {
|
|||||||
return switch (builtin.os.tag) {
|
return switch (builtin.os.tag) {
|
||||||
// macOS apps launched from finder or `open` always have the init
|
// macOS apps launched from finder or `open` always have the init
|
||||||
// process as their parent.
|
// process as their parent.
|
||||||
.macos => c.getppid() == 1,
|
.macos => macos: {
|
||||||
|
// This special case is so that if we launch the app via the
|
||||||
|
// app bundle (i.e. via open) then we still treat it as if it
|
||||||
|
// was launched from the desktop.
|
||||||
|
if (build_config.artifact == .lib and
|
||||||
|
std.os.getenv("GHOSTTY_MAC_APP") != null) break :macos true;
|
||||||
|
|
||||||
|
break :macos c.getppid() == 1;
|
||||||
|
},
|
||||||
|
|
||||||
// On Linux, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
|
// On Linux, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
|
||||||
// GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if
|
// GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if
|
||||||
|
@ -1796,12 +1796,12 @@ fn updateCell(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If the cell has a character, draw it
|
// If the cell has a character, draw it
|
||||||
if (cell.char > 0) {
|
if (cell.char > 0) fg: {
|
||||||
// Render
|
// Render
|
||||||
const glyph = try self.font_group.renderGlyph(
|
const glyph = try self.font_group.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index,
|
shaper_cell.glyph_index orelse break :fg,
|
||||||
.{
|
.{
|
||||||
.grid_metrics = self.grid_metrics,
|
.grid_metrics = self.grid_metrics,
|
||||||
.thicken = self.config.font_thicken,
|
.thicken = self.config.font_thicken,
|
||||||
|
@ -1504,12 +1504,12 @@ fn updateCell(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If the cell has a character, draw it
|
// If the cell has a character, draw it
|
||||||
if (cell.char > 0) {
|
if (cell.char > 0) fg: {
|
||||||
// Render
|
// Render
|
||||||
const glyph = try self.font_group.renderGlyph(
|
const glyph = try self.font_group.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index,
|
shaper_cell.glyph_index orelse break :fg,
|
||||||
.{
|
.{
|
||||||
.grid_metrics = self.grid_metrics,
|
.grid_metrics = self.grid_metrics,
|
||||||
.thicken = self.config.font_thicken,
|
.thicken = self.config.font_thicken,
|
||||||
|
Reference in New Issue
Block a user