<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Billot_P</title>
    <description>The latest articles on DEV Community by Billot_P (@billotp).</description>
    <link>https://dev.to/billotp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F363164%2Fffdd3045-2e14-42d4-a551-cb129b131ff4.jpeg</url>
      <title>DEV Community: Billot_P</title>
      <link>https://dev.to/billotp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/billotp"/>
    <language>en</language>
    <item>
      <title>Ship a desktop Linux app as one Go binary: webview, tray, and a pacman package</title>
      <dc:creator>Billot_P</dc:creator>
      <pubDate>Sat, 14 Nov 2026 09:26:03 +0000</pubDate>
      <link>https://dev.to/billotp/ship-a-desktop-linux-app-as-one-go-binary-webview-tray-and-a-pacman-package-2pk3</link>
      <guid>https://dev.to/billotp/ship-a-desktop-linux-app-as-one-go-binary-webview-tray-and-a-pacman-package-2pk3</guid>
      <description>&lt;p&gt;If your product is a web UI, the usual desktop story is Electron. You ship&lt;br&gt;
a browser, double the memory footprint, and keep a Node toolchain around.&lt;br&gt;
That model is microslop by construction: a thin app-shaped crust over a&lt;br&gt;
rented runtime, spending your users' RAM to pad someone else's platform&lt;br&gt;
metrics. Linux offers a leaner route. Keep the web UI, wrap it in the&lt;br&gt;
system's WebKitGTK through a webview library, add a tray icon, and package&lt;br&gt;
the result with the same &lt;code&gt;PKGBUILD&lt;/code&gt; machinery Arch uses for everything else.&lt;br&gt;
The outcome is one Go binary at ~15 MB, with no bundled browser and no CGO.&lt;/p&gt;

&lt;p&gt;This is how it's built, taken from a real app: a Go single-binary inventory&lt;br&gt;
tool with a Svelte UI. Everything below compiles and runs. The snippets are&lt;br&gt;
the parts that matter.&lt;/p&gt;

&lt;p&gt;Talk is cheap, show me the code: everything below, stripped to a runnable ~270-line skeleton, lives in &lt;a href="https://git.buildr.fun/buildrfun/go-webview-hello" rel="noopener noreferrer"&gt;go-webview-hello&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of the thing
&lt;/h2&gt;

