@adamklein/situp
Version:
A simple cross-platform posture reminder that pops up a video every 15 minutes.
314 lines (282 loc) • 8.26 kB
JavaScript
const { app, BrowserWindow, Menu, Tray } = require('electron');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
const fs = require('fs');
const { nativeImage } = require('electron');
let popupWindow = null;
let intervalId = null;
let tray = null;
const CONFIG_PATH = path.join(os.homedir(), '.situp-config.json');
const DEFAULT_CONFIG = {
width: 480,
interval: 15,
runOnStartup: true,
firstRun: true,
opacity: 0.8, // Default 80% opacity
};
// Calculate height based on 9:16 aspect ratio (portrait)
function calculateHeight(width) {
return Math.round(width * 16 / 9);
}
function loadConfig() {
try {
if (fs.existsSync(CONFIG_PATH)) {
const data = fs.readFileSync(CONFIG_PATH, 'utf8');
return { ...DEFAULT_CONFIG, ...JSON.parse(data), firstRun: false };
}
} catch (e) {}
return { ...DEFAULT_CONFIG };
}
function saveConfig(config) {
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
}
let config = loadConfig();
// Helper: Check Do Not Disturb (macOS)
function isDoNotDisturbMac() {
try {
// Modern macOS Focus/DND detection
const result = execSync(
'defaults read com.apple.controlcenter "NSStatusItem Visible FocusModes"',
{ encoding: 'utf8' }
).trim();
if (result === '1') {
// If Focus menu is visible, check if any focus mode is active
const focusStatus = execSync(
'plutil -extract "v2" xml1 ~/Library/DoNotDisturb/DB/Assertions.json -o - | grep "com.apple.donotdisturb.mode"',
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
);
return focusStatus.length > 0;
}
return false;
} catch (e) {
return false;
}
}
// Helper: Check Focus Assist (Windows 10+)
function isFocusAssistWindows() {
try {
// Query registry for Focus Assist (Quiet Hours)
const result = execSync(
'reg query "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings" /v NOC_GLOBAL_SETTING_TOASTS_ENABLED',
{ encoding: 'utf8' }
);
// If value is 0, Focus Assist is ON
return result.includes('0x0');
} catch (e) {
return false;
}
}
function isDoNotDisturb() {
if (process.platform === 'darwin') {
return isDoNotDisturbMac();
} else if (process.platform === 'win32') {
return isFocusAssistWindows();
}
return false;
}
function createTray() {
if (tray) return;
const iconPath = path.join(__dirname, 'icons', process.platform === 'darwin' ? 'favicon-16x16.png' : 'logo.png');
let trayIcon = nativeImage.createFromPath(iconPath);
if (process.platform === 'darwin') {
// For macOS, use the 16x16 monochrome icon that works in both light and dark mode
trayIcon.setTemplateImage(true);
}
tray = new Tray(trayIcon);
if (process.platform === 'darwin' && typeof tray.setHighlightMode === 'function') {
tray.setHighlightMode('never');
// Remove the title text next to the icon
tray.setTitle('');
}
const contextMenu = Menu.buildFromTemplate([
{
label: 'Preferences',
click: openPreferencesWindow,
},
{ type: 'separator' },
{ role: 'quit', label: 'Quit Sit Up' },
]);
tray.setToolTip('Sit Up');
tray.setContextMenu(contextMenu);
}
function openPreferencesWindow() {
if (popupWindow) {
popupWindow.close();
}
// Only one preferences window at a time
if (BrowserWindow.getAllWindows().some(w => w.getTitle() === 'Preferences')) {
return;
}
const prefWindow = new BrowserWindow({
width: 400,
height: 450,
resizable: false,
movable: true,
skipTaskbar: false,
transparent: false,
center: true,
fullscreenable: false,
alwaysOnTop: true,
title: 'Situp',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
prefWindow.removeMenu();
prefWindow.loadFile(path.join(__dirname, 'preferences.html'));
prefWindow.webContents.on('did-finish-load', () => {
prefWindow.webContents.send('load-config', config);
});
const { ipcMain } = require('electron');
ipcMain.once('save-config', (event, newConfig) => {
config = { ...config, ...newConfig, firstRun: false };
saveConfig(config);
prefWindow.close();
if (intervalId) clearInterval(intervalId);
startReminder();
setRunOnStartup(config.runOnStartup);
});
}
function setRunOnStartup(enabled) {
app.setLoginItemSettings({
openAtLogin: !!enabled,
path: process.execPath,
args: process.argv.slice(1),
});
}
function showPopup() {
if (popupWindow) {
return;
}
if (isDoNotDisturb()) {
return; // Respect DND/Focus Mode
}
const height = calculateHeight(config.width);
let windowOptions = {
width: config.width,
height,
frame: false,
alwaysOnTop: true,
resizable: false,
movable: false,
skipTaskbar: true,
transparent: true,
opacity: config.opacity, // Use configured opacity
backgroundColor: '#00000000',
center: true,
fullscreenable: false,
focusable: false,
title: 'Sit Up',
hasShadow: false,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enablePreferredSizeMode: false,
},
};
if (process.platform === 'darwin') {
windowOptions.alwaysOnTop = true;
windowOptions.alwaysOnTopLevel = 'screen-saver';
} else if (process.platform === 'win32') {
windowOptions.alwaysOnTop = true;
windowOptions.alwaysOnTopLevel = 'pop-up-menu';
}
popupWindow = new BrowserWindow(windowOptions);
popupWindow.removeMenu();
popupWindow.webContents.setAudioMuted(true);
popupWindow.setContentProtection(true);
popupWindow.setIgnoreMouseEvents(true, { forward: true });
popupWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
// Double-lock focus prevention
popupWindow.setFocusable(false);
// Load content and show window when ready
popupWindow.loadFile(path.join(__dirname, 'popup.html'));
popupWindow.webContents.on('did-finish-load', () => {
// Show window without activating it
if (process.platform === 'darwin' || process.platform === 'win32') {
popupWindow.showInactive();
} else {
popupWindow.show();
}
});
popupWindow.on('closed', () => {
popupWindow = null;
});
setTimeout(() => {
if (popupWindow) {
popupWindow.close();
}
}, 3000);
}
async function startReminder() {
if (!app.isReady()) {
await app.whenReady();
}
if (intervalId) clearInterval(intervalId);
if (config.firstRun) {
openPreferencesWindow();
return;
}
showPopup();
intervalId = setInterval(showPopup, config.interval * 60 * 1000);
}
// Parse CLI args for --opts or --preferences
const openPrefsArg = process.argv.includes('--opts') || process.argv.includes('--preferences');
console.log({openPrefsArg});
app.whenReady().then(() => {
if (process.platform === 'darwin') {
app.setName('Sit Up');
// Hide dock by default since we're a menubar app
app.dock.hide();
// Only show dock when windows are open
app.on('browser-window-created', () => {
app.dock.show();
});
app.on('window-all-closed', () => {
// Hide dock when all windows are closed
if (process.platform === 'darwin') {
app.dock.hide();
}
});
}
// Set the app icon for all platforms
const iconPath = path.join(__dirname, 'icons', 'logo.png');
if (process.platform === 'darwin') {
app.dock.setIcon(nativeImage.createFromPath(iconPath));
}
// App menu for Preferences
const template = [
{
label: 'Sit Up',
submenu: [
{
label: 'Preferences...',
click: openPreferencesWindow,
},
{ role: 'quit' },
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
createTray();
setRunOnStartup(config.runOnStartup);
if (openPrefsArg) {
openPreferencesWindow();
return;
}
startReminder();
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
});
// Handle Ctrl+C in terminal
process.on('SIGINT', () => {
if (intervalId) clearInterval(intervalId);
app.quit();
});