2018-02-14 01:51:55 +00:00
# "Open Shell" integration
2017-10-18 00:35:35 +00:00
GitHub Desktop supports launching an available shell found on the user's
machine, to work with Git repositories outside of Desktop.
### My favourite shell XYZ isn't supported!
This is the checklist of things that it needs to support:
2018-02-01 23:08:53 +00:00
- Desktop can check it exists on the user's machine
- Desktop is able to launch it using the operating system's APIs
- It has a stable interface (command line arguments) that doesn't change
between updates
2017-10-18 00:35:35 +00:00
2017-12-20 03:39:24 +00:00
If you think your shell satisfies all these requirements please read on to
2017-10-18 00:35:35 +00:00
understand how Desktop integrates with each OS, and if you're still keen to
2018-02-01 23:08:53 +00:00
integrate this please fork and contribute a pull request for the team to
review.
2017-10-18 00:35:35 +00:00
## Windows
2018-12-27 17:48:46 +00:00
The source for the Windows shell integration is found in [`app/src/lib/shells/win32.ts` ](https://github.com/desktop/desktop/blob/development/app/src/lib/shells/win32.ts ).
2017-10-18 00:35:35 +00:00
These shells are currently supported:
2018-02-01 23:08:53 +00:00
- Command Prompt (cmd)
- PowerShell
2018-03-17 03:15:14 +00:00
- [PowerShell Core ](https://github.com/powershell/powershell/ )
2018-02-01 23:08:53 +00:00
- [Hyper ](https://hyper.sh/ )
- Git Bash (from [Git for Windows ](https://git-for-windows.github.io/ ))
2020-03-09 15:00:23 +00:00
- [Cygwin ](https://www.cygwin.com/ )
2020-03-12 20:16:28 +00:00
- [WSL ](https://docs.microsoft.com/en-us/windows/wsl/about ) (beta)
2020-03-09 15:00:23 +00:00
- [Windows Terminal ](https://github.com/microsoft/terminal )
2020-07-20 22:31:16 +00:00
- [Alacritty ](https://github.com/alacritty/alacritty )
2021-06-24 08:27:46 +00:00
- [Fluent Terminal ](https://github.com/felixse/FluentTerminal )
2017-10-18 00:35:35 +00:00
These are defined in an enum at the top of the file:
```ts
export enum Shell {
Cmd = 'Command Prompt',
PowerShell = 'PowerShell',
2018-03-16 23:55:12 +00:00
PowerShellCore = 'PowerShell Core',
2017-11-30 20:38:33 +00:00
Hyper = 'Hyper',
2017-10-18 00:35:35 +00:00
GitBash = 'Git Bash',
2020-03-09 15:00:23 +00:00
Cygwin = 'Cygwin',
WSL = 'WSL',
WindowTerminal = 'Windows Terminal',
2020-07-20 22:31:16 +00:00
Alacritty = 'Alacritty',
2017-10-18 00:35:35 +00:00
}
```
2018-02-01 23:08:53 +00:00
To add another shell, add a new key to the `Shell` enum with a friendly name
for the value. You will need to add code in this module to find your shell, and I'll
2017-10-18 00:35:35 +00:00
use **Git Bash** as a reference for the rest of the process.
### Step 1: Find the shell executable
2018-02-01 23:08:53 +00:00
The `getAvailableShells()` method is used to find each shell, as some may not
2018-02-13 01:28:17 +00:00
be present on the user's machine.
2017-10-18 00:35:35 +00:00
2018-02-13 01:28:17 +00:00
This is the example for how we resolve if Git Bash is installed.
2017-10-18 00:35:35 +00:00
```ts
2018-02-13 01:28:17 +00:00
const gitBashPath = await findGitBash()
if (gitBashPath != null) {
2017-10-18 00:35:35 +00:00
shells.push({
shell: Shell.GitBash,
2018-02-13 01:28:17 +00:00
path: gitBashPath,
2017-10-18 00:35:35 +00:00
})
}
2018-02-13 01:28:17 +00:00
```
You will need to add some code in here, following this pattern, to resolve a new shell.
The details of this check will vary based on the how the shell is installed,
but Git Bash is a good example of this:
```ts
async function findGitBash(): Promise< string | null > {
const registryPath = enumerateValues(
HKEY.HKEY_LOCAL_MACHINE,
'SOFTWARE\\GitForWindows'
)
if (registryPath.length === 0) {
return null
}
const installPathEntry = registryPath.find(e => e.name === 'InstallPath')
if (installPathEntry & & installPathEntry.type === RegistryValueType.REG_SZ) {
const path = Path.join(installPathEntry.data, 'git-bash.exe')
if (await pathExists(path)) {
return path
} else {
log.debug(
`[Git Bash] registry entry found but does not exist at '${path}'`
)
}
}
return null
2017-10-18 00:35:35 +00:00
}
```
This approximately reads as:
2018-02-01 23:08:53 +00:00
- check if Git for Windows has been installed, using the registry
- if it is, check the installation path exists
- return the path to `git-bash.exe` within that directory
2017-10-18 00:35:35 +00:00
2020-07-20 22:31:16 +00:00
### 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.GitBash) {
return Shell.GitBash
}
```
### Step 3: Launch the shell
2017-10-18 00:35:35 +00:00
The `launch()` function defines the arguments to pass to the shell, and each
shell may require it's own set of command arguments. You will need to make
2018-02-13 01:28:17 +00:00
changes here to handle a new shell.
2017-10-18 00:35:35 +00:00
```ts
2018-02-13 01:28:17 +00:00
case Shell.GitBash:
const gitBashPath = `"${foundShell.path}"`
log.info(`launching ${shell} at path: ${gitBashPath}`)
return spawn(gitBashPath, [`--cd="${path}"`], {
shell: true,
cwd: path,
})
2017-10-18 00:35:35 +00:00
```
## macOS
2018-12-27 17:48:46 +00:00
The source for the macOS shell integration is found in [`app/src/lib/shells/darwin.ts` ](https://github.com/desktop/desktop/blob/development/app/src/lib/shells/darwin.ts ).
2017-10-18 00:35:35 +00:00
These shells are currently supported:
2018-02-01 23:08:53 +00:00
- Terminal
- [Hyper ](https://hyper.sh/ )
- [iTerm2 ](https://www.iterm2.com/ )
2018-03-17 03:15:14 +00:00
- [PowerShell Core ](https://github.com/powershell/powershell/ )
2018-07-23 22:22:28 +00:00
- [Kitty ](https://sw.kovidgoyal.net/kitty/ )
2020-07-20 21:11:13 +00:00
- [Alacritty ](https://github.com/alacritty/alacritty )
2023-02-09 20:19:25 +00:00
- [Tabby ](https://tabby.sh/ )
2021-09-14 12:56:42 +00:00
- [WezTerm ](https://github.com/wez/wezterm )
2017-10-18 00:35:35 +00:00
These are defined in an enum at the top of the file:
```ts
export enum Shell {
Terminal = 'Terminal',
Hyper = 'Hyper',
iTerm2 = 'iTerm2',
2018-03-17 03:15:14 +00:00
PowerShellCore = 'PowerShell Core',
2018-07-23 22:22:28 +00:00
Kitty = 'Kitty',
2017-10-18 00:35:35 +00:00
}
```
If you want to add another shell, add a new key to the `Shell` enum with a
friendly name for the value.
2018-02-01 23:08:53 +00:00
There's a couple of places you need to add code to find your shell, and I'll
use Hyper as a reference to explain the rest of the process.
2017-10-18 00:35:35 +00:00
### Step 1: Find the shell application
2018-02-01 23:08:53 +00:00
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.
2017-10-18 00:35:35 +00:00
```ts
case Shell.Hyper:
return 'co.zeit.hyper'
```
2018-02-01 23:08:53 +00:00
After that, follow the existing patterns in `getAvailableShells()` and add a
new entry to lookup the install path for your shell.
2017-10-18 00:35:35 +00:00
```ts
export async function getAvailableShells(): Promise<
ReadonlyArray< IFoundShell < Shell > >
> {
2018-03-17 03:15:14 +00:00
const [
terminalPath,
hyperPath,
iTermPath,
powerShellCorePath,
2018-07-23 22:22:28 +00:00
kittyPath,
2018-03-17 03:15:14 +00:00
] = await Promise.all([
2017-10-18 00:35:35 +00:00
getShellPath(Shell.Terminal),
getShellPath(Shell.Hyper),
getShellPath(Shell.iTerm2),
2018-03-17 03:15:14 +00:00
getShellPath(Shell.PowerShellCore),
2018-07-23 22:22:28 +00:00
getShellPath(Shell.Kitty),
2017-10-18 00:35:35 +00:00
])
// other code
if (hyperPath) {
shells.push({ shell: Shell.Hyper, path: hyperPath })
}
// other code
}
```
2020-07-20 21:11:13 +00:00
### 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
2017-10-18 00:35:35 +00:00
2018-02-01 23:08:53 +00:00
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,
unless your shell behaviour differs significantly from this.
2017-10-18 00:35:35 +00:00
```ts
2018-02-13 02:20:34 +00:00
export function launch(
foundShell: IFoundShell< Shell > ,
path: string
): ChildProcess {
const bundleID = getBundleID(foundShell.shell)
2017-10-18 00:35:35 +00:00
const commandArgs = ['-b', bundleID, path]
2018-02-13 02:20:34 +00:00
return spawn('open', commandArgs)
2017-10-18 00:35:35 +00:00
}
```
## Linux
2018-12-27 17:48:46 +00:00
The source for the Linux shell integration is found in [`app/src/lib/shells/linux.ts` ](https://github.com/desktop/desktop/blob/development/app/src/lib/shells/linux.ts ).
2017-10-18 00:35:35 +00:00
These shells are currently supported:
2018-02-01 23:08:53 +00:00
- [GNOME Terminal ](https://help.gnome.org/users/gnome-terminal/stable/ )
2018-09-26 18:09:16 +00:00
- [MATE Terminal ](https://github.com/mate-desktop/mate-terminal )
2018-02-01 23:08:53 +00:00
- [Tilix ](https://github.com/gnunn1/tilix )
2018-09-26 18:09:16 +00:00
- [Terminator ](https://gnometerminator.blogspot.com )
2018-02-01 23:08:53 +00:00
- [Rxvt Unicode ](http://software.schmorp.de/pkg/rxvt-unicode.html )
- [Konsole ](https://konsole.kde.org/ )
- [XTerm ](http://invisible-island.net/xterm/ )
2018-09-26 20:37:14 +00:00
- [Terminology ](https://www.enlightenment.org/docs/apps/terminology.md )
2017-10-18 00:35:35 +00:00
These are defined in an enum at the top of the file:
```ts
export enum Shell {
2017-10-24 09:41:18 +00:00
Gnome = 'GNOME Terminal',
2018-09-26 18:03:54 +00:00
Mate = 'MATE Terminal',
2017-10-24 09:41:18 +00:00
Tilix = 'Tilix',
2018-09-26 18:03:54 +00:00
Terminator = 'Terminator',
2017-11-27 07:42:50 +00:00
Urxvt = 'URxvt',
Konsole = 'Konsole',
Xterm = 'XTerm',
2018-09-26 20:37:14 +00:00
Terminology = 'Terminology',
2017-10-18 00:35:35 +00:00
}
```
2018-02-01 23:08:53 +00:00
To add another shell, add a new key to the `Shell` enum with a friendly name
for the value. You will need to add code in this module to find your shell, and
I'll use Tilix as a reference for the rest of the process.
2017-10-18 00:35:35 +00:00
### Step 1: Find the shell application
The `getShellPath()` method is used to check if a given executable exists at a
path on disk. You should add some code in here to find your shell it's a known
location:
```ts
case Shell.Tilix:
return getPathIfAvailable('/usr/bin/tilix')
```
2018-02-01 23:08:53 +00:00
After that, follow the existing patterns in `getAvailableShells()` and add a
new entry to lookup the install path for your shell.
2017-10-18 00:35:35 +00:00
```ts
export async function getAvailableShells(): Promise<
ReadonlyArray< IFoundShell < Shell > >
> {
2018-03-17 03:15:14 +00:00
const [
gnomeTerminalPath,
2018-09-26 18:03:54 +00:00
mateTerminalPath,
2018-03-17 03:15:14 +00:00
tilixPath,
2018-09-26 18:03:54 +00:00
terminatorPath,
2018-03-17 03:15:14 +00:00
urxvtPath,
konsolePath,
xtermPath,
2018-09-26 20:37:14 +00:00
terminologyPath,
2018-03-17 03:15:14 +00:00
] = await Promise.all([
2017-10-18 00:35:35 +00:00
getShellPath(Shell.Gnome),
2018-09-26 18:03:54 +00:00
getShellPath(Shell.Mate),
2017-10-18 00:35:35 +00:00
getShellPath(Shell.Tilix),
2018-09-26 18:03:54 +00:00
getShellPath(Shell.Terminator),
2017-11-27 07:42:50 +00:00
getShellPath(Shell.Urxvt),
getShellPath(Shell.Konsole),
getShellPath(Shell.Xterm),
2018-09-26 20:37:14 +00:00
getShellPath(Shell.Terminology),
2017-10-18 00:35:35 +00:00
])
...
if (tilixPath) {
shells.push({ shell: Shell.Tilix, path: tilixPath })
}
}
```
### Step 2: Launch the shell
2018-02-13 02:20:34 +00:00
The `launch()` method will use the received `foundShell` executable path and
the path requested by the user. You will need to make changes here, to ensure
the correct arguments are passed to the command line interface:
2017-10-18 00:35:35 +00:00
```ts
2018-02-13 02:20:34 +00:00
export function launch(
foundShell: IFoundShell< Shell > ,
2017-10-18 00:35:35 +00:00
path: string
2018-02-13 02:20:34 +00:00
): ChildProcess {
const shell = foundShell.shell
switch (shell) {
2018-09-26 18:03:54 +00:00
case Shell.Gnome:
case Shell.Mate:
case Shell.Tilix:
case Shell.Terminator:
return spawn(foundShell.path, ['--working-directory', path])
2018-02-13 02:20:34 +00:00
case Shell.Urxvt:
return spawn(foundShell.path, ['-cd', path])
case Shell.Konsole:
return spawn(foundShell.path, ['--workdir', path])
case Shell.Xterm:
return spawn(foundShell.path, ['-e', '/bin/bash'], { cwd: path })
2018-09-26 20:37:14 +00:00
case Shell.Terminology:
return spawn(foundShell.path, ['-d', path])
2018-02-13 02:20:34 +00:00
default:
return assertNever(shell, `Unknown shell: ${shell}` )
2017-11-27 08:00:27 +00:00
}
2017-10-18 00:35:35 +00:00
}
2017-10-24 09:41:18 +00:00
```