&lt;p&gt;Three components in one process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an embedded SPA (&lt;code&gt;//go:embed&lt;/code&gt; of the built frontend),&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;http.Server&lt;/code&gt; bound to loopback serving that SPA plus a JSON API,&lt;/li&gt;
&lt;li&gt;a webview window pointing at &lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The webview is the WebKitGTK your distro already ships, loaded through&lt;br&gt;
&lt;code&gt;purego&lt;/code&gt; (dlopen-based FFI, so no CGO and cross-compilation stays simple).&lt;br&gt;
The server and the window are decoupled. The same binary builds as a&lt;br&gt;
headless CLI (&lt;code&gt;go build&lt;/code&gt;) and as a desktop app (&lt;code&gt;go build -tags desktop&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c1"&gt;//go:build desktop&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"github.com/crgimenes/glaze"&lt;/span&gt; &lt;span class="c1"&gt;// webview over WebKitGTK&lt;/span&gt;
    &lt;span class="s2"&gt;"fyne.io/systray"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;runUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;uiDeps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;glaze&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"webview: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SetTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"stow"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SetSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;860&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glaze&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HintNone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"http://"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// loopback server with the embedded SPA&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;uiDeps&lt;/code&gt; carries the running server and a &lt;code&gt;release func()&lt;/code&gt;. Nothing global&lt;br&gt;
crosses the boundary between the server layer and the UI layer, which pays&lt;br&gt;
off when the CLI face needs the same code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The close button is a trap
&lt;/h2&gt;

&lt;p&gt;The obvious way to handle the window's X button is to hook the destroy&lt;br&gt;
signal. It is wrong, and the failure mode is confusing: by the time&lt;br&gt;
&lt;code&gt;destroy&lt;/code&gt; fires, GTK has already decided to tear the window down. What you&lt;br&gt;
want is &lt;code&gt;delete-event&lt;/code&gt;, which fires first and lets you veto the deletion by&lt;br&gt;
returning &lt;code&gt;TRUE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I need the veto because "close" can't mean "quit" when the app lives in the&lt;br&gt;
tray. Closing the window should hide it while the in-process server keeps&lt;br&gt;
running, so the decision has to be intercepted. The webview library binds&lt;br&gt;
&lt;code&gt;destroy&lt;/code&gt; only, so the app dlopens GTK itself through purego and connects&lt;br&gt;
the handler directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sigConnect&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detailedSignal&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;uintptr&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;ensureGTK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gtkOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;purego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"libgtk-3.so.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;purego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RTLD_NOW&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;purego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RTLD_GLOBAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;gtkInitErr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"libgtk-3.so.0: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;purego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RegisterLibFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sigConnect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"g_signal_connect_data"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;purego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RegisterLibFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;widgetHide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"gtk_widget_hide"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;purego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RegisterLibFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;widgetShow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"gtk_widget_show"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gtkInitErr&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;delete-event&lt;/code&gt; handler runs on the UI thread, reads a small&lt;br&gt;
close-mode struct (&lt;code&gt;hide-to-tray&lt;/code&gt; vs &lt;code&gt;quit&lt;/code&gt;), and either hides the window or&lt;br&gt;
lets the event through. The snippet also needs a guard. Probe for&lt;br&gt;
&lt;code&gt;libwebkitgtk-6.0.so&lt;/code&gt; first and fail with an error if it is present, because&lt;br&gt;
loading GTK3 and GTK4 into one process corrupts the GObject type system.&lt;br&gt;
A process can hold only one GTK major version.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tray, and the threading rule that saves you an afternoon
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fyne.io/systray&lt;/code&gt; works over DBus, so it has no GTK dependency of its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c1"&gt;//go:embed packaging/icons/stow-32.png&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;trayPNG&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;trayReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;glaze&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;closeMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quit&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SetIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trayPNG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mShow&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddMenuItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Show window"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Bring the window back"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mQuit&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddMenuItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Quit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Stop everything and exit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;mShow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClickedCh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gtkShowWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// tray callbacks are NOT the UI thread&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;mQuit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClickedCh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tray quit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep this rule in mind: tray menu callbacks arrive on background&lt;br&gt;
goroutines, and every webview/GTK method must run on the UI thread. So every&lt;br&gt;
GTK-side effect goes through &lt;code&gt;w.Dispatch(...)&lt;/code&gt;. Skip this and you get&lt;br&gt;
intermittent segfaults that depend on click timing, which are hard to debug.&lt;/p&gt;
&lt;h2&gt;
  
  
  One shutdown funnel
&lt;/h2&gt;

&lt;p&gt;Three different exit paths (window close in quit mode, tray Quit,&lt;br&gt;
SIGINT/SIGTERM) must produce the same teardown: stop the sidecar, drain the&lt;br&gt;
HTTP server with a timeout, close the store, break the GTK loop. A &lt;code&gt;sync.Once&lt;/code&gt;&lt;br&gt;
shared by all three does the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;quitOnce&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Once&lt;/span&gt;
&lt;span class="n"&gt;quit&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;quitOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sidecar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;shCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpSrv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Terminate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// documented as background-safe&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Terminate&lt;/code&gt; is the one webview method that is safe to call from a&lt;br&gt;
non-UI goroutine, which is why the whole funnel runs in that goroutine&lt;br&gt;
instead of fighting the thread rule from the previous section.&lt;/p&gt;
&lt;h2&gt;
  
  
  Packaging: a PKGBUILD against your own checkout
&lt;/h2&gt;

&lt;p&gt;The desktop entry, the icons, and the binary install through the standard&lt;br&gt;
pacman machinery. The app appears in the launcher and follows the theme, and&lt;br&gt;
it uninstalls cleanly. The interesting part of the &lt;code&gt;PKGBUILD&lt;/code&gt; is the version&lt;br&gt;
rule: the version must move with the sources, or &lt;code&gt;pacman -U&lt;/code&gt; will happily&lt;br&gt;
"upgrade" you to the same version with stale binaries. Derive it from the&lt;br&gt;
checkout itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;_pkgrev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$(git -C "&lt;/span&gt;&lt;span class="nv"&gt;$startdir&lt;/span&gt;/..&lt;span class="s2"&gt;" rev-list --count HEAD 2&amp;gt;/dev/null || echo 0)"&lt;/span&gt;
&lt;span class="nv"&gt;_pkgsha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$(git -C "&lt;/span&gt;&lt;span class="nv"&gt;$startdir&lt;/span&gt;/..&lt;span class="s2"&gt;" rev-parse --short HEAD 2&amp;gt;/dev/null || echo unknown)"&lt;/span&gt;
&lt;span class="nv"&gt;pkgname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp
&lt;span class="nv"&gt;pkgver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.1.0.r${_pkgrev}.g${_pkgsha}"&lt;/span&gt;
&lt;span class="c"&gt;# 0.1.0.r24.gdda9a32 &amp;gt; 0.1.0.r23 &amp;gt; 0.1.0 under vercmp — every commit is a&lt;/span&gt;
&lt;span class="c"&gt;# strictly newer package&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build order matters. The Go binary embeds the SPA at compile time, so the&lt;br&gt;
frontend has to build first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;build&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"$startdir/.."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; web
  bun install --frozen-lockfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bun run build
  &lt;span class="nb"&gt;cd&lt;/span&gt; ..
  go build -trimpath -o build/myapp .
  go build -trimpath -tags desktop -o build/myapp-desktop .
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies are the system web stack, not a bundled browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;depends&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;webkit2gtk-4.1 gtk3 glib2 dbus&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;optdepends&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bubblewrap: sandbox the embedded web content'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;makedepends&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'go'&lt;/span&gt; &lt;span class="s1"&gt;'bun'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then come the usual desktop-entry and icon installs. The build and install&lt;br&gt;
is &lt;code&gt;cd packaging &amp;amp;&amp;amp; makepkg -f &amp;amp;&amp;amp; sudo pacman -U myapp-*.pkg.tar.zst&lt;/code&gt;. Launch&lt;br&gt;
it, and the app shows up in your launcher with its own icon. &lt;code&gt;pacman -R&lt;/code&gt;&lt;br&gt;
removes every file it ever laid down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up, and why it is fine
&lt;/h2&gt;

&lt;p&gt;There is no OS-level autoupdate (the package manager is the updater —&lt;br&gt;
rebuild and &lt;code&gt;pacman -U&lt;/code&gt;), no code signing, and the webview is WebKitGTK&lt;br&gt;
rather than Chromium, so test your CSS against it (Svelte apps in practice&lt;br&gt;
need nothing special). In exchange you get one binary, no Node runtime at&lt;br&gt;
application time, and a memory footprint your users won't complain about.&lt;br&gt;
Desktop integration also comes from the distro rather than from JavaScript.&lt;br&gt;
For a tool-shaped app on Linux, I'd take that trade every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glossary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Electron&lt;/strong&gt;: framework that bundles Chromium and Node.js to run desktop
apps written in JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node / Node.js&lt;/strong&gt;: JavaScript runtime that executes outside the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM&lt;/strong&gt;: the computer's working memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webview&lt;/strong&gt;: a web-display component embedded inside a native application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebKitGTK&lt;/strong&gt;: WebKit-based rendering engine for GTK applications, shipped
by your distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GTK&lt;/strong&gt;: the graphical toolkit behind GNOME and many Linux applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GObject&lt;/strong&gt;: the type and object system GTK is built on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal (GTK)&lt;/strong&gt;: a notification emitted by a GTK object, such as
&lt;code&gt;destroy&lt;/code&gt; or &lt;code&gt;delete-event&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CGO&lt;/strong&gt;: Go's mechanism for calling C code; it complicates
cross-compilation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFI&lt;/strong&gt; (foreign function interface): calling a native library from
another language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dlopen&lt;/strong&gt;: system call that loads a shared library at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;purego&lt;/strong&gt;: Go library that calls C libraries through dlopen, with no CGO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPA&lt;/strong&gt; (single-page application): a UI loaded once, then driven by
JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Svelte&lt;/strong&gt;: JavaScript framework for building user interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS&lt;/strong&gt;: the web's page-styling language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP&lt;/strong&gt;: the client-server communication protocol of the web.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON API&lt;/strong&gt;: an HTTP interface that exchanges data as JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON&lt;/strong&gt;: plain-text format for exchanging structured data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loopback&lt;/strong&gt;: the 127.0.0.1 network interface, reachable only from the
local machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt;: command-line interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Systray&lt;/strong&gt; (system tray): the desktop area showing background-app icons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DBus&lt;/strong&gt;: Linux inter-process message bus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidecar&lt;/strong&gt;: auxiliary process launched by the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt;: the app's persistence layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI thread&lt;/strong&gt;: the thread owning the graphical interface — the only one
allowed to call GTK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goroutine&lt;/strong&gt;: lightweight task scheduled by the Go runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sync.Once&lt;/strong&gt;: Go primitive guaranteeing a function runs exactly once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segfault&lt;/strong&gt;: memory error that crashes a program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIGINT / SIGTERM&lt;/strong&gt;: Unix interruption (Ctrl+C) and termination signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PKGBUILD&lt;/strong&gt;: package build recipe for Arch Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;makepkg&lt;/strong&gt;: tool that runs a PKGBUILD and produces a package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pacman&lt;/strong&gt;: Arch Linux package manager.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>linux</category>
      <category>gtk</category>
      <category>desktop</category>
    </item>
  </channel>
</rss>
