Skip to content
20 changes: 20 additions & 0 deletions apps/desktop/src/desktopSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
DEFAULT_DESKTOP_SETTINGS,
readDesktopSettings,
setDesktopLinuxTitleBarMode,
setDesktopServerExposurePreference,
writeDesktopSettings,
} from "./desktopSettings";
Expand Down Expand Up @@ -35,10 +36,12 @@ describe("desktopSettings", () => {

writeDesktopSettings(settingsPath, {
serverExposureMode: "network-accessible",
linuxTitleBarMode: "custom",
});

expect(readDesktopSettings(settingsPath)).toEqual({
serverExposureMode: "network-accessible",
linuxTitleBarMode: "custom",
});
});

Expand All @@ -47,11 +50,13 @@ describe("desktopSettings", () => {
setDesktopServerExposurePreference(
{
serverExposureMode: "local-only",
linuxTitleBarMode: DEFAULT_DESKTOP_SETTINGS.linuxTitleBarMode,
},
"network-accessible",
),
).toEqual({
serverExposureMode: "network-accessible",
linuxTitleBarMode: DEFAULT_DESKTOP_SETTINGS.linuxTitleBarMode,
});
});

Expand All @@ -61,4 +66,19 @@ describe("desktopSettings", () => {

expect(readDesktopSettings(settingsPath)).toEqual(DEFAULT_DESKTOP_SETTINGS);
});

it("updates the requested linux title bar mode", () => {
expect(
setDesktopLinuxTitleBarMode(
{
...DEFAULT_DESKTOP_SETTINGS,
linuxTitleBarMode: "native",
},
"overlay",
),
).toEqual({
...DEFAULT_DESKTOP_SETTINGS,
linuxTitleBarMode: "overlay",
});
});
});
20 changes: 20 additions & 0 deletions apps/desktop/src/desktopSettings.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as FS from "node:fs";
import * as Path from "node:path";
import type { DesktopServerExposureMode } from "@t3tools/contracts";
import { DEFAULT_LINUX_TITLE_BAR_MODE, type LinuxTitleBarMode } from "@t3tools/contracts/settings";

export interface DesktopSettings {
readonly serverExposureMode: DesktopServerExposureMode;
readonly linuxTitleBarMode: LinuxTitleBarMode;
}

export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
serverExposureMode: "local-only",
linuxTitleBarMode: DEFAULT_LINUX_TITLE_BAR_MODE,
};

export function setDesktopServerExposurePreference(
Expand All @@ -22,6 +25,18 @@ export function setDesktopServerExposurePreference(
};
}

export function setDesktopLinuxTitleBarMode(
settings: DesktopSettings,
requestedMode: LinuxTitleBarMode,
): DesktopSettings {
return settings.linuxTitleBarMode === requestedMode
? settings
: {
...settings,
linuxTitleBarMode: requestedMode,
};
}

