From 58479a389403418cefe03ed59ddde1737849d1c4 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 20:55:19 +0100 Subject: [PATCH 01/13] fe: dress ubuntu, kubuntu and mint in their own colours, every linux wore the same theme --- CLAUDE.md | 2 +- Makefile | 1 + app/AdminerDesktop.php | 4 ++ app/src/Env.php | 4 +- app/src/LinuxDistro.php | 61 +++++++++++++++++++ app/src/Os.php | 8 ++- app/src/Settings/Theme/Theme.php | 11 +++- .../Theme/designs/adminer-desktop/README.md | 3 +- .../designs/adminer-desktop/kubuntu-linux.css | 34 +++++++++++ .../designs/adminer-desktop/mint-linux.css | 33 ++++++++++ .../designs/adminer-desktop/ubuntu-linux.css | 33 ++++++++++ cli/pick-os.sh | 7 ++- tests/cli/LinuxDistroTest.php | 36 +++++++++++ tests/cli/pick-os-test.sh | 4 ++ 14 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 app/src/LinuxDistro.php create mode 100644 app/src/Settings/Theme/designs/adminer-desktop/kubuntu-linux.css create mode 100644 app/src/Settings/Theme/designs/adminer-desktop/mint-linux.css create mode 100644 app/src/Settings/Theme/designs/adminer-desktop/ubuntu-linux.css create mode 100644 tests/cli/LinuxDistroTest.php diff --git a/CLAUDE.md b/CLAUDE.md index 37c3bd2..e333df3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ log found in one line what two rounds of guessing had missed. Never enable the inspector outside `-debug`. -`make debug` asks which OS to look like, so the Windows or Linux theme can be checked from a Mac. It sets `ADMINER_DESKTOP_SIMULATE_OS`, ignored without `-debug`. It changes the look and the window only (`Os::look()`): the engine stays WKWebView, and without Segoe the font falls back. +`make debug` asks which OS to look like, so the Windows or Linux theme can be checked from a Mac. A Linux flavour can be picked too (`LinuxDistro`). It sets `ADMINER_DESKTOP_SIMULATE_OS`, ignored without `-debug`. It changes the look and the window only (`Os::look()`): the engine stays WKWebView, and without Segoe the font falls back. PHP fatal errors reach `~/Library/Logs/Adminer Desktop/adminer-desktop.log`, but only because `app/php/desktop.ini` turns `log_errors` on — frankenphp's default sends them to diff --git a/Makefile b/Makefile index 5c8efdd..811d9cc 100644 --- a/Makefile +++ b/Makefile @@ -338,6 +338,7 @@ qa: fetch app/vendor i18n mcp-bridge-data ## Run every static check (php, go, j ./bin/frankenphp$(EXE) php-cli tests/shortcuts/ShortcutsTest.php ./bin/frankenphp$(EXE) php-cli tests/cli/WindowsIcoTest.php ./bin/frankenphp$(EXE) php-cli tests/cli/I18nTest.php + ./bin/frankenphp$(EXE) php-cli tests/cli/LinuxDistroTest.php @sh tests/cli/pick-os-test.sh @sh tests/cli/kill-orphans-test.sh @sh tests/cli/dresscode-test.sh diff --git a/app/AdminerDesktop.php b/app/AdminerDesktop.php index 284bdc9..859e823 100644 --- a/app/AdminerDesktop.php +++ b/app/AdminerDesktop.php @@ -21,6 +21,7 @@ use Desktop\I18n\Locale; use Desktop\Import; use Desktop\Latte; +use Desktop\LinuxDistro; use Desktop\Mcp; use Desktop\Os; use Desktop\SettingKey; @@ -311,6 +312,9 @@ function csp(array &$csp): void { */ function bodyClass(): void { echo " " . Os::look()->bodyClass(); + if ($distro = LinuxDistro::look()) { + echo " " . $distro->bodyClass(); + } // The launcher sets this under -debug; the desktop scripts read it to stand down so // the web inspector's own behaviour is unobstructed. if (Env::Debug->get()) { diff --git a/app/src/Env.php b/app/src/Env.php index 49c3a2a..82d2c6f 100644 --- a/app/src/Env.php +++ b/app/src/Env.php @@ -14,8 +14,10 @@ enum Env: string { case DataDir = 'ADMINER_DESKTOP_DATA'; /** Set under -debug: turns on Tracy and the web inspector's own affordances. */ case Debug = 'ADMINER_DESKTOP_DEBUG'; - /** Which OS the UI dresses as under -debug, an Os case (Os::look). Ignored without -debug. */ + /** Which OS the UI dresses as under -debug, an Os or LinuxDistro case. Ignored without -debug. */ case SimulateOs = 'ADMINER_DESKTOP_SIMULATE_OS'; + /** The desktop session's own, not the launcher's: it only passes through (LinuxDistro::current). */ + case CurrentDesktop = 'XDG_CURRENT_DESKTOP'; /** `make demo` only: the throwaway connection demo-login.js fills and submits. */ case Demo = 'ADMINER_DESKTOP_DEMO'; /** The launcher's own path (Go's os.Executable), so the MCP panel can print the exact diff --git a/app/src/LinuxDistro.php b/app/src/LinuxDistro.php new file mode 100644 index 0000000..c27a036 --- /dev/null +++ b/app/src/LinuxDistro.php @@ -0,0 +1,61 @@ +get() ? (string) Env::SimulateOs->get() : ''; + if ($simulated !== '') { + return self::tryFrom($simulated); + } + return Os::current() === Os::Linux ? self::current() : null; + } + + /** This machine's flavour. */ + static function current(): ?self { + $osRelease = @file_get_contents('/etc/os-release') ?: @file_get_contents('/usr/lib/os-release') ?: ''; + $home = getenv('HOME'); + // omarchy-theme-set keeps the current theme under this state folder and the user's under .config. + $omarchy = $home !== false && (is_dir("$home/.local/state/omarchy") || is_dir("$home/.config/omarchy")); + return self::detect($osRelease, (string) Env::CurrentDesktop->get(), $omarchy); + } + + /** Omarchy first, as it installs onto Arch and leaves the ID alone; only in its Hyprland session, + * since another session on that machine does not look like it. + * @param string $osRelease the contents of os-release + * @param string $desktop XDG_CURRENT_DESKTOP, a colon-separated list such as "ubuntu:GNOME" + */ + static function detect(string $osRelease, string $desktop, bool $omarchyInstalled): ?self { + $desktops = explode(':', strtolower($desktop)); + if ($omarchyInstalled && in_array('hyprland', $desktops, true)) { + return self::Omarchy; + } + preg_match('~^ID="?([^"\n]+)~m', $osRelease, $match); + return match ($match[1] ?? '') { + 'linuxmint' => self::Mint, + // kubuntu too: no source shows what Kubuntu's os-release says, and KDE decides either way. + 'ubuntu', 'kubuntu' => match (true) { + in_array('kde', $desktops, true) => self::Kubuntu, + in_array('gnome', $desktops, true) => self::Ubuntu, + default => null, + }, + default => null, + }; + } + + /** The class on , and the name of the stylesheet Theme::cssMap() loads if there is one. */ + function bodyClass(): string { + return $this->value . '-linux'; + } +} diff --git a/app/src/Os.php b/app/src/Os.php index dbe6ce5..25f6e90 100644 --- a/app/src/Os.php +++ b/app/src/Os.php @@ -31,8 +31,12 @@ static function current(): self { /** The OS the UI dresses as: the real one, unless -debug asks for another (Env::SimulateOs). * Only for how the page looks. Paths and printed commands stay on current(). */ static function look(): self { - $simulated = Env::Debug->get() ? self::tryFrom((string) Env::SimulateOs->get()) : null; - return $simulated ?? self::current(); + if (!Env::Debug->get()) { + return self::current(); + } + $simulated = (string) Env::SimulateOs->get(); + // A Linux flavour dresses as Linux first (LinuxDistro::look). + return self::tryFrom($simulated) ?? (LinuxDistro::tryFrom($simulated) ? self::Linux : self::current()); } /** The class on the theme keys off (AdminerDesktop::bodyClass). */ diff --git a/app/src/Settings/Theme/Theme.php b/app/src/Settings/Theme/Theme.php index e31d7de..f5a05ca 100644 --- a/app/src/Settings/Theme/Theme.php +++ b/app/src/Settings/Theme/Theme.php @@ -4,6 +4,7 @@ namespace Desktop\Settings\Theme; use Desktop\Latte; +use Desktop\LinuxDistro; use Desktop\Os; use Desktop\SettingKey; use Desktop\UserSettings; @@ -69,7 +70,7 @@ function cssMap(): array { $sheet = match (Os::look()) { Os::Mac => self::MAC_SHEET, Os::Windows => self::WINDOWS_SHEET, - Os::Linux => null, + Os::Linux => $this->linuxSheet(), }; if ($sheet === null) { return $map; @@ -86,6 +87,14 @@ function cssMap(): array { return $return; } + /** This Linux flavour's stylesheet, once it has one: a new look needs only the file. */ + private function linuxSheet(): ?string { + $class = LinuxDistro::look()?->bodyClass(); + return $class !== null && is_file(__DIR__ . "/designs/adminer-desktop/$class.css") + ? "src/Settings/Theme/designs/adminer-desktop/$class.css" + : null; + } + /** Which design each side uses, before anything per-OS is added to it. * @return array */ diff --git a/app/src/Settings/Theme/designs/adminer-desktop/README.md b/app/src/Settings/Theme/designs/adminer-desktop/README.md index 591eeec..a34c058 100644 --- a/app/src/Settings/Theme/designs/adminer-desktop/README.md +++ b/app/src/Settings/Theme/designs/adminer-desktop/README.md @@ -27,7 +27,7 @@ The app's own default look. The gallery designs beside it are downloaded from up | `mac-os.css` | macOS | the system accent, neutral greys, corners, overlay scrollbars | | `windows-os.css` | Windows | Windows 11's tokens and controls, with values from WinUI's resources | -Linux has no file of its own. +Linux has no file of its own, but a flavour can: `omarchy-linux.css`, `ubuntu-linux.css`, `kubuntu-linux.css` or `mint-linux.css` loads as soon as it exists. `LinuxDistro` decides which one a machine is, from `os-release` and the desktop session. ## Light and dark @@ -41,6 +41,7 @@ Linux has no file of its own. All of them come from `AdminerDesktop::bodyClass()`: - `mac-os`, `windows-os`, `linux-os`: the OS. `make debug` can simulate another one. +- `omarchy-linux`, `ubuntu-linux`, `kubuntu-linux`, `mint-linux`: the Linux flavour, beside `linux-os`. Any other Linux has none. - `density-compact`, `density-cozy`, `density-comfortable`: row density. - `scale-100` to `scale-200`: the UI scale. - `theme-auto`, `theme-light`, `theme-dark`: the appearance preference. diff --git a/app/src/Settings/Theme/designs/adminer-desktop/kubuntu-linux.css b/app/src/Settings/Theme/designs/adminer-desktop/kubuntu-linux.css new file mode 100644 index 0000000..6d9f3f5 --- /dev/null +++ b/app/src/Settings/Theme/designs/adminer-desktop/kubuntu-linux.css @@ -0,0 +1,34 @@ +/* Kubuntu's Plasma: Breeze's colours, from KDE/breeze, colors/BreezeLight.colors and BreezeDark.colors: + * Colors:Window for the window, Colors:View for the panel, DecorationFocus for the accent and + * ForegroundLink for the links. Values only. Breeze defines no border colour, so that stays derived. + * + * Tokens sit on the body class, so what html derives from them is restated. */ +.kubuntu-linux { + --ad-menu-bg: light-dark(#eff0f1, #202326); + --bg: light-dark(#ffffff, #141618); + --ad-panel: light-dark(#ffffff, #141618); + --dim: light-dark(#eff0f1, #202326); + --fg: light-dark(#232629, #fcfcfc); + --ad-zebra: light-dark(#f7f7f7, #1d1f22); + --ad-border: color-mix(in srgb, var(--fg) 16%, transparent); + --ad-border-strong: color-mix(in srgb, var(--fg) 30%, transparent); + + --ad-accent: #3daee9; + --ad-accent-fg: light-dark(#ffffff, #fcfcfc); + --ad-accent-text: light-dark(#2980b9, #1d99f3); + --ad-accent-quiet: color-mix(in srgb, var(--ad-accent) 18%, transparent); + --ad-button: var(--ad-accent); + --ad-button-fg: var(--ad-accent-fg); + --lit: color-mix(in srgb, var(--ad-accent) 18%, var(--bg)); + accent-color: var(--ad-accent); + + --ad-danger: #da4453; + --ad-ok: #27ae60; +} + +/* The sidebar is a list to read: native ones keep the text's colour, and the accent on every name + was loud. */ +.kubuntu-linux #logins a, +.kubuntu-linux #tables a { + color: var(--fg); +} diff --git a/app/src/Settings/Theme/designs/adminer-desktop/mint-linux.css b/app/src/Settings/Theme/designs/adminer-desktop/mint-linux.css new file mode 100644 index 0000000..ac37e24 --- /dev/null +++ b/app/src/Settings/Theme/designs/adminer-desktop/mint-linux.css @@ -0,0 +1,33 @@ +/* Linux Mint: Mint-Y's colours, from linuxmint/mint-themes, src/Mint-Y/gtk-3.0/sass/_colors.scss and + * constants.py. Values only: the themes are GPL-3+, so none of their code is here. The window and the + * borders are that file's $header_bg lightened 5% and darkened from there, worked out by hand. + * + * Tokens sit on the body class, so what html derives from them is restated. */ +.mint-linux { + --ad-menu-bg: light-dark(#f8f8f9, #2f2f34); + --bg: light-dark(#ffffff, #333339); + --ad-panel: light-dark(#ffffff, #333339); + --dim: light-dark(#ebebed, #222226); + --fg: light-dark(#303030, #dadada); + --ad-border: light-dark(#c8c8cc, #202023); + --ad-border-strong: light-dark(#bababa, #202023); + + --ad-accent: #35a854; + --ad-accent-fg: #ffffff; + --ad-accent-text: light-dark( + oklch(from var(--ad-accent) min(l, 0.52) c h), + oklch(from var(--ad-accent) max(l, 0.72) c h) + ); + --ad-accent-quiet: color-mix(in srgb, var(--ad-accent) 15%, transparent); + --ad-button: var(--ad-accent); + --ad-button-fg: var(--ad-accent-fg); + --lit: color-mix(in srgb, var(--ad-accent) 15%, var(--bg)); + accent-color: var(--ad-accent); +} + +/* The sidebar is a list to read: native ones keep the text's colour, and the accent on every name + was loud. */ +.mint-linux #logins a, +.mint-linux #tables a { + color: var(--fg); +} diff --git a/app/src/Settings/Theme/designs/adminer-desktop/ubuntu-linux.css b/app/src/Settings/Theme/designs/adminer-desktop/ubuntu-linux.css new file mode 100644 index 0000000..6f713f9 --- /dev/null +++ b/app/src/Settings/Theme/designs/adminer-desktop/ubuntu-linux.css @@ -0,0 +1,33 @@ +/* Ubuntu's GNOME session: Yaru's colours, from ubuntu/yaru, gtk/src/default/gtk-3.0 (_colors.scss, + * _palette.scss) and common/accent-colors.scss.in. Values only: Yaru is GPL-3.0, so none of its code + * is here. #2c2c2c and #272727 are $jet lightened 8% and 6%, as _colors.scss does it. + * + * Tokens sit on the body class, so what html derives from them is restated. */ +.ubuntu-linux { + --ad-menu-bg: light-dark(#fafafa, #2c2c2c); + --bg: light-dark(#ffffff, #272727); + --ad-panel: light-dark(#ffffff, #272727); + --dim: light-dark(#fafafa, #2c2c2c); + --fg: light-dark(#3d3d3d, #f7f7f7); + --ad-border: light-dark(#c7c7c7, #181818); + --ad-border-strong: light-dark(#c7c7c7, #181818); + + --ad-accent: #e95420; + --ad-accent-fg: #ffffff; + --ad-accent-text: light-dark( + oklch(from var(--ad-accent) min(l, 0.52) c h), + oklch(from var(--ad-accent) max(l, 0.72) c h) + ); + --ad-accent-quiet: color-mix(in srgb, var(--ad-accent) 15%, transparent); + --ad-button: var(--ad-accent); + --ad-button-fg: var(--ad-accent-fg); + --lit: color-mix(in srgb, var(--ad-accent) 15%, var(--bg)); + accent-color: var(--ad-accent); +} + +/* The sidebar is a list to read: native ones keep the text's colour, and the accent on every name + was loud. */ +.ubuntu-linux #logins a, +.ubuntu-linux #tables a { + color: var(--fg); +} diff --git a/cli/pick-os.sh b/cli/pick-os.sh index 7393432..41c9e4e 100755 --- a/cli/pick-os.sh +++ b/cli/pick-os.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Ask which OS `make debug` should look like, and print its Os case for +# Ask which OS `make debug` should look like, and print its Os or LinuxDistro case for # ADMINER_DESKTOP_SIMULATE_OS: nothing for this machine. The question goes to stderr, because the # Makefile captures stdout. Without a terminal it asks nothing and answers this machine. # @@ -13,6 +13,10 @@ answer() { 2) echo windows ;; 3) echo linux ;; 4) echo mac ;; + 5) echo ubuntu ;; + 6) echo kubuntu ;; + 7) echo mint ;; + 8) echo omarchy ;; *) echo "no such choice: $1" >&2 return 1 @@ -28,6 +32,7 @@ fi [ -t 0 ] || exit 0 echo "Debug as which OS? 1) this machine 2) Windows 3) Linux 4) macOS" >&2 +echo " 5) Ubuntu 6) Kubuntu 7) Linux Mint 8) Omarchy" >&2 until printf '[1] ' >&2 read -r choice || exit 0 diff --git a/tests/cli/LinuxDistroTest.php b/tests/cli/LinuxDistroTest.php new file mode 100644 index 0000000..39427c9 --- /dev/null +++ b/tests/cli/LinuxDistroTest.php @@ -0,0 +1,36 @@ +bodyClass()); diff --git a/tests/cli/pick-os-test.sh b/tests/cli/pick-os-test.sh index b4f342a..c4b5d3e 100755 --- a/tests/cli/pick-os-test.sh +++ b/tests/cli/pick-os-test.sh @@ -20,6 +20,10 @@ check "1 means this machine" "$("$script" answer 1)" "" check "2 means Windows" "$("$script" answer 2)" "windows" check "3 means Linux" "$("$script" answer 3)" "linux" check "4 means macOS" "$("$script" answer 4)" "mac" +check "5 means Ubuntu" "$("$script" answer 5)" "ubuntu" +check "6 means Kubuntu" "$("$script" answer 6)" "kubuntu" +check "7 means Linux Mint" "$("$script" answer 7)" "mint" +check "8 means Omarchy" "$("$script" answer 8)" "omarchy" check "no terminal asks nothing and answers this machine" "$("$script" &1)" "" if "$script" answer 9 >/dev/null 2>&1; then From 34689a34b76cdb7c6e1a6ea255f244c7b876f1a1 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 21:16:41 +0100 Subject: [PATCH 02/13] fe: dress omarchy in its current theme, each theme has colours of its own --- .docs/threats.md | 1 + Makefile | 1 + app/AdminerDesktop.php | 5 ++ app/src/Settings/Theme/OmarchyTheme.php | 67 +++++++++++++++++ app/src/Settings/Theme/Theme.php | 13 +++- .../Theme/designs/adminer-desktop/README.md | 2 + .../designs/adminer-desktop/omarchy-linux.css | 9 +++ tests/cli/OmarchyThemeTest.php | 73 +++++++++++++++++++ 8 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 app/src/Settings/Theme/OmarchyTheme.php create mode 100644 app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css create mode 100644 tests/cli/OmarchyThemeTest.php diff --git a/.docs/threats.md b/.docs/threats.md index da0d159..2c7eb19 100644 --- a/.docs/threats.md +++ b/.docs/threats.md @@ -22,6 +22,7 @@ Who we assume is hostile: | **Local process holds report connections open** to exhaust sockets and stop the gate hearing a sign-out | The last session lent on after sign-out | A deadline per connection; `Accept` errors are waited out | `launcher/mcpgate/mcpgate_test.go` | | **Another Windows account connects to the bridge's socket**: Windows lets anyone traverse a directory, so the directory's ACL alone stops nothing | The token and the agent's SQL | The gate's directory is created private and its files inherit that; the bridge checks both | `launcher/mcpbridge`, `windows-ci` workflow | | **Another Windows account reads the token from `mcp.json`**, whose `chmod` does nothing there, in a data dir outside the profile | The window's session, if the token alone opened the gate | The token only works over that private socket: nothing listens on the URL's port | `launcher/mcpbridge`, `features/shell/mcp.feature` | +| **An Omarchy theme from a git repository puts markup in `colors.toml`**, which reaches an inline `\n"; } diff --git a/app/src/Settings/Theme/OmarchyTheme.php b/app/src/Settings/Theme/OmarchyTheme.php new file mode 100644 index 0000000..f6593d4 --- /dev/null +++ b/app/src/Settings/Theme/OmarchyTheme.php @@ -0,0 +1,67 @@ + the tokens it sets. What html derives from them follows, being on :root. */ + private const array TOKENS = [ + 'background' => ['--bg', '--ad-panel', '--ad-accent-fg'], + 'dark_background' => ['--ad-menu-bg'], + 'lighter_background' => ['--dim'], + 'foreground' => ['--fg'], + 'accent' => ['--ad-accent'], + 'selection' => ['--lit'], + 'red' => ['--ad-danger'], + 'green' => ['--ad-ok'], + ]; + + /** The current theme's declarations for :root, or nothing when there is no theme to read. */ + static function current(string $appearance): string { + $colors = self::read(); + return $colors !== null ? self::css($colors, $appearance) : ''; + } + + /** The current theme's scheme, which Auto follows here rather than the system's: Theme hands it to + * adminer as a Light or Dark override would be, so its dark.css and the icons go with it. Null + * without a theme that sets any colour. */ + static function currentMode(): ?string { + $colors = self::read(); + return $colors !== null && self::css($colors, 'auto') !== '' ? self::mode($colors) : null; + } + + private static function read(): ?string { + $home = getenv('HOME'); + $colors = $home !== false ? @file_get_contents("$home/.local/state/omarchy/current/theme/colors.toml") : false; + return $colors !== false ? $colors : null; + } + + /** ponytail: dark without a mode line; a theme from before it marks light with a light.mode file. */ + private static function mode(string $toml): string { + return preg_match('~^mode\s*=\s*"light"\s*$~m', $toml) ? 'light' : 'dark'; + } + + /** A theme comes from any git repository and this lands in a "', + 'accent = "red"', + 'accent = "#12345"', + 'accent = "#1234567"', + "accent = \"#123456\"; background: url(x)", + "accent = \"#123456\nbackground = url(x)\"", + ] as $hostile +) { + $out = OmarchyTheme::css($hostile, 'auto'); + Assert::notContains('script', $out); + Assert::notContains('url(', $out); + Assert::notContains('--ad-accent', $out); +} From ddf4c0e61a9f662df195bdf7e7e11fac2d536166 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 23:41:42 +0100 Subject: [PATCH 03/13] devops: name the adminer zip after the version in it, a warm cache outlived the bump .cache/adminer-src.zip carried no version, so make saw the file, called it up to date and never fetched again. Bumping ADMINER_VERSION then failed in the extract -- "cannot stat .cache/src-tmp/adminer-6.1.1" -- on every machine that had built once before the bump, with nothing to do but work out that the cache was the problem and delete it. A bump now names a file that is not there yet, and the zips of versions we have left behind go with it. --- Makefile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f9167ae..f6e2ee6 100644 --- a/Makefile +++ b/Makefile @@ -83,14 +83,24 @@ app/editor.php: $(ADMINER_BUILD) # The release zip is the full source tree: adminer.php and editor.php are compiled from it, and # plugins/ and designs/ copied out of it. Same zip, same pinned tag — plugins can never drift from the core. -.cache/adminer-src.zip: +# +# The version is in the filename, and that is the whole point: with a plain adminer-src.zip a +# checkout that had downloaded one version kept it forever, because make saw the file and called +# it up to date. Bumping ADMINER_VERSION then failed in the extract below ("cannot stat +# .cache/src-tmp/adminer-") on every machine with a warm cache, ours and a contributor's +# alike, and only `rm` fixed it. Now a bump names a file that is not there yet. +ADMINER_ZIP = .cache/adminer-src-$(ADMINER_VERSION).zip + +$(ADMINER_ZIP): @mkdir -p .cache curl -fsSL --retry 3 --retry-delay 2 -o $@ $(ADMINER_URL)/adminer-$(ADMINER_VERSION).zip + @# The zips of every version we have ever built are no use once this one is here. + @find .cache -maxdepth 1 -name 'adminer-src-*.zip' ! -name $(notdir $@) -delete # Extracted whole, once. Selecting with a pattern like 'designs/*' is not portable: # macOS and linux unzip let * match a slash and recurse, the windows one does not, so # only the file directly inside designs/ came out and every design silently vanished. -.cache/adminer-src: .cache/adminer-src.zip +.cache/adminer-src: $(ADMINER_ZIP) rm -rf $@ .cache/src-tmp unzip -qo $< -d .cache/src-tmp mv .cache/src-tmp/adminer-$(ADMINER_VERSION) $@ From a6244d2bd38c52501b8ddbcaf8694775d4cdc765 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 23:41:55 +0100 Subject: [PATCH 04/13] devops: write the webkit shim into .cache, a build leaned on a sudo run months ago webview is cgo and asks pkg-config for webkit2gtk-4.0, which no current distro ships -- Ubuntu dropped it in 24.04, Arch never had it, and both carry 4.1, the same API over libsoup3 and what download_linux.go already names. The shim that bridges that went into /usr with sudo tee from linux-deps, which made `go build` on a working checkout depend on a command someone had run once, by hand, on that machine: a checkout where it had not run failed with "Package webkit2gtk-4.0 was not found", pointing at a search path rather than at the step that fills it. It is a build artifact like any other now, written to .cache and reached through PKG_CONFIG_PATH, exported because every go command here compiles the cgo launcher and not only the one that links it. That also leaves nothing distro-specific but the headers themselves, so linux-deps asks whether they are installed before it asks which package manager is present: Arch and Fedora were turned away up front, and one of them is where this was found. Where apt is missing it now names what to install and stops. --- .docs/development.md | 6 ++++-- .docs/releases-linux.md | 10 ++++++---- Makefile | 44 +++++++++++++++++++++++++++++------------ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/.docs/development.md b/.docs/development.md index ffb8304..3d62be7 100644 --- a/.docs/development.md +++ b/.docs/development.md @@ -13,8 +13,10 @@ make install The toolchain via mise (node, go, composer and npm deps, the WebKit the browser checks drive), plus the pinned Adminer and frankenphp downloads. On Linux it also runs -`make linux-deps` — the one step that needs apt, for the GTK/WebKit dev headers the webview -links against. +`make linux-deps`, for the GTK/WebKit dev headers the webview links against: those are a +distro package rather than a toolchain, so it installs them with apt where it can and +otherwise names what to install (Arch: `gtk3 webkit2gtk-4.1`; Fedora: `gtk3-devel +webkit2gtk4.1-devel`) and stops. Headers already present, and it is a no-op on any distro. ## Building and running diff --git a/.docs/releases-linux.md b/.docs/releases-linux.md index 3616e34..f9b23a5 100644 --- a/.docs/releases-linux.md +++ b/.docs/releases-linux.md @@ -61,11 +61,13 @@ Skipped: signing, arm64, and a tag-push trigger — that last one is four lines Not for Debian. Debian is served by stage 1. Flatpak buys three things: -1. **Every other distro.** `make linux-deps` refuses anything without apt, and says so. - Fedora, Arch, openSUSE and Silverblue users currently have a tarball and their own - luck with GTK/WebKit versions. +1. **Every other distro.** A checkout builds on Fedora, Arch and openSUSE — `make + linux-deps` names the headers to install where it cannot install them itself — but + there is nothing to *ship* them: those users have a tarball and their own luck with + GTK/WebKit versions. 2. **The webkit2gtk version chase ends.** The runtime pins GTK3 and WebKitGTK, so the - `webkit2gtk-4.0` → `4.1` shim `linux-deps` installs stops being a per-distro problem. + `webkit2gtk-4.0` → `4.1` shim the build writes into `.cache/pkgconfig` stops being a + per-distro problem. 3. **Auto-updates**, which a downloaded `.deb` will never have. ### Repackage, do not rebuild diff --git a/Makefile b/Makefile index f6e2ee6..1815800 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,25 @@ else EXE = .exe endif +# webview is cgo and hardcodes `pkg-config webkit2gtk-4.0`, a .pc no current distro ships: +# Ubuntu dropped it in 24.04 and Arch never had it, both carrying webkit2gtk-4.1 instead — +# the same API over libsoup3, and what download_linux.go already asks for by name. A .pc of +# our own that requires 4.1 is what satisfies webview, and it lives in .cache with +# PKG_CONFIG_PATH pointing cgo at it rather than in /usr: writing there needs root, and it +# made `make build` on a working checkout depend on a sudo someone had run once, months ago. +# Here it is a build artifact like any other, so every distro gets the same 4.1 build and no +# build step needs root. Exported, because every go command in this file compiles the cgo +# launcher — `build`, `go vet`, golangci — not just the one that links it. +ifeq ($(UNAME_S),Linux) +WEBKIT_SHIM = .cache/pkgconfig/webkit2gtk-4.0.pc +export PKG_CONFIG_PATH := $(CURDIR)/.cache/pkgconfig$(if $(PKG_CONFIG_PATH),:$(PKG_CONFIG_PATH)) +endif + +.cache/pkgconfig/webkit2gtk-4.0.pc: + @mkdir -p $(dir $@) + @printf 'Name: webkit2gtk-4.0\nDescription: Shim onto webkit2gtk-4.1 for webview_go\nVersion: 2.44.0\nRequires: webkit2gtk-4.1\n' > $@ + @echo "wrote $@ (webview asks for webkit2gtk-4.0; this points it at 4.1)" + .PHONY: help install linux-deps fetch verify qa go-test phpstan dresscode dresscode-hook golangci oxc security check check-app e2e e2e-visual e2e-packaged i18n i18n-check mcp-bridge-data bench build run dev editor debug demo down destroy kill-orphans bundle zip dist tarball windows deb logs serve clean checksums .DEFAULT_GOAL := help @@ -49,18 +68,17 @@ endif $(MAKE) fetch # The one build dependency mise cannot pin: webview is cgo, and on Linux that means the -# system GTK-3 and WebKit2GTK dev headers — distro packages, not a toolchain. webview_go -# also hardcodes `pkg-config webkit2gtk-4.0`, which Ubuntu 24.04 dropped for 4.1 (same API -# over libsoup3), so a shim .pc that just requires 4.1 keeps the build on a current distro. -# Debian/Ubuntu (apt) is the only Linux supported right now — and the path CI and the .deb -# build on — so gate on apt up front and fail clearly on Arch/Fedora/others rather than -# half-installing. The pkg-config guard then makes a re-run a no-op that never re-sudos. -linux-deps: ## Install the Linux webview build deps (GTK/WebKit dev headers, Debian/Ubuntu only) - @command -v apt-get >/dev/null 2>&1 || { echo "linux-deps: Debian/Ubuntu (apt) is the only supported Linux distribution right now. On Arch, Fedora and others, install the GTK-3 and WebKit2GTK 4.1 dev headers with your own package manager."; exit 1; } +# system GTK-3 and WebKit2GTK dev headers — distro packages, not a toolchain. Only the +# install is distro-specific, so the headers being there already is the first thing asked: +# that answer is the same everywhere, and a distro we have no package names for (Arch, +# Fedora) is then a checkout that builds rather than one that is turned away. apt is the +# install path CI and the .deb build on; anywhere else names what to install and stops, +# which beats half-installing. The pkg-config guard makes a re-run a no-op that never +# re-sudos. $(WEBKIT_SHIM) is the other half, and needs no package manager at all. +linux-deps: $(WEBKIT_SHIM) ## Install the Linux webview build deps (GTK/WebKit dev headers) @pkg-config --exists gtk+-3.0 webkit2gtk-4.1 2>/dev/null && echo "gtk/webkit dev headers present" || { \ + command -v apt-get >/dev/null 2>&1 || { echo "linux-deps: install the GTK-3 and WebKit2GTK 4.1 dev headers with your package manager (Arch: gtk3 webkit2gtk-4.1; Fedora: gtk3-devel webkit2gtk4.1-devel), then re-run."; exit 1; }; \ sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev; } - @pkg-config --exists webkit2gtk-4.0 2>/dev/null || printf 'Name: webkit2gtk-4.0\nDescription: Shim onto webkit2gtk-4.1\nVersion: 2.44.0\nRequires: webkit2gtk-4.1\n' \ - | sudo tee /usr/lib/$(UNAME_M)-linux-gnu/pkgconfig/webkit2gtk-4.0.pc >/dev/null fetch: app/adminer.php app/editor.php app/src/Settings/Plugins/available app/src/Settings/Theme/designs/README.md bin/frankenphp$(EXE) ## Download frankenphp, build adminer from its release sources + patches/ (pinned, checksum-verified) @@ -256,7 +274,7 @@ dresscode-hook: # A cache per checkout. The default one is per user and keeps absolute paths, so an agent's # worktree linted second got the first one's findings at that worktree's paths; once it was # deleted, the //nolint comments there could not be read and the lint failed on them. -golangci: mcp-bridge-data # the bridge embeds it: go cannot even type-check the package without it +golangci: mcp-bridge-data $(WEBKIT_SHIM) # the bridge embeds it: go cannot even type-check the package without it GOLANGCI_LINT_CACHE=$(CURDIR)/.cache/golangci-lint go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) run ./... # Format-check CSS and JS with oxfmt and lint JS with oxlint; the file list is package.json's. @@ -313,7 +331,7 @@ security: # Static checks, every one from a tool we already have: the php is the frankenphp we # download, the rest ship with macOS or the go toolchain. Nothing to install. -qa: fetch app/vendor i18n mcp-bridge-data ## Run every static check (php, go, js lint + formatting) +qa: fetch app/vendor i18n mcp-bridge-data $(WEBKIT_SHIM) ## Run every static check (php, go, js lint + formatting) ./bin/frankenphp$(EXE) php-cli cli/lint.php @# No database and no browser: it replays adminer's own parser over a dump. ./bin/frankenphp$(EXE) php-cli tests/postgres/copy-import/run.php @@ -445,7 +463,7 @@ build.noindex/logo.ico: assets/logo.png app/cli/Command/WindowsIco.php app/vendo @mkdir -p $(dir $@) $(if $(EXE),PHP_INI_SCAN_DIR=$(WIN_INI)) $(CLI) windows-ico -build: fetch i18n mcp-bridge-data $(WIN_ICON) ## Build the launcher binary +build: fetch i18n mcp-bridge-data $(WIN_ICON) $(WEBKIT_SHIM) ## Build the launcher binary go build -ldflags "$(LDFLAGS)" -o build.noindex/adminer-desktop$(EXE) ./launcher # NSLocalizedString reads the .lproj folders beside an unbundled binary as it does a bundle's # Resources: without them `make run` showed the menu's keys (menu.edit) for its titles. From 3771c36528a6b1b3cbc46bb281e7b50253054f84 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 23:42:20 +0100 Subject: [PATCH 05/13] be: turn the inspector on under -debug on linux too, there was no console there The whole of "debug the running app, do not guess" was a macOS instruction: enableInspector was real only there, and the !darwin stub returned false, so -debug on Linux printed "web inspector unavailable" and left nothing to debug with -- no console, no DOM, no breakpoints, on the platform where the app has no menu bar to offer them from either. WebKitGTK has an inspector of its own; it only needed developer extras turned on and a show(), from an idle callback because neither the main loop nor the window exists yet at that point. Closing its window only closes it -- WebKit keeps the inspector alive and show() brings it back -- but with no Develop menu there was no way to ask again, so F12 and Ctrl+Shift+I reopen it, taken on the GtkWindow where GTK offers a key press before the focused widget sees it, so it works with the caret in an SQL textarea. Console messages go to stdout as well, and so to the log: that is what is left when the page dies before the inspector is up. The hint in the log is per-platform now rather than naming Safari everywhere, and the two diagnostics split the same way -- a real describeUIDelegate on Linux answers the question worth asking there (is the window's child the WebKitWebView every install* hangs off), the Windows stubs moving to inspector_other.go. --- CLAUDE.md | 6 +++ launcher/README.md | 2 +- launcher/inspector_linux.go | 102 ++++++++++++++++++++++++++++++++++++ launcher/inspector_other.go | 13 +++++ launcher/main.go | 4 +- launcher/menu_darwin.go | 3 ++ launcher/menu_other.go | 6 +-- 7 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 launcher/inspector_linux.go create mode 100644 launcher/inspector_other.go diff --git a/CLAUDE.md b/CLAUDE.md index e333df3..551bed6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,12 @@ this machine → Adminer Desktop — giving a real console, the DOM, network and breakpoints. It also logs diagnostics at startup (which UI delegate is attached, whether the inspector came up). +On Linux it is **WebKitGTK's own inspector**, which opens in a window of its own as the app +starts (`inspector_linux.go`). There is no Develop menu to call it back from, so **F12 or +Ctrl+Shift+I** reopens it after a close — handled on the GtkWindow, so it works with the +caret in an SQL textarea too. Every `console.log` and JavaScript error also goes to stdout +and so into the log, which is what you have when the page dies before the inspector is up. + Reach for it as soon as a symptom is visual or behavioural. `uiDelegate=(nil)` in that log found in one line what two rounds of guessing had missed. diff --git a/launcher/README.md b/launcher/README.md index 92af0c6..d53ad46 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -23,7 +23,7 @@ WebKitGTK) bring most of this themselves. | **downloads** | `installDownloads` | see below | | menu bar | `installMenu` | an unbundled binary gets a bare default menu. | | menu bar on Windows | `menu_windows.c` | webview makes a window with no menu. `SetMenu`, and its clicks taken from the window procedure. Not themed: Win32 draws it light in dark mode too. | -| Web Inspector (`-debug` only) | `enableInspector` | no console or breakpoints otherwise. | +| Web Inspector (`-debug` only) | `enableInspector` | no console or breakpoints otherwise. Real on both macOS (Safari's, over `setInspectable`) and Linux (WebKitGTK's own window, plus F12 to reopen it and console messages to stdout — `inspector_linux.go`); only WebView2 brings its own, on F12. | ## Downloads diff --git a/launcher/inspector_linux.go b/launcher/inspector_linux.go new file mode 100644 index 0000000..b3d1ae4 --- /dev/null +++ b/launcher/inspector_linux.go @@ -0,0 +1,102 @@ +//go:build linux + +package main + +/* +#cgo pkg-config: gtk+-3.0 webkit2gtk-4.1 +#include +#include + +// The GtkWindow holds the WebKitWebView as its only child (webview_go adds it with +// gtk_container_add), so gtk_bin_get_child reaches it -- the same route installDownloads +// takes in download_linux.c. +static WebKitWebView *inspector_web_view(void *window) { + GtkWidget *child = gtk_bin_get_child(GTK_BIN(GTK_WIDGET(window))); + if (!child || !WEBKIT_IS_WEB_VIEW(child)) { + return NULL; + } + return WEBKIT_WEB_VIEW(child); +} + +static gboolean show_inspector_idle(gpointer inspector) { + webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector)); + return G_SOURCE_REMOVE; +} + +// F12 or Ctrl+Shift+I reopens the inspector. Closing its window only closes it -- WebKit +// keeps the inspector itself alive and show() brings it back -- but with no menu bar on +// Linux and nothing like Safari's Develop menu, there was no way to ask for it again: the +// one route left was right-clicking the page for Inspect Element, which is no help at all +// on a page whose own context menu is the thing being debugged. +// +// Connected to the window rather than the webview: GTK offers a key press to handlers on +// the toplevel before its default handler forwards it to whatever has focus, so this fires +// with the caret in an SQL textarea too -- where a page-level shortcut would not. +static gboolean on_inspector_key(GtkWidget *widget, GdkEventKey *event, gpointer inspector) { + (void) widget; + gboolean ctrl_shift_i = (event->state & GDK_CONTROL_MASK) && (event->state & GDK_SHIFT_MASK) + && (event->keyval == GDK_KEY_i || event->keyval == GDK_KEY_I); + if (event->keyval == GDK_KEY_F12 || ctrl_shift_i) { + webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector)); + return TRUE; // handled: the page never sees it + } + return FALSE; +} + +static int enable_inspector(void *window) { + WebKitWebView *view = inspector_web_view(window); + if (!view) { + return 0; + } + WebKitSettings *settings = webkit_web_view_get_settings(view); + // Without developer extras the inspector refuses to open and the context menu has no + // Inspect Element, which is the only way in here: GTK has no menu bar to put one in. + webkit_settings_set_enable_developer_extras(settings, TRUE); + // And every console.log and JavaScript error onto stdout, which the launcher already + // tees into the log file. On macOS Safari's console is a window away; here the log is + // the thing you have when the page dies before the inspector is up. + webkit_settings_set_enable_write_console_messages_to_stdout(settings, TRUE); + WebKitWebInspector *inspector = webkit_web_view_get_inspector(view); + if (!inspector) { + return 0; + } + // Shown from an idle callback, not here: this runs before the GTK main loop starts and + // before the window itself is shown. G_SOURCE_REMOVE so it fires once -- and through a + // wrapper rather than casting webkit_web_inspector_show to GSourceFunc, which returns + // void: that cast leaves the return value to whatever is in the register, and a stray + // TRUE would keep the source alive and reopen the inspector on every idle turn. + g_idle_add(show_inspector_idle, inspector); + g_signal_connect(GTK_WIDGET(window), "key-press-event", G_CALLBACK(on_inspector_key), inspector); + return 1; +} + +// describe_web_view names the widget the window actually holds. The macOS counterpart +// (uiDelegate=(nil)) is what found a broken confirm() in one line; the question worth +// asking here is whether the child is the WebKitWebView every install* hangs off at all. +static const char *describe_web_view(void *window) { + GtkWidget *child = gtk_bin_get_child(GTK_BIN(GTK_WIDGET(window))); + if (!child) { + return "(no child widget)"; + } + // Owned by the GObject type system and never freed, so the caller must not free it. + return G_OBJECT_TYPE_NAME(child); +} +*/ +import "C" + +import "unsafe" + +// Where to look once it is on. GTK has no Develop menu, so the inspector's own window is it. +const inspectorHint = "its own window — F12 or Ctrl+Shift+I reopens it after a close, and console messages also go to the log" + +// enableInspector turns WebKitGTK's Web Inspector on for the app's page. Debug builds only: +// a page with developer extras in a shipped app is an open door, the same reason the macOS +// side gates setInspectable. +func enableInspector(window unsafe.Pointer) bool { + return C.enable_inspector(window) == 1 +} + +// describeUIDelegate reports what the window is holding -- the WebKitWebView, or not. +func describeUIDelegate(window unsafe.Pointer) string { + return "child=" + C.GoString(C.describe_web_view(window)) +} diff --git a/launcher/inspector_other.go b/launcher/inspector_other.go new file mode 100644 index 0000000..88c596c --- /dev/null +++ b/launcher/inspector_other.go @@ -0,0 +1,13 @@ +//go:build !darwin && !linux + +package main + +import "unsafe" + +// WebView2 brings its own devtools (F12), so there is nothing to turn on -- and nothing +// that would be gated on -debug if there were. +func enableInspector(window unsafe.Pointer) bool { return false } + +const inspectorHint = "F12 opens WebView2's own devtools" + +func describeUIDelegate(window unsafe.Pointer) string { return "(darwin and linux only)" } diff --git a/launcher/main.go b/launcher/main.go index 1a07e09..942054c 100644 --- a/launcher/main.go +++ b/launcher/main.go @@ -254,7 +254,7 @@ func loaderPage(html string) string { func main() { editor := flag.Bool("editor", false, "open Adminer Editor instead of Adminer") - debug := flag.Bool("debug", false, "open devtools support: Safari > Develop > Adminer Desktop") + debug := flag.Bool("debug", false, "turn the web inspector on (Safari's Develop menu on macOS, WebKitGTK's own window on Linux)") headless := flag.Bool("headless", false, "start the server, verify it serves, exit (used by `make check-app`)") dev := flag.Bool("dev", false, "reload the window whenever a file under app/ changes") mcp := flag.Bool("mcp", false, "serve MCP over stdio for an agent, and exit (no window)") @@ -465,7 +465,7 @@ func main() { log.Print("webview ", describeUIDelegate(w.Window())) log.Print("titlebar band hits ", describeTitlebarHit(w.Window())) if enableInspector(w.Window()) { - log.Print("web inspector on: Safari > Develop > this machine > Adminer Desktop") + log.Print("web inspector on: ", inspectorHint) } else { log.Print("web inspector unavailable") } diff --git a/launcher/menu_darwin.go b/launcher/menu_darwin.go index 83a2d4d..b47191b 100644 --- a/launcher/menu_darwin.go +++ b/launcher/menu_darwin.go @@ -152,6 +152,9 @@ func describeUIDelegate(window unsafe.Pointer) string { return C.GoString(c) } +// Where to look once it is on; the inspector lives in Safari on this platform. +const inspectorHint = "Safari > Develop > this machine > Adminer Desktop" + // enableInspector turns on Safari's Web Inspector for the app's page. func enableInspector(window unsafe.Pointer) bool { return C.enableInspector(window) == 1 diff --git a/launcher/menu_other.go b/launcher/menu_other.go index 802adf5..bffeb9b 100644 --- a/launcher/menu_other.go +++ b/launcher/menu_other.go @@ -30,9 +30,9 @@ func setWindowAppearance(window unsafe.Pointer, dark int) {} // installDownloads is per-platform: real on Linux (download_linux.c), a no-op on Windows // (download_other.go), and the macOS build has its own in menu_darwin.go. -func enableInspector(window unsafe.Pointer) bool { return false } - -func describeUIDelegate(window unsafe.Pointer) string { return "(darwin only)" } +// enableInspector and describeUIDelegate are per-platform: real on macOS and on Linux +// (inspector_linux.go, where WebKitGTK has an inspector of its own to turn on), stubbed for +// Windows in inspector_other.go. // The title bar stays: GTK and Windows draw their own, and the page lays out under it. func installFrameless(w webview.WebView) {} From 5c4d08e4155e51ec9c28544573f9c24f781cfaca Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 23:42:30 +0100 Subject: [PATCH 06/13] be: show the window when the loader never reports back, it stayed hidden the whole run The window is hidden on purpose until the loader has painted, and the only thing that brought it back was loaderShown -- a call from the page over a JavaScript binding. That binding is not guaranteed: on WebKitGTK 2.54 nothing from the page reaches Go at all, so every run of `make run` on Arch opened no window ever, while the server came up, served the page and logged a clean startup behind it. Nothing in the log said what was wrong, because by every measure Go could take the app had started. A reveal that can fail is not a reveal, so there is a deadline under it now: two seconds, well past the loader's own paint anywhere it works (759ms at its worst here, cold), and sync.Once so the good path still wins and this only ever fires where that path is broken -- where it also says so, since a hidden window is the symptom of a dead binding and that is worth naming rather than quietly papering over. --- launcher/main.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/launcher/main.go b/launcher/main.go index 942054c..5758d0c 100644 --- a/launcher/main.go +++ b/launcher/main.go @@ -18,6 +18,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "syscall" "time" @@ -472,6 +473,20 @@ func main() { } installMenu(w.Window(), w.Navigate, w.Eval, "http://"+addr, filepath.Dir(logPath)) + // The window is hidden until something reveals it, so whatever reveals it must not be + // able to fail: hidden forever is not a slow start, it is an app that never opened. The + // loader's own report below is the good path and the deadline is the floor. Once, because + // both may arrive. + var revealed sync.Once + reveal := func(late bool) { + revealed.Do(func() { + if late { + log.Print("startup: the loader never reported back (the page's native bindings are not working); showing the window anyway") + } + showWindow(w.Window()) + }) + } + // Report from inside the webview when the loader has actually parsed and rendered -- the one // startup phase Go can't time, since it is WebKit's own paint. It separates "the window was // shown before the loader rendered" from "WebKit was slow to paint". loading.html calls this @@ -480,11 +495,21 @@ func main() { log.Printf("startup: loader rendered in %s", time.Since(guiStart).Round(time.Millisecond)) // The loader has parsed and styled, so there is finally something to show: reveal the // window (already sized), painting straight onto the spinner instead of a blank frame. - showWindow(w.Window()) + reveal(false) }); err != nil { log.Print("startup: loader timing bind failed: ", err) } + // And the floor under it, because that report comes back over a JavaScript binding and the + // binding is not guaranteed: on WebKitGTK 2.54 (Arch, Fedora) nothing from the page ever + // reaches Go, so the window stayed hidden for the whole run and `make run` looked like an + // app that had not started -- while the server was up and serving behind it. Two seconds is + // well past the loader's own paint everywhere it does work (759ms was the worst seen here, on + // a cold first launch), so the good path still reveals the window first and this only ever + // fires where that path is broken. Dispatch, because GTK and AppKit both want the main + // thread, which is where every other reveal already happens. + time.AfterFunc(2*time.Second, func() { w.Dispatch(func() { reveal(true) }) }) + // The page's background, from window-background.js, for the strip a fast resize uncovers. // A computed colour is rgb() when opaque; anything else (transparent, rgba) is left alone. if err := bindNative(w, "setBackground", func(css string) { From 295e3f19a0eeb6c76a0cdcbc1562658dc39af827 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 23:42:40 +0100 Subject: [PATCH 07/13] devops: find the servers systemd reparents, kill-orphans reported none on linux An orphan was recognised by ppid 1, which is true on macOS and never true on Linux: systemd --user marks itself a child subreaper, so a server whose launcher was killed is reparented to that instead. So `make kill-orphans` and the SessionEnd hook that calls it after every agent session both printed "no orphaned frankenphp server" while fourteen of them held fourteen ports on this machine -- the exact failure the script exists to prevent, reported as success. The reaper set is init plus any systemd --user now, and the test covers a Linux reparent beside the macOS one. --- cli/kill-orphans.sh | 25 +++++++++++++++++++------ tests/cli/kill-orphans-test.sh | 13 +++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/cli/kill-orphans.sh b/cli/kill-orphans.sh index 6a7518b..0a55dd0 100644 --- a/cli/kill-orphans.sh +++ b/cli/kill-orphans.sh @@ -5,30 +5,43 @@ # The launcher stops its server when its run loop returns, and on macOS a Cmd+Q never returns # (AppKit exits the process), nor does a kill or a crash: the server stays, reparented to init # and holding its port. That is the installed app as much as a checkout's or a worktree's. A server whose window is still -# open has its launcher as parent, and an e2e run's has PHP, so ppid 1 is what tells an orphan -# apart and nothing alive is touched. +# open has its launcher as parent, and an e2e run's has PHP, so the parent being an init or a +# subreaper (see reapers below) is what tells an orphan apart and nothing alive is touched. # # SIGTERM first, SIGKILL for what is still there two seconds on, and what it reports is what is # gone, counted after. # # cli/kill-orphans.sh stop them -# cli/kill-orphans.sh --filter the orphans' pids among `ps -axo pid=,ppid=,command=` lines +# cli/kill-orphans.sh --filter [reaper-pids] +# the orphans' pids among `ps -axo pid=,ppid=,command=` lines # on stdin, for tests/cli/kill-orphans-test.sh set -eu +# Which parent means "the launcher is gone". init on macOS, and on Linux also the per-user +# systemd: it sets itself as a child subreaper, so an orphan of ours is reparented to that +# and never to pid 1. A bare `ppid == 1` test therefore matched nothing on Linux at all -- +# `make kill-orphans` and the SessionEnd hook that calls it both said "no orphaned frankenphp +# server" while a dozen of them held a dozen ports, which is the one thing this exists to stop. +reapers() { + echo 1 + ps -axo pid=,command= | awk '$2 ~ /(^|\/)systemd$/ && /--user/ { print $1 }' +} + filter() { # The whole line, not a field: the installed app's path has a space in it. # php-server: what an install from before the Caddyfile still runs. - awk '$2 == 1 && (index($0, "frankenphp run --config ") || index($0, "frankenphp php-server ")) { print $1 }' + awk -v pids="${1:-1}" ' + BEGIN { split(pids, list, " "); for (i in list) { reaper[list[i]] = 1 } } + reaper[$2] && (index($0, "frankenphp run --config ") || index($0, "frankenphp php-server ")) { print $1 }' } if [ "${1:-}" = "--filter" ]; then - filter + filter "${2:-1}" exit 0 fi orphans() { - ps -axo pid=,ppid=,command= | filter + ps -axo pid=,ppid=,command= | filter "$(reapers | tr '\n' ' ')" } found=$(orphans) diff --git a/tests/cli/kill-orphans-test.sh b/tests/cli/kill-orphans-test.sh index cfaab00..0b04123 100644 --- a/tests/cli/kill-orphans-test.sh +++ b/tests/cli/kill-orphans-test.sh @@ -19,4 +19,17 @@ if [ "$got" != "$want" ]; then echo "kill-orphans picked [$got], wanted [$want]" >&2 exit 1 fi +# The same, reparented the way Linux does it: systemd --user is a subreaper, so the orphan sits +# on its pid rather than on 1. Passed in, because the pid is the machine's; what is asserted is +# that a reaper other than init counts, which is the whole of the Linux path. +got=$(sh "$(dirname "$0")/../../cli/kill-orphans.sh" --filter '1 1013' <<'EOF' + 201 1013 bin/frankenphp run --config /home/u/.config/Adminer Desktop/Caddyfile --adapter caddyfile + 202 1013 /usr/bin/pipewire + 203 2048 bin/frankenphp run --config /home/u/.config/Adminer Desktop/Caddyfile --adapter caddyfile +EOF +) +if [ "$got" != "201" ]; then + echo "kill-orphans picked [$got] under a systemd --user reaper, wanted [201]" >&2 + exit 1 +fi echo "kill-orphans ok" From 23befd6d3c1e38e61734d366e8315aad9b8976e2 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Sun, 1 Nov 2026 23:42:40 +0100 Subject: [PATCH 08/13] devops: name a php binary the hook can run, frankenphp leaves PHP_BINARY empty on linux The dresscode PostToolUse hook started dresscode through PHP_BINARY, which the mac frankenphp fills in and the linux one leaves as "". proc_open refuses an empty program name, so every PHP file an agent wrote on Linux ended in an uncaught ValueError: the file was never checked, and the agent was told nothing -- the convention this hook exists to enforce was simply not enforced there. bin/frankenphp beside the root is the same binary, and asking the filesystem rather than the string also keeps phpstan from calling the check dead code, its stub having PHP_BINARY as non-empty. --- cli/dresscode-hook.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cli/dresscode-hook.php b/cli/dresscode-hook.php index bc95caa..2a8b754 100644 --- a/cli/dresscode-hook.php +++ b/cli/dresscode-hook.php @@ -24,7 +24,18 @@ tellAgent("dresscode did not run on $file: $root has no app/vendor. Run make qa there."); exit(0); } -$command = [PHP_BINARY, "php-cli", $root . "/app/vendor/bin/dresscode", "fix", $file, "-c", $root . "/app/dresscode.neon", "--format", "agent", "--no-color", "--jobs", "1"]; +// frankenphp's php-cli leaves PHP_BINARY empty on Linux -- the mac build fills it in, that one +// does not -- and proc_open refuses an empty program name, so every PHP file an agent wrote on +// Linux ended here in an uncaught ValueError: dresscode never ran, and nothing said it had not. +// The repo's own frankenphp is that binary either way. Asked of the filesystem rather than of +// the string, because phpstan's stub has PHP_BINARY as a non-empty string and would call the +// comparison dead code -- and because a path that is there is the thing proc_open needs. +$php = is_file(PHP_BINARY) ? PHP_BINARY : $root . "/bin/frankenphp"; +if (!is_file($php)) { + tellAgent("dresscode did not run on $file: no PHP binary at $php. Run make fetch there."); + exit(0); +} +$command = [$php, "php-cli", $root . "/app/vendor/bin/dresscode", "fix", $file, "-c", $root . "/app/dresscode.neon", "--format", "agent", "--no-color", "--jobs", "1"]; // cli/ lies outside the config's root, so --skip-excluded would leave every file there unchecked. if (!str_starts_with($file, $root . "/cli/")) { $command[] = "--skip-excluded"; From 1e24f58cbad3549736c4885a74788c7aa3afb28a Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Mon, 2 Nov 2026 00:00:55 +0100 Subject: [PATCH 09/13] devops: keep the boot check off the developer's desktop, it read the omarchy theme check.sh posts a light and a dark design and expects both back gated on prefers-color-scheme, which is how the OS theme picks one. On Omarchy that cannot hold: LinuxDistro finds ~/.local/state/omarchy, Auto then follows that machine's current theme rather than the system, and a theme with mode = "dark" pins the page to one side, so only the dark design comes back and the assertion fails. Nothing was broken -- the check was reading the desktop it ran on. That is the dearest shape a failing test has. It stays green in CI and on macOS, where no such theme exists, and on main, which has no Omarchy code: red for one developer and green everywhere else reads as a regression in whatever that person last touched, and costs an investigation to find out it is not one. HOME into the throwaway data dir the script already makes and removes gives every machine the answer CI gets, on every platform, and leaves the assertion measuring what it is for -- that switching design works before login. The seam is the one tests/cli/OmarchyThemeTest.php already uses, and that test covers the Omarchy reading deterministically, so nothing is lost by keeping it out of here. The e2e fixture inherits this process's environment and boots the same app for scenarios that assert the theme light and dark, so it takes the same line. --- check.sh | 16 +++++++++++++++- tests/e2e/harness/fixture.php | 7 ++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/check.sh b/check.sh index ba5e6bc..25fd437 100755 --- a/check.sh +++ b/check.sh @@ -23,7 +23,21 @@ ADMINER_DESKTOP_DATA=$(mktemp -d) export ADMINER_DESKTOP_DATA # The launcher's own Caddyfile and the cache policy a build gets, so what is checked is what ships. -AD_PORT=$PORT AD_ROOT=app AD_ASSET_CACHE="max-age=31536000, immutable" \ +# +# HOME goes into the throwaway directory with it, on every platform, because the page reads the +# desktop it is running on: LinuxDistro looks for ~/.local/state/omarchy to tell an Omarchy machine +# apart, and on one Auto follows that machine's current theme rather than the system (OmarchyTheme). +# So the design assertions below -- post a light and a dark design, expect both back media-gated -- +# cannot hold on a developer whose theme pins the page to one side, while staying green on macOS and +# in CI, where no such theme exists. Red for one person and green everywhere else is the dearest +# kind of failing test: it reads as a regression in whatever that person last touched. Pointed at a +# directory with no theme in it, every machine answers the way CI does. The same seam +# tests/cli/OmarchyThemeTest.php uses, and the only other thing here that reads a home directory is +# caddy's own storage, which then lands in the throwaway dir and is removed with it. +# +# What os-release says is still this machine's (Mint and Ubuntu keep their flavour), but that only +# adds a stylesheet and pins no scheme, so nothing below depends on it. +AD_PORT=$PORT AD_ROOT=app AD_ASSET_CACHE="max-age=31536000, immutable" HOME="$ADMINER_DESKTOP_DATA" \ ./bin/frankenphp run --config launcher/Caddyfile --adapter caddyfile \ 2>/tmp/adminer-desktop-check.log & SERVER=$! diff --git a/tests/e2e/harness/fixture.php b/tests/e2e/harness/fixture.php index d2d1a50..17851df 100644 --- a/tests/e2e/harness/fixture.php +++ b/tests/e2e/harness/fixture.php @@ -199,7 +199,12 @@ function e2e_boot(): array null, // The launcher's Caddyfile, with the cache policy of a build: what is tested is what ships. // An agent statement stops after 3 s rather than 45, so a scenario can outlast the limit. - ['ADMINER_DESKTOP_DATA' => $data, 'AD_PORT' => $port, 'AD_ROOT' => $app, 'AD_ASSET_CACHE' => 'max-age=31536000, immutable', 'ADMINER_DESKTOP_MCP_STATEMENT_SECONDS' => '3'], + // HOME into the throwaway data dir, as check.sh does and for the reason given there: Process + // inherits this one's environment, and a page that reads the developer's desktop makes the + // theme scenarios assert something different on each machine -- on Omarchy, Auto follows that + // machine's own theme and the light scenario cannot pass. No theme there, so every machine + // answers as CI does. + ['ADMINER_DESKTOP_DATA' => $data, 'HOME' => $data, 'AD_PORT' => $port, 'AD_ROOT' => $app, 'AD_ASSET_CACHE' => 'max-age=31536000, immutable', 'ADMINER_DESKTOP_MCP_STATEMENT_SECONDS' => '3'], ); // frankenphp logs every request it serves and nothing here ever reads that pipe, so it fills: // sixty-four kilobytes in, the server blocks writing to it and stops answering. From the From 0a58ff49f7ff1b98af7e2ea4c9ba25bae843c953 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Mon, 2 Nov 2026 00:07:11 +0100 Subject: [PATCH 10/13] fe: take save out of the terminal green on omarchy, it was a lime nobody picked The dialog's Save is filled in --ad-ok, and an Omarchy palette replaces that token with the theme's own `green`. A terminal palette's green is chosen to be read as text against the background, not to be a surface: Tokyo Night's #9ece6a arrives as a lime slab under dark text, which is the one loud thing on an otherwise quiet window, and every theme whose green is a terminal green does the same to it. So Save wears what .message already wears on this side, the same two tokens the rest of the page pairs: the green as text over --ad-ok-bg. It stays the filled one of the two buttons, Reset being text and a border over nothing, so the pair still reads at a glance -- which is the whole reason Save is green and not the accent. Measured over the 23 themes installed here rather than judged on one: the theme's green on --ad-ok-bg clears 4.5:1 on 19 of them (8.4:1 on Tokyo Night), against 16 for a tint mixed from the theme's own background, which was the other candidate. The four it does not clear are themes with a muted green -- miasma, retro-82 and two light ones -- and .message has that same contrast there today, so it is one fix for both and not this one. --- .../designs/adminer-desktop/omarchy-linux.css | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css b/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css index e3ad401..93ee84d 100644 --- a/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css +++ b/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css @@ -7,3 +7,25 @@ .omarchy-linux #tables a { color: var(--fg); } + +/* Save is filled in --ad-ok, and a palette replaces that with the theme's own `green` -- a colour + chosen to be read as text on the background, not to be a surface. Tokyo Night's #9ece6a fills the + button in a lime nobody picked, and every theme whose green is a terminal green does the same. + So Save takes the shape .message already has on this side, with the same pair of tokens: the + green as text over --ad-ok-bg, which is where that colour is legible. It stays the filled one of + the two -- Reset is text and a border over nothing -- so the pair still reads at a glance. */ +.omarchy-linux .ad-modal-footer .ad-primary { + background: var(--ad-ok-bg); + color: var(--ad-ok); + border-color: color-mix(in srgb, var(--ad-ok) 26%, transparent); +} + +/* A filled button darkens under the pointer; a tinted one has nowhere to darken to, so these + deepen the tint instead -- the same gesture, read on a surface that is mostly background. */ +.omarchy-linux .ad-modal-footer .ad-primary:hover:not(:disabled) { + background: color-mix(in srgb, var(--ad-ok) 12%, var(--ad-ok-bg)); +} + +.omarchy-linux .ad-modal-footer .ad-primary:active:not(:disabled) { + background: color-mix(in srgb, var(--ad-ok) 20%, var(--ad-ok-bg)); +} From 36dc1fefef3f2ce40506327dfcbf0f0935576cba Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Mon, 2 Nov 2026 00:09:24 +0100 Subject: [PATCH 11/13] devops: say why there is no database, "docker is not running" pointed at a running daemon `make debug` brings the e2e's mysql up so there is something to log into, and when it cannot it said "no mysql: docker is not running" -- the one explanation the Makefile had for every way docker can be unusable. On a machine where the daemon is up but the user is not in the docker group, that sends you to systemctl to look at a service that is already active, and the socket is nowhere in the sentence. Found by being that machine. The reason is docker's own now, because it already distinguishes the cases precisely, and a permission denied also gets the fix for it: the group to join, and that a login is what makes it take. The Makefile stops explaining a failure it cannot see the cause of. --- Makefile | 2 +- cli/demo-mysql.sh | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1815800..e4d2f45 100644 --- a/Makefile +++ b/Makefile @@ -491,7 +491,7 @@ editor: build ## Run in editor mode # docker skips it rather than refusing to debug. debug: build ## Run with Safari's Web Inspector attached, and a seeded mysql (docker) @os=$$(./cli/pick-os.sh); \ - ./cli/demo-mysql.sh $(DEMO_MYSQL) || echo "no mysql: docker is not running"; \ + ./cli/demo-mysql.sh $(DEMO_MYSQL) || true; \ ADMINER_DESKTOP_SIMULATE_OS=$$os ./build.noindex/adminer-desktop$(EXE) -debug # The app in dev mode against seeded demo data, for clicking around by hand. Brings up diff --git a/cli/demo-mysql.sh b/cli/demo-mysql.sh index 516f009..8ed5ba5 100755 --- a/cli/demo-mysql.sh +++ b/cli/demo-mysql.sh @@ -7,7 +7,21 @@ set -eu name=$1 -docker info >/dev/null 2>&1 || exit 1 +# In docker's own words when it cannot be used. "Is the daemon running?" and "permission denied" +# are different problems with different fixes, and both used to come out of `make debug` as +# "docker is not running" -- which sends you to look at a daemon that is already up. The daemon +# being reachable is a property of this machine and this user, so say which of the two it is. +if ! why=$(docker info 2>&1 >/dev/null); then + echo "no mysql, the app opens without it:" >&2 + printf '%s\n' "$why" | grep -v '^$' | tail -2 >&2 + case $why in + *"permission denied"*) + echo "The daemon is up; this user cannot reach its socket. Join the docker group" >&2 + echo "(sudo usermod -aG docker \"$USER\") and log in again, or run docker rootless." >&2 + ;; + esac + exit 1 +fi docker start "$name" >/dev/null 2>&1 || docker run -d --name "$name" \ -e MYSQL_ROOT_PASSWORD=demo -e MYSQL_DATABASE=demo -p 127.0.0.1:53306:3306 mysql:8 >/dev/null # Over TCP: the entrypoint's first server listens on the socket only, and is then restarted. From d207c84a5fe21409683f3785e1e04090222f0c02 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Mon, 2 Nov 2026 00:48:13 +0100 Subject: [PATCH 12/13] fe: square the window on omarchy, a rounded card sat on a desktop with hard corners Omarchy's own default is rounding = 0 (default/hypr/looknfeel.lua) and a theme does not touch it -- a theme ships colours, and only the user's own hypr config overrides the rounding -- so the desktop around this window has hard corners while the app put a 7px radius on every panel, input and button in the middle of it, which is the look of a web page in a window rather than of a program on that desktop. The two radius tokens carry most of it. The rest is a short list of corners that do not come through them, found by grep and minus the two sets that cannot show here: window.css is the macOS frameless chrome, and a gallery design never has this sheet loaded over it. A list rather than a blanket * { border-radius: 0 }, which would put a declaration on every cell of a table that can run to thousands. Discs stay discs -- the connection icon and the environment dot are shapes, not corners. Read it from the desktop rather than hardcode it? Only the user's hypr config knows, not the theme, and it is Lua: the rounding is commented out in mine, which a reader would have to not mistake for a value. Not worth it to track a number that is 0 by default. --- .../designs/adminer-desktop/omarchy-linux.css | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css b/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css index 93ee84d..5601aed 100644 --- a/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css +++ b/app/src/Settings/Theme/designs/adminer-desktop/omarchy-linux.css @@ -29,3 +29,24 @@ .omarchy-linux .ad-modal-footer .ad-primary:active:not(:disabled) { background: color-mix(in srgb, var(--ad-ok) 20%, var(--ad-ok-bg)); } + +/* Omarchy is square, so this window is too. Its own default is `rounding = 0` + (/usr/share/omarchy/default/hypr/looknfeel.lua) and a theme does not touch it -- a theme ships + colours, and only the user's own hypr config overrides the rounding -- so the desktop around + this window has hard corners and a 7px card in the middle of it reads as a web page again. + These two tokens carry most of the app's corners. */ +.omarchy-linux { + --ad-radius: 0; + --ad-radius-sm: 0; +} + +/* The corners that do not come through those tokens, which is the whole of the rest on this + platform: `grep -rn border-radius app/src --include "*.css" | grep -v ad-radius`, less + window.css (the macOS frameless chrome, which no Linux window has) and the gallery designs + (this sheet is never loaded over one). A list and not a blanket `* { border-radius: 0 }`, + which would put a declaration on every cell of a table that can run to thousands of them. + Discs are left alone: the connection icon and the environment dot are shapes, not corners. */ +.omarchy-linux :is(code, #ad-busy, .ad-card-databases a), +.omarchy-linux #menu h1 #ad-connection-badge .ad-env { + border-radius: 0; +} From 2f16d59d1e06e8a6d7a48bcd44ddd675996b0b6d Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Mon, 2 Nov 2026 00:48:13 +0100 Subject: [PATCH 13/13] be: leave the inspector closed until it is asked for, -debug is not only for the console -debug turned the inspector on and opened it in the same breath, so every run it is good for -- dressing the window as another OS, reading the console in the log, clicking the app by hand -- began with an inspector window beside the app, to be closed before anything could be seen. macOS has never done that: setInspectable puts the page in Safari's Develop menu and opening it is yours. So Linux does the same now. F12 and Ctrl+Shift+I were already the way back after a close; they are the way in as well, which costs the one keystroke a Develop menu would have cost anyway. Console messages still reach the log whether it is open or not, which is the half of -debug that has to work without being asked for. --- CLAUDE.md | 12 +++++++----- launcher/README.md | 2 +- launcher/inspector_linux.go | 29 +++++++++++------------------ 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 551bed6..152af63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,11 +18,13 @@ this machine → Adminer Desktop — giving a real console, the DOM, network and breakpoints. It also logs diagnostics at startup (which UI delegate is attached, whether the inspector came up). -On Linux it is **WebKitGTK's own inspector**, which opens in a window of its own as the app -starts (`inspector_linux.go`). There is no Develop menu to call it back from, so **F12 or -Ctrl+Shift+I** reopens it after a close — handled on the GtkWindow, so it works with the -caret in an SQL textarea too. Every `console.log` and JavaScript error also goes to stdout -and so into the log, which is what you have when the page dies before the inspector is up. +On Linux it is **WebKitGTK's own inspector** (`inspector_linux.go`), which `-debug` makes +available without opening — as on macOS, where the page only appears in Safari's Develop +menu. **F12 or Ctrl+Shift+I** opens it, in a window of its own, and reopens it after a +close; the key is taken on the GtkWindow, so it works with the caret in an SQL textarea +too. Every `console.log` and JavaScript error goes to stdout and so into the log whether +the inspector is open or not, which is what you have when the page dies before you get to +it — and what makes `-debug` worth running when you only want the app dressed as another OS. Reach for it as soon as a symptom is visual or behavioural. `uiDelegate=(nil)` in that log found in one line what two rounds of guessing had missed. diff --git a/launcher/README.md b/launcher/README.md index d53ad46..f9f8cff 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -23,7 +23,7 @@ WebKitGTK) bring most of this themselves. | **downloads** | `installDownloads` | see below | | menu bar | `installMenu` | an unbundled binary gets a bare default menu. | | menu bar on Windows | `menu_windows.c` | webview makes a window with no menu. `SetMenu`, and its clicks taken from the window procedure. Not themed: Win32 draws it light in dark mode too. | -| Web Inspector (`-debug` only) | `enableInspector` | no console or breakpoints otherwise. Real on both macOS (Safari's, over `setInspectable`) and Linux (WebKitGTK's own window, plus F12 to reopen it and console messages to stdout — `inspector_linux.go`); only WebView2 brings its own, on F12. | +| Web Inspector (`-debug` only) | `enableInspector` | no console or breakpoints otherwise. Real on both macOS (Safari's, over `setInspectable`) and Linux (WebKitGTK's own, opened with F12 or Ctrl+Shift+I, with console messages to stdout either way — `inspector_linux.go`). Neither opens by itself; only WebView2 brings its own, on F12. | ## Downloads diff --git a/launcher/inspector_linux.go b/launcher/inspector_linux.go index b3d1ae4..ee9489f 100644 --- a/launcher/inspector_linux.go +++ b/launcher/inspector_linux.go @@ -18,16 +18,11 @@ static WebKitWebView *inspector_web_view(void *window) { return WEBKIT_WEB_VIEW(child); } -static gboolean show_inspector_idle(gpointer inspector) { - webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector)); - return G_SOURCE_REMOVE; -} - -// F12 or Ctrl+Shift+I reopens the inspector. Closing its window only closes it -- WebKit -// keeps the inspector itself alive and show() brings it back -- but with no menu bar on -// Linux and nothing like Safari's Develop menu, there was no way to ask for it again: the -// one route left was right-clicking the page for Inspect Element, which is no help at all -// on a page whose own context menu is the thing being debugged. +// F12 or Ctrl+Shift+I opens the inspector, and reopens it after a close -- closing its window +// only closes it, WebKit keeps the inspector itself alive and show() brings it back. This is +// the only way in on Linux: there is no menu bar to hang a Develop menu on, and right-clicking +// the page for Inspect Element is no help when the page's own context menu is what is being +// debugged. // // Connected to the window rather than the webview: GTK offers a key press to handlers on // the toplevel before its default handler forwards it to whatever has focus, so this fires @@ -60,12 +55,10 @@ static int enable_inspector(void *window) { if (!inspector) { return 0; } - // Shown from an idle callback, not here: this runs before the GTK main loop starts and - // before the window itself is shown. G_SOURCE_REMOVE so it fires once -- and through a - // wrapper rather than casting webkit_web_inspector_show to GSourceFunc, which returns - // void: that cast leaves the return value to whatever is in the register, and a stray - // TRUE would keep the source alive and reopen the inspector on every idle turn. - g_idle_add(show_inspector_idle, inspector); + // Available, not open. -debug is also how the window is dressed as another OS and how the + // page is checked by hand, and an inspector taking half the window on every one of those + // runs is in the way; macOS does the same, where setInspectable only puts the page in + // Safari's Develop menu and opening it is yours. The key below is the switch. g_signal_connect(GTK_WIDGET(window), "key-press-event", G_CALLBACK(on_inspector_key), inspector); return 1; } @@ -86,8 +79,8 @@ import "C" import "unsafe" -// Where to look once it is on. GTK has no Develop menu, so the inspector's own window is it. -const inspectorHint = "its own window — F12 or Ctrl+Shift+I reopens it after a close, and console messages also go to the log" +// How to open it, GTK having no Develop menu to find it in. +const inspectorHint = "press F12 or Ctrl+Shift+I for it, and console messages go to the log either way" // enableInspector turns WebKitGTK's Web Inspector on for the app's page. Debug builds only: // a page with developer extras in a shipped app is an open door, the same reason the macOS