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/themes/dracula/index.yml b/themes/dracula/index.yml
new file mode 100644
index 0000000..5b85972
--- /dev/null
+++ b/themes/dracula/index.yml
@@ -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 515d883..f311244 100644
--- a/themes/dracula/js/backgrounds.js
+++ b/themes/dracula/js/backgrounds.js
@@ -5,12 +5,22 @@ 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;
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 +67,15 @@ class Backgrounds {
const path = this._backgroundImages[i];
let button = this._createImage(path);
button.addEventListener("click", () => {
- this._backgroundPath = path;
- this._updateBackgroundImages();
+ 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 7544e32..b9b246e 100644
--- a/themes/dracula/js/index.js
+++ b/themes/dracula/js/index.js
@@ -46,11 +46,11 @@ async function initGreeter() {
battery = new Battery();
brightness = new Brightness();
-}
-if (window._ready_event === undefined) {
- _ready_event = new Event("GreeterReady");
- window.dispatchEvent(_ready_event);
+ if (window.nody_greeter && !window.nody_greeter.window_metadata.is_primary) {
+ // Hide login elements on non-primary screen
+ document.querySelector("#screen").classList.add("hide");
+ }
}
window.addEventListener("GreeterReady", initGreeter);
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.yml b/themes/gruvbox/index.yml
new file mode 100644
index 0000000..5b85972
--- /dev/null
+++ b/themes/gruvbox/index.yml
@@ -0,0 +1,2 @@
+primary_html: "index.html"
+secondary_html: "secondary.html"
diff --git a/themes/gruvbox/js/index.js b/themes/gruvbox/js/index.js
index 6d1b56a..d33446f 100644
--- a/themes/gruvbox/js/index.js
+++ b/themes/gruvbox/js/index.js
@@ -48,13 +48,13 @@ async function initGreeter() {
if (lock) {
document.querySelector("#lock-label").classList.remove("hide");
}
+
+ if (window.nody_greeter && !window.nody_greeter.window_metadata.is_primary) {
+ // Hide login elements on non-primary screen
+ document.querySelector("#screen").classList.add("hide");
+ }
}
const notGreeter = false;
-if (window._ready_event === undefined) {
- _ready_event = new Event("GreeterReady");
- window.dispatchEvent(_ready_event);
-}
-
window.addEventListener("GreeterReady", initGreeter);
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
+
+
+
+
+
+
+
diff --git a/ts/bridge/bridge.ts b/ts/bridge/bridge.ts
index ece5a77..a2b740c 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;
@@ -53,7 +54,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 +104,13 @@ 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(
+ CONSTS.channel.lightdm_signal,
+ signal,
+ ...args
+ );
+ }
}
/**
@@ -727,6 +734,37 @@ 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
+ );
+ }
+ }
+});
+
+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/browser.ts b/ts/browser.ts
index 367be27..b0aa894 100644
--- a/ts/browser.ts
+++ b/ts/browser.ts
@@ -8,15 +8,25 @@ 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";
import { logger } from "./logger";
import { set_screensaver, reset_screensaver } from "./utils/screensaver";
+import { WindowMetadata } from "./preload";
+interface NodyWindow {
+ is_primary: boolean;
+ display: Electron.Display;
+ window: BrowserWindow;
+ meta: WindowMetadata;
+}
class Browser {
ready = false;
@@ -27,8 +37,7 @@ class Browser {
});
}
- // @ts-ignore
- win: BrowserWindow;
+ windows: NodyWindow[];
whenReady(): Promise {
return new Promise((resolve) => {
@@ -43,7 +52,7 @@ class Browser {
init(): void {
this.set_protocol();
- this.win = this.create_window();
+ this.windows = this.create_windows();
this.load_theme();
this.init_listeners();
}
@@ -76,69 +85,104 @@ 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");
- }
-
- nody_greeter.config.greeter.theme = path_to_theme;
+ const primary_html = load_primary_theme_path();
+ const secondary_html = load_secondary_theme_path();
- //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:",
});
- //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);
- }
+ const secondary_url = url.format({
+ pathname: secondary_html,
+ host: "app",
+ hostname: "app",
+ protocol: "web-greeter:",
});
+ //console.log({ primary_url, secondary_url });
+ for (const w of this.windows) {
+ 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) => {
+ 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();
+
+ // 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");
@@ -157,61 +201,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.meta.id);
+ });
+ 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 +276,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/config.ts b/ts/config.ts
index 4931399..a397f2f 100644
--- a/ts/config.ts
+++ b/ts/config.ts
@@ -1,32 +1,64 @@
-import * as yaml from "js-yaml";
+import { isRight } from "fp-ts/Either";
+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, 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({
+ 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;
@@ -38,6 +70,7 @@ export interface app_config {
export interface nody_config {
config: web_greeter_config;
app: app_config;
+ theme: theme_config;
}
export const nody_greeter: nody_config = {
@@ -70,21 +103,153 @@ 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/",
+ },
+ theme: {
+ primary_html: "index.html",
+ secondary_html: "",
},
};
-const path_to_config = "/etc/lightdm/web-greeter.yml";
+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;
+}
+
+function validate_config(
+ decoder: io_ts.Type,
+ obj: unknown
+): 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++) {
+ 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`;
+ }
+ 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");
+ try {
+ const file = fs.readFileSync(path_to_theme_config, "utf-8");
+ const theme_config = yaml.load(file);
+
+ nody_greeter.theme = validate_config(THEME_CONFIG, 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);
+ 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;
+ const webg_config = yaml.load(file);
+
+ nody_greeter.config = validate_config(WEB_GREETER_CONFIG, webg_config);
} catch (err) {
logger.error(`Config was not loaded:\n\t${err}`);
+ logger.warn("Using default config");
}
}
load_config();
+
+export {};
diff --git a/ts/consts.ts b/ts/consts.ts
new file mode 100644
index 0000000..d8c6982
--- /dev/null
+++ b/ts/consts.ts
@@ -0,0 +1,12 @@
+/**
+ * Constant values shared across the application
+ *
+ * (used by both backend (node) Node frontend (browser-window) code)
+ */
+export const CONSTS = {
+ channel: {
+ lightdm_signal: "LightDMSignal",
+ window_metadata: "WindowMetadata",
+ window_broadcast: "WindowBroadcast",
+ },
+} as const;
diff --git a/ts/globals.ts b/ts/globals.ts
index 27dcd28..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";
@@ -12,39 +12,47 @@ 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(win.window, message, sourceID, line);
+ } else if (code == 2) {
+ logger.log({
+ level: "warn",
+ message: message,
+ line: line,
+ source: sourceID,
+ });
+ }
}
- }
- );
+ );
+ }
}
/**
* 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.win, {
+ const ind = dialog.showMessageBoxSync(win, {
message:
"An error ocurred. Do you want to change to default theme? (gruvbox)",
detail: `${source} ${line}: ${message}`,
@@ -60,7 +68,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;
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";
diff --git a/ts/preload.ts b/ts/preload.ts
index 81f8c65..ddbc483 100644
--- a/ts/preload.ts
+++ b/ts/preload.ts
@@ -1,4 +1,5 @@
import { ipcRenderer } from "electron";
+import { CONSTS } from "./consts";
import {
LightDMBattery,
LightDMLanguage,
@@ -7,6 +8,108 @@ 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;
+ };
+}
+
+/**
+ * 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`
+ */
+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();
+ });
+
+ // 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;
+ }
+
+ 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;
+
+ /**
+ * 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 = [];
export class Signal {
@@ -58,7 +161,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)
@@ -799,18 +902,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;
@@ -818,6 +936,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;