Skip to content

Commit

Permalink
Merge pull request #5 from andrewschreiber/as-hidewindow
Browse files Browse the repository at this point in the history
Hide mainWindow while performing actions
  • Loading branch information
corbt authored Oct 24, 2024
2 parents 1c9a522 + 23283cc commit 00e462d
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 92 deletions.
82 changes: 6 additions & 76 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
* When running `npm run build` or `npm run build:main`, this file is compiled to
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import { app, BrowserWindow, ipcMain, shell, screen } from 'electron';
import { app, ipcMain } from 'electron';
import log from 'electron-log';
import { autoUpdater } from 'electron-updater';
import path from 'path';
import { mainZustandBridge } from 'zutron/main';
import MenuBuilder from './menu';
import { createMainWindow } from './window';
import { store } from './store/create';
import { resolveHtmlPath } from './util';

class AppUpdater {
constructor() {
Expand All @@ -25,8 +24,6 @@ class AppUpdater {
}
}

let mainWindow: BrowserWindow | null = null;

ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
console.log(msgTemplate(arg));
Expand Down Expand Up @@ -58,7 +55,7 @@ const installExtensions = async () => {
.catch(console.log);
};

const createWindow = async () => {
const initializeApp = async () => {
if (isDebug) {
await installExtensions();
}
Expand All @@ -71,52 +68,7 @@ const createWindow = async () => {
return path.join(RESOURCES_PATH, ...paths);
};

// Get the primary display's work area (screen size minus taskbar/dock)
const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize;

mainWindow = new BrowserWindow({
show: false,
width: 350,
height: 600,
x: width - 350, // Position from right edge
y: 0, // Position from top edge (changed from: y: height - 500)
frame: false, // Remove default frame
transparent: true, // Optional: enables transparency
alwaysOnTop: true, // Keep window on top
icon: getAssetPath('icon.png'),
webPreferences: {
preload: app.isPackaged
? path.join(__dirname, 'preload.js')
: path.join(__dirname, '../../.erb/dll/preload.js'),
},
});

mainWindow.loadURL(resolveHtmlPath('index.html'));

mainWindow.on('ready-to-show', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
mainWindow.show();
}
});

mainWindow.on('closed', () => {
mainWindow = null;
});

const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();

// Open urls in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => {
shell.openExternal(edata.url);
return { action: 'deny' };
});
const mainWindow = await createMainWindow(getAssetPath);

// Remove this if your app does not use auto updates
// eslint-disable-next-line
Expand All @@ -127,23 +79,6 @@ const createWindow = async () => {
});

app.on('quit', unsubscribe);

// Add these window control handlers
ipcMain.handle('minimize-window', () => {
mainWindow?.minimize();
});

ipcMain.handle('maximize-window', () => {
if (mainWindow?.isMaximized()) {
mainWindow?.unmaximize();
} else {
mainWindow?.maximize();
}
});

ipcMain.handle('close-window', () => {
mainWindow?.close();
});
};

