Skip to content

Commit 8628b46

Browse files
committed
feat: move more electron dependencies into their own service
1 parent 55b264b commit 8628b46

File tree

28 files changed

+381
-133
lines changed

28 files changed

+381
-133
lines changed

apps/code/src/main/db/service.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import path from "node:path";
2+
import type { IStoragePaths } from "@posthog/platform/storage-paths";
23
import Database from "better-sqlite3";
34
import {
45
type BetterSQLite3Database,
56
drizzle,
67
} from "drizzle-orm/better-sqlite3";
78
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
8-
import { app } from "electron";
9-
import { injectable, postConstruct, preDestroy } from "inversify";
9+
import { inject, injectable, postConstruct, preDestroy } from "inversify";
10+
import { MAIN_TOKENS } from "../di/tokens";
1011
import { logger } from "../utils/logger";
1112

1213
import * as schema from "./schema";
@@ -20,6 +21,11 @@ export class DatabaseService {
2021
private _db: BetterSQLite3Database<typeof schema> | null = null;
2122
private _sqlite: InstanceType<typeof Database> | null = null;
2223

24+
constructor(
25+
@inject(MAIN_TOKENS.StoragePaths)
26+
private readonly storagePaths: IStoragePaths,
27+
) {}
28+
2329
get db(): BetterSQLite3Database<typeof schema> {
2430
if (!this._db) {
2531
throw new Error("Database not initialized — call initialize() first");
@@ -29,7 +35,7 @@ export class DatabaseService {
2935

3036
@postConstruct()
3137
initialize(): void {
32-
const dbPath = path.join(app.getPath("userData"), "posthog-code.db");
38+
const dbPath = path.join(this.storagePaths.appDataPath, "posthog-code.db");
3339
log.info("Opening database", {
3440
path: dbPath,
3541
migrationsFolder: MIGRATIONS_FOLDER,

apps/code/src/main/di/container.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { SuspensionRepositoryImpl } from "../db/repositories/suspension-reposito
99
import { WorkspaceRepository } from "../db/repositories/workspace-repository";
1010
import { WorktreeRepository } from "../db/repositories/worktree-repository";
1111
import { DatabaseService } from "../db/service";
12+
import { ElectronAppMeta } from "../platform-adapters/electron-app-meta";
13+
import { ElectronClipboard } from "../platform-adapters/electron-clipboard";
14+
import { ElectronDialog } from "../platform-adapters/electron-dialog";
15+
import { ElectronFileIcon } from "../platform-adapters/electron-file-icon";
16+
import { ElectronSecureStorage } from "../platform-adapters/electron-secure-storage";
17+
import { ElectronStoragePaths } from "../platform-adapters/electron-storage-paths";
1218
import { ElectronUrlLauncher } from "../platform-adapters/electron-url-launcher";
1319
import { AgentAuthAdapter } from "../services/agent/auth-adapter";
1420
import { AgentService } from "../services/agent/service";
@@ -54,6 +60,12 @@ export const container = new Container({
5460
});
5561

5662
container.bind(MAIN_TOKENS.UrlLauncher).to(ElectronUrlLauncher);
63+
container.bind(MAIN_TOKENS.StoragePaths).to(ElectronStoragePaths);
64+
container.bind(MAIN_TOKENS.AppMeta).to(ElectronAppMeta);
65+
container.bind(MAIN_TOKENS.Dialog).to(ElectronDialog);
66+
container.bind(MAIN_TOKENS.Clipboard).to(ElectronClipboard);
67+
container.bind(MAIN_TOKENS.FileIcon).to(ElectronFileIcon);
68+
container.bind(MAIN_TOKENS.SecureStorage).to(ElectronSecureStorage);
5769

5870
container.bind(MAIN_TOKENS.DatabaseService).to(DatabaseService);
5971
container

apps/code/src/main/di/tokens.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
export const MAIN_TOKENS = Object.freeze({
88
// Platform ports (host-agnostic interfaces from @posthog/platform)
99
UrlLauncher: Symbol.for("Platform.UrlLauncher"),
10+
StoragePaths: Symbol.for("Platform.StoragePaths"),
11+
AppMeta: Symbol.for("Platform.AppMeta"),
12+
Dialog: Symbol.for("Platform.Dialog"),
13+
Clipboard: Symbol.for("Platform.Clipboard"),
14+
FileIcon: Symbol.for("Platform.FileIcon"),
15+
SecureStorage: Symbol.for("Platform.SecureStorage"),
1016

1117
// Stores
1218
SettingsStore: Symbol.for("Main.SettingsStore"),
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { IAppMeta } from "@posthog/platform/app-meta";
2+
import { app } from "electron";
3+
import { injectable } from "inversify";
4+
5+
@injectable()
6+
export class ElectronAppMeta implements IAppMeta {
7+
public get version(): string {
8+
return app.getVersion();
9+
}
10+
11+
public get isProduction(): boolean {
12+
return app.isPackaged;
13+
}
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { IClipboard } from "@posthog/platform/clipboard";
2+
import { clipboard } from "electron";
3+
import { injectable } from "inversify";
4+
5+
@injectable()
6+
export class ElectronClipboard implements IClipboard {
7+
public async writeText(text: string): Promise<void> {
8+
clipboard.writeText(text);
9+
}
10+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {
2+
ConfirmOptions,
3+
DialogSeverity,
4+
IDialog,
5+
PickFileOptions,
6+
} from "@posthog/platform/dialog";
7+
import {
8+
BrowserWindow,
9+
dialog,
10+
type MessageBoxOptions,
11+
type OpenDialogOptions,
12+
} from "electron";
13+
import { injectable } from "inversify";
14+
15+
type OpenDialogProperty = NonNullable<OpenDialogOptions["properties"]>[number];
16+
17+
function severityToType(severity?: DialogSeverity): MessageBoxOptions["type"] {
18+
return severity ?? "none";
19+
}
20+
21+
function buildProperties(options: PickFileOptions): OpenDialogProperty[] {
22+
const properties: OpenDialogProperty[] = [
23+
options.directories ? "openDirectory" : "openFile",
24+
"treatPackageAsDirectory",
25+
];
26+
if (options.multiple) properties.push("multiSelections");
27+
if (options.createDirectories) properties.push("createDirectory");
28+
return properties;
29+
}
30+
31+
@injectable()
32+
export class ElectronDialog implements IDialog {
33+
public async confirm(options: ConfirmOptions): Promise<number> {
34+
const parent = BrowserWindow.getFocusedWindow();
35+
const electronOptions: MessageBoxOptions = {
36+
type: severityToType(options.severity),
37+
title: options.title,
38+
message: options.message,
39+
detail: options.detail,
40+
buttons: options.options,
41+
defaultId: options.defaultIndex,
42+
cancelId: options.cancelIndex,
43+
};
44+
const result = parent
45+
? await dialog.showMessageBox(parent, electronOptions)
46+
: await dialog.showMessageBox(electronOptions);
47+
return result.response;
48+
}
49+
50+
public async pickFile(options: PickFileOptions): Promise<string[]> {
51+
const parent = BrowserWindow.getFocusedWindow();
52+
const electronOptions: OpenDialogOptions = {
53+
title: options.title,
54+
properties: buildProperties(options),
55+
};
56+
const result = parent
57+
? await dialog.showOpenDialog(parent, electronOptions)
58+
: await dialog.showOpenDialog(electronOptions);
59+
return result.canceled ? [] : result.filePaths;
60+
}
61+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { IFileIcon } from "@posthog/platform/file-icon";
2+
import { app } from "electron";
3+
import { injectable } from "inversify";
4+
5+
type FileIconModule = typeof import("file-icon");
6+
7+
@injectable()
8+
export class ElectronFileIcon implements IFileIcon {
9+
private fileIconModule: FileIconModule | undefined;
10+
11+
public async getAsDataUrl(filePath: string): Promise<string | null> {
12+
try {
13+
if (process.platform === "darwin") {
14+
const mod = await this.loadFileIconModule();
15+
const uint8Array = await mod.fileIconToBuffer(filePath, { size: 64 });
16+
const base64 = Buffer.from(uint8Array).toString("base64");
17+
return `data:image/png;base64,${base64}`;
18+
}
19+
20+
const icon = await app.getFileIcon(filePath, { size: "normal" });
21+
const base64 = icon.toPNG().toString("base64");
22+
return `data:image/png;base64,${base64}`;
23+
} catch {
24+
return null;
25+
}
26+
}
27+
28+
private async loadFileIconModule(): Promise<FileIconModule> {
29+
if (!this.fileIconModule) {
30+
this.fileIconModule = await import("file-icon");
31+
}
32+
return this.fileIconModule;
33+
}
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { ISecureStorage } from "@posthog/platform/secure-storage";
2+
import { safeStorage } from "electron";
3+
import { injectable } from "inversify";
4+
5+
@injectable()
6+
export class ElectronSecureStorage implements ISecureStorage {
7+
public isAvailable(): boolean {
8+
return safeStorage.isEncryptionAvailable();
9+
}
10+
11+
public async encryptString(text: string): Promise<Uint8Array> {
12+
const buffer = safeStorage.encryptString(text);
13+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
14+
}
15+
16+
public async decryptString(data: Uint8Array): Promise<string> {
17+
return safeStorage.decryptString(Buffer.from(data));
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { IStoragePaths } from "@posthog/platform/storage-paths";
2+
import { app } from "electron";
3+
import { injectable } from "inversify";
4+
5+
@injectable()
6+
export class ElectronStoragePaths implements IStoragePaths {
7+
public get appDataPath(): string {
8+
return app.getPath("userData");
9+
}
10+
11+
public get logsPath(): string {
12+
return app.getPath("logs");
13+
}
14+
}

apps/code/src/main/services/context-menu/service.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1+
import type { IDialog } from "@posthog/platform/dialog";
12
import type { DetectedApplication } from "@shared/types";
2-
import {
3-
dialog,
4-
Menu,
5-
type MenuItemConstructorOptions,
6-
nativeImage,
7-
} from "electron";
3+
import { Menu, type MenuItemConstructorOptions, nativeImage } from "electron";
84
import { inject, injectable } from "inversify";
95
import { MAIN_TOKENS } from "../../di/tokens";
10-
import { getMainWindow } from "../../trpc/context";
116
import type { ExternalAppsService } from "../external-apps/service";
127
import type {
138
ArchivedTaskAction,
@@ -46,6 +41,8 @@ export class ContextMenuService {
4641
constructor(
4742
@inject(MAIN_TOKENS.ExternalAppsService)
4843
private readonly externalAppsService: ExternalAppsService,
44+
@inject(MAIN_TOKENS.Dialog)
45+
private readonly dialog: IDialog,
4946
) {}
5047

5148
private async getExternalAppsData() {
@@ -348,17 +345,15 @@ export class ContextMenuService {
348345
}
349346

350347
private async confirm(options: ConfirmOptions): Promise<boolean> {
351-
const win = getMainWindow();
352-
const result = await dialog.showMessageBox({
353-
...(win ? { parent: win } : {}),
354-
type: "question",
348+
const response = await this.dialog.confirm({
349+
severity: "question",
355350
title: options.title,
356351
message: options.message,
357352
detail: options.detail,
358-
buttons: ["Cancel", options.confirmLabel],
359-
defaultId: 1,
360-
cancelId: 0,
353+
options: ["Cancel", options.confirmLabel],
354+
defaultIndex: 1,
355+
cancelIndex: 0,
361356
});
362-
return result.response === 1;
357+
return response === 1;
363358
}
364359
}

0 commit comments

Comments
 (0)