Merge pull request #14 from fuddlesworth/qt-cpp20-polish

qt: phase 8 — C++20 polish
pull/12846/head
Nathan 2026-05-23 16:07:34 -05:00 committed by GitHub
commit 7b33076bea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 62 additions and 49 deletions

View File

@ -23,7 +23,7 @@ project(ghastty LANGUAGES CXX C)
# ./qt/build/ghastty
# cmake --install qt/build --prefix ~/.local
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

View File

@ -317,8 +317,10 @@ 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())) {
double opacity = 0.7;
config::get(&opacity, "unfocused-split-opacity");
double opacity = 0.7; // default: 70% opaque
// On read failure opacity keeps the default; the success bit
// isn't load-bearing.
(void)config::get(&opacity, "unfocused-split-opacity");
if (opacity < 1.0) {
QColor fill(0, 0, 0); // default: dim toward black
ghostty_config_color_s c{};
@ -714,35 +716,36 @@ void GhosttySurface::sendKey(QKeyEvent *ev, ghostty_input_action_e action) {
// likewise reports the XKB keycode, which is libghostty's Linux native.
const uint32_t keycode = ev->nativeScanCode();
ghostty_input_key_s k = {};
k.action = action;
k.mods = translateMods(ev->modifiers());
// OR in any right-side bit for this keycode (e.g. Right-Shift sets
// SHIFT_RIGHT alongside SHIFT) and the live Caps/Num lock state
// from XkbTracker. macOS + GTK populate all of these; without
// them, keybinds like `right_shift+x` can't distinguish from
// `left_shift+x` and the kitty CSI-u encoding loses the lock bits.
k.mods = static_cast<ghostty_input_mods_e>(
k.mods | XkbState::instance().sideBitsForKeycode(keycode) |
const ghostty_input_mods_e mods = static_cast<ghostty_input_mods_e>(
translateMods(ev->modifiers()) |
XkbState::instance().sideBitsForKeycode(keycode) |
XkbState::instance().lockMods());
k.keycode = keycode;
k.text = printable ? text.constData() : nullptr;
// XKB lookups: unshifted codepoint (what this physical key would
// produce with no mods, e.g. ';' for the Shift+; → ':' event) and the
// modifiers the layout consumed to produce the event's text. Without
// consumed_mods, Shift+punctuation is emitted as a kitty CSI sequence
// the shell can't decode; with it set, libghostty's encoder falls
// back to plain text correctly.
k.unshifted_codepoint = XkbState::instance().unshiftedCodepoint(keycode);
// consumed_mods is computed for every event, not just printable ones.
// Function/keypad/Backspace/arrows can also have layout-consumed
// modifiers (e.g. Caps Lock affecting case for letter keys, Mode_Switch
// for layout shifts on Backspace) that the kitty encoder needs to
// strip. macOS + GTK both compute it unconditionally; gating on
// printable lost that info on non-text keys.
k.consumed_mods = XkbState::instance().consumedMods(keycode, k.mods);
k.composing = false;
// XKB lookups:
// unshifted_codepoint — what this physical key would produce with
// no mods (e.g. ';' for the Shift+; → ':' event). Without it
// libghostty's kitty encoder mis-handles punctuation release
// events.
// consumed_mods — modifiers the layout consumed to produce the
// event's text. Computed for every event, not just printable
// ones: function / keypad / Backspace / arrows can have layout-
// consumed mods (Caps Lock for letter case, Mode_Switch for
// layout shifts on Backspace) the encoder needs to strip. macOS
// + GTK both compute it unconditionally.
const ghostty_input_key_s k{
.action = action,
.mods = mods,
.consumed_mods = XkbState::instance().consumedMods(keycode, mods),
.keycode = keycode,
.text = printable ? text.constData() : nullptr,
.unshifted_codepoint = XkbState::instance().unshiftedCodepoint(keycode),
.composing = false,
};
ghostty_surface_key(m_surface, k);
}
@ -1097,14 +1100,15 @@ void GhosttySurface::focusOutEvent(QFocusEvent *) {
void GhosttySurface::commitText(const QString &text) {
if (!m_surface || text.isEmpty()) return;
const QByteArray utf8 = text.toUtf8();
ghostty_input_key_s k = {};
k.action = GHOSTTY_ACTION_PRESS;
k.mods = GHOSTTY_MODS_NONE;
k.consumed_mods = GHOSTTY_MODS_NONE;
k.keycode = 0;
k.text = utf8.constData();
k.unshifted_codepoint = 0;
k.composing = false;
const ghostty_input_key_s k{
.action = GHOSTTY_ACTION_PRESS,
.mods = GHOSTTY_MODS_NONE,
.consumed_mods = GHOSTTY_MODS_NONE,
.keycode = 0,
.text = utf8.constData(),
.unshifted_codepoint = 0,
.composing = false,
};
ghostty_surface_key(m_surface, k);
}

View File

@ -289,7 +289,8 @@ MainWindow *MainWindow::newWindow(ghostty_surface_t parent) {
if (!s_initialWindowConsumed) {
s_initialWindowConsumed = true;
bool initialWindow = true;
config::get(&initialWindow, "initial-window");
// Default-on; the success bit isn't load-bearing.
(void)config::get(&initialWindow, "initial-window");
wantsShow = initialWindow;
}
if (wantsShow) w->show();
@ -1068,7 +1069,8 @@ void MainWindow::refreshChrome() {
// runtime; mirrors the calculation in initialize().
if (GhosttyApp::instance().config()) {
bool quitAfter = true;
config::get(&quitAfter, "quit-after-last-window-closed");
// Default-on; the success bit isn't load-bearing.
(void)config::get(&quitAfter, "quit-after-last-window-closed");
// Same Duration-decode workaround as initialize().
const uint64_t delayNs =
config::durationNs("quit-after-last-window-closed-delay", 0);
@ -1370,9 +1372,10 @@ void MainWindow::toggleCommandPalette(GhosttySurface *surface) {
void MainWindow::applyBlur() {
// background-blur is a union whose C value is an i16: 0 (and the
// macOS-only negatives) means off, a positive radius means on. KWin
// uses its own configured radius, so only on/off matters here.
// uses its own configured radius, so only on/off matters here. On
// read failure blur stays 0 (off).
short blur = 0;
config::get(&blur, "background-blur");
(void)config::get(&blur, "background-blur");
applyWindowBlur(this, blur > 0);
}

View File

@ -20,17 +20,17 @@ 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();
[[nodiscard]] 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);
[[nodiscard]] 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);
[[nodiscard]] 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
@ -41,18 +41,18 @@ bool boolean(const char *key, bool fallback);
// 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);
[[nodiscard]] 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);
[[nodiscard]] 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();
[[nodiscard]] bool hasCustomShader();
// Read a packed-bitfield config key. libghostty serializes packed
// structs as a c_uint via c_get.zig (`ptr.* = @intCast(@as(Backing,
@ -61,14 +61,14 @@ bool hasCustomShader();
// 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);
[[nodiscard]] 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);
[[nodiscard]] 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
@ -79,9 +79,11 @@ QString expandedPath(const char *key);
// `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.
// fails. The success bit MUST be checked — `out` is left untouched
// on failure, so dropping the return masks an unread / unwritten
// access.
template <typename T, size_t N>
inline bool get(T *out, const char (&key)[N]) {
[[nodiscard]] 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);

View File

@ -36,7 +36,9 @@ constexpr const char *kAnimProperty = "ghastty.quickterm.anim";
// and a very large value doesn't lock the user out.
int animationMs() {
double secs = 0.2; // matches Config.zig default
config::get(&secs, "quick-terminal-animation-duration");
// On read failure secs keeps the default; the success bit isn't
// load-bearing.
(void)config::get(&secs, "quick-terminal-animation-duration");
if (secs <= 0.0) return 0;
return std::clamp(static_cast<int>(secs * 1000.0), 1, 1000);
}
@ -117,8 +119,10 @@ void setupLayerShell(QWidget *window) {
const QSize scr = screen ? screen->size() : QSize(1920, 1080);
// quick-terminal-size: primary is the edge-perpendicular extent.
// On read failure qsz stays zero-initialized and toPx falls back to
// its `fallback` argument; the success bit isn't load-bearing.
ghostty_config_quick_terminal_size_s qsz = {};
config::get(&qsz, "quick-terminal-size");
(void)config::get(&qsz, "quick-terminal-size");
const auto toPx = [](const ghostty_quick_terminal_size_s &s, int dim,
int fallback) -> int {
switch (s.tag) {