Make the desktop app a --app flag instead of a format

Wrapping a packed viewer in a .app or .AppImage was its own --format app
value, parallel to zim and binary. But an app is really just the binary
format with a bundle around it, so a separate format meant duplicating the
base/icon handling and made the three formats feel like an awkward choice.

Turn it into a --app flag that builds on the binary format. It composes
with --base (including a webview base) and --icon, while --format stays
zim or binary. The bundle builders are unchanged; only the CLI surface
moves.
This commit is contained in:
Duc-Tam Nguyen
2026-06-15 01:23:08 +07:00
parent 8b8331c435
commit b5f32b7b2b
5 changed files with 35 additions and 29 deletions
+9 -8
View File
@@ -16,14 +16,15 @@ All notable changes to kage are recorded here. The format follows
- Container-aware Chrome flags. kage detects a container from the `IN_DOCKER` environment variable or a `/.dockerenv` marker and, only there, drops the sandbox and adds `--disable-dev-shm-usage` (the default 64 MB `/dev/shm` is too small for Chrome on large pages). Outside a container the faster shared memory is left in place.
- Asset downloads retry on a transient failure (a 403/429, a 5xx, or a network blip) with a short backoff, recovering files that bot-protection rejects on the first request of a burst. Permanent failures (404, 401, ...) are not retried.
- `kage pack --format app` wraps the packed viewer in a double-click desktop app
with the site's favicon as the icon. On macOS it writes a `.app` bundle
(`Info.plist`, the viewer under `Contents/MacOS`, and an `.icns` generated from
the favicon); on Linux, with a Linux `--base`, it writes an AppImage-style
`.AppDir` and folds it into a single `.AppImage` when `appimagetool` is
installed. The icon is found in the mirror automatically (preferring a large
`apple-touch-icon.png`, then `favicon.png` or a PNG-based `favicon.ico`) and
can be overridden with `--icon`.
- `kage pack --app` wraps the packed viewer in a double-click desktop app with
the site's favicon as the icon. The flag builds on the binary format, so it
composes with `--base` (including a `webview` base) and `--icon`. On macOS it
writes a `.app` bundle (`Info.plist`, the viewer under `Contents/MacOS`, and an
`.icns` generated from the favicon); on Linux, with a Linux `--base`, it writes
an AppImage-style `.AppDir` and folds it into a single `.AppImage` when
`appimagetool` is installed. The icon is found in the mirror automatically
(preferring a large `apple-touch-icon.png`, then `favicon.png` or a PNG-based
`favicon.ico`) and can be overridden with `--icon`.
- The release now ships a GUI-subsystem Windows base,
`kage_<version>_windows-gui_<arch>.zip`. Packing a viewer onto it with
`--format binary --base` produces a `.exe` that opens with no console window
+4 -4
View File
@@ -160,22 +160,22 @@ The trade is size. The binary carries a whole kage, so it weighs around 13 MiB p
### A double-click app
A bare binary is great from a terminal, but double-click it in a file manager and the experience is rough: macOS opens a Terminal window behind the site, and on Windows a console flashes up next to it. `--format app` wraps the same viewer in a proper desktop app so a double-click just opens the site, no terminal, with the mirror's own favicon as the icon.
A bare binary is great from a terminal, but double-click it in a file manager and the experience is rough: macOS opens a Terminal window behind the site, and on Windows a console flashes up next to it. Add `--app` and kage wraps the same viewer in a proper desktop app so a double-click just opens the site, no terminal, with the mirror's own favicon as the icon.
On macOS you get a real `.app` bundle:
```bash
kage pack paulgraham.com --format app # -> paulgraham.app
kage pack paulgraham.com --app # -> paulgraham.app
open paulgraham.app # or double-click it in Finder
```
On Linux, point `--base` at a Linux kage and you get an [AppImage](https://appimage.org)-style `.AppDir` with a `.desktop` launcher (`Terminal=false`, so no console). If [`appimagetool`](https://github.com/AppImage/appimagetool) is installed, kage folds it into a single double-clickable `.AppImage` for you:
```bash
kage pack paulgraham.com --format app --base kage-linux-amd64 # -> paulgraham.AppDir (+ .AppImage)
kage pack paulgraham.com --app --base kage-linux-amd64 # -> paulgraham.AppDir (+ .AppImage)
```
kage finds the icon by digging the favicon out of the mirror (it prefers a large `apple-touch-icon.png` and falls back to `favicon.ico`); pass `--icon some.png` to override it. Pair `--format app` with a `webview` base (below) and the double-click opens a native window instead of the browser, which is the full "it's an app" effect.
kage finds the icon by digging the favicon out of the mirror (it prefers a large `apple-touch-icon.png` and falls back to `favicon.ico`); pass `--icon some.png` to override it. Pair `--app` with a `webview` base (below) and the double-click opens a native window instead of the browser, which is the full "it's an app" effect.
Windows needs no bundle, because there a single `.exe` already is the app. The catch is the console window. The release ships a `kage_<version>_windows-gui_<arch>.zip` whose binary is linked for the GUI subsystem, so a viewer packed onto it opens with no console behind it:
+16 -11
View File
@@ -21,6 +21,7 @@ type packFlags struct {
format string
out string
base string
app bool
icon string
noCompress bool
title string
@@ -38,18 +39,19 @@ func newPackCmd() *cobra.Command {
"it writes an open ZIM archive (the format Kiwix uses) that kage open or any\n" +
"ZIM reader can browse. With --format binary it appends that archive to a copy\n" +
"of kage, producing a single executable that serves the site offline when run.\n" +
"With --format app it wraps that executable in a macOS .app you can double-click,\n" +
"with the site's favicon as the icon.",
"Add --app to wrap that executable in a double-click desktop app (a .app bundle\n" +
"on macOS, an AppImage-style .AppDir on Linux) with the site's favicon as the icon.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runPack(args[0], f)
},
}
fs := cmd.Flags()
fs.StringVar(&f.format, "format", "zim", "output format: zim, binary, or app")
fs.StringVar(&f.format, "format", "zim", "output format: zim or binary")
fs.StringVarP(&f.out, "out", "o", "", "output path (default per format)")
fs.StringVar(&f.base, "base", "", "base kage binary for binary/app (default this kage)")
fs.StringVar(&f.icon, "icon", "", "icon file for --format app (default the site's favicon)")
fs.StringVar(&f.base, "base", "", "base kage binary for the viewer (default this kage)")
fs.BoolVar(&f.app, "app", false, "wrap the viewer in a double-click desktop app (.app on macOS, .AppImage/.AppDir on Linux)")
fs.StringVar(&f.icon, "icon", "", "icon file for --app (default the site's favicon)")
fs.BoolVar(&f.noCompress, "no-compress", false, "store every cluster raw, no zstd")
fs.StringVar(&f.title, "title", "", "archive title (default the main page's <title>)")
fs.StringVar(&f.description, "description", "", "archive description")
@@ -70,6 +72,12 @@ func runPack(mirrorArg string, f *packFlags) error {
Version: Version,
}
// --app wraps the packed viewer in a desktop bundle. It builds on the binary
// format, so it owns the flow rather than being one more --format value.
if f.app {
return runPackApp(dir, f, zopts)
}
switch f.format {
case "zim":
out, size, err := pack.BuildZIM(dir, zopts)
@@ -103,11 +111,8 @@ func runPack(mirrorArg string, f *packFlags) error {
printRunHint(path, target)
return nil
case "app":
return runPackApp(dir, f, zopts)
default:
return fmt.Errorf("unknown --format %q (want zim, binary, or app)", f.format)
return fmt.Errorf("unknown --format %q (want zim or binary)", f.format)
}
}
@@ -122,7 +127,7 @@ func runPackApp(dir string, f *packFlags, zopts pack.ZIMOptions) error {
return fmt.Errorf("a Windows app is just the .exe, with no bundle to build: use --format binary and a GUI base (kage built with -ldflags -H=windowsgui)")
case "":
if f.base != "" {
return fmt.Errorf("--format app could not tell which OS %q is for; pass a macOS or Linux kage as --base", f.base)
return fmt.Errorf("--app could not tell which OS %q is for; pass a macOS or Linux kage as --base", f.base)
}
// No base and an unknown runtime: fall through with the host's GOOS.
target = runtime.GOOS
@@ -148,7 +153,7 @@ func runPackApp(dir string, f *packFlags, zopts pack.ZIMOptions) error {
case "linux":
return packLinuxApp(zbytes, dir, f, prog, name, icon, iconSrc)
default:
return fmt.Errorf("--format app supports macOS and Linux bases; %s is not one of them", osLabel(target))
return fmt.Errorf("--app supports macOS and Linux bases; %s is not one of them", osLabel(target))
}
}
+5 -5
View File
@@ -101,12 +101,12 @@ xattr -d com.apple.quarantine ./paulgraham
## A double-click app
The self-contained binary is perfect from a terminal, but double-clicking it in a file manager is less tidy: on macOS Finder opens a Terminal window behind the site, and on Windows a console flashes alongside it. `--format app` wraps the same viewer in a real desktop app, so a double-click just opens the mirror with no terminal in sight, using the site's own favicon as the icon.
The self-contained binary is perfect from a terminal, but double-clicking it in a file manager is less tidy: on macOS Finder opens a Terminal window behind the site, and on Windows a console flashes alongside it. Add `--app` and kage wraps the same viewer in a real desktop app, so a double-click just opens the mirror with no terminal in sight, using the site's own favicon as the icon.
On macOS it writes a standard `.app` bundle:
```bash
kage pack paulgraham.com --format app
kage pack paulgraham.com --app
```
```
@@ -121,16 +121,16 @@ The bundle holds the packed viewer under `Contents/MacOS`, an `Info.plist` descr
On Linux, point `--base` at a Linux kage and you get an [AppImage](https://appimage.org)-style `.AppDir`: the viewer as `AppRun`, a `.desktop` launcher with `Terminal=false`, and the icon as a PNG. When [`appimagetool`](https://github.com/AppImage/appimagetool) is on your `PATH`, kage runs it for you and turns the directory into one double-clickable `.AppImage`; otherwise it leaves the `.AppDir` ready for any AppImage tool.
```bash
kage pack paulgraham.com --format app --base kage-linux-amd64 # -> paulgraham.AppDir (+ .AppImage)
kage pack paulgraham.com --app --base kage-linux-amd64 # -> paulgraham.AppDir (+ .AppImage)
```
kage picks the icon by digging through the mirror for the site's favicon. It prefers a large `apple-touch-icon.png` and falls back to `favicon.png` or a PNG-based `favicon.ico`; if a site only ships a legacy BMP `.ico` the bundle is built without a custom icon rather than with a mangled one. Override the choice with `--icon path/to/image.png`.
For the full "it's an app" effect, pair `--format app` with a `webview` base so the double-click opens a native window instead of the system browser:
For the full "it's an app" effect, pair `--app` with a `webview` base so the double-click opens a native window instead of the system browser:
```bash
make build-webview
kage pack paulgraham.com --format app --base bin/kage
kage pack paulgraham.com --app --base bin/kage
```
Windows needs no bundle, because there a single `.exe` already is the app. What it needs is to lose the console window. A normal build is console-attached (handy for the CLI, since that is where clone progress prints), so the release ships a second Windows binary linked for the GUI subsystem in `kage_<version>_windows-gui_<arch>.zip`. Pack a viewer onto that base and double-clicking the result opens the site with no console behind it:
+1 -1
View File
@@ -10,7 +10,7 @@ The authoritative, commit-level history lives in [`CHANGELOG.md`](https://github
Double-click apps, so a packed mirror opens like a real desktop app instead of a terminal program.
- **`kage pack --format app`** wraps the viewer in a double-click app with the site's favicon as its icon. On macOS that is a `.app` bundle; on Linux, with a Linux `--base`, an [AppImage](https://appimage.org)-style `.AppDir` that becomes a single `.AppImage` when `appimagetool` is installed. The icon is pulled from the mirror automatically, or set with `--icon`.
- **`kage pack --app`** wraps the viewer in a double-click app with the site's favicon as its icon. The flag builds on the binary format, so it composes with `--base` (including a `webview` base) and `--icon`. On macOS that is a `.app` bundle; on Linux, with a Linux `--base`, an [AppImage](https://appimage.org)-style `.AppDir` that becomes a single `.AppImage` when `appimagetool` is installed. The icon is pulled from the mirror automatically, or set with `--icon`.
- **A GUI-subsystem Windows base** ships in the release as `kage_<version>_windows-gui_<arch>.zip`. Pack a viewer onto it with `--format binary --base` and the resulting `.exe` opens with no console window behind it.
- **Smarter cross-platform packing.** kage reads the base binary's executable header to detect its target OS, so a Windows viewer always gets a `.exe` name and the right run hint, regardless of how the base file is named.