mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 11:46:11 +03:00
Merge pull request #483 from mitchellh/gtk-config-errors
gtk: configuration errors window
This commit is contained in:
@ -11,6 +11,7 @@
|
||||
const App = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const glfw = @import("glfw");
|
||||
const configpkg = @import("../../config.zig");
|
||||
@ -20,6 +21,7 @@ const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -35,6 +37,9 @@ ctx: *c.GMainContext,
|
||||
/// The "none" cursor. We use one that is shared across the entire app.
|
||||
cursor_none: ?*c.GdkCursor,
|
||||
|
||||
/// The configuration errors window, if it is currently open.
|
||||
config_errors_window: ?*ConfigErrorsWindow = null,
|
||||
|
||||
/// This is set to false when the main loop should exit.
|
||||
running: bool = true,
|
||||
|
||||
@ -80,7 +85,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
_ = c.g_signal_connect_data(
|
||||
app,
|
||||
"activate",
|
||||
c.G_CALLBACK(&activate),
|
||||
c.G_CALLBACK(>kActivate),
|
||||
core_app,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
@ -155,9 +160,31 @@ pub fn reloadConfig(self: *App) !?*const Config {
|
||||
self.config.deinit();
|
||||
self.config = config;
|
||||
|
||||
// If there were errors, report them
|
||||
self.updateConfigErrors() catch |err| {
|
||||
log.warn("error handling configuration errors err={}", .{err});
|
||||
};
|
||||
|
||||
return &self.config;
|
||||
}
|
||||
|
||||
/// This should be called whenever the configuration changes to update
|
||||
/// the state of our config errors window. This will show the window if
|
||||
/// there are new configuration errors and hide the window if the errors
|
||||
/// are resolved.
|
||||
fn updateConfigErrors(self: *App) !void {
|
||||
if (!self.config._errors.empty()) {
|
||||
if (self.config_errors_window == null) {
|
||||
try ConfigErrorsWindow.create(self);
|
||||
assert(self.config_errors_window != null);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.config_errors_window) |window| {
|
||||
window.update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by CoreApp to wake up the event loop.
|
||||
pub fn wakeup(self: App) void {
|
||||
_ = self;
|
||||
@ -166,6 +193,14 @@ pub fn wakeup(self: App) void {
|
||||
|
||||
/// Run the event loop. This doesn't return until the app exits.
|
||||
pub fn run(self: *App) !void {
|
||||
if (!self.running) return;
|
||||
|
||||
// On startup, we want to check for configuration errors right away
|
||||
// so we can show our error window.
|
||||
self.updateConfigErrors() catch |err| {
|
||||
log.warn("error handling configuration errors err={}", .{err});
|
||||
};
|
||||
|
||||
while (self.running) {
|
||||
_ = c.g_main_context_iteration(self.ctx, 1);
|
||||
|
||||
@ -286,7 +321,7 @@ fn gtkQuitConfirmation(
|
||||
/// This is called by the "activate" signal. This is sent on program
|
||||
/// startup and also when a secondary instance launches and requests
|
||||
/// a new window.
|
||||
fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||
fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = app;
|
||||
|
||||
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
||||
|
201
src/apprt/gtk/ConfigErrorsWindow.zig
Normal file
201
src/apprt/gtk/ConfigErrorsWindow.zig
Normal file
@ -0,0 +1,201 @@
|
||||
/// Configuration errors window.
|
||||
const ConfigErrors = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
||||
const App = @import("App.zig");
|
||||
const View = @import("View.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
app: *App,
|
||||
window: *c.GtkWindow,
|
||||
view: PrimaryView,
|
||||
|
||||
pub fn create(app: *App) !void {
|
||||
if (app.config_errors_window != null) return error.InvalidOperation;
|
||||
|
||||
const alloc = app.core_app.alloc;
|
||||
const self = try alloc.create(ConfigErrors);
|
||||
errdefer alloc.destroy(self);
|
||||
try self.init(app);
|
||||
|
||||
app.config_errors_window = self;
|
||||
}
|
||||
|
||||
pub fn update(self: *ConfigErrors) void {
|
||||
if (self.app.config._errors.empty()) {
|
||||
c.gtk_window_destroy(@ptrCast(self.window));
|
||||
return;
|
||||
}
|
||||
|
||||
self.view.update(&self.app.config);
|
||||
_ = c.gtk_window_present(self.window);
|
||||
_ = c.gtk_widget_grab_focus(@ptrCast(self.window));
|
||||
}
|
||||
|
||||
/// Not public because this should be called by the GTK lifecycle.
|
||||
fn destroy(self: *ConfigErrors) void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
self.app.config_errors_window = null;
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn init(self: *ConfigErrors, app: *App) !void {
|
||||
// Create the window
|
||||
const window = c.gtk_application_window_new(app.app);
|
||||
const gtk_window: *c.GtkWindow = @ptrCast(window);
|
||||
errdefer c.gtk_window_destroy(gtk_window);
|
||||
c.gtk_window_set_title(gtk_window, "Configuration Errors");
|
||||
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Set some state
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.window = gtk_window,
|
||||
.view = undefined,
|
||||
};
|
||||
|
||||
// Show the window
|
||||
const view = try PrimaryView.init(self);
|
||||
self.view = view;
|
||||
c.gtk_window_set_child(@ptrCast(window), view.root);
|
||||
c.gtk_widget_show(window);
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self = userdataSelf(ud.?);
|
||||
self.destroy();
|
||||
}
|
||||
|
||||
fn userdataSelf(ud: *anyopaque) *ConfigErrors {
|
||||
return @ptrCast(@alignCast(ud));
|
||||
}
|
||||
|
||||
const PrimaryView = struct {
|
||||
root: *c.GtkWidget,
|
||||
text: *c.GtkTextView,
|
||||
|
||||
pub fn init(root: *ConfigErrors) !PrimaryView {
|
||||
// All our widgets
|
||||
const label = c.gtk_label_new(
|
||||
"One or more configuration errors were found while loading " ++
|
||||
"the configuration. Please review the errors below and reload " ++
|
||||
"your configuration or ignore the erroneous lines.",
|
||||
);
|
||||
const buf = contentsBuffer(&root.app.config);
|
||||
defer c.g_object_unref(buf);
|
||||
const buttons = try ButtonsView.init(root);
|
||||
const text_scroll = c.gtk_scrolled_window_new();
|
||||
errdefer c.g_object_unref(text_scroll);
|
||||
const text = c.gtk_text_view_new_with_buffer(buf);
|
||||
errdefer c.g_object_unref(text);
|
||||
c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text);
|
||||
|
||||
// Create our view
|
||||
const view = try View.init(&.{
|
||||
.{ .name = "label", .widget = label },
|
||||
.{ .name = "text", .widget = text_scroll },
|
||||
.{ .name = "buttons", .widget = buttons.root },
|
||||
}, &vfl);
|
||||
errdefer view.deinit();
|
||||
|
||||
// We can do additional settings once the layout is setup
|
||||
c.gtk_label_set_wrap(@ptrCast(label), 1);
|
||||
c.gtk_text_view_set_editable(@ptrCast(text), 0);
|
||||
c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0);
|
||||
c.gtk_text_view_set_top_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_left_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_right_margin(@ptrCast(text), 8);
|
||||
|
||||
return .{ .root = view.root, .text = @ptrCast(text) };
|
||||
}
|
||||
|
||||
pub fn update(self: *PrimaryView, config: *const Config) void {
|
||||
const buf = contentsBuffer(config);
|
||||
defer c.g_object_unref(buf);
|
||||
c.gtk_text_view_set_buffer(@ptrCast(self.text), buf);
|
||||
}
|
||||
|
||||
/// Returns the GtkTextBuffer for the config errors that we want to show.
|
||||
fn contentsBuffer(config: *const Config) *c.GtkTextBuffer {
|
||||
const buf = c.gtk_text_buffer_new(null);
|
||||
errdefer c.g_object_unref(buf);
|
||||
|
||||
for (config._errors.list.items) |err| {
|
||||
c.gtk_text_buffer_insert_at_cursor(buf, err.message, @intCast(err.message.len));
|
||||
c.gtk_text_buffer_insert_at_cursor(buf, "\n", -1);
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
const vfl = [_][*:0]const u8{
|
||||
"H:|-8-[label]-8-|",
|
||||
"H:|[text]|",
|
||||
"H:|[buttons]|",
|
||||
"V:|[label(<=80)][text(>=100)]-[buttons]-|",
|
||||
};
|
||||
};
|
||||
|
||||
const ButtonsView = struct {
|
||||
root: *c.GtkWidget,
|
||||
|
||||
pub fn init(root: *ConfigErrors) !ButtonsView {
|
||||
const ignore_button = c.gtk_button_new_with_label("Ignore");
|
||||
errdefer c.g_object_unref(ignore_button);
|
||||
|
||||
const reload_button = c.gtk_button_new_with_label("Reload Configuration");
|
||||
errdefer c.g_object_unref(reload_button);
|
||||
|
||||
// Create our view
|
||||
const view = try View.init(&.{
|
||||
.{ .name = "ignore", .widget = ignore_button },
|
||||
.{ .name = "reload", .widget = reload_button },
|
||||
}, &vfl);
|
||||
|
||||
// Signals
|
||||
_ = c.g_signal_connect_data(
|
||||
ignore_button,
|
||||
"clicked",
|
||||
c.G_CALLBACK(>kIgnoreClick),
|
||||
root,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
_ = c.g_signal_connect_data(
|
||||
reload_button,
|
||||
"clicked",
|
||||
c.G_CALLBACK(>kReloadClick),
|
||||
root,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
|
||||
return .{ .root = view.root };
|
||||
}
|
||||
|
||||
fn gtkIgnoreClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self: *ConfigErrors = @ptrCast(@alignCast(ud));
|
||||
c.gtk_window_destroy(@ptrCast(self.window));
|
||||
}
|
||||
|
||||
fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const self: *ConfigErrors = @ptrCast(@alignCast(ud));
|
||||
_ = self.app.reloadConfig() catch |err| {
|
||||
log.warn("error reloading config error={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const vfl = [_][*:0]const u8{
|
||||
"H:[ignore]-8-[reload]-8-|",
|
||||
};
|
||||
};
|
73
src/apprt/gtk/View.zig
Normal file
73
src/apprt/gtk/View.zig
Normal file
@ -0,0 +1,73 @@
|
||||
/// View helps with creating a view with a constraint layout by
|
||||
/// managing all the boilerplate. The caller is responsible for
|
||||
/// providing the widgets, their names, and the VFL code and gets
|
||||
/// a root box as a result ready to be used.
|
||||
const View = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// The box that contains all of the widgets.
|
||||
root: *c.GtkWidget,
|
||||
|
||||
/// A single widget used in the view.
|
||||
pub const Widget = struct {
|
||||
/// The name of the widget used for the layout code. This is also
|
||||
/// the name set for the widget for CSS styling.
|
||||
name: [:0]const u8,
|
||||
|
||||
/// The widget itself.
|
||||
widget: *c.GtkWidget,
|
||||
};
|
||||
|
||||
/// Initialize a new constraint layout view with the given widgets
|
||||
/// and VFL.
|
||||
pub fn init(widgets: []const Widget, vfl: []const [*:0]const u8) !View {
|
||||
// Box to store all our widgets
|
||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||
errdefer c.g_object_unref(box);
|
||||
c.gtk_widget_set_vexpand(box, 1);
|
||||
c.gtk_widget_set_hexpand(box, 1);
|
||||
|
||||
// Setup our constraint layout and attach it to the box
|
||||
const layout = c.gtk_constraint_layout_new();
|
||||
errdefer c.g_object_unref(layout);
|
||||
c.gtk_widget_set_layout_manager(@ptrCast(box), layout);
|
||||
|
||||
// Setup our views table
|
||||
const views = c.g_hash_table_new(c.g_str_hash, c.g_str_equal);
|
||||
defer c.g_hash_table_unref(views);
|
||||
|
||||
// Add our widgets
|
||||
for (widgets) |widget| {
|
||||
c.gtk_widget_set_parent(widget.widget, box);
|
||||
c.gtk_widget_set_name(widget.widget, widget.name);
|
||||
_ = c.g_hash_table_insert(
|
||||
views,
|
||||
@constCast(@ptrCast(widget.name.ptr)),
|
||||
widget.widget,
|
||||
);
|
||||
}
|
||||
|
||||
// Add all of our constraints for layout
|
||||
var err_: ?*c.GError = null;
|
||||
const list = c.gtk_constraint_layout_add_constraints_from_descriptionv(
|
||||
@ptrCast(layout),
|
||||
vfl.ptr,
|
||||
vfl.len,
|
||||
8,
|
||||
8,
|
||||
views,
|
||||
&err_,
|
||||
);
|
||||
if (err_) |err| {
|
||||
defer c.g_error_free(err);
|
||||
log.warn("error building view message={s}", .{err.message});
|
||||
return error.OperationFailed;
|
||||
}
|
||||
c.g_list_free(list);
|
||||
|
||||
return .{ .root = box };
|
||||
}
|
Reference in New Issue
Block a user