Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
Expand Down Expand Up @@ -284,7 +285,9 @@ const startup = Effect.gen(function* () {
preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;
const linuxElectronOptions =
environment.platform === "linux" && !hasCommandLinePasswordStore
? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess()
? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess(
yield* HostProcessHomeDirectory,
)
: preReadyElectronOptions.linux;
if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {
if (
Expand Down
26 changes: 14 additions & 12 deletions apps/desktop/src/app/DesktopPreReadyPlatform.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available.
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import * as Electron from "electron";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";

import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts";
import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts";
Expand All @@ -31,14 +30,15 @@ function readCommandLineSwitchValue(
return value.length > 0 ? value : null;
}

export const resolveEarlyLinuxElectronOptionsFromProcess =
(): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>
DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({
env: process.env,
homeDirectory: NodeOS.homedir(),
joinPath: NodePath.posix.join,
readFileString: (path) => NodeFS.readFileSync(path, "utf8"),
});
export const resolveEarlyLinuxElectronOptionsFromProcess = (
homeDirectory: string,
): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>
DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({
env: process.env,
homeDirectory,
joinPath: NodePath.posix.join,
readFileString: (path) => NodeFS.readFileSync(path, "utf8"),
});

export class DesktopPreReadyElectronOptions extends Context.Service<
DesktopPreReadyElectronOptions,
Expand All @@ -51,20 +51,22 @@ export class DesktopPreReadyElectronOptions extends Context.Service<
/** @public Service construction is part of the canonical Effect module API. */
export const make = Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const homeDirectory = yield* HostProcessHomeDirectory;
return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => {
const linuxPasswordStoreCommandLine =
platform === "linux"
? readCommandLineSwitchValue(Electron.app.commandLine, "password-store")
: null;
const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null;
const linux =
platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null;

if (linux !== null) {
// The portal also requires a valid desktop entry. An AppImage update may
// have removed the executable referenced by the previous launch's entry.
try {
const applicationsDir = NodePath.posix.join(
process.env.XDG_DATA_HOME?.trim() ||
NodePath.posix.join(NodeOS.homedir(), ".local", "share"),
NodePath.posix.join(homeDirectory, ".local", "share"),
"applications",
);
NodeFS.mkdirSync(applicationsDir, { recursive: true });
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview";
import { resolveEditorCommand } from "@t3tools/shared/editor";
import * as HostProcess from "@t3tools/shared/hostProcess";
import * as NodeOS from "node:os";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -46,6 +45,7 @@ import {
resolveWslPickFolderDefaultPath,
wslUncPathToLinuxPath,
} from "../../wsl/wslPathParsing.ts";
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";

const ContextMenuPosition = Schema.Struct({
x: Schema.Number,
Expand Down Expand Up @@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
// The VS Code extensions directory is the same dotfolder on Windows,
// macOS, and Linux; when it is missing the picker opens wherever the
// platform would by default.
const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions");
const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions");
const defaultPath = yield* fileSystem
.exists(extensionsDir)
.pipe(Effect.orElseSucceed(() => false));
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ beforeEach(async () => {
tools.validateNiri.mockReset().mockResolvedValue(undefined);
tools.hyprlandBindings.mockReset().mockResolvedValue([]);
tools.reloadHyprland.mockReset().mockResolvedValue(undefined);
setup = new CaptureShortcutConfig(tools);
setup = new CaptureShortcutConfig(NodeOS.homedir(), tools);
});
afterEach(async () => {
await NodeFSP.rm(directory, { recursive: true, force: true });
Expand Down Expand Up @@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => {
});
it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => {
expect(
niriCaptureConfigPath(
{ NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" },
"/home/test",
),
niriCaptureConfigPath("/home/test", {
NIRI_CONFIG: "/custom/test.kdl",
XDG_CONFIG_HOME: "/config",
}),
).toBe("/custom/test.kdl");
expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe(
expect(niriCaptureConfigPath("/home/test", { XDG_CONFIG_HOME: "/config" })).toBe(
"/config/niri/config.kdl",
);
expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl");
expect(niriCaptureConfigPath("/home/test", {})).toBe("/home/test/.config/niri/config.kdl");
});
it("preserves a UTF-8 BOM through preview and apply", async () => {
await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n");
Expand All @@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ
NodePath.join(directory, "keys.kdl"),
"binds {\n Ctrl+Alt+Q { quit; }\n}\n",
);
const checked = new CaptureShortcutConfig({
const checked = new CaptureShortcutConfig(NodeOS.homedir(), {
...tools,
validateNiri: async (file) => {
await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]);
Expand Down
9 changes: 5 additions & 4 deletions apps/desktop/src/snapShot/CaptureShortcutConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import * as NodeFSP from "node:fs/promises";
import * as NodeFS from "node:fs";
import * as NodePath from "node:path";
import * as NodeOS from "node:os";
import * as NodeCrypto from "node:crypto";
import * as NodeChildProcess from "node:child_process";
import * as NodeUtil from "node:util";
Expand Down Expand Up @@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync(
),
);

export function niriCaptureConfigPath(env = process.env, home = NodeOS.homedir()) {
export function niriCaptureConfigPath(home: string, env = process.env) {
return (
env.NIRI_CONFIG ||
NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl")
Expand Down Expand Up @@ -99,8 +98,10 @@ export class CaptureShortcutConfig {
| { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] }
| undefined;
private applying = false;
private readonly home: string;
private readonly tools: typeof defaultTools;
constructor(tools = defaultTools) {
constructor(home: string, tools = defaultTools) {
this.home = home;
this.tools = tools;
}

Expand Down Expand Up @@ -166,7 +167,7 @@ export class CaptureShortcutConfig {
if (include.path.includes("$") || /[*?[\]]/.test(include.path))
throw new Error("This config uses a dynamic include. Use manual setup in Advanced.");
const path = include.path.startsWith("~/")
? NodePath.join(NodeOS.homedir(), include.path.slice(2))
? NodePath.join(this.home, include.path.slice(2))
: NodePath.resolve(NodePath.dirname(file.path), include.path);
let child: Snapshot;
try {
Expand Down
14 changes: 8 additions & 6 deletions apps/desktop/src/snapShot/DesktopSnapShot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
snapShotShortcutRegistrationFailureMessage,
snapShotShortcutSystemConflict,
} from "./snapShot.ts";
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";

const MAX_CAPTURE_WIDTH = 2_560;
const MAX_CAPTURE_HEIGHT = 1_600;
Expand Down Expand Up @@ -727,6 +728,7 @@ export const make = Effect.gen(function* () {
const runPromise = Effect.runPromiseWith(context);
const captureDirectory = path.join(environment.stateDir, "snap-shots");
const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, "");
const home = yield* HostProcessHomeDirectory;
let shortcutVerified = false;
const gnomeSetupPaths = {
bundle: environment.isPackaged
Expand Down Expand Up @@ -757,7 +759,7 @@ export const make = Effect.gen(function* () {
dataHome: path.dirname(environment.linuxApplicationsDir),
};
const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs");
const shortcutConfig = new CaptureShortcutConfig();
const shortcutConfig = new CaptureShortcutConfig(home);
const accessibilityWorkerPath = path.join(
__dirname,
"snapShot",
Expand Down Expand Up @@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () {
shortcut,
shortcutRegistered: false,
shortcutBinding: niriCaptureBinding(linuxAppId),
shortcutConfigPath: niriCaptureConfigPath(),
shortcutConfigPath: niriCaptureConfigPath(home),
shortcutActionRegistered: registered,
shortcutMessage: registered
? "Set up the shortcut to add it to your Niri config."
Expand Down Expand Up @@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () {
const configPath =
selectedPath ??
(desktop === "niri"
? niriCaptureConfigPath()
: (await hyprlandCaptureShortcut(linuxAppId)).shortcutConfigPath);
? niriCaptureConfigPath(home)
: (await hyprlandCaptureShortcut(linuxAppId, home)).shortcutConfigPath);
return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request);
},
catch: (cause) =>
Expand Down Expand Up @@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () {
: undefined;
const hyprlandShortcut =
state.linuxBackend === "hyprland"
? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId))
? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId, home))
: undefined;
return {
...state,
Expand All @@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () {
: {}),
...hyprlandShortcut,
...(state.linuxBackend === "niri"
? { shortcutConfigPath: niriCaptureConfigPath() }
? { shortcutConfigPath: niriCaptureConfigPath(home) }
: {}),
...(kdeHelper
? {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/snapShot/HyprlandSnapShot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ it.each([true, false])("uses the active config syntax and user bindings (Lua: %s
NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
"custom bindings",
);
const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", directory);
const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory);
expect(result.shortcutConfigPath).toBe(
NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/snapShot/HyprlandSnapShot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string {
/** Omarchy owns its defaults; instructions always point at a user-owned config. */
export async function hyprlandCaptureShortcut(
appId: string,
configHome = process.env.XDG_CONFIG_HOME || NodePath.join(NodeOS.homedir(), ".config"),
home: string,
configHome = process.env.XDG_CONFIG_HOME || NodePath.join(home, ".config"),
) {
const directory = NodePath.join(configHome, "hypr");
const exists = async (name: string) =>
Expand Down
14 changes: 3 additions & 11 deletions apps/server/src/assets/AssetAccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import {
AssetAccessError,
AssetPreviewTypeValidationError,
Expand Down Expand Up @@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials";
import { githubMediaResponse } from "./GitHubMediaFetch.ts";
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";

vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof NodeFSP>();
return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };
});

vi.mock("node:os", async (importOriginal) => {
const actual = await importOriginal<typeof NodeOS>();
return { ...actual, homedir: vi.fn(actual.homedir) };
});

const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
prefix: "t3-asset-access-test-",
});
Expand Down Expand Up @@ -299,8 +294,7 @@ describe("AssetAccess", () => {
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
yield* fs.writeFileString(filePath, "recording bytes");
const canonicalFile = yield* fs.realPath(filePath);
const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home);
try {
yield* Effect.gen(function* () {
for (const workspaceRoot of [
path.join(home, "project"),
path.join(directory, "srv", "project"),
Expand Down Expand Up @@ -332,9 +326,7 @@ describe("AssetAccess", () => {
expect(yield* Effect.promise(() => response.text())).toBe("recording bytes");
}
}
} finally {
homeSpy.mockRestore();
}
}).pipe(Effect.provideService(HostProcessHomeDirectory, home));
}).pipe(Effect.provide(layerTest)),
);

Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/assets/AssetAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";
import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts";
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";

export const ASSET_ROUTE_PREFIX = "/api/assets";

Expand Down Expand Up @@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i

switch (input.resource._tag) {
case "media-file": {
let requestedPath = expandHomePath(input.resource.path);
let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory);
if (!path.isAbsolute(requestedPath)) {
if (!input.workspaceRoot) {
return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource });
Expand Down
Loading
Loading