mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #942 from mitchellh/themes
Built-in Themes (based on iTerm2-Color-Schemes)
This commit is contained in:
33
README.md
33
README.md
@ -129,6 +129,39 @@ if something isn't working.
|
|||||||
|
|
||||||
Eventually, we'll have a better mechanism for showing errors to the user.
|
Eventually, we'll have a better mechanism for showing errors to the user.
|
||||||
|
|
||||||
|
### Themes
|
||||||
|
|
||||||
|
Ghostty ships with 300+ built-in themes (from
|
||||||
|
[iTerm2 Color Schemes](https://github.com/mbadolato/iTerm2-Color-Schemes)).
|
||||||
|
You can configure Ghostty to use any of these themes using the `theme`
|
||||||
|
configuration. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
theme = Solarized Dark - Patched
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find a list of built-in themes using the `+list-themes` action:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ghostty +list-themes
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, the themes are built-in to the `Ghostty.app` bundle. On Linux,
|
||||||
|
theme support requires a valid Ghostty resources dir ("share" directory).
|
||||||
|
More details about how to validate the resources directory on Linux
|
||||||
|
is covered in the [shell integration section](#shell-integration-installation-and-verification).
|
||||||
|
|
||||||
|
Any custom color configuration (`palette`, `background`, `foreground`, etc.)
|
||||||
|
in your configuration files will override the theme settings. This can be
|
||||||
|
used to load a theme and fine-tune specific colors to your liking.
|
||||||
|
|
||||||
|
**Interested in contributing a new theme or updating an existing theme?**
|
||||||
|
Please send theme changes upstream to the
|
||||||
|
[iTerm2 Color Schemes](https://github.com/mbadolato/iTerm2-Color-Schemes))
|
||||||
|
repository. Ghostty periodically updates the themes from this source.
|
||||||
|
_Do not send theme changes to the Ghostty project directly_.
|
||||||
|
|
||||||
### Shell Integration
|
### Shell Integration
|
||||||
|
|
||||||
Ghostty supports some features that require shell integration. I am aiming
|
Ghostty supports some features that require shell integration. I am aiming
|
||||||
|
23
build.zig
23
build.zig
@ -305,6 +305,29 @@ pub fn build(b: *std.Build) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Themes
|
||||||
|
{
|
||||||
|
const upstream = b.dependency("iterm2_themes", .{});
|
||||||
|
const install = b.addInstallDirectory(.{
|
||||||
|
.source_dir = upstream.path("ghostty"),
|
||||||
|
.install_dir = .{ .custom = "share" },
|
||||||
|
.install_subdir = "themes",
|
||||||
|
.exclude_extensions = &.{".md"},
|
||||||
|
});
|
||||||
|
b.getInstallStep().dependOn(&install.step);
|
||||||
|
|
||||||
|
if (target.isDarwin() and exe_ != null) {
|
||||||
|
const mac_install = b.addInstallDirectory(options: {
|
||||||
|
var copy = install.options;
|
||||||
|
copy.install_dir = .{
|
||||||
|
.custom = "Ghostty.app/Contents/Resources",
|
||||||
|
};
|
||||||
|
break :options copy;
|
||||||
|
});
|
||||||
|
b.getInstallStep().dependOn(&mac_install.step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Terminfo
|
// Terminfo
|
||||||
{
|
{
|
||||||
// Encode our terminfo
|
// Encode our terminfo
|
||||||
|
@ -41,7 +41,11 @@
|
|||||||
.glslang = .{ .path = "./pkg/glslang" },
|
.glslang = .{ .path = "./pkg/glslang" },
|
||||||
.spirv_cross = .{ .path = "./pkg/spirv-cross" },
|
.spirv_cross = .{ .path = "./pkg/spirv-cross" },
|
||||||
|
|
||||||
// System headers
|
// Other
|
||||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||||
|
.iterm2_themes = .{
|
||||||
|
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/53acae071801e0de6ed160315869abb9bdaf20fa.tar.gz",
|
||||||
|
.hash = "12201575c5a2b21c2e110593773040cddcd357544038092d18bd98fc5a2141354bbd",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||||
|
A5CB04382B0F1C1C009ED217 /* themes in Resources */ = {isa = PBXBuildFile; fileRef = A5CB04372B0F1C1C009ED217 /* themes */; };
|
||||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
||||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; };
|
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; };
|
||||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; };
|
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; };
|
||||||
@ -76,6 +77,7 @@
|
|||||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||||
|
A5CB04372B0F1C1C009ED217 /* themes */ = {isa = PBXFileReference; lastKnownFileType = folder; name = themes; path = "../zig-out/share/themes"; sourceTree = "<group>"; };
|
||||||
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
|
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
|
||||||
A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = "<group>"; };
|
A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = "<group>"; };
|
||||||
A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = "<group>"; };
|
A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = "<group>"; };
|
||||||
@ -198,6 +200,7 @@
|
|||||||
A5B30528299BEAAA0047F10C = {
|
A5B30528299BEAAA0047F10C = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A5CB04372B0F1C1C009ED217 /* themes */,
|
||||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
|
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
|
||||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
|
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
|
||||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
|
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
|
||||||
@ -302,6 +305,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A5CB04382B0F1C1C009ED217 /* themes in Resources */,
|
||||||
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */,
|
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */,
|
||||||
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||||
|
11
src/App.zig
11
src/App.zig
@ -41,10 +41,6 @@ mailbox: Mailbox.Queue,
|
|||||||
/// Set to true once we're quitting. This never goes false again.
|
/// Set to true once we're quitting. This never goes false again.
|
||||||
quit: bool,
|
quit: bool,
|
||||||
|
|
||||||
/// The app resources directory, equivalent to zig-out/share when we build
|
|
||||||
/// from source. This is null if we can't detect it.
|
|
||||||
resources_dir: ?[]const u8 = null,
|
|
||||||
|
|
||||||
/// Font discovery mechanism. This is only safe to use from the main thread.
|
/// Font discovery mechanism. This is only safe to use from the main thread.
|
||||||
/// This is lazily initialized on the first call to fontDiscover so do
|
/// This is lazily initialized on the first call to fontDiscover so do
|
||||||
/// not access this directly.
|
/// not access this directly.
|
||||||
@ -59,17 +55,11 @@ pub fn create(
|
|||||||
var app = try alloc.create(App);
|
var app = try alloc.create(App);
|
||||||
errdefer alloc.destroy(app);
|
errdefer alloc.destroy(app);
|
||||||
|
|
||||||
// Find our resources directory once for the app so every launch
|
|
||||||
// hereafter can use this cached value.
|
|
||||||
const resources_dir = try internal_os.resourcesDir(alloc);
|
|
||||||
errdefer if (resources_dir) |dir| alloc.free(dir);
|
|
||||||
|
|
||||||
app.* = .{
|
app.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.surfaces = .{},
|
.surfaces = .{},
|
||||||
.mailbox = .{},
|
.mailbox = .{},
|
||||||
.quit = false,
|
.quit = false,
|
||||||
.resources_dir = resources_dir,
|
|
||||||
};
|
};
|
||||||
errdefer app.surfaces.deinit(alloc);
|
errdefer app.surfaces.deinit(alloc);
|
||||||
|
|
||||||
@ -81,7 +71,6 @@ pub fn destroy(self: *App) void {
|
|||||||
for (self.surfaces.items) |surface| surface.deinit();
|
for (self.surfaces.items) |surface| surface.deinit();
|
||||||
self.surfaces.deinit(self.alloc);
|
self.surfaces.deinit(self.alloc);
|
||||||
|
|
||||||
if (self.resources_dir) |dir| self.alloc.free(dir);
|
|
||||||
if (comptime font.Discover != void) {
|
if (comptime font.Discover != void) {
|
||||||
if (self.font_discover) |*v| v.deinit();
|
if (self.font_discover) |*v| v.deinit();
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ const assert = std.debug.assert;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
const ziglyph = @import("ziglyph");
|
const ziglyph = @import("ziglyph");
|
||||||
|
const main = @import("main.zig");
|
||||||
const renderer = @import("renderer.zig");
|
const renderer = @import("renderer.zig");
|
||||||
const termio = @import("termio.zig");
|
const termio = @import("termio.zig");
|
||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
@ -451,7 +452,7 @@ pub fn init(
|
|||||||
.padding = padding,
|
.padding = padding,
|
||||||
.full_config = config,
|
.full_config = config,
|
||||||
.config = try termio.Impl.DerivedConfig.init(alloc, config),
|
.config = try termio.Impl.DerivedConfig.init(alloc, config),
|
||||||
.resources_dir = app.resources_dir,
|
.resources_dir = main.state.resources_dir,
|
||||||
.renderer_state = &self.renderer_state,
|
.renderer_state = &self.renderer_state,
|
||||||
.renderer_wakeup = render_thread.wakeup,
|
.renderer_wakeup = render_thread.wakeup,
|
||||||
.renderer_mailbox = render_thread.mailbox,
|
.renderer_mailbox = render_thread.mailbox,
|
||||||
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||||||
|
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const c = @import("c.zig");
|
const c = @import("c.zig");
|
||||||
|
const global_state = &@import("../../main.zig").state;
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_icon);
|
const log = std.log.scoped(.gtk_icon);
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ pub fn appIcon(app: *App, widget: *c.GtkWidget) !Icon {
|
|||||||
// to the search path and see if we can find it there.
|
// to the search path and see if we can find it there.
|
||||||
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(widget));
|
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(widget));
|
||||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
||||||
const base = app.core_app.resources_dir orelse {
|
const base = global_state.resources_dir orelse {
|
||||||
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
||||||
log.info("gtk app will not have Ghostty icon", .{});
|
log.info("gtk app will not have Ghostty icon", .{});
|
||||||
break :icon;
|
break :icon;
|
||||||
|
@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const list_fonts = @import("list_fonts.zig");
|
const list_fonts = @import("list_fonts.zig");
|
||||||
const version = @import("version.zig");
|
const version = @import("version.zig");
|
||||||
const list_keybinds = @import("list_keybinds.zig");
|
const list_keybinds = @import("list_keybinds.zig");
|
||||||
|
const list_themes = @import("list_themes.zig");
|
||||||
|
|
||||||
/// Special commands that can be invoked via CLI flags. These are all
|
/// Special commands that can be invoked via CLI flags. These are all
|
||||||
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
||||||
@ -18,6 +19,9 @@ pub const Action = enum {
|
|||||||
/// List available keybinds
|
/// List available keybinds
|
||||||
@"list-keybinds",
|
@"list-keybinds",
|
||||||
|
|
||||||
|
/// List available themes
|
||||||
|
@"list-themes",
|
||||||
|
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
/// Multiple actions were detected. You can specify at most one
|
/// Multiple actions were detected. You can specify at most one
|
||||||
/// action on the CLI otherwise the behavior desired is ambiguous.
|
/// action on the CLI otherwise the behavior desired is ambiguous.
|
||||||
@ -57,6 +61,7 @@ pub const Action = enum {
|
|||||||
.version => try version.run(),
|
.version => try version.run(),
|
||||||
.@"list-fonts" => try list_fonts.run(alloc),
|
.@"list-fonts" => try list_fonts.run(alloc),
|
||||||
.@"list-keybinds" => try list_keybinds.run(alloc),
|
.@"list-keybinds" => try list_keybinds.run(alloc),
|
||||||
|
.@"list-themes" => try list_themes.run(alloc),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -67,6 +67,11 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
while (iter.next()) |arg| {
|
while (iter.next()) |arg| {
|
||||||
|
// If an _inputs fields exist we keep track of the inputs.
|
||||||
|
if (@hasField(T, "_inputs")) {
|
||||||
|
try dst._inputs.append(arena_alloc, try arena_alloc.dupe(u8, arg));
|
||||||
|
}
|
||||||
|
|
||||||
// Do manual parsing if we have a hook for it.
|
// Do manual parsing if we have a hook for it.
|
||||||
if (@hasDecl(T, "parseManuallyHook")) {
|
if (@hasDecl(T, "parseManuallyHook")) {
|
||||||
if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return;
|
if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return;
|
||||||
@ -381,6 +386,30 @@ test "parse: error tracking" {
|
|||||||
try testing.expect(!data._errors.empty());
|
try testing.expect(!data._errors.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "parse: input tracking" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: []const u8 = "",
|
||||||
|
b: enum { one } = .one,
|
||||||
|
|
||||||
|
_arena: ?ArenaAllocator = null,
|
||||||
|
_errors: ErrorList = .{},
|
||||||
|
_inputs: std.ArrayListUnmanaged([]const u8) = .{},
|
||||||
|
} = .{};
|
||||||
|
defer if (data._arena) |arena| arena.deinit();
|
||||||
|
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
testing.allocator,
|
||||||
|
"--what --a=42",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||||
|
try testing.expect(data._arena != null);
|
||||||
|
try testing.expect(data._inputs.items.len == 2);
|
||||||
|
try testing.expectEqualStrings("--what", data._inputs.items[0]);
|
||||||
|
try testing.expectEqualStrings("--a=42", data._inputs.items[1]);
|
||||||
|
}
|
||||||
test "parseIntoField: ignore underscore-prefixed fields" {
|
test "parseIntoField: ignore underscore-prefixed fields" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
@ -732,6 +761,25 @@ pub fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) {
|
|||||||
return .{ .r = reader };
|
return .{ .r = reader };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An iterator valid for arg parsing from a slice.
|
||||||
|
pub const SliceIterator = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
slice: []const []const u8,
|
||||||
|
idx: usize = 0,
|
||||||
|
|
||||||
|
pub fn next(self: *Self) ?[]const u8 {
|
||||||
|
if (self.idx >= self.slice.len) return null;
|
||||||
|
defer self.idx += 1;
|
||||||
|
return self.slice[self.idx];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Construct a SliceIterator from a slice.
|
||||||
|
pub fn sliceIterator(slice: []const []const u8) SliceIterator {
|
||||||
|
return .{ .slice = slice };
|
||||||
|
}
|
||||||
|
|
||||||
test "LineIterator" {
|
test "LineIterator" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var fbs = std.io.fixedBufferStream(
|
var fbs = std.io.fixedBufferStream(
|
||||||
|
74
src/cli/list_themes.zig
Normal file
74
src/cli/list_themes.zig
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const inputpkg = @import("../input.zig");
|
||||||
|
const args = @import("args.zig");
|
||||||
|
const Arena = std.heap.ArenaAllocator;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Config = @import("../config/Config.zig");
|
||||||
|
const global_state = &@import("../main.zig").state;
|
||||||
|
|
||||||
|
pub const Options = struct {
|
||||||
|
pub fn deinit(self: Options) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The "list-themes" command is used to list all the available themes
|
||||||
|
/// for Ghostty.
|
||||||
|
///
|
||||||
|
/// Themes require that Ghostty have access to the resources directory.
|
||||||
|
/// On macOS this is embedded in the app bundle. On Linux, this is usually
|
||||||
|
/// in `/usr/share`. If you're compiling from source, this is the
|
||||||
|
/// `zig-out/share` directory. You can also set the `GHOSTTY_RESOURCES_DIR`
|
||||||
|
/// environment variable to point to the resources directory. Themes
|
||||||
|
/// live in the `themes` subdirectory of the resources directory.
|
||||||
|
pub fn run(alloc: Allocator) !u8 {
|
||||||
|
var opts: Options = .{};
|
||||||
|
defer opts.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
var iter = try std.process.argsWithAllocator(alloc);
|
||||||
|
defer iter.deinit();
|
||||||
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stderr = std.io.getStdErr().writer();
|
||||||
|
const stdout = std.io.getStdOut().writer();
|
||||||
|
|
||||||
|
const resources_dir = global_state.resources_dir orelse {
|
||||||
|
try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
|
||||||
|
"that Ghostty is installed correctly.\n", .{});
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = try std.fs.path.join(alloc, &.{ resources_dir, "themes" });
|
||||||
|
defer alloc.free(path);
|
||||||
|
|
||||||
|
var dir = try std.fs.cwd().openIterableDir(path, .{});
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
var walker = try dir.walk(alloc);
|
||||||
|
defer walker.deinit();
|
||||||
|
|
||||||
|
var themes = std.ArrayList([]const u8).init(alloc);
|
||||||
|
defer {
|
||||||
|
for (themes.items) |v| alloc.free(v);
|
||||||
|
themes.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (try walker.next()) |entry| {
|
||||||
|
if (entry.kind != .file) continue;
|
||||||
|
try themes.append(try alloc.dupe(u8, entry.basename));
|
||||||
|
}
|
||||||
|
|
||||||
|
std.mem.sortUnstable([]const u8, themes.items, {}, struct {
|
||||||
|
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
|
||||||
|
return std.ascii.orderIgnoreCase(lhs, rhs) == .lt;
|
||||||
|
}
|
||||||
|
}.lessThan);
|
||||||
|
|
||||||
|
for (themes.items) |theme| {
|
||||||
|
try stdout.print("{s}\n", .{theme});
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
@ -7,6 +7,7 @@ const builtin = @import("builtin");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const global_state = &@import("../main.zig").state;
|
||||||
const fontpkg = @import("../font/main.zig");
|
const fontpkg = @import("../font/main.zig");
|
||||||
const inputpkg = @import("../input.zig");
|
const inputpkg = @import("../input.zig");
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
@ -153,6 +154,26 @@ const c = @cImport({
|
|||||||
@"adjust-strikethrough-position": ?MetricModifier = null,
|
@"adjust-strikethrough-position": ?MetricModifier = null,
|
||||||
@"adjust-strikethrough-thickness": ?MetricModifier = null,
|
@"adjust-strikethrough-thickness": ?MetricModifier = null,
|
||||||
|
|
||||||
|
/// A named theme to use. The available themes are currently hardcoded to
|
||||||
|
/// the themes that ship with Ghostty. On macOS, this list is in the
|
||||||
|
/// `Ghostty.app/Contents/Resources/themes` directory. On Linux, this
|
||||||
|
/// list is in the `share/ghostty/themes` directory (wherever you installed
|
||||||
|
/// the Ghostty "share" directory.
|
||||||
|
///
|
||||||
|
/// To see a list of available themes, run `ghostty +list-themes`.
|
||||||
|
///
|
||||||
|
/// Any additional colors specified via background, foreground, palette,
|
||||||
|
/// etc. will override the colors specified in the theme.
|
||||||
|
///
|
||||||
|
/// This configuration can be changed at runtime, but the new theme will
|
||||||
|
/// only affect new cells. Existing colored cells will not be updated.
|
||||||
|
/// Therefore, after changing the theme, you should restart any running
|
||||||
|
/// programs to ensure they get the new colors.
|
||||||
|
///
|
||||||
|
/// A future update will allow custom themes to be installed in
|
||||||
|
/// certain directories.
|
||||||
|
theme: ?[]const u8 = null,
|
||||||
|
|
||||||
/// Background color for the window.
|
/// Background color for the window.
|
||||||
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||||
|
|
||||||
@ -726,6 +747,10 @@ _arena: ?ArenaAllocator = null,
|
|||||||
/// configuration file.
|
/// configuration file.
|
||||||
_errors: ErrorList = .{},
|
_errors: ErrorList = .{},
|
||||||
|
|
||||||
|
/// The inputs that built up this configuration. This is used to reload
|
||||||
|
/// the configuration if we have to.
|
||||||
|
_inputs: std.ArrayListUnmanaged([]const u8) = .{},
|
||||||
|
|
||||||
pub fn deinit(self: *Config) void {
|
pub fn deinit(self: *Config) void {
|
||||||
if (self._arena) |arena| arena.deinit();
|
if (self._arena) |arena| arena.deinit();
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
@ -1182,6 +1207,16 @@ fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods {
|
|||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load configuration from an iterator that yields values that look like
|
||||||
|
/// command-line arguments, i.e. `--key=value`.
|
||||||
|
pub fn loadIter(
|
||||||
|
self: *Config,
|
||||||
|
alloc: Allocator,
|
||||||
|
iter: anytype,
|
||||||
|
) !void {
|
||||||
|
try cli.args.parse(Config, alloc, self, iter);
|
||||||
|
}
|
||||||
|
|
||||||
/// Load the configuration from the default configuration file. The default
|
/// Load the configuration from the default configuration file. The default
|
||||||
/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`.
|
/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`.
|
||||||
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
|
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
|
||||||
@ -1195,7 +1230,7 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
|
|||||||
|
|
||||||
var buf_reader = std.io.bufferedReader(file.reader());
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||||
try cli.args.parse(Config, alloc, self, &iter);
|
try self.loadIter(alloc, &iter);
|
||||||
try self.expandPaths(std.fs.path.dirname(config_path).?);
|
try self.expandPaths(std.fs.path.dirname(config_path).?);
|
||||||
} else |err| switch (err) {
|
} else |err| switch (err) {
|
||||||
error.FileNotFound => std.log.info(
|
error.FileNotFound => std.log.info(
|
||||||
@ -1222,7 +1257,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
|||||||
// Parse the config from the CLI args
|
// Parse the config from the CLI args
|
||||||
var iter = try std.process.argsWithAllocator(alloc_gpa);
|
var iter = try std.process.argsWithAllocator(alloc_gpa);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try cli.args.parse(Config, alloc_gpa, self, &iter);
|
try self.loadIter(alloc_gpa, &iter);
|
||||||
|
|
||||||
// Config files loaded from the CLI args are relative to pwd
|
// Config files loaded from the CLI args are relative to pwd
|
||||||
if (self.@"config-file".value.list.items.len > 0) {
|
if (self.@"config-file".value.list.items.len > 0) {
|
||||||
@ -1279,7 +1314,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
|||||||
log.info("loading config-file path={s}", .{path});
|
log.info("loading config-file path={s}", .{path});
|
||||||
var buf_reader = std.io.bufferedReader(file.reader());
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||||
try cli.args.parse(Config, alloc_gpa, self, &iter);
|
try self.loadIter(alloc_gpa, &iter);
|
||||||
try self.expandPaths(std.fs.path.dirname(path).?);
|
try self.expandPaths(std.fs.path.dirname(path).?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1299,7 +1334,85 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn loadTheme(self: *Config, theme: []const u8) !void {
|
||||||
|
const alloc = self._arena.?.allocator();
|
||||||
|
const resources_dir = global_state.resources_dir orelse {
|
||||||
|
try self._errors.add(alloc, .{
|
||||||
|
.message = "no resources directory found, themes will not work",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = try std.fs.path.join(alloc, &.{
|
||||||
|
resources_dir,
|
||||||
|
"themes",
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cwd = std.fs.cwd();
|
||||||
|
var file = cwd.openFile(path, .{}) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.FileNotFound => try self._errors.add(alloc, .{
|
||||||
|
.message = try std.fmt.allocPrintZ(
|
||||||
|
alloc,
|
||||||
|
"theme \"{s}\" not found, path={s}",
|
||||||
|
.{ theme, path },
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
else => try self._errors.add(alloc, .{
|
||||||
|
.message = try std.fmt.allocPrintZ(
|
||||||
|
alloc,
|
||||||
|
"failed to load theme \"{s}\": {}",
|
||||||
|
.{ theme, err },
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
// From this point onwards, we load the theme and do a bit of a dance
|
||||||
|
// to achive two separate goals:
|
||||||
|
//
|
||||||
|
// (1) We want the theme to be loaded and our existing config to
|
||||||
|
// override the theme. So we need to load the theme and apply
|
||||||
|
// our config on top of it.
|
||||||
|
//
|
||||||
|
// (2) We want to free existing memory that we aren't using anymore
|
||||||
|
// as a result of reloading the configuration.
|
||||||
|
//
|
||||||
|
// Point 2 is strictly a result of aur approach to point 1.
|
||||||
|
|
||||||
|
// Keep track of our input length prior ot loading the theme
|
||||||
|
// so that we can replay the previous config to override values.
|
||||||
|
const input_len = self._inputs.items.len;
|
||||||
|
|
||||||
|
// Load into a new configuration so that we can free the existing memory.
|
||||||
|
const alloc_gpa = self._arena.?.child_allocator;
|
||||||
|
var new_config = try default(alloc_gpa);
|
||||||
|
errdefer new_config.deinit();
|
||||||
|
|
||||||
|
// Load our theme
|
||||||
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
|
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||||
|
try new_config.loadIter(alloc_gpa, &iter);
|
||||||
|
|
||||||
|
// Replay our previous inputs so that we can override values
|
||||||
|
// from the theme.
|
||||||
|
var slice_it = cli.args.sliceIterator(self._inputs.items[0..input_len]);
|
||||||
|
try new_config.loadIter(alloc_gpa, &slice_it);
|
||||||
|
|
||||||
|
// Success, swap our new config in and free the old.
|
||||||
|
self.deinit();
|
||||||
|
self.* = new_config;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn finalize(self: *Config) !void {
|
pub fn finalize(self: *Config) !void {
|
||||||
|
// We always load the theme first because it may set other fields
|
||||||
|
// in our config.
|
||||||
|
if (self.theme) |theme| try self.loadTheme(theme);
|
||||||
|
|
||||||
// If we have a font-family set and don't set the others, default
|
// If we have a font-family set and don't set the others, default
|
||||||
// the others to the font family. This way, if someone does
|
// the others to the font family. This way, if someone does
|
||||||
// --font-family=foo, then we try to get the stylized versions of
|
// --font-family=foo, then we try to get the stylized versions of
|
||||||
|
12
src/main.zig
12
src/main.zig
@ -175,6 +175,10 @@ pub const GlobalState = struct {
|
|||||||
action: ?cli.Action,
|
action: ?cli.Action,
|
||||||
logging: Logging,
|
logging: Logging,
|
||||||
|
|
||||||
|
/// The app resources directory, equivalent to zig-out/share when we build
|
||||||
|
/// from source. This is null if we can't detect it.
|
||||||
|
resources_dir: ?[]const u8,
|
||||||
|
|
||||||
/// Where logging should go
|
/// Where logging should go
|
||||||
pub const Logging = union(enum) {
|
pub const Logging = union(enum) {
|
||||||
disabled: void,
|
disabled: void,
|
||||||
@ -192,6 +196,7 @@ pub const GlobalState = struct {
|
|||||||
.tracy = undefined,
|
.tracy = undefined,
|
||||||
.action = null,
|
.action = null,
|
||||||
.logging = .{ .stderr = {} },
|
.logging = .{ .stderr = {} },
|
||||||
|
.resources_dir = null,
|
||||||
};
|
};
|
||||||
errdefer self.deinit();
|
errdefer self.deinit();
|
||||||
|
|
||||||
@ -271,11 +276,18 @@ pub const GlobalState = struct {
|
|||||||
|
|
||||||
// Initialize glslang for shader compilation
|
// Initialize glslang for shader compilation
|
||||||
try glslang.init();
|
try glslang.init();
|
||||||
|
|
||||||
|
// Find our resources directory once for the app so every launch
|
||||||
|
// hereafter can use this cached value.
|
||||||
|
self.resources_dir = try internal_os.resourcesDir(self.alloc);
|
||||||
|
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cleans up the global state. This doesn't _need_ to be called but
|
/// Cleans up the global state. This doesn't _need_ to be called but
|
||||||
/// doing so in dev modes will check for memory leaks.
|
/// doing so in dev modes will check for memory leaks.
|
||||||
pub fn deinit(self: *GlobalState) void {
|
pub fn deinit(self: *GlobalState) void {
|
||||||
|
if (self.resources_dir) |dir| self.alloc.free(dir);
|
||||||
|
|
||||||
if (self.gpa) |*value| {
|
if (self.gpa) |*value| {
|
||||||
// We want to ensure that we deinit the GPA because this is
|
// We want to ensure that we deinit the GPA because this is
|
||||||
// the point at which it will output if there were safety violations.
|
// the point at which it will output if there were safety violations.
|
||||||
|
Reference in New Issue
Block a user