Skip to content

Commit 96f73b5

Browse files
authored
Replace pywal with a new ImageMagick color extraction, and custom algorithm. (#20)
* Add initial new algo for color extraction * Improve the algo * Tweak the color selection algo * fix blueprint * Comment the pywal button * Tweak the algo, keep the colors close to what is present in the image * Remove pywal * Remove pywal * Remove commented code * Ensure reset button resets it all
1 parent 483bcf0 commit 96f73b5

7 files changed

Lines changed: 939 additions & 153 deletions

File tree

CLAUDE.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
Aether is a GTK/Libadwaita theming application for Omarchy. It provides a visual interface for creating and applying desktop themes through pywal color extraction, wallhaven.cc wallpaper browsing, and template-based configuration generation.
7+
Aether is a GTK/Libadwaita theming application for Omarchy. It provides a visual interface for creating and applying desktop themes through intelligent ImageMagick-based color extraction, wallhaven.cc wallpaper browsing, and template-based configuration generation.
88

99
**Core workflow:**
10-
1. User selects wallpaper (local file/drag-drop OR wallhaven.cc browser)
10+
1. User selects wallpaper (local file/drag-drop OR wallhaven.cc/local browser)
1111
2. **(Optional)** Edit wallpaper with filters (blur, brightness, sepia, etc.) before extraction
12-
3. Pywal extracts 16 ANSI colors from wallpaper (or filtered version)
12+
3. ImageMagick intelligently extracts 16 ANSI colors from wallpaper (or filtered version)
13+
- Automatically classifies image type (monochrome, low-diversity, chromatic)
14+
- Adapts palette generation strategy for optimal results
15+
- Ensures readability through brightness normalization
16+
- Caches results for instant re-extraction
1317
4. Colors auto-assign to UI roles (background, foreground, color0-15)
1418
5. User customizes colors/settings in GUI
1519
6. "Apply Theme" processes templates → writes to `~/.config/omarchy/themes/aether/` → runs `omarchy-theme-set aether`
@@ -42,12 +46,15 @@ npm run dev
4246
- Tab 1 (Palette Editor): Unified interface for wallpaper + custom palette creation
4347
- File picker/drag-drop for wallpaper selection
4448
- **Edit wallpaper button** (icon button next to Extract) - opens WallpaperEditor for filter application
45-
- Manual extract button (no auto-extraction)
49+
- **Extract button** - Intelligent ImageMagick-based color extraction
4650
- 16-color swatch grid with click-to-edit and lock feature
4751
- Loads default Catppuccin-inspired colors on startup
48-
- Tab 2 (Find Wallpaper): WallpaperBrowser component for wallhaven.cc integration
49-
- Calls `extractColorsFromWallpaper()` which runs `wal -n -s -t -e -i <image>`
50-
- Light mode flag (`_lightMode`) controlled by SettingsSidebar, saved to blueprints, affects pywal extraction
52+
- Tab 2 (Find Wallpaper): Three sub-tabs (Wallhaven, Local, Favorites)
53+
- WallpaperBrowser - wallhaven.cc API integration
54+
- LocalWallpaperBrowser - ~/Wallpapers directory browser
55+
- FavoritesView - Favorited wallpapers from any source
56+
- Calls `extractColorsFromWallpaperIM()` which uses advanced ImageMagick algorithm
57+
- Light mode flag (`_lightMode`) controlled by SettingsSidebar, saved to blueprints, affects extraction
5158
- Emits: `palette-generated` signal with 16 colors, `open-wallpaper-editor` signal with wallpaper path
5259

5360
**WallpaperEditor** (`src/components/WallpaperEditor.js`)
@@ -102,7 +109,7 @@ npm run dev
102109
- JPEG format (quality 95) for both preview and final output
103110
- Unique timestamped filename: `processed-wallpaper-{timestamp}.jpg`
104111
- Saves to `~/.cache/aether/`
105-
- Bypasses pywal cache (forces fresh color extraction)
112+
- Bypasses color extraction cache (forces fresh extraction)
106113

107114
**Sub-components:**
108115
- `src/components/wallpaper-editor/FilterControls.js` - All filter UI controls, presets, tone picker
@@ -164,7 +171,7 @@ The "Find Wallpaper" tab contains a sub-navigation with 3 tabs:
164171
**SettingsSidebar** (`src/components/SettingsSidebar.js`)
165172
- Collapsible right sidebar with Adw.NavigationSplitView
166173
- Contains multiple sections:
167-
- Light Mode toggle (for pywal light/dark scheme extraction)
174+
- Light Mode toggle (for light/dark color scheme extraction)
168175
- Color adjustments (vibrance, contrast, brightness, hue shift, temperature, gamma)
169176
- Color harmony generator (complementary, analogous, triadic, split-complementary, tetradic, monochromatic)
170177
- Gradient generator (smooth transitions between two colors)
@@ -189,8 +196,6 @@ The "Find Wallpaper" tab contains a sub-navigation with 3 tabs:
189196

190197
### Services
191198

192-
**wallpaper-service.js**: Executes pywal via `Gio.Subprocess`, reads colors from `~/.cache/wal/colors`, brightens colors 9-15 for better terminal contrast
193-
194199
**wallhaven-service.js**: HTTP client for wallhaven.cc API v1
195200
- Uses `Soup.Session` (libsoup3) for async HTTP requests
196201
- Methods: `search(params)`, `getWallpaper(id)`, `downloadWallpaper(url, destPath)`
@@ -229,8 +234,7 @@ ConfigWriter iterates all template files, performs string substitution, and writ
229234
- GJS (GNOME JavaScript bindings)
230235
- GTK 4 + Libadwaita 1
231236
- libsoup3 (HTTP client for wallhaven API)
232-
- pywal (`python-pywal` package)
233-
- ImageMagick (`magick` command) - required for wallpaper filter processing
237+
- ImageMagick (`magick` command) - required for color extraction and wallpaper filter processing
234238
- omarchy theme manager (provides `omarchy-theme-set` command)
235239

236240
**Import pattern:**
@@ -282,8 +286,7 @@ PreviewArea (click gesture) → temporarily disables filters → shows original
282286
## Important Notes
283287

284288
- Application ID is `com.aether.DesktopSynthesizer` (in main.js) but desktop file is `li.oever.aether.desktop`
285-
- Wallpaper extraction requires pywal installed and in PATH
286-
- **Wallpaper filter processing requires ImageMagick (`magick` command) installed**
289+
- **Color extraction and wallpaper filter processing require ImageMagick (`magick` command) installed**
287290
- Theme application requires `omarchy-theme-set` command available
288291
- Default colors defined in `src/constants/colors.js` as fallback
289292
- All file operations use GLib/Gio APIs (file-utils.js wrappers)

README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ A visual theming application for Omarchy. Create beautiful desktop themes throug
1010

1111
## Key Features
1212

13-
- **Wallpaper Integration** - Extract colors with pywal or browse wallhaven.cc directly
13+
- **Intelligent Color Extraction** - Advanced ImageMagick-based algorithm with automatic image classification (monochrome, low-diversity, chromatic)
14+
- **Smart Palette Generation** - Adaptive strategies ensure readability and preserve image aesthetics
1415
- **Image Filter Editor** - Apply blur, exposure, vignette, grain, and 12 presets before color extraction
16+
- **Wallpaper Browsing** - Integrated wallhaven.cc browser, local wallpaper manager, and favorites system
1517
- **Color Presets** - 10 popular themes: Dracula, Nord, Gruvbox, Tokyo Night, Catppuccin, and more
1618
- **Advanced Color Tools** - Harmony generator, gradients, and adjustment sliders (vibrance, contrast, temperature)
1719
- **Color Lock System** - Protect specific colors while experimenting with adjustments
@@ -27,15 +29,14 @@ A visual theming application for Omarchy. Create beautiful desktop themes throug
2729
- GTK 4
2830
- Libadwaita 1
2931
- libsoup3 - HTTP client library for wallhaven API
30-
- **pywal** - Color extraction from wallpapers
31-
- **ImageMagick** - Image filter processing
32+
- **ImageMagick** - Intelligent color extraction and image filter processing
3233
- **Omarchy** - Distro
3334

3435
## Installation
3536

3637
1. Install system dependencies:
3738
```bash
38-
sudo pacman -S gjs gtk4 libadwaita libsoup3 python-pywal imagemagick
39+
sudo pacman -S gjs gtk4 libadwaita libsoup3 imagemagick
3940
```
4041

4142
2. Clone the repository:
@@ -88,9 +89,9 @@ Example:
8889
### Basic Workflow
8990

9091
1. **Create a palette:**
91-
- Upload a wallpaper and extract colors with pywal
92+
- Upload a wallpaper and extract colors with intelligent ImageMagick algorithm
9293
- (Optional) Edit wallpaper with filters before extraction
93-
- Browse wallhaven.cc and download wallpapers
94+
- Browse wallhaven.cc, local wallpapers, or favorites
9495
- Choose from 10 color presets
9596
- Generate color harmonies or gradients
9697

@@ -106,6 +107,16 @@ Example:
106107

107108
Changes apply instantly via live reload.
108109

110+
### Color Extraction Algorithm
111+
112+
Aether uses an advanced ImageMagick-based extraction system that:
113+
114+
- **Automatically classifies images** as monochrome, low-diversity, or chromatic
115+
- **Adapts palette generation** strategy based on image characteristics
116+
- **Ensures readability** through intelligent brightness normalization
117+
- **Preserves image aesthetics** by prioritizing hue accuracy
118+
- **Caches results** for instant re-extraction (< 0.1s)
119+
109120
## Development
110121

111122
```bash
@@ -149,10 +160,10 @@ pacman -S gjs gtk4 libadwaita libsoup3
149160
gjs -m src/main.js # Check for errors
150161
```
151162

152-
**pywal not found:**
163+
**ImageMagick not found:**
153164
```bash
154-
pacman -S python-pywal
155-
which wal # Verify installation
165+
pacman -S imagemagick
166+
magick --version # Verify installation
156167
```
157168

158169
**Wallhaven not loading:**

src/components/PaletteGenerator.js

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Adw from 'gi://Adw?version=1';
66
import Gdk from 'gi://Gdk?version=4.0';
77
import GdkPixbuf from 'gi://GdkPixbuf';
88

9-
import {extractColorsFromWallpaper} from '../services/wallpaper-service.js';
9+
import {extractColorsFromWallpaperIM} from '../utils/imagemagick-color-extraction.js';
1010
import {adjustColor} from '../utils/color-utils.js';
1111
import {ColorSwatchGrid} from './palette/color-swatch-grid.js';
1212
import {ColorPickerDialog} from './palette/color-picker-dialog.js';
@@ -274,34 +274,35 @@ export const PaletteGenerator = GObject.registerClass(
274274
selectButton.connect('clicked', () => this._selectWallpaper());
275275
buttonBox.append(selectButton);
276276

277-
// Extract button (initially hidden)
278-
const extractButtonBox = new Gtk.Box({
277+
// ImageMagick extract button
278+
const imExtractButtonBox = new Gtk.Box({
279279
orientation: Gtk.Orientation.HORIZONTAL,
280280
spacing: 6,
281281
});
282282

283-
const extractIcon = new Gtk.Image({
283+
const imExtractIcon = new Gtk.Image({
284284
icon_name: 'color-select-symbolic',
285285
});
286286

287-
const extractLabel = new Gtk.Label({
287+
const imExtractLabel = new Gtk.Label({
288288
label: 'Extract',
289289
});
290290

291-
extractButtonBox.append(extractIcon);
292-
extractButtonBox.append(extractLabel);
291+
imExtractButtonBox.append(imExtractIcon);
292+
imExtractButtonBox.append(imExtractLabel);
293293

294-
this._extractButton = new Gtk.Button({
295-
child: extractButtonBox,
294+
this._imExtractButton = new Gtk.Button({
295+
child: imExtractButtonBox,
296296
css_classes: ['suggested-action'],
297297
visible: false,
298+
tooltip_text: 'Extract colors from wallpaper',
298299
});
299-
this._extractButton.connect('clicked', () => {
300+
this._imExtractButton.connect('clicked', () => {
300301
if (this._currentWallpaper) {
301-
this._extractColors(this._currentWallpaper);
302+
this._extractColorsIM(this._currentWallpaper);
302303
}
303304
});
304-
buttonBox.append(this._extractButton);
305+
buttonBox.append(this._imExtractButton);
305306

306307
// Edit wallpaper button (initially hidden)
307308
const editButtonBox = new Gtk.Box({
@@ -509,7 +510,7 @@ export const PaletteGenerator = GObject.registerClass(
509510
this._wallpaperPreview.set_visible(true);
510511
}
511512

512-
this._extractButton.set_visible(true);
513+
this._imExtractButton.set_visible(true);
513514
this._pickFromWallpaperBtn.set_visible(true);
514515
this._editWallpaperBtn.set_visible(true);
515516
}
@@ -540,15 +541,15 @@ export const PaletteGenerator = GObject.registerClass(
540541
this._wallpaperPreview.set_visible(true);
541542
}
542543

543-
this._extractButton.set_visible(true);
544+
this._imExtractButton.set_visible(true);
544545
this._pickFromWallpaperBtn.set_visible(true);
545546
this._editWallpaperBtn.set_visible(true);
546547
}
547548

548-
_extractColors(imagePath) {
549+
_extractColorsIM(imagePath) {
549550
this._showLoading(true);
550551

551-
extractColorsFromWallpaper(
552+
extractColorsFromWallpaperIM(
552553
imagePath,
553554
this._lightMode,
554555
colors => {
@@ -558,7 +559,7 @@ export const PaletteGenerator = GObject.registerClass(
558559
this._showLoading(false);
559560
},
560561
error => {
561-
console.error('Error extracting colors:', error.message);
562+
console.error('Error extracting colors with ImageMagick:', error.message);
562563
this._showLoading(false);
563564
}
564565
);
@@ -745,17 +746,21 @@ export const PaletteGenerator = GObject.registerClass(
745746
}
746747

747748
reset() {
748-
this._palette = [];
749-
this._originalPalette = [];
750749
this._currentWallpaper = null;
751750
this._wallpaperPreview.set_file(null);
751+
this._wallpaperPreview.set_visible(false);
752752
if (this._customWallpaperPreview) {
753753
this._customWallpaperPreview.set_file(null);
754754
this._customWallpaperPreview.set_visible(false);
755755
}
756-
this._swatchGrid.setPalette([]);
756+
this._imExtractButton.set_visible(false);
757+
this._pickFromWallpaperBtn.set_visible(false);
758+
this._editWallpaperBtn.set_visible(false);
757759
this._swatchGrid.setLockedColors(new Array(16).fill(false)); // Reset all locks
758760
this._showLoading(false);
761+
762+
// Reload default colors
763+
this._loadDefaultCustomColors();
759764
}
760765

761766
getPalette() {

src/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,12 @@ const AetherWindow = GObject.registerClass(
441441
const settings = this.settingsSidebar.getSettings();
442442
const lightMode = this.settingsSidebar.getLightMode();
443443

444+
// Initialize blueprint service if not already initialized
445+
if (!this.blueprintService) {
446+
// Show blueprint manager first to initialize the service
447+
this._showBlueprintManager();
448+
}
449+
444450
this.blueprintService.saveBlueprint(palette, settings, lightMode);
445451
}
446452

src/services/wallpaper-service.js

Lines changed: 0 additions & 107 deletions
This file was deleted.

0 commit comments

Comments
 (0)