Skip to content

Commit 66c2aed

Browse files
committed
Add edit=true to aether:// to open imports in the editor
A theme link can now carry edit=true to load its colors and wallpaper into the Aether editor instead of applying or installing them. Nothing is written to disk until the user clicks Apply, so it's the safe way to ship a palette as a starting point rather than a finished theme. edit overrides silent and as_omarchy_theme. Backend: extract the shared stage-into-state logic from ConfirmExternalImport so the new OpenExternalImportInEditor reuses it (stage + emit, no apply). The URL handler routes edit/omarchy/silent/default through a single precedence switch. The confirm dialog is parameterized on the edit flag (heading, body, button, action).
1 parent f02c646 commit 66c2aed

8 files changed

Lines changed: 115 additions & 28 deletions

File tree

cli/url_handler.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ func runHandleURL(args []string, templatesFS embed.FS) int {
9898
if v := strings.ToLower(q.Get("silent")); v == "true" || v == "1" || v == "yes" {
9999
imp.Silent = true
100100
}
101+
if v := strings.ToLower(q.Get("edit")); v == "true" || v == "1" || v == "yes" {
102+
imp.Edit = true
103+
}
101104
if v := q.Get("as_omarchy_theme"); v != "" {
102105
if !safeThemeName.MatchString(v) {
103106
fmt.Fprintf(os.Stderr, "Error: as_omarchy_theme must match [A-Za-z0-9][A-Za-z0-9_.-]* (got %q)\n", v)
@@ -111,17 +114,23 @@ func runHandleURL(args []string, templatesFS embed.FS) int {
111114
return 1
112115
}
113116

114-
// Omarchy install: drop files into ~/.config/omarchy/themes/<name>/ and
115-
// run omarchy-theme-set. Always silent — installing into a system
116-
// location is the consent action by the publisher.
117-
if imp.OmarchyThemeName != "" {
117+
// Routing precedence, highest first:
118+
// edit — fall through to the GUI pending-import path below (load the
119+
// assets into the editor, apply nothing). Safest action, so it
120+
// wins over silent / as_omarchy_theme.
121+
// omarchy — install into ~/.config/omarchy/themes/<name>/ and run
122+
// omarchy-theme-set. Always silent: installing into a system
123+
// location is the publisher's consent action.
124+
// silent — apply directly in this process, no GUI, no dialog. Matches
125+
// `aether --import-colors-toml`; first-party flows only, since
126+
// any web page can construct silent URLs.
127+
// default — stage and hand off to the GUI confirm dialog (below).
128+
switch {
129+
case imp.Edit:
130+
// fall through to pending.Write + GUI handoff below
131+
case imp.OmarchyThemeName != "":
118132
return runOmarchyInstall(&imp, templatesFS)
119-
}
120-
121-
// Silent mode: apply directly in this process, no GUI, no dialog.
122-
// Matches `aether --import-colors-toml` semantics — first-party flows
123-
// only, since any web page can construct silent URLs.
124-
if imp.Silent {
133+
case imp.Silent:
125134
return runSilentApply(&imp, templatesFS)
126135
}
127136

docs/web-handler.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ aether://apply?<param>=<https-url>[&<param>=<https-url>...]
1717
| `wallpaper` | URL to an image | Sets the wallpaper (no re-extraction, even when used alone) |
1818
| `mode` | `light` or `dark` | Forces Aether into light or dark mode before applying. Omit to fall back to the colors.toml's own `mode = "..."` field, then to the current setting. |
1919
| `silent` | `true` | Skips the confirm dialog and applies immediately. Use with care: any web page can construct this URL. |
20+
| `edit` | `true` | Opens the colors and wallpaper in the Aether editor without applying. Nothing is written to disk until the user clicks Apply. Overrides `silent` and `as_omarchy_theme`. |
2021
| `as_omarchy_theme` | theme name | Installs into `~/.config/omarchy/themes/<name>/` and runs `omarchy-theme-set <name>`. Always silent. Name must match `[A-Za-z0-9][A-Za-z0-9_.-]*`. |
2122

22-
`external_theme` and `colors` are mutually exclusive; `external_theme` wins when both are present. `wallpaper` can be combined with either, or used alone. `mode` and `silent` can be combined with any of the above.
23+
`external_theme` and `colors` are mutually exclusive; `external_theme` wins when both are present. `wallpaper` can be combined with either, or used alone. `mode` and `silent` can be combined with any of the above. `edit` takes precedence over `silent` and `as_omarchy_theme`: if `edit=true` is present, the import always opens in the editor instead of applying or installing.
2324

2425
## Examples
2526

@@ -59,6 +60,12 @@ Install as a named Omarchy theme and activate it:
5960
aether://apply?colors=https://themes.example.com/nord/colors.toml&wallpaper=https://themes.example.com/nord/wp.jpg&as_omarchy_theme=nord
6061
```
6162

63+
Open in the editor to tweak before applying (nothing is written until you click Apply):
64+
65+
```
66+
aether://apply?colors=https://themes.example.com/nord/colors.toml&wallpaper=https://themes.example.com/nord/wp.jpg&edit=true
67+
```
68+
6269
## HTML button
6370

6471
```html
@@ -109,6 +116,14 @@ Wallpaper-only silent links preserve the current palette by reading the existing
109116

110117
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.
111118

119+
### `edit=true` — open in the editor without applying
120+
121+
`edit=true` is the opposite of `silent`: instead of applying, the import loads the palette and wallpaper into the Aether editor and switches to the editor tab. Nothing is written to disk, no target apps are touched, and the Omarchy themes directory is untouched. The user reviews the palette in the confirm dialog, clicks "Open in editor", tweaks colors or adjustments, then applies manually when ready.
122+
123+
It's the safest action, so it wins over the auto-apply flags: a link with both `edit=true` and `silent=true` (or `as_omarchy_theme=...`) opens in the editor and ignores the others. Use it for "customize this theme" buttons where the published palette is a starting point rather than a finished product.
124+
125+
The launch/IPC handoff is the same as the default interactive flow: if the GUI is running it's notified over IPC, otherwise it's launched and picks up the staged import on startup.
126+
112127
## What's not supported
113128

114129
- Other actions. Only `apply` is recognized; `aether://save?...`, `aether://preview?...`, etc. return an error.

external_import.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type ExternalImportPreview struct {
3434
Wallpaper string `json:"wallpaper,omitempty"`
3535
ThemeName string `json:"theme_name,omitempty"`
3636
Mode string `json:"mode,omitempty"` // "light" | "dark" | ""
37+
Edit bool `json:"edit"` // load into the editor without applying (aether://...&edit=true)
3738
}
3839

3940
// loadPendingImport reads the staged file, stores it in memory, and emits
@@ -72,6 +73,7 @@ func (a *App) buildPreview(imp *pending.Import) ExternalImportPreview {
7273
HasWallpaper: imp.Wallpaper != "",
7374
Wallpaper: imp.Wallpaper,
7475
Mode: imp.Mode,
76+
Edit: imp.Edit,
7577
}
7678

7779
if imp.ExternalTheme != "" {
@@ -107,11 +109,14 @@ func (a *App) GetPendingExternalImport() *ExternalImportPreview {
107109
return &preview
108110
}
109111

110-
// ConfirmExternalImport applies the staged assets verbatim: the colors.toml
111-
// (or external_theme JSON) becomes the palette without re-extracting, and
112-
// the wallpaper is set as the background. Auto-extraction is intentionally
113-
// skipped — the user already trusts the source.
114-
func (a *App) ConfirmExternalImport() error {
112+
// stageImportIntoState consumes the pending import and writes its assets into
113+
// the app state: the colors.toml (or external_theme JSON) becomes the palette
114+
// verbatim (no re-extraction — the user trusts the source), the wallpaper is set
115+
// as the background, and light/dark mode is resolved. It pushes an undo snapshot
116+
// and clears the handoff file, but does NOT apply the theme to disk or emit to
117+
// the frontend; callers decide whether to apply (ConfirmExternalImport) or just
118+
// load it for editing (OpenExternalImportInEditor).
119+
func (a *App) stageImportIntoState() error {
115120
a.pending.mu.Lock()
116121
imp := a.pending.curr
117122
a.pending.curr = nil
@@ -190,6 +195,17 @@ func (a *App) ConfirmExternalImport() error {
190195
}
191196
}
192197

198+
return nil
199+
}
200+
201+
// ConfirmExternalImport stages the pending assets into the editor state and then
202+
// applies the theme to all configured target apps. Used by the confirm dialog's
203+
// Apply button.
204+
func (a *App) ConfirmExternalImport() error {
205+
if err := a.stageImportIntoState(); err != nil {
206+
return err
207+
}
208+
193209
result, err := a.writer.ApplyTheme(a.state, theme.DefaultApplySettings())
194210
if err != nil {
195211
return fmt.Errorf("apply: %w", err)
@@ -201,6 +217,19 @@ func (a *App) ConfirmExternalImport() error {
201217
return nil
202218
}
203219

220+
// OpenExternalImportInEditor loads the staged assets into the editor without
221+
// applying them: the palette, extended colors, wallpaper, and light/dark mode
222+
// become the current editing state and are pushed to the frontend, but nothing
223+
// is written to disk. Backs the aether://...&edit=true flow so a user can tweak
224+
// an imported theme and apply it manually when ready.
225+
func (a *App) OpenExternalImportInEditor() error {
226+
if err := a.stageImportIntoState(); err != nil {
227+
return err
228+
}
229+
a.emitIPCStateChanged()
230+
return nil
231+
}
232+
204233
// CancelExternalImport drops the staged import without touching theme state.
205234
func (a *App) CancelExternalImport() {
206235
a.pending.mu.Lock()

frontend/src/lib/components/ExternalImportDialog.svelte

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script lang="ts">
22
import Modal from '$lib/components/shared/Modal.svelte';
3-
import {showToast} from '$lib/stores/ui.svelte';
3+
import {showToast, setActiveTab} from '$lib/stores/ui.svelte';
44
import {
55
loadFullImage,
66
getCachedFullImage,
77
} from '$lib/stores/imagecache.svelte';
88
import {
99
ConfirmExternalImport,
10+
OpenExternalImportInEditor,
1011
CancelExternalImport,
1112
GetPendingExternalImport,
1213
} from '../../../wailsjs/go/main/App';
@@ -22,11 +23,14 @@
2223
wallpaper?: string;
2324
theme_name?: string;
2425
mode?: string;
26+
edit?: boolean;
2527
};
2628
2729
let preview = $state<Preview | null>(null);
2830
let wallpaperDataUrl = $state('');
2931
let isApplying = $state(false);
32+
// edit=true imports load into the editor instead of applying.
33+
let isEdit = $derived(!!preview?.edit);
3034
3135
onMount(() => {
3236
// The backend emits this on startup when a staged file is present,
@@ -87,17 +91,27 @@
8791
}
8892
});
8993
90-
async function handleApply() {
94+
// edit=true links load the colors + wallpaper into the editor without
95+
// applying, so the user can tweak before hitting Apply themselves. Any
96+
// other link applies the theme immediately on confirm.
97+
async function handleConfirm() {
9198
if (!preview) return;
99+
const edit = isEdit; // snapshot before the await nulls preview
92100
isApplying = true;
93101
try {
94-
await ConfirmExternalImport();
95-
showToast('Applied');
102+
if (edit) {
103+
await OpenExternalImportInEditor();
104+
setActiveTab('editor');
105+
showToast('Loaded into editor');
106+
} else {
107+
await ConfirmExternalImport();
108+
showToast('Applied');
109+
}
96110
preview = null;
97111
} catch (err) {
98-
showToast('Failed to apply');
112+
showToast(edit ? 'Failed to load' : 'Failed to apply');
99113
// eslint-disable-next-line no-console
100-
console.error('external-import apply:', err);
114+
console.error('external-import confirm:', err);
101115
} finally {
102116
isApplying = false;
103117
}
@@ -116,13 +130,15 @@
116130
<Modal
117131
open={preview !== null}
118132
onclose={handleCancel}
119-
onenter={handleApply}
133+
onenter={handleConfirm}
120134
panelClass="w-[420px]"
121135
z="z-50"
122136
>
123137
{#if preview}
124138
<h3 class="text-fg-primary mb-1 text-[13px] font-medium">
125-
Apply theme from web?
139+
{isEdit
140+
? 'Open theme from web in editor?'
141+
: 'Apply theme from web?'}
126142
</h3>
127143
<p class="text-fg-dimmed mb-3 text-[10px] uppercase tracking-wider">
128144
{assetKind()}
@@ -163,8 +179,13 @@
163179
</p>
164180

165181
<p class="text-fg-dimmed mb-4 text-[10px]">
166-
Aether will replace your palette and background. Only apply from
167-
sources you trust.
182+
{#if isEdit}
183+
Aether will load these colors and wallpaper into the editor.
184+
Nothing is applied until you click Apply.
185+
{:else}
186+
Aether will replace your palette and background. Only apply from
187+
sources you trust.
188+
{/if}
168189
</p>
169190

170191
<div class="flex justify-end gap-2">
@@ -177,10 +198,14 @@
177198
</button>
178199
<button
179200
class="bg-accent hover:bg-accent-hover text-accent-fg px-3 py-1.5 text-[11px] font-medium disabled:opacity-50"
180-
onclick={handleApply}
201+
onclick={handleConfirm}
181202
disabled={isApplying}
182203
>
183-
{isApplying ? 'Applying...' : 'Apply'}
204+
{#if isEdit}
205+
{isApplying ? 'Opening...' : 'Open in editor'}
206+
{:else}
207+
{isApplying ? 'Applying...' : 'Apply'}
208+
{/if}
184209
</button>
185210
</div>
186211
{/if}

frontend/wailsjs/go/main/App.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export function LoadBlueprint(arg1: string): Promise<void>;
110110

111111
export function LoadOmarchyThemes(): Promise<Array<omarchy.Theme>>;
112112

113+
export function OpenExternalImportInEditor(): Promise<void>;
114+
113115
export function OpenFileDialog(): Promise<string>;
114116

115117
export function PreviewExtractColors(

frontend/wailsjs/go/main/App.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ export function LoadOmarchyThemes() {
174174
return window['go']['main']['App']['LoadOmarchyThemes']();
175175
}
176176

177+
export function OpenExternalImportInEditor() {
178+
return window['go']['main']['App']['OpenExternalImportInEditor']();
179+
}
180+
177181
export function OpenFileDialog() {
178182
return window['go']['main']['App']['OpenFileDialog']();
179183
}

frontend/wailsjs/go/models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export namespace main {
216216
wallpaper?: string;
217217
theme_name?: string;
218218
mode?: string;
219+
edit: boolean;
219220

220221
static createFrom(source: any = {}) {
221222
return new ExternalImportPreview(source);
@@ -231,6 +232,7 @@ export namespace main {
231232
this.wallpaper = source['wallpaper'];
232233
this.theme_name = source['theme_name'];
233234
this.mode = source['mode'];
235+
this.edit = source['edit'];
234236
}
235237
}
236238
export class ExtractFromImagesResult {

internal/pending/pending.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Import struct {
2222
Wallpaper string `json:"wallpaper,omitempty"`
2323
Mode string `json:"mode,omitempty"` // "light" | "dark" — empty leaves the current setting alone
2424
Silent bool `json:"silent,omitempty"` // skip the confirm dialog and apply immediately
25+
Edit bool `json:"edit,omitempty"` // load colors + wallpaper into the editor without applying (overrides silent/omarchy)
2526
OmarchyThemeName string `json:"omarchy_theme_name,omitempty"` // install as ~/.config/omarchy/themes/<name>/ and run omarchy-theme-set
2627
SourceURL string `json:"source_url,omitempty"`
2728
Timestamp int64 `json:"ts,omitempty"`

0 commit comments

Comments
 (0)