diff --git a/tabby-local/src/config.ts b/tabby-local/src/config.ts index e79ee21a..c7f02b15 100644 --- a/tabby-local/src/config.ts +++ b/tabby-local/src/config.ts @@ -3,9 +3,6 @@ import { ConfigProvider, Platform } from 'tabby-core' /** @hidden */ export class TerminalConfigProvider extends ConfigProvider { defaults = { - hotkeys: { - 'copy-current-path': [], - }, terminal: { autoOpen: false, useConPTY: true, diff --git a/tabby-local/src/session.ts b/tabby-local/src/session.ts index fd165245..d66d5ea3 100644 --- a/tabby-local/src/session.ts +++ b/tabby-local/src/session.ts @@ -1,6 +1,5 @@ import * as psNode from 'ps-node' import * as fs from 'mz/fs' -import * as os from 'os' import { Injector } from '@angular/core' import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core' import { BaseSession } from 'tabby-terminal' @@ -19,8 +18,6 @@ try { } catch { } const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi -const OSC1337Prefix = Buffer.from('\x1b]1337;') -const OSC1337Suffix = Buffer.from('\x07') // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class PTYProxy { @@ -90,7 +87,6 @@ export class Session extends BaseSession { private ptyClosed = false private pauseAfterExit = false private guessedCWD: string|null = null - private reportedCWD: string private initialCWD: string|null = null private config: ConfigService private hostApp: HostAppService @@ -184,9 +180,7 @@ export class Session extends BaseSession { this.pty.subscribe('data', (array: Uint8Array) => { this.pty!.ackData(array.length) - - let data = Buffer.from(array) - data = this.processOSC1337(data) + const data = Buffer.from(array) this.emitOutput(data) if (this.hostApp.platform === Platform.Windows) { this.guessWindowsCWD(data.toString()) @@ -293,7 +287,7 @@ export class Session extends BaseSession { } supportsWorkingDirectory (): boolean { - return !!(this.truePID || this.reportedCWD || this.guessedCWD) + return !!(this.truePID || this.reportedCWD ?? this.guessedCWD) } async getWorkingDirectory (): Promise { @@ -336,22 +330,4 @@ export class Session extends BaseSession { this.guessedCWD = match[0] } } - - private processOSC1337 (data: Buffer) { - if (data.includes(OSC1337Prefix)) { - const preData = data.subarray(0, data.indexOf(OSC1337Prefix)) - const params = data.subarray(data.indexOf(OSC1337Prefix) + OSC1337Prefix.length) - const postData = params.subarray(params.indexOf(OSC1337Suffix) + OSC1337Suffix.length) - const paramString = params.subarray(0, params.indexOf(OSC1337Suffix)).toString() - - if (paramString.startsWith('CurrentDir=')) { - this.reportedCWD = paramString.split('=')[1] - if (this.reportedCWD.startsWith('~')) { - this.reportedCWD = os.homedir() + this.reportedCWD.substring(1) - } - data = Buffer.concat([preData, postData]) - } - } - return data - } } diff --git a/tabby-local/src/tabContextMenu.ts b/tabby-local/src/tabContextMenu.ts index 7b6ad4d4..57577ced 100644 --- a/tabby-local/src/tabContextMenu.ts +++ b/tabby-local/src/tabContextMenu.ts @@ -138,13 +138,6 @@ export class NewTabContextMenu extends TabContextMenuItemProvider { }) } - if (tab instanceof TerminalTabComponent && tab.session?.supportsWorkingDirectory()) { - items.push({ - label: 'Copy current path', - click: () => tab.copyCurrentPath(), - }) - } - return items } } diff --git a/tabby-ssh/src/api.ts b/tabby-ssh/src/api.ts index f69aebc8..114eec2c 100644 --- a/tabby-ssh/src/api.ts +++ b/tabby-ssh/src/api.ts @@ -530,11 +530,11 @@ export class SSHSession extends BaseSession { } supportsWorkingDirectory (): boolean { - return true + return !!this.reportedCWD } async getWorkingDirectory (): Promise { - return null + return this.reportedCWD ?? null } private openShellChannel (options): Promise { diff --git a/tabby-ssh/src/components/sshTab.component.pug b/tabby-ssh/src/components/sshTab.component.pug index f6139bab..091fc56e 100644 --- a/tabby-ssh/src/components/sshTab.component.pug +++ b/tabby-ssh/src/components/sshTab.component.pug @@ -6,6 +6,16 @@ i.fas.fa-xs.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open') strong.mr-auto {{profile.options.user}}@{{profile.options.host}}:{{profile.options.port}} + .mr-2(ngbDropdown, *ngIf='session && !session.supportsWorkingDirectory()', placement='bottom-right') + button.btn.btn-sm.btn-link(ngbDropdownToggle) + i.far.fa-lightbulb + .bg-dark(ngbDropdownMenu) + a.d-flex.align-items-center(ngbDropdownItem, (click)='platform.openExternal("https://tabby.sh/go/cwd-detection")') + .mr-auto + strong Working directory detection + div Learn how to allow Tabby to detect remote shell's working directory. + i.fas.fa-arrow-right.ml-4 + button.btn.btn-sm.btn-link.mr-2((click)='reconnect()') i.fas.fa-redo span Reconnect diff --git a/tabby-ssh/src/components/sshTab.component.ts b/tabby-ssh/src/components/sshTab.component.ts index f12eb015..a0c55797 100644 --- a/tabby-ssh/src/components/sshTab.component.ts +++ b/tabby-ssh/src/components/sshTab.component.ts @@ -22,6 +22,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent { session: SSHSession|null = null sftpPanelVisible = false sftpPath = '/' + enableToolbar = true private sessionStack: SSHSession[] = [] private recentInputs = '' private reconnectOffered = false @@ -32,7 +33,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent { private ngbModal: NgbModal, ) { super(injector) - this.enableToolbar = true } ngOnInit (): void { @@ -214,7 +214,8 @@ export class SSHTabComponent extends BaseTerminalTabComponent { )).response === 1 } - openSFTP (): void { + async openSFTP (): Promise { + this.sftpPath = await this.session?.getWorkingDirectory() ?? this.sftpPath setTimeout(() => { this.sftpPanelVisible = true }, 100) diff --git a/tabby-ssh/src/services/ssh.service.ts b/tabby-ssh/src/services/ssh.service.ts index 1ae34bf2..ad89b0ef 100644 --- a/tabby-ssh/src/services/ssh.service.ts +++ b/tabby-ssh/src/services/ssh.service.ts @@ -159,13 +159,13 @@ export class SSHService { return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath } - async getWinSCPURI (profile: SSHProfile): Promise { + async getWinSCPURI (profile: SSHProfile, cwd?: string): Promise { let uri = `scp://${profile.options.user}` const password = await this.passwordStorage.loadPassword(profile) if (password) { uri += ':' + encodeURIComponent(password) } - uri += `@${profile.options.host}:${profile.options.port}/` + uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}` return uri } diff --git a/tabby-ssh/src/tabContextMenu.ts b/tabby-ssh/src/tabContextMenu.ts index dae9c269..8f72ca54 100644 --- a/tabby-ssh/src/tabContextMenu.ts +++ b/tabby-ssh/src/tabContextMenu.ts @@ -22,7 +22,9 @@ export class SFTPContextMenu extends TabContextMenuItemProvider { } const items = [{ label: 'Open SFTP panel', - click: () => tab.openSFTP(), + click: () => { + tab.openSFTP() + }, }] if (this.hostApp.platform === Platform.Windows && this.ssh.getWinSCPPath()) { items.push({ diff --git a/tabby-terminal/src/api/baseTerminalTab.component.ts b/tabby-terminal/src/api/baseTerminalTab.component.ts index d8277c90..d26b90ab 100644 --- a/tabby-terminal/src/api/baseTerminalTab.component.ts +++ b/tabby-terminal/src/api/baseTerminalTab.component.ts @@ -268,16 +268,17 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit break } }) + this.bellPlayer = document.createElement('audio') this.bellPlayer.src = require('../bell.ogg').default this.contextMenuProviders.sort((a, b) => a.weight - b.weight) - - this.pinToolbar = this.enableToolbar && (window.localStorage.pinTerminalToolbar ?? 'true') === 'true' } /** @hidden */ ngOnInit (): void { + this.pinToolbar = this.enableToolbar && (window.localStorage.pinTerminalToolbar ?? 'true') === 'true' + this.focused$.subscribe(() => { this.configure() this.frontend?.focus() diff --git a/tabby-terminal/src/api/osc1337Processing.ts b/tabby-terminal/src/api/osc1337Processing.ts new file mode 100644 index 00000000..58e61169 --- /dev/null +++ b/tabby-terminal/src/api/osc1337Processing.ts @@ -0,0 +1,37 @@ +import * as os from 'os' +import { Subject, Observable } from 'rxjs' + +const OSC1337Prefix = Buffer.from('\x1b]1337;') +const OSC1337Suffix = Buffer.from('\x07') + +export class OSC1337Processor { + get cwdReported$ (): Observable { return this.cwdReported } + + private cwdReported = new Subject() + + process (data: Buffer): Buffer { + if (data.includes(OSC1337Prefix)) { + const preData = data.subarray(0, data.indexOf(OSC1337Prefix)) + const params = data.subarray(data.indexOf(OSC1337Prefix) + OSC1337Prefix.length) + const postData = params.subarray(params.indexOf(OSC1337Suffix) + OSC1337Suffix.length) + const paramString = params.subarray(0, params.indexOf(OSC1337Suffix)).toString() + + if (paramString.startsWith('CurrentDir=')) { + let reportedCWD = paramString.split('=')[1] + if (reportedCWD.startsWith('~')) { + reportedCWD = os.homedir() + reportedCWD.substring(1) + } + this.cwdReported.next(reportedCWD) + } else { + console.debug('Unsupported OSC 1337 parameter:', paramString) + } + + data = Buffer.concat([preData, postData]) + } + return data + } + + close (): void { + this.cwdReported.complete() + } +} diff --git a/tabby-terminal/src/config.ts b/tabby-terminal/src/config.ts index 5e17f6d0..9c1808b9 100644 --- a/tabby-terminal/src/config.ts +++ b/tabby-terminal/src/config.ts @@ -3,6 +3,9 @@ import { ConfigProvider, Platform } from 'tabby-core' /** @hidden */ export class TerminalConfigProvider extends ConfigProvider { defaults = { + hotkeys: { + 'copy-current-path': [], + }, terminal: { frontend: 'xterm', fontSize: 14, diff --git a/tabby-terminal/src/index.ts b/tabby-terminal/src/index.ts index 1f7d9793..8651bea0 100644 --- a/tabby-terminal/src/index.ts +++ b/tabby-terminal/src/index.ts @@ -27,7 +27,7 @@ import { PathDropDecorator } from './features/pathDrop' import { ZModemDecorator } from './features/zmodem' import { TerminalConfigProvider } from './config' import { TerminalHotkeyProvider } from './hotkeys' -import { CopyPasteContextMenu, LegacyContextMenu } from './tabContextMenu' +import { CopyPasteContextMenu, MiscContextMenu, LegacyContextMenu } from './tabContextMenu' import { hterm } from './frontends/hterm' import { Frontend } from './frontends/frontend' @@ -56,6 +56,7 @@ import { TerminalCLIHandler } from './cli' { provide: TerminalDecorator, useClass: DebugDecorator, multi: true }, { provide: TabContextMenuItemProvider, useClass: CopyPasteContextMenu, multi: true }, + { provide: TabContextMenuItemProvider, useClass: MiscContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: LegacyContextMenu, multi: true }, { provide: CLIHandler, useClass: TerminalCLIHandler, multi: true }, @@ -119,5 +120,6 @@ export { BaseTerminalTabComponent } from './api/baseTerminalTab.component' export * from './api/interfaces' export * from './api/streamProcessing' export * from './api/loginScriptProcessing' +export * from './api/osc1337Processing' export * from './session' export { LoginScriptsSettingsComponent, StreamProcessingSettingsComponent } diff --git a/tabby-terminal/src/session.ts b/tabby-terminal/src/session.ts index 185f3111..36e28d36 100644 --- a/tabby-terminal/src/session.ts +++ b/tabby-terminal/src/session.ts @@ -1,6 +1,7 @@ import { Observable, Subject } from 'rxjs' import { Logger } from 'tabby-core' import { LoginScriptProcessor, LoginScriptsOptions } from './api/loginScriptProcessing' +import { OSC1337Processor } from './api/osc1337Processing' /** * A session object for a [[BaseTerminalTabComponent]] @@ -14,6 +15,8 @@ export abstract class BaseSession { protected closed = new Subject() protected destroyed = new Subject() protected loginScriptProcessor: LoginScriptProcessor | null = null + protected reportedCWD?: string + protected osc1337Processor = new OSC1337Processor() private initialDataBuffer = Buffer.from('') private initialDataBufferReleased = false @@ -22,9 +25,14 @@ export abstract class BaseSession { get closed$ (): Observable { return this.closed } get destroyed$ (): Observable { return this.destroyed } - constructor (protected logger: Logger) { } + constructor (protected logger: Logger) { + this.osc1337Processor.cwdReported$.subscribe(cwd => { + this.reportedCWD = cwd + }) + } emitOutput (data: Buffer): void { + data = this.osc1337Processor.process(data) if (!this.initialDataBufferReleased) { this.initialDataBuffer = Buffer.concat([this.initialDataBuffer, data]) } else { @@ -56,6 +64,7 @@ export abstract class BaseSession { this.destroyed.next() await this.gracefullyKillProcess() } + this.osc1337Processor.close() this.closed.complete() this.destroyed.complete() this.output.complete() diff --git a/tabby-terminal/src/tabContextMenu.ts b/tabby-terminal/src/tabContextMenu.ts index 2c895b39..92c3c6fb 100644 --- a/tabby-terminal/src/tabContextMenu.ts +++ b/tabby-terminal/src/tabContextMenu.ts @@ -39,6 +39,22 @@ export class CopyPasteContextMenu extends TabContextMenuItemProvider { } } +/** @hidden */ +@Injectable() +export class MiscContextMenu extends TabContextMenuItemProvider { + weight = 1 + + async getItems (tab: BaseTabComponent): Promise { + if (tab instanceof BaseTerminalTabComponent && tab.session?.supportsWorkingDirectory()) { + return [{ + label: 'Copy current path', + click: () => tab.copyCurrentPath(), + }] + } + return [] + } +} + /** @hidden */ @Injectable() export class LegacyContextMenu extends TabContextMenuItemProvider {