diff --git a/forge.config.ts b/forge.config.ts
index 71c17fe..84de803 100644
--- a/forge.config.ts
+++ b/forge.config.ts
@@ -159,7 +159,12 @@ const config: ForgeConfig = {
target: "preload",
},
],
- renderer: [],
+ renderer: [
+ {
+ name: "main_window",
+ config: "vite.renderer.config.ts",
+ },
+ ],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
diff --git a/index.html b/index.html
index ebcd263..02bdeac 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,62 @@
-
+
- Hello World!
+
+ Stoat β Select Server
- π Hello World!
- Welcome to your Electron application.
+
+
Stoat Desktop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/config.d.ts b/src/config.d.ts
index d5f0cde..068205e 100644
--- a/src/config.d.ts
+++ b/src/config.d.ts
@@ -5,6 +5,7 @@ declare type DesktopConfig = {
spellchecker: boolean;
hardwareAcceleration: boolean;
discordRpc: boolean;
+ customServer: string;
windowState: {
x: number;
y: number;
diff --git a/src/index.css b/src/index.css
index 9bfd0b1..2550222 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,8 +1,286 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --bg: #111114;
+ --card: #1c1c21;
+ --border: #2e2e38;
+ --accent: #5865f2;
+ --accent-hover: #4752c4;
+ --text: #f2f2f2;
+ --text-sub: #8e9297;
+ --error: #ed4245;
+ --option-hover: #26262d;
+ --option-active-bg: rgba(88, 101, 242, 0.12);
+ --titlebar-h: 32px;
+}
+
+html,
body {
+ height: 100%;
+ background: var(--bg);
+ color: var(--text);
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
- margin: auto;
- max-width: 38rem;
- padding: 2rem;
+ font-size: 14px;
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ user-select: none;
+ overflow: hidden;
+}
+
+/* ββ Titlebar βββββββββββββββββββββββββββββββ */
+
+.titlebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: var(--titlebar-h);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 8px 0 14px;
+ background: #0d0d10;
+ border-bottom: 1px solid var(--border);
+ -webkit-app-region: drag;
+ z-index: 100;
+}
+
+.titlebar-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-sub);
+ letter-spacing: 0.3px;
+}
+
+.window-controls {
+ display: flex;
+ gap: 2px;
+ -webkit-app-region: no-drag;
+}
+
+.wc-btn {
+ width: 28px;
+ height: 24px;
+ border: none;
+ background: transparent;
+ color: var(--text-sub);
+ font-size: 13px;
+ cursor: pointer;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.1s, color 0.1s;
+}
+
+.wc-btn:hover {
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--text);
+}
+
+.wc-close:hover {
+ background: #c42b1c;
+ color: #fff;
+}
+
+/* ββ Main layout ββββββββββββββββββββββββββββ */
+
+.container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding: calc(var(--titlebar-h) + 24px) 24px 24px;
+}
+
+/* ββ Card βββββββββββββββββββββββββββββββββββ */
+
+.card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 32px 28px;
+ width: 100%;
+ max-width: 420px;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
+}
+
+/* ββ Header βββββββββββββββββββββββββββββββββ */
+
+.header {
+ text-align: center;
+ margin-bottom: 28px;
+}
+
+.logo {
+ font-size: 44px;
+ display: block;
+ margin-bottom: 10px;
+}
+
+.header h1 {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--text);
+ margin-bottom: 5px;
+}
+
+.header .subtitle {
+ color: var(--text-sub);
+ font-size: 13px;
+}
+
+/* ββ Server option buttons ββββββββββββββββββ */
+
+.server-options {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.server-option {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 13px 16px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--text);
+ cursor: pointer;
+ text-align: left;
+ transition:
+ background 0.12s,
+ border-color 0.12s;
+}
+
+.server-option:hover {
+ background: var(--option-hover);
+}
+
+.server-option.active {
+ border-color: var(--accent);
+ background: var(--option-active-bg);
+}
+
+.server-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.server-name {
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.server-url {
+ color: var(--text-sub);
+ font-size: 11px;
+ font-family: "Consolas", "Courier New", monospace;
+}
+
+.check-icon {
+ color: var(--accent);
+ font-size: 16px;
+ font-weight: 700;
+ opacity: 0;
+ transition: opacity 0.12s;
+ flex-shrink: 0;
+}
+
+.server-option.active .check-icon {
+ opacity: 1;
+}
+
+/* ββ Custom URL input βββββββββββββββββββββββ */
+
+.input-group {
+ margin-bottom: 20px;
+ display: none;
+}
+
+.input-group.visible {
+ display: block;
+}
+
+.input-group label {
+ display: block;
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text-sub);
+ text-transform: uppercase;
+ letter-spacing: 0.6px;
+ margin-bottom: 8px;
+}
+
+.input-group input {
+ width: 100%;
+ padding: 10px 14px;
+ background: rgba(0, 0, 0, 0.35);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text);
+ font-size: 13px;
+ font-family: "Consolas", "Courier New", monospace;
+ outline: none;
+ transition: border-color 0.12s;
+}
+
+.input-group input:focus {
+ border-color: var(--accent);
+}
+
+.input-group input::placeholder {
+ color: #4a4a55;
+}
+
+.input-error {
+ display: block;
+ color: var(--error);
+ font-size: 12px;
+ margin-top: 6px;
+ min-height: 16px;
+}
+
+/* ββ Connect button βββββββββββββββββββββββββ */
+
+.connect-btn {
+ width: 100%;
+ padding: 12px;
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ border-radius: 10px;
+ font-size: 15px;
+ font-weight: 600;
+ cursor: pointer;
+ transition:
+ background 0.12s,
+ transform 0.08s,
+ opacity 0.12s;
+}
+
+.connect-btn:hover {
+ background: var(--accent-hover);
+}
+
+.connect-btn:active {
+ transform: scale(0.98);
+}
+
+.connect-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
}
diff --git a/src/main.ts b/src/main.ts
index 911db87..e8e9f5e 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,13 +1,13 @@
import { IUpdateInfo, updateElectronApp } from "update-electron-app";
-import { BrowserWindow, Notification, app, shell } from "electron";
+import { BrowserWindow, Notification, app, globalShortcut, shell } from "electron";
import started from "electron-squirrel-startup";
import { autoLaunch } from "./native/autoLaunch";
import { config } from "./native/config";
import { initDiscordRpc } from "./native/discordRpc";
import { initTray } from "./native/tray";
-import { BUILD_URL, createMainWindow, mainWindow } from "./native/window";
+import { getBuildURL, createMainWindow, loadServerPicker, mainWindow } from "./native/window";
// Squirrel-specific logic
// create/remove shortcuts on Windows when installing / uninstalling
@@ -54,6 +54,10 @@ if (acquiredLock) {
initTray();
initDiscordRpc();
+ globalShortcut.register("CmdOrCtrl+Shift+S", () => {
+ loadServerPicker(mainWindow);
+ });
+
// Windows specific fix for notifications
if (process.platform === "win32") {
app.setAppUserModelId("chat.stoat.notifications");
@@ -87,9 +91,12 @@ if (acquiredLock) {
// ensure URLs launch in external context
app.on("web-contents-created", (_, contents) => {
- // prevent navigation out of build URL origin
+ // prevent navigation out of current server origin
contents.on("will-navigate", (event, navigationUrl) => {
- if (new URL(navigationUrl).origin !== BUILD_URL.origin) {
+ const currentOrigin = getBuildURL().origin;
+ const navOrigin = new URL(navigationUrl).origin;
+ // allow same-origin navigation and local file/dev-server navigation
+ if (navOrigin !== currentOrigin && navOrigin !== "null") {
event.preventDefault();
}
});
diff --git a/src/native/config.ts b/src/native/config.ts
index f7c2d37..9f7d1c1 100644
--- a/src/native/config.ts
+++ b/src/native/config.ts
@@ -28,6 +28,9 @@ const schema = {
discordRpc: {
type: "boolean",
} as JSONSchema.Boolean,
+ customServer: {
+ type: "string",
+ } as JSONSchema.String,
windowState: {
type: "object",
properties: {
@@ -60,6 +63,7 @@ const store = new Store({
spellchecker: true,
hardwareAcceleration: true,
discordRpc: true,
+ customServer: "",
windowState: {
x: 0,
y: 0,
@@ -83,6 +87,7 @@ class Config {
spellchecker: this.spellchecker,
hardwareAcceleration: this.hardwareAcceleration,
discordRpc: this.discordRpc,
+ customServer: this.customServer,
windowState: this.windowState,
});
}
@@ -192,6 +197,19 @@ class Config {
this.sync();
}
+ get customServer() {
+ return (store as never as { get(k: string): string }).get("customServer");
+ }
+
+ set customServer(value: string) {
+ (store as never as { set(k: string, value: string): void }).set(
+ "customServer",
+ value,
+ );
+
+ this.sync();
+ }
+
get windowState() {
return (
store as never as { get(k: string): DesktopConfig["windowState"] }
diff --git a/src/native/discordRpc.ts b/src/native/discordRpc.ts
index 9df475b..a7f3140 100644
--- a/src/native/discordRpc.ts
+++ b/src/native/discordRpc.ts
@@ -31,8 +31,8 @@ export async function initDiscordRpc() {
rpc.on("disconnected", reconnect);
- rpc.login({ clientId: "872068124005007420" });
- } catch (err) {
+ await rpc.login({ clientId: "872068124005007420" });
+ } catch {
reconnect();
}
}
diff --git a/src/native/tray.ts b/src/native/tray.ts
index 45fd6be..153bdeb 100644
--- a/src/native/tray.ts
+++ b/src/native/tray.ts
@@ -4,7 +4,7 @@ import trayIconAsset from "../../assets/desktop/icon.png?asset";
import macOsTrayIconAsset from "../../assets/desktop/iconTemplate.png?asset";
import { version } from "../../package.json";
-import { mainWindow, quitApp } from "./window";
+import { mainWindow, quitApp, getBuildURL, loadServerPicker } from "./window";
// internal tray state
let tray: Tray = null;
@@ -64,6 +64,20 @@ export function updateTrayMenu() {
}
},
},
+ { type: "separator" },
+ {
+ label: `Connected to: ${getBuildURL().hostname}`,
+ type: "normal",
+ enabled: false,
+ },
+ {
+ label: "Change Serverβ¦",
+ type: "normal",
+ click() {
+ loadServerPicker(mainWindow);
+ },
+ },
+ { type: "separator" },
{
label: "Quit App",
type: "normal",
diff --git a/src/native/window.ts b/src/native/window.ts
index 3cc68c8..b8a1bb8 100644
--- a/src/native/window.ts
+++ b/src/native/window.ts
@@ -14,15 +14,22 @@ import windowIconAsset from "../../assets/desktop/icon.png?asset";
import { config } from "./config";
import { updateTrayMenu } from "./tray";
+// injected by Electron Forge Vite plugin
+declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
+declare const MAIN_WINDOW_VITE_NAME: string;
+
+export const DEFAULT_SERVER = "https://beta.revolt.chat";
+
// global reference to main window
export let mainWindow: BrowserWindow;
-// currently in-use build
-export const BUILD_URL = new URL(
- app.commandLine.hasSwitch("force-server")
- ? app.commandLine.getSwitchValue("force-server")
- : /*MAIN_WINDOW_VITE_DEV_SERVER_URL ??*/ "https://beta.revolt.chat",
-);
+// currently in-use server URL β initialized inside createMainWindow() to avoid
+// the circular config β window import being evaluated before both modules are ready
+let _buildUrl: URL;
+
+export function getBuildURL(): URL {
+ return _buildUrl;
+}
// internal window state
let shouldQuit = false;
@@ -30,12 +37,90 @@ let shouldQuit = false;
// load the window icon
const windowIcon = nativeImage.createFromDataURL(windowIconAsset);
-// windowIcon.setTemplateImage(true);
+/**
+ * Load the local server-picker renderer page
+ */
+export function loadServerPicker(win: BrowserWindow = mainWindow) {
+ if (
+ typeof MAIN_WINDOW_VITE_DEV_SERVER_URL !== "undefined" &&
+ MAIN_WINDOW_VITE_DEV_SERVER_URL
+ ) {
+ win.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
+ } else {
+ win.loadFile(
+ join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
+ );
+ }
+}
+
+/**
+ * Inject a floating "Switch Server" button into the currently-loaded chat page.
+ * executeJavaScript bypasses the remote site's CSP, so no external assets are needed.
+ */
+function injectSwitchServerButton() {
+ mainWindow.webContents.executeJavaScript(`
+ (function () {
+ if (document.getElementById('__stoat-switch-btn')) return;
+
+ const btn = document.createElement('button');
+ btn.id = '__stoat-switch-btn';
+ btn.title = 'Switch Server (Ctrl+Shift+S)';
+ btn.innerHTML = 'Switch Server';
+
+ Object.assign(btn.style, {
+ position: 'fixed',
+ bottom: '16px',
+ right: '16px',
+ zIndex: '2147483647',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '6px',
+ padding: '6px 12px 6px 9px',
+ background: 'rgba(20,20,26,0.82)',
+ color: '#ccc',
+ border: '1px solid rgba(255,255,255,0.10)',
+ borderRadius: '20px',
+ fontSize: '12px',
+ fontFamily: '-apple-system,system-ui,sans-serif',
+ fontWeight: '500',
+ cursor: 'pointer',
+ backdropFilter: 'blur(10px)',
+ opacity: '0.35',
+ transition: 'opacity 0.18s, background 0.18s, color 0.18s',
+ userSelect: 'none',
+ lineHeight: '1',
+ });
+
+ btn.onmouseenter = () => {
+ btn.style.opacity = '1';
+ btn.style.background = 'rgba(88,101,242,0.92)';
+ btn.style.color = '#fff';
+ };
+ btn.onmouseleave = () => {
+ btn.style.opacity = '0.35';
+ btn.style.background = 'rgba(20,20,26,0.82)';
+ btn.style.color = '#ccc';
+ };
+
+ btn.onclick = () => window.native?.showServerPicker?.();
+
+ document.body.appendChild(btn);
+ })();
+ `);
+}
/**
* Create the main application window
*/
export function createMainWindow() {
+ // resolve which server URL to use β done here (not at module level) to avoid
+ // the circular config β window import triggering a TDZ error on startup
+ _buildUrl = new URL(
+ app.commandLine.hasSwitch("force-server")
+ ? app.commandLine.getSwitchValue("force-server")
+ : config.customServer || DEFAULT_SERVER,
+ );
+
// (CLI arg --hidden or config)
const startHidden =
app.commandLine.hasSwitch("hidden") || config.startMinimisedToTray;
@@ -46,7 +131,7 @@ export function createMainWindow() {
minHeight: 300,
width: 1280,
height: 720,
- backgroundColor: "#191919",
+ backgroundColor: "#111114",
frame: !config.customFrame,
icon: windowIcon,
show: !startHidden,
@@ -83,8 +168,14 @@ export function createMainWindow() {
mainWindow.maximize();
}
- // load the entrypoint
- mainWindow.loadURL(BUILD_URL.toString());
+ // show server picker on first launch / when no server is saved,
+ // otherwise go straight to the saved (or CLI-forced) server
+ const forceServer = app.commandLine.hasSwitch("force-server");
+ if (!forceServer && !config.customServer) {
+ loadServerPicker(mainWindow);
+ } else {
+ mainWindow.loadURL(_buildUrl.toString());
+ }
// minimise window to tray
mainWindow.on("close", (event) => {
@@ -141,8 +232,21 @@ export function createMainWindow() {
}
});
- // send the config
- mainWindow.webContents.on("did-finish-load", () => config.sync());
+ // send the config, then inject the switch-server button when on the chat page
+ mainWindow.webContents.on("did-finish-load", () => {
+ config.sync();
+ const pageUrl = mainWindow.webContents.getURL();
+ try {
+ if (
+ pageUrl.startsWith("http") &&
+ new URL(pageUrl).origin === _buildUrl.origin
+ ) {
+ injectSwitchServerButton();
+ }
+ } catch {
+ // ignore malformed URLs
+ }
+ });
// configure spellchecker context menu
mainWindow.webContents.on("context-menu", (_, params) => {
@@ -193,13 +297,33 @@ export function createMainWindow() {
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize(),
);
ipcMain.on("close", () => mainWindow.close());
-
- // mainWindow.webContents.openDevTools();
-
- // let i = 0;
- // setInterval(() => setBadgeCount((++i % 30) + 1), 1000);
}
+// navigate to a server URL, persist it, and load it in the window
+// registered once at module level β ipcMain.handle throws on duplicate registration
+ipcMain.handle("navigate", (_, url: string) => {
+ try {
+ _buildUrl = new URL(url);
+ // only persist if not overridden by --force-server CLI flag
+ if (!app.commandLine.hasSwitch("force-server")) {
+ config.customServer = url;
+ }
+ mainWindow.loadURL(url);
+ return true;
+ } catch {
+ return false;
+ }
+});
+
+// return the current server URL to the renderer
+ipcMain.handle("getServerUrl", () => _buildUrl.toString());
+
+// show the server picker without wiping the saved server
+// (picker pre-fills from getServerUrl; a new choice is only saved on Connect)
+ipcMain.handle("showServerPicker", () => {
+ loadServerPicker(mainWindow);
+});
+
/**
* Quit the entire app
*/
diff --git a/src/renderer.ts b/src/renderer.ts
index 08b05f6..d9e5708 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -1,32 +1,142 @@
-/**
- * This file will automatically be loaded by vite and run in the "renderer" context.
- * To learn more about the differences between the "main" and the "renderer" context in
- * Electron, visit:
- *
- * https://electronjs.org/docs/tutorial/process-model
- *
- * By default, Node.js integration in this file is disabled. When enabling Node.js integration
- * in a renderer process, please be aware of potential security implications. You can read
- * more about security risks here:
- *
- * https://electronjs.org/docs/tutorial/security
- *
- * To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration`
- * flag:
- *
- * ```
- * // Create the browser window.
- * mainWindow = new BrowserWindow({
- * width: 800,
- * height: 600,
- * webPreferences: {
- * nodeIntegration: true
- * }
- * });
- * ```
- */
import "./index.css";
-console.log(
- 'π This message is being logged by "renderer.ts", included via Vite',
-);
+const DEFAULT_SERVER = "https://beta.revolt.chat";
+
+type NativeAPI = {
+ minimise: () => void;
+ maximise: () => void;
+ close: () => void;
+ navigate: (url: string) => Promise;
+ getServerUrl: () => Promise;
+};
+
+declare global {
+ interface Window {
+ native: NativeAPI;
+ }
+}
+
+async function init() {
+ const optOfficial = document.getElementById(
+ "opt-official",
+ ) as HTMLButtonElement;
+ const optCustom = document.getElementById("opt-custom") as HTMLButtonElement;
+ const inputGroup = document.getElementById(
+ "custom-input-group",
+ ) as HTMLDivElement;
+ const serverUrlInput = document.getElementById(
+ "server-url-input",
+ ) as HTMLInputElement;
+ const customUrlPreview = document.getElementById(
+ "custom-url-preview",
+ ) as HTMLSpanElement;
+ const connectBtn = document.getElementById(
+ "connect-btn",
+ ) as HTMLButtonElement;
+ const urlError = document.getElementById("url-error") as HTMLSpanElement;
+
+ // Window control bindings
+ document
+ .getElementById("btn-minimise")
+ ?.addEventListener("click", () => window.native.minimise());
+ document
+ .getElementById("btn-maximise")
+ ?.addEventListener("click", () => window.native.maximise());
+ document
+ .getElementById("btn-close")
+ ?.addEventListener("click", () => window.native.close());
+
+ let mode: "official" | "custom" = "official";
+
+ function setMode(next: "official" | "custom") {
+ mode = next;
+ optOfficial.classList.toggle("active", next === "official");
+ optCustom.classList.toggle("active", next === "custom");
+ inputGroup.classList.toggle("visible", next === "custom");
+ if (next === "custom") {
+ serverUrlInput.focus();
+ }
+ }
+
+ // Try to pre-select the saved server
+ try {
+ const saved = await window.native.getServerUrl();
+ if (saved && saved !== DEFAULT_SERVER) {
+ serverUrlInput.value = saved;
+ try {
+ customUrlPreview.textContent = new URL(saved).hostname;
+ } catch {
+ customUrlPreview.textContent = saved;
+ }
+ setMode("custom");
+ } else {
+ setMode("official");
+ }
+ } catch {
+ setMode("official");
+ }
+
+ optOfficial.addEventListener("click", () => {
+ setMode("official");
+ urlError.textContent = "";
+ });
+
+ optCustom.addEventListener("click", () => {
+ setMode("custom");
+ });
+
+ serverUrlInput.addEventListener("input", () => {
+ urlError.textContent = "";
+ const raw = serverUrlInput.value.trim();
+ try {
+ customUrlPreview.textContent = new URL(raw).hostname || "Enter URL below";
+ } catch {
+ customUrlPreview.textContent = raw ? "Invalid URL" : "Enter URL below";
+ }
+ });
+
+ async function connect() {
+ urlError.textContent = "";
+
+ const url =
+ mode === "official" ? DEFAULT_SERVER : serverUrlInput.value.trim();
+
+ if (mode === "custom") {
+ if (!url) {
+ urlError.textContent = "Please enter a server URL.";
+ serverUrlInput.focus();
+ return;
+ }
+ try {
+ const parsed = new URL(url);
+ if (!parsed.protocol.startsWith("http")) {
+ urlError.textContent = "URL must start with http:// or https://";
+ serverUrlInput.focus();
+ return;
+ }
+ } catch {
+ urlError.textContent = "Invalid URL β please check and try again.";
+ serverUrlInput.focus();
+ return;
+ }
+ }
+
+ connectBtn.disabled = true;
+ connectBtn.textContent = "Connectingβ¦";
+
+ const ok = await window.native.navigate(url);
+ if (!ok) {
+ connectBtn.disabled = false;
+ connectBtn.textContent = "Connect";
+ urlError.textContent = "Could not connect. Please try again.";
+ }
+ }
+
+ connectBtn.addEventListener("click", connect);
+
+ serverUrlInput.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") connect();
+ });
+}
+
+init();
diff --git a/src/world/window.ts b/src/world/window.ts
index 2e9957b..5e166bd 100644
--- a/src/world/window.ts
+++ b/src/world/window.ts
@@ -15,4 +15,12 @@ contextBridge.exposeInMainWorld("native", {
close: () => ipcRenderer.send("close"),
setBadgeCount: (count: number) => ipcRenderer.send("setBadgeCount", count),
+
+ navigate: (url: string) =>
+ ipcRenderer.invoke("navigate", url) as Promise,
+
+ getServerUrl: () =>
+ ipcRenderer.invoke("getServerUrl") as Promise,
+
+ showServerPicker: () => ipcRenderer.invoke("showServerPicker") as Promise,
});