diff --git a/README.md b/README.md index 3b1077795..796590401 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to The file format is documented below as an example: -``` +```ini # The syntax is "key = value". The whitespace around the equals doesn't matter. background = 282c34 foreground= ffffff @@ -375,9 +375,9 @@ test cases. We believe Ghostty is one of the most compliant terminal emulators available. -Terminal behavior is partially a dejour standard +Terminal behavior is partially a de jure standard (i.e. [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/)) -but mostly a defacto standard as defined by popular terminal emulators +but mostly a de facto standard as defined by popular terminal emulators worldwide. Ghostty takes the approach that our behavior is defined by (1) standards, if available, (2) xterm, if the feature exists, (3) other popular terminals, in that order. This defines what the Ghostty project diff --git a/build.zig b/build.zig index 4e7a072e6..fb74de340 100644 --- a/build.zig +++ b/build.zig @@ -499,6 +499,22 @@ pub fn build(b: *std.Build) !void { }); } + // Neovim plugin + // This is just a copy-paste of the Vim plugin, but using a Neovim subdir. + // By default, Neovim doesn't look inside share/vim/vimfiles. Some distros + // configure it to do that however. Fedora, does not as a counterexample. + { + const wf = b.addWriteFiles(); + _ = wf.add("syntax/ghostty.vim", config_vim.syntax); + _ = wf.add("ftdetect/ghostty.vim", config_vim.ftdetect); + _ = wf.add("ftplugin/ghostty.vim", config_vim.ftplugin); + b.installDirectory(.{ + .source_dir = wf.getDirectory(), + .install_dir = .prefix, + .install_subdir = "share/nvim/site", + }); + } + // Documentation if (emit_docs) { try buildDocumentation(b, config); @@ -1227,14 +1243,7 @@ fn addDeps( .optimize = optimize, }); - // This is a bit of a hack that should probably be fixed upstream - // in zig-objc, but we need to add the apple SDK paths to the - // zig-objc module so that it can find the objc runtime headers. - const module = objc_dep.module("objc"); - module.resolved_target = step.root_module.resolved_target; - try @import("apple_sdk").addPaths(b, module); - step.root_module.addImport("objc", module); - + step.root_module.addImport("objc", objc_dep.module("objc")); step.root_module.addImport("macos", macos_dep.module("macos")); step.linkLibrary(macos_dep.artifact("macos")); try static_libs.append(macos_dep.artifact("macos").getEmittedBin()); diff --git a/build.zig.zon b/build.zig.zon index b0c409778..b475b25ca 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,8 +14,8 @@ .lazy = true, }, .zig_objc = .{ - .url = "https://github.com/mitchellh/zig-objc/archive/fe5ac419530cf800294369d996133fe9cd067aec.tar.gz", - .hash = "122034b3e15d582d8d101a7713e5f13c872b8b8eb6d9cb47515b8e34ee75e122630d", + .url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz", + .hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634", }, .zig_js = .{ .url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz", diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index fc57ed034..65d26e146 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-5LBZAExb4PJefW+M0Eo+TcoszhBdIFTGBOv6lte5L0Q=" +"sha256-dNGDbVaPxDbIrDUkDwjzeRHHVcX4KnWKciXiTp1c7lE=" diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 017fc0ed4..e6bf24bd1 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -929,6 +929,13 @@ fn loadRuntimeCss( \\ --headerbar-bg-color: rgb({d},{d},{d}); \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha); \\}} + \\windowhandle {{ + \\ background-color: var(--headerbar-bg-color); + \\ color: var(--headerbar-fg-color); + \\}} + \\windowhandle:backdrop {{ + \\ background-color: var(--headerbar-backdrop-color); + \\}} , .{ headerbar_foreground.r, headerbar_foreground.g, diff --git a/src/font/embedded.zig b/src/font/embedded.zig index 2f496f86a..098aa3eb4 100644 --- a/src/font/embedded.zig +++ b/src/font/embedded.zig @@ -14,6 +14,7 @@ pub const emoji = @embedFile("res/NotoColorEmoji.ttf"); pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf"); /// Fonts with general properties +pub const arabic = @embedFile("res/KawkabMono-Regular.ttf"); pub const variable = @embedFile("res/Lilex-VF.ttf"); /// Font with nerd fonts embedded. diff --git a/src/font/res/KawkabMono-Regular.ttf b/src/font/res/KawkabMono-Regular.ttf new file mode 100644 index 000000000..4841678de Binary files /dev/null and b/src/font/res/KawkabMono-Regular.ttf differ diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index b3c8400b3..ccb422f20 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -190,6 +190,11 @@ pub const Shaper = struct { // Reset the buffer for our current run self.shaper.hb_buf.reset(); self.shaper.hb_buf.setContentType(.unicode); + + // We don't support RTL text because RTL in terminals is messy. + // Its something we want to improve. For now, we force LTR because + // our renderers assume a strictly increasing X value. + self.shaper.hb_buf.setDirection(.ltr); } pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { @@ -453,6 +458,46 @@ test "shape monaspace ligs" { } } +// Ghostty doesn't currently support RTL and our renderers assume +// that cells are in strict LTR order. This means that we need to +// force RTL text to be LTR for rendering. This test ensures that +// we are correctly forcing RTL text to be LTR. +test "shape arabic forced LTR" { + const testing = std.testing; + const alloc = testing.allocator; + + var testdata = try testShaperWithFont(alloc, .arabic); + defer testdata.deinit(); + + var screen = try terminal.Screen.init(alloc, 120, 30, 0); + defer screen.deinit(); + try screen.testWriteString(@embedFile("testdata/arabic.txt")); + + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + null, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + try testing.expectEqual(@as(usize, 25), run.cells); + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 25), cells.len); + + var x: u16 = cells[0].x; + for (cells[1..]) |cell| { + try testing.expectEqual(x + 1, cell.x); + x = cell.x; + } + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape emoji width" { const testing = std.testing; const alloc = testing.allocator; @@ -1146,6 +1191,7 @@ const TestShaper = struct { const TestFont = enum { inconsolata, monaspace_neon, + arabic, }; /// Helper to return a fully initialized shaper. @@ -1159,6 +1205,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { const testFont = switch (font_req) { .inconsolata => font.embedded.inconsolata, .monaspace_neon => font.embedded.monaspace_neon, + .arabic => font.embedded.arabic, }; var lib = try Library.init(); diff --git a/src/font/shaper/testdata/arabic.txt b/src/font/shaper/testdata/arabic.txt new file mode 100644 index 000000000..d450c7623 --- /dev/null +++ b/src/font/shaper/testdata/arabic.txt @@ -0,0 +1,3 @@ +غريبه لاني عربي أبا عن جد +واتكلم الانجليزية بطلاقة اكثر من ٢٥ سنه +ومع هذا اجد العربيه افضل لان فيها الكثير من المفردات الاكثر دقه بالوصف diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f586d22b4..cb0f5a3de 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -175,9 +175,9 @@ pub const GPUState = struct { instance: InstanceBuffer, // MTLBuffer pub fn init() !GPUState { - const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + const device = try chooseDevice(); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); - errdefer queue.msgSend(void, objc.sel("release"), .{}); + errdefer queue.release(); var instance = try InstanceBuffer.initFill(device, &.{ 0, 1, 3, // Top-left triangle @@ -200,13 +200,33 @@ pub const GPUState = struct { return result; } + fn chooseDevice() error{NoMetalDevice}!objc.Object { + const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); + defer devices.release(); + var chosen_device: ?objc.Object = null; + var iter = devices.iterate(); + while (iter.next()) |device| { + // We want a GPU that’s connected to a display. + if (device.getProperty(bool, "isHeadless")) continue; + chosen_device = device; + // If the user has an eGPU plugged in, they probably want + // to use it. Otherwise, integrated GPUs are better for + // battery life and thermals. + if (device.getProperty(bool, "isRemovable") or + device.getProperty(bool, "isLowPower")) break; + } + const device = chosen_device orelse return error.NoMetalDevice; + return device.retain(); + } + pub fn deinit(self: *GPUState) void { // Wait for all of our inflight draws to complete so that // we can cleanly deinit our GPU state. for (0..BufferCount) |_| self.frame_sema.wait(); for (&self.frames) |*frame| frame.deinit(); self.instance.deinit(); - self.queue.msgSend(void, objc.sel("release"), .{}); + self.queue.release(); + self.device.release(); } /// Get the next frame state to draw to. This will wait on the @@ -269,13 +289,13 @@ pub const FrameState = struct { .size = 8, .format = .grayscale, }); - errdefer deinitMTLResource(grayscale); + errdefer grayscale.release(); const color = try initAtlasTexture(device, &.{ .data = undefined, .size = 8, .format = .rgba, }); - errdefer deinitMTLResource(color); + errdefer color.release(); return .{ .uniforms = uniforms, @@ -290,8 +310,8 @@ pub const FrameState = struct { self.uniforms.deinit(); self.cells.deinit(); self.cells_bg.deinit(); - deinitMTLResource(self.grayscale); - deinitMTLResource(self.color); + self.grayscale.release(); + self.color.release(); } }; @@ -319,8 +339,8 @@ pub const CustomShaderState = struct { } pub fn deinit(self: *CustomShaderState) void { - deinitMTLResource(self.front_texture); - deinitMTLResource(self.back_texture); + self.front_texture.release(); + self.back_texture.release(); self.sampler.deinit(); } }; @@ -2057,8 +2077,8 @@ pub fn setScreenSize( // Only free our previous texture if this isn't our first // time setting the custom shader state. if (state.uniforms.resolution[0] > 0) { - deinitMTLResource(state.front_texture); - deinitMTLResource(state.back_texture); + state.front_texture.release(); + state.back_texture.release(); } state.uniforms.resolution = .{ @@ -2982,7 +3002,7 @@ fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *obj const width = texture.getProperty(c_ulong, "width"); if (atlas.size > width) { // Free our old texture - deinitMTLResource(texture.*); + texture.*.release(); // Reallocate texture.* = try initAtlasTexture(device, atlas); @@ -3049,12 +3069,6 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object return objc.Object.fromId(id); } -/// Deinitialize a metal resource (buffer, texture, etc.) and free the -/// memory associated with it. -fn deinitMTLResource(obj: objc.Object) void { - obj.msgSend(void, objc.sel("release"), .{}); -} - test { _ = mtl_cell; } diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 0781812ac..bd4f407cd 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -175,4 +175,4 @@ pub const MTLSize = extern struct { depth: c_ulong, }; -pub extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; +pub extern "c" fn MTLCopyAllDevices() ?*anyopaque;