qt: phase 3.0 — extract config:: namespace for typed config access
Pulls the on-disk config helpers (configValue/parseDurationNs) and the ghostty_config_get wrappers (configGet template) out of Util into a new config:: namespace under qt/src/config/. Every accessor resolves GhosttyApp::instance().config() at access time, so reads stay coherent across replaceConfig swaps. Also moves GhosttyApp's static configHasCustomShader scan into config::hasCustomShader so the disk-fallback logic lives in one place. MainWindow::configString / MainWindow::configBool stay as forwarders to config::string / config::boolean for now — phase 3.1 retires them after migrating GhosttySurface / CommandPalette / ChromeActions / SystemActions / InspectorWindow's m_owner->configString(...) callsites. Co-Authored-By: claude-flow <ruv@ruv.net>pull/12846/head
parent
bca65fb6c4
commit
0c9f846e0e
|
|
@ -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"
|
||||
|
|
@ -243,7 +244,7 @@ void GhosttySurface::layoutScrollbar() {
|
|||
bool GhosttySurface::scrollbarAllowed() const {
|
||||
if (!m_owner || !m_owner->config()) return true;
|
||||
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 +271,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 (m_owner && 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 +318,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 +374,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 +522,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 +547,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.
|
||||
|
|
@ -593,8 +584,7 @@ void GhosttySurface::paintResizeOverlay(QPainter &painter) {
|
|||
|
||||
// resize-overlay-position: center / {top,bottom}-{left,center,right}.
|
||||
const QString pos =
|
||||
m_owner ? cfgString(m_owner->config(), "resize-overlay-position")
|
||||
: QString();
|
||||
m_owner ? config::string("resize-overlay-position") : QString();
|
||||
const qreal m = 8;
|
||||
qreal x = (width() - boxW) / 2;
|
||||
qreal y = (height() - boxH) / 2;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
#include <QApplication>
|
||||
#include <QAudioOutput>
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QCursor>
|
||||
#include <QCloseEvent>
|
||||
|
|
@ -17,7 +16,6 @@
|
|||
#include <QDesktopServices>
|
||||
#include <QEvent>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
|
|
@ -55,6 +53,7 @@
|
|||
#include <LayerShellQt/window.h>
|
||||
|
||||
#include "app/GhosttyApp.h"
|
||||
#include "config/Config.h"
|
||||
#include "CommandPalette.h"
|
||||
#include "GhosttySurface.h"
|
||||
#include "TabWidget.h"
|
||||
|
|
@ -207,8 +206,8 @@ bool MainWindow::initialize() {
|
|||
// 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)))
|
||||
|
|
@ -268,17 +267,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 +298,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();
|
||||
|
|
@ -770,9 +768,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 +783,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 +801,7 @@ void MainWindow::animateQuickTerminalIn() {
|
|||
}
|
||||
|
||||
void MainWindow::animateQuickTerminalOut() {
|
||||
const int ms = quickTerminalAnimationMs(GhosttyApp::instance().config());
|
||||
const int ms = quickTerminalAnimationMs();
|
||||
if (ms <= 0) {
|
||||
hide();
|
||||
return;
|
||||
|
|
@ -885,7 +883,7 @@ void MainWindow::setupLayerShell() {
|
|||
|
||||
// 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) {
|
||||
|
|
@ -1022,7 +1020,7 @@ void MainWindow::gotoSplit(GhosttySurface *from,
|
|||
// `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");
|
||||
config::get(&pzBits, "split-preserve-zoom");
|
||||
const bool preserveZoom = (pzBits & 0x1) != 0 && m_zoomed == from;
|
||||
|
||||
const auto centerOf = [](GhosttySurface *s) {
|
||||
|
|
@ -1186,7 +1184,7 @@ void MainWindow::ringBell(GhosttySurface *surface) {
|
|||
// 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")) {
|
||||
if (!config::get(&features, "bell-features")) {
|
||||
features = BellAttention;
|
||||
}
|
||||
if (features & BellAttention) QApplication::alert(this);
|
||||
|
|
@ -1251,12 +1249,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)))
|
||||
|
|
@ -1344,8 +1342,8 @@ void MainWindow::reloadConfigGlobal() {
|
|||
// 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
|
||||
const bool notifOk = config::get(¬ifBits, "app-notifications");
|
||||
// config::get failure → defaults (both bits set) so the feature
|
||||
// still works as documented.
|
||||
if (!notifOk) notifBits = 0x3;
|
||||
const bool wantConfigReload = (notifBits & 0x2) != 0;
|
||||
|
|
@ -1354,23 +1352,21 @@ void MainWindow::reloadConfigGlobal() {
|
|||
QStringLiteral("Configuration reloaded."));
|
||||
}
|
||||
|
||||
// configString / configBool are forwarders to config::string /
|
||||
// config::boolean. Kept for now so external callers (GhosttySurface,
|
||||
// CommandPalette) don't have to migrate their existing
|
||||
// `m_owner->configString(...)` callsites in this commit; phase 3.1
|
||||
// retires the forwarders.
|
||||
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);
|
||||
return config::string(key);
|
||||
}
|
||||
|
||||
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;
|
||||
return config::boolean(key, fallback);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -1636,13 +1632,13 @@ 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"));
|
||||
QString path = config::diskValue("bell-audio-path");
|
||||
if (path.startsWith(QLatin1String("~/")))
|
||||
path = QDir::homePath() + path.mid(1);
|
||||
m_bellAudioPath = 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 +1646,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 +1658,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; }")
|
||||
|
|
@ -1690,7 +1686,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 +1704,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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QVariantMap>
|
||||
|
||||
#include "../app/GhosttyApp.h"
|
||||
#include "../config/Config.h"
|
||||
#include "../GhosttySurface.h"
|
||||
#include "../MainWindow.h"
|
||||
#include "../Util.h"
|
||||
|
|
@ -158,20 +159,19 @@ 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.
|
||||
// "config::get 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
|
||||
const bool actOk =
|
||||
config::get(&actBits, "notify-on-command-finish-action");
|
||||
// config::get failure → fall back to the documented defaults
|
||||
// (bell=true, notify=false) so the feature still works.
|
||||
if (!actOk) actBits = 0x1;
|
||||
const bool actBell = (actBits & 0x1) != 0;
|
||||
|
|
@ -260,8 +260,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]() {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@
|
|||
#include <cstdio>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QMetaObject>
|
||||
|
|
@ -18,35 +15,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 +56,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 +90,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,133 @@
|
|||
#include "Config.h"
|
||||
|
||||
#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 or the input is empty.
|
||||
static 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;
|
||||
}
|
||||
|
||||
uint64_t durationNs(const char *key, uint64_t fallbackNs) {
|
||||
return parseDurationNs(diskValue(key), fallbackNs);
|
||||
}
|
||||
|
||||
bool hasCustomShader() {
|
||||
// libghostty does not expose this through ghostty_config_get
|
||||
// (`custom-shader` is a repeatable path), so scan the primary
|
||||
// config file directly.
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace config
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#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. Always reads through
|
||||
// the disk-fallback (configDiskValue) because libghostty's
|
||||
// ghostty_config_get rejects Duration types (non-extern non-packed
|
||||
// struct). 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();
|
||||
|
||||
// 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]) {
|
||||
ghostty_config_t cfg = handle();
|
||||
return cfg && ghostty_config_get(cfg, out, key, N - 1);
|
||||
}
|
||||
|
||||
} // namespace config
|
||||
Loading…
Reference in New Issue