export function readDesktopSettings(settingsPath: string): DesktopSettings {
try {
if (!FS.existsSync(settingsPath)) {
Expand All @@ -31,11 +46,16 @@ export function readDesktopSettings(settingsPath: string): DesktopSettings {
const raw = FS.readFileSync(settingsPath, "utf8");
const parsed = JSON.parse(raw) as {
readonly serverExposureMode?: unknown;
readonly linuxTitleBarMode?: unknown;
};

return {
serverExposureMode:
parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",
linuxTitleBarMode:
parsed.linuxTitleBarMode === "overlay" || parsed.linuxTitleBarMode === "custom"
? parsed.linuxTitleBarMode
: DEFAULT_LINUX_TITLE_BAR_MODE,
};
} catch {
return DEFAULT_DESKTOP_SETTINGS;
Expand Down
47 changes: 47 additions & 0 deletions apps/desktop/src/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";

import { getWindowControlsLayout } from "./env";

vi.mock("./linuxWindowControls", () => ({
getLinuxWindowControlsLayout: vi.fn().mockReturnValue({
left: [],
right: ["minimize", "maximize", "close"],
}),
}));

describe("getWindowControlsLayout", () => {
it("uses the standard macOS traffic-light placement in ltr locales", () => {
expect(getWindowControlsLayout({ locale: "en-US", platform: "macos" })).toEqual({
left: ["close", "minimize", "maximize"],
right: [],
});
});

it("keeps macOS traffic lights left-aligned in rtl locales", () => {
expect(getWindowControlsLayout({ locale: "ar", platform: "macos" })).toEqual({
left: ["close", "minimize", "maximize"],
right: [],
});
});

it("uses the standard Windows control layout in ltr locales", () => {
expect(getWindowControlsLayout({ locale: "en-US", platform: "windows" })).toEqual({
left: [],
right: ["minimize", "maximize", "close"],
});
});

it("mirrors Windows controls in rtl locales", () => {
expect(getWindowControlsLayout({ locale: "he", platform: "windows" })).toEqual({
left: ["close", "maximize", "minimize"],
right: [],
});
});

it("keeps Linux layout unchanged even in rtl locales", () => {
expect(getWindowControlsLayout({ locale: "ar", platform: "linux" })).toEqual({
left: [],
right: ["minimize", "maximize", "close"],
});
});
});
105 changes: 105 additions & 0 deletions apps/desktop/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { DesktopPlatform, DesktopWindowControlsLayout } from "@t3tools/contracts";
import type { LinuxTitleBarMode } from "@t3tools/contracts/settings";
import { getLinuxWindowControlsLayout } from "./linuxWindowControls";

const DESKTOP_TITLEBAR_HEIGHT = 52;
const RTL_LANGUAGES = new Set(["ar", "dv", "fa", "he", "ku", "ps", "sd", "ug", "ur", "yi"]);
const MACOS_WINDOW_CONTROLS_LAYOUT: DesktopWindowControlsLayout = {
left: ["close", "minimize", "maximize"],
right: [],
};
const WINDOWS_WINDOW_CONTROLS_LAYOUT: DesktopWindowControlsLayout = {
left: [],
right: ["minimize", "maximize", "close"],
};

export const platform: DesktopPlatform = (() => {
switch (process.platform) {
case "darwin":
return "macos";
case "win32":
return "windows";
case "linux":
return "linux";
default:
throw new Error(`Unsupported desktop platform: ${process.platform}`);
}
})();

function isRightToLeftLocale(locale: string | undefined): boolean {
if (!locale) {
return false;
}

const language = locale.split(/[-_]/, 1)[0]?.toLowerCase();
return language !== undefined && RTL_LANGUAGES.has(language);
}

function mirrorWindowControlsLayout(
layout: DesktopWindowControlsLayout,
): DesktopWindowControlsLayout {
return {
left: layout.right.toReversed(),
right: layout.left.toReversed(),
};
}

export function getWindowControlsLayout(options?: {
locale?: string;
platform?: DesktopPlatform;
}): DesktopWindowControlsLayout {
const resolvedPlatform = options?.platform ?? platform;
if (resolvedPlatform === "linux") {
return getLinuxWindowControlsLayout();
}

const rtl = isRightToLeftLocale(options?.locale);
const layout =
resolvedPlatform === "macos" ? MACOS_WINDOW_CONTROLS_LAYOUT : WINDOWS_WINDOW_CONTROLS_LAYOUT;

if (!rtl || resolvedPlatform === "macos") {
return layout;
}

return mirrorWindowControlsLayout(layout);
}

export function getWindowChromeOptions(linuxTitleBarMode: LinuxTitleBarMode): {
titleBarStyle?: "hiddenInset" | "hidden";
titleBarOverlay?: { height: number; color?: string };
trafficLightPosition?: { x: number; y: number };
} {
if (platform === "macos") {
return {
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 18 },
};
}

if (platform === "linux") {
if (linuxTitleBarMode === "native") {
return {};
}

if (linuxTitleBarMode === "overlay") {
return {
titleBarStyle: "hidden",
titleBarOverlay: {
height: DESKTOP_TITLEBAR_HEIGHT,
color: "#01000000", // #00000000 doesn't work falling back to default value, not sure why, probably some bug in Electron
},
};
}

return {
titleBarStyle: "hidden",
};
}

return {
titleBarStyle: "hidden",
titleBarOverlay: {
height: DESKTOP_TITLEBAR_HEIGHT,
},
};
}
91 changes: 91 additions & 0 deletions apps/desktop/src/linuxWindowControls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from "vitest";

import { getLinuxWindowControlsLayout } from "./linuxWindowControls";

describe("getLinuxWindowControlsLayout", () => {
it("reads KDE button placement from kwinrc", () => {
const layout = getLinuxWindowControlsLayout({
existsSync: vi.fn().mockReturnValue(true),
homeDir: "/home/tester",
readFileSync: vi
.fn()
.mockReturnValue("[org.kde.kdecoration2]\nButtonsOnLeft=XIA\nButtonsOnRight=M\n"),
spawnSync: vi.fn(),
});

expect(layout).toEqual({
left: ["close", "minimize", "maximize"],
right: [],
});
});

it("falls back to GNOME button placement when KDE config is unavailable", () => {
const layout = getLinuxWindowControlsLayout({
existsSync: vi.fn().mockReturnValue(false),
homeDir: "/home/tester",
readFileSync: vi.fn(),
spawnSync: vi.fn().mockReturnValue({
status: 0,
stdout: "'close,minimize:maximize'\n",
}),
});

expect(layout).toEqual({
left: ["close", "minimize"],
right: ["maximize"],
});
});

it("falls back when kwinrc exists but does not define button placement", () => {
const layout = getLinuxWindowControlsLayout({
existsSync: vi.fn().mockReturnValue(true),
homeDir: "/home/tester",
readFileSync: vi.fn().mockReturnValue("[org.kde.kdecoration2]\n"),
spawnSync: vi.fn().mockReturnValue({
status: 0,
stdout: "'close,minimize:maximize'\n",
}),
});

expect(layout).toEqual({
left: ["close", "minimize"],
right: ["maximize"],
});
});

it("falls back to GNOME when reading kwinrc throws", () => {
const layout = getLinuxWindowControlsLayout({
existsSync: vi.fn().mockReturnValue(true),
homeDir: "/home/tester",
readFileSync: vi.fn().mockImplementation(() => {
throw new Error("permission denied");
}),
spawnSync: vi.fn().mockReturnValue({
status: 0,
stdout: "'close,minimize:maximize'\n",
}),
});

expect(layout).toEqual({
left: ["close", "minimize"],
right: ["maximize"],
});
});

it("uses the default right-side controls when no desktop layout is available", () => {
const layout = getLinuxWindowControlsLayout({
existsSync: vi.fn().mockReturnValue(false),
homeDir: "/home/tester",
readFileSync: vi.fn(),
spawnSync: vi.fn().mockReturnValue({
status: 1,
stdout: "",
}),
});

expect(layout).toEqual({
left: [],
right: ["minimize", "maximize", "close"],
});
});
});
Loading
Loading