@@ -3,7 +3,7 @@ import * as fs from 'fs';
33import * as os from 'os' ;
44import * as path from 'path' ;
55import * as util from 'util' ;
6- import { spawn , ChildProcess , SpawnOptions } from 'child_process' ;
6+ import { spawn , exec , ChildProcess , SpawnOptions } from 'child_process' ;
77import * as GSettings from 'node-gsettings-wrapper' ;
88import * as ensureCommandExists from 'command-exists' ;
99
@@ -39,51 +39,150 @@ interface SpawnArgs {
3939 skipStartupScripts ?: true ;
4040}
4141
42+ const execAsync = ( command : string ) : Promise < { stdout : string , stderr : string } > => {
43+ return new Promise ( ( resolve , reject ) => exec ( command , ( error , stdout , stderr ) => {
44+ if ( error ) reject ( error ) ;
45+ else resolve ( { stdout, stderr } ) ;
46+ } ) ) ;
47+ } ;
48+
4249const getTerminalCommand = _ . memoize ( async ( ) : Promise < SpawnArgs | null > => {
43- if ( process . platform === 'win32' ) {
44- if ( await commandExists ( 'git-bash' ) ) {
45- return { command : 'git-bash' } ;
46- } else if ( await canAccess ( DEFAULT_GIT_BASH_PATH ) ) {
47- return { command : DEFAULT_GIT_BASH_PATH } ;
48- } else {
49- return { command : 'start' , args : [ 'cmd' ] , options : { shell : true } , skipStartupScripts : true } ;
50- }
51- } else if ( process . platform === 'linux' ) {
52- if ( await commandExists ( 'x-terminal-emulator' ) ) return { command : 'x-terminal-emulator' } ;
53-
54- if ( GSettings . isAvailable ( ) ) {
55- const gSettingsTerminalKey = GSettings . Key . findById (
56- 'org.gnome.desktop.default-applications.terminal' , 'exec'
57- ) ;
58-
59- const defaultTerminal = gSettingsTerminalKey && gSettingsTerminalKey . getValue ( ) ;
60- if ( defaultTerminal && await commandExists ( defaultTerminal ) ) {
61- return { command : defaultTerminal } ;
62- }
63- }
50+ if ( process . platform === 'win32' ) return getWindowsTerminalCommand ( ) ;
51+ else if ( process . platform === 'darwin' ) return getOSXTerminalCommand ( ) ;
52+ else if ( process . platform === 'linux' ) return getLinuxTerminalCommand ( ) ;
53+ else return null ;
54+ } ) ;
55+
56+ const getWindowsTerminalCommand = async ( ) : Promise < SpawnArgs | null > => {
57+ if ( await commandExists ( 'git-bash' ) ) {
58+ return { command : 'git-bash' } ;
59+ } else if ( await canAccess ( DEFAULT_GIT_BASH_PATH ) ) {
60+ return { command : DEFAULT_GIT_BASH_PATH } ;
61+ }
62+
63+ return { command : 'start' , args : [ 'cmd' ] , options : { shell : true } , skipStartupScripts : true } ;
64+ } ;
65+
66+ const getOSXTerminalCommand = async ( ) : Promise < SpawnArgs | null > => {
67+ const terminalExecutables = ( await Promise . all (
68+ [
69+ 'co.zeit.hyper' ,
70+ 'com.googlecode.iterm2' ,
71+ 'com.googlecode.iterm' ,
72+ 'com.apple.Terminal'
73+ ] . map (
74+ ( bundleId ) => findOsxExecutable ( bundleId ) . catch ( ( ) => null )
75+ )
76+ ) ) . filter ( ( executablePath ) => ! ! executablePath ) ;
77+
78+ const bestAvailableTerminal = terminalExecutables [ 0 ] ;
79+
80+ if ( bestAvailableTerminal ) return { command : bestAvailableTerminal } ;
81+ else return null ;
82+ } ;
83+
84+ const getLinuxTerminalCommand = async ( ) : Promise < SpawnArgs | null > => {
85+ // Symlink/wrapper that should indicate the system default
86+ if ( await commandExists ( 'x-terminal-emulator' ) ) return getXTerminalCommand ( ) ;
87+
88+ // Check gnome app settings, if available
89+ if ( GSettings . isAvailable ( ) ) {
90+ const gSettingsTerminalKey = GSettings . Key . findById (
91+ 'org.gnome.desktop.default-applications.terminal' , 'exec'
92+ ) ;
6493
65- if ( await commandExists ( 'xfce4-terminal' ) ) return { command : 'xfce4-terminal' } ;
66- if ( await commandExists ( 'xterm' ) ) return { command : 'xterm' } ;
67- } else if ( process . platform === 'darwin' ) {
68- const terminalExecutables = ( await Promise . all (
69- [
70- 'co.zeit.hyper' ,
71- 'com.googlecode.iterm2' ,
72- 'com.googlecode.iterm' ,
73- 'com.apple.Terminal'
74- ] . map (
75- ( bundleId ) => findOsxExecutable ( bundleId ) . catch ( ( ) => null )
76- )
77- ) ) . filter ( ( executablePath ) => ! ! executablePath ) ;
78-
79- const bestAvailableTerminal = terminalExecutables [ 0 ] ;
80- if ( bestAvailableTerminal ) {
81- return { command : bestAvailableTerminal } ;
94+ const defaultTerminal = gSettingsTerminalKey && gSettingsTerminalKey . getValue ( ) ;
95+ if ( defaultTerminal && await commandExists ( defaultTerminal ) ) {
96+ if ( defaultTerminal . includes ( 'gnome-terminal' ) ) return getGnomeTerminalCommand ( defaultTerminal ) ;
97+ if ( defaultTerminal . includes ( 'konsole' ) ) return getKonsoleTerminalCommand ( defaultTerminal ) ;
98+ if ( defaultTerminal . includes ( 'xfce4-terminal' ) ) return getXfceTerminalCommand ( defaultTerminal ) ;
99+ if ( defaultTerminal . includes ( 'x-terminal-emulator' ) ) return getXTerminalCommand ( defaultTerminal ) ;
100+ return { command : defaultTerminal } ;
82101 }
83102 }
84103
104+ // If a specific term like this is installed, it's probably the preferred one
105+ if ( await commandExists ( 'konsole' ) ) return getKonsoleTerminalCommand ( ) ;
106+ if ( await commandExists ( 'xfce4-terminal' ) ) return getXfceTerminalCommand ( ) ;
107+ if ( await commandExists ( 'xterm' ) ) return { command : 'xterm' } ;
108+
85109 return null ;
86- } ) ;
110+ }
111+
112+ const getXTerminalCommand = async ( command = 'x-terminal-emulator' ) : Promise < SpawnArgs > => {
113+ // x-terminal-emulator is a wrapper/symlink to the terminal of choice.
114+ // Unfortunately, we need to pass specific args that aren't supported by all terminals (to ensure
115+ // terminals run in the foreground), and the Debian XTE wrapper at least doesn't pass through
116+ // any of the args we want to use. To fix this, we parse --help to try and detect the underlying
117+ // terminal, and run it directly with the args we need.
118+ try {
119+ const { stdout } = await execAsync ( `${ command } --help` ) ; // debian wrapper ignores --version
120+ const helpOutput = stdout . toLowerCase ( ) . replace ( / [ ^ \w \d ] + / g, ' ' ) ;
121+
122+ if ( helpOutput . includes ( 'gnome terminal' ) && await commandExists ( 'gnome-terminal' ) ) {
123+ return getGnomeTerminalCommand ( ) ;
124+ } else if ( helpOutput . includes ( 'xfce4 terminal' ) && await commandExists ( 'xfce4-terminal' ) ) {
125+ return getXfceTerminalCommand ( ) ;
126+ } else if ( helpOutput . includes ( 'konsole' ) && await commandExists ( 'konsole' ) ) {
127+ return getKonsoleTerminalCommand ( ) ;
128+ }
129+ } catch ( e ) {
130+ reportError ( e ) ;
131+ }
132+
133+ // If there's an error, or we just don't recognize the console, give up & run it directly
134+ return { command : 'x-terminal-emulator' } ;
135+ } ;
136+
137+ const getKonsoleTerminalCommand = async ( command = 'konsole' ) : Promise < SpawnArgs > => {
138+ let extraArgs : string [ ] = [ ] ;
139+
140+ const { stdout } = await execAsync ( `${ command } --help` ) ;
141+
142+ // Forces Konsole to run in the foreground, with no separate process
143+ // Seems to be well supported for a long time, but check just in case
144+ if ( stdout . includes ( '--nofork' ) ) {
145+ extraArgs = [ '--nofork' ] ;
146+ }
147+
148+ return { command, args : extraArgs } ;
149+ } ;
150+
151+ const getGnomeTerminalCommand = async ( command = 'gnome-terminal' ) : Promise < SpawnArgs > => {
152+ let extraArgs : string [ ] = [ ] ;
153+
154+ const { stdout } = await execAsync ( `${ command } --help-all` ) ;
155+
156+ // Officially supported option, but only supported in v3.28+
157+ if ( stdout . includes ( '--wait' ) ) {
158+ extraArgs = [ '--wait' ] ;
159+ } else {
160+ // Debugging option - works back to v3.7 (2012), but not officially supported
161+ // Documented at https://wiki.gnome.org/Apps/Terminal/Debugging
162+ const randomId = Math . round ( ( Math . random ( ) * 100000 ) ) ;
163+ extraArgs = [ '--app-id' , `com.httptoolkit.${ randomId } ` ] ;
164+ }
165+
166+ // We're assuming here that nobody is developing in a pre-2012 un-updated gnome-terminal.
167+ // If they are then gnome-terminal is not going to recognize --app-id, and will fail to
168+ // start. Hard to avoid, rare case, so c'est la vie.
169+
170+ return { command, args : extraArgs } ;
171+ } ;
172+
173+ const getXfceTerminalCommand = async ( command = 'xfce4-terminal' ) : Promise < SpawnArgs > => {
174+ let extraArgs : string [ ] = [ ] ;
175+
176+ const { stdout } = await execAsync ( `${ command } --help` ) ;
177+
178+ // Disables the XFCE terminal server for this terminal, so it runs in the foreground.
179+ // Seems to be well supported for a long time, but check just in case
180+ if ( stdout . includes ( '--disable-server' ) ) {
181+ extraArgs = [ '--disable-server' ] ;
182+ }
183+
184+ return { command, args : extraArgs } ;
185+ } ;
87186
88187const appendOrCreateFile = util . promisify ( fs . appendFile ) ;
89188const appendToFirstExisting = async ( paths : string [ ] , forceWrite : boolean , contents : string ) => {
@@ -105,8 +204,10 @@ const END_CONFIG_SECTION = '# --httptoolkit-end--';
105204
106205// Works in bash, zsh, dash, ksh, sh (not fish)
107206const SH_SHELL_PATH_CONFIG = `
108- ${ START_CONFIG_SECTION } This section will be removed shortly
207+ ${ START_CONFIG_SECTION }
208+ # This section will be reset each time a HTTP Toolkit terminal is opened
109209if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then
210+ # When HTTP Toolkit is active, we inject various overrides into PATH
110211 export PATH="${ POSIX_OVERRIDE_BIN_PATH } :$PATH"
111212
112213 if command -v winpty >/dev/null 2>&1; then
@@ -116,8 +217,10 @@ if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then
116217fi
117218${ END_CONFIG_SECTION } `;
118219const FISH_SHELL_PATH_CONFIG = `
119- ${ START_CONFIG_SECTION } This section will be removed shortly
220+ ${ START_CONFIG_SECTION }
221+ # This section will be reset each time a HTTP Toolkit terminal is opened
120222if [ -n "$HTTP_TOOLKIT_ACTIVE" ]
223+ # When HTTP Toolkit is active, we inject various overrides into PATH
121224 set -x PATH "${ POSIX_OVERRIDE_BIN_PATH } " $PATH;
122225
123226 if command -v winpty >/dev/null 2>&1
0 commit comments