diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 2c971a301..10cbdacfe 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -96,6 +96,12 @@ jobs: - name: Build Ghostty.app run: cd macos && xcodebuild -configuration Release + # Copy the resources we build during zig build into the final Ghostty.app + - name: Copy Resources + run: | + # Terminfo + cp -R zig-out/Ghostty.app/Contents/Resources/terminfo macos/build/Release/Ghostty.app/Contents/Resources/terminfo + # 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: Inject Build Number diff --git a/build.zig b/build.zig index 8f5d075ab..b846894b7 100644 --- a/build.zig +++ b/build.zig @@ -3,7 +3,16 @@ const builtin = @import("builtin"); const fs = std.fs; const LibExeObjStep = std.build.LibExeObjStep; const RunStep = std.build.RunStep; + const apprt = @import("src/apprt.zig"); +const font = @import("src/font/main.zig"); +const terminfo = @import("src/terminfo/main.zig"); +const WasmTarget = @import("src/os/wasm/target.zig").Target; +const LibtoolStep = @import("src/build/LibtoolStep.zig"); +const LipoStep = @import("src/build/LipoStep.zig"); +const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig"); +const Version = @import("src/build/Version.zig"); + const glfw = @import("vendor/mach-glfw/build.zig"); const fontconfig = @import("pkg/fontconfig/build.zig"); const freetype = @import("pkg/freetype/build.zig"); @@ -21,12 +30,6 @@ const utf8proc = @import("pkg/utf8proc/build.zig"); const zlib = @import("pkg/zlib/build.zig"); const tracylib = @import("pkg/tracy/build.zig"); const system_sdk = @import("vendor/mach-glfw/system_sdk.zig"); -const font = @import("src/font/main.zig"); -const WasmTarget = @import("src/os/wasm/target.zig").Target; -const LibtoolStep = @import("src/build/LibtoolStep.zig"); -const LipoStep = @import("src/build/LipoStep.zig"); -const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig"); -const Version = @import("src/build/Version.zig"); // Do a comptime Zig version requirement. The required Zig version is // somewhat arbitrary: it is meant to be a version that we feel works well, @@ -271,6 +274,80 @@ pub fn build(b: *std.Build) !void { } } + // Terminfo + { + // Encode our terminfo + var str = std.ArrayList(u8).init(b.allocator); + defer str.deinit(); + try terminfo.ghostty.encode(str.writer()); + + // Write it + var wf = b.addWriteFiles(); + const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items); + const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo"); + b.getInstallStep().dependOn(&src_install.step); + if (target.isDarwin()) { + const mac_src_install = b.addInstallFile( + src_source, + "Ghostty.app/Contents/Resources/terminfo/ghostty.terminfo", + ); + b.getInstallStep().dependOn(&mac_src_install.step); + } + + // Convert to termcap source format if thats helpful to people and + // install it. The resulting value here is the termcap source in case + // that is used for other commands. + { + const run_step = RunStep.create(b, "infotocap"); + run_step.addArg("infotocap"); + run_step.addFileSourceArg(src_source); + const out_source = run_step.captureStdOut(); + _ = run_step.captureStdErr(); // so we don't see stderr + + const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap"); + b.getInstallStep().dependOn(&cap_install.step); + + if (target.isDarwin()) { + const mac_cap_install = b.addInstallFile( + out_source, + "Ghostty.app/Contents/Resources/terminfo/ghostty.termcap", + ); + b.getInstallStep().dependOn(&mac_cap_install.step); + } + } + + // Compile the terminfo source into a terminfo database + { + const run_step = RunStep.create(b, "tic"); + run_step.addArgs(&.{ "tic", "-x", "-o" }); + const path = run_step.addOutputFileArg("terminfo"); + run_step.addFileSourceArg(src_source); + _ = run_step.captureStdErr(); // so we don't see stderr + + // Depend on the terminfo source install step so that Zig build + // creates the "share" directory for us. + run_step.step.dependOn(&src_install.step); + + { + const copy_step = RunStep.create(b, "copy terminfo db"); + copy_step.addArgs(&.{ "cp", "-R" }); + copy_step.addFileSourceArg(path); + copy_step.addArg(b.fmt("{s}/share", .{b.install_prefix})); + b.getInstallStep().dependOn(©_step.step); + } + + if (target.isDarwin()) { + const copy_step = RunStep.create(b, "copy terminfo db"); + copy_step.addArgs(&.{ "cp", "-R" }); + copy_step.addFileSourceArg(path); + copy_step.addArg( + b.fmt("{s}/Ghostty.app/Contents/Resources", .{b.install_prefix}), + ); + b.getInstallStep().dependOn(©_step.step); + } + } + } + // App (Linux) if (target.isLinux()) { // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html diff --git a/nix/devshell.nix b/nix/devshell.nix index e606c091d..1f627c5ac 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -4,6 +4,7 @@ , flatpak-builder , gdb , glxinfo +, ncurses , nodejs , parallel , pkg-config @@ -67,6 +68,7 @@ in mkShell rec { nativeBuildInputs = [ # For builds llvmPackages_latest.llvm + ncurses pkg-config scdoc zig diff --git a/src/main.zig b/src/main.zig index 9600a4a66..f9160a282 100644 --- a/src/main.zig +++ b/src/main.zig @@ -190,6 +190,7 @@ test { // Libraries _ = @import("segmented_pool.zig"); _ = @import("terminal/main.zig"); + _ = @import("terminfo/main.zig"); // TODO _ = @import("blocking_queue.zig"); diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 0c3d8ca73..07466e19e 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -382,15 +382,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { self.params_idx += 1; } - // We only allow the colon separator for the 'm' command. - switch (self.params_sep) { - .none => {}, - .semicolon => {}, - .colon => if (c != 'm') break :csi_dispatch null, - .mixed => break :csi_dispatch null, - } - - break :csi_dispatch Action{ + const result: Action = .{ .csi_dispatch = .{ .intermediates = self.intermediates[0..self.intermediates_idx], .params = self.params[0..self.params_idx], @@ -398,10 +390,35 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { .sep = switch (self.params_sep) { .none, .semicolon => .semicolon, .colon => .colon, - .mixed => unreachable, + + // This should never happen because of the checks below + // but we have to exhaustively handle the switch. + .mixed => .semicolon, }, }, }; + + // We only allow the colon separator for the 'm' command. + switch (self.params_sep) { + .none => {}, + .semicolon => {}, + .colon => if (c != 'm') { + log.warn( + "CSI colon separator only allowed for 'm' command, got: {}", + .{result}, + ); + break :csi_dispatch null; + }, + .mixed => { + log.warn( + "CSI command had mixed colons and semicolons, got: {}", + .{result}, + ); + break :csi_dispatch null; + }, + } + + break :csi_dispatch result; }, .esc_dispatch => Action{ .esc_dispatch = .{ @@ -418,6 +435,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { fn clear(self: *Parser) void { self.intermediates_idx = 0; self.params_idx = 0; + self.params_sep = .none; self.param_acc = 0; self.param_acc_idx = 0; } @@ -531,6 +549,64 @@ test "csi: SGR ESC [ 38 : 2 m" { } } +test "csi: SGR colon followed by semicolon" { + var p = init(); + _ = p.next(0x1B); + for ("[48:2") |c| { + const a = p.next(c); + try testing.expect(a[0] == null); + try testing.expect(a[1] == null); + try testing.expect(a[2] == null); + } + + { + const a = p.next('m'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[2] == null); + } + + _ = p.next(0x1B); + _ = p.next('['); + { + const a = p.next('H'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[2] == null); + } +} + +test "csi: SGR ESC [ 48 : 2 m" { + var p = init(); + _ = p.next(0x1B); + for ("[48:2:240:143:104") |c| { + const a = p.next(c); + try testing.expect(a[0] == null); + try testing.expect(a[1] == null); + try testing.expect(a[2] == null); + } + + { + const a = p.next('m'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[2] == null); + + const d = a[1].?.csi_dispatch; + try testing.expect(d.final == 'm'); + try testing.expect(d.sep == .colon); + try testing.expect(d.params.len == 5); + try testing.expectEqual(@as(u16, 48), d.params[0]); + try testing.expectEqual(@as(u16, 2), d.params[1]); + try testing.expectEqual(@as(u16, 240), d.params[2]); + try testing.expectEqual(@as(u16, 143), d.params[3]); + try testing.expectEqual(@as(u16, 104), d.params[4]); + } +} + test "csi: SGR ESC [4:3m colon" { var p = init(); _ = p.next(0x1B); diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index c7fcda035..865518b8e 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -467,6 +467,16 @@ test "sgr: 256 color" { try testing.expect(p.next().? == .@"256_bg"); } +test "sgr: 24-bit bg color" { + { + const v = testParseColon(&[_]u16{ 48, 2, 1, 2, 3 }); + try testing.expect(v == .direct_color_bg); + try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r); + try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g); + try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b); + } +} + test "sgr: underline color" { { const v = testParseColon(&[_]u16{ 58, 2, 1, 2, 3 }); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d681992e5..e78b3f457 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -48,7 +48,7 @@ pub fn Stream(comptime Handler: type) type { tracy.value(@intCast(u64, c)); defer tracy.end(); - //log.debug("char: {x}", .{c}); + // log.debug("char: {c}", .{c}); const actions = self.parser.next(c); for (actions) |action_opt| { // if (action_opt) |action| { diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig new file mode 100644 index 000000000..46ea8bd97 --- /dev/null +++ b/src/terminfo/Source.zig @@ -0,0 +1,98 @@ +//! Terminfo source format. This can be used to encode terminfo files. +//! This cannot parse terminfo source files yet because it isn't something +//! I need to do but this can be added later. +//! +//! Background: https://invisible-island.net/ncurses/man/terminfo.5.html + +const Source = @This(); + +const std = @import("std"); + +/// The set of names for the terminal. These match the TERM environment variable +/// and are used to look up this terminal. Historically, the final name in the +/// list was the most common name for the terminal and contains spaces and +/// other characters. See terminfo(5) for details. +names: []const []const u8, + +/// The set of capabilities in this terminfo file. +capabilities: []const Capability, + +/// A capability in a terminfo file. This also includes any "use" capabilities +/// since they behave just like other capabilities as documented in terminfo(5). +pub const Capability = struct { + /// The name of capability. This is the "Cap-name" value in terminfo(5). + name: []const u8, + value: Value, + + pub const Value = union(enum) { + /// Canceled value, i.e. suffixed with @ + canceled: void, + + /// Boolean values are always true if they exist so there is no value. + boolean: void, + + /// Numeric values are always "unsigned decimal integers". The size + /// of the integer is unspecified in terminfo(5). I chose 32-bits + /// because it is a common integer size but this may be wrong. + numeric: u32, + + string: []const u8, + }; +}; + +/// Encode as a terminfo source file. The encoding is always done in a +/// human-readable format with whitespace. Fields are always written in the +/// order of the slices on this struct; this will not do any reordering. +pub fn encode(self: Source, writer: anytype) !void { + // Encode the names in the order specified + for (self.names, 0..) |name, i| { + if (i != 0) try writer.writeAll("|"); + try writer.writeAll(name); + } + try writer.writeAll(",\n"); + + // Encode each of the capabilities in the order specified + for (self.capabilities) |cap| { + try writer.writeAll("\t"); + try writer.writeAll(cap.name); + switch (cap.value) { + .canceled => try writer.writeAll("@"), + .boolean => {}, + .numeric => |v| try writer.print("#{d}", .{v}), + .string => |v| try writer.print("={s}", .{v}), + } + try writer.writeAll(",\n"); + } +} + +test "encode" { + const src: Source = .{ + .names = &.{ + "ghostty", + "xterm-ghostty", + "Ghostty", + }, + + .capabilities = &.{ + .{ .name = "am", .value = .{ .boolean = {} } }, + .{ .name = "ccc", .value = .{ .canceled = {} } }, + .{ .name = "colors", .value = .{ .numeric = 256 } }, + .{ .name = "bel", .value = .{ .string = "^G" } }, + }, + }; + + // Encode + var buf: [1024]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + try src.encode(buf_stream.writer()); + + const expected = + \\ghostty|xterm-ghostty|Ghostty, + \\ am, + \\ ccc@, + \\ colors#256, + \\ bel=^G, + \\ + ; + try std.testing.expectEqualStrings(@as([]const u8, expected), buf_stream.getWritten()); +} diff --git a/src/terminfo/ghostty.zig b/src/terminfo/ghostty.zig new file mode 100644 index 000000000..2e37bfc64 --- /dev/null +++ b/src/terminfo/ghostty.zig @@ -0,0 +1,334 @@ +const std = @import("std"); +const Source = @import("Source.zig"); + +/// Ghostty's terminfo entry. +pub const ghostty: Source = .{ + .names = &.{ + // The preferred name + "ghostty", + + // We support the "xterm-" prefix because some poorly behaved programs + // use this to detect if the terminal supports 256 colors and other + // features. + "xterm-ghostty", + + // Our "formal" name + "Ghostty", + }, + + // NOTE: These capabilities are super underdocumented and I'm not 100% + // I've got the list or my understanding of any in this list fully correct. + // As we learn more, please update the comments to better explain what + // anything means. + // + // I've marked some capabilities as "???" if I don't understand what they + // mean but I just assume I support since other modern terminals do. In + // this case, I'd love if anyone could help explain what this means and + // verify that Ghostty does indeed support it and if not we can fix it. + .capabilities = &.{ + // automatic right margin -- when reaching the end of a line, text is + // wrapped to the next line. + .{ .name = "am", .value = .{ .boolean = {} } }, + + // background color erase -- screen is erased with the background color + .{ .name = "bce", .value = .{ .boolean = {} } }, + + // terminal can change color definitions, i.e. we can change the color + // palette. TODO: this may require implementing CSI 4 which we don't + // at the time of writing this comment. + .{ .name = "ccc", .value = .{ .boolean = {} } }, + + // supports changing the window title. + .{ .name = "hs", .value = .{ .boolean = {} } }, + + // terminal has a meta key + .{ .name = "km", .value = .{ .boolean = {} } }, + + // terminal will not echo input on the screen on its own + .{ .name = "mc5i", .value = .{ .boolean = {} } }, + + // safe to move (move what?) while in insert/standout mode. (???) + .{ .name = "mir", .value = .{ .boolean = {} } }, + .{ .name = "msgr", .value = .{ .boolean = {} } }, + + // no pad character (???) + .{ .name = "npc", .value = .{ .boolean = {} } }, + + // newline ignored after 80 cols (???) + .{ .name = "xenl", .value = .{ .boolean = {} } }, + + // Tmux "truecolor" mode. Other programs also use this to detect + // if the terminal supports "truecolor". This means that the terminal + // can display 24-bit RGB colors. + .{ .name = "Tc", .value = .{ .boolean = {} } }, + + // Colored underlines. https://sw.kovidgoyal.net/kitty/underlines/ + .{ .name = "Su", .value = .{ .boolean = {} } }, + + // Full keyboard support using Kitty's keyboard protocol: + // https://sw.kovidgoyal.net/kitty/keyboard-protocol/ + // Commented out because we don't yet support this. + // .{ .name = "fullkbd", .value = .{ .boolean = {} } }, + + // Number of colors in the color palette. + .{ .name = "colors", .value = .{ .numeric = 256 } }, + + // Number of columns in a line. Our terminal is variable width on + // Window resize but this appears to just be the value set by most + // terminals. + .{ .name = "cols", .value = .{ .numeric = 80 } }, + + // Initial tabstop interval. + .{ .name = "it", .value = .{ .numeric = 8 } }, + + // Number of lines on a page. Similar to cols this is variable width + // but this appears to be the value set by most terminals. + .{ .name = "lines", .value = .{ .numeric = 24 } }, + + // Number of color pairs on the screen. + .{ .name = "pairs", .value = .{ .numeric = 32767 } }, + + // Alternate character set. This is the VT100 alternate character set. + // I don't know what the value means, I copied this from Kitty and + // verified with some other terminals (looks similar). + .{ .name = "acsc", .value = .{ .string = "++\\,\\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~" } }, + + // These are all capabilities that should be pretty straightforward + // and map to input sequences. + .{ .name = "bel", .value = .{ .string = "^G" } }, + .{ .name = "blink", .value = .{ .string = "\\E[5m" } }, + .{ .name = "bold", .value = .{ .string = "\\E[1m" } }, + .{ .name = "cbt", .value = .{ .string = "\\E[Z" } }, + .{ .name = "civis", .value = .{ .string = "\\E[?25l" } }, + .{ .name = "clear", .value = .{ .string = "\\E[H\\E[2J" } }, + .{ .name = "cnorm", .value = .{ .string = "\\E[?12l\\E[?25h" } }, + .{ .name = "cr", .value = .{ .string = "\\r" } }, + .{ .name = "csr", .value = .{ .string = "\\E[%i%p1%d;%p2%dr" } }, + .{ .name = "cub", .value = .{ .string = "\\E[%p1%dD" } }, + .{ .name = "cub1", .value = .{ .string = "^H" } }, + .{ .name = "cud", .value = .{ .string = "\\E[%p1%dB" } }, + .{ .name = "cud1", .value = .{ .string = "^J" } }, + .{ .name = "cuf", .value = .{ .string = "\\E[%p1%dC" } }, + .{ .name = "cuf1", .value = .{ .string = "\\E[C" } }, + .{ .name = "cup", .value = .{ .string = "\\E[%i%p1%d;%p2%dH" } }, + .{ .name = "cuu", .value = .{ .string = "\\E[%p1%dA" } }, + .{ .name = "cuu1", .value = .{ .string = "\\E[A" } }, + .{ .name = "cvvis", .value = .{ .string = "\\E[?12;25h" } }, + .{ .name = "dch", .value = .{ .string = "\\E[%p1%dP" } }, + .{ .name = "dch1", .value = .{ .string = "\\E[P" } }, + .{ .name = "dim", .value = .{ .string = "\\E[2m" } }, + .{ .name = "dl", .value = .{ .string = "\\E[%p1%dM" } }, + .{ .name = "dl1", .value = .{ .string = "\\E[M" } }, + .{ .name = "dsl", .value = .{ .string = "\\E]2;\\007" } }, + .{ .name = "ech", .value = .{ .string = "\\E[%p1%dX" } }, + .{ .name = "ed", .value = .{ .string = "\\E[J" } }, + .{ .name = "el", .value = .{ .string = "\\E[K" } }, + .{ .name = "el1", .value = .{ .string = "\\E[1K" } }, + .{ .name = "flash", .value = .{ .string = "\\E[?5h$<100/>\\E[?5l" } }, + .{ .name = "fsl", .value = .{ .string = "^G" } }, + .{ .name = "home", .value = .{ .string = "\\E[H" } }, + .{ .name = "hpa", .value = .{ .string = "\\E[%i%p1%dG" } }, + .{ .name = "ht", .value = .{ .string = "^I" } }, + .{ .name = "hts", .value = .{ .string = "\\EH" } }, + .{ .name = "ich", .value = .{ .string = "\\E[%p1%d@" } }, + .{ .name = "il", .value = .{ .string = "\\E[%p1%dL" } }, + .{ .name = "il1", .value = .{ .string = "\\E[L" } }, + .{ .name = "ind", .value = .{ .string = "\\n" } }, + .{ .name = "indn", .value = .{ .string = "\\E[%p1%dS" } }, + .{ .name = "initc", .value = .{ .string = "\\E]4;%p1%d;rgb\\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\" } }, + .{ .name = "invis", .value = .{ .string = "\\E[8m" } }, + .{ .name = "oc", .value = .{ .string = "\\E]104\\007" } }, + .{ .name = "op", .value = .{ .string = "\\E[39;49m" } }, + .{ .name = "rc", .value = .{ .string = "\\E8" } }, + .{ .name = "rep", .value = .{ .string = "%p1%c\\E[%p2%{1}%-%db" } }, + .{ .name = "rev", .value = .{ .string = "\\E[7m" } }, + .{ .name = "ri", .value = .{ .string = "\\EM" } }, + .{ .name = "rin", .value = .{ .string = "\\E[%p1%dT" } }, + .{ .name = "ritm", .value = .{ .string = "\\E[23m" } }, + .{ .name = "rmacs", .value = .{ .string = "\\E(B" } }, + .{ .name = "rmam", .value = .{ .string = "\\E[?7l" } }, + .{ .name = "rmcup", .value = .{ .string = "\\E[?1049l" } }, + .{ .name = "rmir", .value = .{ .string = "\\E[4l" } }, + .{ .name = "rmkx", .value = .{ .string = "\\E[?1l" } }, + .{ .name = "rmso", .value = .{ .string = "\\E[27m" } }, + .{ .name = "rmul", .value = .{ .string = "\\E[24m" } }, + .{ .name = "rmxx", .value = .{ .string = "\\E[29m" } }, + .{ .name = "setab", .value = .{ .string = "\\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m" } }, + .{ .name = "setaf", .value = .{ .string = "\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m" } }, + .{ .name = "setrgbb", .value = .{ .string = "\\E[48:2:%p1%d:%p2%d:%p3%dm" } }, + .{ .name = "setrgbf", .value = .{ .string = "\\E[38:2:%p1%d:%p2%d:%p3%dm" } }, + .{ .name = "sgr", .value = .{ .string = "%?%p9%t\\E(0%e\\E(B%;\\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m" } }, + .{ .name = "sgr0", .value = .{ .string = "\\E(B\\E[m" } }, + .{ .name = "sitm", .value = .{ .string = "\\E[3m" } }, + .{ .name = "smacs", .value = .{ .string = "\\E(0" } }, + .{ .name = "smam", .value = .{ .string = "\\E[?7h" } }, + .{ .name = "smcup", .value = .{ .string = "\\E[?1049h" } }, + .{ .name = "smir", .value = .{ .string = "\\E[4h" } }, + .{ .name = "smkx", .value = .{ .string = "\\E[?1h" } }, + .{ .name = "smso", .value = .{ .string = "\\E[7m" } }, + .{ .name = "smul", .value = .{ .string = "\\E[4m" } }, + .{ .name = "smxx", .value = .{ .string = "\\E[9m" } }, + .{ .name = "tbc", .value = .{ .string = "\\E[3g" } }, + .{ .name = "tsl", .value = .{ .string = "\\E]2;" } }, + .{ .name = "u6", .value = .{ .string = "\\E[%i%d;%dR" } }, + .{ .name = "u7", .value = .{ .string = "\\E[6n" } }, + .{ .name = "u8", .value = .{ .string = "\\E[?%[;0123456789]c" } }, + .{ .name = "u9", .value = .{ .string = "\\E[c" } }, + .{ .name = "vpa", .value = .{ .string = "\\E[%i%p1%dd" } }, + + //----------------------------------------------------------- + // Completely unvalidated entries that are blindly copied from + // other terminals (Kitty, Wezterm, Alacritty) and may or may not + // actually work with Ghostty. todo is to validate these! + + .{ .name = "kDC", .value = .{ .string = "\\E[3;2~" } }, + .{ .name = "kDC3", .value = .{ .string = "\\E[3;3~" } }, + .{ .name = "kDC4", .value = .{ .string = "\\E[3;4~" } }, + .{ .name = "kDC5", .value = .{ .string = "\\E[3;5~" } }, + .{ .name = "kDC6", .value = .{ .string = "\\E[3;6~" } }, + .{ .name = "kDC7", .value = .{ .string = "\\E[3;7~" } }, + .{ .name = "kDN", .value = .{ .string = "\\E[1;2B" } }, + .{ .name = "kDN3", .value = .{ .string = "\\E[1;3B" } }, + .{ .name = "kDN4", .value = .{ .string = "\\E[1;4B" } }, + .{ .name = "kDN5", .value = .{ .string = "\\E[1;5B" } }, + .{ .name = "kDN6", .value = .{ .string = "\\E[1;6B" } }, + .{ .name = "kDN7", .value = .{ .string = "\\E[1;7B" } }, + .{ .name = "kEND", .value = .{ .string = "\\E[1;2F" } }, + .{ .name = "kEND3", .value = .{ .string = "\\E[1;3F" } }, + .{ .name = "kEND4", .value = .{ .string = "\\E[1;4F" } }, + .{ .name = "kEND5", .value = .{ .string = "\\E[1;5F" } }, + .{ .name = "kEND6", .value = .{ .string = "\\E[1;6F" } }, + .{ .name = "kEND7", .value = .{ .string = "\\E[1;7F" } }, + .{ .name = "kHOM", .value = .{ .string = "\\E[1;2H" } }, + .{ .name = "kHOM3", .value = .{ .string = "\\E[1;3H" } }, + .{ .name = "kHOM4", .value = .{ .string = "\\E[1;4H" } }, + .{ .name = "kHOM5", .value = .{ .string = "\\E[1;5H" } }, + .{ .name = "kHOM6", .value = .{ .string = "\\E[1;6H" } }, + .{ .name = "kHOM7", .value = .{ .string = "\\E[1;7H" } }, + .{ .name = "kIC", .value = .{ .string = "\\E[2;2~" } }, + .{ .name = "kIC3", .value = .{ .string = "\\E[2;3~" } }, + .{ .name = "kIC4", .value = .{ .string = "\\E[2;4~" } }, + .{ .name = "kIC5", .value = .{ .string = "\\E[2;5~" } }, + .{ .name = "kIC6", .value = .{ .string = "\\E[2;6~" } }, + .{ .name = "kIC7", .value = .{ .string = "\\E[2;7~" } }, + .{ .name = "kLFT", .value = .{ .string = "\\E[1;2D" } }, + .{ .name = "kLFT3", .value = .{ .string = "\\E[1;3D" } }, + .{ .name = "kLFT4", .value = .{ .string = "\\E[1;4D" } }, + .{ .name = "kLFT5", .value = .{ .string = "\\E[1;5D" } }, + .{ .name = "kLFT6", .value = .{ .string = "\\E[1;6D" } }, + .{ .name = "kLFT7", .value = .{ .string = "\\E[1;7D" } }, + .{ .name = "kNXT", .value = .{ .string = "\\E[6;2~" } }, + .{ .name = "kNXT3", .value = .{ .string = "\\E[6;3~" } }, + .{ .name = "kNXT4", .value = .{ .string = "\\E[6;4~" } }, + .{ .name = "kNXT5", .value = .{ .string = "\\E[6;5~" } }, + .{ .name = "kNXT6", .value = .{ .string = "\\E[6;6~" } }, + .{ .name = "kNXT7", .value = .{ .string = "\\E[6;7~" } }, + .{ .name = "kPRV", .value = .{ .string = "\\E[5;2~" } }, + .{ .name = "kPRV3", .value = .{ .string = "\\E[5;3~" } }, + .{ .name = "kPRV4", .value = .{ .string = "\\E[5;4~" } }, + .{ .name = "kPRV5", .value = .{ .string = "\\E[5;5~" } }, + .{ .name = "kPRV6", .value = .{ .string = "\\E[5;6~" } }, + .{ .name = "kPRV7", .value = .{ .string = "\\E[5;7~" } }, + .{ .name = "kRIT", .value = .{ .string = "\\E[1;2C" } }, + .{ .name = "kRIT3", .value = .{ .string = "\\E[1;3C" } }, + .{ .name = "kRIT4", .value = .{ .string = "\\E[1;4C" } }, + .{ .name = "kRIT5", .value = .{ .string = "\\E[1;5C" } }, + .{ .name = "kRIT6", .value = .{ .string = "\\E[1;6C" } }, + .{ .name = "kRIT7", .value = .{ .string = "\\E[1;7C" } }, + .{ .name = "kUP", .value = .{ .string = "\\E[1;2A" } }, + .{ .name = "kUP3", .value = .{ .string = "\\E[1;3A" } }, + .{ .name = "kUP4", .value = .{ .string = "\\E[1;4A" } }, + .{ .name = "kUP5", .value = .{ .string = "\\E[1;5A" } }, + .{ .name = "kUP6", .value = .{ .string = "\\E[1;6A" } }, + .{ .name = "kUP7", .value = .{ .string = "\\E[1;7A" } }, + .{ .name = "kbs", .value = .{ .string = "^?" } }, + .{ .name = "kcbt", .value = .{ .string = "\\E[Z" } }, + .{ .name = "kcub1", .value = .{ .string = "\\EOD" } }, + .{ .name = "kcud1", .value = .{ .string = "\\EOB" } }, + .{ .name = "kcuf1", .value = .{ .string = "\\EOC" } }, + .{ .name = "kcuu1", .value = .{ .string = "\\EOA" } }, + .{ .name = "kdch1", .value = .{ .string = "\\E[3~" } }, + .{ .name = "kend", .value = .{ .string = "\\EOF" } }, + .{ .name = "kent", .value = .{ .string = "\\EOM" } }, + .{ .name = "kf1", .value = .{ .string = "\\EOP" } }, + .{ .name = "kf10", .value = .{ .string = "\\E[21~" } }, + .{ .name = "kf11", .value = .{ .string = "\\E[23~" } }, + .{ .name = "kf12", .value = .{ .string = "\\E[24~" } }, + .{ .name = "kf13", .value = .{ .string = "\\E[1;2P" } }, + .{ .name = "kf14", .value = .{ .string = "\\E[1;2Q" } }, + .{ .name = "kf15", .value = .{ .string = "\\E[1;2R" } }, + .{ .name = "kf16", .value = .{ .string = "\\E[1;2S" } }, + .{ .name = "kf17", .value = .{ .string = "\\E[15;2~" } }, + .{ .name = "kf18", .value = .{ .string = "\\E[17;2~" } }, + .{ .name = "kf19", .value = .{ .string = "\\E[18;2~" } }, + .{ .name = "kf2", .value = .{ .string = "\\EOQ" } }, + .{ .name = "kf20", .value = .{ .string = "\\E[19;2~" } }, + .{ .name = "kf21", .value = .{ .string = "\\E[20;2~" } }, + .{ .name = "kf22", .value = .{ .string = "\\E[21;2~" } }, + .{ .name = "kf23", .value = .{ .string = "\\E[23;2~" } }, + .{ .name = "kf24", .value = .{ .string = "\\E[24;2~" } }, + .{ .name = "kf25", .value = .{ .string = "\\E[1;5P" } }, + .{ .name = "kf26", .value = .{ .string = "\\E[1;5Q" } }, + .{ .name = "kf27", .value = .{ .string = "\\E[1;5R" } }, + .{ .name = "kf28", .value = .{ .string = "\\E[1;5S" } }, + .{ .name = "kf29", .value = .{ .string = "\\E[15;5~" } }, + .{ .name = "kf3", .value = .{ .string = "\\EOR" } }, + .{ .name = "kf30", .value = .{ .string = "\\E[17;5~" } }, + .{ .name = "kf31", .value = .{ .string = "\\E[18;5~" } }, + .{ .name = "kf32", .value = .{ .string = "\\E[19;5~" } }, + .{ .name = "kf33", .value = .{ .string = "\\E[20;5~" } }, + .{ .name = "kf34", .value = .{ .string = "\\E[21;5~" } }, + .{ .name = "kf35", .value = .{ .string = "\\E[23;5~" } }, + .{ .name = "kf36", .value = .{ .string = "\\E[24;5~" } }, + .{ .name = "kf37", .value = .{ .string = "\\E[1;6P" } }, + .{ .name = "kf38", .value = .{ .string = "\\E[1;6Q" } }, + .{ .name = "kf39", .value = .{ .string = "\\E[1;6R" } }, + .{ .name = "kf4", .value = .{ .string = "\\EOS" } }, + .{ .name = "kf40", .value = .{ .string = "\\E[1;6S" } }, + .{ .name = "kf41", .value = .{ .string = "\\E[15;6~" } }, + .{ .name = "kf42", .value = .{ .string = "\\E[17;6~" } }, + .{ .name = "kf43", .value = .{ .string = "\\E[18;6~" } }, + .{ .name = "kf44", .value = .{ .string = "\\E[19;6~" } }, + .{ .name = "kf45", .value = .{ .string = "\\E[20;6~" } }, + .{ .name = "kf46", .value = .{ .string = "\\E[21;6~" } }, + .{ .name = "kf47", .value = .{ .string = "\\E[23;6~" } }, + .{ .name = "kf48", .value = .{ .string = "\\E[24;6~" } }, + .{ .name = "kf49", .value = .{ .string = "\\E[1;3P" } }, + .{ .name = "kf5", .value = .{ .string = "\\E[15~" } }, + .{ .name = "kf50", .value = .{ .string = "\\E[1;3Q" } }, + .{ .name = "kf51", .value = .{ .string = "\\E[1;3R" } }, + .{ .name = "kf52", .value = .{ .string = "\\E[1;3S" } }, + .{ .name = "kf53", .value = .{ .string = "\\E[15;3~" } }, + .{ .name = "kf54", .value = .{ .string = "\\E[17;3~" } }, + .{ .name = "kf55", .value = .{ .string = "\\E[18;3~" } }, + .{ .name = "kf56", .value = .{ .string = "\\E[19;3~" } }, + .{ .name = "kf57", .value = .{ .string = "\\E[20;3~" } }, + .{ .name = "kf58", .value = .{ .string = "\\E[21;3~" } }, + .{ .name = "kf59", .value = .{ .string = "\\E[23;3~" } }, + .{ .name = "kf6", .value = .{ .string = "\\E[17~" } }, + .{ .name = "kf60", .value = .{ .string = "\\E[24;3~" } }, + .{ .name = "kf61", .value = .{ .string = "\\E[1;4P" } }, + .{ .name = "kf62", .value = .{ .string = "\\E[1;4Q" } }, + .{ .name = "kf63", .value = .{ .string = "\\E[1;4R" } }, + .{ .name = "kf7", .value = .{ .string = "\\E[18~" } }, + .{ .name = "kf8", .value = .{ .string = "\\E[19~" } }, + .{ .name = "kf9", .value = .{ .string = "\\E[20~" } }, + .{ .name = "khome", .value = .{ .string = "\\EOH" } }, + .{ .name = "kich1", .value = .{ .string = "\\E[2~" } }, + .{ .name = "kind", .value = .{ .string = "\\E[1;2B" } }, + .{ .name = "kmous", .value = .{ .string = "\\E[M" } }, + .{ .name = "knp", .value = .{ .string = "\\E[6~" } }, + .{ .name = "kpp", .value = .{ .string = "\\E[5~" } }, + .{ .name = "kri", .value = .{ .string = "\\E[1;2A" } }, + .{ .name = "rs1", .value = .{ .string = "\\E]\\E\\\\\\Ec" } }, + .{ .name = "sc", .value = .{ .string = "\\E7" } }, + }, +}; + +test "encode" { + // Encode + var buf: [4096]u8 = undefined; + var buf_stream = std.io.fixedBufferStream(&buf); + try ghostty.encode(buf_stream.writer()); + try std.testing.expect(buf_stream.getWritten().len > 0); +} diff --git a/src/terminfo/main.zig b/src/terminfo/main.zig new file mode 100644 index 000000000..c76c328cc --- /dev/null +++ b/src/terminfo/main.zig @@ -0,0 +1,13 @@ +//! Package terminfo provides functionality related to terminfo/termcap files. +//! +//! At the time of writing this comment, the focus is on generating terminfo +//! files so that we can maintain our terminfo in Zig instead of hand-writing +//! the archaic (imo) terminfo format by hand. But eventually we may want to +//! extract this into a more full-featured library on its own. + +pub const ghostty = @import("ghostty.zig").ghostty; +pub const Source = @import("Source.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 80ea35348..1345d8f0d 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -491,8 +491,27 @@ const Subprocess = struct { break :env try std.process.getEnvMap(alloc); }; errdefer env.deinit(); - try env.put("TERM", "xterm-256color"); - try env.put("COLORTERM", "truecolor"); + + // Set our TERM var. This is a bit complicated because we want to use + // the ghostty TERM value but we want to only do that if we have + // ghostty in the TERMINFO database. + // + // For now, we just look up a bundled dir but in the future we should + // also load the terminfo database and look for it. + if (try terminfoDir(alloc)) |dir| { + try env.put("TERM", "xterm-ghostty"); + try env.put("COLORTERM", "truecolor"); + try env.put("TERMINFO", dir); + } else { + if (comptime builtin.target.isDarwin()) { + log.warn("ghostty terminfo not found, using xterm-256color", .{}); + log.warn("the terminfo SHOULD exist on macos, please ensure", .{}); + log.warn("you're using a valid app bundle.", .{}); + } + + try env.put("TERM", "xterm-256color"); + try env.put("COLORTERM", "truecolor"); + } // When embedding in macOS and running via XCode, XCode injects // a bunch of things that break our shell process. We remove those. @@ -745,6 +764,41 @@ const Subprocess = struct { fn killCommandFlatpak(command: *FlatpakHostCommand) !void { try command.signal(c.SIGHUP, true); } + + /// Gets the directory to the terminfo database, if it can be detected. + /// The memory returned can't be easily freed so the alloc should be + /// an arena or something similar. + fn terminfoDir(alloc: Allocator) !?[]const u8 { + // We only support Mac lookups right now because the terminfo + // DB can be embedded directly in the App bundle. + if (comptime !builtin.target.isDarwin()) return null; + + // Get the path to our running binary + var exe_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null; + + // We have an exe path! Climb the tree looking for the terminfo + // bundle as we expect it. + while (std.fs.path.dirname(exe)) |dir| { + exe = dir; + + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try std.fmt.bufPrint( + &buf, + "{s}/Contents/Resources/terminfo", + .{dir}, + ); + + if (std.fs.accessAbsolute(path, .{})) { + return try alloc.dupe(u8, path); + } else |_| { + // Folder doesn't exist. If a different error happens its okay + // we just ignore it and move on. + } + } + + return null; + } }; /// The read thread sits in a loop doing the following pseudo code: