Skip to content
Closed
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ A visual theming application for [Omarchy](https://omarchy.org). Extract colors
- Per-app overrides, reload hooks, and post-apply scripts
- Add your own apps with custom templates

### Multiple Displays
- Read the monitor layout from Omarchy/Hyprland (position, scale, rotation, native resolution)
- Assign a separate wallpaper to each display, so a portrait monitor gets a native image instead of a crop
- Ships a per-display background service, so it works with no external dependency
- Overlay the monitor layout on the wallpaper in the editor and elect which display drives the tools
- Draw the palette from one display, or blend several equally
- Save per-display wallpapers in a blueprint and restore them with the theme
- Find wallpapers for a display from Wallhaven, filtered by its aspect ratio and resolution

### Extras
- WCAG contrast ratio checker with AAA/AA accessibility grading
- Gradient generator and single-color palette builder
Expand Down Expand Up @@ -146,6 +155,7 @@ From `frontend/`, run `npm ci`, `npm run check`, `npm test`, and `npm run build`
| [Wallhaven](docs/wallhaven.md) | Browse online wallpapers |
| [GitHub Source](docs/github-source.md) | Browse repository wallpapers |
| [Favorites](docs/favorites.md) | Save wallpapers and export a collection |
| [Displays](docs/displays.md) | Per-display wallpapers and portrait monitors |
| [Blueprints](docs/blueprints.md) | Save and restore themes |
| [Custom Templates](docs/custom-templates.md) | Add support for your apps |
| [Custom Apps](docs/custom-apps.md) | Per-app template system |
Expand Down
100 changes: 98 additions & 2 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ func (a *App) startup(ctx context.Context) {
}
a.themeWatcher.Start(ctx)

// Refresh the display list when a monitor is plugged or unplugged.
platform.WatchMonitorEvents(ctx, func() {
if a.ctx != nil {
wailsrt.EventsEmit(a.ctx, "displays-changed")
}
})

// Start IPC server for remote control
srv, err := ipc.NewServer(ipc.DefaultSocketPath(), a)
if err != nil {
Expand Down Expand Up @@ -373,6 +380,8 @@ type ApplyThemeRequest struct {
Settings theme.Settings `json:"settings"`
AppOverrides map[string]map[string]string `json:"appOverrides"`
IconTheme icontheme.Selection `json:"iconTheme"`
// Displays restores per-display wallpapers (e.g. from a loaded blueprint).
Displays map[string]string `json:"displays,omitempty"`
}

// ApplyTheme processes all templates and applies the theme to the system.
Expand All @@ -397,7 +406,12 @@ func (a *App) ApplyTheme(req ApplyThemeRequest) (*theme.ApplyResult, error) {
IconTheme: req.IconTheme,
}

return a.writer.ApplyTheme(state, req.Settings)
result, err := a.writer.ApplyTheme(state, req.Settings)
if err == nil {
// Restore per-display wallpapers from a loaded blueprint, if any.
a.applyDisplayAssignments(req.Displays)
}
return result, err
}

// SaveAndApplyThemeRequest is the payload for saving the current state as a
Expand Down Expand Up @@ -511,6 +525,31 @@ func (a *App) ApplyWallpaperOnly(path string) error {
return omarchy.SetBackground(path)
}

// ListDisplays returns the connected displays and whether the active Omarchy
// shell background service can render a different wallpaper per display.
func (a *App) ListDisplays() (omarchy.DisplaysResult, error) {
return omarchy.Displays()
}

// SetDisplayWallpaper assigns a wallpaper to one display through the active
// per-screen background service. It fails when no such service is running.
func (a *App) SetDisplayWallpaper(screenKeys []string, path string) error {
info, err := os.Stat(path)
if err != nil || !info.Mode().IsRegular() {
return fmt.Errorf("wallpaper must be a readable regular file")
}
if err := wallpaper.ValidateImageFile(path); err != nil {
return err
}
return omarchy.SetDisplayWallpaper(screenKeys, path)
}

// ClearDisplayWallpaper removes a display's wallpaper so it falls back to the
// global Omarchy background.
func (a *App) ClearDisplayWallpaper(screenKeys []string) error {
return omarchy.ClearDisplayWallpaper(screenKeys)
}

// ClearTheme removes the Aether theme and reverts to the default.
func (a *App) ClearTheme() error {
return theme.ClearTheme()
Expand Down Expand Up @@ -546,6 +585,7 @@ func (a *App) ListBlueprints() ([]map[string]interface{}, error) {
"extendedColors": bp.Palette.ExtendedColors,
"nativeColors": bp.Palette.NativeColors,
"additionalImages": bp.Palette.AdditionalImages,
"displays": bp.Palette.Displays,
},
"adjustments": bp.Adjustments,
"appOverrides": bp.AppOverrides,
Expand Down Expand Up @@ -590,9 +630,60 @@ func (a *App) SaveBlueprint(req SaveBlueprintRequest) error {
if err := bp.SetIconThemeSelection(req.IconTheme); err != nil {
return fmt.Errorf("iconTheme: %w", err)
}
bp.Palette.Displays = a.currentDisplayAssignments()
return a.blueprints.Save(req.Name, bp)
}

// currentDisplayAssignments snapshots the live per-display wallpapers, keyed by
// each display's preferred key, so a blueprint can restore them. It returns nil
// when no per-screen background service is available.
func (a *App) currentDisplayAssignments() map[string]string {
result, err := omarchy.Displays()
if err != nil || !result.PerScreen {
return nil
}
assignments := make(map[string]string)
for _, display := range result.Displays {
if display.Assignment != nil && display.Assignment.Type == "image" && display.Assignment.Path != "" {
key := display.Key
if key == "" && len(display.Keys) > 0 {
key = display.Keys[0]
}
if key != "" {
assignments[key] = display.Assignment.Path
}
}
}
if len(assignments) == 0 {
return nil
}
return assignments
}

// applyDisplayAssignments re-applies saved per-display wallpapers to the
// matching connected displays. Displays without a saved entry are left alone.
func (a *App) applyDisplayAssignments(assignments map[string]string) {
if len(assignments) == 0 {
return
}
result, err := omarchy.Displays()
if err != nil || !result.PerScreen {
return
}
for _, display := range result.Displays {
for _, key := range display.Keys {
path, ok := assignments[key]
if !ok || path == "" {
continue
}
if err := omarchy.SetDisplayWallpaper(display.Keys, path); err != nil {
log.Printf("restore display wallpaper for %s: %v", display.Name, err)
}
break
}
}
}

// resolveWallpaper returns a local wallpaper path from a blueprint. If the local
// path exists it is returned directly. Otherwise, if a URL is available, the
// wallpaper is downloaded first.
Expand Down Expand Up @@ -706,7 +797,12 @@ func (a *App) ApplyBlueprint(name string) (*theme.ApplyResult, error) {
}
a.state.IconTheme = iconTheme

return a.writer.ApplyTheme(a.state, theme.DefaultApplySettings())
result, err := a.writer.ApplyTheme(a.state, theme.DefaultApplySettings())
if err == nil {
// Restore any saved per-display wallpapers after the theme is applied.
a.applyDisplayAssignments(bp.Palette.Displays)
}
return result, err
}

// BlueprintExists checks whether a blueprint with the given name already exists.
Expand Down
17 changes: 15 additions & 2 deletions contrib/quickshell/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Aether Omarchy shell plugins

Native `omarchy-shell` overlays for selecting wallpapers and Aether blueprints. Both use Omarchy shell colors and call the headless `aether` CLI, so the Aether GUI does not need to be open.
Native `omarchy-shell` plugins for Aether:

- **`aether.wallpapers`** and **`aether.blueprints`** — overlays for selecting
wallpapers and Aether blueprints. They use Omarchy shell colors and call the
headless `aether` CLI, so the Aether GUI does not need to be open.
- **`aether.background`** — a background **service** that replaces
`omarchy.background` and adds per-display wallpapers. It keeps the stock
global background, transition, theme payload, and desktop double-click, so
with no assignments the behavior is identical to stock.

## Install

Expand All @@ -10,6 +18,8 @@ From the repository root:
make install-omarchy-plugins
```

This installs and enables all three plugins.

## Open

```bash
Expand All @@ -22,8 +32,11 @@ omarchy-shell shell toggle aether.blueprints '{}'
```bash
omarchy plugin validate contrib/quickshell/wallpapers
omarchy plugin validate contrib/quickshell/blueprints
omarchy plugin validate contrib/quickshell/background
qmllint -I /usr/share/omarchy/shell contrib/quickshell/wallpapers/*.qml
qmllint -I /usr/share/omarchy/shell contrib/quickshell/blueprints/*.qml
qmllint -I /usr/share/omarchy/shell contrib/quickshell/background/*.qml
```

See [the shell plugin guide](../../docs/quickshell.md) for keybinds, controls, and troubleshooting.
See [the shell plugin guide](../../docs/quickshell.md) and
[Displays](../../docs/displays.md) for details.
Loading