From feb4c5bcb6dee1b3e0f982ada919ea6400f34b30 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Wed, 26 Jul 2017 16:04:55 +0200 Subject: [PATCH] tmux wip --- terminus-terminal/package.json | 2 + .../src/components/terminalTab.component.scss | 2 +- terminus-terminal/src/hterm.ts | 6 + terminus-terminal/src/index.ts | 12 +- terminus-terminal/src/tmux.ts | 190 ++++++++++++++++++ 5 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 terminus-terminal/src/tmux.ts diff --git a/terminus-terminal/package.json b/terminus-terminal/package.json index 81a05c58..95e1cccf 100644 --- a/terminus-terminal/package.json +++ b/terminus-terminal/package.json @@ -37,6 +37,8 @@ "terminus-settings": "*" }, "dependencies": { + "@types/async-lock": "0.0.19", + "async-lock": "^1.0.0", "font-manager": "0.2.2", "hterm-umdjs": "1.1.3", "mz": "^2.6.0", diff --git a/terminus-terminal/src/components/terminalTab.component.scss b/terminus-terminal/src/components/terminalTab.component.scss index 5be43d0f..0556f84c 100644 --- a/terminus-terminal/src/components/terminalTab.component.scss +++ b/terminus-terminal/src/components/terminalTab.component.scss @@ -9,7 +9,7 @@ display: block; overflow: hidden; margin: 15px; - transition: opacity ease-out 0.1s; + transition: opacity ease-out 0.25s; opacity: 0; div[style]:last-child { diff --git a/terminus-terminal/src/hterm.ts b/terminus-terminal/src/hterm.ts index 4f76c629..7393e608 100644 --- a/terminus-terminal/src/hterm.ts +++ b/terminus-terminal/src/hterm.ts @@ -66,3 +66,9 @@ hterm.hterm.VT.CSI[' q'] = function (parseState) { this.terminal.cursorMode = arg this.terminal.applyCursorShape() } + +Selection.prototype.collapseToEnd = function () { + try { + this.collapseToEnd() + } catch (err) { ; } +} diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index 68e97efa..fbd77337 100644 --- a/terminus-terminal/src/index.ts +++ b/terminus-terminal/src/index.ts @@ -14,6 +14,7 @@ import { SessionsService } from './services/sessions.service' import { ShellsService } from './services/shells.service' import { ScreenPersistenceProvider } from './persistenceProviders' +import { TMuxPersistenceProvider } from './tmux' import { ButtonProvider } from './buttonProvider' import { RecoveryProvider } from './recoveryProvider' import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator } from './api' @@ -34,18 +35,23 @@ import { hterm } from './hterm' SessionsService, ShellsService, ScreenPersistenceProvider, + TMuxPersistenceProvider, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, { provide: SessionPersistenceProvider, - useFactory: (hostApp: HostAppService, screen: ScreenPersistenceProvider) => { + useFactory: ( + hostApp: HostAppService, + screen: ScreenPersistenceProvider, + tmux: TMuxPersistenceProvider, + ) => { if (hostApp.platform === Platform.Windows) { return null } else { - return screen + return tmux } }, - deps: [HostAppService, ScreenPersistenceProvider], + deps: [HostAppService, ScreenPersistenceProvider, TMuxPersistenceProvider], }, { provide: SettingsTabProvider, useClass: TerminalSettingsTabProvider, multi: true }, { provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true }, diff --git a/terminus-terminal/src/tmux.ts b/terminus-terminal/src/tmux.ts new file mode 100644 index 00000000..d5b18e91 --- /dev/null +++ b/terminus-terminal/src/tmux.ts @@ -0,0 +1,190 @@ +import { Injectable } from '@angular/core' +import * as AsyncLock from 'async-lock' +import { Observable, Subject } from 'rxjs' +import * as childProcess from 'child_process' +import { SessionOptions, SessionPersistenceProvider } from './api' + +const TMUX_CONFIG = ` + set -g status off +` + +export class TMuxBlock { + time: number + number: number + error: boolean + lines: string[] + + constructor (line: string) { + this.time = parseInt(line.split(' ')[1]) + this.number = parseInt(line.split(' ')[2]) + this.lines = [] + } +} + +export class TMuxMessage { + type: string + content: string + + constructor (line: string) { + this.type = line.substring(0, line.indexOf(' ')) + this.content = line.substring(line.indexOf(' ') + 1) + } +} + +export class TMuxCommandProcess { + private process: childProcess.ChildProcess + private rawOutput$ = new Subject() + private line$ = new Subject() + private message$ = new Subject() + private block$ = new Subject() + private response$: Observable + private lock = new AsyncLock({ timeout: 1000 }) + + constructor () { + this.process = childProcess.spawn('tmux', ['-C', '-L', 'terminus', 'new-session', '-A', '-D', '-s', 'control']) + console.log('[tmux] started') + this.process.stdout.on('data', data => { + console.debug('tmux says:', data.toString()) + this.rawOutput$.next(data.toString()) + }) + + let rawBuffer = '' + this.rawOutput$.subscribe(raw => { + rawBuffer += raw + if (rawBuffer.includes('\n')) { + let lines = rawBuffer.split('\n') + rawBuffer = lines.pop() + lines.forEach(line => this.line$.next(line)) + } + }) + + let currentBlock = null + this.line$.subscribe(line => { + if (currentBlock) { + if (line.startsWith('%end ')) { + let block = currentBlock + currentBlock = null + setImmediate(() => { + this.block$.next(block) + }) + } else if (line.startsWith('%error ')) { + let block = currentBlock + block.error = true + currentBlock = null + setImmediate(() => { + this.block$.next(block) + }) + } else { + currentBlock.lines.push(line) + } + } else { + if (line.startsWith('%begin ')) { + currentBlock = new TMuxBlock(line) + } else { + this.message$.next(line) + } + } + }) + + this.response$ = this.block$.skip(1).share() + + this.block$.subscribe(block => { + console.debug('[tmux] block:', block) + }) + + this.response$.subscribe(response => { + console.debug('[tmux] response:', response) + }) + + this.message$.subscribe(message => { + console.debug('[tmux] message:', message) + }) + } + + command (command: string): Promise { + return this.lock.acquire('key', () => { + let p = this.response$.take(1).toPromise() + console.debug('[tmux] command:', command) + this.process.stdin.write(command + '\n') + p.then(x => console.log('promise then', x)) + p.catch(x => console.log('promise catch', x)) + return p + }).then(response => { + if (response.error) { + throw response + } + return response + }) as Promise + } + + destroy () { + this.rawOutput$.complete() + this.line$.complete() + this.block$.complete() + this.message$.complete() + this.process.kill('SIGTERM') + } +} + +export class TMux { + private process: TMuxCommandProcess + + constructor () { + this.process = new TMuxCommandProcess() + TMUX_CONFIG.split('\n').filter(x => x).forEach(async (line) => { + await this.process.command(line) + }) + } + + async create (id: string, options: SessionOptions): Promise { + let args = [options.command].concat(options.args) + let cmd = args.map(x => `"${x.replace('"', '\\"')}"`) + await this.process.command( + `new-session -s "${id}" -d` + + (options.cwd ? ` -c '${options.cwd.replace("'", "\\'")}'` : '') + + ` '${cmd}'` + ) + } + + async list (): Promise { + let block = await this.process.command('list-sessions -F "#{session_name}"') + return block.lines + } + + async terminate (id: string): Promise { + await this.process.command(`kill-session -t ${id}`) + } +} + +@Injectable() +export class TMuxPersistenceProvider extends SessionPersistenceProvider { + private tmux: TMux + + constructor () { + super() + this.tmux = new TMux() + } + + async attachSession (recoveryId: any): Promise { + let sessions = await this.tmux.list() + if (!sessions.includes(recoveryId)) { + return null + } + return { + command: 'tmux', + args: ['-L', 'terminus', 'attach-session', '-d', '-t', recoveryId], + recoveryId, + } + } + + async startSession (options: SessionOptions): Promise { + // TODO env + let recoveryId = Date.now().toString() + await this.tmux.create(recoveryId, options) + return recoveryId + } + + async terminateSession (recoveryId: string): Promise { + await this.tmux.terminate(recoveryId) + } +}