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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
{
|
||||
// Encode our terminfo
|
||||
|
@ -41,7 +41,11 @@
|
||||
.glslang = .{ .path = "./pkg/glslang" },
|
||||
.spirv_cross = .{ .path = "./pkg/spirv-cross" },
|
||||
|
||||
// System headers
|
||||
// Other
|
||||
.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 */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
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 */; };
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -198,6 +200,7 @@
|
||||
A5B30528299BEAAA0047F10C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5CB04372B0F1C1C009ED217 /* themes */,
|
||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
|
||||
@ -302,6 +305,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5CB04382B0F1C1C009ED217 /* themes in Resources */,
|
||||
A545D1A22A5772CE006E0AE4 /* shell-integration in Resources */,
|
||||
A596309A2AEE1C6400D64628 /* Terminal.xib 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.
|
||||
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.
|
||||
/// This is lazily initialized on the first call to fontDiscover so do
|
||||
/// not access this directly.
|
||||
@ -59,17 +55,11 @@ pub fn create(
|
||||
var app = try alloc.create(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.* = .{
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.quit = false,
|
||||
.resources_dir = resources_dir,
|
||||
};
|
||||
errdefer app.surfaces.deinit(alloc);
|
||||
|
||||
@ -81,7 +71,6 @@ pub fn destroy(self: *App) void {
|
||||
for (self.surfaces.items) |surface| surface.deinit();
|
||||
self.surfaces.deinit(self.alloc);
|
||||
|
||||
if (self.resources_dir) |dir| self.alloc.free(dir);
|
||||
if (comptime font.Discover != void) {
|
||||
if (self.font_discover) |*v| v.deinit();
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const main = @import("main.zig");
|
||||
const renderer = @import("renderer.zig");
|
||||
const termio = @import("termio.zig");
|
||||
const objc = @import("objc");
|
||||
@ -451,7 +452,7 @@ pub fn init(
|
||||
.padding = padding,
|
||||
.full_config = 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_wakeup = render_thread.wakeup,
|
||||
.renderer_mailbox = render_thread.mailbox,
|
||||
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const c = @import("c.zig");
|
||||
const global_state = &@import("../../main.zig").state;
|
||||
|
||||
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.
|
||||
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: {
|
||||
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 will not have Ghostty icon", .{});
|
||||
break :icon;
|
||||
|
@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
|
||||
const list_fonts = @import("list_fonts.zig");
|
||||
const version = @import("version.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
|
||||
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
||||
@ -18,6 +19,9 @@ pub const Action = enum {
|
||||
/// List available keybinds
|
||||
@"list-keybinds",
|
||||
|
||||
/// List available themes
|
||||
@"list-themes",
|
||||
|
||||
pub const Error = error{
|
||||
/// Multiple actions were detected. You can specify at most one
|
||||
/// action on the CLI otherwise the behavior desired is ambiguous.
|
||||
@ -57,6 +61,7 @@ pub const Action = enum {
|
||||
.version => try version.run(),
|
||||
.@"list-fonts" => try list_fonts.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| {
|
||||
// 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.
|
||||
if (@hasDecl(T, "parseManuallyHook")) {
|
||||
if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return;
|
||||
@ -381,6 +386,30 @@ test "parse: error tracking" {
|
||||
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" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
@ -732,6 +761,25 @@ pub fn lineIterator(reader: anytype) LineIterator(@TypeOf(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" {
|
||||
const testing = std.testing;
|
||||
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 Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const global_state = &@import("../main.zig").state;
|
||||
const fontpkg = @import("../font/main.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
@ -153,6 +154,26 @@ const c = @cImport({
|
||||
@"adjust-strikethrough-position": ?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 = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||
|
||||
@ -726,6 +747,10 @@ _arena: ?ArenaAllocator = null,
|
||||
/// configuration file.
|
||||
_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 {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
@ -1182,6 +1207,16 @@ fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods {
|
||||
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
|
||||
/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`.
|
||||
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 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).?);
|
||||
} else |err| switch (err) {
|
||||
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
|
||||
var iter = try std.process.argsWithAllocator(alloc_gpa);
|
||||
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
|
||||
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});
|
||||
var buf_reader = std.io.bufferedReader(file.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).?);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
// 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
|
||||
// the others to the font family. This way, if someone does
|
||||
// --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,
|
||||
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
|
||||
pub const Logging = union(enum) {
|
||||
disabled: void,
|
||||
@ -192,6 +196,7 @@ pub const GlobalState = struct {
|
||||
.tracy = undefined,
|
||||
.action = null,
|
||||
.logging = .{ .stderr = {} },
|
||||
.resources_dir = null,
|
||||
};
|
||||
errdefer self.deinit();
|
||||
|
||||
@ -271,11 +276,18 @@ pub const GlobalState = struct {
|
||||
|
||||
// Initialize glslang for shader compilation
|
||||
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
|
||||
/// doing so in dev modes will check for memory leaks.
|
||||
pub fn deinit(self: *GlobalState) void {
|
||||
if (self.resources_dir) |dir| self.alloc.free(dir);
|
||||
|
||||
if (self.gpa) |*value| {
|
||||
// We want to ensure that we deinit the GPA because this is
|
||||
// the point at which it will output if there were safety violations.
|
||||
|
Reference in New Issue
Block a user