Merge pull request #12 from fuddlesworth/qt-config-helper
qt: phase 3 — extract config:: namespace for typed config accesspull/12846/head
commit
4102618fbe
|
|
@ -106,6 +106,7 @@ add_executable(ghastty
|
|||
src/actions/TabActions.cpp
|
||||
src/actions/WindowActions.cpp
|
||||
src/app/GhosttyApp.cpp
|
||||
src/config/Config.cpp
|
||||
src/CommandPalette.cpp
|
||||
src/GhosttySurface.cpp
|
||||
src/GlobalShortcuts.cpp
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "config/Config.h"
|
||||
#include "GhosttySurface.h"
|
||||
#include "MainWindow.h"
|
||||
#include "Util.h"
|
||||
|
|
@ -95,12 +96,10 @@ void CommandPalette::toggleFor(GhosttySurface *surface) {
|
|||
void CommandPalette::populate() {
|
||||
m_model->clear();
|
||||
if (!m_surface || !m_surface->owner()) return;
|
||||
ghostty_config_t cfg = m_surface->owner()->config();
|
||||
if (!cfg) return;
|
||||
|
||||
// command-palette-entry defaults to a large built-in command set.
|
||||
ghostty_config_command_list_s list = {};
|
||||
if (!configGet(cfg, &list, "command-palette-entry")) return;
|
||||
if (!config::get(&list, "command-palette-entry")) return;
|
||||
for (size_t i = 0; i < list.len; ++i) {
|
||||
const ghostty_command_s &c = list.commands[i];
|
||||
const QString title = QString::fromUtf8(c.title ? c.title : "");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "GhosttySurface.h"
|
||||
|
||||
#include "config/Config.h"
|
||||
#include "InspectorWindow.h"
|
||||
#include "MainWindow.h"
|
||||
#include "OverlayScrollbar.h"
|
||||
|
|
@ -241,9 +242,10 @@ void GhosttySurface::layoutScrollbar() {
|
|||
// `scrollbar = never` in the config hides the scrollbar unconditionally;
|
||||
// `system` (the default) shows it whenever there is scrollback.
|
||||
bool GhosttySurface::scrollbarAllowed() const {
|
||||
if (!m_owner || !m_owner->config()) return true;
|
||||
// config::get is null-safe (returns false when handle() is null),
|
||||
// so we only need the "could not read" → default-to-showing path.
|
||||
const char *value = nullptr;
|
||||
if (configGet(m_owner->config(), &value, "scrollbar") && value)
|
||||
if (config::get(&value, "scrollbar") && value)
|
||||
return qstrcmp(value, "never") != 0;
|
||||
return true; // unknown — default to showing
|
||||
}
|
||||
|
|
@ -270,7 +272,7 @@ void GhosttySurface::flashScrollbar() {
|
|||
if (!m_scrollbar || !scrollbarAllowed()) return;
|
||||
// Handle colour: light on a dark terminal, dark on a light one.
|
||||
ghostty_config_color_s bg{};
|
||||
if (m_owner && configGet(m_owner->config(), &bg, "background")) {
|
||||
if (config::get(&bg, "background")) {
|
||||
const double luma = 0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b;
|
||||
m_scrollbar->setHandleColor(luma < 128.0 ? QColor(235, 235, 235)
|
||||
: QColor(45, 45, 45));
|
||||
|
|
@ -317,13 +319,12 @@ void GhosttySurface::paintEvent(QPaintEvent *) {
|
|||
// Unfocused-split dimming: a translucent fill over an inactive pane.
|
||||
// Only split panes (a QSplitter parent) are dimmed, matching GTK.
|
||||
if (!hasFocus() && qobject_cast<QSplitter *>(parentWidget())) {
|
||||
ghostty_config_t cfg = m_owner ? m_owner->config() : nullptr;
|
||||
double opacity = 0.7;
|
||||
configGet(cfg, &opacity, "unfocused-split-opacity");
|
||||
config::get(&opacity, "unfocused-split-opacity");
|
||||
if (opacity < 1.0) {
|
||||
QColor fill(0, 0, 0); // default: dim toward black
|
||||
ghostty_config_color_s c{};
|
||||
if (configGet(cfg, &c, "unfocused-split-fill"))
|
||||
if (config::get(&c, "unfocused-split-fill"))
|
||||
fill = QColor(c.r, c.g, c.b);
|
||||
fill.setAlphaF(1.0 - opacity);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
|
@ -374,14 +375,6 @@ static QLabel *makeOverlayLabel(QWidget *parent) {
|
|||
return label;
|
||||
}
|
||||
|
||||
// Read a string/enum config value (enums arrive as their tag name).
|
||||
static QString cfgString(ghostty_config_t cfg, const char *key) {
|
||||
const char *v = nullptr;
|
||||
if (cfg && ghostty_config_get(cfg, &v, key, qstrlen(key)) && v)
|
||||
return QString::fromUtf8(v);
|
||||
return {};
|
||||
}
|
||||
|
||||
void GhosttySurface::promptTitle(bool tabScope) {
|
||||
bool ok = false;
|
||||
const QString title = QInputDialog::getText(
|
||||
|
|
@ -530,8 +523,7 @@ void GhosttySurface::showResizeOverlay() {
|
|||
if (!m_surface || !m_owner) return;
|
||||
const ghostty_surface_size_s sz = ghostty_surface_size(m_surface);
|
||||
|
||||
ghostty_config_t cfg = m_owner->config();
|
||||
const QString mode = cfgString(cfg, "resize-overlay");
|
||||
const QString mode = config::string("resize-overlay");
|
||||
if (mode == QLatin1String("never")) return;
|
||||
|
||||
if (sz.columns != m_lastCols || sz.rows != m_lastRows) {
|
||||
|
|
@ -556,7 +548,7 @@ void GhosttySurface::showResizeOverlay() {
|
|||
// the hide timer fired on the next event-loop tick and the overlay
|
||||
// vanished the instant it appeared.
|
||||
unsigned long long durCfgMs = 0;
|
||||
const bool durOk = configGet(cfg, &durCfgMs, "resize-overlay-duration");
|
||||
const bool durOk = config::get(&durCfgMs, "resize-overlay-duration");
|
||||
// Clamp before narrowing: a Duration's millisecond value can exceed
|
||||
// INT_MAX, and a wrapped negative int would make QTimer::start()
|
||||
// reject the interval, leaving the overlay stuck on screen.
|
||||
|
|
@ -592,9 +584,7 @@ void GhosttySurface::paintResizeOverlay(QPainter &painter) {
|
|||
const qreal boxH = ts.height() + 2 * padY;
|
||||
|
||||
// resize-overlay-position: center / {top,bottom}-{left,center,right}.
|
||||
const QString pos =
|
||||
m_owner ? cfgString(m_owner->config(), "resize-overlay-position")
|
||||
: QString();
|
||||
const QString pos = config::string("resize-overlay-position");
|
||||
const qreal m = 8;
|
||||
qreal x = (width() - boxW) / 2;
|
||||
qreal y = (height() - boxH) / 2;
|
||||
|
|
|
|||
|
|
@ -7,17 +7,12 @@
|
|||
|
||||
#include <QApplication>
|
||||
#include <QAudioOutput>
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QCursor>
|
||||
#include <QCloseEvent>
|
||||
#include <QCoreApplication>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDesktopServices>
|
||||
#include <QEvent>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
|
|
@ -55,6 +50,7 @@
|
|||
#include <LayerShellQt/window.h>
|
||||
|
||||
#include "app/GhosttyApp.h"
|
||||
#include "config/Config.h"
|
||||
#include "CommandPalette.h"
|
||||
#include "GhosttySurface.h"
|
||||
#include "TabWidget.h"
|
||||
|
|
@ -203,12 +199,12 @@ bool MainWindow::initialize() {
|
|||
// quit-after-last-window-closed: Qt's native "quit on last window"
|
||||
// covers the common (no-delay) case; a configured delay is honored
|
||||
// through the libghostty quit_timer action (see handleQuitTimer).
|
||||
const bool quitAfter = configBool("quit-after-last-window-closed", true);
|
||||
const bool quitAfter = config::boolean("quit-after-last-window-closed", true);
|
||||
// quit-after-last-window-closed-delay is a `?Duration` and Duration
|
||||
// is neither extern nor packed, so libghostty's ghostty_config_get
|
||||
// returns false for it. Read from disk and parse.
|
||||
const uint64_t delayNs = parseDurationNs(
|
||||
configValue(QStringLiteral("quit-after-last-window-closed-delay")), 0);
|
||||
const uint64_t delayNs =
|
||||
config::durationNs("quit-after-last-window-closed-delay", 0);
|
||||
const uint64_t delayMs = delayNs / 1000000ULL;
|
||||
const int delayMsInt = quitAfter
|
||||
? static_cast<int>(std::min(delayMs, uint64_t(INT_MAX)))
|
||||
|
|
@ -223,13 +219,13 @@ bool MainWindow::initialize() {
|
|||
// window-decoration `none` drops the native frame; `auto`/`server`/
|
||||
// `client` keep a decorated window (the compositor picks the side
|
||||
// on Wayland).
|
||||
if (configString("window-decoration") == QLatin1String("none"))
|
||||
if (config::string("window-decoration") == QLatin1String("none"))
|
||||
setWindowFlag(Qt::FramelessWindowHint, true);
|
||||
// fullscreen wins over maximize; its enum is `false` when unset.
|
||||
const QString fullscreen = configString("fullscreen");
|
||||
const QString fullscreen = config::string("fullscreen");
|
||||
if (!fullscreen.isEmpty() && fullscreen != QLatin1String("false"))
|
||||
setWindowState(windowState() | Qt::WindowFullScreen);
|
||||
else if (configBool("maximize", false))
|
||||
else if (config::boolean("maximize", false))
|
||||
setWindowState(windowState() | Qt::WindowMaximized);
|
||||
}
|
||||
|
||||
|
|
@ -268,17 +264,16 @@ MainWindow *MainWindow::newWindow(ghostty_surface_t parent) {
|
|||
w->resize(800, 600);
|
||||
|
||||
// Window position: window-position-x/y are optional (?i16 in
|
||||
// Config.zig). configGet writes the value and returns true when the
|
||||
// optional is present. Both must be set to take effect (matching
|
||||
// Config.zig). config::get writes the value and returns true when
|
||||
// the optional is present. Both must be set to take effect (matching
|
||||
// the Config.zig doc comment). If unset, fall back to a cascade
|
||||
// offset from the previous window so Cmd+N spam doesn't pile every
|
||||
// window at the same origin — macOS does this via
|
||||
// NSWindow.cascadeTopLeft. Wayland compositors typically ignore
|
||||
// window placement requests; this is a hint at most.
|
||||
ghostty_config_t cfg = GhosttyApp::instance().config();
|
||||
int16_t posX = 0, posY = 0;
|
||||
const bool haveX = configGet(cfg, &posX, "window-position-x");
|
||||
const bool haveY = configGet(cfg, &posY, "window-position-y");
|
||||
const bool haveX = config::get(&posX, "window-position-x");
|
||||
const bool haveY = config::get(&posY, "window-position-y");
|
||||
if (haveX && haveY) {
|
||||
w->move(posX, posY);
|
||||
} else {
|
||||
|
|
@ -300,7 +295,7 @@ MainWindow *MainWindow::newWindow(ghostty_surface_t parent) {
|
|||
if (!s_initialWindowConsumed) {
|
||||
s_initialWindowConsumed = true;
|
||||
bool initialWindow = true;
|
||||
configGet(cfg, &initialWindow, "initial-window");
|
||||
config::get(&initialWindow, "initial-window");
|
||||
wantsShow = initialWindow;
|
||||
}
|
||||
if (wantsShow) w->show();
|
||||
|
|
@ -354,7 +349,7 @@ GhosttySurface *MainWindow::newTab(ghostty_surface_t parent) {
|
|||
// window-new-tab-position: place the tab right after the current one,
|
||||
// or append it at the end (the default).
|
||||
int index;
|
||||
if (configString("window-new-tab-position") == QLatin1String("current") &&
|
||||
if (config::string("window-new-tab-position") == QLatin1String("current") &&
|
||||
m_tabs->count() > 0)
|
||||
index = m_tabs->insertTab(m_tabs->currentIndex() + 1, page,
|
||||
QStringLiteral("Ghastty"));
|
||||
|
|
@ -671,7 +666,7 @@ bool MainWindow::confirmCloseSurfaces(
|
|||
// true -> prompt only when libghostty says a process is running
|
||||
// always -> always prompt, even for surfaces with no live process
|
||||
// (libghostty Config.zig: ConfirmCloseSurface enum.)
|
||||
const QString mode = configString("confirm-close-surface");
|
||||
const QString mode = config::string("confirm-close-surface");
|
||||
if (mode == QLatin1String("false")) return true;
|
||||
|
||||
bool needsConfirm = (mode == QLatin1String("always"));
|
||||
|
|
@ -770,9 +765,9 @@ MainWindow *MainWindow::makeQuickTerminal() {
|
|||
}
|
||||
|
||||
// Read quick-terminal-animation-duration (seconds) and convert to ms.
|
||||
static int quickTerminalAnimationMs(ghostty_config_t cfg) {
|
||||
static int quickTerminalAnimationMs() {
|
||||
double secs = 0.2; // matches Config.zig default
|
||||
configGet(cfg, &secs, "quick-terminal-animation-duration");
|
||||
config::get(&secs, "quick-terminal-animation-duration");
|
||||
// Clamp to a sane range so a misconfigured 0 or negative value
|
||||
// doesn't make the window appear/disappear instantly without an
|
||||
// animation, and a very large value doesn't lock the user out.
|
||||
|
|
@ -785,7 +780,7 @@ void MainWindow::animateQuickTerminalIn() {
|
|||
show();
|
||||
raise();
|
||||
activateWindow();
|
||||
const int ms = quickTerminalAnimationMs(GhosttyApp::instance().config());
|
||||
const int ms = quickTerminalAnimationMs();
|
||||
if (ms <= 0) {
|
||||
setWindowOpacity(1.0);
|
||||
return;
|
||||
|
|
@ -803,7 +798,7 @@ void MainWindow::animateQuickTerminalIn() {
|
|||
}
|
||||
|
||||
void MainWindow::animateQuickTerminalOut() {
|
||||
const int ms = quickTerminalAnimationMs(GhosttyApp::instance().config());
|
||||
const int ms = quickTerminalAnimationMs();
|
||||
if (ms <= 0) {
|
||||
hide();
|
||||
return;
|
||||
|
|
@ -845,7 +840,7 @@ void MainWindow::setupLayerShell() {
|
|||
using LSW = LayerShellQt::Window;
|
||||
|
||||
ls->setLayer(LSW::LayerTop);
|
||||
const QString ki = configString("quick-terminal-keyboard-interactivity");
|
||||
const QString ki = config::string("quick-terminal-keyboard-interactivity");
|
||||
ls->setKeyboardInteractivity(
|
||||
ki == QLatin1String("exclusive") ? LSW::KeyboardInteractivityExclusive
|
||||
: ki == QLatin1String("none") ? LSW::KeyboardInteractivityNone
|
||||
|
|
@ -861,7 +856,7 @@ void MainWindow::setupLayerShell() {
|
|||
// Pass null to fall back to the QWindow's screen (LayerShellQt's
|
||||
// documented default when neither setScreen nor
|
||||
// setWantsToBeOnActiveScreen is set).
|
||||
const QString screenMode = configString("quick-terminal-screen");
|
||||
const QString screenMode = config::string("quick-terminal-screen");
|
||||
QScreen *screen = nullptr;
|
||||
if (screenMode == QLatin1String("mouse")) {
|
||||
screen = QGuiApplication::screenAt(QCursor::pos());
|
||||
|
|
@ -872,20 +867,20 @@ void MainWindow::setupLayerShell() {
|
|||
ls->setScreen(screen);
|
||||
if (!screen) screen = handle->screen();
|
||||
|
||||
// quick-terminal-space-behavior (`remain` / `move`): macOS
|
||||
// controls whether the dropdown follows the active Space or pins
|
||||
// to the one it was opened on. Wayland's wlr-layer-shell has no
|
||||
// equivalent — the compositor always renders the surface on the
|
||||
// active workspace (KWin behaviour), which corresponds to `move`.
|
||||
// Achieving `remain` would need a per-workspace pin that no
|
||||
// mainstream compositor exposes; honour by no-op and document.
|
||||
Q_UNUSED(configString("quick-terminal-space-behavior"));
|
||||
// quick-terminal-space-behavior (`remain` / `move`) is intentionally
|
||||
// not read: macOS controls whether the dropdown follows the active
|
||||
// Space or pins to the one it was opened on, but Wayland's
|
||||
// wlr-layer-shell has no equivalent — the compositor always renders
|
||||
// the surface on the active workspace (KWin behaviour), which
|
||||
// corresponds to `move`. Achieving `remain` would need a
|
||||
// per-workspace pin that no mainstream compositor exposes; honour
|
||||
// by no-op and document.
|
||||
|
||||
const QSize scr = screen ? screen->size() : QSize(1920, 1080);
|
||||
|
||||
// quick-terminal-size: primary is the edge-perpendicular extent.
|
||||
ghostty_config_quick_terminal_size_s qsz = {};
|
||||
configGet(GhosttyApp::instance().config(), &qsz, "quick-terminal-size");
|
||||
config::get(&qsz, "quick-terminal-size");
|
||||
const auto toPx = [](const ghostty_quick_terminal_size_s &s, int dim,
|
||||
int fallback) -> int {
|
||||
switch (s.tag) {
|
||||
|
|
@ -898,7 +893,7 @@ void MainWindow::setupLayerShell() {
|
|||
}
|
||||
};
|
||||
|
||||
const QString pos = configString("quick-terminal-position");
|
||||
const QString pos = config::string("quick-terminal-position");
|
||||
LSW::Anchors anchors;
|
||||
QSize size;
|
||||
if (pos == QLatin1String("bottom")) {
|
||||
|
|
@ -935,7 +930,7 @@ void MainWindow::changeEvent(QEvent *e) {
|
|||
// an explicit toggle).
|
||||
if (e->type() == QEvent::ActivationChange && m_quickTerminal &&
|
||||
isVisible() && !isActiveWindow() &&
|
||||
configBool("quick-terminal-autohide", true))
|
||||
config::boolean("quick-terminal-autohide", true))
|
||||
animateQuickTerminalOut();
|
||||
QWidget::changeEvent(e);
|
||||
}
|
||||
|
|
@ -1015,14 +1010,10 @@ void MainWindow::gotoSplit(GhosttySurface *from,
|
|||
// we'll re-zoom the destination once the focus moves. Otherwise
|
||||
// the existing semantics of dropping zoom on navigation apply.
|
||||
//
|
||||
// libghostty serializes packed structs into a c_uint bitfield via
|
||||
// c_get.zig: `ptr.* = @intCast(@as(Backing, @bitCast(value)));`.
|
||||
// SplitPreserveZoom = packed struct { navigation: bool } so bit 0
|
||||
// is `navigation`. Reading into a smaller C struct (sizeof
|
||||
// `bool`==1) under-sized the buffer and corrupted adjacent stack;
|
||||
// read into c_uint and mask the bits.
|
||||
unsigned int pzBits = 0;
|
||||
configGet(GhosttyApp::instance().config(), &pzBits, "split-preserve-zoom");
|
||||
// is `navigation`. config::bitfield handles the c_uint sizing
|
||||
// dance documented there.
|
||||
const unsigned int pzBits = config::bitfield("split-preserve-zoom", 0);
|
||||
const bool preserveZoom = (pzBits & 0x1) != 0 && m_zoomed == from;
|
||||
|
||||
const auto centerOf = [](GhosttySurface *s) {
|
||||
|
|
@ -1185,10 +1176,8 @@ void MainWindow::ringBell(GhosttySurface *surface) {
|
|||
// dropping the field), use BellAttention as a sane minimum fallback.
|
||||
// If config-get succeeds with features=0, the user explicitly opted
|
||||
// out of every bell feature and we honor that.
|
||||
unsigned int features = 0;
|
||||
if (!configGet(GhosttyApp::instance().config(), &features, "bell-features")) {
|
||||
features = BellAttention;
|
||||
}
|
||||
const unsigned int features =
|
||||
config::bitfield("bell-features", BellAttention);
|
||||
if (features & BellAttention) QApplication::alert(this);
|
||||
if (features & BellSystem) QApplication::beep();
|
||||
if (features & BellAudio) playBellAudio();
|
||||
|
|
@ -1251,12 +1240,12 @@ void MainWindow::refreshChrome() {
|
|||
// Refresh app-scoped state. quit-after-last-window-closed[-delay]
|
||||
// can change the delay or the quitOnLastWindowClosed strategy at
|
||||
// runtime; mirrors the calculation in initialize().
|
||||
if (ghostty_config_t cfg = GhosttyApp::instance().config()) {
|
||||
if (GhosttyApp::instance().config()) {
|
||||
bool quitAfter = true;
|
||||
configGet(cfg, &quitAfter, "quit-after-last-window-closed");
|
||||
config::get(&quitAfter, "quit-after-last-window-closed");
|
||||
// Same Duration-decode workaround as initialize().
|
||||
const uint64_t delayNs = parseDurationNs(
|
||||
configValue(QStringLiteral("quit-after-last-window-closed-delay")), 0);
|
||||
const uint64_t delayNs =
|
||||
config::durationNs("quit-after-last-window-closed-delay", 0);
|
||||
const uint64_t delayMs = delayNs / 1000000ULL;
|
||||
const int delayMsInt = quitAfter
|
||||
? static_cast<int>(std::min(delayMs, uint64_t(INT_MAX)))
|
||||
|
|
@ -1279,7 +1268,7 @@ void MainWindow::refreshChrome() {
|
|||
// Toggling Qt::FramelessWindowHint hides+reshows the window, so
|
||||
// gate on a real change.
|
||||
const bool wantFrameless =
|
||||
w->configString("window-decoration") == QLatin1String("none");
|
||||
config::string("window-decoration") == QLatin1String("none");
|
||||
const bool isFrameless =
|
||||
w->windowFlags().testFlag(Qt::FramelessWindowHint);
|
||||
if (wantFrameless != isFrameless) {
|
||||
|
|
@ -1294,9 +1283,9 @@ void MainWindow::refreshChrome() {
|
|||
// fullscreen / maximize: `fullscreen=true` wins over `maximize`.
|
||||
// Setting back to a non-fullscreen window goes through showNormal
|
||||
// first so the WM lets us out of fullscreen cleanly.
|
||||
const QString fs = w->configString("fullscreen");
|
||||
const QString fs = config::string("fullscreen");
|
||||
const bool wantFullscreen = !fs.isEmpty() && fs != QLatin1String("false");
|
||||
const bool wantMax = w->configBool("maximize", false);
|
||||
const bool wantMax = config::boolean("maximize", false);
|
||||
if (wantFullscreen) {
|
||||
if (!w->isFullScreen()) w->showFullScreen();
|
||||
} else if (w->isFullScreen()) {
|
||||
|
|
@ -1343,34 +1332,17 @@ void MainWindow::reloadConfigGlobal() {
|
|||
// bit 1 = config-reload. The clipboard-copy bit is read for
|
||||
// forward compatibility — Qt doesn't currently post a copy
|
||||
// toast, but a future one will pick up the same gate.
|
||||
unsigned int notifBits = 0;
|
||||
const bool notifOk = configGet(GhosttyApp::instance().config(), ¬ifBits, "app-notifications");
|
||||
// configGet failure → defaults (both bits set) so the feature
|
||||
// still works as documented.
|
||||
if (!notifOk) notifBits = 0x3;
|
||||
// config::bitfield failure → defaults (both bits set) so the
|
||||
// feature still works as documented.
|
||||
const unsigned int notifBits = config::bitfield("app-notifications", 0x3);
|
||||
const bool wantConfigReload = (notifBits & 0x2) != 0;
|
||||
if (wantConfigReload)
|
||||
postNotification(QStringLiteral("Ghostty"),
|
||||
QStringLiteral("Configuration reloaded."));
|
||||
}
|
||||
|
||||
QString MainWindow::configString(const char *key) const {
|
||||
ghostty_config_t cfg = GhosttyApp::instance().config();
|
||||
const char *value = nullptr;
|
||||
if (!cfg || !ghostty_config_get(cfg, &value, key, qstrlen(key)) || !value)
|
||||
return {};
|
||||
return QString::fromUtf8(value);
|
||||
}
|
||||
|
||||
bool MainWindow::configBool(const char *key, bool fallback) const {
|
||||
bool value = fallback; // ghostty_config_get leaves it untouched if absent
|
||||
if (ghostty_config_t cfg = GhosttyApp::instance().config())
|
||||
ghostty_config_get(cfg, &value, key, qstrlen(key));
|
||||
return value;
|
||||
}
|
||||
|
||||
bool MainWindow::focusFollowsMouse() const {
|
||||
return configBool("focus-follows-mouse", false);
|
||||
return config::boolean("focus-follows-mouse", false);
|
||||
}
|
||||
|
||||
// Bring this window forward and focus the surface inside it. Mirrors
|
||||
|
|
@ -1466,7 +1438,7 @@ void MainWindow::setSizeLimits(uint32_t minW, uint32_t minH, uint32_t maxW,
|
|||
// strictly a bonus.
|
||||
void MainWindow::setCellSize(uint32_t w, uint32_t h) {
|
||||
m_cellSize = QSize(int(w), int(h));
|
||||
if (configBool("window-step-resize", false))
|
||||
if (config::boolean("window-step-resize", false))
|
||||
setSizeIncrement(int(w), int(h));
|
||||
else
|
||||
setSizeIncrement(0, 0); // back to pixel-precise
|
||||
|
|
@ -1618,7 +1590,7 @@ void MainWindow::redoLastClose() {
|
|||
void MainWindow::applyWindowConfig() {
|
||||
// window-show-tab-bar: always shown / auto-hidden with a lone tab /
|
||||
// never shown.
|
||||
const QString tabBar = configString("window-show-tab-bar");
|
||||
const QString tabBar = config::string("window-show-tab-bar");
|
||||
if (tabBar == QLatin1String("never")) {
|
||||
m_tabs->setTabBarAutoHide(false);
|
||||
m_tabs->tabBar()->hide();
|
||||
|
|
@ -1636,13 +1608,10 @@ void MainWindow::applyWindowConfig() {
|
|||
// doesn't re-scan the on-disk config on every bell. Refreshed on
|
||||
// each applyWindowConfig (i.e. at init and on reload).
|
||||
{
|
||||
QString path = configValue(QStringLiteral("bell-audio-path"));
|
||||
if (path.startsWith(QLatin1String("~/")))
|
||||
path = QDir::homePath() + path.mid(1);
|
||||
m_bellAudioPath = path;
|
||||
m_bellAudioPath = config::expandedPath("bell-audio-path");
|
||||
bool volOk = false;
|
||||
const double v =
|
||||
configValue(QStringLiteral("bell-audio-volume")).toDouble(&volOk);
|
||||
config::diskValue("bell-audio-volume").toDouble(&volOk);
|
||||
m_bellAudioVolume = volOk ? v : 0.5;
|
||||
}
|
||||
|
||||
|
|
@ -1650,7 +1619,7 @@ void MainWindow::applyWindowConfig() {
|
|||
// title via Qt's window-title system font is harder to override
|
||||
// portably; the tab bar is what users actually look at). Empty /
|
||||
// unset reverts to the application font.
|
||||
const QString titleFamily = configValue(QStringLiteral("window-title-font-family"));
|
||||
const QString titleFamily = config::diskValue("window-title-font-family");
|
||||
if (m_tabs && m_tabs->tabBar()) {
|
||||
QFont tf = QApplication::font();
|
||||
if (!titleFamily.isEmpty()) tf.setFamily(titleFamily);
|
||||
|
|
@ -1662,7 +1631,7 @@ void MainWindow::applyWindowConfig() {
|
|||
// leaves Qt's default. Applied via setStyleSheet on this window's
|
||||
// QSplitter children since splitters can be added/removed at any
|
||||
// time, walk them on each apply.
|
||||
const QString divider = configValue(QStringLiteral("split-divider-color"));
|
||||
const QString divider = config::diskValue("split-divider-color");
|
||||
const QString splitterCss = divider.isEmpty()
|
||||
? QString()
|
||||
: QStringLiteral("QSplitter::handle { background-color: %1; }")
|
||||
|
|
@ -1682,7 +1651,7 @@ void MainWindow::applyWindowConfig() {
|
|||
// CMake doesn't compile against older Qt). The setColorScheme
|
||||
// hint propagates to chrome (tab bar, dialogs); the terminal
|
||||
// itself honours its own theme via libghostty.
|
||||
const QString theme = configString("window-theme");
|
||||
const QString theme = config::string("window-theme");
|
||||
Qt::ColorScheme scheme = Qt::ColorScheme::Unknown;
|
||||
if (theme == QLatin1String("dark")) {
|
||||
scheme = Qt::ColorScheme::Dark;
|
||||
|
|
@ -1690,7 +1659,7 @@ void MainWindow::applyWindowConfig() {
|
|||
scheme = Qt::ColorScheme::Light;
|
||||
} else if (theme == QLatin1String("ghostty")) {
|
||||
ghostty_config_color_s bg{};
|
||||
if (configGet(GhosttyApp::instance().config(), &bg, "background")) {
|
||||
if (config::get(&bg, "background")) {
|
||||
const double luma = 0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b;
|
||||
scheme = luma < 128.0 ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light;
|
||||
}
|
||||
|
|
@ -1708,7 +1677,7 @@ void MainWindow::applyBlur() {
|
|||
// macOS-only negatives) means off, a positive radius means on. KWin
|
||||
// uses its own configured radius, so only on/off matters here.
|
||||
short blur = 0;
|
||||
configGet(GhosttyApp::instance().config(), &blur, "background-blur");
|
||||
config::get(&blur, "background-blur");
|
||||
applyWindowBlur(this, blur > 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,12 +158,6 @@ public:
|
|||
QSize defaultWindowSize() const { return m_defaultWindowSize; }
|
||||
void setDefaultWindowSize(QSize s) { m_defaultWindowSize = s; }
|
||||
|
||||
// Typed wrappers over ghostty_config_get. configString also serves
|
||||
// enum keys — libghostty returns an enum as its tag name string.
|
||||
// Public so handler files can read config without friending.
|
||||
QString configString(const char *key) const;
|
||||
bool configBool(const char *key, bool fallback) const;
|
||||
|
||||
// App-scoped reload entry point and chrome refresh. Both are
|
||||
// called from actions::dispatch (RELOAD_CONFIG, CONFIG_CHANGE).
|
||||
static void reloadConfigGlobal();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
#include "Util.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QStringList>
|
||||
#include <QStringLiteral>
|
||||
#include <QVariantMap>
|
||||
|
|
@ -40,78 +37,6 @@ QString triggerKeyName(const ghostty_input_trigger_s &t) {
|
|||
}
|
||||
}
|
||||
|
||||
uint64_t parseDurationNs(const QString &s, uint64_t fallback) {
|
||||
if (s.isEmpty()) return fallback;
|
||||
// Order matters: longer units first so `ms` is matched before `s`,
|
||||
// `us` before `s`, etc. Mirrors Config.zig's units array.
|
||||
static constexpr struct { const char *name; uint64_t factor; } kUnits[] = {
|
||||
{"ns", 1ULL},
|
||||
{"us", 1000ULL},
|
||||
{"µs", 1000ULL},
|
||||
{"ms", 1000000ULL},
|
||||
{"s", 1000000000ULL},
|
||||
{"m", 60ULL * 1000000000ULL},
|
||||
{"h", 3600ULL * 1000000000ULL},
|
||||
{"d", 86400ULL * 1000000000ULL},
|
||||
{"w", 7ULL * 86400ULL * 1000000000ULL},
|
||||
{"y", 365ULL * 86400ULL * 1000000000ULL},
|
||||
};
|
||||
uint64_t total = 0;
|
||||
int i = 0;
|
||||
const int n = s.size();
|
||||
bool anyMatched = false;
|
||||
while (i < n) {
|
||||
while (i < n && s.at(i).isSpace()) ++i;
|
||||
if (i >= n) break;
|
||||
int start = i;
|
||||
while (i < n && s.at(i).isDigit()) ++i;
|
||||
if (i == start) return fallback; // expected a number
|
||||
bool ok = false;
|
||||
const uint64_t value = s.mid(start, i - start).toULongLong(&ok);
|
||||
if (!ok) return fallback;
|
||||
while (i < n && s.at(i).isSpace()) ++i;
|
||||
// Match the longest unit prefix at i. unitLen is counted in
|
||||
// QChar (UTF-16 code unit) length, NOT byte length, because `i`
|
||||
// and `s.size()` are QChar-counted. `µs` is 3 UTF-8 bytes but
|
||||
// 2 QChars (µ + s); using qstrlen here over-advanced past the
|
||||
// input.
|
||||
const QString rest = s.mid(i);
|
||||
uint64_t factor = 0;
|
||||
int unitLen = 0;
|
||||
for (const auto &u : kUnits) {
|
||||
const QString unit = QString::fromUtf8(u.name);
|
||||
const int ulen = unit.size();
|
||||
if (rest.startsWith(unit) && ulen > unitLen) {
|
||||
factor = u.factor;
|
||||
unitLen = ulen;
|
||||
}
|
||||
}
|
||||
if (unitLen == 0) return fallback;
|
||||
total += value * factor;
|
||||
i += unitLen;
|
||||
anyMatched = true;
|
||||
}
|
||||
return anyMatched ? total : fallback;
|
||||
}
|
||||
|
||||
QString configValue(const QString &key) {
|
||||
QString dir = qEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
if (dir.isEmpty()) dir = QDir::homePath() + QStringLiteral("/.config");
|
||||
|
||||
QFile f(dir + QStringLiteral("/ghostty/config"));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||
|
||||
const QByteArray wanted = key.toUtf8();
|
||||
QString value;
|
||||
while (!f.atEnd()) {
|
||||
const QByteArray line = f.readLine().trimmed();
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq < 0 || line.left(eq).trimmed() != wanted) continue;
|
||||
value = QString::fromUtf8(line.mid(eq + 1).trimmed());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void postNotification(const QString &title, const QString &body) {
|
||||
QDBusMessage msg = QDBusMessage::createMethodCall(
|
||||
QStringLiteral("org.freedesktop.Notifications"),
|
||||
|
|
|
|||
|
|
@ -36,18 +36,6 @@ inline ghostty_input_mods_e translateMods(Qt::KeyboardModifiers m) {
|
|||
// (CATCH_ALL, an unmapped physical key, etc.).
|
||||
QString triggerKeyName(const ghostty_input_trigger_s &t);
|
||||
|
||||
// Parse a libghostty duration string ("750ms", "1s500us", "2h", ...)
|
||||
// into nanoseconds. Returns `fallback` if parsing fails or the input
|
||||
// is empty. libghostty exposes Duration via ghostty_config_get as a
|
||||
// non-extern non-packed struct, which c_get silently rejects; we
|
||||
// fall back to scanning the config file text.
|
||||
uint64_t parseDurationNs(const QString &s, uint64_t fallback);
|
||||
|
||||
// Scan the primary Ghostty config file for `key = value`, returning
|
||||
// the last matching value (empty if absent). For keys not cleanly
|
||||
// exposed by ghostty_config_get (Duration, paths, ...).
|
||||
QString configValue(const QString &key);
|
||||
|
||||
// Post a desktop notification via the freedesktop D-Bus service.
|
||||
// Fire-and-forget; no return code (notifications are best-effort).
|
||||
void postNotification(const QString &title, const QString &body);
|
||||
|
|
@ -58,17 +46,6 @@ void postNotification(const QString &title, const QString &body);
|
|||
// as "…".
|
||||
QString formatTrigger(const ghostty_input_trigger_s &t);
|
||||
|
||||
// Wrapper around ghostty_config_get that infers the value's length
|
||||
// from a string literal, so call sites stop repeating qstrlen().
|
||||
//
|
||||
// The template only binds to char-array references (string literals);
|
||||
// passing a `const char*` is intentionally a compile error — runtime-
|
||||
// length keys must call ghostty_config_get directly with qstrlen.
|
||||
template <typename T, size_t N>
|
||||
inline bool configGet(ghostty_config_t cfg, T *out, const char (&key)[N]) {
|
||||
return cfg && ghostty_config_get(cfg, out, key, N - 1);
|
||||
}
|
||||
|
||||
// Queue `f` on `target`'s thread, but only if `target` is still alive
|
||||
// when the slot runs (Qt cancels queued slots whose receiver was
|
||||
// deleted). Cross-captured pointers must be wrapped in QPointer
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <Qt>
|
||||
|
||||
#include "../app/GhosttyApp.h"
|
||||
#include "../config/Config.h"
|
||||
#include "../GhosttySurface.h"
|
||||
#include "../MainWindow.h"
|
||||
#include "../Util.h"
|
||||
|
|
@ -86,9 +87,13 @@ bool handleChrome(const Context &ctx, const ghostty_action_s &action) {
|
|||
if (action.action.color_change.kind ==
|
||||
GHOSTTY_ACTION_COLOR_KIND_BACKGROUND) {
|
||||
const ghostty_action_color_change_s c = action.action.color_change;
|
||||
post(qApp, [winp, c]() {
|
||||
if (!winp) return;
|
||||
if (winp->configString("window-theme") != QLatin1String("ghostty"))
|
||||
// No window capture: setColorScheme and config::string are
|
||||
// both process-scoped, so the originating window's lifetime
|
||||
// doesn't affect this slot's correctness — and gating on it
|
||||
// would silently drop chrome flips for the *other* windows
|
||||
// that are still alive.
|
||||
post(qApp, [c]() {
|
||||
if (config::string("window-theme") != QLatin1String("ghostty"))
|
||||
return;
|
||||
const double luma = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
|
||||
QGuiApplication::styleHints()->setColorScheme(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QVariantMap>
|
||||
|
||||
#include "../app/GhosttyApp.h"
|
||||
#include "../config/Config.h"
|
||||
#include "../GhosttySurface.h"
|
||||
#include "../MainWindow.h"
|
||||
#include "../Util.h"
|
||||
|
|
@ -147,7 +148,7 @@ bool handleSystem(const Context &ctx, const ghostty_action_s &action) {
|
|||
// the never/unfocused gate (matches GTK's setup-menu).
|
||||
const bool armed = srcp->consumeCommandNotify();
|
||||
// notify-on-command-finish enum (string).
|
||||
const QString mode = winp->configString("notify-on-command-finish");
|
||||
const QString mode = config::string("notify-on-command-finish");
|
||||
bool fire = armed;
|
||||
if (!fire) {
|
||||
if (mode == QLatin1String("always")) fire = true;
|
||||
|
|
@ -158,22 +159,16 @@ bool handleSystem(const Context &ctx, const ghostty_action_s &action) {
|
|||
// -after Duration; default 5s. Duration isn't decodable via
|
||||
// ghostty_config_get (non-extern non-packed struct), so parse
|
||||
// from the on-disk config.
|
||||
const uint64_t afterNs = parseDurationNs(
|
||||
configValue(QStringLiteral("notify-on-command-finish-after")),
|
||||
const uint64_t afterNs = config::durationNs(
|
||||
"notify-on-command-finish-after",
|
||||
5ULL * 1000 * 1000 * 1000);
|
||||
if (duration < afterNs) return;
|
||||
// -action: NotifyOnCommandFinishAction = packed struct
|
||||
// { bell: bool = true, notify: bool = false }. Serialized
|
||||
// as c_uint via c_get.zig; bit 0 = bell, bit 1 = notify.
|
||||
// A zero-init reads as no-bell-no-notify, which matches the
|
||||
// "configGet failed; nothing to do" semantics.
|
||||
unsigned int actBits = 0;
|
||||
const bool actOk = configGet(
|
||||
GhosttyApp::instance().config(), &actBits,
|
||||
"notify-on-command-finish-action");
|
||||
// configGet failure → fall back to the documented defaults
|
||||
// (bell=true, notify=false) so the feature still works.
|
||||
if (!actOk) actBits = 0x1;
|
||||
// { bell: bool = true, notify: bool = false }. Bit 0 = bell,
|
||||
// bit 1 = notify. Fallback (config::bitfield read failure)
|
||||
// is bell=true so the feature still works.
|
||||
const unsigned int actBits =
|
||||
config::bitfield("notify-on-command-finish-action", 0x1);
|
||||
const bool actBell = (actBits & 0x1) != 0;
|
||||
const bool actNotify = (actBits & 0x2) != 0;
|
||||
if (actBell) winp->ringBell(srcp);
|
||||
|
|
@ -193,13 +188,9 @@ bool handleSystem(const Context &ctx, const ghostty_action_s &action) {
|
|||
|
||||
case GHOSTTY_ACTION_PROGRESS_REPORT: {
|
||||
// Honor `progress-style`: when false, OSC 9;4 progress
|
||||
// sequences are silently ignored (no taskbar entry). It is a
|
||||
// *bool* in Config.zig — it MUST be read with configBool.
|
||||
// configString would hand ghostty_config_get a `const char**`;
|
||||
// the 1-byte bool write leaves a `0x1` pointer that
|
||||
// QString::fromUtf8 then dereferences and crashes on (e.g.
|
||||
// when Claude emits progress).
|
||||
if (win && !win->configBool("progress-style", true)) return true;
|
||||
// sequences are silently ignored (no taskbar entry). The gate
|
||||
// is process-wide (a config setting, not a per-window setting).
|
||||
if (!config::boolean("progress-style", true)) return true;
|
||||
const ghostty_action_progress_report_s p = action.action.progress_report;
|
||||
const ghostty_action_progress_report_state_e state = p.state;
|
||||
const double fraction = p.progress >= 0 ? p.progress / 100.0 : 0.0;
|
||||
|
|
@ -260,8 +251,7 @@ bool handleSystem(const Context &ctx, const ghostty_action_s &action) {
|
|||
// abnormal threshold (default 250ms). Banner = "the process
|
||||
// died unexpectedly," not "the process exited."
|
||||
uint32_t threshold = 250;
|
||||
configGet(GhosttyApp::instance().config(), &threshold,
|
||||
"abnormal-command-exit-runtime");
|
||||
config::get(&threshold, "abnormal-command-exit-runtime");
|
||||
if (ce.runtime_ms < threshold) return true;
|
||||
const int code = static_cast<int>(ce.exit_code);
|
||||
post(src, [srcp, code]() {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@
|
|||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QMetaObject>
|
||||
|
|
@ -18,35 +16,13 @@
|
|||
#include <QTimer>
|
||||
|
||||
#include "../actions/ActionDispatcher.h"
|
||||
#include "../config/Config.h"
|
||||
#include "../GhosttySurface.h"
|
||||
#include "../MainWindow.h"
|
||||
|
||||
// Process-wide libghostty state and the runtime callbacks libghostty
|
||||
// hands back. onAction stays on MainWindow until phase 2 introduces
|
||||
// the ActionDispatcher; everything else is here. The undo/redo stack
|
||||
// stays on MainWindow as well.
|
||||
|
||||
// Whether the Ghostty config enables a custom shader. libghostty does
|
||||
// not expose this through ghostty_config_get (`custom-shader` is a
|
||||
// repeatable path), so scan the primary config file directly. Same
|
||||
// implementation MainWindow had before — moved here because
|
||||
// needsPremultiply is now an app-level fact.
|
||||
static bool configHasCustomShader() {
|
||||
QString dir = qEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
if (dir.isEmpty()) dir = QDir::homePath() + QStringLiteral("/.config");
|
||||
|
||||
QFile f(dir + QStringLiteral("/ghostty/config"));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return false;
|
||||
|
||||
while (!f.atEnd()) {
|
||||
const QByteArray line = f.readLine().trimmed();
|
||||
if (!line.startsWith("custom-shader")) continue;
|
||||
// Require a non-empty value: `custom-shader =` alone clears it.
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq >= 0 && !line.mid(eq + 1).trimmed().isEmpty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// hands back. Action dispatch is handled by actions::dispatch (see
|
||||
// qt/src/actions/); the undo/redo stack stays on MainWindow.
|
||||
|
||||
GhosttyApp &GhosttyApp::instance() {
|
||||
// Static-local singleton: deterministic destruction at process exit
|
||||
|
|
@ -81,7 +57,7 @@ bool GhosttyApp::ensureInitialized() {
|
|||
ghostty_config_load_cli_args(m_config);
|
||||
ghostty_config_load_recursive_files(m_config);
|
||||
ghostty_config_finalize(m_config);
|
||||
m_needsPremultiply = configHasCustomShader();
|
||||
m_needsPremultiply = config::hasCustomShader();
|
||||
|
||||
ghostty_runtime_config_s rt = {};
|
||||
// No app userdata: actions are routed to a window via their target
|
||||
|
|
@ -115,7 +91,7 @@ void GhosttyApp::replaceConfig(ghostty_config_t new_config) {
|
|||
// the queue has adopted the new config and the old is safe to free.
|
||||
if (m_config && m_config != new_config) ghostty_config_free(m_config);
|
||||
m_config = new_config;
|
||||
m_needsPremultiply = configHasCustomShader();
|
||||
m_needsPremultiply = config::hasCustomShader();
|
||||
}
|
||||
|
||||
void GhosttyApp::registerWindow(MainWindow *w) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
#include "Config.h"
|
||||
|
||||
#include <climits>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QStringLiteral>
|
||||
|
||||
#include "../app/GhosttyApp.h"
|
||||
|
||||
namespace config {
|
||||
|
||||
ghostty_config_t handle() {
|
||||
return GhosttyApp::instance().config();
|
||||
}
|
||||
|
||||
QString string(const char *key) {
|
||||
ghostty_config_t cfg = handle();
|
||||
const char *value = nullptr;
|
||||
if (!cfg || !ghostty_config_get(cfg, &value, key, qstrlen(key)) || !value)
|
||||
return {};
|
||||
return QString::fromUtf8(value);
|
||||
}
|
||||
|
||||
bool boolean(const char *key, bool fallback) {
|
||||
bool value = fallback; // ghostty_config_get leaves it untouched if absent
|
||||
if (ghostty_config_t cfg = handle())
|
||||
ghostty_config_get(cfg, &value, key, qstrlen(key));
|
||||
return value;
|
||||
}
|
||||
|
||||
QString diskValue(const char *key) {
|
||||
QString dir = qEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
if (dir.isEmpty()) dir = QDir::homePath() + QStringLiteral("/.config");
|
||||
|
||||
QFile f(dir + QStringLiteral("/ghostty/config"));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||
|
||||
const QByteArray wanted(key);
|
||||
QString value;
|
||||
while (!f.atEnd()) {
|
||||
const QByteArray line = f.readLine().trimmed();
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq < 0 || line.left(eq).trimmed() != wanted) continue;
|
||||
value = QString::fromUtf8(line.mid(eq + 1).trimmed());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Parse a libghostty duration string into nanoseconds. The format is
|
||||
// concatenated `<n><unit>` segments per Config.zig's Duration.parseCLI:
|
||||
// y w d h m s ms µs us ns
|
||||
// Each segment is added to the total. Returns the supplied fallback
|
||||
// when parsing fails, when the input is empty, or when the running
|
||||
// total would overflow uint64 (Config.zig rejects this; we mirror).
|
||||
static uint64_t parseDurationNs(const QString &s, uint64_t fallback) {
|
||||
if (s.isEmpty()) return fallback;
|
||||
// kUnits mirrors Config.zig's units array; the longest-prefix match
|
||||
// at the matching site below makes table order semantically
|
||||
// irrelevant (kept aligned with Zig for diffability).
|
||||
static constexpr struct { const char *name; uint64_t factor; } kUnits[] = {
|
||||
{"ns", 1ULL},
|
||||
{"us", 1000ULL},
|
||||
{"µs", 1000ULL},
|
||||
{"ms", 1000000ULL},
|
||||
{"s", 1000000000ULL},
|
||||
{"m", 60ULL * 1000000000ULL},
|
||||
{"h", 3600ULL * 1000000000ULL},
|
||||
{"d", 86400ULL * 1000000000ULL},
|
||||
{"w", 7ULL * 86400ULL * 1000000000ULL},
|
||||
{"y", 365ULL * 86400ULL * 1000000000ULL},
|
||||
};
|
||||
uint64_t total = 0;
|
||||
int i = 0;
|
||||
const int n = s.size();
|
||||
bool anyMatched = false;
|
||||
while (i < n) {
|
||||
while (i < n && s.at(i).isSpace()) ++i;
|
||||
if (i >= n) break;
|
||||
int start = i;
|
||||
while (i < n && s.at(i).isDigit()) ++i;
|
||||
if (i == start) return fallback; // expected a number
|
||||
bool ok = false;
|
||||
const uint64_t value = s.mid(start, i - start).toULongLong(&ok);
|
||||
if (!ok) return fallback;
|
||||
while (i < n && s.at(i).isSpace()) ++i;
|
||||
// Match the longest unit prefix at i. unitLen is counted in
|
||||
// QChar (UTF-16 code unit) length, NOT byte length, because `i`
|
||||
// and `s.size()` are QChar-counted. `µs` is 3 UTF-8 bytes but
|
||||
// 2 QChars (µ + s); using qstrlen here over-advanced past the
|
||||
// input.
|
||||
const QString rest = s.mid(i);
|
||||
uint64_t factor = 0;
|
||||
int unitLen = 0;
|
||||
for (const auto &u : kUnits) {
|
||||
const QString unit = QString::fromUtf8(u.name);
|
||||
const int ulen = unit.size();
|
||||
if (rest.startsWith(unit) && ulen > unitLen) {
|
||||
factor = u.factor;
|
||||
unitLen = ulen;
|
||||
}
|
||||
}
|
||||
if (unitLen == 0) return fallback;
|
||||
// Reject overflow on multiply or running-sum: a typo like
|
||||
// `1000000000y` would otherwise wrap into a small bogus value
|
||||
// that callers treat as a real (tiny) duration.
|
||||
if (factor && value > ULLONG_MAX / factor) return fallback;
|
||||
const uint64_t segment = value * factor;
|
||||
if (segment > ULLONG_MAX - total) return fallback;
|
||||
total += segment;
|
||||
i += unitLen;
|
||||
anyMatched = true;
|
||||
}
|
||||
return anyMatched ? total : fallback;
|
||||
}
|
||||
|
||||
uint64_t durationNs(const char *key, uint64_t fallbackNs) {
|
||||
return parseDurationNs(diskValue(key), fallbackNs);
|
||||
}
|
||||
|
||||
unsigned int bitfield(const char *key, unsigned int fallbackBits) {
|
||||
unsigned int bits = 0;
|
||||
ghostty_config_t cfg = handle();
|
||||
if (cfg && ghostty_config_get(cfg, &bits, key, qstrlen(key))) return bits;
|
||||
return fallbackBits;
|
||||
}
|
||||
|
||||
QString expandedPath(const char *key) {
|
||||
QString p = diskValue(key);
|
||||
if (p.startsWith(QLatin1String("~/"))) p = QDir::homePath() + p.mid(1);
|
||||
return p;
|
||||
}
|
||||
|
||||
bool hasCustomShader() {
|
||||
// libghostty does not expose this through ghostty_config_get
|
||||
// (`custom-shader` is a repeatable path), so scan the on-disk
|
||||
// config text. diskValue does the exact-key match (so
|
||||
// `custom-shader-animation = …` is not mistaken for our key) and
|
||||
// last-write-wins (so `custom-shader =` clears any earlier
|
||||
// assignment, matching libghostty's repeating-key semantics).
|
||||
return !diskValue("custom-shader").isEmpty();
|
||||
}
|
||||
|
||||
} // namespace config
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "ghostty.h"
|
||||
|
||||
// Typed accessors over the live libghostty config held by
|
||||
// GhosttyApp::instance().config(). Every call here resolves the
|
||||
// singleton's config pointer at access time, so reads stay coherent
|
||||
// after a config reload (replaceConfig swaps the pointer in place).
|
||||
//
|
||||
// Layout: this header is include-anywhere (depends only on QString
|
||||
// and ghostty.h). The implementations live in Config.cpp; the
|
||||
// templated string-literal `get()` stays inline so callers don't pay
|
||||
// a function-call hop on each config read.
|
||||
namespace config {
|
||||
|
||||
// The live ghostty_config_t. Returns nullptr before the singleton has
|
||||
// finished ensureInitialized — callers that read config during early
|
||||
// startup (before the first MainWindow::initialize) must check.
|
||||
ghostty_config_t handle();
|
||||
|
||||
// Read a string-valued config key (or an enum, which libghostty
|
||||
// returns as its tag-name string). Empty if absent or the call
|
||||
// fails.
|
||||
QString string(const char *key);
|
||||
|
||||
// Read a bool-valued config key. Returns `fallback` when the key is
|
||||
// absent or the call fails. Note: libghostty's bool config keys are
|
||||
// strict bools, NOT packed bitfields — see bitfield<>() for those.
|
||||
bool boolean(const char *key, bool fallback);
|
||||
|
||||
// Parse a duration config key as nanoseconds via the on-disk
|
||||
// fallback. Use this for `?Duration` (optional) keys: c_get.zig
|
||||
// returns false for a null optional, so the disk text is the only
|
||||
// way to recover the configured value. Non-optional `Duration` keys
|
||||
// surface through ghostty_config_get directly (it returns the value
|
||||
// in *milliseconds*, per Duration.cval()) and should use config::get
|
||||
// with `unsigned long long` and a manual ms→ns multiplication, NOT
|
||||
// this wrapper, to avoid a redundant disk re-scan on every read.
|
||||
// Returns `fallbackNs` on parse failure or absent key.
|
||||
uint64_t durationNs(const char *key, uint64_t fallbackNs);
|
||||
|
||||
// Scan the user's primary on-disk config file for `key = value`
|
||||
// directly. Used for keys ghostty_config_get can't decode (Duration,
|
||||
// repeating paths). Returns the last matching value, or empty.
|
||||
QString diskValue(const char *key);
|
||||
|
||||
// True if the live config has any custom-shader entry. Drives
|
||||
// GhosttySurface's premultiply pass — `custom-shader` is a
|
||||
// repeatable path that ghostty_config_get can't expose, so we scan
|
||||
// the on-disk config text directly.
|
||||
bool hasCustomShader();
|
||||
|
||||
// Read a packed-bitfield config key. libghostty serializes packed
|
||||
// structs as a c_uint via c_get.zig (`ptr.* = @intCast(@as(Backing,
|
||||
// @bitCast(value)))`), so the returned bits are flag-indexed by the
|
||||
// struct field order. Reading into a smaller buffer (e.g. a `bool`
|
||||
// for a one-field packed struct) under-sizes the write and corrupts
|
||||
// adjacent stack — always go through this. Returns `fallbackBits`
|
||||
// when ghostty_config_get fails.
|
||||
unsigned int bitfield(const char *key, unsigned int fallbackBits);
|
||||
|
||||
// Read a path-valued disk config and expand a leading `~/` to the
|
||||
// user's home directory. Returns empty when the key is absent.
|
||||
// Path-valued keys are read off-disk (libghostty doesn't surface
|
||||
// them through ghostty_config_get) so this is just diskValue() with
|
||||
// a tilde-expansion pass.
|
||||
QString expandedPath(const char *key);
|
||||
|
||||
// Wrapper around ghostty_config_get that infers the value's length
|
||||
// from a string literal so call sites stop repeating qstrlen(). The
|
||||
// template only binds to char-array references (string literals);
|
||||
// passing a `const char*` is intentionally a compile error —
|
||||
// runtime-length keys must call ghostty_config_get directly.
|
||||
//
|
||||
// `out` must point to the type ghostty.h documents for the key
|
||||
// (bool* for bool keys, ghostty_config_color_s* for colors, etc.).
|
||||
// Returns false when the key is absent or the underlying call
|
||||
// fails.
|
||||
template <typename T, size_t N>
|
||||
inline bool get(T *out, const char (&key)[N]) {
|
||||
static_assert(N > 1, "config::get requires a non-empty key literal");
|
||||
ghostty_config_t cfg = handle();
|
||||
return cfg && ghostty_config_get(cfg, out, key, N - 1);
|
||||
}
|
||||
|
||||
} // namespace config
|
||||
Loading…
Reference in New Issue