diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index a8ab37c0b..ecca7da7c 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -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) diff --git a/qt/src/GhosttySurface.cpp b/qt/src/GhosttySurface.cpp index 6176d8ae7..e5abd29cf 100644 --- a/qt/src/GhosttySurface.cpp +++ b/qt/src/GhosttySurface.cpp @@ -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(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( - k.mods | XkbState::instance().sideBitsForKeycode(keycode) | + const ghostty_input_mods_e mods = static_cast( + 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); } diff --git a/qt/src/MainWindow.cpp b/qt/src/MainWindow.cpp index f7485aa6b..61a85922c 100644 --- a/qt/src/MainWindow.cpp +++ b/qt/src/MainWindow.cpp @@ -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); } diff --git a/qt/src/config/Config.h b/qt/src/config/Config.h index 7dcc8971e..dc5386cd2 100644 --- a/qt/src/config/Config.h +++ b/qt/src/config/Config.h @@ -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 -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); diff --git a/qt/src/quickterm/QuickTerminal.cpp b/qt/src/quickterm/QuickTerminal.cpp index b00d647c6..dbc83e281 100644 --- a/qt/src/quickterm/QuickTerminal.cpp +++ b/qt/src/quickterm/QuickTerminal.cpp @@ -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(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) {