fix(electron): respect tray title settings #7823 (#8097)

Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
This commit is contained in:
felix bear 2026-06-08 19:07:09 +09:00 committed by GitHub
parent cb53bb97ff
commit 376e068412
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 291 additions and 7 deletions

View file

@ -12,6 +12,17 @@ Note: Differences between the Web app and the Desktop app are specified in [[3.0
#### global-settings.Misc-Settings
- **Show current countdown in the tray / status menu (macOS only)** — Shows the
active timer or countdown in the desktop status menu title. When this is
disabled and current-task display is enabled, the title can show the current
task instead.
#### global-settings.Tasks
- **Show current task in the tray / status menu (macOS/Windows only)** — Allows
the desktop tray or status menu to show the active task. On macOS, the current
countdown setting takes priority while it is enabled.
#### global-settings.Short-Syntax
#### global-settings.Idle-Handling

View file

@ -0,0 +1,213 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const Module = require('node:module');
require('ts-node/register/transpile-only');
const originalModuleLoad = Module._load;
const appControlModulePath = path.resolve(__dirname, 'ipc-handlers/app-control.ts');
let ipcHandlers;
let nextIsLocked;
let sharedState;
let refreshIndicatorCalls;
let localRestApiConfig;
const resetModule = () => {
delete require.cache[appControlModulePath];
};
const installMocks = () => {
Module._load = function patchedLoad(request, parent, isMain) {
if (request === 'electron') {
return {
app: {
exit: () => {},
relaunch: () => {},
},
ipcMain: {
on: (eventName, handler) => {
ipcHandlers.set(eventName, handler);
},
},
};
}
if (request === '../shared-with-frontend/ipc-events.const') {
return {
IPC: {
SHUTDOWN_NOW: 'SHUTDOWN_NOW',
EXIT: 'EXIT',
RELAUNCH: 'RELAUNCH',
OPEN_DEV_TOOLS: 'OPEN_DEV_TOOLS',
RELOAD_MAIN_WIN: 'RELOAD_MAIN_WIN',
TRANSFER_SETTINGS_TO_ELECTRON: 'TRANSFER_SETTINGS_TO_ELECTRON',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
SHOW_OR_FOCUS: 'SHOW_OR_FOCUS',
LOCK_SCREEN: 'LOCK_SCREEN',
SET_PROGRESS_BAR: 'SET_PROGRESS_BAR',
FLASH_FRAME: 'FLASH_FRAME',
},
};
}
if (request === '../main-window') {
return {
getWin: () => ({
webContents: {
openDevTools: () => {},
},
reload: () => {},
setProgressBar: () => {},
flashFrame: () => {},
once: () => {},
}),
};
}
if (request === '../various-shared') {
return {
quitApp: () => {},
showOrFocus: () => {},
};
}
if (request === '../shared-state') {
return {
getIsLocked: () => nextIsLocked,
setIsMinimizeToTray: (value) => {
sharedState.isMinimizeToTray = value;
},
setIsTrayShowCurrentTask: (value) => {
sharedState.isTrayShowCurrentTask = value;
},
setIsTrayShowCurrentCountdown: (value) => {
sharedState.isTrayShowCurrentCountdown = value;
},
};
}
if (request === '../indicator') {
return {
refreshIndicator: () => {
refreshIndicatorCalls += 1;
},
};
}
if (request === '../lockscreen') {
return {
lockscreen: () => {},
};
}
if (request === '../error-handler-with-frontend-inform') {
return {
errorHandlerWithFrontendInform: () => {},
};
}
if (request === '../../src/app/features/config/global-config.model') {
return {};
}
if (request === '../simple-store') {
return {
saveSimpleStore: async () => {},
};
}
if (request === '../shared-with-frontend/simple-store.const') {
return {
SimpleStoreKey: {
IS_USE_CUSTOM_WINDOW_TITLE_BAR: 'isUseCustomWindowTitleBar',
},
};
}
if (request === '../local-rest-api') {
return {
updateLocalRestApiConfig: (cfg) => {
localRestApiConfig = cfg;
},
};
}
return originalModuleLoad.call(this, request, parent, isMain);
};
};
const loadAppControlModule = () => {
resetModule();
return require(appControlModulePath);
};
test.beforeEach(() => {
ipcHandlers = new Map();
nextIsLocked = false;
sharedState = {
isMinimizeToTray: undefined,
isTrayShowCurrentTask: undefined,
isTrayShowCurrentCountdown: undefined,
};
refreshIndicatorCalls = 0;
localRestApiConfig = undefined;
installMocks();
});
test.afterEach(() => {
Module._load = originalModuleLoad;
resetModule();
});
test('settings update reads current task tray setting from tasks config', async () => {
const { initAppControlIpc } = loadAppControlModule();
initAppControlIpc();
const updateSettings = ipcHandlers.get('TRANSFER_SETTINGS_TO_ELECTRON');
assert.equal(typeof updateSettings, 'function');
const cfg = {
tasks: {
isTrayShowCurrent: false,
},
misc: {
isMinimizeToTray: true,
isTrayShowCurrentTask: true,
isTrayShowCurrentCountdown: false,
},
};
await updateSettings({}, cfg);
assert.equal(sharedState.isMinimizeToTray, true);
assert.equal(sharedState.isTrayShowCurrentTask, false);
assert.equal(sharedState.isTrayShowCurrentCountdown, false);
assert.equal(refreshIndicatorCalls, 1);
assert.equal(localRestApiConfig, cfg);
});
test('settings update falls back to legacy misc tray task setting', async () => {
const { initAppControlIpc } = loadAppControlModule();
initAppControlIpc();
const updateSettings = ipcHandlers.get('UPDATE_SETTINGS');
assert.equal(typeof updateSettings, 'function');
await updateSettings(
{},
{
misc: {
isMinimizeToTray: false,
isTrayShowCurrentTask: true,
isTrayShowCurrentCountdown: true,
},
},
);
assert.equal(sharedState.isTrayShowCurrentTask, true);
assert.equal(sharedState.isTrayShowCurrentCountdown, true);
assert.equal(refreshIndicatorCalls, 1);
});

View file

@ -13,9 +13,13 @@ const indicatorModulePath = path.resolve(__dirname, 'indicator.ts');
let createdTrayArgs;
let createdFromPath = [];
let traySetImageCalls = [];
let traySetTitleCalls = [];
let traySetToolTipCalls = [];
let ipcHandlers = new Map();
let nextNativeImageIsEmpty = false;
let beforeQuitHandler = () => {};
let mockIsTrayShowCurrentTask = false;
let mockIsTrayShowCurrentCountdown = false;
const resetModule = () => {
delete require.cache[indicatorModulePath];
@ -41,9 +45,13 @@ const installMocks = () => {
traySetImageCalls.push(image);
}
setTitle() {}
setTitle(title) {
traySetTitleCalls.push(title);
}
setToolTip() {}
setToolTip(title) {
traySetToolTipCalls.push(title);
}
destroy() {}
}
@ -68,6 +76,7 @@ const installMocks = () => {
iconPath,
kind: 'native-image',
isEmpty: () => nextNativeImageIsEmpty,
setTemplateImage: () => {},
};
},
},
@ -95,8 +104,8 @@ const installMocks = () => {
if (request === './shared-state') {
return {
getIsTrayShowCurrentTask: () => false,
getIsTrayShowCurrentCountdown: () => false,
getIsTrayShowCurrentTask: () => mockIsTrayShowCurrentTask,
getIsTrayShowCurrentCountdown: () => mockIsTrayShowCurrentCountdown,
};
}
@ -136,9 +145,13 @@ test.beforeEach(() => {
createdTrayArgs = [];
createdFromPath = [];
traySetImageCalls = [];
traySetTitleCalls = [];
traySetToolTipCalls = [];
ipcHandlers = new Map();
nextNativeImageIsEmpty = false;
beforeQuitHandler = () => {};
mockIsTrayShowCurrentTask = false;
mockIsTrayShowCurrentCountdown = false;
Object.defineProperty(process, 'platform', {
configurable: true,
@ -216,3 +229,40 @@ test('initIndicator falls back to icon path if NativeImage creation is empty', (
assert.equal(typeof createdTrayArgs[0][0], 'string');
assert.match(createdTrayArgs[0][0], /\/icons\/indicator\/stopped-d\.png$/);
});
test('tray title shows the task title when countdown display is disabled', () => {
mockIsTrayShowCurrentTask = true;
mockIsTrayShowCurrentCountdown = false;
const { initIndicator } = loadIndicatorModule();
initIndicator({
showApp: () => {},
quitApp: () => {},
ICONS_FOLDER: '/icons/',
forceDarkTray: false,
app: {
on: () => {},
},
});
const currentTaskUpdated = ipcHandlers.get('CURRENT_TASK_UPDATED');
assert.equal(typeof currentTaskUpdated, 'function');
currentTaskUpdated(
{},
{
id: 'T1',
title: 'Write release notes',
timeSpent: 5 * 60000,
timeEstimate: 25 * 60000,
},
false,
0,
false,
0,
undefined,
);
assert.equal(traySetTitleCalls.at(-1), 'Write release notes');
assert.equal(traySetToolTipCalls.at(-1), 'Write release notes');
});

View file

@ -142,6 +142,12 @@ export const ensureIndicator = (): Tray | undefined => {
return tray;
};
export const refreshIndicator = (): void => {
if (tray) {
syncTray(tray);
}
};
const createTray = (): Tray => {
const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
const trayIconPath = DIR + `stopped${suf}`;
@ -431,6 +437,7 @@ const syncTray = (tr: Tray): void => {
tr.setToolTip('');
}
}
_lastTrayMsg = trayMsg;
if (_lastCurrentTask?.title && !_lastIsFocusModeEnabled) {
const progress = _lastCurrentTask.timeEstimate
@ -497,8 +504,7 @@ function createIndicatorMessage(
return timeStr;
}
// Fallback if no countdown is supposed to be shown, but we have a running task
return getProgressMessage(task.timeSpent);
return task.title;
}
return '';

View file

@ -10,6 +10,7 @@ import {
setIsTrayShowCurrentTask,
setIsTrayShowCurrentCountdown,
} from '../shared-state';
import { refreshIndicator } from '../indicator';
import { lockscreen } from '../lockscreen';
import { errorHandlerWithFrontendInform } from '../error-handler-with-frontend-inform';
import { GlobalConfigState } from '../../src/app/features/config/global-config.model';
@ -41,8 +42,11 @@ export const initAppControlIpc = (): void => {
const updateSettings = async (ev: any, cfg: GlobalConfigState): Promise<void> => {
setIsMinimizeToTray(cfg.misc.isMinimizeToTray);
setIsTrayShowCurrentTask(!!cfg.misc.isTrayShowCurrentTask);
setIsTrayShowCurrentTask(
cfg.tasks?.isTrayShowCurrent ?? !!cfg.misc.isTrayShowCurrentTask,
);
setIsTrayShowCurrentCountdown(!!cfg.misc.isTrayShowCurrentCountdown);
refreshIndicator();
updateLocalRestApiConfig(cfg);
if (cfg.misc.isUseCustomWindowTitleBar !== undefined) {