mirror of
https://github.com/Eugeny/tabby.git
synced 2024-11-22 20:55:03 +03:00
open SFTP panel in the CWD
This commit is contained in:
parent
7fa29b4b37
commit
a2e0db2a16
@ -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,
|
||||
|
@ -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<string|null> {
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -530,11 +530,11 @@ export class SSHSession extends BaseSession {
|
||||
}
|
||||
|
||||
supportsWorkingDirectory (): boolean {
|
||||
return true
|
||||
return !!this.reportedCWD
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string|null> {
|
||||
return null
|
||||
return this.reportedCWD ?? null
|
||||
}
|
||||
|
||||
private openShellChannel (options): Promise<ClientChannel> {
|
||||
|
@ -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
|
||||
|
@ -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<void> {
|
||||
this.sftpPath = await this.session?.getWorkingDirectory() ?? this.sftpPath
|
||||
setTimeout(() => {
|
||||
this.sftpPanelVisible = true
|
||||
}, 100)
|
||||
|
@ -159,13 +159,13 @@ export class SSHService {
|
||||
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
|
||||
}
|
||||
|
||||
async getWinSCPURI (profile: SSHProfile): Promise<string> {
|
||||
async getWinSCPURI (profile: SSHProfile, cwd?: string): Promise<string> {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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()
|
||||
|
37
tabby-terminal/src/api/osc1337Processing.ts
Normal file
37
tabby-terminal/src/api/osc1337Processing.ts
Normal file
@ -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<string> { return this.cwdReported }
|
||||
|
||||
private cwdReported = new Subject<string>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 }
|
||||
|
@ -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<void>()
|
||||
protected destroyed = new Subject<void>()
|
||||
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<void> { return this.closed }
|
||||
get destroyed$ (): Observable<void> { 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()
|
||||
|
@ -39,6 +39,22 @@ export class CopyPasteContextMenu extends TabContextMenuItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class MiscContextMenu extends TabContextMenuItemProvider {
|
||||
weight = 1
|
||||
|
||||
async getItems (tab: BaseTabComponent): Promise<MenuItemOptions[]> {
|
||||
if (tab instanceof BaseTerminalTabComponent && tab.session?.supportsWorkingDirectory()) {
|
||||
return [{
|
||||
label: 'Copy current path',
|
||||
click: () => tab.copyCurrentPath(),
|
||||
}]
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class LegacyContextMenu extends TabContextMenuItemProvider {
|
||||
|
Loading…
Reference in New Issue
Block a user