From b7bac490d24b71ecb9050679828012020f4b0c0f Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sat, 25 Mar 2017 18:12:43 +0100 Subject: [PATCH] . --- app/src/api/index.ts | 5 +- app/src/api/tabRecovery.ts | 6 +- app/src/api/toolbarButtonProvider.ts | 6 +- app/src/app.module.ts | 1 + app/src/components/appRoot.ts | 14 +-- app/src/link-highlighter/api.ts | 6 + app/src/link-highlighter/decorator.ts | 126 +++++++++++++++++++ app/src/link-highlighter/handlers.ts | 36 ++++++ app/src/link-highlighter/index.ts | 20 +++ app/src/plugin.hyperlinks.ts | 139 --------------------- app/src/services/app.ts | 12 +- app/src/settings/buttonProvider.ts | 8 +- app/src/settings/index.ts | 16 +-- app/src/settings/recoveryProvider.ts | 4 +- app/src/terminal/api.ts | 3 + app/src/terminal/buttonProvider.ts | 6 +- app/src/terminal/components/terminalTab.ts | 11 +- app/src/terminal/index.ts | 16 +-- app/src/terminal/recoveryProvider.ts | 8 +- 19 files changed, 239 insertions(+), 204 deletions(-) create mode 100644 app/src/link-highlighter/api.ts create mode 100644 app/src/link-highlighter/decorator.ts create mode 100644 app/src/link-highlighter/handlers.ts create mode 100644 app/src/link-highlighter/index.ts create mode 100644 app/src/terminal/api.ts diff --git a/app/src/api/index.ts b/app/src/api/index.ts index 5b838f6b..39291908 100644 --- a/app/src/api/index.ts +++ b/app/src/api/index.ts @@ -1,6 +1,7 @@ export { Tab } from './tab' -export { TabRecoveryProviderType, ITabRecoveryProvider } from './tabRecovery' -export { ToolbarButtonProviderType, IToolbarButton, IToolbarButtonProvider } from './toolbarButtonProvider' +export { TabRecoveryProvider } from './tabRecovery' +export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider' export { AppService } from 'services/app' export { PluginsService } from 'services/plugins' +export { ElectronService } from 'services/electron' diff --git a/app/src/api/tabRecovery.ts b/app/src/api/tabRecovery.ts index 1e245017..4f7f87fb 100644 --- a/app/src/api/tabRecovery.ts +++ b/app/src/api/tabRecovery.ts @@ -1,7 +1,5 @@ import { Tab } from './tab' -export interface ITabRecoveryProvider { - recover (recoveryToken: any): Tab +export abstract class TabRecoveryProvider { + abstract recover (recoveryToken: any): Tab } - -export const TabRecoveryProviderType = 'app:TabRecoveryProviderType' diff --git a/app/src/api/toolbarButtonProvider.ts b/app/src/api/toolbarButtonProvider.ts index 4a0e3e82..bb317028 100644 --- a/app/src/api/toolbarButtonProvider.ts +++ b/app/src/api/toolbarButtonProvider.ts @@ -5,8 +5,6 @@ export interface IToolbarButton { click: () => void } -export interface IToolbarButtonProvider { - provide (): IToolbarButton[] +export abstract class ToolbarButtonProvider { + abstract provide (): IToolbarButton[] } - -export const ToolbarButtonProviderType = 'app:ToolbarButtonProviderType' diff --git a/app/src/app.module.ts b/app/src/app.module.ts index 7db40aac..73555cc9 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -27,6 +27,7 @@ import { TitleBarComponent } from 'components/titleBar' let plugins = [ require('./settings').default, require('./terminal').default, + require('./link-highlighter').default, ] @NgModule({ diff --git a/app/src/components/appRoot.ts b/app/src/components/appRoot.ts index ee94c6eb..f254ff32 100644 --- a/app/src/components/appRoot.ts +++ b/app/src/components/appRoot.ts @@ -1,4 +1,4 @@ -import { Component, trigger, style, animate, transition, state } from '@angular/core' +import { Component, Inject, trigger, style, animate, transition, state } from '@angular/core' import { ToasterConfig } from 'angular2-toaster' import { ElectronService } from 'services/electron' @@ -8,9 +8,8 @@ import { LogService } from 'services/log' import { QuitterService } from 'services/quitter' import { ConfigService } from 'services/config' import { DockingService } from 'services/docking' -import { PluginsService } from 'services/plugins' -import { AppService, IToolbarButton, IToolbarButtonProvider, ToolbarButtonProviderType } from 'api' +import { AppService, IToolbarButton, ToolbarButtonProvider } from 'api' import 'angular2-toaster/lib/toaster.css' import 'global.less' @@ -49,8 +48,8 @@ export class AppRootComponent { public hostApp: HostAppService, public hotkeys: HotkeysService, public config: ConfigService, - private plugins: PluginsService, public app: AppService, + @Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[], log: LogService, _quitter: QuitterService, ) { @@ -129,10 +128,9 @@ export class AppRootComponent { getToolbarButtons (aboveZero: boolean): IToolbarButton[] { let buttons: IToolbarButton[] = [] - this.plugins.getAll(ToolbarButtonProviderType) - .forEach((provider) => { - buttons = buttons.concat(provider.provide()) - }) + this.toolbarButtonProviders.forEach((provider) => { + buttons = buttons.concat(provider.provide()) + }) return buttons .filter((button) => (button.weight > 0) === aboveZero) .sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0)) diff --git a/app/src/link-highlighter/api.ts b/app/src/link-highlighter/api.ts new file mode 100644 index 00000000..663462c0 --- /dev/null +++ b/app/src/link-highlighter/api.ts @@ -0,0 +1,6 @@ +export abstract class LinkHandler { + regex: string + convert (uri: string): string { return uri } + verify (_uri: string): boolean { return true } + abstract handle (uri: string): void +} diff --git a/app/src/link-highlighter/decorator.ts b/app/src/link-highlighter/decorator.ts new file mode 100644 index 00000000..89c9be00 --- /dev/null +++ b/app/src/link-highlighter/decorator.ts @@ -0,0 +1,126 @@ +/* + This plugin is based on Hyperterm Hyperlinks: + https://github.com/zeit/hyperlinks/blob/master/index.js +*/ + +import { Inject, Injectable } from '@angular/core' +import { LinkHandler } from './api' +import { TerminalDecorator } from '../terminal/api' + +const debounceDelay = 500 + + +@Injectable() +export class LinkHighlighterDecorator extends TerminalDecorator { + constructor (@Inject(LinkHandler) private handlers: LinkHandler[]) { + super() + } + + decorate (terminal): void { + const Screen = terminal.screen_.constructor + if (Screen._linkHighlighterInstalled) { + return + } + Screen._linkHighlighterInstalled = true + + const oldInsertString = Screen.prototype.insertString + const oldDeleteChars = Screen.prototype.deleteChars + let self = this + Screen.prototype.insertString = function (...args) { + let ret = oldInsertString.bind(this)(...args) + self.debounceInsertLinks(this) + return ret + } + Screen.prototype.deleteChars = function (...args) { + let ret = oldDeleteChars.bind(this)(...args) + self.debounceInsertLinks(this) + return ret + } + } + + debounceInsertLinks (screen) { + if (screen.__insertLinksTimeout) { + screen.__insertLinksRebounce = true + } else { + screen.__insertLinksTimeout = window.setTimeout(() => { + this.insertLinks(screen) + screen.__insertLinksTimeout = null + if (screen.__insertLinksRebounce) { + screen.__insertLinksRebounce = false + this.debounceInsertLinks(screen) + } + }, debounceDelay) + } + } + + insertLinks (screen) { + if ('#text' === screen.cursorNode_.nodeName) { + // replace text node to element + const cursorNode = document.createElement('span'); + cursorNode.textContent = screen.cursorNode_.textContent; + screen.cursorRowNode_.replaceChild(cursorNode, screen.cursorNode_); + screen.cursorNode_ = cursorNode; + } + + const traverse = (parentNode: Node) => { + Array.from(parentNode.childNodes).forEach((node) => { + if (node.nodeName == '#text') { + parentNode.replaceChild(this.urlizeNode(node), node) + } else if (node.nodeName != 'A') { + traverse(node) + } + }) + } + + screen.rowsArray.forEach((x) => traverse(x)) + } + + urlizeNode (node) { + let matches = [] + this.handlers.forEach((handler) => { + let regex = new RegExp(handler.regex, 'gi') + let match + while (match = regex.exec(node.textContent)) { + let uri = handler.convert(match[0]) + if (!handler.verify(uri)) { + continue; + } + matches.push({ + start: regex.lastIndex - match[0].length, + end: regex.lastIndex, + text: match[0], + uri, + handler + }) + } + }) + + if (matches.length == 0) { + return node + } + + matches.sort((a, b) => a.start < b.start ? -1 : 1) + + let span = document.createElement('span') + let position = 0 + matches.forEach((match) => { + if (match.start < position) { + return + } + if (match.start > position) { + span.appendChild(document.createTextNode(node.textContent.slice(position, match.start))) + } + + let a = document.createElement('a') + a.textContent = match.text + a.addEventListener('click', () => { + match.handler.handle(match.uri) + }) + span.appendChild(a) + + position = match.end + }) + span.appendChild(document.createTextNode(node.textContent.slice(position))) + return span + } +} diff --git a/app/src/link-highlighter/handlers.ts b/app/src/link-highlighter/handlers.ts new file mode 100644 index 00000000..915c6bfd --- /dev/null +++ b/app/src/link-highlighter/handlers.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs' + +import { Injectable } from '@angular/core' +import { LinkHandler } from './api' +import { ElectronService } from 'api' + + +@Injectable() +export class URLHandler extends LinkHandler { + regex = 'http(s)?://[^\\s;\'"]+[^,;\\s]' + + constructor (private electron: ElectronService) { + super() + } + + handle (uri: string) { + this.electron.shell.openExternal(uri) + } +} + +@Injectable() +export class FileHandler extends LinkHandler { + regex = '/[^\\s.,;\'"]+' + + constructor (private electron: ElectronService) { + super() + } + + verify (uri: string) { + return fs.existsSync(uri) + } + + handle (uri: string) { + this.electron.shell.openExternal('file://' + uri) + } +} diff --git a/app/src/link-highlighter/index.ts b/app/src/link-highlighter/index.ts new file mode 100644 index 00000000..93eeb468 --- /dev/null +++ b/app/src/link-highlighter/index.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' + +import { LinkHandler } from './api' +import { FileHandler, URLHandler } from './handlers' +import { TerminalDecorator } from '../terminal/api' +import { LinkHighlighterDecorator } from './decorator' + + +@NgModule({ + providers: [ + { provide: LinkHandler, useClass: FileHandler, multi: true }, + { provide: LinkHandler, useClass: URLHandler, multi: true }, + { provide: TerminalDecorator, useClass: LinkHighlighterDecorator, multi: true }, + ], +}) +class LinkHighlighterModule { +} + + +export default LinkHighlighterModule diff --git a/app/src/plugin.hyperlinks.ts b/app/src/plugin.hyperlinks.ts index e135e892..e69de29b 100644 --- a/app/src/plugin.hyperlinks.ts +++ b/app/src/plugin.hyperlinks.ts @@ -1,139 +0,0 @@ -import * as fs from 'fs' -import { ElectronService } from 'services/electron' - -const debounceDelay = 500 - -abstract class Handler { - constructor (protected plugin) { } - regex: string - convert (uri: string): string { return uri } - verify (_uri: string): boolean { return true } - abstract handle (uri: string): void -} - -class URLHandler extends Handler { - regex = 'http(s)?://[^\\s;\'"]+[^.,;\\s]' - - handle (uri: string) { - this.plugin.electron.shell.openExternal(uri) - } -} - -class FileHandler extends Handler { - regex = '/[^\\s.,;\'"]+' - - verify (uri: string) { - return fs.existsSync(uri) - } - - handle (uri: string) { - this.plugin.electron.shell.openExternal('file://' + uri) - } -} - -export default class HyperlinksPlugin { - handlers = [] - handlerClasses = [ - URLHandler, - FileHandler, - ] - electron: ElectronService - - constructor ({ electron }) { - this.electron = electron - this.handlers = this.handlerClasses.map((x) => new x(this)) - } - - preTerminalInit ({ terminal }) { - const oldInsertString = terminal.screen_.constructor.prototype.insertString - const oldDeleteChars = terminal.screen_.constructor.prototype.deleteChars - terminal.screen_.insertString = (...args) => { - let ret = oldInsertString.bind(terminal.screen_)(...args) - this.debounceInsertLinks(terminal.screen_) - return ret - } - terminal.screen_.deleteChars = (...args) => { - let ret = oldDeleteChars.bind(terminal.screen_)(...args) - this.debounceInsertLinks(terminal.screen_) - return ret - } - } - - debounceInsertLinks (screen) { - if (screen.__insertLinksTimeout) { - screen.__insertLinksRebounce = true - } else { - screen.__insertLinksTimeout = window.setTimeout(() => { - this.insertLinks(screen) - screen.__insertLinksTimeout = null - if (screen.__insertLinksRebounce) { - screen.__insertLinksRebounce = false - this.debounceInsertLinks(screen) - } - }, debounceDelay) - } - } - - insertLinks (screen) { - const traverse = (parentNode: Node) => { - Array.from(parentNode.childNodes).forEach((node) => { - if (node.nodeName == '#text') { - parentNode.replaceChild(this.urlizeNode(node), node) - } else if (node.nodeName != 'A') { - traverse(node) - } - }) - } - - screen.rowsArray.forEach((x) => traverse(x)) - } - - urlizeNode (node) { - let matches = [] - this.handlers.forEach((handler) => { - let regex = new RegExp(handler.regex, 'gi') - let match - while (match = regex.exec(node.textContent)) { - let uri = handler.convert(match[0]) - if (!handler.verify(uri)) { - continue; - } - matches.push({ - start: regex.lastIndex - match[0].length, - end: regex.lastIndex, - text: match[0], - uri, - handler - }) - } - }) - - if (matches.length == 0) { - return node - } - - matches.sort((a, b) => a.start < b.start ? -1 : 1) - - let span = document.createElement('span') - let position = 0 - matches.forEach((match) => { - if (match.start < position) { - return - } - if (match.start > position) { - span.appendChild(document.createTextNode(node.textContent.slice(position, match.start))) - } - - let a = document.createElement('a') - a.textContent = match.text - a.addEventListener('click', () => { - match.handler.handle(match.uri) - }) - span.appendChild(a) - - position = match.end - }) - span.appendChild(document.createTextNode(node.textContent.slice(position))) - return span - } -} diff --git a/app/src/services/app.ts b/app/src/services/app.ts index 806fb56a..9d301393 100644 --- a/app/src/services/app.ts +++ b/app/src/services/app.ts @@ -1,8 +1,7 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { Logger, LogService } from 'services/log' import { Tab } from 'api/tab' -import { PluginsService } from 'services/plugins' -import { ITabRecoveryProvider, TabRecoveryProviderType } from 'api/tabRecovery' +import { TabRecoveryProvider } from 'api/tabRecovery' @Injectable() @@ -13,7 +12,7 @@ export class AppService { logger: Logger constructor ( - private plugins: PluginsService, + @Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[], log: LogService, ) { this.logger = log.create('app') @@ -83,9 +82,8 @@ export class AppService { restoreTabs () { if (window.localStorage.tabsRecovery) { - let providers = this.plugins.getAll(TabRecoveryProviderType) JSON.parse(window.localStorage.tabsRecovery).forEach((token) => { - for (let provider of providers) { + for (let provider of this.tabRecoveryProviders) { try { let tab = provider.recover(token) if (tab) { @@ -93,8 +91,8 @@ export class AppService { return } } catch (_) { } - this.logger.warn('Cannot restore tab from the token:', token) } + this.logger.warn('Cannot restore tab from the token:', token) }) this.saveTabs() } diff --git a/app/src/settings/buttonProvider.ts b/app/src/settings/buttonProvider.ts index 16c720a1..6133f1ef 100644 --- a/app/src/settings/buttonProvider.ts +++ b/app/src/settings/buttonProvider.ts @@ -1,13 +1,15 @@ import { Injectable } from '@angular/core' -import { IToolbarButtonProvider, IToolbarButton, AppService } from 'api' +import { ToolbarButtonProvider, IToolbarButton, AppService } from 'api' import { SettingsTab } from './tab' @Injectable() -export class ButtonProvider implements IToolbarButtonProvider { +export class ButtonProvider extends ToolbarButtonProvider { constructor ( private app: AppService, - ) { } + ) { + super() + } provide (): IToolbarButton[] { return [{ diff --git a/app/src/settings/index.ts b/app/src/settings/index.ts index a8604df9..d8561f0c 100644 --- a/app/src/settings/index.ts +++ b/app/src/settings/index.ts @@ -1,5 +1,5 @@ -import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' +import { BrowserModule } from '@angular/platform-browser' import { FormsModule } from '@angular/forms' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' @@ -9,7 +9,7 @@ import { HotkeyHintComponent } from './components/hotkeyHint' import { HotkeyInputModalComponent } from './components/hotkeyInputModal' import { SettingsPaneComponent } from './components/settingsPane' -import { PluginsService, ToolbarButtonProviderType, TabRecoveryProviderType } from 'api' +import { ToolbarButtonProvider, TabRecoveryProvider } from 'api' import { ButtonProvider } from './buttonProvider' import { RecoveryProvider } from './recoveryProvider' @@ -22,8 +22,8 @@ import { RecoveryProvider } from './recoveryProvider' NgbModule, ], providers: [ - ButtonProvider, - RecoveryProvider, + { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, + { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true } ], entryComponents: [ HotkeyInputModalComponent, @@ -38,14 +38,6 @@ import { RecoveryProvider } from './recoveryProvider' ], }) class SettingsModule { - constructor ( - plugins: PluginsService, - buttonProvider: ButtonProvider, - recoveryProvider: RecoveryProvider, - ) { - plugins.register(ToolbarButtonProviderType, buttonProvider, 1) - plugins.register(TabRecoveryProviderType, recoveryProvider) - } } diff --git a/app/src/settings/recoveryProvider.ts b/app/src/settings/recoveryProvider.ts index 6d1d321a..074a5751 100644 --- a/app/src/settings/recoveryProvider.ts +++ b/app/src/settings/recoveryProvider.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core' -import { Tab, ITabRecoveryProvider } from 'api' +import { Tab, TabRecoveryProvider } from 'api' import { SettingsTab } from './tab' @Injectable() -export class RecoveryProvider implements ITabRecoveryProvider { +export class RecoveryProvider extends TabRecoveryProvider { recover (recoveryToken: any): Tab { if (recoveryToken.type == 'app:settings') { return new SettingsTab() diff --git a/app/src/terminal/api.ts b/app/src/terminal/api.ts new file mode 100644 index 00000000..26d9278c --- /dev/null +++ b/app/src/terminal/api.ts @@ -0,0 +1,3 @@ +export abstract class TerminalDecorator { + abstract decorate (terminal): void +} diff --git a/app/src/terminal/buttonProvider.ts b/app/src/terminal/buttonProvider.ts index 05497ece..149d645c 100644 --- a/app/src/terminal/buttonProvider.ts +++ b/app/src/terminal/buttonProvider.ts @@ -1,16 +1,16 @@ import { Injectable } from '@angular/core' -import { IToolbarButtonProvider, IToolbarButton, AppService } from 'api' +import { ToolbarButtonProvider, IToolbarButton, AppService } from 'api' import { SessionsService } from './services/sessions' import { TerminalTab } from './tab' @Injectable() -export class ButtonProvider implements IToolbarButtonProvider { +export class ButtonProvider extends ToolbarButtonProvider { constructor ( private app: AppService, private sessions: SessionsService, ) { - + super() } provide (): IToolbarButton[] { diff --git a/app/src/terminal/components/terminalTab.ts b/app/src/terminal/components/terminalTab.ts index 913a4e38..38d6187e 100644 --- a/app/src/terminal/components/terminalTab.ts +++ b/app/src/terminal/components/terminalTab.ts @@ -1,11 +1,11 @@ import { Subscription } from 'rxjs' -import { Component, NgZone, Output, EventEmitter, ElementRef } from '@angular/core' +import { Component, NgZone, Output, Inject, EventEmitter, ElementRef } from '@angular/core' import { ConfigService } from 'services/config' -import { PluginsService } from 'services/plugins' import { BaseTabComponent } from 'components/baseTab' import { TerminalTab } from '../tab' +import { TerminalDecorator } from '../api' import { hterm, preferenceManager } from '../hterm' @@ -27,7 +27,7 @@ export class TerminalTabComponent extends BaseTabComponent { private zone: NgZone, private elementRef: ElementRef, public config: ConfigService, - private plugins: PluginsService, + @Inject(TerminalDecorator) private decorators: TerminalDecorator[], ) { super() this.startupTime = performance.now() @@ -42,7 +42,9 @@ export class TerminalTabComponent extends BaseTabComponent { }) this.terminal = new hterm.hterm.Terminal() - //this.pluginDispatcher.emit('preTerminalInit', { terminal: this.terminal }) + this.decorators.forEach((decorator) => { + decorator.decorate(this.terminal) + }) this.terminal.setWindowTitle = (title) => { this.zone.run(() => { this.title = title @@ -77,7 +79,6 @@ export class TerminalTabComponent extends BaseTabComponent { } this.terminal.decorate(this.elementRef.nativeElement) this.configure() - //this.pluginDispatcher.emit('postTerminalInit', { terminal: this.terminal }) } configure () { diff --git a/app/src/terminal/index.ts b/app/src/terminal/index.ts index b3a3e328..81c50eb8 100644 --- a/app/src/terminal/index.ts +++ b/app/src/terminal/index.ts @@ -1,8 +1,8 @@ -import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' +import { BrowserModule } from '@angular/platform-browser' import { FormsModule } from '@angular/forms' -import { PluginsService, ToolbarButtonProviderType, TabRecoveryProviderType } from 'api' +import { ToolbarButtonProvider, TabRecoveryProvider } from 'api' import { TerminalTabComponent } from './components/terminalTab' import { SessionsService } from './services/sessions' @@ -16,9 +16,9 @@ import { RecoveryProvider } from './recoveryProvider' FormsModule, ], providers: [ - ButtonProvider, + { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, + { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, SessionsService, - RecoveryProvider, ], entryComponents: [ TerminalTabComponent, @@ -28,14 +28,6 @@ import { RecoveryProvider } from './recoveryProvider' ], }) class TerminalModule { - constructor ( - plugins: PluginsService, - buttonProvider: ButtonProvider, - recoveryProvider: RecoveryProvider, - ) { - plugins.register(ToolbarButtonProviderType, buttonProvider) - plugins.register(TabRecoveryProviderType, recoveryProvider) - } } diff --git a/app/src/terminal/recoveryProvider.ts b/app/src/terminal/recoveryProvider.ts index bccb4247..8149fbbd 100644 --- a/app/src/terminal/recoveryProvider.ts +++ b/app/src/terminal/recoveryProvider.ts @@ -1,12 +1,14 @@ import { Injectable } from '@angular/core' -import { Tab, ITabRecoveryProvider } from 'api' +import { Tab, TabRecoveryProvider } from 'api' import { TerminalTab } from './tab' import { SessionsService } from './services/sessions' @Injectable() -export class RecoveryProvider implements ITabRecoveryProvider { - constructor (private sessions: SessionsService) { } +export class RecoveryProvider extends TabRecoveryProvider { + constructor (private sessions: SessionsService) { + super() + } recover (recoveryToken: any): Tab { if (recoveryToken.type == 'app:terminal') {