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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ else
-update-desktop-database $(HOME)/.local/share/applications 2>/dev/null
-gtk-update-icon-cache -f $(HOME)/.local/share/icons/hicolor 2>/dev/null
-xdg-mime default li.oever.aether.url-handler.desktop x-scheme-handler/aether 2>/dev/null
mkdir -p $(HOME)/.local/share/xdg-desktop-portal/portals $(HOME)/.local/share/dbus-1/services
cp contrib/portal/aether.portal $(HOME)/.local/share/xdg-desktop-portal/portals/
cp contrib/portal/org.freedesktop.impl.portal.desktop.aether.service $(HOME)/.local/share/dbus-1/services/
@if command -v omarchy >/dev/null 2>&1; then $(MAKE) install-omarchy-plugins; fi
endif

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ From `frontend/`, run `npm ci`, `npm run check`, `npm test`, and `npm run build`
| [GitHub Source](docs/github-source.md) | Browse repository wallpapers |
| [Favorites](docs/favorites.md) | Save wallpapers and export a collection |
| [Blueprints](docs/blueprints.md) | Save and restore themes |
| [Set as Background](docs/wallpaper-portal.md) | Theme from a file manager's context menu |
| [Custom Templates](docs/custom-templates.md) | Add support for your apps |
| [Custom Apps](docs/custom-apps.md) | Per-app template system |
| [File System](docs/filesystem.md) | Where Aether stores files |
Expand Down
3 changes: 3 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func Run(args []string, templatesFS embed.FS) int {
return runImportColorsToml(args[1:], templatesFS)
case "--handle-url":
return runHandleURL(args[1:], templatesFS)
case "--wallpaper-portal":
return runWallpaperPortal(args[1:])

// --- Color utilities ---
case "--color-convert":
Expand Down Expand Up @@ -160,6 +162,7 @@ Import commands:
aether://apply?wallpaper=URL
aether://apply?mode=light|dark
aether://apply?as_omarchy_theme=NAME
aether --wallpaper-portal Serve "Set as Background" for file managers (started by D-Bus)

Color utilities:
aether --color-info <hex> Show all color representations
Expand Down
98 changes: 98 additions & 0 deletions cli/wallpaper_portal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cli

import (
"fmt"
"net/url"
"os"
"os/exec"

"github.com/godbus/dbus/v5"

"aether/internal/theme"
)

const (
wallpaperPortalBusName = "org.freedesktop.impl.portal.desktop.aether"
wallpaperPortalPath = "/org/freedesktop/portal/desktop"
wallpaperPortalInterface = "org.freedesktop.impl.portal.Wallpaper"

// Portal response codes.
portalResponseSuccess = 0
portalResponseOther = 2
)

// runWallpaperPortal serves the xdg-desktop-portal Wallpaper backend on the
// session bus. File managers and image viewers call it for "Set as
// Background"; Aether then applies the picture with a generated theme, as
// --generate does. D-Bus starts it on demand via the service file in
// contrib/portal.
func runWallpaperPortal(args []string) int {
conn, err := dbus.ConnectSessionBus()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: session bus: %v\n", err)
return 1
}
defer conn.Close()

if err := conn.Export(wallpaperPortal{}, wallpaperPortalPath, wallpaperPortalInterface); err != nil {
fmt.Fprintf(os.Stderr, "Error: export portal: %v\n", err)
return 1
}
reply, err := conn.RequestName(wallpaperPortalBusName, dbus.NameFlagDoNotQueue)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: request bus name: %v\n", err)
return 1
}
if reply != dbus.RequestNameReplyPrimaryOwner {
fmt.Fprintln(os.Stderr, "Wallpaper portal is already running")
return 0
}

select {}
}

type wallpaperPortal struct{}

// SetWallpaperURI implements org.freedesktop.impl.portal.Wallpaper. Aether
// themes the desktop background only, so a lock-screen-only request fails.
func (wallpaperPortal) SetWallpaperURI(handle dbus.ObjectPath, appID, parentWindow, uri string, options map[string]dbus.Variant) (uint32, *dbus.Error) {
if setOn, ok := options["set-on"].Value().(string); ok && setOn == "lockscreen" {
return portalResponseOther, nil
}
path, err := wallpaperPathFromURI(uri)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return portalResponseOther, nil
}