/**
Expand All @@ -160,12 +95,7 @@ app.on('window-all-closed', () => {

app
.whenReady()
.then(() => {
createWindow();
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});
.then(async () => {
await initializeApp();
})
.catch(console.log);
36 changes: 20 additions & 16 deletions src/main/store/runAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { desktopCapturer, screen } from 'electron';
import { anthropic } from './anthropic';
import { AppState, NextAction } from './types';
import { extractAction } from './extractAction';
import { hideWindowBlock, showWindow } from '../window';

const MAX_STEPS = 50;

Expand Down Expand Up @@ -36,26 +37,28 @@ function getAiScaledScreenDimensions(): { width: number; height: number } {
return { width: scaledWidth, height: scaledHeight };
}

const getScreenshot = async () => {
const getScreenshot = async (): Promise<string> => {
const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.size;
const aiDimensions = getAiScaledScreenDimensions();

const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width, height },
});
const primarySource = sources[0]; // Assuming the first source is the primary display
return hideWindowBlock(async () => {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width, height },
});
const primarySource = sources[0]; // Assuming the first source is the primary display

if (primarySource) {
const screenshot = primarySource.thumbnail;
// Resize the screenshot to AI dimensions
const resizedScreenshot = screenshot.resize(aiDimensions);
// Convert the resized screenshot to a base64-encoded PNG
const base64Image = resizedScreenshot.toPNG().toString('base64');
return base64Image;
}
throw new Error('No display found for screenshot');
if (primarySource) {
const screenshot = primarySource.thumbnail;
// Resize the screenshot to AI dimensions
const resizedScreenshot = screenshot.resize(aiDimensions);
// Convert the resized screenshot to a base64-encoded PNG
const base64Image = resizedScreenshot.toPNG().toString('base64');
return base64Image;
}
throw new Error('No display found for screenshot');
});
};

const mapToAiSpace = (x: number, y: number) => {
Expand Down Expand Up @@ -246,7 +249,8 @@ export const runAgent = async (
if (!getState().running) {
break;
}
performAction(action);

hideWindowBlock(() => performAction(action));

await new Promise((resolve) => setTimeout(resolve, 500));
if (!getState().running) {
Expand Down
177 changes: 177 additions & 0 deletions src/main/window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { app, BrowserWindow, ipcMain, shell, screen } from 'electron';
import path from 'path';
import { resolveHtmlPath } from './util';
import MenuBuilder from './menu';

let mainWindow: BrowserWindow | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
let showTimeout: NodeJS.Timeout | null = null;

const FADE_STEP = 0.1;
const FADE_INTERVAL = 16;
const SHOW_DELAY = 500;

function executeFade(show: boolean, resolve: () => void) {
if (!mainWindow) {
resolve();
return;
}

if (show) {
mainWindow.setOpacity(0);
mainWindow.showInactive();
}

let opacity = show ? 0 : 1;

fadeInterval = setInterval(() => {
if (!mainWindow) {
if (fadeInterval) clearInterval(fadeInterval);
resolve();
return;
}

opacity = show ? opacity + FADE_STEP : opacity - FADE_STEP;
opacity = Math.min(Math.max(opacity, 0), 1);
mainWindow.setOpacity(opacity);

if ((show && opacity >= 1) || (!show && opacity <= 0)) {
if (fadeInterval) clearInterval(fadeInterval);
if (!show) mainWindow.hide();
resolve();
}
}, FADE_INTERVAL);
}

function fadeWindow(show: boolean, immediate = false): Promise<void> {
return new Promise((resolve) => {
if (!mainWindow) {
resolve();
return;
}

if (fadeInterval) {
clearInterval(fadeInterval);
}

if (!show) {
if (showTimeout) {
clearTimeout(showTimeout);
showTimeout = null;
}
executeFade(show, resolve);
return;
}

if (showTimeout) {
clearTimeout(showTimeout);
}

if (immediate) {
executeFade(show, resolve);
} else {
showTimeout = setTimeout(() => {
executeFade(show, resolve);
}, SHOW_DELAY);
}
});
}

export async function createMainWindow(
getAssetPath: (...paths: string[]) => string,
): Promise<BrowserWindow> {
const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize;

mainWindow = new BrowserWindow({
show: false,
width: 350,
height: 600,
x: width - 350,
y: 0,
frame: false,
transparent: true,
alwaysOnTop: true,
icon: getAssetPath('icon.png'),
webPreferences: {
preload: app.isPackaged
? path.join(__dirname, 'preload.js')
: path.join(__dirname, '../../.erb/dll/preload.js'),
},
});

mainWindow.loadURL(resolveHtmlPath('index.html'));

mainWindow.on('ready-to-show', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
// Use immediate=true for initial show
fadeWindow(true, true);
}
});

mainWindow.on('closed', () => {
if (fadeInterval) {
clearInterval(fadeInterval);
}
if (showTimeout) {
clearTimeout(showTimeout);
}
mainWindow = null;
});

const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();

mainWindow.webContents.setWindowOpenHandler((edata) => {
shell.openExternal(edata.url);
return { action: 'deny' };
});

ipcMain.handle('minimize-window', () => {
mainWindow?.minimize();
});

ipcMain.handle('maximize-window', () => {
if (mainWindow?.isMaximized()) {
mainWindow?.unmaximize();
} else {
mainWindow?.maximize();
}
});

ipcMain.handle('close-window', async () => {
if (mainWindow) {
await fadeWindow(false);
mainWindow.close();
}
});

app.on('activate', () => {
if (mainWindow === null) createMainWindow(getAssetPath);
});

return mainWindow;
}

export async function showWindow(show: boolean) {
if (mainWindow) {
await fadeWindow(show);
}
}

export async function hideWindowBlock<T>(
operation: () => Promise<T> | T,
): Promise<T> {
try {
await fadeWindow(false);
const result = await Promise.resolve(operation());
return result;
} finally {
await fadeWindow(true);
}
}

0 comments on commit 00e462d

Please sign in to comment.