Skip to content

feat: add customizable tray menu with template-based display#96

Open
Serdnaley wants to merge 1 commit into
solidtime-io:mainfrom
Serdnaley:feat/tray-menu
Open

feat: add customizable tray menu with template-based display#96
Serdnaley wants to merge 1 commit into
solidtime-io:mainfrom
Serdnaley:feat/tray-menu

Conversation

@Serdnaley

@Serdnaley Serdnaley commented Apr 2, 2026

Copy link
Copy Markdown

Summary

Adds a colored project and template of the tray menu in settings. Still not validated on all platforms and a bit risky to merge it yet I guess, but may be someone can help me checking it on other OS and leave some feedback so I can adjust it for the main version.

telegram-cloud-photo-size-2-5364015812127167276-y
telegram-cloud-photo-size-2-5364015812127167275-y

  • Replaces the simple timer-only tray title with a canvas-rendered display that supports configurable templates
  • Adds @napi-rs/canvas for native tray icon + text compositing, enabling colored project names directly in the menu bar / system tray
  • Introduces a trayTemplate setting with placeholders: {hours}, {minutes}, {seconds}, {project}, {project_colored}, {description}, {task}
  • Adds a live preview and template editor in the Settings page
  • Supports dark/light mode with proper text colors

Changes

File Description
src/main/tray.ts Canvas-based tray rendering with template parsing, segment coloring, and icon compositing
src/main/settings.ts New trayTemplate setting with persistence
src/renderer/src/pages/SettingsPage.vue Template editor with live preview
src/renderer/src/components/MainTimeEntryTable.vue Pass project/task info to tray via IPC
src/renderer/src/utils/settings.ts Reactive trayTemplate binding
src/preload/main.ts, src/preload/interface.d.ts New updateTrayTemplate IPC channel
src/renderer/src/utils/tray.ts Extended type to include project/task data
package.json Added @napi-rs/canvas dependency

Screenshots

The tray now renders like: 🕐 01:23:45 – Project Name with the project name in its assigned color.

(Will add actual screenshots if requested)

Test plan

  • Verify tray displays timer with default template on macOS
  • Verify tray displays timer on Windows/Linux with dark and light themes
  • Change template in Settings and confirm tray updates live
  • Remove project placeholder from template and confirm timer-only display works
  • Stop timer and confirm tray returns to inactive icon
  • Restart app and confirm template persists from settings

Replace the simple timer-only tray title with a canvas-rendered display
that supports configurable templates with placeholders for time, project
name (with color), task, and description.

- Add @napi-rs/canvas for native tray icon + text compositing
- Add trayTemplate setting with configurable placeholders:
  {hours}, {minutes}, {seconds}, {project}, {project_colored},
  {description}, {task}
- Pass project/task info from renderer to main process via IPC
- Add live preview and template editor in Settings page
- Support dark/light mode with proper text colors
@CLAassistant

CLAassistant commented Apr 2, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Onatcer

Onatcer commented May 11, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution.

I know code is cheap now but please, next time you add a feature, make an issue or discussion first, where you explain and outline what you want to do, instead of submitting a PR. Revamping this after the fact is more time-intensive and just makes this project harder to maintain for us.

I think customizability for the tray menu is nice; however, I don't like the templating. That works for tech-savvy users, but it will just confuse others. I'd prefer a few options that are preconfigured and selectable via the dropdown. As far as I understand it, this will not work on Windows (because icons are limited to squares here) and probably not on Linux (there are so many different desktop environments and at least some of them only do squares). So really this can only be a macOS-only feature, realistically.

the current implementation of this PR also changes the default, please keep it at time only (hh:mm). For the other options i think:

  • Time (hh:mm)
  • Time + project (hh:mm - Project name)
  • Time + colored project (hh:mm - colored project name)
  • Time + description (hh:mm - description)

should cover most use cases. The implementation here should be a lot simpler, the custom template parsing adds uncecessary code complexity here. Also for time formatting the ui package exposes time utilities that are used in various places (and dayjs where it is not possible).

Project names and descriptions can get very long, there should be some sort of truncation (maybe 20-30 chars) otherwise it will just get very big (i think macos does truncate with setTitle() but only when there are other items copeting for the space).

Pulling in a native binary dependency which is as far as i can tell only used for the colored text, and rerender an image for that is imo a trade off thats not worth it. macOS supports a limited color palette via .setTitle(...) https://github.com/electron/electron/blob/main/shell/browser/ui/cocoa/NSString%2BANSI.mm electron/electron#29287 I'm fine with having the project color mapped to the closest one (the ui package uses chroma-js for color calculation stuff), or just dropping the support for colors here. the setTitle for macOs should keep fontType: 'monospacedDigit.

Also there are multiple comments removed in tray.ts for no apparent reason.

@Serdnaley

Serdnaley commented May 11, 2026

Copy link
Copy Markdown
Author

@Onatcer Hey! Thanks for your review. I made this PR just as a proposal, I made it for my use case and for some of my friends, we just use local build for now. This is version that worked for us, but I'm happy discussing and adjusting it for general purpose.

Templating - agree, we can drop it for sure. We can do several hardcoded templates or just simple check-box list with options e.g. include seconds, project, task. I really like seconds in the tray, and I guess many people might like it as well, I just got used to it after using Clockify.
Default - for sure, I just picked up what worked for me, I can do it same as it was, np.

MacOS only - most probably, I don't have an option to test it out on other platforms, so designed it for macos as a first version. Anyway I guess the only way to make it work on different systems is making different adapters and each of them will have restrictions so we can start with macos and port to other platforms later if possible. This is for you to decide.

Truncate - agree.

