From 7c6ae13150b01e1f8f7a525cbbfd32539549ddf0 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 00:25:16 +0000 Subject: [PATCH 01/17] Initial multi-monitor support --- ts/bridge/bridge.ts | 6 +- ts/browser.ts | 188 +++++++++++++++++++++++++------------------- ts/globals.ts | 50 ++++++------ 3 files changed, 140 insertions(+), 104 deletions(-) diff --git a/ts/bridge/bridge.ts b/ts/bridge/bridge.ts index ece5a77..0407545 100644 --- a/ts/bridge/bridge.ts +++ b/ts/bridge/bridge.ts @@ -53,7 +53,7 @@ export class Greeter { } catch (err) { logger.error(err); browser.whenReady().then(() => { - dialog.showMessageBoxSync(browser.win, { + dialog.showMessageBoxSync(browser.primary_window, { message: "Detected a problem that could interfere with the system login process", // Yeah, that problematic message detail: `LightDM: ${err}\nYou can continue without major problems, but you won't be able to log in`, @@ -103,7 +103,9 @@ export class Greeter { _emit_signal(signal: string, ...args: unknown[]): void { //console.log("SIGNAL EMITTED", signal, args) - browser.win.webContents.send("LightDMSignal", signal, ...args); + for (const win of browser.windows) { + win.window.webContents.send("LightDMSignal", signal, ...args); + } } /** diff --git a/ts/browser.ts b/ts/browser.ts index 367be27..3649b09 100644 --- a/ts/browser.ts +++ b/ts/browser.ts @@ -17,6 +17,11 @@ import { Brightness } from "./utils/brightness"; import { logger } from "./logger"; import { set_screensaver, reset_screensaver } from "./utils/screensaver"; +interface NodyWindow { + is_primary: boolean; + display: Electron.Display; + window: BrowserWindow; +} class Browser { ready = false; @@ -27,8 +32,7 @@ class Browser { }); } - // @ts-ignore - win: BrowserWindow; + windows: NodyWindow[]; whenReady(): Promise { return new Promise((resolve) => { @@ -43,7 +47,7 @@ class Browser { init(): void { this.set_protocol(); - this.win = this.create_window(); + this.windows = this.create_windows(); this.load_theme(); this.init_listeners(); } @@ -105,41 +109,50 @@ class Browser { protocol: "web-greeter:", }); //console.log({ theme_url, url: new URL(theme_url) }); - this.win.loadURL(`${theme_url}`); - this.win.setBackgroundColor("#000000"); - - this.win.webContents.on("before-input-event", (_event, input) => { - const value = nody_greeter.config.features.backlight.value; - if (input.type == "keyUp") return; - if (input.code == "BrightnessDown") { - Brightness.dec_brightness(value); - } else if (input.code == "BrightnessUp") { - Brightness.inc_brightness(value); - } - }); + for (const w of this.windows) { + w.window.loadURL(`${theme_url}`); + w.window.setBackgroundColor("#000000"); + + w.window.webContents.on("before-input-event", (_event, input) => { + const value = nody_greeter.config.features.backlight.value; + if (input.type == "keyUp") return; + if (input.code == "BrightnessDown") { + Brightness.dec_brightness(value); + } else if (input.code == "BrightnessUp") { + Brightness.inc_brightness(value); + } + }); + } logger.debug("Theme loaded"); } - create_window(): BrowserWindow { + create_windows(): NodyWindow[] { logger.debug("Initializing Browser Window"); - const screen_size = screen.getPrimaryDisplay().workAreaSize; - - const win = new BrowserWindow({ - height: screen_size.height, - width: screen_size.width, - backgroundColor: "#000000", - frame: nody_greeter.app.frame, - show: false, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - nodeIntegration: false, - contextIsolation: false, - allowRunningInsecureContent: !nody_greeter.config.greeter.secure_mode, // Should set option - devTools: nody_greeter.app.debug_mode, // Should set option - }, - }); + const displays = screen.getAllDisplays(); + const primaryDisplay = screen.getPrimaryDisplay(); + + const windows: NodyWindow[] = displays.map((display) => ({ + is_primary: display.id === primaryDisplay.id, + display, + window: new BrowserWindow({ + height: display.workAreaSize.height, + width: display.workAreaSize.width, + x: display.bounds.x, + y: display.bounds.y, + backgroundColor: "#000000", + frame: nody_greeter.app.frame, + show: false, + webPreferences: { + preload: path.join(__dirname, "preload.js"), + nodeIntegration: false, + contextIsolation: false, + allowRunningInsecureContent: !nody_greeter.config.greeter.secure_mode, // Should set option + devTools: nody_greeter.app.debug_mode, // Should set option + }, + }), + })); logger.debug("Browser Window created"); @@ -157,61 +170,69 @@ class Browser { this.ready = true; - return win; + return windows; } init_listeners(): void { - this.win.once("ready-to-show", () => { - this.win.setFullScreen(nody_greeter.app.fullscreen); - this.win.show(); - this.win.focus(); - logger.debug("Nody Greeter started"); - }); - this.win.webContents.on("devtools-opened", () => { - this.win.webContents.devToolsWebContents.focus(); - }); + for (const w of this.windows) { + w.window.once("ready-to-show", () => { + w.window.setFullScreen(nody_greeter.app.fullscreen); + w.window.show(); + if (w.is_primary) { + w.window.focus(); + } + logger.debug("Nody Greeter started win"); + }); + w.window.webContents.on("devtools-opened", () => { + w.window.webContents.devToolsWebContents.focus(); + }); + + w.window.webContents.on("context-menu", (_ev, params) => { + if (!nody_greeter.app.debug_mode) return; + const position = { x: params.x, y: params.y }; + const menu_template: MenuItemConstructorOptions[] = [ + { role: "undo", enabled: params.editFlags.canUndo, accelerator: "U" }, + { role: "redo", enabled: params.editFlags.canRedo, accelerator: "R" }, + { type: "separator" }, + { role: "cut", enabled: params.editFlags.canCut, accelerator: "C" }, + { role: "copy", enabled: params.editFlags.canCopy, accelerator: "C" }, + { + role: "paste", + enabled: params.editFlags.canPaste, + accelerator: "P", + }, + { + role: "delete", + enabled: params.editFlags.canDelete, + accelerator: "D", + }, + { + role: "selectAll", + enabled: params.editFlags.canSelectAll, + accelerator: "S", + registerAccelerator: true, + }, + { type: "separator" }, + { role: "reload", accelerator: "R", registerAccelerator: false }, + { role: "forceReload", accelerator: "F", registerAccelerator: false }, + { role: "toggleDevTools", accelerator: "T" }, + { + label: "Inspect Element", + click: (): void => { + w.window.webContents.inspectElement(position.x, position.y); + }, + accelerator: "I", + }, + ]; + const menu = Menu.buildFromTemplate(menu_template); + menu.popup(); + }); + } app.on("quit", () => { reset_screensaver(); }); - this.win.webContents.on("context-menu", (_ev, params) => { - if (!nody_greeter.app.debug_mode) return; - const position = { x: params.x, y: params.y }; - const menu_template: MenuItemConstructorOptions[] = [ - { role: "undo", enabled: params.editFlags.canUndo, accelerator: "U" }, - { role: "redo", enabled: params.editFlags.canRedo, accelerator: "R" }, - { type: "separator" }, - { role: "cut", enabled: params.editFlags.canCut, accelerator: "C" }, - { role: "copy", enabled: params.editFlags.canCopy, accelerator: "C" }, - { role: "paste", enabled: params.editFlags.canPaste, accelerator: "P" }, - { - role: "delete", - enabled: params.editFlags.canDelete, - accelerator: "D", - }, - { - role: "selectAll", - enabled: params.editFlags.canSelectAll, - accelerator: "S", - registerAccelerator: true, - }, - { type: "separator" }, - { role: "reload", accelerator: "R", registerAccelerator: false }, - { role: "forceReload", accelerator: "F", registerAccelerator: false }, - { role: "toggleDevTools", accelerator: "T" }, - { - label: "Inspect Element", - click: (): void => { - this.win.webContents.inspectElement(position.x, position.y); - }, - accelerator: "I", - }, - ]; - const menu = Menu.buildFromTemplate(menu_template); - menu.popup(); - }); - session.defaultSession.webRequest.onBeforeRequest((details, callback) => { const url = new URL(details.url); //console.log({ origin: details.url, url }); @@ -224,6 +245,15 @@ class Browser { callback({ cancel: block }); }); } + + public get primary_window(): BrowserWindow { + for (const w of this.windows) { + if (w.is_primary) { + return w.window; + } + } + throw new Error("No primary window initialized"); + } } export { Browser }; diff --git a/ts/globals.ts b/ts/globals.ts index 27dcd28..3216ad0 100644 --- a/ts/globals.ts +++ b/ts/globals.ts @@ -12,28 +12,30 @@ browser.whenReady().then(() => { function initLogger(): void { logger.debug("Javascript logger is ready"); - browser.win.webContents.addListener( - "console-message", - (_ev, code, message, line, sourceID) => { - sourceID = sourceID == "" ? "console" : sourceID; - if (code == 3) { - logger.log({ - level: "error", - message: message, - line: line, - source: sourceID, - }); - error_prompt(message, sourceID, line); - } else if (code == 2) { - logger.log({ - level: "warn", - message: message, - line: line, - source: sourceID, - }); + for (const win of browser.windows) { + win.window.webContents.addListener( + "console-message", + (ev, code, message, line, sourceID) => { + sourceID = sourceID == "" ? "console" : sourceID; + if (code == 3) { + logger.log({ + level: "error", + message: message, + line: line, + source: sourceID, + }); + error_prompt(message, sourceID, line); + } else if (code == 2) { + logger.log({ + level: "warn", + message: message, + line: line, + source: sourceID, + }); + } } - } - ); + ); + } } /** @@ -44,7 +46,7 @@ function initLogger(): void { */ function error_prompt(message: string, source: string, line: number): void { if (!nody_greeter.config.greeter.detect_theme_errors) return; - const ind = dialog.showMessageBoxSync(browser.win, { + const ind = dialog.showMessageBoxSync(browser.primary_window, { message: "An error ocurred. Do you want to change to default theme? (gruvbox)", detail: `${source} ${line}: ${message}`, @@ -60,7 +62,9 @@ function error_prompt(message: string, source: string, line: number): void { browser.load_theme(); break; case 2: // Reload theme - browser.win.reload(); + for (const win of browser.windows) { + win.window.reload(); + } break; default: break; From 368b100b2f9c84c16d1f077d73761c176092f212 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 09:25:54 +0000 Subject: [PATCH 02/17] Display error messages in the relevant window When a window produces an error message, display the error as a message for the same window. --- ts/globals.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ts/globals.ts b/ts/globals.ts index 3216ad0..a3fb82a 100644 --- a/ts/globals.ts +++ b/ts/globals.ts @@ -1,4 +1,4 @@ -import { dialog } from "electron"; +import { BrowserWindow, dialog } from "electron"; import { Browser } from "./browser"; import { logger } from "./logger"; import { nody_greeter } from "./config"; @@ -24,7 +24,7 @@ function initLogger(): void { line: line, source: sourceID, }); - error_prompt(message, sourceID, line); + error_prompt(win.window, message, sourceID, line); } else if (code == 2) { logger.log({ level: "warn", @@ -40,13 +40,19 @@ function initLogger(): void { /** * Prompts to change to default theme (gruvbox) on error + * @param {BrowserWindow} win The browser window originating the message * @param {string} message Message or error to show * @param {string} source Source of error * @param {number} line Line number where error was detected */ -function error_prompt(message: string, source: string, line: number): void { +function error_prompt( + win: BrowserWindow, + message: string, + source: string, + line: number +): void { if (!nody_greeter.config.greeter.detect_theme_errors) return; - const ind = dialog.showMessageBoxSync(browser.primary_window, { + const ind = dialog.showMessageBoxSync(win, { message: "An error ocurred. Do you want to change to default theme? (gruvbox)", detail: `${source} ${line}: ${message}`, From 707db4817dca7c2702d84cc0a76626add2bf7646 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 10:41:06 +0000 Subject: [PATCH 03/17] Allow overriding config and theme paths To aid in easier development workflow, the theme directory and config path can be overwritten with environment variables to avoid having to run `node make install` after changes to themes to test them, and to allow for a separate configuration file for development and testing, so that nody-greeter can continue to be used as the primary greeter on a system with sensible & safe config --- ts/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ts/config.ts b/ts/config.ts index 4931399..e8bed24 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -70,11 +70,13 @@ export const nody_greeter: nody_config = { fullscreen: true, frame: false, debug_mode: false, - theme_dir: "/usr/share/web-greeter/themes/", + theme_dir: + process.env.NODY_GREETER_THEME_DIR || "/usr/share/web-greeter/themes/", }, }; -const path_to_config = "/etc/lightdm/web-greeter.yml"; +const path_to_config = + process.env.NODY_GREETER_CONFIG || "/etc/lightdm/web-greeter.yml"; export function load_config(): void { try { From cbc0ca933525a1f9c3d1a237b82040fe88df1e4a Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 10:44:51 +0000 Subject: [PATCH 04/17] Add --no-install to npx command Using --no-install ensures that the local node_modules version of typescript is used, and that an error is thrown if it's not installed for some reason --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf7630d..21262ce 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ git clone https://github.com/JezerM/nody-greeter.git cd nody-greeter npm install npm run rebuild -npx tsc --build +npx --no-install tsc --build node make build sudo node make install ``` From cbd896f4ad4eade104145f0e8d0268ba939fd54f Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 11:22:32 +0000 Subject: [PATCH 05/17] Introduce constants file for channel strings --- ts/bridge/bridge.ts | 7 ++++++- ts/consts.ts | 10 ++++++++++ ts/preload.ts | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 ts/consts.ts diff --git a/ts/bridge/bridge.ts b/ts/bridge/bridge.ts index 0407545..f689023 100644 --- a/ts/bridge/bridge.ts +++ b/ts/bridge/bridge.ts @@ -30,6 +30,7 @@ import { LightDMUser, } from "../ldm_interfaces"; import { logger } from "../logger"; +import { CONSTS } from "../consts"; export class Greeter { _config: web_greeter_config; @@ -104,7 +105,11 @@ export class Greeter { _emit_signal(signal: string, ...args: unknown[]): void { //console.log("SIGNAL EMITTED", signal, args) for (const win of browser.windows) { - win.window.webContents.send("LightDMSignal", signal, ...args); + win.window.webContents.send( + CONSTS.channel.lightdm_signal, + signal, + ...args + ); } } diff --git a/ts/consts.ts b/ts/consts.ts new file mode 100644 index 0000000..b6e17b7 --- /dev/null +++ b/ts/consts.ts @@ -0,0 +1,10 @@ +/** + * Constant values shared across the application + * + * (used by both backend (node) Node frontend (browser-window) code) + */ +export const CONSTS = { + channel: { + lightdm_signal: "LightDMSignal", + }, +} as const; diff --git a/ts/preload.ts b/ts/preload.ts index 81f8c65..44d41a9 100644 --- a/ts/preload.ts +++ b/ts/preload.ts @@ -1,4 +1,5 @@ import { ipcRenderer } from "electron"; +import { CONSTS } from "./consts"; import { LightDMBattery, LightDMLanguage, @@ -58,7 +59,7 @@ export class PromptSignal extends Signal { } } -ipcRenderer.on("LightDMSignal", (_ev, signal, ...args) => { +ipcRenderer.on(CONSTS.channel.lightdm_signal, (_ev, signal, ...args) => { allSignals.forEach((v) => { if (v._name == signal) { //console.log(args) From 954c2e16607ed0817f0bdd28e01ca23279987c61 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 11:56:54 +0000 Subject: [PATCH 06/17] Introduce WindowMetadata and make available to themes Given that this functionality is unique to nody-greeter, a new class / namespace has been created under `nody_greeter` that includes the ability to get this information, to allow for the existing interfaces to remain backwards-compatible with web-greeter --- ts/browser.ts | 37 ++++++++++++++++++++ ts/consts.ts | 1 + ts/preload.ts | 94 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/ts/browser.ts b/ts/browser.ts index 3649b09..95992f2 100644 --- a/ts/browser.ts +++ b/ts/browser.ts @@ -16,6 +16,8 @@ import * as url from "url"; import { Brightness } from "./utils/brightness"; import { logger } from "./logger"; import { set_screensaver, reset_screensaver } from "./utils/screensaver"; +import { WindowMetadata } from "./preload"; +import { CONSTS } from "./consts"; interface NodyWindow { is_primary: boolean; @@ -174,6 +176,27 @@ class Browser { } init_listeners(): void { + // Calculate the total display area + const overallBoundary: WindowMetadata["overallBoundary"] = { + minX: Infinity, + maxX: -Infinity, + minY: Infinity, + maxY: -Infinity, + }; + + for (const w of this.windows) { + overallBoundary.minX = Math.min(overallBoundary.minX, w.display.bounds.x); + overallBoundary.minY = Math.min(overallBoundary.minY, w.display.bounds.y); + overallBoundary.maxX = Math.max( + overallBoundary.maxX, + w.display.bounds.x + w.display.bounds.width + ); + overallBoundary.maxY = Math.max( + overallBoundary.maxY, + w.display.bounds.y + w.display.bounds.height + ); + } + for (const w of this.windows) { w.window.once("ready-to-show", () => { w.window.setFullScreen(nody_greeter.app.fullscreen); @@ -181,6 +204,20 @@ class Browser { if (w.is_primary) { w.window.focus(); } + const metadata: WindowMetadata = { + id: w.display.id, + is_primary: w.is_primary, + size: { + width: w.display.workAreaSize.width, + height: w.display.workAreaSize.height, + }, + position: { + x: w.display.bounds.x, + y: w.display.bounds.y, + }, + overallBoundary, + }; + w.window.webContents.send(CONSTS.channel.window_metadata, metadata); logger.debug("Nody Greeter started win"); }); w.window.webContents.on("devtools-opened", () => { diff --git a/ts/consts.ts b/ts/consts.ts index b6e17b7..a3304f1 100644 --- a/ts/consts.ts +++ b/ts/consts.ts @@ -6,5 +6,6 @@ export const CONSTS = { channel: { lightdm_signal: "LightDMSignal", + window_metadata: "WindowMetadata", }, } as const; diff --git a/ts/preload.ts b/ts/preload.ts index 44d41a9..c3e45de 100644 --- a/ts/preload.ts +++ b/ts/preload.ts @@ -8,6 +8,76 @@ import { LightDMUser, } from "./ldm_interfaces"; +/** + * Metadata that is sent to each window to handle more interesting multi-monitor + * functionality / themes. + */ +export interface WindowMetadata { + id: number; + is_primary: boolean; + position: { + x: number; + y: number; + }; + size: { + width: number; + height: number; + }; + /** + * The total real-estate across all screens, + * this can be used to assist in, for example, + * correctly positioning multi-monitor backgrounds. + */ + overallBoundary: { + minX: number; + maxX: number; + minY: number; + maxY: number; + }; +} + +/** + * A class that exposes functionality that is unique to `nody-greeter` and not + * present in `web-greeter` + */ +export class Nody { + private _window_metadata: WindowMetadata | null = null; + /** + * callback that should be called when the metadata is received + */ + private _ready: (() => void) | null = null; + private readonly _ready_promise: Promise; + + constructor() { + if ("nody_greeter" in globalThis) { + return globalThis.nody_greeter; + } + + globalThis.nody_greeter = this; + + ipcRenderer.on(CONSTS.channel.window_metadata, (_ev, metadata) => { + this._window_metadata = metadata; + this._ready(); + }); + + this._ready_promise = new Promise((resolve) => (this._ready = resolve)); + + return globalThis.nody_greeter; + } + + public get window_metadata(): WindowMetadata { + if (this._window_metadata) { + return this._window_metadata; + } + throw new Error( + `window_metadata not available, did you wait for the GreeterReady event?` + ); + } + + /** Resolves when we have received WindowMetadata */ + public whenReady = (): Promise => this._ready_promise; +} + const allSignals = []; export class Signal { @@ -800,18 +870,33 @@ export class ThemeUtils { } } +new Nody(); new ThemeUtils(); new GreeterConfig(); new Greeter(); window._ready_event = new Event("GreeterReady"); -window.addEventListener("DOMContentLoaded", () => { - setTimeout(() => { - window.dispatchEvent(globalThis._ready_event); - }, 2); +const domLoaded = new Promise((resolve) => { + window.addEventListener("DOMContentLoaded", () => { + setTimeout(() => { + resolve(); + }, 2); + }); }); +/** + * Promise that fires when all initialization has completed, + * and the theme can start (i.e. _ready_event can be sent) + */ +const readyPromise = Promise.all([ + domLoaded, + globalThis.nody_greeter.whenReady(), +]); + +readyPromise.then(() => window.dispatchEvent(globalThis._ready_event)); + +export declare const nody_greeter: Nody; export declare const lightdm: Greeter; export declare const greeter_config: GreeterConfig; export declare const theme_utils: ThemeUtils; @@ -819,6 +904,7 @@ export declare const _ready_event: Event; declare global { interface Window { + nody_greeter: Nody | undefined; lightdm: Greeter | undefined; greeter_config: GreeterConfig | undefined; theme_utils: ThemeUtils | undefined; From 16f4bc2e0cde13a15d86d50bd1753668f0b21283 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Fri, 14 Jan 2022 12:03:53 +0000 Subject: [PATCH 07/17] Only display login screen for primary monitor in themes All other monitors will continue to display the wallpaper for the theme. --- themes/dracula/js/index.js | 5 +++++ themes/gruvbox/js/index.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/themes/dracula/js/index.js b/themes/dracula/js/index.js index 7544e32..1476f9b 100644 --- a/themes/dracula/js/index.js +++ b/themes/dracula/js/index.js @@ -46,6 +46,11 @@ async function initGreeter() { battery = new Battery(); brightness = new Brightness(); + + if (!nody_greeter.window_metadata.is_primary) { + // Hide login elements on non-primary screen + document.querySelector("#screen").classList.add("hide"); + } } if (window._ready_event === undefined) { diff --git a/themes/gruvbox/js/index.js b/themes/gruvbox/js/index.js index 6d1b56a..d93de70 100644 --- a/themes/gruvbox/js/index.js +++ b/themes/gruvbox/js/index.js @@ -48,6 +48,11 @@ async function initGreeter() { if (lock) { document.querySelector("#lock-label").classList.remove("hide"); } + + if (!nody_greeter.window_metadata.is_primary) { + // Hide login elements on non-primary screen + document.querySelector("#screen").classList.add("hide"); + } } const notGreeter = false; From 2e5382b713f918ac9f093773a205f69cecb9594c Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Sun, 16 Jan 2022 00:15:15 +0000 Subject: [PATCH 08/17] Send metadata after request from BrowserWindow metadata would only be sent once, and not after page refreshes or after reloading the window. To address this, the metadata is caculated once and stored in the windows array, and is requested by the preload script for every window load. --- ts/bridge/bridge.ts | 15 ++++++ ts/browser.ts | 113 +++++++++++++++++++++++--------------------- ts/preload.ts | 3 ++ 3 files changed, 76 insertions(+), 55 deletions(-) diff --git a/ts/bridge/bridge.ts b/ts/bridge/bridge.ts index f689023..f205256 100644 --- a/ts/bridge/bridge.ts +++ b/ts/bridge/bridge.ts @@ -734,6 +734,21 @@ ipcMain.on("lightdm", (ev, ...args) => { handler(globalThis.lightdm, ev, ...args); }); +ipcMain.on(CONSTS.channel.window_metadata, (ev) => { + /** + * A request on this channel simply means that a browser window is ready to + * receive metadata (i.e. on initial load or a refresh) + */ + for (const window of browser.windows) { + if (window.window.webContents === ev.sender) { + window.window.webContents.send( + CONSTS.channel.window_metadata, + window.meta + ); + } + } +}); + browser.whenReady().then(() => { new Greeter(nody_greeter.config); new GreeterConfig(nody_greeter.config); diff --git a/ts/browser.ts b/ts/browser.ts index 95992f2..caf32a0 100644 --- a/ts/browser.ts +++ b/ts/browser.ts @@ -17,12 +17,12 @@ import { Brightness } from "./utils/brightness"; import { logger } from "./logger"; import { set_screensaver, reset_screensaver } from "./utils/screensaver"; import { WindowMetadata } from "./preload"; -import { CONSTS } from "./consts"; interface NodyWindow { is_primary: boolean; display: Electron.Display; window: BrowserWindow; + meta: WindowMetadata; } class Browser { ready = false; @@ -135,26 +135,64 @@ class Browser { const displays = screen.getAllDisplays(); const primaryDisplay = screen.getPrimaryDisplay(); - const windows: NodyWindow[] = displays.map((display) => ({ - is_primary: display.id === primaryDisplay.id, - display, - window: new BrowserWindow({ - height: display.workAreaSize.height, - width: display.workAreaSize.width, - x: display.bounds.x, - y: display.bounds.y, - backgroundColor: "#000000", - frame: nody_greeter.app.frame, - show: false, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - nodeIntegration: false, - contextIsolation: false, - allowRunningInsecureContent: !nody_greeter.config.greeter.secure_mode, // Should set option - devTools: nody_greeter.app.debug_mode, // Should set option + // Calculate the total display area + const overallBoundary: WindowMetadata["overallBoundary"] = { + minX: Infinity, + maxX: -Infinity, + minY: Infinity, + maxY: -Infinity, + }; + + for (const display of displays) { + overallBoundary.minX = Math.min(overallBoundary.minX, display.bounds.x); + overallBoundary.minY = Math.min(overallBoundary.minY, display.bounds.y); + overallBoundary.maxX = Math.max( + overallBoundary.maxX, + display.bounds.x + display.bounds.width + ); + overallBoundary.maxY = Math.max( + overallBoundary.maxY, + display.bounds.y + display.bounds.height + ); + } + + const windows: NodyWindow[] = displays.map((display) => { + const is_primary = display.id === primaryDisplay.id; + return { + is_primary, + display, + window: new BrowserWindow({ + height: display.workAreaSize.height, + width: display.workAreaSize.width, + x: display.bounds.x, + y: display.bounds.y, + backgroundColor: "#000000", + frame: nody_greeter.app.frame, + show: false, + webPreferences: { + preload: path.join(__dirname, "preload.js"), + nodeIntegration: false, + contextIsolation: false, + allowRunningInsecureContent: + !nody_greeter.config.greeter.secure_mode, // Should set option + devTools: nody_greeter.app.debug_mode, // Should set option + }, + }), + meta: { + id: display.id, + is_primary, + size: { + width: display.workAreaSize.width, + height: display.workAreaSize.height, + }, + position: { + x: display.bounds.x, + y: display.bounds.y, + }, + overallBoundary, }, - }), - })); + }; + }); logger.debug("Browser Window created"); @@ -176,27 +214,6 @@ class Browser { } init_listeners(): void { - // Calculate the total display area - const overallBoundary: WindowMetadata["overallBoundary"] = { - minX: Infinity, - maxX: -Infinity, - minY: Infinity, - maxY: -Infinity, - }; - - for (const w of this.windows) { - overallBoundary.minX = Math.min(overallBoundary.minX, w.display.bounds.x); - overallBoundary.minY = Math.min(overallBoundary.minY, w.display.bounds.y); - overallBoundary.maxX = Math.max( - overallBoundary.maxX, - w.display.bounds.x + w.display.bounds.width - ); - overallBoundary.maxY = Math.max( - overallBoundary.maxY, - w.display.bounds.y + w.display.bounds.height - ); - } - for (const w of this.windows) { w.window.once("ready-to-show", () => { w.window.setFullScreen(nody_greeter.app.fullscreen); @@ -204,20 +221,6 @@ class Browser { if (w.is_primary) { w.window.focus(); } - const metadata: WindowMetadata = { - id: w.display.id, - is_primary: w.is_primary, - size: { - width: w.display.workAreaSize.width, - height: w.display.workAreaSize.height, - }, - position: { - x: w.display.bounds.x, - y: w.display.bounds.y, - }, - overallBoundary, - }; - w.window.webContents.send(CONSTS.channel.window_metadata, metadata); logger.debug("Nody Greeter started win"); }); w.window.webContents.on("devtools-opened", () => { diff --git a/ts/preload.ts b/ts/preload.ts index c3e45de..624ca69 100644 --- a/ts/preload.ts +++ b/ts/preload.ts @@ -60,6 +60,9 @@ export class Nody { this._ready(); }); + // Send initial request for metadata + ipcRenderer.send(CONSTS.channel.window_metadata); + this._ready_promise = new Promise((resolve) => (this._ready = resolve)); return globalThis.nody_greeter; From 31067910b3bdb3aeb8caed509886da80231ac189 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Sun, 16 Jan 2022 00:35:49 +0000 Subject: [PATCH 09/17] Introduce cross-window communication mechanism --- ts/bridge/bridge.ts | 16 ++++++++++++++++ ts/consts.ts | 1 + ts/preload.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/ts/bridge/bridge.ts b/ts/bridge/bridge.ts index f205256..a2b740c 100644 --- a/ts/bridge/bridge.ts +++ b/ts/bridge/bridge.ts @@ -749,6 +749,22 @@ ipcMain.on(CONSTS.channel.window_metadata, (ev) => { } }); +ipcMain.on(CONSTS.channel.window_broadcast, (ev, data: unknown) => { + const sendingWindow = browser.windows.find( + (w) => w.window.webContents === ev.sender + ); + if (!sendingWindow) { + throw new Error(`Unable to find window for event ${ev}`); + } + for (const window of browser.windows) { + window.window.webContents.send( + CONSTS.channel.window_broadcast, + sendingWindow.meta, + data + ); + } +}); + browser.whenReady().then(() => { new Greeter(nody_greeter.config); new GreeterConfig(nody_greeter.config); diff --git a/ts/consts.ts b/ts/consts.ts index a3304f1..d8c6982 100644 --- a/ts/consts.ts +++ b/ts/consts.ts @@ -7,5 +7,6 @@ export const CONSTS = { channel: { lightdm_signal: "LightDMSignal", window_metadata: "WindowMetadata", + window_broadcast: "WindowBroadcast", }, } as const; diff --git a/ts/preload.ts b/ts/preload.ts index 624ca69..ddbc483 100644 --- a/ts/preload.ts +++ b/ts/preload.ts @@ -36,6 +36,21 @@ export interface WindowMetadata { }; } +/** + * An event that is fired and dispatched when one browser window of a theme + * sends a broadcast to all windows (which happens for multi-monitor setups) + */ +export class NodyBroadcastEvent extends Event { + constructor( + /** Metadata for the window that originated the request */ + public readonly window: WindowMetadata, + /** Data sent in the broadcast */ + public readonly data: unknown + ) { + super("NodyBroadcastEvent"); + } +} + /** * A class that exposes functionality that is unique to `nody-greeter` and not * present in `web-greeter` @@ -63,6 +78,11 @@ export class Nody { // Send initial request for metadata ipcRenderer.send(CONSTS.channel.window_metadata); + ipcRenderer.on(CONSTS.channel.window_broadcast, (_ev, metadata, data) => { + const event = new NodyBroadcastEvent(metadata, data); + window.dispatchEvent(event); + }); + this._ready_promise = new Promise((resolve) => (this._ready = resolve)); return globalThis.nody_greeter; @@ -79,6 +99,15 @@ export class Nody { /** Resolves when we have received WindowMetadata */ public whenReady = (): Promise => this._ready_promise; + + /** + * Send a message to all windows currently open for the greeter. + * + * This is primarily for themes that are runing in multi-monitor environments + */ + public broadcast(data: unknown): void { + ipcRenderer.send(CONSTS.channel.window_broadcast, data); + } } const allSignals = []; From 67008661b6e35356760bf720526ff1cc1d2a0255 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Sun, 16 Jan 2022 00:44:09 +0000 Subject: [PATCH 10/17] Use broadcast for dracula wallpaper changes Using the broadcast mechanism ensures that the wallpaper is changed across all screens. --- themes/dracula/js/backgrounds.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/themes/dracula/js/backgrounds.js b/themes/dracula/js/backgrounds.js index 515d883..c00ad4d 100644 --- a/themes/dracula/js/backgrounds.js +++ b/themes/dracula/js/backgrounds.js @@ -11,6 +11,17 @@ class Backgrounds { this._backgroundImages = null; this._backgroundImagesDir = null; this._backgroundPath = ""; + + /** + * Background change requests are handled via broadcast events so that all + * windows correctly update. + */ + window.addEventListener('NodyBroadcastEvent', (ev) => { + if (ev.data.type == 'change-background') { + this._backgroundPath = ev.data.path; + this._updateBackgroundImages(); + } + }) } _createImage(path) { @@ -57,8 +68,10 @@ class Backgrounds { const path = this._backgroundImages[i]; let button = this._createImage(path); button.addEventListener("click", () => { - this._backgroundPath = path; - this._updateBackgroundImages(); + nody_greeter.broadcast({ + type: 'change-background', + path + }); }); this._backgroundsList.appendChild(button); } From b831b67a8fd7127a20193d2d17f64a59bbd58bd0 Mon Sep 17 00:00:00 2001 From: JezerM Date: Sat, 15 Jan 2022 21:57:39 -0600 Subject: [PATCH 11/17] Load index.theme configuration and ensure theme does exists --- ts/browser.ts | 45 +++++++----------- ts/config.ts | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++ ts/index.ts | 5 +- 3 files changed, 149 insertions(+), 28 deletions(-) diff --git a/ts/browser.ts b/ts/browser.ts index caf32a0..b47f5fa 100644 --- a/ts/browser.ts +++ b/ts/browser.ts @@ -8,9 +8,12 @@ import { MenuItemConstructorOptions, } from "electron"; import * as path from "path"; -import * as fs from "fs"; -import { nody_greeter } from "./config"; +import { + load_primary_theme_path, + load_secondary_theme_path, + nody_greeter, +} from "./config"; import { URL } from "url"; import * as url from "url"; import { Brightness } from "./utils/brightness"; @@ -82,37 +85,25 @@ class Browser { } load_theme(): void { - const theme = nody_greeter.config.greeter.theme; - const dir = nody_greeter.app.theme_dir; - let path_to_theme = path.join(dir, theme, "index.html"); - const def_theme = "gruvbox"; - - if (theme.startsWith("/")) path_to_theme = theme; - else if (theme.includes(".") || theme.includes("/")) - path_to_theme = path.join(process.cwd(), theme); - - if (!path_to_theme.endsWith(".html")) - path_to_theme = path.join(path_to_theme, "index.html"); - - if (!fs.existsSync(path_to_theme)) { - logger.warn( - `"${theme}" theme does not exists. Using "${def_theme}" theme` - ); - path_to_theme = path.join(dir, def_theme, "index.html"); - } + const primary_html = load_primary_theme_path(); + const secondary_html = load_secondary_theme_path(); - nody_greeter.config.greeter.theme = path_to_theme; - - //this.win.loadFile(path_to_theme); - const theme_url = url.format({ - pathname: path_to_theme, + const primary_url = url.format({ + pathname: primary_html, + host: "app", + hostname: "app", + protocol: "web-greeter:", + }); + const secondary_url = url.format({ + pathname: secondary_html, host: "app", hostname: "app", protocol: "web-greeter:", }); - //console.log({ theme_url, url: new URL(theme_url) }); + //console.log({ primary_url, secondary_url }); for (const w of this.windows) { - w.window.loadURL(`${theme_url}`); + if (w.is_primary) w.window.loadURL(`${primary_url}`); + else w.window.loadURL(`${secondary_url}`); w.window.setBackgroundColor("#000000"); w.window.webContents.on("before-input-event", (_event, input) => { diff --git a/ts/config.ts b/ts/config.ts index e8bed24..b0ed16c 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -1,5 +1,6 @@ import * as yaml from "js-yaml"; import * as fs from "fs"; +import * as path from "path"; import { logger } from "./logger"; export interface web_greeter_config { @@ -38,6 +39,25 @@ export interface app_config { export interface nody_config { config: web_greeter_config; app: app_config; + theme: theme_config; +} + +/** + * Theme's config inside `$THEME/index.theme` + */ +export interface theme_config { + /** + * HTML file to use in main monitor + * @example primary_html: "index.html" + */ + primary_html: string; + /** + * HTML file to use in non-main (secondary) monitors + * If the file does not exists or it's not set, `primary_html` will be used + * @example secondary_html: "secondary.html" + * @example secondary_html: "" + */ + secondary_html: string; } export const nody_greeter: nody_config = { @@ -73,11 +93,116 @@ export const nody_greeter: nody_config = { theme_dir: process.env.NODY_GREETER_THEME_DIR || "/usr/share/web-greeter/themes/", }, + theme: { + primary_html: "index.html", + secondary_html: "", + }, }; const path_to_config = process.env.NODY_GREETER_CONFIG || "/etc/lightdm/web-greeter.yml"; +let theme_dir: string | undefined; + +export function load_theme_dir(): string { + const theme = nody_greeter.config.greeter.theme; + const dir = nody_greeter.app.theme_dir; + const def_theme = "gruvbox"; + theme_dir = path.join(dir, theme); + + if (theme.startsWith("/")) theme_dir = theme; + else if (theme.includes(".") || theme.includes("/")) + theme_dir = path.join(process.cwd(), theme); + + if (theme_dir.endsWith(".html")) theme_dir = path.dirname(theme_dir); + + if (!fs.existsSync(theme_dir)) { + logger.warn(`"${theme}" theme does not exists. Using "${def_theme}" theme`); + theme_dir = path.join(dir, def_theme); + } + + return theme_dir; +} + +export function load_primary_theme_path(): string { + if (!theme_dir) load_theme_dir(); + const dir = nody_greeter.app.theme_dir; + const def_theme = "gruvbox"; + const primary = nody_greeter.theme.primary_html; + let path_to_theme = path.join(theme_dir, primary); + + if (!path_to_theme.endsWith(".html")) + path_to_theme = path.join(path_to_theme, "index.html"); + + if (!fs.existsSync(path_to_theme)) { + logger.warn( + `"${path_to_theme}" theme does not exists. Using "${def_theme}" theme` + ); + path_to_theme = path.join(dir, def_theme, "index.html"); + } + + nody_greeter.config.greeter.theme = path_to_theme; + return path_to_theme; +} +export function load_secondary_theme_path(): string { + if (!theme_dir) load_theme_dir(); + const primary = nody_greeter.theme.primary_html; + const secondary = nody_greeter.theme.secondary_html; + let path_to_theme = path.join(theme_dir, secondary); + + if (!path_to_theme.endsWith(".html")) + path_to_theme = path.join(path_to_theme, "index.html"); + + if (!fs.existsSync(path_to_theme)) { + logger.warn( + `"${secondary}" does not exists. Using "${primary}" for secondary monitors` + ); + path_to_theme = load_primary_theme_path(); + } + + return path_to_theme; +} + +export function load_theme_config(): void { + if (!theme_dir) load_theme_dir(); + const path_to_theme_config = path.join(theme_dir, "index.theme"); + try { + if (!fs.existsSync(path_to_theme_config)) + throw new Error("Theme config not found"); + + const file = fs.readFileSync(path_to_theme_config, "utf-8"); + const theme_config = yaml.load(file) as theme_config; + + if (!theme_config.primary_html.match(/.*.html/)) + theme_config.primary_html = "index.html"; + if (!theme_config.secondary_html.match(/.*.html/)) + theme_config.secondary_html = ""; + + nody_greeter.theme = theme_config; + } catch (err) { + logger.warn(`Theme config was not loaded:\n\t${err}`); + logger.debug("Using default theme config"); + } +} + +export function ensure_theme(): void { + if (!theme_dir) load_theme_dir(); + + const primary = nody_greeter.theme.primary_html; + const dir = nody_greeter.app.theme_dir; + const def_theme = "gruvbox"; + + const primary_exists = fs.existsSync(path.join(theme_dir, primary)); + + if (!primary_exists) { + theme_dir = path.join(dir, def_theme); + + if (fs.existsSync(path.join(theme_dir, "index.theme"))) { + load_theme_config(); + } + } +} + export function load_config(): void { try { if (!fs.existsSync(path_to_config)) @@ -90,3 +215,5 @@ export function load_config(): void { } load_config(); + +export {}; diff --git a/ts/index.ts b/ts/index.ts index 1774383..7700d34 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -2,7 +2,7 @@ import * as yargs from "yargs"; import * as fs from "fs"; import * as path from "path"; -import { nody_greeter } from "./config"; +import { ensure_theme, load_theme_config, nody_greeter } from "./config"; const res = yargs .scriptName("nody-greeter") @@ -61,6 +61,9 @@ if (nody_greeter.config.greeter.debug_mode == true) { // Import browser and bridge to initialize nody-greeter +load_theme_config(); +ensure_theme(); + import "./browser"; import "./bridge/bridge"; From bf62927d716a4bea6440d1ecf8fb5d5512fca12a Mon Sep 17 00:00:00 2001 From: JezerM Date: Sat, 15 Jan 2022 22:41:21 -0600 Subject: [PATCH 12/17] Added secondary.html pages --- themes/dracula/index.theme | 2 ++ themes/dracula/js/backgrounds.js | 17 ++++++++--------- themes/dracula/js/secondary.js | 31 +++++++++++++++++++++++++++++++ themes/dracula/secondary.html | 32 ++++++++++++++++++++++++++++++++ themes/gruvbox/index.theme | 2 ++ themes/gruvbox/js/secondary.js | 28 ++++++++++++++++++++++++++++ themes/gruvbox/secondary.html | 27 +++++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 themes/dracula/index.theme create mode 100644 themes/dracula/js/secondary.js create mode 100644 themes/dracula/secondary.html create mode 100644 themes/gruvbox/index.theme create mode 100644 themes/gruvbox/js/secondary.js create mode 100644 themes/gruvbox/secondary.html diff --git a/themes/dracula/index.theme b/themes/dracula/index.theme new file mode 100644 index 0000000..5b85972 --- /dev/null +++ b/themes/dracula/index.theme @@ -0,0 +1,2 @@ +primary_html: "index.html" +secondary_html: "secondary.html" diff --git a/themes/dracula/js/backgrounds.js b/themes/dracula/js/backgrounds.js index c00ad4d..facc60f 100644 --- a/themes/dracula/js/backgrounds.js +++ b/themes/dracula/js/backgrounds.js @@ -5,7 +5,6 @@ class Backgrounds { "assets/dracula.png", "assets/window-blurred.png", ]; - this._sidebar = document.querySelector("#sidebar"); this._backgroundsList = document.querySelector("#background-selector"); this._background = document.querySelector("#background"); this._backgroundImages = null; @@ -16,12 +15,12 @@ class Backgrounds { * Background change requests are handled via broadcast events so that all * windows correctly update. */ - window.addEventListener('NodyBroadcastEvent', (ev) => { - if (ev.data.type == 'change-background') { - this._backgroundPath = ev.data.path; - this._updateBackgroundImages(); - } - }) + window.addEventListener("NodyBroadcastEvent", (ev) => { + if (ev.data.type == "change-background") { + this._backgroundPath = ev.data.path; + this._updateBackgroundImages(); + } + }); } _createImage(path) { @@ -69,8 +68,8 @@ class Backgrounds { let button = this._createImage(path); button.addEventListener("click", () => { nody_greeter.broadcast({ - type: 'change-background', - path + type: "change-background", + path, }); }); this._backgroundsList.appendChild(button); diff --git a/themes/dracula/js/secondary.js b/themes/dracula/js/secondary.js new file mode 100644 index 0000000..4ba7850 --- /dev/null +++ b/themes/dracula/js/secondary.js @@ -0,0 +1,31 @@ +async function wait(ms) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); +} + +async function _authentication_done() { + await wait(1000); + var body = document.querySelector("body"); + body.style.opacity = 0; +} + +function authentication_done() { + if (lightdm.is_authenticated) _authentication_done(); +} + +function initGreeter() { + lightdm.authentication_complete?.connect(() => authentication_done()); + + backgrounds = new Backgrounds(); + backgrounds._init(); +} + +if (window._ready_event === undefined) { + _ready_event = new Event("GreeterReady"); + window.dispatchEvent(_ready_event); +} + +window.addEventListener("GreeterReady", initGreeter); diff --git a/themes/dracula/secondary.html b/themes/dracula/secondary.html new file mode 100644 index 0000000..6937649 --- /dev/null +++ b/themes/dracula/secondary.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + Dracula theme + + +
+
+
+ + + + + + + diff --git a/themes/gruvbox/index.theme b/themes/gruvbox/index.theme new file mode 100644 index 0000000..5b85972 --- /dev/null +++ b/themes/gruvbox/index.theme @@ -0,0 +1,2 @@ +primary_html: "index.html" +secondary_html: "secondary.html" diff --git a/themes/gruvbox/js/secondary.js b/themes/gruvbox/js/secondary.js new file mode 100644 index 0000000..04eb704 --- /dev/null +++ b/themes/gruvbox/js/secondary.js @@ -0,0 +1,28 @@ +async function wait(ms) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); +} + +async function _authentication_done() { + await wait(500); + var body = document.querySelector("body"); + body.style.opacity = 0; +} + +function authentication_done() { + if (lightdm.is_authenticated) _authentication_done(); +} + +function initGreeter() { + lightdm.authentication_complete?.connect(() => authentication_done()); +} + +if (window._ready_event === undefined) { + _ready_event = new Event("GreeterReady"); + window.dispatchEvent(_ready_event); +} + +window.addEventListener("GreeterReady", initGreeter); diff --git a/themes/gruvbox/secondary.html b/themes/gruvbox/secondary.html new file mode 100644 index 0000000..0a84ea0 --- /dev/null +++ b/themes/gruvbox/secondary.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + Gruvbox theme + + +
+ + + + From 0bd7edca3d6c66b6d72fb6a30f2ffad8050943a1 Mon Sep 17 00:00:00 2001 From: JezerM Date: Sun, 16 Jan 2022 20:57:31 -0600 Subject: [PATCH 13/17] Unnecessary checks removed from config. "index.theme" renamed to "index.yml" --- themes/dracula/{index.theme => index.yml} | 0 themes/gruvbox/{index.theme => index.yml} | 0 ts/config.ts | 20 ++++++-------------- 3 files changed, 6 insertions(+), 14 deletions(-) rename themes/dracula/{index.theme => index.yml} (100%) rename themes/gruvbox/{index.theme => index.yml} (100%) diff --git a/themes/dracula/index.theme b/themes/dracula/index.yml similarity index 100% rename from themes/dracula/index.theme rename to themes/dracula/index.yml diff --git a/themes/gruvbox/index.theme b/themes/gruvbox/index.yml similarity index 100% rename from themes/gruvbox/index.theme rename to themes/gruvbox/index.yml diff --git a/ts/config.ts b/ts/config.ts index b0ed16c..ae116f8 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -43,7 +43,7 @@ export interface nody_config { } /** - * Theme's config inside `$THEME/index.theme` + * Theme's config inside `$THEME/index.yml` */ export interface theme_config { /** @@ -57,7 +57,7 @@ export interface theme_config { * @example secondary_html: "secondary.html" * @example secondary_html: "" */ - secondary_html: string; + secondary_html?: string; } export const nody_greeter: nody_config = { @@ -165,17 +165,14 @@ export function load_secondary_theme_path(): string { export function load_theme_config(): void { if (!theme_dir) load_theme_dir(); - const path_to_theme_config = path.join(theme_dir, "index.theme"); + const path_to_theme_config = path.join(theme_dir, "index.yml"); try { - if (!fs.existsSync(path_to_theme_config)) - throw new Error("Theme config not found"); - const file = fs.readFileSync(path_to_theme_config, "utf-8"); const theme_config = yaml.load(file) as theme_config; - if (!theme_config.primary_html.match(/.*.html/)) + if (!theme_config.primary_html.endsWith(".html")) theme_config.primary_html = "index.html"; - if (!theme_config.secondary_html.match(/.*.html/)) + if (!theme_config.secondary_html.endsWith(".html")) theme_config.secondary_html = ""; nody_greeter.theme = theme_config; @@ -196,17 +193,12 @@ export function ensure_theme(): void { if (!primary_exists) { theme_dir = path.join(dir, def_theme); - - if (fs.existsSync(path.join(theme_dir, "index.theme"))) { - load_theme_config(); - } + load_theme_config(); } } export function load_config(): void { try { - if (!fs.existsSync(path_to_config)) - throw new Error("Config file not found"); const file = fs.readFileSync(path_to_config, "utf-8"); nody_greeter.config = yaml.load(file) as web_greeter_config; } catch (err) { From b865a7810f1e17357758648b8e0310d4590f8590 Mon Sep 17 00:00:00 2001 From: JezerM Date: Sun, 16 Jan 2022 23:58:23 -0600 Subject: [PATCH 14/17] Validate greeter and theme's config with io-ts --- package-lock.json | 26 ++++++++ package.json | 2 + ts/config.ts | 159 +++++++++++++++++++++++++++++++--------------- 3 files changed, 135 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85ed3fe..e4a940d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.3.2", "license": "ISC", "dependencies": { + "fp-ts": "^2.11.7", + "io-ts": "^2.2.16", "js-yaml": "^4.1.0", "node-gtk": "^0.9.0", "winston": "^3.3.3", @@ -3133,6 +3135,11 @@ "node": ">= 0.12" } }, + "node_modules/fp-ts": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.11.7.tgz", + "integrity": "sha512-UUpeygu50mV/J96Nk92fzHDznYXJxsO20wrUZGJppja1f8P+fhCaclcqcfubEyrH7XXPsmYn98CJF0BVAEn3ZQ==" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -3629,6 +3636,14 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/io-ts": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.16.tgz", + "integrity": "sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q==", + "peerDependencies": { + "fp-ts": "^2.5.0" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -8653,6 +8668,11 @@ "mime-types": "^2.1.12" } }, + "fp-ts": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.11.7.tgz", + "integrity": "sha512-UUpeygu50mV/J96Nk92fzHDznYXJxsO20wrUZGJppja1f8P+fhCaclcqcfubEyrH7XXPsmYn98CJF0BVAEn3ZQ==" + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -9025,6 +9045,12 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "io-ts": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.16.tgz", + "integrity": "sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q==", + "requires": {} + }, "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", diff --git a/package.json b/package.json index 0de24cc..a2dca98 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,8 @@ "typescript": "^4.3.5" }, "dependencies": { + "fp-ts": "^2.11.7", + "io-ts": "^2.2.16", "js-yaml": "^4.1.0", "node-gtk": "^0.9.0", "winston": "^3.3.3", diff --git a/ts/config.ts b/ts/config.ts index ae116f8..53f692b 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -1,33 +1,65 @@ -import * as yaml from "js-yaml"; +import { fold } from "fp-ts/Either"; +import { pipe } from "fp-ts/function"; +import * as util from "util"; import * as fs from "fs"; +import * as io_ts from "io-ts"; +import * as yaml from "js-yaml"; import * as path from "path"; import { logger } from "./logger"; -export interface web_greeter_config { - branding: { - background_images_dir: string; - logo_image: string; - user_image: string; - }; - greeter: { - debug_mode: boolean; - detect_theme_errors: boolean; - screensaver_timeout: number; - secure_mode: boolean; - theme: string; - icon_theme: string | undefined; - time_language: string | undefined; - }; - layouts: string[]; - features: { - battery: boolean; - backlight: { - enabled: boolean; - value: number; - steps: number; - }; - }; -} +export const WEB_GREETER_CONFIG = io_ts.type({ + branding: io_ts.type({ + background_images_dir: io_ts.string, + logo_image: io_ts.string, + user_image: io_ts.string, + }), + greeter: io_ts.type({ + debug_mode: io_ts.boolean, + detect_theme_errors: io_ts.boolean, + screensaver_timeout: io_ts.number, + secure_mode: io_ts.boolean, + theme: io_ts.string, + icon_theme: io_ts.union([io_ts.string, io_ts.null]), + time_language: io_ts.union([io_ts.string, io_ts.null]), + }), + layouts: io_ts.array(io_ts.string), + features: io_ts.type({ + battery: io_ts.boolean, + backlight: io_ts.type({ + enabled: io_ts.boolean, + value: io_ts.number, + steps: io_ts.number, + }), + }), +}); + +export const THEME_CONFIG = io_ts.intersection([ + io_ts.type({ + /** + * HTML file to use in main monitor + * @example primary_html: "index.html" + */ + primary_html: io_ts.string, + }), + io_ts.partial({ + /** + * HTML file to use in non-main (secondary) monitors + * If the file does not exists or it's not set, `primary_html` will be used + * @example secondary_html: "secondary.html" + * @example secondary_html: "" + */ + secondary_html: io_ts.string, + }), +]); + +/** + * web-greeter's config inside `/etc/lightdm/web-greeter.yml` + */ +export type web_greeter_config = io_ts.TypeOf; +/** + * Theme's config inside `$THEME/index.yml` + */ +export type theme_config = io_ts.TypeOf; export interface app_config { fullscreen: boolean; @@ -42,24 +74,6 @@ export interface nody_config { theme: theme_config; } -/** - * Theme's config inside `$THEME/index.yml` - */ -export interface theme_config { - /** - * HTML file to use in main monitor - * @example primary_html: "index.html" - */ - primary_html: string; - /** - * HTML file to use in non-main (secondary) monitors - * If the file does not exists or it's not set, `primary_html` will be used - * @example secondary_html: "secondary.html" - * @example secondary_html: "" - */ - secondary_html?: string; -} - export const nody_greeter: nody_config = { config: { branding: { @@ -163,21 +177,53 @@ export function load_secondary_theme_path(): string { return path_to_theme; } +function validate_config( + decoder: io_ts.Type, + obj: unknown +): string { + const onLeft = (errors: io_ts.Errors): string => { + let message = ""; + const fm_errors: { key: string; value: string; type: string }[] = []; + for (let i = 0; i < errors.length; i++) { + const context = errors[i].context; + const type = context[context.length - 1].type.name; + const key = context[context.length - 2].key; + const value = errors[i].value; + const can_color = process.stdout.isTTY && process.stdout.hasColors(); + const fm_value = util.inspect(value, { colors: can_color }); + + const ind = fm_errors.findIndex((e) => e.key === key); + if (ind == -1) { + fm_errors.push({ key, value: fm_value, type }); + } else { + fm_errors[ind].type += "|" + type; + } + } + for (const err of fm_errors) { + message += `{ ${err.key}: ${err.value} } couldn't be validated as (${err.type})\n`; + } + return message; + }; + const onRight = (): string => ""; + return pipe(decoder.decode(obj), fold(onLeft, onRight)); +} + export function load_theme_config(): void { if (!theme_dir) load_theme_dir(); const path_to_theme_config = path.join(theme_dir, "index.yml"); + let error_message = ""; try { const file = fs.readFileSync(path_to_theme_config, "utf-8"); - const theme_config = yaml.load(file) as theme_config; + const theme_config = yaml.load(file); - if (!theme_config.primary_html.endsWith(".html")) - theme_config.primary_html = "index.html"; - if (!theme_config.secondary_html.endsWith(".html")) - theme_config.secondary_html = ""; + if (!THEME_CONFIG.is(theme_config)) { + error_message = validate_config(THEME_CONFIG, theme_config); + throw new Error("Invalid config"); + } nody_greeter.theme = theme_config; } catch (err) { - logger.warn(`Theme config was not loaded:\n\t${err}`); + logger.warn(`Theme config was not loaded:\n\t${err}\n${error_message}`); logger.debug("Using default theme config"); } } @@ -198,11 +244,20 @@ export function ensure_theme(): void { } export function load_config(): void { + let error_message = ""; try { const file = fs.readFileSync(path_to_config, "utf-8"); - nody_greeter.config = yaml.load(file) as web_greeter_config; + const webg_config = yaml.load(file); + + if (!WEB_GREETER_CONFIG.is(webg_config)) { + error_message = validate_config(WEB_GREETER_CONFIG, webg_config); + throw new Error("Invalid config"); + } + + nody_greeter.config = webg_config; } catch (err) { - logger.error(`Config was not loaded:\n\t${err}`); + logger.error(`Config was not loaded:\n\t${err}\n${error_message}`); + logger.warn("Using default config"); } } From d162e3859fe291633d69ce7d2852652aef14b116 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Mon, 17 Jan 2022 12:20:19 +0000 Subject: [PATCH 15/17] Simplify io-ts usage for config --- ts/config.ts | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/ts/config.ts b/ts/config.ts index 53f692b..0eb33ee 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -1,5 +1,4 @@ -import { fold } from "fp-ts/Either"; -import { pipe } from "fp-ts/function"; +import { isRight } from "fp-ts/Either"; import * as util from "util"; import * as fs from "fs"; import * as io_ts from "io-ts"; @@ -177,11 +176,15 @@ export function load_secondary_theme_path(): string { return path_to_theme; } -function validate_config( - decoder: io_ts.Type, +function validate_config( + decoder: io_ts.Type, obj: unknown -): string { - const onLeft = (errors: io_ts.Errors): string => { +): T { + const decoded = decoder.decode(obj); + if (isRight(decoded)) { + return decoded.right; + } else { + const errors = decoded.left; let message = ""; const fm_errors: { key: string; value: string; type: string }[] = []; for (let i = 0; i < errors.length; i++) { @@ -202,28 +205,20 @@ function validate_config( for (const err of fm_errors) { message += `{ ${err.key}: ${err.value} } couldn't be validated as (${err.type})\n`; } - return message; - }; - const onRight = (): string => ""; - return pipe(decoder.decode(obj), fold(onLeft, onRight)); + throw new Error(`Invalid config: ${message}`); + } } export function load_theme_config(): void { if (!theme_dir) load_theme_dir(); const path_to_theme_config = path.join(theme_dir, "index.yml"); - let error_message = ""; try { const file = fs.readFileSync(path_to_theme_config, "utf-8"); const theme_config = yaml.load(file); - if (!THEME_CONFIG.is(theme_config)) { - error_message = validate_config(THEME_CONFIG, theme_config); - throw new Error("Invalid config"); - } - - nody_greeter.theme = theme_config; + nody_greeter.theme = validate_config(THEME_CONFIG, theme_config); } catch (err) { - logger.warn(`Theme config was not loaded:\n\t${err}\n${error_message}`); + logger.warn(`Theme config was not loaded:\n\t${err}`); logger.debug("Using default theme config"); } } @@ -244,19 +239,13 @@ export function ensure_theme(): void { } export function load_config(): void { - let error_message = ""; try { const file = fs.readFileSync(path_to_config, "utf-8"); const webg_config = yaml.load(file); - if (!WEB_GREETER_CONFIG.is(webg_config)) { - error_message = validate_config(WEB_GREETER_CONFIG, webg_config); - throw new Error("Invalid config"); - } - - nody_greeter.config = webg_config; + nody_greeter.config = validate_config(WEB_GREETER_CONFIG, webg_config); } catch (err) { - logger.error(`Config was not loaded:\n\t${err}\n${error_message}`); + logger.error(`Config was not loaded:\n\t${err}`); logger.warn("Using default config"); } } From bfdd0c1d50cac12d9792c159288e34194381b2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jezer=20Mej=C3=ADa?= <59768785+JezerM@users.noreply.github.com> Date: Mon, 17 Jan 2022 11:12:34 -0600 Subject: [PATCH 16/17] Update ts/config.ts Co-authored-by: Sam Lanning --- ts/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/config.ts b/ts/config.ts index 0eb33ee..a397f2f 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -18,8 +18,8 @@ export const WEB_GREETER_CONFIG = io_ts.type({ screensaver_timeout: io_ts.number, secure_mode: io_ts.boolean, theme: io_ts.string, - icon_theme: io_ts.union([io_ts.string, io_ts.null]), - time_language: io_ts.union([io_ts.string, io_ts.null]), + icon_theme: io_ts.union([io_ts.string, io_ts.null, io_ts.undefined]), + time_language: io_ts.union([io_ts.string, io_ts.null, io_ts.undefined]), }), layouts: io_ts.array(io_ts.string), features: io_ts.type({ From 010b9c8c5fefcb4738b4862212acc9eeead1256c Mon Sep 17 00:00:00 2001 From: JezerM Date: Mon, 17 Jan 2022 13:39:24 -0600 Subject: [PATCH 17/17] Added retro-compatibility with web-greeter --- themes/dracula/js/backgrounds.js | 13 +++++++++---- themes/dracula/js/index.js | 7 +------ themes/gruvbox/js/index.js | 7 +------ ts/browser.ts | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/themes/dracula/js/backgrounds.js b/themes/dracula/js/backgrounds.js index facc60f..f311244 100644 --- a/themes/dracula/js/backgrounds.js +++ b/themes/dracula/js/backgrounds.js @@ -67,10 +67,15 @@ class Backgrounds { const path = this._backgroundImages[i]; let button = this._createImage(path); button.addEventListener("click", () => { - nody_greeter.broadcast({ - type: "change-background", - path, - }); + if (window.nody_greeter) { + nody_greeter.broadcast({ + type: "change-background", + path, + }); + } else { + this._backgroundPath = path; + this._updateBackgroundImages(); + } }); this._backgroundsList.appendChild(button); } diff --git a/themes/dracula/js/index.js b/themes/dracula/js/index.js index 1476f9b..b9b246e 100644 --- a/themes/dracula/js/index.js +++ b/themes/dracula/js/index.js @@ -47,15 +47,10 @@ async function initGreeter() { brightness = new Brightness(); - if (!nody_greeter.window_metadata.is_primary) { + if (window.nody_greeter && !window.nody_greeter.window_metadata.is_primary) { // Hide login elements on non-primary screen document.querySelector("#screen").classList.add("hide"); } } -if (window._ready_event === undefined) { - _ready_event = new Event("GreeterReady"); - window.dispatchEvent(_ready_event); -} - window.addEventListener("GreeterReady", initGreeter); diff --git a/themes/gruvbox/js/index.js b/themes/gruvbox/js/index.js index d93de70..d33446f 100644 --- a/themes/gruvbox/js/index.js +++ b/themes/gruvbox/js/index.js @@ -49,7 +49,7 @@ async function initGreeter() { document.querySelector("#lock-label").classList.remove("hide"); } - if (!nody_greeter.window_metadata.is_primary) { + if (window.nody_greeter && !window.nody_greeter.window_metadata.is_primary) { // Hide login elements on non-primary screen document.querySelector("#screen").classList.add("hide"); } @@ -57,9 +57,4 @@ async function initGreeter() { const notGreeter = false; -if (window._ready_event === undefined) { - _ready_event = new Event("GreeterReady"); - window.dispatchEvent(_ready_event); -} - window.addEventListener("GreeterReady", initGreeter); diff --git a/ts/browser.ts b/ts/browser.ts index b47f5fa..b0aa894 100644 --- a/ts/browser.ts +++ b/ts/browser.ts @@ -212,7 +212,7 @@ class Browser { if (w.is_primary) { w.window.focus(); } - logger.debug("Nody Greeter started win"); + logger.debug("Nody Greeter started win: " + w.meta.id); }); w.window.webContents.on("devtools-opened", () => { w.window.webContents.devToolsWebContents.focus();