Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ func (a *App) SetExtractionMode(mode string) {
type SyncStateRequest struct {
Palette []string `json:"palette"`
WallpaperPath string `json:"wallpaperPath"`
OriginalWallpaperPath string `json:"originalWallpaperPath"`
LightMode bool `json:"lightMode"`
ExtendedColors map[string]string `json:"extendedColors"`
AppOverrides map[string]map[string]string `json:"appOverrides"`
Expand All @@ -240,6 +241,7 @@ func (a *App) SyncState(req SyncStateRequest) {
a.state.SetAdjustedPalette(p)
}
a.state.WallpaperPath = req.WallpaperPath
a.state.OriginalWallpaperPath = req.OriginalWallpaperPath
a.state.LightMode = req.LightMode
if req.ExtendedColors != nil {
a.state.ExtendedColors = req.ExtendedColors
Expand Down Expand Up @@ -321,6 +323,7 @@ func (a *App) ComputeVariables(paletteSlice []string, extendedColors map[string]
type ApplyThemeRequest struct {
Palette []string `json:"palette"`
WallpaperPath string `json:"wallpaperPath"`
OriginalWallpaperPath string `json:"originalWallpaperPath"`
LightMode bool `json:"lightMode"`
AdditionalImages []string `json:"additionalImages"`
ExtendedColors map[string]string `json:"extendedColors"`
Expand All @@ -340,6 +343,7 @@ func (a *App) ApplyTheme(req ApplyThemeRequest) (*theme.ApplyResult, error) {
state := &theme.ThemeState{
Palette: palette,
WallpaperPath: req.WallpaperPath,
OriginalWallpaperPath: req.OriginalWallpaperPath,
LightMode: req.LightMode,
ColorRoles: roles,
ExtendedColors: req.ExtendedColors,
Expand All @@ -357,6 +361,7 @@ type SaveAndApplyThemeRequest struct {
UpdateExisting bool `json:"updateExisting"`
Palette []string `json:"palette"`
WallpaperPath string `json:"wallpaperPath"`
OriginalWallpaperPath string `json:"originalWallpaperPath"`
LightMode bool `json:"lightMode"`
AdditionalImages []string `json:"additionalImages"`
ExtendedColors map[string]string `json:"extendedColors"`
Expand All @@ -380,6 +385,7 @@ func (a *App) SaveAndApplyTheme(req SaveAndApplyThemeRequest) (*theme.ApplyResul
state := &theme.ThemeState{
Palette: palette,
WallpaperPath: req.WallpaperPath,
OriginalWallpaperPath: req.OriginalWallpaperPath,
LightMode: req.LightMode,
ColorRoles: roles,
ExtendedColors: req.ExtendedColors,
Expand All @@ -401,7 +407,8 @@ func (a *App) SaveAndApplyTheme(req SaveAndApplyThemeRequest) (*theme.ApplyResul
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("check theme folder: %w", err)
}
if err := a.writer.GenerateOmarchyV4Only(state, req.Settings, targetDir); err != nil {
wallpaperDest, err := a.writer.GenerateOmarchyV4Only(state, req.Settings, targetDir)
if err != nil {
return nil, fmt.Errorf("save theme: %w", err)
}
if !theme.IsOmarchyInstalled() {
Expand All @@ -415,13 +422,46 @@ func (a *App) SaveAndApplyTheme(req SaveAndApplyThemeRequest) (*theme.ApplyResul
if _, err := platform.RunSync("omarchy-theme-set", name); err != nil {
return nil, fmt.Errorf("activate theme: %w", err)
}
// omarchy-theme-set cycles through a theme's bundled backgrounds (its own
// plus ours), so it can land on a stock image instead of the selected
// wallpaper. Apply our copy explicitly — the blurred variant when blur is
// enabled, since the request carries it as WallpaperPath.
if wallpaperDest != "" {
if err := theme.ApplyWallpaper(wallpaperDest); err != nil {
log.Printf("Warning: wallpaper application failed: %v", err)
}
}
return &theme.ApplyResult{
Success: true,
IsOmarchy: true,
ThemePath: targetDir,
}, nil
}

// ThemeFolderExists reports whether a saved theme folder with the given name
// already exists. The frontend uses this to offer updating the folder in
// place instead of refusing to save.
func (a *App) ThemeFolderExists(name string) bool {
name = strings.ToLower(strings.TrimSpace(name))
// Same charset SaveAndApplyTheme accepts.
if name == "" || name[0] == '-' {
return false
}
for _, r := range name {
if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
return false
}
}
for _, dir := range []string{platform.OmarchyThemesDir(), platform.SavedThemesDir()} {
// Theme folders are directories; match SaveAndApplyTheme's stat
// check rather than platform.FileExists (files only).
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
return true
}
}
return false
}

// ClearTheme removes the Aether theme and reverts to the default.
func (a *App) ClearTheme() error {
return theme.ClearTheme()
Expand Down Expand Up @@ -832,6 +872,17 @@ func (a *App) CancelBatchProcessing() {
// File / Image Utilities
// ---------------------------------------------------------------------------

// BlurWallpaper generates a heavily Gaussian-blurred JPEG variant of an image
// for use as the applied wallpaper. The original file is never modified, so
// color extraction keeps sampling the unblurred source. Returns the variant
// path (cached by source identity, so repeat calls are cheap).
func (a *App) BlurWallpaper(path string) (string, error) {
if !theme.IsImageFile(path) {
return "", fmt.Errorf("unsupported image file: %s", path)
}
return wallpaper.CreateBlurredVariant(path, platform.BlurDir())
}

// ReadImageAsDataURL reads a local image file and returns it as a base64 data URL.
// This is needed because webkit2gtk cannot load file:// paths directly.
func (a *App) ReadImageAsDataURL(path string) (string, error) {
Expand Down Expand Up @@ -937,6 +988,7 @@ type ExportThemeRequest struct {
IncludedApps []string `json:"includedApps"`
Palette []string `json:"palette"`
WallpaperPath string `json:"wallpaperPath"`
OriginalWallpaperPath string `json:"originalWallpaperPath"`
LightMode bool `json:"lightMode"`
AdditionalImages []string `json:"additionalImages"`
ExtendedColors map[string]string `json:"extendedColors"`
Expand Down Expand Up @@ -982,6 +1034,7 @@ func (a *App) ExportTheme(req ExportThemeRequest) (string, error) {
state := &theme.ThemeState{
Palette: palette,
WallpaperPath: req.WallpaperPath,
OriginalWallpaperPath: req.OriginalWallpaperPath,
LightMode: req.LightMode,
ColorRoles: roles,
ExtendedColors: req.ExtendedColors,
Expand Down Expand Up @@ -1273,6 +1326,7 @@ func (a *App) HandleIPC(req ipc.Request) ipc.Response {
result, err := a.ApplyTheme(ApplyThemeRequest{
Palette: a.state.Palette[:],
WallpaperPath: a.state.WallpaperPath,
OriginalWallpaperPath: a.state.OriginalWallpaperPath,
LightMode: a.state.LightMode,
ExtendedColors: a.state.ExtendedColors,
AppOverrides: a.state.AppOverrides,
Expand Down
115 changes: 115 additions & 0 deletions app_themes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"image"
"image/color"
"image/png"
"os"
"path/filepath"
"testing"
)

func TestThemeFolderExists(t *testing.T) {
configHome := t.TempDir()
home := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", configHome)
t.Setenv("HOME", home)

app := NewApp()

if app.ThemeFolderExists("midnight") {
t.Error("ThemeFolderExists() = true for missing folder")
}

// Omarchy themes dir (~/.config/omarchy/themes).
omarchyDir := filepath.Join(configHome, "omarchy", "themes", "midnight")
if err := os.MkdirAll(omarchyDir, 0o755); err != nil {
t.Fatal(err)
}
if !app.ThemeFolderExists("midnight") {
t.Error("ThemeFolderExists() = false for existing omarchy theme folder")
}

// Saved themes dir (~/.config/aether/themes), with case/whitespace
// normalization matching SaveAndApplyTheme's validation.
savedDir := filepath.Join(configHome, "aether", "themes", "dusk")
if err := os.MkdirAll(savedDir, 0o755); err != nil {
t.Fatal(err)
}
if !app.ThemeFolderExists(" Dusk ") {
t.Error("ThemeFolderExists() = false for existing saved theme folder")
}

// Invalid names never resolve to a folder.
for _, bad := range []string{"", "-x", "foo/bar", "foo bar", "..", "."} {
if app.ThemeFolderExists(bad) {
t.Errorf("ThemeFolderExists(%q) = true; want false", bad)
}
}
}

func writeTestImage(t *testing.T, dir, name string, w, h int) string {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
img.SetRGBA(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), B: 128, A: 0xff})
}
}
path := filepath.Join(dir, name)
f, err := os.Create(path)
if err != nil {
t.Fatalf("create test image: %v", err)
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
t.Fatalf("encode test image: %v", err)
}
return path
}

func TestBlurWallpaperCreatesVariant(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
t.Setenv("HOME", t.TempDir())

app := NewApp()
src := writeTestImage(t, t.TempDir(), "photo.png", 128, 80)

got, err := app.BlurWallpaper(src)
if err != nil {
t.Fatalf("BlurWallpaper: %v", err)
}
if got == "" {
t.Fatal("BlurWallpaper returned empty path")
}
if _, err := os.Stat(got); err != nil {
t.Fatalf("variant not created: %v", err)
}
ext := filepath.Ext(got)
if ext != ".jpg" {
t.Errorf("variant ext = %q; want .jpg", ext)
}
}

func TestBlurWallpaperRejectsNonImage(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())

app := NewApp()
txt := filepath.Join(t.TempDir(), "notes.txt")
if err := os.WriteFile(txt, []byte("not an image"), 0o644); err != nil {
t.Fatal(err)
}

if _, err := app.BlurWallpaper(txt); err == nil {
t.Error("BlurWallpaper() error = nil; want error for non-image file")
}
}

func TestBlurWallpaperRejectsMissingFile(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())

app := NewApp()
if _, err := app.BlurWallpaper("/nonexistent/photo.png"); err == nil {
t.Error("BlurWallpaper() error = nil; want error for missing file")
}
}
4 changes: 2 additions & 2 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -488,9 +488,9 @@
<ApplySaveDialog
open={getApplySaveDialogOpen()}
onclose={() => setApplySaveDialogOpen(false)}
onsave={name => {
onsave={(name, updateExisting) => {
setApplySaveDialogOpen(false);
saveAndApplyTheme(name);
saveAndApplyTheme(name, updateExisting);
}}
/>
</div>
7 changes: 5 additions & 2 deletions frontend/src/lib/actions/themeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getIsExtracting,
setIsExtracting,
getWallpaperPath,
getApplyWallpaperPath,
setWallpaperPath,
getPalette,
setPalette,
Expand Down Expand Up @@ -54,7 +55,8 @@ async function runApply(): Promise<{success: boolean}> {
const {ApplyTheme} = await import('../../../wailsjs/go/main/App');
const result = await ApplyTheme({
palette: getPalette(),
wallpaperPath: getWallpaperPath(),
wallpaperPath: getApplyWallpaperPath(),
originalWallpaperPath: getWallpaperPath(),
lightMode: getLightMode(),
additionalImages: getAdditionalImages(),
extendedColors: getExtendedColors(),
Expand Down Expand Up @@ -150,7 +152,8 @@ export async function saveAndApplyTheme(
name,
updateExisting,
palette: getPalette(),
wallpaperPath: getWallpaperPath(),
wallpaperPath: getApplyWallpaperPath(),
originalWallpaperPath: getWallpaperPath(),
lightMode: getLightMode(),
additionalImages: getAdditionalImages(),
extendedColors: getExtendedColors(),
Expand Down
Loading