Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions build/installer.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
; NSIS additions for the Windows installer (included via electron-builder.yml
; `nsis.include`). Adds the Explorer folder context menu — right-click a folder
; (or a folder window's background) → "Open in GitGrove", which launches
; `GitGrove.exe --repo "<folder>"`; the single-instance lock routes that into a
; running instance, which focuses or opens the repo in a window.
;
; Registered under HKCU to match the per-user install (`nsis.perMachine: false`)
; — no elevation needed, and the uninstaller can always remove what it wrote.
; `%V` is Explorer's verbatim selected-folder placeholder (works for both the
; selected folder and the window background, unlike %1).

!macro customInstall
WriteRegStr HKCU "Software\Classes\Directory\shell\GitGrove" "" "Open in GitGrove"
WriteRegStr HKCU "Software\Classes\Directory\shell\GitGrove" "Icon" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
WriteRegStr HKCU "Software\Classes\Directory\shell\GitGrove\command" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" --repo "%V"'
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\GitGrove" "" "Open in GitGrove"
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\GitGrove" "Icon" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\GitGrove\command" "" '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" --repo "%V"'
!macroend

!macro customUnInstall
DeleteRegKey HKCU "Software\Classes\Directory\shell\GitGrove"
DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\GitGrove"
!macroend
14 changes: 14 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,24 @@ nsis:
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
# Explorer folder context menu ("Open in GitGrove" → `--repo "<folder>"`),
# registered per-user at install and removed at uninstall.
include: build/installer.nsh

linux:
icon: build/icon.png
category: Development
artifactName: ${productName}-${version}-Linux-${arch}.${ext}
# Launcher right-click → "New Window" (GNOME/KDE read [Desktop Action]s from
# the .desktop file — the platform has no runtime API, so unlike the macOS
# dock menu / Windows Jump List this stays static: no recents list here).
# `AppRun --no-sandbox` mirrors the main Exec electron-builder writes for
# AppImage; integrators (AppImageLauncher/appimaged) rewrite it to the
# AppImage's real path. `--new-window` is routed by the single-instance lock.
desktop:
desktopActions:
NewWindow:
Name: New Window
Exec: AppRun --no-sandbox --new-window
target:
- AppImage
8 changes: 8 additions & 0 deletions src/main/app-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { app } from 'electron'

export const REPO_URL = 'https://github.com/danipen/gitgrove'

/**
* Windows AppUserModelID — must equal electron-builder's `appId`, which the
* installer stamps onto the Start-menu/desktop shortcuts. Setting the same ID
* on the process makes the taskbar group our windows under those shortcuts,
* which is also what attaches the Jump List (app-shortcuts.ts) to them.
*/
export const APP_USER_MODEL_ID = 'software.gitgrove.app'

