-
Notifications
You must be signed in to change notification settings - Fork 31
Expand file tree
/
Copy pathexternal_import.go
More file actions
262 lines (235 loc) · 8.14 KB
/
Copy pathexternal_import.go
File metadata and controls
262 lines (235 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package main
import (
"fmt"
"log"
"os"
"sync"
"aether/internal/blueprint"
"aether/internal/pending"
"aether/internal/theme"
wailsrt "github.com/wailsapp/wails/v2/pkg/runtime"
)
// pendingImportState holds the most recent pending import surfaced to the
// frontend. The dialog reads it via GetPendingExternalImport, then either
// confirms (applying the assets verbatim, no extraction) or cancels.
type pendingImportState struct {
mu sync.Mutex
curr *pending.Import
}
// ExternalImportPreview is the payload sent to the frontend so the
// confirmation dialog can render swatches + wallpaper preview without making
// extra calls back into Go.
type ExternalImportPreview struct {
HasExternalTheme bool `json:"has_external_theme"`
HasColors bool `json:"has_colors"`
HasWallpaper bool `json:"has_wallpaper"`
SourceURL string `json:"source_url"`
Palette []string `json:"palette,omitempty"`
Wallpaper string `json:"wallpaper,omitempty"`
ThemeName string `json:"theme_name,omitempty"`
Mode string `json:"mode,omitempty"` // "light" | "dark" | ""
Edit bool `json:"edit"` // load into the editor without applying (aether://...&edit=true)
OmarchyThemeName string `json:"omarchy_theme_name,omitempty"`
}
// loadPendingImport reads the staged file, stores it in memory, and emits
// "external-import-requested" to the frontend. Safe to call from startup
// and from the IPC handler — repeated calls just refresh the in-memory
// snapshot and re-emit. Browser imports always reach this confirmation path.
func (a *App) loadPendingImport() {
imp, err := pending.Read()
if err != nil {
log.Printf("pending-import: %v", err)
return
}
if imp == nil {
return
}
a.pending.mu.Lock()
a.pending.curr = imp
a.pending.mu.Unlock()
if a.ctx != nil {
preview := a.buildPreview(imp)
wailsrt.EventsEmit(a.ctx, "external-import-requested", preview)
}
}
// buildPreview reads the staged assets enough to populate the dialog.
// For external_theme + colors it parses the file so swatches can be shown.
// Errors degrade gracefully (preview shows what it can).
func (a *App) buildPreview(imp *pending.Import) ExternalImportPreview {
preview := ExternalImportPreview{
SourceURL: imp.SourceURL,
HasExternalTheme: imp.ExternalTheme != "",
HasColors: imp.ColorsToml != "",
HasWallpaper: imp.Wallpaper != "",
Wallpaper: imp.Wallpaper,
Mode: imp.Mode,
Edit: imp.Edit,
OmarchyThemeName: imp.OmarchyThemeName,
}
if imp.ExternalTheme != "" {
if bp, err := blueprint.ImportJSON(imp.ExternalTheme); err == nil {
preview.ThemeName = bp.Name
preview.Palette = clonePalette(bp.Palette.Colors)
} else {
log.Printf("pending-import: parse external_theme: %v", err)
}
} else if imp.ColorsToml != "" {
if bp, err := blueprint.ImportColorsToml(imp.ColorsToml); err == nil {
preview.ThemeName = bp.Name
preview.Palette = clonePalette(bp.Palette.Colors)
} else {
log.Printf("pending-import: parse colors.toml: %v", err)
}
}
return preview
}
// GetPendingExternalImport returns the active preview for the dialog. nil
// (zero value) when nothing is pending — the frontend mounts the dialog on
// the event, then pulls this for the actual payload.
func (a *App) GetPendingExternalImport() *ExternalImportPreview {
a.pending.mu.Lock()
imp := a.pending.curr
a.pending.mu.Unlock()
if imp == nil {
return nil
}
preview := a.buildPreview(imp)
return &preview
}
// stageImportIntoState consumes the pending import and writes its assets into
// the app state: the colors.toml (or external_theme JSON) becomes the palette
// without re-extraction, the wallpaper is set
// as the background, and light/dark mode is resolved. It pushes an undo snapshot
// and clears the handoff file, but does NOT apply the theme to disk or emit to
// the frontend; callers decide whether to apply (ConfirmExternalImport) or just
// load it for editing (OpenExternalImportInEditor).
func (a *App) stageImportIntoState(expectedSourceURL string) (*pending.Import, error) {
a.pending.mu.Lock()
imp := a.pending.curr
if imp == nil || imp.SourceURL != expectedSourceURL {
a.pending.mu.Unlock()
return nil, fmt.Errorf("pending import changed; review the latest request")
}
a.pending.curr = nil
a.pending.mu.Unlock()
defer pending.Clear()
a.history.Push(*a.state)
var bp *blueprint.Blueprint
var err error
switch {
case imp.ExternalTheme != "":
bp, err = blueprint.ImportJSON(imp.ExternalTheme)
case imp.ColorsToml != "":
bp, err = blueprint.ImportColorsToml(imp.ColorsToml)
}
if err != nil {
return nil, fmt.Errorf("parse import: %w", err)
}
if bp == nil && imp.OmarchyThemeName != "" {
if current, loadErr := blueprint.ImportCurrentColorsToml(); loadErr == nil {
bp = current
} else {
return nil, fmt.Errorf("load current palette for wallpaper-only install: %w", loadErr)
}
}
if bp != nil {
// Merge extended colors before SetPalette so buildColorRoles uses the
// imported accent/cursor/selection instead of re-deriving them.
a.state.ExtendedColors = make(map[string]string, len(bp.Palette.ExtendedColors))
for k, v := range bp.Palette.ExtendedColors {
a.state.ExtendedColors[k] = v
}
a.state.NativeColors = make(map[string]string, len(bp.Palette.NativeColors))
for k, v := range bp.Palette.NativeColors {
a.state.NativeColors[k] = v
}
var palette [16]string
for i := 0; i < 16 && i < len(bp.Palette.Colors); i++ {
palette[i] = bp.Palette.Colors[i]
}
a.state.SetPalette(palette)
}
if imp.Wallpaper != "" {
if _, statErr := os.Stat(imp.Wallpaper); statErr == nil {
a.state.WallpaperPath = imp.Wallpaper
}
}
// Light/dark precedence: explicit URL mode= wins. Otherwise use the
// colors.toml's own `mode = "..."` (parsed into bp.Palette.Mode, handles
// both light and dark explicitly). Otherwise an external_theme JSON may
// have set LightMode=true. Otherwise leave the current setting alone.
switch imp.Mode {
case "light":
a.state.LightMode = true
case "dark":
a.state.LightMode = false
default:
applied := false
if bp != nil {
switch bp.Palette.Mode {
case "light":
a.state.LightMode = true
applied = true
case "dark":
a.state.LightMode = false
applied = true
}
}
if !applied && bp != nil && bp.Palette.LightMode {
a.state.LightMode = true
}
}
return imp, nil
}
// ConfirmExternalImport stages the pending assets, then applies them or installs
// the requested named Omarchy theme. Used by the confirmation dialog.
func (a *App) ConfirmExternalImport(expectedSourceURL string) error {
imp, err := a.stageImportIntoState(expectedSourceURL)
if err != nil {
return err
}
if imp.OmarchyThemeName != "" {
if err := a.writer.InstallOmarchyTheme(a.state, theme.DefaultApplySettings(), imp.OmarchyThemeName); err != nil {
return fmt.Errorf("install Omarchy theme: %w", err)
}
a.emitIPCStateChanged()
return nil
}
result, err := a.writer.ApplyTheme(a.state, theme.DefaultApplySettings())
if err != nil {
return fmt.Errorf("apply: %w", err)
}
if !result.Success {
return fmt.Errorf("apply returned failure")
}
a.emitIPCStateChanged()
return nil
}
// OpenExternalImportInEditor loads the staged assets into the editor without
// applying them: the palette, extended colors, wallpaper, and light/dark mode
// become the current editing state and are pushed to the frontend, but nothing
// is written to disk. Backs the aether://...&edit=true flow so a user can tweak
// an imported theme and apply it manually when ready.
func (a *App) OpenExternalImportInEditor(expectedSourceURL string) error {
if _, err := a.stageImportIntoState(expectedSourceURL); err != nil {
return err
}
a.emitIPCStateChanged()
return nil
}
// CancelExternalImport drops the staged import without touching theme state.
func (a *App) CancelExternalImport(expectedSourceURL string) {
a.pending.mu.Lock()
if a.pending.curr == nil || a.pending.curr.SourceURL != expectedSourceURL {
a.pending.mu.Unlock()
return
}
a.pending.curr = nil
a.pending.mu.Unlock()
_ = pending.Clear()
}
func clonePalette(colors []string) []string {
out := make([]string, len(colors))
copy(out, colors)
return out
}