From 2cf5f718a41e0bf989b7908e668cc4d1ac6c72b5 Mon Sep 17 00:00:00 2001 From: Doug Burks Date: Mon, 16 Mar 2026 17:38:30 -0400 Subject: [PATCH 1/3] add OhMyDebn support via --ohmydebn flag --- src/AetherApplication.js | 32 ++- src/AetherWindow.js | 25 ++- src/components/BlueprintsView.js | 20 +- src/components/OmarchyThemeCard.js | 152 +++++++++++-- src/components/OmarchyThemesBrowser.js | 67 +++++- src/services/omarchy-theme-service.js | 109 ++++++++- src/services/theme-manager.js | 300 ++++++++++++++++++++++++- 7 files changed, 649 insertions(+), 56 deletions(-) diff --git a/src/AetherApplication.js b/src/AetherApplication.js index 0d284bc..16487cb 100644 --- a/src/AetherApplication.js +++ b/src/AetherApplication.js @@ -52,6 +52,7 @@ export const AetherApplication = GObject.registerClass( flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE, }); this._wallpaperPath = null; + this._ohmydebnMode = false; // Ensure ~/Wallpapers directory exists const wallpapersDir = GLib.build_filenamev([ @@ -184,6 +185,15 @@ export const AetherApplication = GObject.registerClass( 'Output directory for generated theme files (use with --generate --no-apply)', 'PATH' ); + + this.add_main_option( + 'ohmydebn', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + 'Enable OhMyDebn-specific behavior (start on Themes tab)', + null + ); } /** @@ -195,6 +205,9 @@ export const AetherApplication = GObject.registerClass( vfunc_command_line(commandLine) { const options = commandLine.get_options_dict(); + // Handle --ohmydebn flag + this._ohmydebnMode = options.contains('ohmydebn'); + // Run migrations before any command execution runMigrations(); @@ -429,7 +442,7 @@ export const AetherApplication = GObject.registerClass( _launchWidget() { // Initialize theme manager for consistent styling if (!themeManager) { - themeManager = new ThemeManager(); + themeManager = new ThemeManager(this._ohmydebnMode); } this.themeManager = themeManager; @@ -516,9 +529,8 @@ export const AetherApplication = GObject.registerClass( */ async _downloadWallpaperForBlueprint(url) { try { - const {ensureDirectoryExists} = await import( - './utils/file-utils.js' - ); + const {ensureDirectoryExists} = + await import('./utils/file-utils.js'); const Soup = (await import('gi://Soup?version=3.0')).default; const wallpapersDir = GLib.build_filenamev([ @@ -624,11 +636,13 @@ export const AetherApplication = GObject.registerClass( // Initialize theme manager only when GUI is activated if (!themeManager) { - themeManager = new ThemeManager(); - console.log(`Base theme: ${themeManager.getThemePath()}`); - console.log( - `Override theme: ${themeManager.getOverridePath()} (edit this file)` - ); + themeManager = new ThemeManager(this._ohmydebnMode); + if (!this._ohmydebnMode) { + console.log(`Base theme: ${themeManager.getThemePath()}`); + console.log( + `Override theme: ${themeManager.getOverridePath()} (edit this file)` + ); + } } this.themeManager = themeManager; diff --git a/src/AetherWindow.js b/src/AetherWindow.js index 93bcce1..e0447e3 100644 --- a/src/AetherWindow.js +++ b/src/AetherWindow.js @@ -60,10 +60,14 @@ export const AetherWindow = GObject.registerClass( default_height: 700, }); + this._ohmydebnMode = application._ohmydebnMode || false; this.configWriter = new ConfigWriter(); this._initializeUI(); - this._connectSignals(); - this._connectThemeState(); + + // Set initial tab based on OhMyDebn mode + if (this._ohmydebnMode) { + this._viewStack.set_visible_child_name('blueprints'); + } } /** @@ -346,7 +350,7 @@ export const AetherWindow = GObject.registerClass( ); // Blueprints page - this._blueprintsView = new BlueprintsView(); + this._blueprintsView = new BlueprintsView(this._ohmydebnMode); this._blueprintsView.connect( 'blueprint-applied', (_, blueprint) => { @@ -362,6 +366,11 @@ export const AetherWindow = GObject.registerClass( this._importOmarchyTheme(theme); }); this._blueprintsView.connect('theme-applied', (_, theme) => { + // Reload OhMyDebn theme if in OhMyDebn mode + if (this._ohmydebnMode && this.application.themeManager) { + this.application.themeManager.reloadOhMyDebnTheme(); + } + const toast = new Adw.Toast({ title: `Applied theme: ${theme.name}`, timeout: 3, @@ -404,8 +413,9 @@ export const AetherWindow = GObject.registerClass( * @param {Object} [metadata] - Optional wallpaper metadata */ _onBrowserWallpaperSelected(path, metadata = null) { - // Switch to editor tab - this._viewStack.set_visible_child_name('editor'); + // Switch to editor tab (or themes tab in OhMyDebn mode) + const defaultTab = this._ohmydebnMode ? 'blueprints' : 'editor'; + this._viewStack.set_visible_child_name(defaultTab); // Reset adjustments and app overrides when changing wallpaper this.settingsSidebar.resetAdjustments(); @@ -743,6 +753,11 @@ export const AetherWindow = GObject.registerClass( }); if (result.success) { + // Reload OhMyDebn theme if in OhMyDebn mode + if (this._ohmydebnMode && this.application.themeManager) { + this.application.themeManager.reloadOhMyDebnTheme(); + } + const message = result.isOmarchy ? 'Theme applied successfully' : `Theme created at ${result.themePath}`; diff --git a/src/components/BlueprintsView.js b/src/components/BlueprintsView.js index 4f59083..cfaa59e 100644 --- a/src/components/BlueprintsView.js +++ b/src/components/BlueprintsView.js @@ -54,12 +54,13 @@ export const BlueprintsView = GObject.registerClass( }, }, class BlueprintsView extends Gtk.Box { - _init() { + _init(ohmydebnMode = false) { super._init({ orientation: Gtk.Orientation.VERTICAL, spacing: 0, }); + this._ohmydebnMode = ohmydebnMode; this._blueprints = []; this._blueprintsDir = GLib.build_filenamev([ GLib.get_user_config_dir(), @@ -96,7 +97,7 @@ export const BlueprintsView = GObject.registerClass( active: true, }); this._themesToggle = new Gtk.ToggleButton({ - label: 'Omarchy Themes', + label: this._ohmydebnMode ? 'System Themes' : 'Omarchy Themes', active: false, }); @@ -145,7 +146,7 @@ export const BlueprintsView = GObject.registerClass( this._contentStack.add_named(blueprintsContent, 'blueprints'); // Omarchy themes browser - this._omarchyBrowser = new OmarchyThemesBrowser(); + this._omarchyBrowser = new OmarchyThemesBrowser(this._ohmydebnMode); this._omarchyBrowser.connect('theme-imported', (_, theme) => { this.emit('theme-imported', theme); }); @@ -154,7 +155,18 @@ export const BlueprintsView = GObject.registerClass( }); this._contentStack.add_named(this._omarchyBrowser, 'themes'); - this._contentStack.set_visible_child_name('blueprints'); + const initialView = this._ohmydebnMode ? 'themes' : 'blueprints'; + this._contentStack.set_visible_child_name(initialView); + + // Set toggle button state to match initial view + if (this._ohmydebnMode) { + this._themesToggle.set_active(true); + this._blueprintsToggle.set_active(false); + } else { + this._blueprintsToggle.set_active(true); + this._themesToggle.set_active(false); + } + this.append(this._contentStack); } diff --git a/src/components/OmarchyThemeCard.js b/src/components/OmarchyThemeCard.js index 80da7a1..8aa66f8 100644 --- a/src/components/OmarchyThemeCard.js +++ b/src/components/OmarchyThemeCard.js @@ -46,6 +46,30 @@ export const OmarchyThemeCard = GObject.registerClass( }); this._theme = theme; + this._currentImageIndex = 0; + + // Build list of available preview images + this._availableImages = []; + if (theme.previewImage) { + this._availableImages.push(theme.previewImage); + } + if (theme.wallpapers) { + for (const wp of theme.wallpapers) { + if (wp !== theme.previewImage) { + this._availableImages.push(wp); + } + } + } + this._hasMultipleImages = this._availableImages.length > 1; + + // For current theme, show first wallpaper instead of preview.png + if ( + theme.isCurrentTheme && + theme.wallpapers && + theme.wallpapers.length > 0 + ) { + this._availableImages = [...theme.wallpapers]; + } // Sharp card styling applyCssToWidget( @@ -67,19 +91,54 @@ export const OmarchyThemeCard = GObject.registerClass( * @private */ _buildUI() { + // Content area that expands to push buttons to bottom + this._contentBox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + vexpand: true, + }); + // Thumbnail/preview - const thumbnail = this._createThumbnail(); - this.append(thumbnail); + this._thumbnailWidget = this._createThumbnail( + this._availableImages[0] + ); + // Enforce fixed size for thumbnail + applyCssToWidget( + this._thumbnailWidget, + 'picture { min-height: 120px; max-height: 120px; }' + ); + this._contentBox.append(this._thumbnailWidget); + + // Add method to update thumbnail for external callers + this.updateThumbnail = imageIndex => { + if (imageIndex < this._availableImages.length) { + this._currentImageIndex = imageIndex; + const newThumbnail = this._createThumbnail( + this._availableImages[imageIndex] + ); + applyCssToWidget( + newThumbnail, + 'picture { min-height: 120px; max-height: 120px; }' + ); + const child = this._contentBox.get_first_child(); + if (child) { + this._contentBox.remove(child); + } + this._contentBox.prepend(newThumbnail); + this._thumbnailWidget = newThumbnail; + } + }; // Color grid const colorGrid = this._createColorGrid(); - this.append(colorGrid); + this._contentBox.append(colorGrid); // Name and badges row const nameRow = this._createNameRow(); - this.append(nameRow); + this._contentBox.append(nameRow); + + this.append(this._contentBox); - // Action buttons + // Action buttons - always at bottom const buttonBox = this._createButtons(); this.append(buttonBox); } @@ -89,19 +148,16 @@ export const OmarchyThemeCard = GObject.registerClass( * @private * @returns {Gtk.Widget} Thumbnail widget */ - _createThumbnail() { + _createThumbnail(imagePath = null) { const theme = this._theme; + const useImage = imagePath || theme.previewImage; - // If we have a preview image, try to load it - if ( - theme.previewImage && - GLib.file_test(theme.previewImage, GLib.FileTest.EXISTS) - ) { + // If we have an image, try to load it + if (useImage && GLib.file_test(useImage, GLib.FileTest.EXISTS)) { try { - const file = Gio.File.new_for_path(theme.previewImage); - const thumbPath = thumbnailService.getThumbnailPath( - theme.previewImage - ); + const file = Gio.File.new_for_path(useImage); + const thumbPath = + thumbnailService.getThumbnailPath(useImage); const thumbFile = Gio.File.new_for_path(thumbPath); let pixbuf; @@ -115,7 +171,7 @@ export const OmarchyThemeCard = GObject.registerClass( } else { // Generate thumbnail pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - theme.previewImage, + useImage, 300, 300, true @@ -343,10 +399,15 @@ export const OmarchyThemeCard = GObject.registerClass( margin_bottom: 12, }); - // Import button + // Import button - disable if theme has no colors.toml + const hasColors = this._theme.hasColors !== false; const importButton = this._createButton('Import', false, () => { this.emit('theme-import', this._theme); }); + importButton.set_sensitive(hasColors); + if (!hasColors) { + applyCssToWidget(importButton, 'button { opacity: 0.5; }'); + } buttonBox.append(importButton); // Apply button @@ -359,6 +420,63 @@ export const OmarchyThemeCard = GObject.registerClass( applyButton.set_sensitive(!isCurrentTheme); buttonBox.append(applyButton); + // Next Image button - for non-current themes with multiple images + if (!isCurrentTheme && this._hasMultipleImages) { + const nextImageButton = this._createButton( + 'Next Image', + false, + () => { + this._currentImageIndex = + (this._currentImageIndex + 1) % + this._availableImages.length; + const newImagePath = + this._availableImages[this._currentImageIndex]; + const newThumbnail = + this._createThumbnail(newImagePath); + // Enforce fixed size for new thumbnail + applyCssToWidget( + newThumbnail, + 'picture { min-height: 120px; max-height: 120px; }' + ); + + // Replace old thumbnail with new one + const child = this._contentBox.get_first_child(); + if (child) { + this._contentBox.remove(child); + } + this._contentBox.prepend(newThumbnail); + } + ); + buttonBox.append(nextImageButton); + } + + // Next Background button - only for current theme with multiple backgrounds + if (isCurrentTheme && this._theme.hasMultipleBackgrounds) { + const nextBgButton = this._createButton( + 'Next BG', + false, + () => { + // Cycle to next background image in thumbnail + this._currentImageIndex = + (this._currentImageIndex + 1) % + this._availableImages.length; + this.updateThumbnail(this._currentImageIndex); + + try { + GLib.spawn_command_line_async( + '/usr/share/ohmydebn/bin/ohmydebn-theme-bg-next' + ); + } catch (e) { + console.error( + 'Failed to run ohmydebn-theme-bg-next:', + e.message + ); + } + } + ); + buttonBox.append(nextBgButton); + } + return buttonBox; } diff --git a/src/components/OmarchyThemesBrowser.js b/src/components/OmarchyThemesBrowser.js index e2bb707..af882f2 100644 --- a/src/components/OmarchyThemesBrowser.js +++ b/src/components/OmarchyThemesBrowser.js @@ -35,15 +35,19 @@ export const OmarchyThemesBrowser = GObject.registerClass( }, }, class OmarchyThemesBrowser extends Gtk.Box { - _init() { + _init(ohmydebnMode = false) { super._init({ orientation: Gtk.Orientation.VERTICAL, spacing: 0, }); + this._ohmydebnMode = ohmydebnMode; this._themes = []; this._searchQuery = ''; + // Set the mode on the service so applyTheme uses correct command + omarchyThemeService.setOhMyDebnMode(ohmydebnMode); + this._buildUI(); this._loadThemesAsync(); } @@ -61,8 +65,8 @@ export const OmarchyThemesBrowser = GObject.registerClass( }); const clamp = new Adw.Clamp({ - maximum_size: 900, - tightening_threshold: 700, + maximum_size: this._ohmydebnMode ? 1600 : 900, + tightening_threshold: this._ohmydebnMode ? 1200 : 700, }); const mainBox = new Gtk.Box({ @@ -122,7 +126,7 @@ export const OmarchyThemesBrowser = GObject.registerClass( }); const title = new Gtk.Label({ - label: 'Omarchy Themes', + label: this._ohmydebnMode ? 'System Themes' : 'Omarchy Themes', halign: Gtk.Align.START, css_classes: ['title-2'], }); @@ -262,7 +266,7 @@ export const OmarchyThemesBrowser = GObject.registerClass( _createContentView() { this._flowBox = new Gtk.FlowBox({ valign: Gtk.Align.START, - max_children_per_line: 6, + max_children_per_line: 20, min_children_per_line: 2, selection_mode: Gtk.SelectionMode.NONE, homogeneous: false, @@ -270,9 +274,58 @@ export const OmarchyThemesBrowser = GObject.registerClass( column_spacing: GRID.COLUMN_SPACING, }); + // Set up responsive columns + this._setupResponsiveColumns(); + return this._flowBox; } + /** + * Set up responsive column management + * @private + */ + _setupResponsiveColumns() { + let lastWidth = 0; + + const updateColumns = () => { + const width = this._flowBox.get_allocated_width(); + if (width === lastWidth || width === 0) return; + lastWidth = width; + + // Calculate columns based on width (each theme card is ~200px) + let columns = Math.max(2, Math.floor(width / 200)); + // Cap at 12 columns max + columns = Math.min(12, columns); + this._flowBox.set_property('max_children_per_line', columns); + }; + + // Initial calculation + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + updateColumns(); + return GLib.SOURCE_REMOVE; + }); + + // Poll for size changes (similar to ResponsiveGridManager) + this._columnTimeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT_IDLE, + 500, + () => { + updateColumns(); + return GLib.SOURCE_CONTINUE; + } + ); + } + + /** + * Clean up resources + * @private + */ + _cleanup() { + if (this._columnTimeout) { + GLib.source_remove(this._columnTimeout); + } + } + /** * Load themes asynchronously * @private @@ -281,7 +334,9 @@ export const OmarchyThemesBrowser = GObject.registerClass( this._contentStack.set_visible_child_name('loading'); try { - this._themes = await omarchyThemeService.loadAllThemes(); + this._themes = await omarchyThemeService.loadAllThemes({ + ohmydebnMode: this._ohmydebnMode, + }); this._updateCurrentThemeLabel(); if (this._themes.length === 0) { diff --git a/src/services/omarchy-theme-service.js b/src/services/omarchy-theme-service.js index b20954a..d8beb9e 100644 --- a/src/services/omarchy-theme-service.js +++ b/src/services/omarchy-theme-service.js @@ -58,6 +58,23 @@ const SYSTEM_THEMES_DIR = GLib.build_filenamev([ 'themes', ]); +/** + * OhMyDebn system themes directory path (/usr/share/ohmydebn-themes) + * @constant {string} + */ +const OHMYDEBN_SYSTEM_THEMES_DIR = '/usr/share/ohmydebn-themes'; + +/** + * OhMyDebn user themes directory path (~/.config/ohmydebn/themes) + * @constant {string} + */ +const OHMYDEBN_USER_THEMES_DIR = GLib.build_filenamev([ + GLib.get_home_dir(), + '.config', + 'ohmydebn', + 'themes', +]); + /** * Current theme file path * @constant {string} @@ -70,6 +87,18 @@ const CURRENT_THEME_FILE = GLib.build_filenamev([ 'theme.name', ]); +/** + * OhMyDebn current theme file path + * @constant {string} + */ +const OHMYDEBN_CURRENT_THEME_FILE = GLib.build_filenamev([ + GLib.get_home_dir(), + '.config', + 'ohmydebn', + 'current', + 'theme.name', +]); + /** * Aether theme output directory * @constant {string} @@ -91,13 +120,37 @@ export class OmarchyThemeService { /** @private @type {string|null} */ this._currentThemeName = null; + + /** @private @type {boolean} */ + this._ohmydebnMode = false; + } + + /** + * Set the OhMyDebn mode + * @param {boolean} mode - Whether to use OhMyDebn paths/commands + */ + setOhMyDebnMode(mode) { + this._ohmydebnMode = mode; } /** * Load all installed Omarchy themes from both user and system directories + * @param {Object} [options] - Options object + * @param {boolean} [options.ohmydebnMode] - If true, load from OhMyDebn directories instead * @returns {Promise} Array of theme objects */ - async loadAllThemes() { + async loadAllThemes(options = {}) { + const {ohmydebnMode = false} = options; + this._ohmydebnMode = ohmydebnMode; + + // Use OhMyDebn directories if in ohmydebn mode + const userThemesDir = ohmydebnMode + ? OHMYDEBN_USER_THEMES_DIR + : USER_THEMES_DIR; + const systemThemesDir = ohmydebnMode + ? OHMYDEBN_SYSTEM_THEMES_DIR + : SYSTEM_THEMES_DIR; + this._themes = []; this._currentThemeName = this.getCurrentThemeName(); @@ -105,12 +158,8 @@ export class OmarchyThemeService { const loadedThemeNames = new Set(); // Load user themes first (they take priority), then system themes - this._loadThemesFromDirectory(USER_THEMES_DIR, false, loadedThemeNames); - this._loadThemesFromDirectory( - SYSTEM_THEMES_DIR, - true, - loadedThemeNames - ); + this._loadThemesFromDirectory(userThemesDir, false, loadedThemeNames); + this._loadThemesFromDirectory(systemThemesDir, true, loadedThemeNames); // Sort themes alphabetically, with current theme first this._themes.sort((a, b) => { @@ -174,11 +223,15 @@ export class OmarchyThemeService { * @returns {string|null} Current theme name or null */ getCurrentThemeName() { + const currentThemeFile = this._ohmydebnMode + ? OHMYDEBN_CURRENT_THEME_FILE + : CURRENT_THEME_FILE; + try { - if (!fileExists(CURRENT_THEME_FILE)) { + if (!fileExists(currentThemeFile)) { return null; } - const content = readFileAsText(CURRENT_THEME_FILE); + const content = readFileAsText(currentThemeFile); return content.trim(); } catch (e) { console.error('Error reading current theme:', e.message); @@ -192,9 +245,13 @@ export class OmarchyThemeService { * @returns {Promise} Success status */ async applyTheme(themeName) { + const themeCommand = this._ohmydebnMode + ? 'ohmydebn-theme-set' + : 'omarchy-theme-set'; + try { const subprocess = Gio.Subprocess.new( - ['omarchy-theme-set', themeName], + [themeCommand, themeName], Gio.SubprocessFlags.NONE ); const success = await new Promise(resolve => { @@ -204,7 +261,7 @@ export class OmarchyThemeService { resolve(proc.get_successful()); } catch (e) { console.error( - 'Error running omarchy-theme-set:', + `Error running ${themeCommand}:`, e.message ); resolve(false); @@ -270,6 +327,33 @@ export class OmarchyThemeService { // Extract colors const colorResult = this._extractColors(resolvedPath); if (!colorResult) { + // In OhMyDebn mode, still load theme but mark as having no colors + if (this._ohmydebnMode) { + const metadata = this._getThemeMetadata( + resolvedPath, + themeName + ); + return { + name: themeName, + path: themePath, + colors: null, + background: null, + foreground: null, + extendedColors: {}, + description: metadata.description, + previewImage: metadata.previewImage, + wallpapers: metadata.wallpapers, + isSymlink, + symlinkTarget, + isCurrentTheme: themeName === this._currentThemeName, + isAetherGenerated, + isSystemTheme, + hasColors: false, + hasMultipleBackgrounds: + metadata.wallpapers && + metadata.wallpapers.length > 1, + }; + } // Skip themes without colors.toml return null; } @@ -292,6 +376,9 @@ export class OmarchyThemeService { isCurrentTheme: themeName === this._currentThemeName, isAetherGenerated, isSystemTheme, + hasColors: true, + hasMultipleBackgrounds: + metadata.wallpapers && metadata.wallpapers.length > 1, }; } catch (e) { console.error(`Error loading theme ${themeName}:`, e.message); diff --git a/src/services/theme-manager.js b/src/services/theme-manager.js index 1cca1dd..0eaca4c 100644 --- a/src/services/theme-manager.js +++ b/src/services/theme-manager.js @@ -3,7 +3,7 @@ import Gio from 'gi://Gio'; import Gtk from 'gi://Gtk?version=4.0'; import Gdk from 'gi://Gdk?version=4.0'; -import {ensureDirectoryExists} from '../utils/file-utils.js'; +import {ensureDirectoryExists, readFileAsText} from '../utils/file-utils.js'; /** * ThemeManager - Manages Aether's custom theming system with live reload @@ -42,21 +42,309 @@ export class ThemeManager { * Creates base theme if missing, sets up file monitors, applies CSS * @constructor */ - constructor() { + constructor(ohmydebnMode = false) { this.cssProvider = null; this.sharpCornersCssProvider = null; + this._ohmydebnCssProvider = null; + this._ohmydebnThemeMonitor = null; this.fileMonitor = null; this.overrideFileMonitor = null; this.omarchyThemeMonitor = null; this.themeFile = null; this.overrideFile = null; + this._ohmydebnMode = ohmydebnMode; this._initializeThemeFiles(); - this._applyGlobalSharpCorners(); - this._applyTheme(); + + if (this._ohmydebnMode) { + // In OhMyDebn mode, try to use the system GTK theme + const settings = Gtk.Settings.get_default(); + + // Get the theme from dconf + let themeName = null; + try { + const [ok, stdout] = GLib.spawn_command_line_sync( + 'dconf read /org/gnome/desktop/interface/gtk-theme' + ); + if (ok) { + themeName = new TextDecoder().decode(stdout).trim(); + themeName = themeName.replace(/^'|'$/g, ''); + } + } catch (e) { + // Use system default if dconf fails + } + + console.log(`OhMyDebn theme: ${themeName}`); + + // Try to load GTK4 theme from ~/.themes/ (where OhMyDebn generates cinnamon themes) + if (themeName) { + const userThemePath = GLib.build_filenamev([ + GLib.get_home_dir(), + '.themes', + themeName, + 'gtk-4.0', + 'gtk.css', + ]); + + if (GLib.file_test(userThemePath, GLib.FileTest.EXISTS)) { + try { + const provider = new Gtk.CssProvider(); + provider.load_from_path(userThemePath); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + this._ohmydebnCssProvider = provider; + console.log(`Loaded GTK4 theme from: ${userThemePath}`); + } catch (e) { + console.error(`Failed to load theme: ${e.message}`); + } + } else { + console.log(`Theme not found at: ${userThemePath}`); + } + + // Set up file monitor for theme changes + this._setupOhMyDebnThemeMonitor(userThemePath); + } + } else { + // Normal (non-OhMyDebn) mode: apply Aether's custom theme with sharp corners + this._applyGlobalSharpCorners(); + this._applyTheme(); + } + this._setupFileMonitors(); } + /** + * Reload the OhMyDebn GTK4 theme (called when a theme is applied) + * @public + */ + reloadOhMyDebnTheme() { + if (!this._ohmydebnMode) return; + + // Add a small delay to allow theme regeneration to complete + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + this._doReloadOhMyDebnTheme(); + return GLib.SOURCE_REMOVE; + }); + } + + _doReloadOhMyDebnTheme() { + // Get the current theme from dconf + let themeName = null; + try { + const [ok, stdout] = GLib.spawn_command_line_sync( + 'dconf read /org/gnome/desktop/interface/gtk-theme' + ); + if (ok) { + themeName = new TextDecoder().decode(stdout).trim(); + themeName = themeName.replace(/^'|'$/g, ''); + } + } catch (e) { + return; + } + + if (!themeName) return; + + console.log(`Reloading OhMyDebn theme: ${themeName}`); + + // Remove old provider if exists + if (this._ohmydebnCssProvider) { + try { + Gtk.StyleContext.remove_provider_for_display( + Gdk.Display.get_default(), + this._ohmydebnCssProvider + ); + } catch (e) { + // Provider may already be removed + } + } + + // Load GTK4 theme from ~/.themes/ + const userThemePath = GLib.build_filenamev([ + GLib.get_home_dir(), + '.themes', + themeName, + 'gtk-4.0', + 'gtk.css', + ]); + + if (GLib.file_test(userThemePath, GLib.FileTest.EXISTS)) { + try { + // Always create a new provider for each reload + const provider = new Gtk.CssProvider(); + provider.load_from_path(userThemePath); + + // Use THEME priority which is higher than APPLICATION + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_THEME + ); + + // Keep track of the new provider + this._ohmydebnCssProvider = provider; + console.log(`Reloaded GTK4 theme from: ${userThemePath}`); + + // Update file monitor for new theme + this._setupOhMyDebnThemeMonitor(userThemePath); + } catch (e) { + console.error(`Failed to reload theme: ${e.message}`); + } + } + } + + _setupOhMyDebnThemeMonitor(themeCssPath) { + // Remove old monitor if exists + if (this._ohmydebnThemeMonitor) { + this._ohmydebnThemeMonitor.cancel(); + } + + try { + const themeFile = Gio.File.new_for_path(themeCssPath); + this._ohmydebnThemeMonitor = themeFile.monitor_file( + Gio.FileMonitorFlags.NONE, + null + ); + + this._ohmydebnThemeMonitor.connect( + 'changed', + (monitor, file, otherFile, eventType) => { + if ( + eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT || + eventType === Gio.FileMonitorEvent.CHANGED + ) { + console.log( + 'OhMyDebn theme file changed, reloading...' + ); + this._doReloadOhMyDebnTheme(); + } + } + ); + + console.log('File monitor setup for OhMyDebn theme'); + } catch (e) { + console.error(`Failed to setup theme monitor: ${e.message}`); + } + } + + _applyOhMyDebnColors(themeName) { + if (!themeName) return; + + // Look for colors.toml in ohmydebn-themes directories + const themePaths = [ + `/usr/share/ohmydebn-themes/${themeName}/colors.toml`, + `${GLib.get_home_dir()}/.local/share/ohmydebn/themes/${themeName}/colors.toml`, + ]; + + let colorsTomlPath = null; + for (const path of themePaths) { + if (GLib.file_test(path, GLib.FileTest.EXISTS)) { + colorsTomlPath = path; + break; + } + } + + if (!colorsTomlPath) { + console.log(`Could not find colors.toml for theme: ${themeName}`); + return; + } + + try { + const content = readFileAsText(colorsTomlPath); + const colors = this._parseColorsToml(content); + + // Build CSS with @define-color + const css = this._buildOhMyDebnCss(colors); + this._applyOhMyDebnCss(css, colorsTomlPath); + } catch (e) { + console.error(`Error applying OhMyDebn colors: ${e.message}`); + } + } + + _parseColorsToml(content) { + const colors = {}; + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const match = trimmed.match(/^(\w+)\s*=\s*"([^"]+)"$/); + if (match) { + colors[match[1]] = match[2]; + } + } + } + return colors; + } + + _buildOhMyDebnCss(colors) { + let css = '/* OhMyDebn colors */\n'; + + // First define the colors as variables + css += `@define-color window_bg_color ${colors.background || '#000000'};\n`; + css += `@define-color window_fg_color ${colors.foreground || '#ffffff'};\n`; + css += `@define-color accent_bg_color ${colors.accent || '#000000'};\n`; + css += `@define-color accent_fg_color ${colors.background || '#000000'};\n`; + css += `@define-color view_bg_color ${colors.background || '#000000'};\n`; + css += `@define-color view_fg_color ${colors.foreground || '#ffffff'};\n`; + css += `@define-color headerbar_bg_color ${colors.background || '#000000'};\n`; + css += `@define-color headerbar_fg_color ${colors.foreground || '#ffffff'};\n`; + css += `@define-color card_bg_color ${colors.color0 || '#000000'};\n`; + css += `@define-color card_fg_color ${colors.foreground || '#ffffff'};\n`; + + // Add color palette + for (let i = 0; i <= 15; i++) { + const color = colors[`color${i}`]; + if (color) { + css += `@define-color color${i} ${color};\n`; + } + } + + // Also add direct CSS overrides to ensure they take effect + css += ` +window, .window { + background-color: ${colors.background || '#000000'}; + color: ${colors.foreground || '#ffffff'}; +} +.view, .content { + background-color: ${colors.background || '#000000'}; + color: ${colors.foreground || '#ffffff'}; +} +headerbar, .headerbar { + background-color: ${colors.background || '#000000'}; + color: ${colors.foreground || '#ffffff'}; +} +button, .button { + background-color: ${colors.color0 || '#000000'}; +} +button.suggested-action, .suggested-action { + background-color: ${colors.accent || '#000000'}; + color: ${colors.background || '#ffffff'}; +} +`; + + return css; + } + + _applyOhMyDebnCss(css, sourcePath) { + try { + const provider = new Gtk.CssProvider(); + provider.load_from_string(css); + + // Apply to display with high priority + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + 800 + ); + + console.log(`Applied OhMyDebn colors from: ${sourcePath}`); + } catch (e) { + console.error(`Error applying OhMyDebn CSS: ${e.message}`); + } + } + _initializeThemeFiles() { const configDir = GLib.build_filenamev([ GLib.get_user_config_dir(), @@ -355,6 +643,8 @@ export class ThemeManager { } _applyTheme() { + if (this._ohmydebnMode) return; + try { // Remove old provider if exists if (this.cssProvider) { @@ -382,6 +672,7 @@ export class ThemeManager { } _reloadTheme() { + if (this._ohmydebnMode) return; GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { this._applyTheme(); return GLib.SOURCE_REMOVE; @@ -389,6 +680,7 @@ export class ThemeManager { } _revalidateAndReloadTheme() { + if (this._ohmydebnMode) return; GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { this._handleOverrideFile(); this._applyTheme(); From 4094eab66ddb6d07266c011da08e93b00eeeb27f Mon Sep 17 00:00:00 2001 From: Doug Burks Date: Mon, 16 Mar 2026 18:57:33 -0400 Subject: [PATCH 2/3] remove max-height --- src/AetherApplication.js | 32 ++- src/AetherWindow.js | 25 ++- src/components/BlueprintsView.js | 20 +- src/components/OmarchyThemeCard.js | 152 +++++++++++-- src/components/OmarchyThemesBrowser.js | 67 +++++- src/services/omarchy-theme-service.js | 109 ++++++++- src/services/theme-manager.js | 300 ++++++++++++++++++++++++- 7 files changed, 649 insertions(+), 56 deletions(-) diff --git a/src/AetherApplication.js b/src/AetherApplication.js index 0d284bc..16487cb 100644 --- a/src/AetherApplication.js +++ b/src/AetherApplication.js @@ -52,6 +52,7 @@ export const AetherApplication = GObject.registerClass( flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE, }); this._wallpaperPath = null; + this._ohmydebnMode = false; // Ensure ~/Wallpapers directory exists const wallpapersDir = GLib.build_filenamev([ @@ -184,6 +185,15 @@ export const AetherApplication = GObject.registerClass( 'Output directory for generated theme files (use with --generate --no-apply)', 'PATH' ); + + this.add_main_option( + 'ohmydebn', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + 'Enable OhMyDebn-specific behavior (start on Themes tab)', + null + ); } /** @@ -195,6 +205,9 @@ export const AetherApplication = GObject.registerClass( vfunc_command_line(commandLine) { const options = commandLine.get_options_dict(); + // Handle --ohmydebn flag + this._ohmydebnMode = options.contains('ohmydebn'); + // Run migrations before any command execution runMigrations(); @@ -429,7 +442,7 @@ export const AetherApplication = GObject.registerClass( _launchWidget() { // Initialize theme manager for consistent styling if (!themeManager) { - themeManager = new ThemeManager(); + themeManager = new ThemeManager(this._ohmydebnMode); } this.themeManager = themeManager; @@ -516,9 +529,8 @@ export const AetherApplication = GObject.registerClass( */ async _downloadWallpaperForBlueprint(url) { try { - const {ensureDirectoryExists} = await import( - './utils/file-utils.js' - ); + const {ensureDirectoryExists} = + await import('./utils/file-utils.js'); const Soup = (await import('gi://Soup?version=3.0')).default; const wallpapersDir = GLib.build_filenamev([ @@ -624,11 +636,13 @@ export const AetherApplication = GObject.registerClass( // Initialize theme manager only when GUI is activated if (!themeManager) { - themeManager = new ThemeManager(); - console.log(`Base theme: ${themeManager.getThemePath()}`); - console.log( - `Override theme: ${themeManager.getOverridePath()} (edit this file)` - ); + themeManager = new ThemeManager(this._ohmydebnMode); + if (!this._ohmydebnMode) { + console.log(`Base theme: ${themeManager.getThemePath()}`); + console.log( + `Override theme: ${themeManager.getOverridePath()} (edit this file)` + ); + } } this.themeManager = themeManager; diff --git a/src/AetherWindow.js b/src/AetherWindow.js index 93bcce1..e0447e3 100644 --- a/src/AetherWindow.js +++ b/src/AetherWindow.js @@ -60,10 +60,14 @@ export const AetherWindow = GObject.registerClass( default_height: 700, }); + this._ohmydebnMode = application._ohmydebnMode || false; this.configWriter = new ConfigWriter(); this._initializeUI(); - this._connectSignals(); - this._connectThemeState(); + + // Set initial tab based on OhMyDebn mode + if (this._ohmydebnMode) { + this._viewStack.set_visible_child_name('blueprints'); + } } /** @@ -346,7 +350,7 @@ export const AetherWindow = GObject.registerClass( ); // Blueprints page - this._blueprintsView = new BlueprintsView(); + this._blueprintsView = new BlueprintsView(this._ohmydebnMode); this._blueprintsView.connect( 'blueprint-applied', (_, blueprint) => { @@ -362,6 +366,11 @@ export const AetherWindow = GObject.registerClass( this._importOmarchyTheme(theme); }); this._blueprintsView.connect('theme-applied', (_, theme) => { + // Reload OhMyDebn theme if in OhMyDebn mode + if (this._ohmydebnMode && this.application.themeManager) { + this.application.themeManager.reloadOhMyDebnTheme(); + } + const toast = new Adw.Toast({ title: `Applied theme: ${theme.name}`, timeout: 3, @@ -404,8 +413,9 @@ export const AetherWindow = GObject.registerClass( * @param {Object} [metadata] - Optional wallpaper metadata */ _onBrowserWallpaperSelected(path, metadata = null) { - // Switch to editor tab - this._viewStack.set_visible_child_name('editor'); + // Switch to editor tab (or themes tab in OhMyDebn mode) + const defaultTab = this._ohmydebnMode ? 'blueprints' : 'editor'; + this._viewStack.set_visible_child_name(defaultTab); // Reset adjustments and app overrides when changing wallpaper this.settingsSidebar.resetAdjustments(); @@ -743,6 +753,11 @@ export const AetherWindow = GObject.registerClass( }); if (result.success) { + // Reload OhMyDebn theme if in OhMyDebn mode + if (this._ohmydebnMode && this.application.themeManager) { + this.application.themeManager.reloadOhMyDebnTheme(); + } + const message = result.isOmarchy ? 'Theme applied successfully' : `Theme created at ${result.themePath}`; diff --git a/src/components/BlueprintsView.js b/src/components/BlueprintsView.js index 4f59083..cfaa59e 100644 --- a/src/components/BlueprintsView.js +++ b/src/components/BlueprintsView.js @@ -54,12 +54,13 @@ export const BlueprintsView = GObject.registerClass( }, }, class BlueprintsView extends Gtk.Box { - _init() { + _init(ohmydebnMode = false) { super._init({ orientation: Gtk.Orientation.VERTICAL, spacing: 0, }); + this._ohmydebnMode = ohmydebnMode; this._blueprints = []; this._blueprintsDir = GLib.build_filenamev([ GLib.get_user_config_dir(), @@ -96,7 +97,7 @@ export const BlueprintsView = GObject.registerClass( active: true, }); this._themesToggle = new Gtk.ToggleButton({ - label: 'Omarchy Themes', + label: this._ohmydebnMode ? 'System Themes' : 'Omarchy Themes', active: false, }); @@ -145,7 +146,7 @@ export const BlueprintsView = GObject.registerClass( this._contentStack.add_named(blueprintsContent, 'blueprints'); // Omarchy themes browser - this._omarchyBrowser = new OmarchyThemesBrowser(); + this._omarchyBrowser = new OmarchyThemesBrowser(this._ohmydebnMode); this._omarchyBrowser.connect('theme-imported', (_, theme) => { this.emit('theme-imported', theme); }); @@ -154,7 +155,18 @@ export const BlueprintsView = GObject.registerClass( }); this._contentStack.add_named(this._omarchyBrowser, 'themes'); - this._contentStack.set_visible_child_name('blueprints'); + const initialView = this._ohmydebnMode ? 'themes' : 'blueprints'; + this._contentStack.set_visible_child_name(initialView); + + // Set toggle button state to match initial view + if (this._ohmydebnMode) { + this._themesToggle.set_active(true); + this._blueprintsToggle.set_active(false); + } else { + this._blueprintsToggle.set_active(true); + this._themesToggle.set_active(false); + } + this.append(this._contentStack); } diff --git a/src/components/OmarchyThemeCard.js b/src/components/OmarchyThemeCard.js index 80da7a1..8589881 100644 --- a/src/components/OmarchyThemeCard.js +++ b/src/components/OmarchyThemeCard.js @@ -46,6 +46,30 @@ export const OmarchyThemeCard = GObject.registerClass( }); this._theme = theme; + this._currentImageIndex = 0; + + // Build list of available preview images + this._availableImages = []; + if (theme.previewImage) { + this._availableImages.push(theme.previewImage); + } + if (theme.wallpapers) { + for (const wp of theme.wallpapers) { + if (wp !== theme.previewImage) { + this._availableImages.push(wp); + } + } + } + this._hasMultipleImages = this._availableImages.length > 1; + + // For current theme, show first wallpaper instead of preview.png + if ( + theme.isCurrentTheme && + theme.wallpapers && + theme.wallpapers.length > 0 + ) { + this._availableImages = [...theme.wallpapers]; + } // Sharp card styling applyCssToWidget( @@ -67,19 +91,54 @@ export const OmarchyThemeCard = GObject.registerClass( * @private */ _buildUI() { + // Content area that expands to push buttons to bottom + this._contentBox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + vexpand: true, + }); + // Thumbnail/preview - const thumbnail = this._createThumbnail(); - this.append(thumbnail); + this._thumbnailWidget = this._createThumbnail( + this._availableImages[0] + ); + // Enforce fixed size for thumbnail + applyCssToWidget( + this._thumbnailWidget, + 'picture { min-height: 120px; }' + ); + this._contentBox.append(this._thumbnailWidget); + + // Add method to update thumbnail for external callers + this.updateThumbnail = imageIndex => { + if (imageIndex < this._availableImages.length) { + this._currentImageIndex = imageIndex; + const newThumbnail = this._createThumbnail( + this._availableImages[imageIndex] + ); + applyCssToWidget( + newThumbnail, + 'picture { min-height: 120px; }' + ); + const child = this._contentBox.get_first_child(); + if (child) { + this._contentBox.remove(child); + } + this._contentBox.prepend(newThumbnail); + this._thumbnailWidget = newThumbnail; + } + }; // Color grid const colorGrid = this._createColorGrid(); - this.append(colorGrid); + this._contentBox.append(colorGrid); // Name and badges row const nameRow = this._createNameRow(); - this.append(nameRow); + this._contentBox.append(nameRow); + + this.append(this._contentBox); - // Action buttons + // Action buttons - always at bottom const buttonBox = this._createButtons(); this.append(buttonBox); } @@ -89,19 +148,16 @@ export const OmarchyThemeCard = GObject.registerClass( * @private * @returns {Gtk.Widget} Thumbnail widget */ - _createThumbnail() { + _createThumbnail(imagePath = null) { const theme = this._theme; + const useImage = imagePath || theme.previewImage; - // If we have a preview image, try to load it - if ( - theme.previewImage && - GLib.file_test(theme.previewImage, GLib.FileTest.EXISTS) - ) { + // If we have an image, try to load it + if (useImage && GLib.file_test(useImage, GLib.FileTest.EXISTS)) { try { - const file = Gio.File.new_for_path(theme.previewImage); - const thumbPath = thumbnailService.getThumbnailPath( - theme.previewImage - ); + const file = Gio.File.new_for_path(useImage); + const thumbPath = + thumbnailService.getThumbnailPath(useImage); const thumbFile = Gio.File.new_for_path(thumbPath); let pixbuf; @@ -115,7 +171,7 @@ export const OmarchyThemeCard = GObject.registerClass( } else { // Generate thumbnail pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - theme.previewImage, + useImage, 300, 300, true @@ -343,10 +399,15 @@ export const OmarchyThemeCard = GObject.registerClass( margin_bottom: 12, }); - // Import button + // Import button - disable if theme has no colors.toml + const hasColors = this._theme.hasColors !== false; const importButton = this._createButton('Import', false, () => { this.emit('theme-import', this._theme); }); + importButton.set_sensitive(hasColors); + if (!hasColors) { + applyCssToWidget(importButton, 'button { opacity: 0.5; }'); + } buttonBox.append(importButton); // Apply button @@ -359,6 +420,63 @@ export const OmarchyThemeCard = GObject.registerClass( applyButton.set_sensitive(!isCurrentTheme); buttonBox.append(applyButton); + // Next Image button - for non-current themes with multiple images + if (!isCurrentTheme && this._hasMultipleImages) { + const nextImageButton = this._createButton( + 'Next Image', + false, + () => { + this._currentImageIndex = + (this._currentImageIndex + 1) % + this._availableImages.length; + const newImagePath = + this._availableImages[this._currentImageIndex]; + const newThumbnail = + this._createThumbnail(newImagePath); + // Enforce fixed size for new thumbnail + applyCssToWidget( + newThumbnail, + 'picture { min-height: 120px; }' + ); + + // Replace old thumbnail with new one + const child = this._contentBox.get_first_child(); + if (child) { + this._contentBox.remove(child); + } + this._contentBox.prepend(newThumbnail); + } + ); + buttonBox.append(nextImageButton); + } + + // Next Background button - only for current theme with multiple backgrounds + if (isCurrentTheme && this._theme.hasMultipleBackgrounds) { + const nextBgButton = this._createButton( + 'Next BG', + false, + () => { + // Cycle to next background image in thumbnail + this._currentImageIndex = + (this._currentImageIndex + 1) % + this._availableImages.length; + this.updateThumbnail(this._currentImageIndex); + + try { + GLib.spawn_command_line_async( + '/usr/share/ohmydebn/bin/ohmydebn-theme-bg-next' + ); + } catch (e) { + console.error( + 'Failed to run ohmydebn-theme-bg-next:', + e.message + ); + } + } + ); + buttonBox.append(nextBgButton); + } + return buttonBox; } diff --git a/src/components/OmarchyThemesBrowser.js b/src/components/OmarchyThemesBrowser.js index e2bb707..af882f2 100644 --- a/src/components/OmarchyThemesBrowser.js +++ b/src/components/OmarchyThemesBrowser.js @@ -35,15 +35,19 @@ export const OmarchyThemesBrowser = GObject.registerClass( }, }, class OmarchyThemesBrowser extends Gtk.Box { - _init() { + _init(ohmydebnMode = false) { super._init({ orientation: Gtk.Orientation.VERTICAL, spacing: 0, }); + this._ohmydebnMode = ohmydebnMode; this._themes = []; this._searchQuery = ''; + // Set the mode on the service so applyTheme uses correct command + omarchyThemeService.setOhMyDebnMode(ohmydebnMode); + this._buildUI(); this._loadThemesAsync(); } @@ -61,8 +65,8 @@ export const OmarchyThemesBrowser = GObject.registerClass( }); const clamp = new Adw.Clamp({ - maximum_size: 900, - tightening_threshold: 700, + maximum_size: this._ohmydebnMode ? 1600 : 900, + tightening_threshold: this._ohmydebnMode ? 1200 : 700, }); const mainBox = new Gtk.Box({ @@ -122,7 +126,7 @@ export const OmarchyThemesBrowser = GObject.registerClass( }); const title = new Gtk.Label({ - label: 'Omarchy Themes', + label: this._ohmydebnMode ? 'System Themes' : 'Omarchy Themes', halign: Gtk.Align.START, css_classes: ['title-2'], }); @@ -262,7 +266,7 @@ export const OmarchyThemesBrowser = GObject.registerClass( _createContentView() { this._flowBox = new Gtk.FlowBox({ valign: Gtk.Align.START, - max_children_per_line: 6, + max_children_per_line: 20, min_children_per_line: 2, selection_mode: Gtk.SelectionMode.NONE, homogeneous: false, @@ -270,9 +274,58 @@ export const OmarchyThemesBrowser = GObject.registerClass( column_spacing: GRID.COLUMN_SPACING, }); + // Set up responsive columns + this._setupResponsiveColumns(); + return this._flowBox; } + /** + * Set up responsive column management + * @private + */ + _setupResponsiveColumns() { + let lastWidth = 0; + + const updateColumns = () => { + const width = this._flowBox.get_allocated_width(); + if (width === lastWidth || width === 0) return; + lastWidth = width; + + // Calculate columns based on width (each theme card is ~200px) + let columns = Math.max(2, Math.floor(width / 200)); + // Cap at 12 columns max + columns = Math.min(12, columns); + this._flowBox.set_property('max_children_per_line', columns); + }; + + // Initial calculation + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + updateColumns(); + return GLib.SOURCE_REMOVE; + }); + + // Poll for size changes (similar to ResponsiveGridManager) + this._columnTimeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT_IDLE, + 500, + () => { + updateColumns(); + return GLib.SOURCE_CONTINUE; + } + ); + } + + /** + * Clean up resources + * @private + */ + _cleanup() { + if (this._columnTimeout) { + GLib.source_remove(this._columnTimeout); + } + } + /** * Load themes asynchronously * @private @@ -281,7 +334,9 @@ export const OmarchyThemesBrowser = GObject.registerClass( this._contentStack.set_visible_child_name('loading'); try { - this._themes = await omarchyThemeService.loadAllThemes(); + this._themes = await omarchyThemeService.loadAllThemes({ + ohmydebnMode: this._ohmydebnMode, + }); this._updateCurrentThemeLabel(); if (this._themes.length === 0) { diff --git a/src/services/omarchy-theme-service.js b/src/services/omarchy-theme-service.js index b20954a..d8beb9e 100644 --- a/src/services/omarchy-theme-service.js +++ b/src/services/omarchy-theme-service.js @@ -58,6 +58,23 @@ const SYSTEM_THEMES_DIR = GLib.build_filenamev([ 'themes', ]); +/** + * OhMyDebn system themes directory path (/usr/share/ohmydebn-themes) + * @constant {string} + */ +const OHMYDEBN_SYSTEM_THEMES_DIR = '/usr/share/ohmydebn-themes'; + +/** + * OhMyDebn user themes directory path (~/.config/ohmydebn/themes) + * @constant {string} + */ +const OHMYDEBN_USER_THEMES_DIR = GLib.build_filenamev([ + GLib.get_home_dir(), + '.config', + 'ohmydebn', + 'themes', +]); + /** * Current theme file path * @constant {string} @@ -70,6 +87,18 @@ const CURRENT_THEME_FILE = GLib.build_filenamev([ 'theme.name', ]); +/** + * OhMyDebn current theme file path + * @constant {string} + */ +const OHMYDEBN_CURRENT_THEME_FILE = GLib.build_filenamev([ + GLib.get_home_dir(), + '.config', + 'ohmydebn', + 'current', + 'theme.name', +]); + /** * Aether theme output directory * @constant {string} @@ -91,13 +120,37 @@ export class OmarchyThemeService { /** @private @type {string|null} */ this._currentThemeName = null; + + /** @private @type {boolean} */ + this._ohmydebnMode = false; + } + + /** + * Set the OhMyDebn mode + * @param {boolean} mode - Whether to use OhMyDebn paths/commands + */ + setOhMyDebnMode(mode) { + this._ohmydebnMode = mode; } /** * Load all installed Omarchy themes from both user and system directories + * @param {Object} [options] - Options object + * @param {boolean} [options.ohmydebnMode] - If true, load from OhMyDebn directories instead * @returns {Promise} Array of theme objects */ - async loadAllThemes() { + async loadAllThemes(options = {}) { + const {ohmydebnMode = false} = options; + this._ohmydebnMode = ohmydebnMode; + + // Use OhMyDebn directories if in ohmydebn mode + const userThemesDir = ohmydebnMode + ? OHMYDEBN_USER_THEMES_DIR + : USER_THEMES_DIR; + const systemThemesDir = ohmydebnMode + ? OHMYDEBN_SYSTEM_THEMES_DIR + : SYSTEM_THEMES_DIR; + this._themes = []; this._currentThemeName = this.getCurrentThemeName(); @@ -105,12 +158,8 @@ export class OmarchyThemeService { const loadedThemeNames = new Set(); // Load user themes first (they take priority), then system themes - this._loadThemesFromDirectory(USER_THEMES_DIR, false, loadedThemeNames); - this._loadThemesFromDirectory( - SYSTEM_THEMES_DIR, - true, - loadedThemeNames - ); + this._loadThemesFromDirectory(userThemesDir, false, loadedThemeNames); + this._loadThemesFromDirectory(systemThemesDir, true, loadedThemeNames); // Sort themes alphabetically, with current theme first this._themes.sort((a, b) => { @@ -174,11 +223,15 @@ export class OmarchyThemeService { * @returns {string|null} Current theme name or null */ getCurrentThemeName() { + const currentThemeFile = this._ohmydebnMode + ? OHMYDEBN_CURRENT_THEME_FILE + : CURRENT_THEME_FILE; + try { - if (!fileExists(CURRENT_THEME_FILE)) { + if (!fileExists(currentThemeFile)) { return null; } - const content = readFileAsText(CURRENT_THEME_FILE); + const content = readFileAsText(currentThemeFile); return content.trim(); } catch (e) { console.error('Error reading current theme:', e.message); @@ -192,9 +245,13 @@ export class OmarchyThemeService { * @returns {Promise} Success status */ async applyTheme(themeName) { + const themeCommand = this._ohmydebnMode + ? 'ohmydebn-theme-set' + : 'omarchy-theme-set'; + try { const subprocess = Gio.Subprocess.new( - ['omarchy-theme-set', themeName], + [themeCommand, themeName], Gio.SubprocessFlags.NONE ); const success = await new Promise(resolve => { @@ -204,7 +261,7 @@ export class OmarchyThemeService { resolve(proc.get_successful()); } catch (e) { console.error( - 'Error running omarchy-theme-set:', + `Error running ${themeCommand}:`, e.message ); resolve(false); @@ -270,6 +327,33 @@ export class OmarchyThemeService { // Extract colors const colorResult = this._extractColors(resolvedPath); if (!colorResult) { + // In OhMyDebn mode, still load theme but mark as having no colors + if (this._ohmydebnMode) { + const metadata = this._getThemeMetadata( + resolvedPath, + themeName + ); + return { + name: themeName, + path: themePath, + colors: null, + background: null, + foreground: null, + extendedColors: {}, + description: metadata.description, + previewImage: metadata.previewImage, + wallpapers: metadata.wallpapers, + isSymlink, + symlinkTarget, + isCurrentTheme: themeName === this._currentThemeName, + isAetherGenerated, + isSystemTheme, + hasColors: false, + hasMultipleBackgrounds: + metadata.wallpapers && + metadata.wallpapers.length > 1, + }; + } // Skip themes without colors.toml return null; } @@ -292,6 +376,9 @@ export class OmarchyThemeService { isCurrentTheme: themeName === this._currentThemeName, isAetherGenerated, isSystemTheme, + hasColors: true, + hasMultipleBackgrounds: + metadata.wallpapers && metadata.wallpapers.length > 1, }; } catch (e) { console.error(`Error loading theme ${themeName}:`, e.message); diff --git a/src/services/theme-manager.js b/src/services/theme-manager.js index 1cca1dd..0eaca4c 100644 --- a/src/services/theme-manager.js +++ b/src/services/theme-manager.js @@ -3,7 +3,7 @@ import Gio from 'gi://Gio'; import Gtk from 'gi://Gtk?version=4.0'; import Gdk from 'gi://Gdk?version=4.0'; -import {ensureDirectoryExists} from '../utils/file-utils.js'; +import {ensureDirectoryExists, readFileAsText} from '../utils/file-utils.js'; /** * ThemeManager - Manages Aether's custom theming system with live reload @@ -42,21 +42,309 @@ export class ThemeManager { * Creates base theme if missing, sets up file monitors, applies CSS * @constructor */ - constructor() { + constructor(ohmydebnMode = false) { this.cssProvider = null; this.sharpCornersCssProvider = null; + this._ohmydebnCssProvider = null; + this._ohmydebnThemeMonitor = null; this.fileMonitor = null; this.overrideFileMonitor = null; this.omarchyThemeMonitor = null; this.themeFile = null; this.overrideFile = null; + this._ohmydebnMode = ohmydebnMode; this._initializeThemeFiles(); - this._applyGlobalSharpCorners(); - this._applyTheme(); + + if (this._ohmydebnMode) { + // In OhMyDebn mode, try to use the system GTK theme + const settings = Gtk.Settings.get_default(); + + // Get the theme from dconf + let themeName = null; + try { + const [ok, stdout] = GLib.spawn_command_line_sync( + 'dconf read /org/gnome/desktop/interface/gtk-theme' + ); + if (ok) { + themeName = new TextDecoder().decode(stdout).trim(); + themeName = themeName.replace(/^'|'$/g, ''); + } + } catch (e) { + // Use system default if dconf fails + } + + console.log(`OhMyDebn theme: ${themeName}`); + + // Try to load GTK4 theme from ~/.themes/ (where OhMyDebn generates cinnamon themes) + if (themeName) { + const userThemePath = GLib.build_filenamev([ + GLib.get_home_dir(), + '.themes', + themeName, + 'gtk-4.0', + 'gtk.css', + ]); + + if (GLib.file_test(userThemePath, GLib.FileTest.EXISTS)) { + try { + const provider = new Gtk.CssProvider(); + provider.load_from_path(userThemePath); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + this._ohmydebnCssProvider = provider; + console.log(`Loaded GTK4 theme from: ${userThemePath}`); + } catch (e) { + console.error(`Failed to load theme: ${e.message}`); + } + } else { + console.log(`Theme not found at: ${userThemePath}`); + } + + // Set up file monitor for theme changes + this._setupOhMyDebnThemeMonitor(userThemePath); + } + } else { + // Normal (non-OhMyDebn) mode: apply Aether's custom theme with sharp corners + this._applyGlobalSharpCorners(); + this._applyTheme(); + } + this._setupFileMonitors(); } + /** + * Reload the OhMyDebn GTK4 theme (called when a theme is applied) + * @public + */ + reloadOhMyDebnTheme() { + if (!this._ohmydebnMode) return; + + // Add a small delay to allow theme regeneration to complete + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + this._doReloadOhMyDebnTheme(); + return GLib.SOURCE_REMOVE; + }); + } + + _doReloadOhMyDebnTheme() { + // Get the current theme from dconf + let themeName = null; + try { + const [ok, stdout] = GLib.spawn_command_line_sync( + 'dconf read /org/gnome/desktop/interface/gtk-theme' + ); + if (ok) { + themeName = new TextDecoder().decode(stdout).trim(); + themeName = themeName.replace(/^'|'$/g, ''); + } + } catch (e) { + return; + } + + if (!themeName) return; + + console.log(`Reloading OhMyDebn theme: ${themeName}`); + + // Remove old provider if exists + if (this._ohmydebnCssProvider) { + try { + Gtk.StyleContext.remove_provider_for_display( + Gdk.Display.get_default(), + this._ohmydebnCssProvider + ); + } catch (e) { + // Provider may already be removed + } + } + + // Load GTK4 theme from ~/.themes/ + const userThemePath = GLib.build_filenamev([ + GLib.get_home_dir(), + '.themes', + themeName, + 'gtk-4.0', + 'gtk.css', + ]); + + if (GLib.file_test(userThemePath, GLib.FileTest.EXISTS)) { + try { + // Always create a new provider for each reload + const provider = new Gtk.CssProvider(); + provider.load_from_path(userThemePath); + + // Use THEME priority which is higher than APPLICATION + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_THEME + ); + + // Keep track of the new provider + this._ohmydebnCssProvider = provider; + console.log(`Reloaded GTK4 theme from: ${userThemePath}`); + + // Update file monitor for new theme + this._setupOhMyDebnThemeMonitor(userThemePath); + } catch (e) { + console.error(`Failed to reload theme: ${e.message}`); + } + } + } + + _setupOhMyDebnThemeMonitor(themeCssPath) { + // Remove old monitor if exists + if (this._ohmydebnThemeMonitor) { + this._ohmydebnThemeMonitor.cancel(); + } + + try { + const themeFile = Gio.File.new_for_path(themeCssPath); + this._ohmydebnThemeMonitor = themeFile.monitor_file( + Gio.FileMonitorFlags.NONE, + null + ); + + this._ohmydebnThemeMonitor.connect( + 'changed', + (monitor, file, otherFile, eventType) => { + if ( + eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT || + eventType === Gio.FileMonitorEvent.CHANGED + ) { + console.log( + 'OhMyDebn theme file changed, reloading...' + ); + this._doReloadOhMyDebnTheme(); + } + } + ); + + console.log('File monitor setup for OhMyDebn theme'); + } catch (e) { + console.error(`Failed to setup theme monitor: ${e.message}`); + } + } + + _applyOhMyDebnColors(themeName) { + if (!themeName) return; + + // Look for colors.toml in ohmydebn-themes directories + const themePaths = [ + `/usr/share/ohmydebn-themes/${themeName}/colors.toml`, + `${GLib.get_home_dir()}/.local/share/ohmydebn/themes/${themeName}/colors.toml`, + ]; + + let colorsTomlPath = null; + for (const path of themePaths) { + if (GLib.file_test(path, GLib.FileTest.EXISTS)) { + colorsTomlPath = path; + break; + } + } + + if (!colorsTomlPath) { + console.log(`Could not find colors.toml for theme: ${themeName}`); + return; + } + + try { + const content = readFileAsText(colorsTomlPath); + const colors = this._parseColorsToml(content); + + // Build CSS with @define-color + const css = this._buildOhMyDebnCss(colors); + this._applyOhMyDebnCss(css, colorsTomlPath); + } catch (e) { + console.error(`Error applying OhMyDebn colors: ${e.message}`); + } + } + + _parseColorsToml(content) { + const colors = {}; + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const match = trimmed.match(/^(\w+)\s*=\s*"([^"]+)"$/); + if (match) { + colors[match[1]] = match[2]; + } + } + } + return colors; + } + + _buildOhMyDebnCss(colors) { + let css = '/* OhMyDebn colors */\n'; + + // First define the colors as variables + css += `@define-color window_bg_color ${colors.background || '#000000'};\n`; + css += `@define-color window_fg_color ${colors.foreground || '#ffffff'};\n`; + css += `@define-color accent_bg_color ${colors.accent || '#000000'};\n`; + css += `@define-color accent_fg_color ${colors.background || '#000000'};\n`; + css += `@define-color view_bg_color ${colors.background || '#000000'};\n`; + css += `@define-color view_fg_color ${colors.foreground || '#ffffff'};\n`; + css += `@define-color headerbar_bg_color ${colors.background || '#000000'};\n`; + css += `@define-color headerbar_fg_color ${colors.foreground || '#ffffff'};\n`; + css += `@define-color card_bg_color ${colors.color0 || '#000000'};\n`; + css += `@define-color card_fg_color ${colors.foreground || '#ffffff'};\n`; + + // Add color palette + for (let i = 0; i <= 15; i++) { + const color = colors[`color${i}`]; + if (color) { + css += `@define-color color${i} ${color};\n`; + } + } + + // Also add direct CSS overrides to ensure they take effect + css += ` +window, .window { + background-color: ${colors.background || '#000000'}; + color: ${colors.foreground || '#ffffff'}; +} +.view, .content { + background-color: ${colors.background || '#000000'}; + color: ${colors.foreground || '#ffffff'}; +} +headerbar, .headerbar { + background-color: ${colors.background || '#000000'}; + color: ${colors.foreground || '#ffffff'}; +} +button, .button { + background-color: ${colors.color0 || '#000000'}; +} +button.suggested-action, .suggested-action { + background-color: ${colors.accent || '#000000'}; + color: ${colors.background || '#ffffff'}; +} +`; + + return css; + } + + _applyOhMyDebnCss(css, sourcePath) { + try { + const provider = new Gtk.CssProvider(); + provider.load_from_string(css); + + // Apply to display with high priority + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + 800 + ); + + console.log(`Applied OhMyDebn colors from: ${sourcePath}`); + } catch (e) { + console.error(`Error applying OhMyDebn CSS: ${e.message}`); + } + } + _initializeThemeFiles() { const configDir = GLib.build_filenamev([ GLib.get_user_config_dir(), @@ -355,6 +643,8 @@ export class ThemeManager { } _applyTheme() { + if (this._ohmydebnMode) return; + try { // Remove old provider if exists if (this.cssProvider) { @@ -382,6 +672,7 @@ export class ThemeManager { } _reloadTheme() { + if (this._ohmydebnMode) return; GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { this._applyTheme(); return GLib.SOURCE_REMOVE; @@ -389,6 +680,7 @@ export class ThemeManager { } _revalidateAndReloadTheme() { + if (this._ohmydebnMode) return; GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { this._handleOverrideFile(); this._applyTheme(); From 21e4212846b7de095d6c113a3990b9cbb7bca0d3 Mon Sep 17 00:00:00 2001 From: Doug Burks Date: Tue, 17 Mar 2026 07:37:54 -0400 Subject: [PATCH 3/3] improve button order and logic --- src/components/OmarchyThemeCard.js | 149 +++++++++++++++----------- src/services/omarchy-theme-service.js | 45 +++++++- 2 files changed, 131 insertions(+), 63 deletions(-) diff --git a/src/components/OmarchyThemeCard.js b/src/components/OmarchyThemeCard.js index 8589881..c85567c 100644 --- a/src/components/OmarchyThemeCard.js +++ b/src/components/OmarchyThemeCard.js @@ -69,6 +69,45 @@ export const OmarchyThemeCard = GObject.registerClass( theme.wallpapers.length > 0 ) { this._availableImages = [...theme.wallpapers]; + + // Try to find the currently active background from symlink + const bgSymlink = GLib.build_filenamev([ + GLib.get_home_dir(), + '.config', + 'ohmydebn', + 'current', + 'background', + ]); + try { + const target = GLib.file_read_link(bgSymlink); + if (target) { + const targetBasename = GLib.path_get_basename(target); + const idx = this._availableImages.findIndex(img => { + return ( + GLib.path_get_basename(img) === targetBasename + ); + }); + if (idx >= 0) { + this._currentImageIndex = idx; + console.log( + `Found current background: ${targetBasename} at index ${idx}` + ); + } else { + console.log( + `Could not find current background: ${targetBasename} in available images:`, + this._availableImages.map(p => + GLib.path_get_basename(p) + ) + ); + } + } + } catch (e) { + // Symlink doesn't exist or can't be read, use default index 0 + console.log( + `Failed to read symlink ${bgSymlink}:`, + e.message + ); + } } // Sharp card styling @@ -99,7 +138,7 @@ export const OmarchyThemeCard = GObject.registerClass( // Thumbnail/preview this._thumbnailWidget = this._createThumbnail( - this._availableImages[0] + this._availableImages[this._currentImageIndex] ); // Enforce fixed size for thumbnail applyCssToWidget( @@ -399,69 +438,33 @@ export const OmarchyThemeCard = GObject.registerClass( margin_bottom: 12, }); - // Import button - disable if theme has no colors.toml - const hasColors = this._theme.hasColors !== false; - const importButton = this._createButton('Import', false, () => { - this.emit('theme-import', this._theme); - }); - importButton.set_sensitive(hasColors); - if (!hasColors) { - applyCssToWidget(importButton, 'button { opacity: 0.5; }'); - } - buttonBox.append(importButton); - - // Apply button + // Next Image button - always visible but disabled for single image const isCurrentTheme = this._theme.isCurrentTheme; - const applyButton = this._createButton( - isCurrentTheme ? 'Current' : 'Apply', - true, - () => this.emit('theme-apply', this._theme) - ); - applyButton.set_sensitive(!isCurrentTheme); - buttonBox.append(applyButton); - - // Next Image button - for non-current themes with multiple images - if (!isCurrentTheme && this._hasMultipleImages) { - const nextImageButton = this._createButton( - 'Next Image', - false, - () => { - this._currentImageIndex = - (this._currentImageIndex + 1) % - this._availableImages.length; - const newImagePath = - this._availableImages[this._currentImageIndex]; - const newThumbnail = - this._createThumbnail(newImagePath); - // Enforce fixed size for new thumbnail - applyCssToWidget( - newThumbnail, - 'picture { min-height: 120px; }' - ); + const nextImageButton = this._createButton( + 'Next Image', + false, + () => { + this._currentImageIndex = + (this._currentImageIndex + 1) % + this._availableImages.length; + const newImagePath = + this._availableImages[this._currentImageIndex]; + const newThumbnail = this._createThumbnail(newImagePath); + // Enforce fixed size for new thumbnail + applyCssToWidget( + newThumbnail, + 'picture { min-height: 120px; }' + ); - // Replace old thumbnail with new one - const child = this._contentBox.get_first_child(); - if (child) { - this._contentBox.remove(child); - } - this._contentBox.prepend(newThumbnail); + // Replace old thumbnail with new one + const child = this._contentBox.get_first_child(); + if (child) { + this._contentBox.remove(child); } - ); - buttonBox.append(nextImageButton); - } - - // Next Background button - only for current theme with multiple backgrounds - if (isCurrentTheme && this._theme.hasMultipleBackgrounds) { - const nextBgButton = this._createButton( - 'Next BG', - false, - () => { - // Cycle to next background image in thumbnail - this._currentImageIndex = - (this._currentImageIndex + 1) % - this._availableImages.length; - this.updateThumbnail(this._currentImageIndex); + this._contentBox.prepend(newThumbnail); + // For active theme, also cycle the system background + if (isCurrentTheme) { try { GLib.spawn_command_line_async( '/usr/share/ohmydebn/bin/ohmydebn-theme-bg-next' @@ -473,9 +476,31 @@ export const OmarchyThemeCard = GObject.registerClass( ); } } - ); - buttonBox.append(nextBgButton); + } + ); + // Disable button if only one image + nextImageButton.set_sensitive(this._hasMultipleImages); + buttonBox.append(nextImageButton); + + // Apply button + const applyButton = this._createButton( + isCurrentTheme ? 'Current' : 'Apply', + true, + () => this.emit('theme-apply', this._theme) + ); + applyButton.set_sensitive(!isCurrentTheme); + buttonBox.append(applyButton); + + // Edit button - disable if theme has no colors.toml + const hasColors = this._theme.hasColors !== false; + const editButton = this._createButton('Edit', false, () => { + this.emit('theme-import', this._theme); + }); + editButton.set_sensitive(hasColors); + if (!hasColors) { + applyCssToWidget(editButton, 'button { opacity: 0.5; }'); } + buttonBox.append(editButton); return buttonBox; } diff --git a/src/services/omarchy-theme-service.js b/src/services/omarchy-theme-service.js index d8beb9e..56c56d7 100644 --- a/src/services/omarchy-theme-service.js +++ b/src/services/omarchy-theme-service.js @@ -361,6 +361,49 @@ export class OmarchyThemeService { // Get metadata const metadata = this._getThemeMetadata(resolvedPath, themeName); + // For the current theme, get wallpapers from ohmydebn current directory (if available) + // This ensures wallpaper paths match the ohmydebn symlink + let wallpaperList = metadata.wallpapers; + if (themeName === this._currentThemeName) { + const ohmydebnCurrentThemeDir = GLib.build_filenamev([ + GLib.get_home_dir(), + '.config', + 'ohmydebn', + 'current', + 'theme', + ]); + const ohmydebnBackgroundsDir = GLib.build_filenamev([ + ohmydebnCurrentThemeDir, + 'backgrounds', + ]); + if (fileExists(ohmydebnBackgroundsDir)) { + try { + const ohmydebnWallpapers = []; + enumerateDirectory( + ohmydebnBackgroundsDir, + (fileInfo, filePath, fileName) => { + const contentType = fileInfo.get_content_type(); + if ( + contentType && + contentType.startsWith('image/') + ) { + ohmydebnWallpapers.push(filePath); + } + }, + 'standard::name,standard::content-type' + ); + if (ohmydebnWallpapers.length > 0) { + wallpaperList = ohmydebnWallpapers; + } + } catch (e) { + console.warn( + `Failed to read ohmydebn current theme backgrounds:`, + e.message + ); + } + } + } + return { name: themeName, path: themePath, @@ -370,7 +413,7 @@ export class OmarchyThemeService { extendedColors: colorResult.extendedColors || {}, description: metadata.description, previewImage: metadata.previewImage, - wallpapers: metadata.wallpapers, + wallpapers: wallpaperList, isSymlink, symlinkTarget, isCurrentTheme: themeName === this._currentThemeName,