export function appInfo(): AppInfo {
return {
name: app.getName(),
Expand Down
82 changes: 82 additions & 0 deletions src/main/app-shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// OS launcher shortcuts: recent repositories plus a "New Window" entry, one
// right-click away on the app's dock / taskbar icon — even before the app is
// focused (or, on macOS and via pinned Jump Lists, before it is *running*).
//
// Per-platform reality check:
// - macOS: recents ride the OS-managed recent-documents list
// (`app.addRecentDocument` in index.ts, stored per bundle id by the system)
// — the Dock itself renders that section in the icon's menu whether or not
// GitGrove is running, and clicks come back as 'open-file' events. Only the
// "New Window" item needs the custom (running-only) dock menu here.
// - Windows: Jump List items can only *launch a program*, so each entry
// relaunches GitGrove with `--repo <path>` (or `--new-window`); the
// single-instance lock routes that into the running instance, which opens
// or focuses the repo (see 'second-instance' in index.ts). Rebuilt whenever
// the recents change; the pinned icon keeps it while the app is closed.
// - Linux: launchers only read static `[Desktop Action]`s from the .desktop
// file — no runtime API — so it gets a packaged "New Window" action only
// (electron-builder.yml) and nothing to do here.

import { app, Menu } from 'electron'
import { getRecentRepos } from './store'

// Windows renders Jump Lists as a tall dedicated flyout where ~8 entries is
// the platform's customary depth.
const JUMP_LIST_RECENTS = 8

/** What the shortcuts need from the app shell. */
export interface AppShortcutActions {
/** Open a fresh window on the welcome screen. */
newWindow(): void
}

/**
* Rebuild the platform's launcher shortcuts from the current recents. Cheap
* (one small template) and idempotent — safe to call on every recents write.
*/
export function refreshAppShortcuts(actions: AppShortcutActions): void {
if (process.platform === 'darwin') {
// Recents are the OS's section (see module comment) — adding them here too
// would show every repo twice in the same menu.
app.dock?.setMenu(
Menu.buildFromTemplate([{ label: 'New Window', click: () => actions.newWindow() }])
)
}
if (process.platform === 'win32') refreshJumpList()
}

function refreshJumpList(): void {
// A Jump List entry launches `program` with `args` — in dev that program is
// the generic Electron binary, which needs the app directory as its first
// argument to become GitGrove.
const devAppArg = app.isPackaged ? '' : `"${app.getAppPath()}" `
const task = (args: string, title: string, description: string) =>
({
type: 'task',
program: process.execPath,
args: `${devAppArg}${args}`,
title,
description,
iconPath: process.execPath,
iconIndex: 0
}) as const

// Folders that still exist, newest first — same filter as the repo switcher.
const recents = getRecentRepos()
.filter((repo) => !repo.missing)
.slice(0, JUMP_LIST_RECENTS)

// setJumpList can fail (e.g. items the OS rejects) — a launcher shortcut is
// a convenience, never worth surfacing an error for, so ignore the outcome.
app.setJumpList([
{
type: 'custom',
name: 'Recent Repositories',
items: recents.map((repo) => task(`--repo "${repo.path}"`, repo.name, repo.path))
},
{
type: 'tasks',
items: [task('--new-window', 'New Window', 'Open a new GitGrove window')]
}
])
}
14 changes: 13 additions & 1 deletion src/main/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'bun:test'
import { resolveStartupRepo } from './cli'
import { hasNewWindowFlag, resolveStartupRepo } from './cli'

const noEnv: NodeJS.ProcessEnv = {}

Expand Down Expand Up @@ -38,3 +38,15 @@ describe('resolveStartupRepo', () => {
expect(resolveStartupRepo([], { GITGROVE_OPEN_REPO: ' ' })).toBeNull()
})
})

describe('hasNewWindowFlag', () => {
test('spots --new-window anywhere in argv', () => {
expect(hasNewWindowFlag(['electron', '.', '--new-window'])).toBe(true)
expect(hasNewWindowFlag(['gitgrove', '--new-window', 'ignored'])).toBe(true)
})

test('requires the exact flag', () => {
expect(hasNewWindowFlag(['electron', '.'])).toBe(false)
expect(hasNewWindowFlag(['electron', '--new-window=1'])).toBe(false)
})
})
11 changes: 11 additions & 0 deletions src/main/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// open the wrong thing. An explicit flag keeps the intent unambiguous.

const REPO_FLAG = '--repo'
const NEW_WINDOW_FLAG = '--new-window'

/**
* Resolve the repository to open on startup from the process argv and env, or
Expand All @@ -34,3 +35,13 @@ export function resolveStartupRepo(argv: readonly string[], env: NodeJS.ProcessE
const fromEnv = env.GITGROVE_OPEN_REPO
return fromEnv && fromEnv.trim() !== '' ? fromEnv : null
}

/**
* Whether the invocation asks for a fresh window (`--new-window`) — used by
* launcher shortcuts (the Windows Jump List's "New Window" task, the Linux
* desktop action) whose relaunch is routed into the running instance by the
* single-instance lock.
*/
export function hasNewWindowFlag(argv: readonly string[]): boolean {
return argv.includes(NEW_WINDOW_FLAG)
}
Loading
Loading