self, err := os.Executable()
if err != nil {
self = "aether"
}
cmd := exec.Command(self, "--generate", path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Error: start aether --generate: %v\n", err)
return portalResponseOther, nil
}
// Reply right away: the caller only waits for the request to be
// accepted, and generating a theme can take a few seconds.
go cmd.Wait()
return portalResponseSuccess, nil
}

// wallpaperPathFromURI turns the portal's file:// URI into a local image path.
func wallpaperPathFromURI(uri string) (string, error) {
u, err := url.Parse(uri)
if err != nil {
return "", fmt.Errorf("invalid wallpaper URI %q: %w", uri, err)
}
if u.Scheme != "file" || u.Path == "" {
return "", fmt.Errorf("unsupported wallpaper URI %q (expected file://)", uri)
}
if !theme.IsImageFile(u.Path) {
return "", fmt.Errorf("not an image file: %s", u.Path)
}
return u.Path, nil
}
42 changes: 42 additions & 0 deletions cli/wallpaper_portal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"testing"

"github.com/godbus/dbus/v5"
)

func TestWallpaperPathFromURI(t *testing.T) {
tests := []struct {
uri string
want string
ok bool
}{
{"file:///home/me/Pictures/sky.jpg", "/home/me/Pictures/sky.jpg", true},
{"file:///home/me/My%20Pictures/sky.png", "/home/me/My Pictures/sky.png", true},
{"https://example.com/sky.jpg", "", false},
{"file:///home/me/notes.txt", "", false},
{"file://", "", false},
}
for _, tt := range tests {
got, err := wallpaperPathFromURI(tt.uri)
if (err == nil) != tt.ok || got != tt.want {
t.Errorf("wallpaperPathFromURI(%q) = %q, %v; want %q, ok=%v", tt.uri, got, err, tt.want, tt.ok)
}
}
}

func TestSetWallpaperURIRejectsLockScreenOnly(t *testing.T) {
options := map[string]dbus.Variant{"set-on": dbus.MakeVariant("lockscreen")}
got, err := wallpaperPortal{}.SetWallpaperURI("/request", "", "", "file:///home/me/sky.jpg", options)
if err != nil || got != portalResponseOther {
t.Fatalf("SetWallpaperURI() = %d, %v; want %d", got, err, portalResponseOther)
}
}

func TestSetWallpaperURIRejectsNonImage(t *testing.T) {
got, err := wallpaperPortal{}.SetWallpaperURI("/request", "", "", "file:///home/me/notes.txt", nil)
if err != nil || got != portalResponseOther {
t.Fatalf("SetWallpaperURI() = %d, %v; want %d", got, err, portalResponseOther)
}
}
3 changes: 3 additions & 0 deletions contrib/portal/aether.portal
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.aether
Interfaces=org.freedesktop.impl.portal.Wallpaper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.impl.portal.desktop.aether
Exec=/usr/bin/aether --wallpaper-portal
39 changes: 39 additions & 0 deletions docs/wallpaper-portal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Set as Background

Aether can handle "Set as Background" in file managers and image viewers. It applies the picture with a generated theme, as `aether --generate` does.

These apps hand the picture to xdg-desktop-portal. On GNOME the portal sets the wallpaper itself. On Hyprland and other window managers no portal backend does, so the menu entry does nothing.

## Setup

`make install` installs two files:

```text
~/.local/share/xdg-desktop-portal/portals/aether.portal
~/.local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.aether.service
```

Packages install them to `/usr/share/xdg-desktop-portal/portals/` and `/usr/share/dbus-1/services/`. D-Bus starts `aether --wallpaper-portal` when an app asks for it.

Then select Aether as the Wallpaper backend for your desktop. On Hyprland, add the last line to `~/.config/xdg-desktop-portal/hyprland-portals.conf`. Without it, the portal picks the first configured backend that offers Wallpaper (usually `gtk`, which only works on GNOME).

```ini
[preferred]
default=hyprland;gtk
org.freedesktop.impl.portal.Wallpaper=aether
```

Copy the `default=` line from `/usr/share/xdg-desktop-portal/hyprland-portals.conf`, because the user file replaces it. For other desktops use `<desktop>-portals.conf`, where `<desktop>` is `$XDG_CURRENT_DESKTOP` in lowercase.

Restart the portal:

```bash
systemctl --user restart xdg-desktop-portal
```

The first time an app sets a background, the portal asks whether apps may change it. Choose Allow.

## Limits

- Aether themes the desktop background. A request for the lock screen only is rejected.
- Only local still images are accepted.