Skipping native binary - it was my original implementation and it actually worked, but it only supports ANSI colors which don't really cover the most of colors we have and it looks really weird. You can try it out, I guess you'll also not like it. I'll include the snippet for color mapping I used.

For the chroma-js - it doesn't have ANSI color mapping, I made a custom script comparing colors to the closest ones visibly and mathematically. Color support is essential for me as it gives me a quick visual understanding if timer is running and what project selected, it's much easier to see it by color, to not read it. In my case I have Personal and primary full-time work and I don't want to mix them up, which I did really often before adding the color.

Comments - I can cleanup the changes and get them back, np.

Let me know if you have some inputs here, let's discuss it and I can do it. Again, it's just a concept, that's why I said "leave some feedback so I can adjust it for the main version".

P.S. I'll also tag some guy in Discord who was interested in the same feature.

Code snippet mentioned before: native to ANSI colors mapping ```typescript const ansiColors = [ { name: 'Black', ansi: '\x1b[30m', hex: '#000000' }, { name: 'Red', ansi: '\x1b[31m', hex: '#ff0000' }, { name: 'Green', ansi: '\x1b[32m', hex: '#00ff00' }, { name: 'Yellow', ansi: '\x1b[33m', hex: '#ffff00' }, { name: 'Blue', ansi: '\x1b[34m', hex: '#5c5cff' }, { name: 'Magenta', ansi: '\x1b[35m', hex: '#ff00ff' }, { name: 'Cyan', ansi: '\x1b[36m', hex: '#00ffff' }, { name: 'White', ansi: '\x1b[37m', hex: '#ffffff' },
{ name: 'Black Bold', ansi: '\x1b[1;30m', hex: '#7f7f7f' },
{ name: 'Red Bold', ansi: '\x1b[1;31m', hex: '#cd0000' },
{ name: 'Green Bold', ansi: '\x1b[1;32m', hex: '#00cd00' },
{ name: 'Yellow Bold', ansi: '\x1b[1;33m', hex: '#cdcd00' },
{ name: 'Blue Bold', ansi: '\x1b[1;34m', hex: '#0000ee' },
{ name: 'Magenta Bold', ansi: '\x1b[1;35m', hex: '#cd00cd' },
{ name: 'Cyan Bold', ansi: '\x1b[1;36m', hex: '#00cdcd' },
{ name: 'White Bold', ansi: '\x1b[1;37m', hex: '#e5e5e5' },

]
const ansiColorsMap = new Map(ansiColors.map(({ ansi, hex }) => [hex, ansi]))

const solidtimeColors = [
{ native: '#ef5350', ansi: '#cd0000' },
{ native: '#ec407a', ansi: '#cd0000' },
{ native: '#ab47bc', ansi: '#cd00cd' },
{ native: '#7e57c2', ansi: '#cd00cd' },
{ native: '#5c6bc0', ansi: '#5c5cff' },
{ native: '#42a5f5', ansi: '#5c5cff' },
{ native: '#29b6f6', ansi: '#00cdcd' },
{ native: '#26c6da', ansi: '#00cdcd' },
{ native: '#26a69a', ansi: '#00cdcd' },
{ native: '#66bb6a', ansi: '#00cd00' },
{ native: '#9ccc65', ansi: '#00cd00' },
{ native: '#d4e157', ansi: '#ffff00' },
{ native: '#ffee58', ansi: '#ffff00' },
{ native: '#ffca28', ansi: '#cdcd00' },
{ native: '#ffa726', ansi: '#cdcd00' },
{ native: '#ff7043', ansi: '#cd0000' },
{ native: '#8d6e63', ansi: '#cd0000' },
{ native: '#bdbdbd', ansi: '#7f7f7f' },
{ native: '#78909c', ansi: '#7f7f7f' },
]
const hexToAnsiMap: Map<string, string | null> = new Map(
solidtimeColors.map(({ native, ansi }) => [native, ansiColorsMap.get(ansi) ?? null])
)

// Later in the code
const duration = dayjs.duration(dayjs().diff(dayjs(timeEntry.start), 'second'), 's')
const hours = Math.floor(duration.asHours()).toString().padStart(2, '0')
const minutes = duration.minutes().toString().padStart(2, '0')
const seconds = duration.seconds().toString().padStart(2, '0')
const formattedTime = ${hours}:${minutes}:${seconds}
let projectName = timeEntry.project || 'No project'

const ansiColorCode = hexToAnsiMap.get(timeEntry.projectColor)
projectName = ansiColorCode ? ${ansiColorCode}${projectName}\x1b[0m : projectName

const formattedDuration = ${formattedTime} – ${projectName}
tray.setTitle(formattedDuration, { fontType: 'monospacedDigit' })

</details>

@Onatcer

Onatcer commented May 12, 2026

Copy link
Copy Markdown
Contributor

@Serdnaley I think having the time format configurable to either hh:mm or hh:mm:ss is a improvement. I'm not sure what other apps have as a default format, I'm fine with just hh:mm:ss too, my primary issue was with the project title in the default.

Ad MacOS only: yeah i don't even think most would expose reasonable APIs here, just make it macOS only.

As for the color: My honest take is i'd rather have no color than having a custom canvas renderer for this, but I recognise that having some sort of colour coding is useful. Maybe we can go a simpler path by having the text native via .setTitle but render a simple dot in the project color on demand and use it as the tray icon/image. Computing and caching a PNG circle/dot in the project colour is a lot simpler and can be reasonably done with either an already installed dependency (zlib) or a smaller library (fast-png). Both are fine for me. Would that work for your use case too?

@Serdnaley

Copy link
Copy Markdown
Author

@Onatcer that might be a deal, I'll play with it a bit when have a free time (not nearest weeks I guess), I guess some dot or smth similar should work as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants