terminalos 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PtyManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const pty = __importStar(require("node-pty"));
41
+ const uuid_1 = require("uuid");
42
+ const process_detector_1 = require("./process-detector");
43
+ function createZdotdir() {
44
+ const zdotdir = fs.mkdtempSync(path.join(os.tmpdir(), "aiterm-"));
45
+ const zprofile = [
46
+ "# aiTerm: source real .zprofile (restores full PATH in packaged app)",
47
+ '[ -f "$HOME/.zprofile" ] && source "$HOME/.zprofile"',
48
+ ].join("\n");
49
+ fs.writeFileSync(path.join(zdotdir, ".zprofile"), zprofile);
50
+ const zshrc = [
51
+ "# aiTerm: source real .zshrc first",
52
+ "unset ZDOTDIR",
53
+ '[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc"',
54
+ "",
55
+ "# Add our hook as the LAST precmd so it wins over oh-my-zsh/conda/starship",
56
+ "_aiterm_precmd() {",
57
+ ' printf "\\n"',
58
+ ' PROMPT=" "',
59
+ ' RPROMPT=""',
60
+ ' printf "\\033]9001;%s\\007" "${CONDA_DEFAULT_ENV:-}"',
61
+ "}",
62
+ "precmd_functions+=(_aiterm_precmd)",
63
+ "",
64
+ "# Bold the command line after Enter is pressed",
65
+ "_aiterm_preexec() {",
66
+ ' printf "\\x1b[1A\\x1b[2K \\x1b[1m%s\\x1b[22m\\r\\n" "$1"',
67
+ "}",
68
+ "preexec_functions+=(_aiterm_preexec)",
69
+ 'PROMPT=" "',
70
+ 'RPROMPT=""',
71
+ ].join("\n");
72
+ fs.writeFileSync(path.join(zdotdir, ".zshrc"), zshrc);
73
+ return zdotdir;
74
+ }
75
+ class PtyManager {
76
+ sessions = new Map();
77
+ win;
78
+ constructor(win) {
79
+ this.win = win;
80
+ }
81
+ create(opts) {
82
+ const sessionId = (0, uuid_1.v4)();
83
+ const shell = process.platform === "win32"
84
+ ? "cmd.exe"
85
+ : (process.env.SHELL ?? "/bin/bash");
86
+ const cwd = opts.cwd ?? process.env.HOME ?? "/";
87
+ const isZsh = shell.endsWith("zsh");
88
+ const promptEnv = isZsh
89
+ ? { ZDOTDIR: createZdotdir() }
90
+ : { PS1: " ", PROMPT: " " };
91
+ const ptyProcess = pty.spawn(shell, ["-l"], {
92
+ name: "xterm-256color",
93
+ cols: 80,
94
+ rows: 24,
95
+ cwd,
96
+ env: {
97
+ ...process.env,
98
+ TERM: "xterm-256color",
99
+ COLORTERM: "truecolor",
100
+ ...promptEnv,
101
+ ...opts.env,
102
+ },
103
+ });
104
+ const detector = new process_detector_1.ProcessDetector();
105
+ const session = {
106
+ id: sessionId,
107
+ pty: ptyProcess,
108
+ buffer: [],
109
+ flushTimer: null,
110
+ detector,
111
+ cwd,
112
+ };
113
+ // Buffer data and flush every 12ms (Batching inteligente para evitar IPC slicing)
114
+ ptyProcess.onData((data) => {
115
+ session.buffer.push(data);
116
+ if (!session.flushTimer) {
117
+ session.flushTimer = setTimeout(() => {
118
+ if (session.buffer.length > 0) {
119
+ const chunk = session.buffer.join("");
120
+ session.buffer = [];
121
+ session.flushTimer = null; // Libera o timer para a próxima rodada de dados
122
+ if (!this.win.isDestroyed()) {
123
+ this.win.webContents.send("pty:data", sessionId, chunk);
124
+ }
125
+ // Check for AI process signatures
126
+ const result = detector.detect(chunk);
127
+ if (result === "detected" && !this.win.isDestroyed()) {
128
+ const ai = detector.getCurrentAI();
129
+ if (ai) {
130
+ this.win.webContents.send("pty:ai-detected", sessionId, ai);
131
+ }
132
+ }
133
+ else if (result === "exited" && !this.win.isDestroyed()) {
134
+ this.win.webContents.send("pty:ai-exited", sessionId);
135
+ }
136
+ }
137
+ }, 1);
138
+ }
139
+ });
140
+ ptyProcess.onExit(({ exitCode }) => {
141
+ if (session.flushTimer) {
142
+ clearTimeout(session.flushTimer); // Alterado de clearInterval
143
+ }
144
+ if (!this.win.isDestroyed()) {
145
+ this.win.webContents.send("pty:exit", sessionId, exitCode ?? 0);
146
+ }
147
+ this.sessions.delete(sessionId);
148
+ });
149
+ this.sessions.set(sessionId, session);
150
+ return sessionId;
151
+ }
152
+ write(sessionId, data) {
153
+ const session = this.sessions.get(sessionId);
154
+ if (session) {
155
+ session.pty.write(data);
156
+ }
157
+ }
158
+ resizeTimers = new Map();
159
+ resize(sessionId, cols, rows) {
160
+ const existing = this.resizeTimers.get(sessionId);
161
+ if (existing)
162
+ clearTimeout(existing);
163
+ const timer = setTimeout(() => {
164
+ const session = this.sessions.get(sessionId);
165
+ if (session) {
166
+ session.pty.resize(cols, rows);
167
+ }
168
+ this.resizeTimers.delete(sessionId);
169
+ }, 50);
170
+ this.resizeTimers.set(sessionId, timer);
171
+ }
172
+ async kill(sessionId) {
173
+ const session = this.sessions.get(sessionId);
174
+ if (session) {
175
+ if (session.flushTimer) {
176
+ clearTimeout(session.flushTimer); // Alterado de clearInterval
177
+ }
178
+ session.pty.kill();
179
+ this.sessions.delete(sessionId);
180
+ }
181
+ }
182
+ killAll() {
183
+ for (const [id] of this.sessions) {
184
+ this.kill(id);
185
+ }
186
+ }
187
+ }
188
+ exports.PtyManager = PtyManager;
@@ -0,0 +1,181 @@
1
+ import { BrowserWindow } from "electron";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import * as pty from "node-pty";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { ProcessDetector } from "./process-detector";
8
+
9
+ function createZdotdir(): string {
10
+ const zdotdir = fs.mkdtempSync(path.join(os.tmpdir(), "aiterm-"));
11
+ const zprofile = [
12
+ "# aiTerm: source real .zprofile (restores full PATH in packaged app)",
13
+ '[ -f "$HOME/.zprofile" ] && source "$HOME/.zprofile"',
14
+ ].join("\n");
15
+ fs.writeFileSync(path.join(zdotdir, ".zprofile"), zprofile);
16
+ const zshrc = [
17
+ "# aiTerm: source real .zshrc first",
18
+ "unset ZDOTDIR",
19
+ '[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc"',
20
+ "",
21
+ "# Add our hook as the LAST precmd so it wins over oh-my-zsh/conda/starship",
22
+ "_aiterm_precmd() {",
23
+ ' printf "\\n"',
24
+ ' PROMPT=" "',
25
+ ' RPROMPT=""',
26
+ ' printf "\\033]9001;%s\\007" "${CONDA_DEFAULT_ENV:-}"',
27
+ "}",
28
+ "precmd_functions+=(_aiterm_precmd)",
29
+ "",
30
+ "# Bold the command line after Enter is pressed",
31
+ "_aiterm_preexec() {",
32
+ ' printf "\\x1b[1A\\x1b[2K \\x1b[1m%s\\x1b[22m\\r\\n" "$1"',
33
+ "}",
34
+ "preexec_functions+=(_aiterm_preexec)",
35
+ 'PROMPT=" "',
36
+ 'RPROMPT=""',
37
+ ].join("\n");
38
+ fs.writeFileSync(path.join(zdotdir, ".zshrc"), zshrc);
39
+ return zdotdir;
40
+ }
41
+
42
+ interface Session {
43
+ id: string;
44
+ pty: pty.IPty;
45
+ buffer: string[];
46
+ flushTimer: ReturnType<typeof setTimeout> | null; // Alterado de setInterval para setTimeout
47
+ detector: ProcessDetector;
48
+ cwd: string;
49
+ }
50
+
51
+ export class PtyManager {
52
+ private sessions = new Map<string, Session>();
53
+ private win: BrowserWindow;
54
+
55
+ constructor(win: BrowserWindow) {
56
+ this.win = win;
57
+ }
58
+
59
+ create(opts: { cwd?: string; env?: Record<string, string> }): string {
60
+ const sessionId = uuidv4();
61
+ const shell =
62
+ process.platform === "win32"
63
+ ? "cmd.exe"
64
+ : (process.env.SHELL ?? "/bin/bash");
65
+
66
+ const cwd = opts.cwd ?? process.env.HOME ?? "/";
67
+
68
+ const isZsh = shell.endsWith("zsh");
69
+ const promptEnv = isZsh
70
+ ? { ZDOTDIR: createZdotdir() }
71
+ : { PS1: " ", PROMPT: " " };
72
+
73
+ const ptyProcess = pty.spawn(shell, ["-l"], {
74
+ name: "xterm-256color",
75
+ cols: 80,
76
+ rows: 24,
77
+ cwd,
78
+ env: {
79
+ ...process.env,
80
+ TERM: "xterm-256color",
81
+ COLORTERM: "truecolor",
82
+ ...promptEnv,
83
+ ...opts.env,
84
+ },
85
+ });
86
+
87
+ const detector = new ProcessDetector();
88
+ const session: Session = {
89
+ id: sessionId,
90
+ pty: ptyProcess,
91
+ buffer: [],
92
+ flushTimer: null,
93
+ detector,
94
+ cwd,
95
+ };
96
+
97
+ // Buffer data and flush every 12ms (Batching inteligente para evitar IPC slicing)
98
+ ptyProcess.onData((data) => {
99
+ session.buffer.push(data);
100
+
101
+ if (!session.flushTimer) {
102
+ session.flushTimer = setTimeout(() => {
103
+ if (session.buffer.length > 0) {
104
+ const chunk = session.buffer.join("");
105
+ session.buffer = [];
106
+ session.flushTimer = null; // Libera o timer para a próxima rodada de dados
107
+
108
+ if (!this.win.isDestroyed()) {
109
+ this.win.webContents.send("pty:data", sessionId, chunk);
110
+ }
111
+
112
+ // Check for AI process signatures
113
+ const result = detector.detect(chunk);
114
+ if (result === "detected" && !this.win.isDestroyed()) {
115
+ const ai = detector.getCurrentAI();
116
+ if (ai) {
117
+ this.win.webContents.send("pty:ai-detected", sessionId, ai);
118
+ }
119
+ } else if (result === "exited" && !this.win.isDestroyed()) {
120
+ this.win.webContents.send("pty:ai-exited", sessionId);
121
+ }
122
+ }
123
+ }, 1);
124
+ }
125
+ });
126
+
127
+ ptyProcess.onExit(({ exitCode }) => {
128
+ if (session.flushTimer) {
129
+ clearTimeout(session.flushTimer); // Alterado de clearInterval
130
+ }
131
+ if (!this.win.isDestroyed()) {
132
+ this.win.webContents.send("pty:exit", sessionId, exitCode ?? 0);
133
+ }
134
+ this.sessions.delete(sessionId);
135
+ });
136
+
137
+ this.sessions.set(sessionId, session);
138
+ return sessionId;
139
+ }
140
+
141
+ write(sessionId: string, data: string): void {
142
+ const session = this.sessions.get(sessionId);
143
+ if (session) {
144
+ session.pty.write(data);
145
+ }
146
+ }
147
+
148
+ private resizeTimers = new Map<string, ReturnType<typeof setTimeout>>();
149
+
150
+ resize(sessionId: string, cols: number, rows: number): void {
151
+ const existing = this.resizeTimers.get(sessionId);
152
+ if (existing) clearTimeout(existing);
153
+
154
+ const timer = setTimeout(() => {
155
+ const session = this.sessions.get(sessionId);
156
+ if (session) {
157
+ session.pty.resize(cols, rows);
158
+ }
159
+ this.resizeTimers.delete(sessionId);
160
+ }, 50);
161
+
162
+ this.resizeTimers.set(sessionId, timer);
163
+ }
164
+
165
+ async kill(sessionId: string): Promise<void> {
166
+ const session = this.sessions.get(sessionId);
167
+ if (session) {
168
+ if (session.flushTimer) {
169
+ clearTimeout(session.flushTimer); // Alterado de clearInterval
170
+ }
171
+ session.pty.kill();
172
+ this.sessions.delete(sessionId);
173
+ }
174
+ }
175
+
176
+ killAll(): void {
177
+ for (const [id] of this.sessions) {
178
+ this.kill(id);
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": ".",
10
+ "rootDir": "."
11
+ },
12
+ "include": ["./**/*.ts"],
13
+ "exclude": ["node_modules"]
14
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VersionsManager = void 0;
7
+ const electron_1 = require("electron");
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const MAX_VERSIONS = 50;
12
+ class VersionsManager {
13
+ getVersionsDir() {
14
+ return path_1.default.join(electron_1.app.getPath('userData'), 'md-versions');
15
+ }
16
+ getKey(filePath) {
17
+ return crypto_1.default.createHash('sha256').update(filePath).digest('hex');
18
+ }
19
+ async load(filePath) {
20
+ const dir = this.getVersionsDir();
21
+ const key = this.getKey(filePath);
22
+ const vFile = path_1.default.join(dir, `${key}.json`);
23
+ try {
24
+ const data = await promises_1.default.readFile(vFile, 'utf8');
25
+ const parsed = JSON.parse(data);
26
+ return parsed.versions ?? [];
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ }
32
+ async persist(filePath, versions) {
33
+ const dir = this.getVersionsDir();
34
+ await promises_1.default.mkdir(dir, { recursive: true });
35
+ const key = this.getKey(filePath);
36
+ const vFile = path_1.default.join(dir, `${key}.json`);
37
+ const data = { filePath, versions };
38
+ await promises_1.default.writeFile(vFile, JSON.stringify(data), 'utf8');
39
+ }
40
+ async saveVersion(filePath, content) {
41
+ const versions = await this.load(filePath);
42
+ // Skip if content is identical to last version
43
+ if (versions.length > 0 && versions[versions.length - 1].content === content) {
44
+ return null;
45
+ }
46
+ const nextVersion = versions.length > 0 ? versions[versions.length - 1].version + 1 : 1;
47
+ const now = Date.now();
48
+ const newVersion = {
49
+ id: new Date(now).toISOString(),
50
+ version: nextVersion,
51
+ timestamp: now,
52
+ content,
53
+ };
54
+ versions.push(newVersion);
55
+ // Prune to MAX_VERSIONS (keep most recent)
56
+ const pruned = versions.length > MAX_VERSIONS
57
+ ? versions.slice(versions.length - MAX_VERSIONS)
58
+ : versions;
59
+ await this.persist(filePath, pruned);
60
+ return { id: newVersion.id, version: newVersion.version, timestamp: newVersion.timestamp };
61
+ }
62
+ async listVersions(filePath) {
63
+ const versions = await this.load(filePath);
64
+ return versions
65
+ .map(({ id, version, timestamp }) => ({ id, version, timestamp }))
66
+ .reverse(); // newest first
67
+ }
68
+ async getVersion(filePath, versionId) {
69
+ const versions = await this.load(filePath);
70
+ const found = versions.find(v => v.id === versionId);
71
+ return found?.content ?? null;
72
+ }
73
+ }
74
+ exports.VersionsManager = VersionsManager;
@@ -0,0 +1,98 @@
1
+ import { app } from 'electron'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import crypto from 'crypto'
5
+
6
+ const MAX_VERSIONS = 50
7
+
8
+ export interface FileVersion {
9
+ id: string
10
+ version: number
11
+ timestamp: number
12
+ content: string
13
+ }
14
+
15
+ export interface VersionMeta {
16
+ id: string
17
+ version: number
18
+ timestamp: number
19
+ }
20
+
21
+ interface VersionsFile {
22
+ filePath: string
23
+ versions: FileVersion[]
24
+ }
25
+
26
+ export class VersionsManager {
27
+ private getVersionsDir(): string {
28
+ return path.join(app.getPath('userData'), 'md-versions')
29
+ }
30
+
31
+ private getKey(filePath: string): string {
32
+ return crypto.createHash('sha256').update(filePath).digest('hex')
33
+ }
34
+
35
+ private async load(filePath: string): Promise<FileVersion[]> {
36
+ const dir = this.getVersionsDir()
37
+ const key = this.getKey(filePath)
38
+ const vFile = path.join(dir, `${key}.json`)
39
+ try {
40
+ const data = await fs.readFile(vFile, 'utf8')
41
+ const parsed: VersionsFile = JSON.parse(data)
42
+ return parsed.versions ?? []
43
+ } catch {
44
+ return []
45
+ }
46
+ }
47
+
48
+ private async persist(filePath: string, versions: FileVersion[]): Promise<void> {
49
+ const dir = this.getVersionsDir()
50
+ await fs.mkdir(dir, { recursive: true })
51
+ const key = this.getKey(filePath)
52
+ const vFile = path.join(dir, `${key}.json`)
53
+ const data: VersionsFile = { filePath, versions }
54
+ await fs.writeFile(vFile, JSON.stringify(data), 'utf8')
55
+ }
56
+
57
+ async saveVersion(filePath: string, content: string): Promise<VersionMeta | null> {
58
+ const versions = await this.load(filePath)
59
+
60
+ // Skip if content is identical to last version
61
+ if (versions.length > 0 && versions[versions.length - 1].content === content) {
62
+ return null
63
+ }
64
+
65
+ const nextVersion = versions.length > 0 ? versions[versions.length - 1].version + 1 : 1
66
+ const now = Date.now()
67
+ const newVersion: FileVersion = {
68
+ id: new Date(now).toISOString(),
69
+ version: nextVersion,
70
+ timestamp: now,
71
+ content,
72
+ }
73
+
74
+ versions.push(newVersion)
75
+
76
+ // Prune to MAX_VERSIONS (keep most recent)
77
+ const pruned = versions.length > MAX_VERSIONS
78
+ ? versions.slice(versions.length - MAX_VERSIONS)
79
+ : versions
80
+
81
+ await this.persist(filePath, pruned)
82
+
83
+ return { id: newVersion.id, version: newVersion.version, timestamp: newVersion.timestamp }
84
+ }
85
+
86
+ async listVersions(filePath: string): Promise<VersionMeta[]> {
87
+ const versions = await this.load(filePath)
88
+ return versions
89
+ .map(({ id, version, timestamp }) => ({ id, version, timestamp }))
90
+ .reverse() // newest first
91
+ }
92
+
93
+ async getVersion(filePath: string, versionId: string): Promise<string | null> {
94
+ const versions = await this.load(filePath)
95
+ const found = versions.find(v => v.id === versionId)
96
+ return found?.content ?? null
97
+ }
98
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WindowState = void 0;
7
+ const electron_1 = require("electron");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ class WindowState {
11
+ filePath;
12
+ bounds = { width: 1280, height: 800 };
13
+ constructor() {
14
+ this.filePath = path_1.default.join(electron_1.app.getPath('userData'), 'window-state.json');
15
+ this.load();
16
+ }
17
+ load() {
18
+ try {
19
+ if (fs_1.default.existsSync(this.filePath)) {
20
+ const data = fs_1.default.readFileSync(this.filePath, 'utf8');
21
+ this.bounds = JSON.parse(data);
22
+ }
23
+ }
24
+ catch {
25
+ // Use defaults
26
+ }
27
+ }
28
+ get() {
29
+ return this.bounds;
30
+ }
31
+ save(win) {
32
+ if (!win.isMaximized() && !win.isMinimized()) {
33
+ const bounds = win.getBounds();
34
+ this.bounds = {
35
+ x: bounds.x,
36
+ y: bounds.y,
37
+ width: bounds.width,
38
+ height: bounds.height,
39
+ };
40
+ try {
41
+ fs_1.default.writeFileSync(this.filePath, JSON.stringify(this.bounds), 'utf8');
42
+ }
43
+ catch {
44
+ // Ignore write errors
45
+ }
46
+ }
47
+ }
48
+ }
49
+ exports.WindowState = WindowState;
@@ -0,0 +1,52 @@
1
+ import { BrowserWindow, app } from 'electron'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ interface WindowBounds {
6
+ x?: number
7
+ y?: number
8
+ width: number
9
+ height: number
10
+ }
11
+
12
+ export class WindowState {
13
+ private filePath: string
14
+ private bounds: WindowBounds = { width: 1280, height: 800 }
15
+
16
+ constructor() {
17
+ this.filePath = path.join(app.getPath('userData'), 'window-state.json')
18
+ this.load()
19
+ }
20
+
21
+ private load(): void {
22
+ try {
23
+ if (fs.existsSync(this.filePath)) {
24
+ const data = fs.readFileSync(this.filePath, 'utf8')
25
+ this.bounds = JSON.parse(data)
26
+ }
27
+ } catch {
28
+ // Use defaults
29
+ }
30
+ }
31
+
32
+ get(): WindowBounds {
33
+ return this.bounds
34
+ }
35
+
36
+ save(win: BrowserWindow): void {
37
+ if (!win.isMaximized() && !win.isMinimized()) {
38
+ const bounds = win.getBounds()
39
+ this.bounds = {
40
+ x: bounds.x,
41
+ y: bounds.y,
42
+ width: bounds.width,
43
+ height: bounds.height,
44
+ }
45
+ try {
46
+ fs.writeFileSync(this.filePath, JSON.stringify(this.bounds), 'utf8')
47
+ } catch {
48
+ // Ignore write errors
49
+ }
50
+ }
51
+ }
52
+ }