Skip to content

Commit ea92046

Browse files
committed
Route aether:// silent=true through the CLI apply path
silent=true now applies directly inside the URL-handler process via writer.ApplyTheme, matching aether --import-colors-toml semantics. No GUI launch, no IPC, no staged pending file. Wallpaper-only silent links preserve the current palette by reading the applied colors.toml on disk.
1 parent 9267660 commit ea92046

4 files changed

Lines changed: 100 additions & 19 deletions

File tree

cli/cli.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func Run(args []string, templatesFS embed.FS) int {
3333
case "--import-colors-toml":
3434
return runImportColorsToml(args[1:], templatesFS)
3535
case "--handle-url":
36-
return runHandleURL(args[1:])
36+
return runHandleURL(args[1:], templatesFS)
3737

3838
// --- Color utilities ---
3939
case "--color-convert":

cli/url_handler.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package cli
22

33
import (
4+
"embed"
45
"fmt"
56
"net/url"
67
"os"
78
"os/exec"
9+
"path/filepath"
810
"strings"
911
"time"
1012

13+
"aether/internal/blueprint"
14+
"aether/internal/omarchy"
1115
"aether/internal/pending"
16+
"aether/internal/platform"
17+
"aether/internal/theme"
1218
"aether/internal/wallpaper"
1319
"aether/ipc"
1420
)
@@ -23,7 +29,7 @@ import (
2329
// aether://apply?colors=https://…/colors.toml
2430
// aether://apply?wallpaper=https://…/wp.jpg
2531
// aether://apply?colors=…&wallpaper=…
26-
func runHandleURL(args []string) int {
32+
func runHandleURL(args []string, templatesFS embed.FS) int {
2733
if len(args) == 0 {
2834
fmt.Fprintln(os.Stderr, "Usage: aether --handle-url <aether://...>")
2935
return 1
@@ -92,6 +98,13 @@ func runHandleURL(args []string) int {
9298
return 1
9399
}
94100

101+
// Silent mode: apply directly in this process, no GUI, no dialog.
102+
// Matches `aether --import-colors-toml` semantics — first-party flows
103+
// only, since any web page can construct silent URLs.
104+
if imp.Silent {
105+
return runSilentApply(&imp, templatesFS)
106+
}
107+
95108
if err := pending.Write(&imp); err != nil {
96109
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
97110
return 1
@@ -129,3 +142,78 @@ func runHandleURL(args []string) int {
129142
fmt.Println("Launching Aether.")
130143
return 0
131144
}
145+
146+
// runSilentApply runs the equivalent of `--import-colors-toml URL` directly
147+
// inside the URL-handler process: parse the downloaded palette source, build
148+
// a ThemeState, and call writer.ApplyTheme. When only a wallpaper was given,
149+
// the current colors.toml on disk is reused so the existing palette is kept
150+
// instead of clobbered. Never touches the GUI or IPC.
151+
func runSilentApply(imp *pending.Import, templatesFS embed.FS) int {
152+
var palette [16]string
153+
var bp *blueprint.Blueprint
154+
var err error
155+
156+
switch {
157+
case imp.ExternalTheme != "":
158+
bp, err = blueprint.ImportJSON(imp.ExternalTheme)
159+
case imp.ColorsToml != "":
160+
bp, err = blueprint.ImportColorsToml(imp.ColorsToml)
161+
}
162+
if err != nil {
163+
fmt.Fprintf(os.Stderr, "Error: parse: %v\n", err)
164+
return 1
165+
}
166+
167+
extended := map[string]string{}
168+
if bp != nil {
169+
for i := 0; i < 16 && i < len(bp.Palette.Colors); i++ {
170+
palette[i] = bp.Palette.Colors[i]
171+
}
172+
for k, v := range bp.Palette.ExtendedColors {
173+
extended[k] = v
174+
}
175+
} else {
176+
// Wallpaper-only silent apply: preserve current palette by reading
177+
// the existing applied colors.toml. Falls back to a default-ish
178+
// state when nothing has been applied yet.
179+
existing := filepath.Join(platform.ThemeDir(), "colors.toml")
180+
if data, err := os.ReadFile(existing); err == nil {
181+
current, bg, fg := omarchy.ParseColorsToml(string(data))
182+
palette = current
183+
if bg != "" {
184+
extended["background"] = bg
185+
}
186+
if fg != "" {
187+
extended["foreground"] = fg
188+
}
189+
} else {
190+
fmt.Fprintln(os.Stderr, "Warning: no existing colors.toml to preserve; palette will be empty")
191+
}
192+
}
193+
194+
// mode= is the only signal we have for light/dark in the silent CLI
195+
// path. Omit → false (matches existing --import-colors-toml default).
196+
lightMode := imp.Mode == "light"
197+
198+
colorRoles := MapColorsToRoles(palette)
199+
writer := theme.NewWriter(templatesFS, "templates")
200+
state := &theme.ThemeState{
201+
Palette: palette,
202+
WallpaperPath: imp.Wallpaper,
203+
LightMode: lightMode,
204+
ColorRoles: colorRoles,
205+
ExtendedColors: extended,
206+
}
207+
208+
fmt.Println("Applying theme silently...")
209+
result, err := writer.ApplyTheme(state, theme.Settings{})
210+
if err != nil {
211+
fmt.Fprintf(os.Stderr, "Error: apply: %v\n", err)
212+
return 1
213+
}
214+
if result.Success {
215+
fmt.Println("Theme applied successfully")
216+
}
217+
_ = pending.Clear() // best-effort cleanup of any prior staged file
218+
return 0
219+
}

docs/web-handler.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ If Aether is closed when the link is clicked, the launch is automatic and the di
7777

7878
### `silent=true` — apply without confirming
7979

80-
`silent=true` makes the click apply immediately, no dialog. Useful for in-app links where the user has already chosen the theme on the web side and doesn't need a second confirmation. The trade-off is real, though: any web page can produce a silent-apply link. Only mark links silent inside flows where the user has already opted in to the theme they're about to install (your own theme gallery, an in-app catalog), and prefer the default interactive flow for third-party content.
80+
`silent=true` makes the click apply immediately, no dialog. The URL handler runs the apply *itself*, in the same process — same code path as `aether --import-colors-toml URL`. No GUI is launched and no IPC happens; this works identically whether or not the Aether GUI is running. The downloaded files still land in `~/.cache/aether/web-imports/`, but the staging file and confirmation step are skipped.
81+
82+
Wallpaper-only silent links preserve the current palette by reading the existing `colors.toml` on disk. Light/dark mode is controlled by `mode=light|dark`; omit it to default to dark, matching the existing CLI behavior.
83+
84+
The trade-off is real, though: any web page can produce a silent-apply link. Only mark links silent inside flows where the user has already opted in to the theme they're about to install (your own theme gallery, an in-app catalog), and prefer the default interactive flow for third-party content.
8185

8286
## What's not supported
8387

external_import.go

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ type ExternalImportPreview struct {
3535
Mode string `json:"mode,omitempty"` // "light" | "dark" | ""
3636
}
3737

38-
// loadPendingImport reads the staged file, stores it in memory, and either
39-
// emits "external-import-requested" (interactive — the dialog will pick it
40-
// up) or applies it immediately when imp.Silent is set. Safe to call from
41-
// startup and from the IPC handler — repeated calls just refresh the
42-
// in-memory snapshot.
38+
// loadPendingImport reads the staged file, stores it in memory, and emits
39+
// "external-import-requested" to the frontend. Safe to call from startup
40+
// and from the IPC handler — repeated calls just refresh the in-memory
41+
// snapshot and re-emit. Silent imports never reach the GUI (the CLI URL
42+
// handler applies them directly), so no branching is needed here.
4343
func (a *App) loadPendingImport() {
4444
imp, err := pending.Read()
4545
if err != nil {
@@ -54,17 +54,6 @@ func (a *App) loadPendingImport() {
5454
a.pending.curr = imp
5555
a.pending.mu.Unlock()
5656

57-
if imp.Silent {
58-
// Off the IPC/startup goroutine so the caller returns promptly;
59-
// writer.ApplyTheme can take a moment when many target files exist.
60-
go func() {
61-
if err := a.ConfirmExternalImport(); err != nil {
62-
log.Printf("pending-import: silent apply failed: %v", err)
63-
}
64-
}()
65-
return
66-
}
67-
6857
if a.ctx != nil {
6958
preview := a.buildPreview(imp)
7059
wailsrt.EventsEmit(a.ctx, "external-import-requested", preview)

0 commit comments

Comments
 (0)