1
0
mirror of https://github.com/desktop/desktop synced 2024-06-30 22:54:41 +00:00

Merge pull request #18096 from lhvy/alacritty

Add support for multiple macOS shell bundle IDs
This commit is contained in:
Sergio Padrino 2024-03-13 15:37:50 +01:00 committed by GitHub
commit 329b6c2e93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 142 additions and 121 deletions

View File

@ -1,8 +1,8 @@
import { spawn, ChildProcess } from 'child_process'
import { assertNever } from '../fatal-error'
import { IFoundShell } from './found-shell'
import appPath from 'app-path'
import { parseEnumValue } from '../enum'
import { FoundShell } from './shared'
export enum Shell {
Terminal = 'Terminal',
@ -22,113 +22,142 @@ export function parse(label: string): Shell {
return parseEnumValue(Shell, label) ?? Default
}
function getBundleID(shell: Shell): string {
function getBundleIDs(shell: Shell): ReadonlyArray<string> {
switch (shell) {
case Shell.Terminal:
return 'com.apple.Terminal'
return ['com.apple.Terminal']
case Shell.iTerm2:
return 'com.googlecode.iterm2'
return ['com.googlecode.iterm2']
case Shell.Hyper:
return 'co.zeit.hyper'
return ['co.zeit.hyper']
case Shell.PowerShellCore:
return 'com.microsoft.powershell'
return ['com.microsoft.powershell']
case Shell.Kitty:
return 'net.kovidgoyal.kitty'
return ['net.kovidgoyal.kitty']
case Shell.Alacritty:
return 'org.alacritty'
return ['org.alacritty', 'io.alacritty']
case Shell.Tabby:
return 'org.tabby'
return ['org.tabby']
case Shell.WezTerm:
return 'com.github.wez.wezterm'
return ['com.github.wez.wezterm']
case Shell.Warp:
return 'dev.warp.Warp-Stable'
return ['dev.warp.Warp-Stable']
default:
return assertNever(shell, `Unknown shell: ${shell}`)
}
}
async function getShellPath(shell: Shell): Promise<string | null> {
const bundleId = getBundleID(shell)
try {
return await appPath(bundleId)
} catch (e) {
// `appPath` will raise an error if it cannot find the program.
return null
async function getShellInfo(
shell: Shell
): Promise<{ path: string; bundleID: string } | null> {
const bundleIds = getBundleIDs(shell)
for (const id of bundleIds) {
try {
const path = await appPath(id)
return { path, bundleID: id }
} catch (error) {
log.debug(
`Unable to locate ${shell} installation with bundle id ${id}`,
error
)
}
}
return null
}
export async function getAvailableShells(): Promise<
ReadonlyArray<IFoundShell<Shell>>
ReadonlyArray<FoundShell<Shell>>
> {
const [
terminalPath,
hyperPath,
iTermPath,
powerShellCorePath,
kittyPath,
alacrittyPath,
tabbyPath,
wezTermPath,
warpPath,
terminalInfo,
hyperInfo,
iTermInfo,
powerShellCoreInfo,
kittyInfo,
alacrittyInfo,
tabbyInfo,
wezTermInfo,
warpInfo,
] = await Promise.all([
getShellPath(Shell.Terminal),
getShellPath(Shell.Hyper),
getShellPath(Shell.iTerm2),
getShellPath(Shell.PowerShellCore),
getShellPath(Shell.Kitty),
getShellPath(Shell.Alacritty),
getShellPath(Shell.Tabby),
getShellPath(Shell.WezTerm),
getShellPath(Shell.Warp),
getShellInfo(Shell.Terminal),
getShellInfo(Shell.Hyper),
getShellInfo(Shell.iTerm2),
getShellInfo(Shell.PowerShellCore),
getShellInfo(Shell.Kitty),
getShellInfo(Shell.Alacritty),
getShellInfo(Shell.Tabby),
getShellInfo(Shell.WezTerm),
getShellInfo(Shell.Warp),
])
const shells: Array<IFoundShell<Shell>> = []
if (terminalPath) {
shells.push({ shell: Shell.Terminal, path: terminalPath })
const shells: Array<FoundShell<Shell>> = []
if (terminalInfo) {
shells.push({ shell: Shell.Terminal, ...terminalInfo })
}
if (hyperPath) {
shells.push({ shell: Shell.Hyper, path: hyperPath })
if (hyperInfo) {
shells.push({ shell: Shell.Hyper, ...hyperInfo })
}
if (iTermPath) {
shells.push({ shell: Shell.iTerm2, path: iTermPath })
if (iTermInfo) {
shells.push({ shell: Shell.iTerm2, ...iTermInfo })
}
if (powerShellCorePath) {
shells.push({ shell: Shell.PowerShellCore, path: powerShellCorePath })
if (powerShellCoreInfo) {
shells.push({ shell: Shell.PowerShellCore, ...powerShellCoreInfo })
}
if (kittyPath) {
const kittyExecutable = `${kittyPath}/Contents/MacOS/kitty`
shells.push({ shell: Shell.Kitty, path: kittyExecutable })
if (kittyInfo) {
const kittyExecutable = `${kittyInfo.path}/Contents/MacOS/kitty`
shells.push({
shell: Shell.Kitty,
path: kittyExecutable,
bundleID: kittyInfo.bundleID,
})
}
if (alacrittyPath) {
const alacrittyExecutable = `${alacrittyPath}/Contents/MacOS/alacritty`
shells.push({ shell: Shell.Alacritty, path: alacrittyExecutable })
if (alacrittyInfo) {
const alacrittyExecutable = `${alacrittyInfo.path}/Contents/MacOS/alacritty`
shells.push({
shell: Shell.Alacritty,
path: alacrittyExecutable,
bundleID: alacrittyInfo.bundleID,
})
}
if (tabbyPath) {
const tabbyExecutable = `${tabbyPath}/Contents/MacOS/Tabby`
shells.push({ shell: Shell.Tabby, path: tabbyExecutable })
if (tabbyInfo) {
const tabbyExecutable = `${tabbyInfo.path}/Contents/MacOS/Tabby`
shells.push({
shell: Shell.Tabby,
path: tabbyExecutable,
bundleID: tabbyInfo.bundleID,
})
}
if (wezTermPath) {
const wezTermExecutable = `${wezTermPath}/Contents/MacOS/wezterm`
shells.push({ shell: Shell.WezTerm, path: wezTermExecutable })
if (wezTermInfo) {
const wezTermExecutable = `${wezTermInfo.path}/Contents/MacOS/wezterm`
shells.push({
shell: Shell.WezTerm,
path: wezTermExecutable,
bundleID: wezTermInfo.bundleID,
})
}
if (warpPath) {
const warpExecutable = `${warpPath}/Contents/MacOS/stable`
shells.push({ shell: Shell.Warp, path: warpExecutable })
if (warpInfo) {
const warpExecutable = `${warpInfo.path}/Contents/MacOS/stable`
shells.push({
shell: Shell.Warp,
path: warpExecutable,
bundleID: warpInfo.bundleID,
})
}
return shells
}
export function launch(
foundShell: IFoundShell<Shell>,
foundShell: FoundShell<Shell>,
path: string
): ChildProcess {
if (foundShell.shell === Shell.Kitty) {
@ -158,7 +187,6 @@ export function launch(
// the working directory, followed by the path.
return spawn(foundShell.path, ['start', '--cwd', path])
} else {
const bundleID = getBundleID(foundShell.shell)
return spawn('open', ['-b', bundleID, path])
return spawn('open', ['-b', foundShell.bundleID, path])
}
}

View File

@ -1,5 +0,0 @@
export interface IFoundShell<T> {
readonly shell: T
readonly path: string
readonly extraArgs?: string[]
}

View File

@ -1,8 +1,8 @@
import { spawn, ChildProcess } from 'child_process'
import { assertNever } from '../fatal-error'
import { IFoundShell } from './found-shell'
import { parseEnumValue } from '../enum'
import { pathExists } from '../../ui/lib/path-exists'
import { FoundShell } from './shared'
export enum Shell {
Gnome = 'GNOME Terminal',
@ -64,7 +64,7 @@ function getShellPath(shell: Shell): Promise<string | null> {
}
export async function getAvailableShells(): Promise<
ReadonlyArray<IFoundShell<Shell>>
ReadonlyArray<FoundShell<Shell>>
> {
const [
gnomeTerminalPath,
@ -96,7 +96,7 @@ export async function getAvailableShells(): Promise<
getShellPath(Shell.Kitty),
])
const shells: Array<IFoundShell<Shell>> = []
const shells: Array<FoundShell<Shell>> = []
if (gnomeTerminalPath) {
shells.push({ shell: Shell.Gnome, path: gnomeTerminalPath })
}
@ -153,7 +153,7 @@ export async function getAvailableShells(): Promise<
}
export function launch(
foundShell: IFoundShell<Shell>,
foundShell: FoundShell<Shell>,
path: string
): ChildProcess {
const shell = foundShell.shell

View File

@ -3,13 +3,22 @@ import { ChildProcess } from 'child_process'
import * as Darwin from './darwin'
import * as Win32 from './win32'
import * as Linux from './linux'
import { IFoundShell } from './found-shell'
import { ShellError } from './error'
import { pathExists } from '../../ui/lib/path-exists'
export type Shell = Darwin.Shell | Win32.Shell | Linux.Shell
export type FoundShell = IFoundShell<Shell>
export type FoundShell<T extends Shell> = {
readonly shell: T
readonly path: string
readonly extraArgs?: string[]
} & (T extends Darwin.Shell
? {
readonly bundleID: string
}
: {})
type AnyFoundShell = FoundShell<Shell>
/** The default shell. */
export const Default = (function () {
@ -22,7 +31,7 @@ export const Default = (function () {
}
})()
let shellCache: ReadonlyArray<FoundShell> | null = null
let shellCache: ReadonlyArray<AnyFoundShell> | null = null
/** Parse the label into the specified shell type. */
export function parse(label: string): Shell {
@ -40,7 +49,9 @@ export function parse(label: string): Shell {
}
/** Get the shells available for the user. */
export async function getAvailableShells(): Promise<ReadonlyArray<FoundShell>> {
export async function getAvailableShells(): Promise<
ReadonlyArray<AnyFoundShell>
> {
if (shellCache) {
return shellCache
}
@ -62,7 +73,7 @@ export async function getAvailableShells(): Promise<ReadonlyArray<FoundShell>> {
}
/** Find the given shell or the default if the given shell can't be found. */
export async function findShellOrDefault(shell: Shell): Promise<FoundShell> {
export async function findShellOrDefault(shell: Shell): Promise<AnyFoundShell> {
const available = await getAvailableShells()
const found = available.find(s => s.shell === shell)
if (found) {
@ -74,7 +85,7 @@ export async function findShellOrDefault(shell: Shell): Promise<FoundShell> {
/** Launch the given shell at the path. */
export async function launchShell(
shell: FoundShell,
shell: AnyFoundShell,
path: string,
onError: (error: Error) => void
): Promise<void> {
@ -92,11 +103,11 @@ export async function launchShell(
let cp: ChildProcess | null = null
if (__DARWIN__) {
cp = Darwin.launch(shell as IFoundShell<Darwin.Shell>, path)
cp = Darwin.launch(shell as FoundShell<Darwin.Shell>, path)
} else if (__WIN32__) {
cp = Win32.launch(shell as IFoundShell<Win32.Shell>, path)
cp = Win32.launch(shell as FoundShell<Win32.Shell>, path)
} else if (__LINUX__) {
cp = Linux.launch(shell as IFoundShell<Linux.Shell>, path)
cp = Linux.launch(shell as FoundShell<Linux.Shell>, path)
}
if (cp != null) {

View File

@ -2,11 +2,11 @@ import { spawn, ChildProcess } from 'child_process'
import * as Path from 'path'
import { enumerateValues, HKEY, RegistryValueType } from 'registry-js'
import { assertNever } from '../fatal-error'
import { IFoundShell } from './found-shell'
import { enableWSLDetection } from '../feature-flag'
import { findGitOnPath } from '../is-git-on-path'
import { parseEnumValue } from '../enum'
import { pathExists } from '../../ui/lib/path-exists'
import { FoundShell } from './shared'
export enum Shell {
Cmd = 'Command Prompt',
@ -28,12 +28,12 @@ export function parse(label: string): Shell {
}
export async function getAvailableShells(): Promise<
ReadonlyArray<IFoundShell<Shell>>
ReadonlyArray<FoundShell<Shell>>
> {
const gitPath = await findGitOnPath()
const rootDir = process.env.WINDIR || 'C:\\Windows'
const dosKeyExePath = `"${rootDir}\\system32\\doskey.exe git=^"${gitPath}^" $*"`
const shells: IFoundShell<Shell>[] = [
const shells: FoundShell<Shell>[] = [
{
shell: Shell.Cmd,
path: process.env.comspec || 'C:\\Windows\\System32\\cmd.exe',
@ -392,7 +392,7 @@ async function findFluentTerminal(): Promise<string | null> {
}
export function launch(
foundShell: IFoundShell<Shell>,
foundShell: FoundShell<Shell>,
path: string
): ChildProcess {
const shell = foundShell.shell

View File

@ -172,13 +172,13 @@ use Hyper as a reference to explain the rest of the process.
### Step 1: Find the shell application
The `getBundleID()` function is used to map a shell enum to it's bundle ID
that is defined in it's manifest. You should add a new entry here for your
shell.
The `getBundleIDs()` function is used to map a shell enum to the possible bundle IDs
that are defined in its manifest. You should add a new entry here for your
shell. An array is returned to handle the case where a shell updates its bundle ID.
```ts
case Shell.Hyper:
return 'co.zeit.hyper'
return ['co.zeit.hyper']
```
After that, follow the existing patterns in `getAvailableShells()` and add a
@ -186,44 +186,33 @@ new entry to lookup the install path for your shell.
```ts
export async function getAvailableShells(): Promise<
ReadonlyArray<IFoundShell<Shell>>
ReadonlyArray<FoundShell<Shell>>
> {
const [
terminalPath,
hyperPath,
iTermPath,
powerShellCorePath,
kittyPath,
terminalInfo,
hyperInfo,
iTermInfo,
powerShellCoreInfo,
kittyInfo,
] = await Promise.all([
getShellPath(Shell.Terminal),
getShellPath(Shell.Hyper),
getShellPath(Shell.iTerm2),
getShellPath(Shell.PowerShellCore),
getShellPath(Shell.Kitty),
getShellInfo(Shell.Terminal),
getShellInfo(Shell.Hyper),
getShellInfo(Shell.iTerm2),
getShellInfo(Shell.PowerShellCore),
getShellInfo(Shell.Kitty),
])
// other code
if (hyperPath) {
shells.push({ shell: Shell.Hyper, path: hyperPath })
if (hyperInfo) {
shells.push({ shell: Shell.Hyper, ...hyperInfo })
}
// other code
}
```
### Step 2: Parse the shell
The `parse()` function is used to parse shell names. You should add a new entry here for your
shell.
```ts
if (label === Shell.Hyper) {
return Shell.Hyper
}
```
### Step 3: Launch the shell
### Step 2: Launch the shell
The launch step will use the `open` command in macOS to launch a given bundle
at the path requested by the user. You may not need to make changes here,
@ -231,12 +220,10 @@ unless your shell behaviour differs significantly from this.
```ts
export function launch(
foundShell: IFoundShell<Shell>,
foundShell: FoundShell<Shell>,
path: string
): ChildProcess {
const bundleID = getBundleID(foundShell.shell)
const commandArgs = ['-b', bundleID, path]
return spawn('open', commandArgs)
return spawn('open', ['-b', foundShell.bundleID, path])
}
```
@ -290,7 +277,7 @@ new entry to lookup the install path for your shell.
```ts
export async function getAvailableShells(): Promise<
ReadonlyArray<IFoundShell<Shell>>
ReadonlyArray<FoundShell<Shell>>
> {
const [
gnomeTerminalPath,
@ -328,7 +315,7 @@ the correct arguments are passed to the command line interface:
```ts
export function launch(
foundShell: IFoundShell<Shell>,
foundShell: FoundShell<Shell>,
path: string
): ChildProcess {
const shell = foundShell.shell