Merge pull request #425 from mitchellh/split-config

Configurable unfocused split opacity, C API for reading Config
This commit is contained in:
Mitchell Hashimoto
2023-09-10 19:03:36 -07:00
committed by GitHub
10 changed files with 1798 additions and 1621 deletions

View File

@ -301,6 +301,7 @@ void ghostty_config_load_string(ghostty_config_t, const char *, uintptr_t);
void ghostty_config_load_default_files(ghostty_config_t);
void ghostty_config_load_recursive_files(ghostty_config_t);
void ghostty_config_finalize(ghostty_config_t);
bool ghostty_config_get(ghostty_config_t, void *, const char *, uintptr_t);
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, const char *, uintptr_t);
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t);

View File

@ -96,6 +96,7 @@ struct PrimaryView: View {
Ghostty.TerminalSplit(onClose: Self.closeWindow, baseConfig: self.baseConfig)
.ghosttyApp(ghostty.app!)
.ghosttyConfig(ghostty.config!)
.background(WindowAccessor(window: $window))
.onReceive(gotoTab) { onGotoTab(notification: $0) }
.onReceive(toggleFullscreen) { onToggleFullscreen(notification: $0) }

View File

@ -350,15 +350,28 @@ private struct GhosttyAppKey: EnvironmentKey {
static let defaultValue: ghostty_app_t? = nil
}
private struct GhosttyConfigKey: EnvironmentKey {
static let defaultValue: ghostty_config_t? = nil
}
extension EnvironmentValues {
var ghosttyApp: ghostty_app_t? {
get { self[GhosttyAppKey.self] }
set { self[GhosttyAppKey.self] = newValue }
}
var ghosttyConfig: ghostty_config_t? {
get { self[GhosttyConfigKey.self] }
set { self[GhosttyConfigKey.self] = newValue }
}
}
extension View {
func ghosttyApp(_ app: ghostty_app_t?) -> some View {
environment(\.ghosttyApp, app)
}
func ghosttyConfig(_ config: ghostty_config_t?) -> some View {
environment(\.ghosttyConfig, config)
}
}

View File

@ -48,10 +48,20 @@ extension Ghostty {
// Maintain whether our window has focus (is key) or not
@State private var windowFocus: Bool = true
@Environment(\.ghosttyConfig) private var ghostty_config
// This is true if the terminal is considered "focused". The terminal is focused if
// it is both individually focused and the containing window is key.
private var hasFocus: Bool { surfaceFocus && windowFocus }
// The opacity of the rectangle when unfocused.
private var unfocusedOpacity: Double {
var opacity: Double = 0.85
let key = "unfocused-split-opacity"
_ = ghostty_config_get(ghostty_config, &opacity, key, UInt(key.count))
return 1 - opacity
}
var body: some View {
ZStack {
// We use a GeometryReader to get the frame bounds so that our metal surface
@ -129,7 +139,8 @@ extension Ghostty {
if (isSplit && !surfaceFocus) {
Rectangle()
.fill(.white)
.opacity(0.15)
.allowsHitTesting(false)
.opacity(unfocusedOpacity)
}
}
}

File diff suppressed because it is too large Load Diff

110
src/config/CAPI.zig Normal file
View File

@ -0,0 +1,110 @@
const std = @import("std");
const cli_args = @import("../cli_args.zig");
const inputpkg = @import("../input.zig");
const global = &@import("../main.zig").state;
const Config = @import("Config.zig");
const c_get = @import("c_get.zig");
const Key = @import("key.zig").Key;
const log = std.log.scoped(.config);
/// Create a new configuration filled with the initial default values.
export fn ghostty_config_new() ?*Config {
const result = global.alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
result.* = Config.default(global.alloc) catch |err| {
log.err("error creating config err={}", .{err});
return null;
};
return result;
}
export fn ghostty_config_free(ptr: ?*Config) void {
if (ptr) |v| {
v.deinit();
global.alloc.destroy(v);
}
}
/// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void {
self.loadCliArgs(global.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
/// Load the configuration from a string in the same format as
/// the file-based syntax for the desktop version of the terminal.
export fn ghostty_config_load_string(
self: *Config,
str: [*]const u8,
len: usize,
) void {
config_load_string_(self, str[0..len]) catch |err| {
log.err("error loading config err={}", .{err});
};
}
fn config_load_string_(self: *Config, str: []const u8) !void {
var fbs = std.io.fixedBufferStream(str);
var iter = cli_args.lineIterator(fbs.reader());
try cli_args.parse(Config, global.alloc, self, &iter);
}
/// Load the configuration from the default file locations. This
/// is usually done first. The default file locations are locations
/// such as the home directory.
export fn ghostty_config_load_default_files(self: *Config) void {
self.loadDefaultFiles(global.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
/// Load the configuration from the user-specified configuration
/// file locations in the previously loaded configuration. This will
/// recursively continue to load up to a built-in limit.
export fn ghostty_config_load_recursive_files(self: *Config) void {
self.loadRecursiveFiles(global.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
export fn ghostty_config_finalize(self: *Config) void {
self.finalize() catch |err| {
log.err("error finalizing config err={}", .{err});
};
}
export fn ghostty_config_get(
self: *Config,
ptr: *anyopaque,
key_str: [*]const u8,
len: usize,
) bool {
const key = std.meta.stringToEnum(Key, key_str[0..len]) orelse return false;
return c_get.get(self, key, ptr);
}
export fn ghostty_config_trigger(
self: *Config,
str: [*]const u8,
len: usize,
) inputpkg.Binding.Trigger {
return config_trigger_(self, str[0..len]) catch |err| err: {
log.err("error finding trigger err={}", .{err});
break :err .{};
};
}
fn config_trigger_(
self: *Config,
str: []const u8,
) !inputpkg.Binding.Trigger {
const action = try inputpkg.Binding.Action.parse(str);
return self.keybind.set.getTrigger(action) orelse .{};
}

1463
src/config/Config.zig Normal file

File diff suppressed because it is too large Load Diff

54
src/config/Wasm.zig Normal file
View File

@ -0,0 +1,54 @@
const std = @import("std");
const wasm = @import("../os/wasm.zig");
const cli_args = @import("../cli_args.zig");
const alloc = wasm.alloc;
const Config = @import("Config.zig");
const log = std.log.scoped(.config);
/// Create a new configuration filled with the initial default values.
export fn config_new() ?*Config {
const result = alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
result.* = Config.default(alloc) catch |err| {
log.err("error creating config err={}", .{err});
return null;
};
return result;
}
export fn config_free(ptr: ?*Config) void {
if (ptr) |v| {
v.deinit();
alloc.destroy(v);
}
}
/// Load the configuration from a string in the same format as
/// the file-based syntax for the desktop version of the terminal.
export fn config_load_string(
self: *Config,
str: [*]const u8,
len: usize,
) void {
config_load_string_(self, str[0..len]) catch |err| {
log.err("error loading config err={}", .{err});
};
}
fn config_load_string_(self: *Config, str: []const u8) !void {
var fbs = std.io.fixedBufferStream(str);
var iter = cli_args.lineIterator(fbs.reader());
try cli_args.parse(Config, alloc, self, &iter);
}
export fn config_finalize(self: *Config) void {
self.finalize() catch |err| {
log.err("error finalizing config err={}", .{err});
};
}

76
src/config/c_get.zig Normal file
View File

@ -0,0 +1,76 @@
const std = @import("std");
const key = @import("key.zig");
const Config = @import("Config.zig");
const Key = key.Key;
const Value = key.Value;
/// Get a value from the config by key into the given pointer. This is
/// specifically for C-compatible APIs. If you're using Zig, just access
/// the configuration directly.
///
/// The return value is false if the given key is not supported by the
/// C API yet. This is a fixable problem so if it is important to support
/// some key, please open an issue.
pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool {
@setEvalBranchQuota(10_000);
switch (k) {
inline else => |tag| {
const value = fieldByKey(config, tag);
switch (@TypeOf(value)) {
?[:0]const u8 => {
const ptr: *[*c]const u8 = @ptrCast(@alignCast(ptr_raw));
ptr.* = if (value) |slice| @ptrCast(slice.ptr) else null;
},
bool => {
const ptr: *bool = @ptrCast(@alignCast(ptr_raw));
ptr.* = value;
},
u8, u32 => {
const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw));
ptr.* = @intCast(value);
},
f32, f64 => {
const ptr: *f64 = @ptrCast(@alignCast(ptr_raw));
ptr.* = @floatCast(value);
},
else => return false,
}
return true;
},
}
}
/// Get a value from the config by key.
fn fieldByKey(self: *const Config, comptime k: Key) Value(k) {
const field = comptime field: {
const fields = std.meta.fields(Config);
for (fields) |field| {
if (@field(Key, field.name) == k) {
break :field field;
}
}
unreachable;
};
return @field(self, field.name);
}
test "u8" {
const testing = std.testing;
const alloc = testing.allocator;
var c = try Config.default(alloc);
defer c.deinit();
c.@"font-size" = 24;
var cval: c_uint = undefined;
try testing.expect(get(&c, .@"font-size", &cval));
try testing.expectEqual(@as(c_uint, 24), cval);
}

55
src/config/key.zig Normal file
View File

@ -0,0 +1,55 @@
const std = @import("std");
const Config = @import("Config.zig");
/// Key is an enum of all the available configuration keys. This is used
/// when paired with diff to determine what fields have changed in a config,
/// amongst other things.
pub const Key = key: {
const field_infos = std.meta.fields(Config);
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
var i: usize = 0;
inline for (field_infos) |field| {
// Ignore fields starting with "_" since they're internal and
// not copied ever.
if (field.name[0] == '_') continue;
enumFields[i] = .{
.name = field.name,
.value = i,
};
i += 1;
}
var decls = [_]std.builtin.Type.Declaration{};
break :key @Type(.{
.Enum = .{
.tag_type = std.math.IntFittingRange(0, field_infos.len - 1),
.fields = enumFields[0..i],
.decls = &decls,
.is_exhaustive = true,
},
});
};
/// Returns the value type for a key
pub fn Value(comptime key: Key) type {
const field = comptime field: {
const fields = std.meta.fields(Config);
for (fields) |field| {
if (@field(Key, field.name) == key) {
break :field field;
}
}
unreachable;
};
return field.type;
}
test "Value" {
const testing = std.testing;
try testing.expectEqual(?[:0]const u8, Value(.@"font-family"));
try testing.expectEqual(bool, Value(.@"cursor-style-blink"));
}