diff --git a/lib/shared/theme.js b/lib/shared/theme.js index ae14422e..79ea115d 100644 --- a/lib/shared/theme.js +++ b/lib/shared/theme.js @@ -100,13 +100,10 @@ export class ThemeManagerBase extends GObject.Object { * @param {any} selector */ getCssRule(selector) { - if (this.cssAst) { - const rules = this.cssAst.stylesheet.rules; - // return only the first match, Forge CSS authors should make sure class names are unique :) - const matchRules = rules.filter((r) => r.selectors.filter((s) => s === selector).length > 0); - return matchRules.length > 0 ? matchRules[0] : {}; - } - return {}; + const rules = this.cssAst?.stylesheet?.rules ?? []; + // return only the first match, Forge CSS authors should make sure class names are unique :) + // (comments and @-rules have no selectors) + return rules.find((r) => r.selectors?.includes(selector)) ?? null; } /** @@ -115,13 +112,7 @@ export class ThemeManagerBase extends GObject.Object { */ getCssProperty(selector, propertyName) { const cssRule = this.getCssRule(selector); - - if (cssRule) { - const matchDeclarations = cssRule.declarations.filter((d) => d.property === propertyName); - return matchDeclarations.length > 0 ? matchDeclarations[0] : {}; - } - - return {}; + return cssRule?.declarations?.find((d) => d.property === propertyName) ?? {}; } /** @@ -143,16 +134,86 @@ export class ThemeManagerBase extends GObject.Object { * Returns the AST for stylesheet.css */ _importCss() { + const defaultFile = this.configMgr.defaultStylesheetFile; let cssFile = this.configMgr.stylesheetFile; if (!cssFile || !production) { - cssFile = this.configMgr.defaultStylesheetFile; + cssFile = defaultFile; } - let [success, contents] = cssFile.load_contents(null); - if (success) { - const cssContents = new TextDecoder().decode(contents); - this.cssAst = parse(cssContents); + this.cssAst = cssFile ? this._parseCssFile(cssFile) : null; + if (!defaultFile || cssFile?.equal(defaultFile)) return; + + // The user's stylesheet can be empty, from an older Forge or edited by hand: take the rules + // and properties Forge reads that it lacks from the default stylesheet, so that reading them + // doesn't fail and the shell styles everything (#448). If it can't be parsed at all, use the + // default stylesheet. Either way the stylesheet is saved completed, and the user's own file + // is kept as stylesheet.css.bak. + const defaultAst = this._parseCssFile(defaultFile); + if (!defaultAst) return; + let completed = false; + if (!this.cssAst) { + this.cssAst = defaultAst; + completed = true; + } else { + const rules = this.cssAst.stylesheet.rules; + for (const defaultRule of defaultAst.stylesheet.rules) { + if (!defaultRule.selectors) continue; + const rule = this.getCssRule(defaultRule.selectors[0]); + if (!rule) { + rules.push(defaultRule); + completed = true; + continue; + } + rule.declarations ??= []; + for (const declaration of defaultRule.declarations ?? []) { + if (declaration.type !== "declaration") continue; + if (!rule.declarations.some((d) => d.property === declaration.property)) { + rule.declarations.push(declaration); + completed = true; + } + } + } + } + if (completed) this._saveCompletedCss(cssFile); + } + + /** + * Writes the completed AST to the user's stylesheet, after copying the file to + * stylesheet.css.bak. + * @param {Gio.File} cssFile + */ + _saveCompletedCss(cssFile) { + try { + const backup = Gio.File.new_for_path(`${cssFile.get_path()}.bak`); + const flags = Gio.FileCopyFlags.OVERWRITE | Gio.FileCopyFlags.TARGET_DEFAULT_PERMS; + cssFile.copy(backup, flags, null, null); + cssFile.replace_contents( + stringify(this.cssAst), + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null + ); + console.warn( + `Forge: completed the stylesheet ${cssFile.get_path()} (the previous one is ${backup.get_path()})` + ); + } catch (e) { + console.warn(`Forge: can't save the stylesheet ${cssFile.get_path()}: ${e.message}`); + } + } + + /** + * Parses a stylesheet file, or returns null (with a warning) if it can't be read or parsed. + * @param {Gio.File} cssFile + */ + _parseCssFile(cssFile) { + try { + const [success, contents] = cssFile.load_contents(null); + if (success) return parse(new TextDecoder().decode(contents)); + } catch (e) { + console.warn(`Forge: can't use the stylesheet ${cssFile.get_path()}: ${e.message}`); } + return null; } /**