diff --git a/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/packages/teleterm/src/mainProcess/fixtures/mocks.ts index 450356c7b..cab95c27f 100644 --- a/packages/teleterm/src/mainProcess/fixtures/mocks.ts +++ b/packages/teleterm/src/mainProcess/fixtures/mocks.ts @@ -5,50 +5,30 @@ import { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mo // Importing electron breaks the fixtures if that's done from within storybook. import { createConfigService } from 'teleterm/services/config/configService'; -const platform = 'darwin'; - export class MockMainProcessClient implements MainProcessClient { + configService: ReturnType; + + constructor(private runtimeSettings: Partial = {}) { + this.configService = createConfigService( + createMockFileStorage(), + this.getRuntimeSettings().platform + ); + } getRuntimeSettings(): RuntimeSettings { - return { - platform, - dev: true, - userDataDir: '', - binDir: '', - certsDir: '', - kubeConfigsDir: '', - defaultShell: '', - tshd: { - insecure: true, - requestedNetworkAddress: '', - binaryPath: '', - homeDir: '', - flags: [], - }, - sharedProcess: { - requestedNetworkAddress: '', - }, - tshdEvents: { - requestedNetworkAddress: '', - }, - installationId: '123e4567-e89b-12d3-a456-426614174000', - }; + return { ...defaultRuntimeSettings, ...this.runtimeSettings }; } getResolvedChildProcessAddresses = () => Promise.resolve({ tsh: '', shared: '' }); openTerminalContextMenu() {} - openClusterContextMenu() {} - openTabContextMenu() {} showFileSaveDialog() { return Promise.resolve({ canceled: false, filePath: '' }); } - configService = createConfigService(createMockFileStorage(), platform); - fileStorage = createMockFileStorage(); removeKubeConfig(): Promise { @@ -56,4 +36,35 @@ export class MockMainProcessClient implements MainProcessClient { } forceFocusWindow() {} + + async symlinkTshMacOs() { + return true; + } + async removeTshSymlinkMacOs() { + return true; + } } + +const defaultRuntimeSettings = { + platform: 'darwin' as const, + dev: true, + userDataDir: '', + binDir: '', + certsDir: '', + kubeConfigsDir: '', + defaultShell: '', + tshd: { + insecure: true, + requestedNetworkAddress: '', + binaryPath: '', + homeDir: '', + flags: [], + }, + sharedProcess: { + requestedNetworkAddress: '', + }, + tshdEvents: { + requestedNetworkAddress: '', + }, + installationId: '123e4567-e89b-12d3-a456-426614174000', +}; diff --git a/packages/teleterm/src/mainProcess/mainProcess.ts b/packages/teleterm/src/mainProcess/mainProcess.ts index b8cf21bdf..ff98342f3 100644 --- a/packages/teleterm/src/mainProcess/mainProcess.ts +++ b/packages/teleterm/src/mainProcess/mainProcess.ts @@ -1,8 +1,8 @@ -import { ChildProcess, fork, spawn } from 'child_process'; +import { ChildProcess, fork, spawn, exec } from 'child_process'; import path from 'path'; - import fs from 'fs/promises'; +import { promisify } from 'util'; import { app, dialog, @@ -203,6 +203,52 @@ export default class MainProcess { this.windowsManager.forceFocusWindow(); }); + // Used in the `tsh install` command on macOS to make the bundled tsh available in PATH. + // Returns true if tsh got successfully installed, false if the user closed the osascript + // prompt. Throws an error when osascript fails. + ipcMain.handle('main-process-symlink-tsh-macos', async () => { + const source = this.settings.tshd.binaryPath; + const target = '/usr/local/bin/tsh'; + const prompt = + 'Teleport Connect wants to create a symlink for tsh in /usr/local/bin.'; + const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf '${source}' '${target}'\\" with prompt \\"${prompt}\\" with administrator privileges"`; + + try { + await promisify(exec)(command); + this.logger.info(`Created the symlink to ${source} under ${target}`); + return true; + } catch (error) { + // Ignore the error if the user canceled the prompt. + // https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_codes.html#//apple_ref/doc/uid/TP40000983-CH220-SW2 + if (error instanceof Error && error.message.includes('-128')) { + return false; + } + this.logger.error(error); + throw error; + } + }); + + ipcMain.handle('main-process-remove-tsh-symlink-macos', async () => { + const target = '/usr/local/bin/tsh'; + const prompt = + 'Teleport Connect wants to remove a symlink for tsh from /usr/local/bin.'; + const command = `osascript -e "do shell script \\"rm '${target}'\\" with prompt \\"${prompt}\\" with administrator privileges"`; + + try { + await promisify(exec)(command); + this.logger.info(`Removed the symlink under ${target}`); + return true; + } catch (error) { + // Ignore the error if the user canceled the prompt. + // https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_codes.html#//apple_ref/doc/uid/TP40000983-CH220-SW2 + if (error instanceof Error && error.message.includes('-128')) { + return false; + } + this.logger.error(error); + throw error; + } + }); + subscribeToTerminalContextMenuEvent(); subscribeToTabContextMenuEvent(); subscribeToConfigServiceEvents(this.configService); diff --git a/packages/teleterm/src/mainProcess/mainProcessClient.ts b/packages/teleterm/src/mainProcess/mainProcessClient.ts index 200a557bc..763af92fb 100644 --- a/packages/teleterm/src/mainProcess/mainProcessClient.ts +++ b/packages/teleterm/src/mainProcess/mainProcessClient.ts @@ -31,5 +31,11 @@ export default function createMainProcessClient(): MainProcessClient { forceFocusWindow() { return ipcRenderer.invoke('main-process-force-focus-window'); }, + symlinkTshMacOs() { + return ipcRenderer.invoke('main-process-symlink-tsh-macos'); + }, + removeTshSymlinkMacOs() { + return ipcRenderer.invoke('main-process-remove-tsh-symlink-macos'); + }, }; } diff --git a/packages/teleterm/src/mainProcess/types.ts b/packages/teleterm/src/mainProcess/types.ts index 9816427c2..e06916c8d 100644 --- a/packages/teleterm/src/mainProcess/types.ts +++ b/packages/teleterm/src/mainProcess/types.ts @@ -43,6 +43,16 @@ export type MainProcessClient = { isDirectory?: boolean; }): Promise; forceFocusWindow(): void; + /** + * The promise returns true if tsh got successfully symlinked, false if the user closed the + * osascript prompt. The promise gets rejected if osascript encountered an error. + */ + symlinkTshMacOs(): Promise; + /** + * The promise returns true if the tsh symlink got removed, false if the user closed the osascript + * prompt. The promise gets rejected if osascript encountered an error. + */ + removeTshSymlinkMacOs(): Promise; }; export type ChildProcessAddresses = { diff --git a/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx b/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx index b1d72f914..d69064560 100644 --- a/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx +++ b/packages/teleterm/src/ui/QuickInput/QuickInput.story.tsx @@ -16,12 +16,21 @@ import React from 'react'; +import Flex from 'design/Flex'; + import AppContextProvider from 'teleterm/ui/appContextProvider'; import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; - -import { getEmptyPendingAccessRequest } from '../services/workspacesService/accessRequestsService'; +import { getEmptyPendingAccessRequest } from 'teleterm/ui/services/workspacesService/accessRequestsService'; +import * as types from 'teleterm/services/tshd/types'; +import { + SuggestionCmd, + SuggestionDatabase, + SuggestionServer, + SuggestionSshLogin, +} from 'teleterm/ui/services/quickInput'; import QuickInput from './QuickInput'; +import QuickInputList from './QuickInputList'; export default { title: 'Teleterm/QuickInput', @@ -46,24 +55,25 @@ export const Story = () => { }; appContext.clustersService.getClusters = () => { - return [ - { - uri: '/clusters/localhost', - name: 'Test', - leaf: false, - connected: true, - proxyHost: 'localhost:3080', - loggedInUser: { - activeRequestsList: [], - name: 'admin', - acl: {}, - sshLoginsList: [], - rolesList: [], - }, - }, - ]; + return [cluster]; }; + appContext.clustersService.setState(draftState => { + draftState.clusters = new Map([[cluster.uri, cluster]]); + }); + + appContext.resourcesService.fetchServers = async () => ({ + agentsList: servers, + totalCount: 3, + startKey: '', + }); + + appContext.resourcesService.fetchDatabases = async () => ({ + agentsList: databases, + totalCount: 3, + startKey: '', + }); + return (
{ ); }; + +export const Suggestions = () => { + const commandSuggestions: SuggestionCmd[] = [ + { + kind: 'suggestion.cmd', + token: '', + data: { + displayName: 'tsh foo', + description: 'Nulla convallis lorem ut ipsum maximus venenatis.', + }, + }, + { + kind: 'suggestion.cmd', + token: '', + data: { + displayName: 'tsh bar', + description: 'Vivamus id nulla sed neque efficitur ornare nec in diam.', + }, + }, + { + kind: 'suggestion.cmd', + token: '', + data: { + displayName: 'tsh quux foo', + description: + 'Sed porta nibh eget lacus suscipit vehicula. Curabitur eget sapien in lacus blandit pretium.', + }, + }, + { + kind: 'suggestion.cmd', + token: '', + data: { + displayName: 'tsh baz quux', + description: 'Etiam cursus magna at feugiat ornare.', + }, + }, + ]; + + const loginSuggestions: SuggestionSshLogin[] = + cluster.loggedInUser.sshLoginsList.map(login => ({ + kind: 'suggestion.ssh-login', + token: '', + appendToToken: '', + data: login, + })); + + const serverSuggestions: SuggestionServer[] = servers.map(server => ({ + kind: 'suggestion.server', + token: '', + data: server, + })); + + const dbSuggestions: SuggestionDatabase[] = databases.map(db => ({ + kind: 'suggestion.database', + token: '', + data: db, + })); + + return ( + + + + + + + ); +}; + +const defaultWidth = 200; +const defaultHeight = 200; + +const QuickInputListWrapper = ({ + items, + width = defaultWidth, + height = defaultHeight, +}) => { + return ( +
+ {}} + /> +
+ ); +}; + +const longIdentifier = + 'lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-quisque-elementum-nulla'; + +const servers: types.Server[] = [ + { + uri: '/clusters/localhost/servers/foo' as const, + tunnel: false, + name: '2018454d-ef3b-4b15-84f7-61ca213d37e3', + hostname: 'foo', + addr: 'foo.localhost', + labelsList: [ + { name: 'env', value: 'prod' }, + { name: 'kernel', value: '5.15.0-1023-aws' }, + ], + }, + { + uri: '/clusters/localhost/servers/bar' as const, + tunnel: false, + name: '24c7aebe-4741-4464-ab69-f076fe467ebd', + hostname: 'bar', + addr: 'bar.localhost', + labelsList: [ + { name: 'env', value: 'staging' }, + { name: 'kernel', value: '5.14.1-1058-aws' }, + ], + }, + { + uri: '/clusters/localhost/servers/lorem' as const, + tunnel: false, + name: '24c7aebe-4741-4464-ab69-f076fe467ebd', + hostname: longIdentifier, + addr: 'lorem.localhost', + labelsList: [ + { name: 'env', value: 'staging' }, + { name: 'kernel', value: '5.14.1-1058-aws' }, + { name: 'lorem', value: longIdentifier }, + { name: 'kernel2', value: '5.14.1-1058-aws' }, + { name: 'env2', value: 'staging' }, + { name: 'kernel3', value: '5.14.1-1058-aws' }, + ], + }, +]; + +const databases: types.Database[] = [ + { + uri: '/clusters/localhost/dbs/postgres' as const, + name: 'postgres', + desc: 'A PostgreSQL database', + protocol: 'postgres', + type: 'self-hosted', + hostname: 'postgres.localhost', + addr: 'postgres.localhost', + labelsList: [ + { name: 'env', value: 'prod' }, + { name: 'kernel', value: '5.15.0-1023-aws' }, + ], + }, + { + uri: '/clusters/localhost/dbs/mysql' as const, + name: 'mysql', + desc: 'A MySQL database', + protocol: 'mysql', + type: 'self-hosted', + hostname: 'mysql.localhost', + addr: 'mysql.localhost', + labelsList: [ + { name: 'env', value: 'staging' }, + { name: 'kernel', value: '5.14.1-1058-aws' }, + ], + }, + { + uri: '/clusters/localhost/dbs/lorem' as const, + name: longIdentifier, + desc: 'Vestibulum ut blandit est, sed dapibus sem. Pellentesque egestas mi eu scelerisque ultricies.', + protocol: 'mysql', + type: 'self-hosted', + hostname: 'lorem.localhost', + addr: 'lorem.localhost', + labelsList: [ + { name: 'env', value: 'staging' }, + { name: 'kernel', value: '5.14.1-1058-aws' }, + { name: 'lorem', value: longIdentifier }, + { name: 'kernel2', value: '5.14.1-1058-aws' }, + { name: 'env2', value: 'staging' }, + { name: 'kernel3', value: '5.14.1-1058-aws' }, + ], + }, +]; + +const cluster = { + uri: '/clusters/localhost' as const, + name: 'Test', + leaf: false, + connected: true, + proxyHost: 'localhost:3080', + loggedInUser: { + activeRequestsList: [], + name: 'admin', + acl: {}, + sshLoginsList: ['root', 'ubuntu', 'ansible', longIdentifier], + rolesList: [], + }, +}; diff --git a/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx b/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx index 74b4dcd4d..936488a39 100644 --- a/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx +++ b/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx @@ -24,6 +24,10 @@ import { Cli, Server, Person, Database } from 'design/Icon'; import * as types from 'teleterm/ui/services/quickInput/types'; const QuickInputList = React.forwardRef((props, ref) => { + // Ideally, this property would be described by the suggestion object itself rather than depending + // on `kind`. But for now we need it just for a single suggestion kind anyway. + const shouldSuggestionsStayInPlace = + props.items[0]?.kind === 'suggestion.cmd'; const activeItemRef = useRef(); const { items, activeItem } = props; if (items.length === 0) { @@ -60,7 +64,7 @@ const QuickInputList = React.forwardRef((props, ref) => { return ( + - {props.item.data.displayName} + {/* Equivalent of flex-shrink: 0, but styled-system doesn't support flex-shrink. */} + + {props.item.data.displayName} + + {props.item.data.description} ); } function SshLoginItem(props: { item: types.SuggestionSshLogin }) { return ( - + @@ -104,7 +112,7 @@ function ServerItem(props: { item: types.SuggestionServer }) { )); return ( - + @@ -125,7 +133,7 @@ function DatabaseItem(props: { item: types.SuggestionDatabase }) { )); return ( - + diff --git a/packages/teleterm/src/ui/QuickInput/useQuickInput.ts b/packages/teleterm/src/ui/QuickInput/useQuickInput.ts index 2a2f9a0f6..e5ebe7bfb 100644 --- a/packages/teleterm/src/ui/QuickInput/useQuickInput.ts +++ b/packages/teleterm/src/ui/QuickInput/useQuickInput.ts @@ -31,7 +31,7 @@ import { import { routing } from 'teleterm/ui/uri'; import { KeyboardShortcutType } from 'teleterm/services/config'; -import { retryWithRelogin } from '../utils'; +import { assertUnreachable, retryWithRelogin } from '../utils'; export default function useQuickInput() { const appContext = useAppContext(); @@ -122,6 +122,17 @@ export default function useQuickInput() { }); break; } + case 'command.tsh-install': { + commandLauncher.executeCommand('tsh-install', undefined); + break; + } + case 'command.tsh-uninstall': { + commandLauncher.executeCommand('tsh-uninstall', undefined); + break; + } + default: { + assertUnreachable(command); + } } quickInputService.clearInputValueAndHide(); diff --git a/packages/teleterm/src/ui/commandLauncher.test.ts b/packages/teleterm/src/ui/commandLauncher.test.ts new file mode 100644 index 000000000..c39a30dce --- /dev/null +++ b/packages/teleterm/src/ui/commandLauncher.test.ts @@ -0,0 +1,36 @@ +import { MockAppContext } from 'teleterm/ui/fixtures/mocks'; + +import { CommandLauncher } from './commandLauncher'; + +it('returns tsh install & uninstall autocomplete command on macOS', () => { + const appContext = new MockAppContext({ platform: 'darwin' }); + const commandLauncher = new CommandLauncher(appContext); + const autocompleteCommandNames = commandLauncher + .getAutocompleteCommands() + .map(c => c.displayName); + + expect(autocompleteCommandNames).toContain('tsh install'); + expect(autocompleteCommandNames).toContain('tsh uninstall'); +}); + +it('does not return tsh install & uninstall autocomplete command on Linux', () => { + const appContext = new MockAppContext({ platform: 'linux' }); + const commandLauncher = new CommandLauncher(appContext); + const autocompleteCommandNames = commandLauncher + .getAutocompleteCommands() + .map(c => c.displayName); + + expect(autocompleteCommandNames).not.toContain('tsh install'); + expect(autocompleteCommandNames).not.toContain('tsh uninstall'); +}); + +it('does not return tsh install & uninstall autocomplete command on Windows', () => { + const appContext = new MockAppContext({ platform: 'win32' }); + const commandLauncher = new CommandLauncher(appContext); + const autocompleteCommandNames = commandLauncher + .getAutocompleteCommands() + .map(c => c.displayName); + + expect(autocompleteCommandNames).not.toContain('tsh install'); + expect(autocompleteCommandNames).not.toContain('tsh uninstall'); +}); diff --git a/packages/teleterm/src/ui/commandLauncher.ts b/packages/teleterm/src/ui/commandLauncher.ts index 3fa4cc700..d9870d8ff 100644 --- a/packages/teleterm/src/ui/commandLauncher.ts +++ b/packages/teleterm/src/ui/commandLauncher.ts @@ -24,6 +24,7 @@ import { } from 'teleterm/ui/uri'; import { tsh } from 'teleterm/ui/services/clusters/types'; import { TrackedKubeConnection } from 'teleterm/ui/services/connectionTracker'; +import { Platform } from 'teleterm/mainProcess/types'; const commands = { // For handling "tsh ssh" executed from the command bar. @@ -92,6 +93,50 @@ const commands = { }, }, + 'tsh-install': { + displayName: '', + description: '', + run(ctx: IAppContext) { + ctx.mainProcessClient.symlinkTshMacOs().then( + isSymlinked => { + if (isSymlinked) { + ctx.notificationsService.notifyInfo( + 'tsh successfully installed in PATH' + ); + } + }, + error => { + ctx.notificationsService.notifyError({ + title: 'Could not install tsh in PATH', + description: `Ran into an error: ${error}`, + }); + } + ); + }, + }, + + 'tsh-uninstall': { + displayName: '', + description: '', + run(ctx: IAppContext) { + ctx.mainProcessClient.removeTshSymlinkMacOs().then( + isRemoved => { + if (isRemoved) { + ctx.notificationsService.notifyInfo( + 'tsh successfully removed from PATH' + ); + } + }, + error => { + ctx.notificationsService.notifyError({ + title: 'Could not remove tsh from PATH', + description: `Ran into an error: ${error}`, + }); + } + ); + }, + }, + 'kube-connect': { displayName: '', description: '', @@ -164,19 +209,32 @@ const commands = { } }, }, +}; - 'autocomplete.tsh-ssh': { +const autocompleteCommands: { + displayName: string; + description: string; + platforms?: Array; +}[] = [ + { displayName: 'tsh ssh', description: 'Run shell or execute a command on a remote SSH node', - run() {}, }, - 'autocomplete.tsh-proxy-db': { + { displayName: 'tsh proxy db', - description: - 'Start local TLS proxy for database connections when using Teleport', - run() {}, + description: 'Start a local proxy for a database connection', }, -}; + { + displayName: 'tsh install', + description: 'Install tsh in PATH', + platforms: ['darwin'], + }, + { + displayName: 'tsh uninstall', + description: 'Uninstall tsh from PATH', + platforms: ['darwin'], + }, +]; export class CommandLauncher { appContext: IAppContext; @@ -190,9 +248,12 @@ export class CommandLauncher { } getAutocompleteCommands() { - return Object.entries(commands) - .filter(([key]) => key.startsWith('autocomplete.')) - .map(([key, value]) => ({ name: key, ...value })); + const { platform } = this.appContext.mainProcessClient.getRuntimeSettings(); + + return autocompleteCommands.filter(command => { + const platforms = command.platforms; + return !command.platforms || platforms.includes(platform); + }); } } diff --git a/packages/teleterm/src/ui/fixtures/mocks.ts b/packages/teleterm/src/ui/fixtures/mocks.ts index 706df9dbc..d81eb0d13 100644 --- a/packages/teleterm/src/ui/fixtures/mocks.ts +++ b/packages/teleterm/src/ui/fixtures/mocks.ts @@ -2,10 +2,11 @@ import { MockMainProcessClient } from 'teleterm/mainProcess/fixtures/mocks'; import { MockTshClient } from 'teleterm/services/tshd/fixtures/mocks'; import { MockPtyServiceClient } from 'teleterm/services/pty/fixtures/mocks'; import AppContext from 'teleterm/ui/appContext'; +import { RuntimeSettings } from 'teleterm/types'; export class MockAppContext extends AppContext { - constructor() { - const mainProcessClient = new MockMainProcessClient(); + constructor(runtimeSettings?: Partial) { + const mainProcessClient = new MockMainProcessClient(runtimeSettings); const tshdClient = new MockTshClient(); const ptyServiceClient = new MockPtyServiceClient(); diff --git a/packages/teleterm/src/ui/services/quickInput/parsers.ts b/packages/teleterm/src/ui/services/quickInput/parsers.ts index bfcfb1b17..f12b0bce0 100644 --- a/packages/teleterm/src/ui/services/quickInput/parsers.ts +++ b/packages/teleterm/src/ui/services/quickInput/parsers.ts @@ -17,6 +17,7 @@ limitations under the License. import { CommandLauncher } from 'teleterm/ui/commandLauncher'; import { + AutocompleteCommand, AutocompleteToken, SuggestionCmd, QuickInputParser, @@ -42,7 +43,6 @@ export class QuickCommandParser implements QuickInputParser { // TODO(ravicious): Handle env vars. parse(rawInput: string): ParseResult { - const autocompleteCommands = this.launcher.getAutocompleteCommands(); // We can safely ignore any whitespace at the start. However, `startIndex` needs to account for // any removed whitespace. const input = rawInput.trimStart(); @@ -53,6 +53,8 @@ export class QuickCommandParser implements QuickInputParser { // Return all commands if there's no input. if (input === '') { + const autocompleteCommands = this.launcher.getAutocompleteCommands(); + return { targetToken, command: { kind: 'command.unknown' }, @@ -134,15 +136,23 @@ export class QuickCommandParser implements QuickInputParser { } private mapAutocompleteCommandsToSuggestions( - commands: { name: string; displayName: string; description: string }[] + commands: { displayName: string; description: string }[] ): SuggestionCmd[] { - return commands.map(cmd => ({ - kind: 'suggestion.cmd' as const, - token: cmd.displayName, + return commands.map(cmd => { + const acceptsNoArguments = + this.parserRegistry.get(cmd.displayName) instanceof + QuickNoArgumentsParser; + // If a command accepts arguments, let's append a space when that suggestion is picked. // This allows us to immediately show autocomplete for the first argument of the command. - appendToToken: ' ', - data: cmd, - })); + const appendToToken = acceptsNoArguments ? '' : ' '; + + return { + kind: 'suggestion.cmd' as const, + token: cmd.displayName, + appendToToken: appendToToken, + data: cmd, + }; + }); } } @@ -307,3 +317,24 @@ export class QuickTshProxyDbParser implements QuickInputParser { }; } } + +// QuickNoArgumentsParser is useful in situations where a command does not accept any arguments. +// If QuickNoArgumentsParser is registered as the parser for that command, selecting that command +// from suggestions will simply close autocomplete. Pressing Enter again will execute the command +// passed to the constructor of QuickNoArgumentsParser. +export class QuickNoArgumentsParser implements QuickInputParser { + constructor(private command: AutocompleteCommand) {} + + parse(rawInput: string, startIndex: number): ParseResult { + const targetToken = { + value: '', + startIndex, + }; + + return { + targetToken, + command: this.command, + getSuggestions: noSuggestions, + }; + } +} diff --git a/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts b/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts index dc66d47c7..ae60bfc49 100644 --- a/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts +++ b/packages/teleterm/src/ui/services/quickInput/quickInputService.test.ts @@ -537,7 +537,6 @@ test('picking a command suggestion in an empty input autocompletes the command', kind: 'suggestion.cmd', token: 'tsh ssh', data: { - name: 'autocomplete.tsh-ssh', displayName: 'tsh ssh', description: '', }, @@ -564,7 +563,6 @@ test('picking a command suggestion in an input with a single space preserves the kind: 'suggestion.cmd', token: 'tsh ssh', data: { - name: 'autocomplete.tsh-ssh', displayName: 'tsh ssh', description: '', }, diff --git a/packages/teleterm/src/ui/services/quickInput/quickInputService.ts b/packages/teleterm/src/ui/services/quickInput/quickInputService.ts index 7e4dca341..1d114c7bc 100644 --- a/packages/teleterm/src/ui/services/quickInput/quickInputService.ts +++ b/packages/teleterm/src/ui/services/quickInput/quickInputService.ts @@ -68,6 +68,14 @@ export class QuickInputService extends Store { 'tsh proxy db', new parsers.QuickTshProxyDbParser(databaseSuggester) ); + this.quickCommandParser.registerParserForCommand( + 'tsh install', + new parsers.QuickNoArgumentsParser({ kind: 'command.tsh-install' }) + ); + this.quickCommandParser.registerParserForCommand( + 'tsh uninstall', + new parsers.QuickNoArgumentsParser({ kind: 'command.tsh-uninstall' }) + ); } state: State = { diff --git a/packages/teleterm/src/ui/services/quickInput/types.ts b/packages/teleterm/src/ui/services/quickInput/types.ts index 77831a7e0..930d54b0c 100644 --- a/packages/teleterm/src/ui/services/quickInput/types.ts +++ b/packages/teleterm/src/ui/services/quickInput/types.ts @@ -9,7 +9,7 @@ type SuggestionBase = { export type SuggestionCmd = SuggestionBase< 'suggestion.cmd', - { name: string; displayName: string; description: string } + { displayName: string; description: string } >; export type SuggestionSshLogin = SuggestionBase< @@ -62,6 +62,12 @@ export type AutocompleteTshSshCommand = CommandBase<'command.tsh-ssh'> & { loginHost: string; }; +export type AutocompleteTshInstallCommand = CommandBase<'command.tsh-install'>; +export type AutocompleteTshUninstallCommand = + CommandBase<'command.tsh-uninstall'>; + export type AutocompleteCommand = | AutocompleteUnknownCommand - | AutocompleteTshSshCommand; + | AutocompleteTshSshCommand + | AutocompleteTshInstallCommand + | AutocompleteTshUninstallCommand;