Skip to content

Commit e2eab27

Browse files
committed
Fix kgx detection via x-terminal-emulator symlink
1 parent 74fad98 commit e2eab27

File tree

1 file changed

+54
-33
lines changed

1 file changed

+54
-33
lines changed

src/interceptors/terminal/fresh-terminal-interceptor.ts

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Interceptor } from '..';
88
import { HtkConfig } from '../../config';
99
import { logError, addBreadcrumb } from '../../error-tracking';
1010
import { isErrorLike } from '../../util/error';
11-
import { canAccess, commandExists } from '../../util/fs';
11+
import { canAccess, commandExists, getRealPath, resolveCommandPath } from '../../util/fs';
1212
import { spawnToResult } from '../../util/process-management';
1313

1414
import { getTerminalEnvVars } from './terminal-env-overrides';
@@ -108,38 +108,6 @@ const getLinuxTerminalCommand = async (): Promise<SpawnArgs | null> => {
108108
return null;
109109
};
110110

111-
const getXTerminalCommand = async (command = 'x-terminal-emulator'): Promise<SpawnArgs> => {
112-
// x-terminal-emulator is a wrapper/symlink to the terminal of choice.
113-
// Unfortunately, we need to pass specific args that aren't supported by all terminals (to ensure
114-
// terminals run in the foreground), and the Debian XTE wrapper at least doesn't pass through
115-
// any of the args we want to use. To fix this, we parse --help to try and detect the underlying
116-
// terminal, and run it directly with the args we need.
117-
try {
118-
// Run the command with -h to get some output we can use to infer the terminal itself.
119-
// --version would be nice, but the debian wrapper ignores it. --help isn't supported by xterm.
120-
const { stdout } = await spawnToResult(command, ['-h']);
121-
const helpOutput = stdout.toLowerCase().replace(/[^\w\d]+/g, ' ');
122-
123-
if (helpOutput.includes('gnome terminal') && await commandExists('gnome-terminal')) {
124-
return getGnomeTerminalCommand();
125-
} else if (helpOutput.includes('xfce4 terminal') && await commandExists('xfce4-terminal')) {
126-
return getXfceTerminalCommand();
127-
} else if (helpOutput.includes('konsole') && await commandExists('konsole')) {
128-
return getKonsoleTerminalCommand();
129-
}
130-
} catch (e) {
131-
if (isErrorLike(e) && e.message?.includes('rxvt')) {
132-
// Bizarrely, rxvt -h prints help but always returns a non-zero exit code.
133-
// Doesn't need any special arguments anyway though, so just ignore it
134-
} else {
135-
logError(e);
136-
}
137-
}
138-
139-
// If there's an error, or we just don't recognize the console, give up & run it directly
140-
return { command };
141-
};
142-
143111
const getKgxCommand = async (command = 'kgx'): Promise<SpawnArgs> => {
144112
return { command, envVars: { DBUS_SESSION_BUS_ADDRESS: '' } };
145113
};
@@ -194,6 +162,59 @@ const getXfceTerminalCommand = async (command = 'xfce4-terminal'): Promise<Spawn
194162
return { command, args: extraArgs };
195163
};
196164

165+
const X_TERMINAL_MATCHERS = [
166+
{ helpString: 'gnome terminal', command: 'gnome-terminal', commandBuilder: getGnomeTerminalCommand },
167+
{ helpString: 'xfce4 terminal', command: 'xfce4-terminal', commandBuilder: getXfceTerminalCommand },
168+
{ helpString: 'konsole', command: 'konsole', commandBuilder: getKonsoleTerminalCommand },
169+
{ helpString: 'kgx', command: 'kgx', commandBuilder: getKgxCommand }
170+
] as const;
171+
172+
const getXTerminalCommand = async (command = 'x-terminal-emulator'): Promise<SpawnArgs> => {
173+
// x-terminal-emulator is a wrapper/symlink to the terminal of choice.
174+
// Unfortunately, we need to pass specific args that aren't supported by all terminals (to ensure
175+
// terminals run in the foreground), and the Debian XTE wrapper at least doesn't pass through
176+
// any of the args we want to use. To fix this, we parse --help to try and detect the underlying
177+
// terminal, and run it directly with the args we need.
178+
try {
179+
// In parallel, get the command's help output, and its real resolved path on disk:
180+
const [helpOutput, commandPath] = await Promise.all([
181+
// Run the command with -h to get some output we can use to infer the terminal itself.
182+
// --version would be nice, but the debian wrapper ignores it. --help isn't supported by xterm.
183+
spawnToResult(command, ['-h'])
184+
.then(({ stdout }) => stdout.toLowerCase().replace(/[^\w\d]+/g, ' ')),
185+
// We find the command in PATH, and then follow any symbolic links, and see what the real
186+
// underlying path is:
187+
resolveCommandPath(command)
188+
.then((path) => { if (path) return getRealPath(path) })
189+
]);
190+
191+
for (let terminalPattern of X_TERMINAL_MATCHERS) {
192+
// We match the help output or command path for known strings:
193+
if (
194+
!helpOutput.includes(terminalPattern.helpString) &&
195+
!commandPath?.includes(command)
196+
) continue;
197+
198+
// If found, we use the terminal _if_ it exists globally with our expected command.
199+
// We can't use 'command' directly, because some terms (gnome-terminal, Debian generally)
200+
// use weird wrappers that don't behave properly.
201+
if (await commandExists(terminalPattern.command)) {
202+
return terminalPattern.commandBuilder();
203+
}
204+
}
205+
} catch (e) {
206+
if (isErrorLike(e) && e.message?.includes('rxvt')) {
207+
// Bizarrely, rxvt -h prints help but always returns a non-zero exit code.
208+
// Doesn't need any special arguments anyway though, so just ignore it
209+
} else {
210+
logError(e);
211+
}
212+
}
213+
214+
// If there's an error, or we just don't recognize the console, give up & run it directly:
215+
return { command };
216+
};
217+
197218
const terminals: _.Dictionary<ChildProcess[] | undefined> = {}
198219

199220
export class FreshTerminalInterceptor implements Interceptor {

0 commit comments

Comments
 (0)