baseTerminalTab.component.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. import { Observable, Subject, Subscription, first, auditTime } from 'rxjs'
  2. import { Spinner } from 'cli-spinner'
  3. import colors from 'ansi-colors'
  4. import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core'
  5. import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
  6. import { AppService, ConfigService, BaseTabComponent, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService, HostWindowService, ResettableTimeout, TranslateService } from 'tabby-core'
  7. import { BaseSession } from '../session'
  8. import { Frontend } from '../frontends/frontend'
  9. import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend'
  10. import { ResizeEvent } from './interfaces'
  11. import { TerminalDecorator } from './decorator'
  12. import { SearchPanelComponent } from '../components/searchPanel.component'
  13. /**
  14. * A class to base your custom terminal tabs on
  15. */
  16. export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
  17. static template: string = require<string>('../components/baseTerminalTab.component.pug')
  18. static styles: string[] = [require<string>('../components/baseTerminalTab.component.scss')]
  19. static animations: AnimationTriggerMetadata[] = [
  20. trigger('toolbarSlide', [
  21. transition(':enter', [
  22. style({
  23. transform: 'translateY(-25%)',
  24. opacity: '0',
  25. }),
  26. animate('100ms ease-out', style({
  27. transform: 'translateY(0%)',
  28. opacity: '1',
  29. })),
  30. ]),
  31. transition(':leave', [
  32. animate('100ms ease-out', style({
  33. transform: 'translateY(-25%)',
  34. opacity: '0',
  35. })),
  36. ]),
  37. ]),
  38. trigger('panelSlide', [
  39. transition(':enter', [
  40. style({
  41. transform: 'translateY(25%)',
  42. opacity: '0',
  43. }),
  44. animate('100ms ease-out', style({
  45. transform: 'translateY(0%)',
  46. opacity: '1',
  47. })),
  48. ]),
  49. transition(':leave', [
  50. animate('100ms ease-out', style({
  51. transform: 'translateY(25%)',
  52. opacity: '0',
  53. })),
  54. ]),
  55. ]),
  56. ]
  57. session: BaseSession|null = null
  58. savedState?: any
  59. savedStateIsLive = false
  60. @Input() zoom = 0
  61. @Input() showSearchPanel = false
  62. /** @hidden */
  63. @ViewChild('content') content
  64. /** @hidden */
  65. @HostBinding('style.background-color') backgroundColor: string|null = null
  66. /** @hidden */
  67. @HostBinding('class.toolbar-enabled') enableToolbar = false
  68. /** @hidden */
  69. @HostBinding('class.toolbar-pinned') pinToolbar = false
  70. /** @hidden */
  71. @HostBinding('class.toolbar-revealed') revealToolbar = false
  72. frontend?: Frontend
  73. /** @hidden */
  74. frontendIsReady = false
  75. frontendReady = new Subject<void>()
  76. size: ResizeEvent
  77. /**
  78. * Enables normall passthrough from session output to terminal input
  79. */
  80. enablePassthrough = true
  81. /**
  82. * Disables display of dynamic window/tab title provided by the shell
  83. */
  84. disableDynamicTitle = false
  85. alternateScreenActive = false
  86. @ViewChild(SearchPanelComponent, { 'static': false }) searchPanel?: SearchPanelComponent
  87. // Deps start
  88. config: ConfigService
  89. element: ElementRef
  90. protected zone: NgZone
  91. protected app: AppService
  92. protected hostApp: HostAppService
  93. protected hotkeys: HotkeysService
  94. protected platform: PlatformService
  95. protected notifications: NotificationsService
  96. protected log: LogService
  97. protected decorators: TerminalDecorator[] = []
  98. protected contextMenuProviders: TabContextMenuItemProvider[]
  99. protected hostWindow: HostWindowService
  100. protected translate: TranslateService
  101. // Deps end
  102. protected logger: Logger
  103. protected output = new Subject<string>()
  104. protected sessionChanged = new Subject<BaseSession|null>()
  105. private bellPlayer: HTMLAudioElement
  106. private termContainerSubscriptions = new SubscriptionContainer()
  107. private allFocusModeSubscription: Subscription|null = null
  108. private sessionHandlers = new SubscriptionContainer()
  109. private spinner = new Spinner({
  110. stream: {
  111. write: x => {
  112. try {
  113. this.writeRaw(x)
  114. } catch {
  115. this.spinner.stop()
  116. }
  117. },
  118. },
  119. })
  120. private spinnerActive = false
  121. private spinnerPaused = false
  122. private toolbarRevealTimeout = new ResettableTimeout(() => {
  123. this.revealToolbar = false
  124. }, 1000)
  125. private frontendWriteLock = Promise.resolve()
  126. get input$ (): Observable<Buffer> {
  127. if (!this.frontend) {
  128. throw new Error('Frontend not ready')
  129. }
  130. return this.frontend.input$
  131. }
  132. get output$ (): Observable<string> { return this.output }
  133. get resize$ (): Observable<ResizeEvent> {
  134. if (!this.frontend) {
  135. throw new Error('Frontend not ready')
  136. }
  137. return this.frontend.resize$
  138. }
  139. get alternateScreenActive$ (): Observable<boolean> {
  140. if (!this.frontend) {
  141. throw new Error('Frontend not ready')
  142. }
  143. return this.frontend.alternateScreenActive$
  144. }
  145. get frontendReady$ (): Observable<void> { return this.frontendReady }
  146. get sessionChanged$ (): Observable<BaseSession|null> { return this.sessionChanged }
  147. constructor (protected injector: Injector) {
  148. super()
  149. this.config = injector.get(ConfigService)
  150. this.element = injector.get(ElementRef)
  151. this.zone = injector.get(NgZone)
  152. this.app = injector.get(AppService)
  153. this.hostApp = injector.get(HostAppService)
  154. this.hotkeys = injector.get(HotkeysService)
  155. this.platform = injector.get(PlatformService)
  156. this.notifications = injector.get(NotificationsService)
  157. this.log = injector.get(LogService)
  158. this.decorators = injector.get<any>(TerminalDecorator, null, InjectFlags.Optional) as TerminalDecorator[]
  159. this.contextMenuProviders = injector.get<any>(TabContextMenuItemProvider, null, InjectFlags.Optional) as TabContextMenuItemProvider[]
  160. this.hostWindow = injector.get(HostWindowService)
  161. this.translate = injector.get(TranslateService)
  162. this.logger = this.log.create('baseTerminalTab')
  163. this.setTitle(this.translate.instant('Terminal'))
  164. this.subscribeUntilDestroyed(this.hotkeys.unfilteredHotkey$, async hotkey => {
  165. if (!this.hasFocus) {
  166. return
  167. }
  168. if (hotkey === 'search') {
  169. this.showSearchPanel = true
  170. setImmediate(() => {
  171. const input = this.element.nativeElement.querySelector('.search-input')
  172. input?.focus()
  173. input?.select()
  174. })
  175. }
  176. })
  177. this.subscribeUntilDestroyed(this.hotkeys.hotkey$, async hotkey => {
  178. if (!this.hasFocus) {
  179. return
  180. }
  181. switch (hotkey) {
  182. case 'ctrl-c':
  183. if (this.frontend?.getSelection()) {
  184. this.frontend.copySelection()
  185. this.frontend.clearSelection()
  186. this.notifications.notice(this.translate.instant('Copied'))
  187. } else {
  188. this.forEachFocusedTerminalPane(tab => tab.sendInput('\x03'))
  189. }
  190. break
  191. case 'copy':
  192. this.frontend?.copySelection()
  193. this.frontend?.clearSelection()
  194. this.notifications.notice(this.translate.instant('Copied'))
  195. break
  196. case 'paste':
  197. this.forEachFocusedTerminalPane(tab => tab.paste())
  198. break
  199. case 'select-all':
  200. this.frontend?.selectAll()
  201. break
  202. case 'clear':
  203. this.forEachFocusedTerminalPane(tab => tab.frontend?.clear())
  204. break
  205. case 'zoom-in':
  206. this.forEachFocusedTerminalPane(tab => tab.zoomIn())
  207. break
  208. case 'zoom-out':
  209. this.forEachFocusedTerminalPane(tab => tab.zoomOut())
  210. break
  211. case 'reset-zoom':
  212. this.forEachFocusedTerminalPane(tab => tab.resetZoom())
  213. break
  214. case 'previous-word':
  215. this.forEachFocusedTerminalPane(tab => {
  216. tab.sendInput({
  217. [Platform.Windows]: '\x1b[1;5D',
  218. [Platform.macOS]: '\x1bb',
  219. [Platform.Linux]: '\x1bb',
  220. }[this.hostApp.platform])
  221. })
  222. break
  223. case 'next-word':
  224. this.forEachFocusedTerminalPane(tab => {
  225. tab.sendInput({
  226. [Platform.Windows]: '\x1b[1;5C',
  227. [Platform.macOS]: '\x1bf',
  228. [Platform.Linux]: '\x1bf',
  229. }[this.hostApp.platform])
  230. })
  231. break
  232. case 'delete-line':
  233. this.forEachFocusedTerminalPane(tab => {
  234. tab.sendInput('\x1bw')
  235. })
  236. break
  237. case 'delete-previous-word':
  238. this.forEachFocusedTerminalPane(tab => {
  239. tab.sendInput('\x1b\x7f')
  240. })
  241. break
  242. case 'delete-next-word':
  243. this.forEachFocusedTerminalPane(tab => {
  244. tab.sendInput({
  245. [Platform.Windows]: '\x1bd\x1b[3;5~',
  246. [Platform.macOS]: '\x1bd',
  247. [Platform.Linux]: '\x1bd',
  248. }[this.hostApp.platform])
  249. })
  250. break
  251. case 'pane-focus-all':
  252. this.focusAllPanes()
  253. break
  254. case 'copy-current-path':
  255. this.copyCurrentPath()
  256. break
  257. case 'scroll-to-top':
  258. this.frontend?.scrollToTop()
  259. break
  260. case 'scroll-up':
  261. this.frontend?.scrollPages(-1)
  262. break
  263. case 'scroll-down':
  264. this.frontend?.scrollPages(1)
  265. break
  266. case 'scroll-to-bottom':
  267. this.frontend?.scrollToBottom()
  268. break
  269. }
  270. })
  271. this.bellPlayer = document.createElement('audio')
  272. this.bellPlayer.src = require('../bell.ogg').default
  273. this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
  274. }
  275. /** @hidden */
  276. ngOnInit (): void {
  277. this.pinToolbar = this.enableToolbar && (window.localStorage.pinTerminalToolbar ?? 'true') === 'true'
  278. this.focused$.subscribe(() => {
  279. this.configure()
  280. this.frontend?.focus()
  281. })
  282. const cls: new (..._) => Frontend = {
  283. xterm: XTermFrontend,
  284. 'xterm-webgl': XTermWebGLFrontend,
  285. }[this.config.store.terminal.frontend] ?? XTermFrontend
  286. this.frontend = new cls(this.injector)
  287. this.frontendReady$.pipe(first()).subscribe(() => {
  288. this.onFrontendReady()
  289. })
  290. this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
  291. this.size = { columns, rows }
  292. this.frontendReady.next()
  293. this.frontendReady.complete()
  294. this.config.enabledServices(this.decorators).forEach(decorator => {
  295. try {
  296. decorator.attach(this)
  297. } catch (e) {
  298. this.logger.warn('Decorator attach() throws', e)
  299. }
  300. })
  301. setTimeout(() => {
  302. this.session?.resize(columns, rows)
  303. }, 1000)
  304. this.session?.releaseInitialDataBuffer()
  305. this.sessionChanged$.subscribe(() => {
  306. this.session?.releaseInitialDataBuffer()
  307. })
  308. })
  309. this.alternateScreenActive$.subscribe(x => {
  310. this.alternateScreenActive = x
  311. })
  312. setImmediate(async () => {
  313. if (this.hasFocus) {
  314. await this.frontend?.attach(this.content.nativeElement)
  315. this.frontend?.configure()
  316. } else {
  317. this.focused$.pipe(first()).subscribe(async () => {
  318. await this.frontend?.attach(this.content.nativeElement)
  319. this.frontend?.configure()
  320. })
  321. }
  322. })
  323. this.attachTermContainerHandlers()
  324. this.configure()
  325. setTimeout(() => {
  326. this.output.subscribe(() => {
  327. this.displayActivity()
  328. })
  329. }, 1000)
  330. this.frontend.bell$.subscribe(() => {
  331. if (this.config.store.terminal.bell === 'visual') {
  332. this.frontend?.visualBell()
  333. }
  334. if (this.config.store.terminal.bell === 'audible') {
  335. this.bellPlayer.play()
  336. }
  337. })
  338. this.frontend.focus()
  339. this.blurred$.subscribe(() => {
  340. this.cancelFocusAllPanes()
  341. })
  342. }
  343. protected onFrontendReady (): void {
  344. this.frontendIsReady = true
  345. if (this.savedState) {
  346. this.frontend!.restoreState(this.savedState)
  347. if (!this.savedStateIsLive) {
  348. this.frontend!.write('\r\n\r\n')
  349. this.frontend!.write(colors.bgWhite.black(' * ') + colors.bgBlackBright.white(' History restored '))
  350. this.frontend!.write('\r\n\r\n')
  351. }
  352. }
  353. }
  354. async buildContextMenu (): Promise<MenuItemOptions[]> {
  355. let items: MenuItemOptions[] = []
  356. for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
  357. items = items.concat(section)
  358. items.push({ type: 'separator' })
  359. }
  360. items.splice(items.length - 1, 1)
  361. return items
  362. }
  363. /**
  364. * Feeds input into the active session
  365. */
  366. sendInput (data: string|Buffer): void {
  367. if (!(data instanceof Buffer)) {
  368. data = Buffer.from(data, 'utf-8')
  369. }
  370. this.session?.feedFromTerminal(data)
  371. if (this.config.store.terminal.scrollOnInput) {
  372. this.frontend?.scrollToBottom()
  373. }
  374. }
  375. /**
  376. * Feeds input into the terminal frontend
  377. */
  378. async write (data: string): Promise<void> {
  379. this.frontendWriteLock = this.frontendWriteLock.then(() =>
  380. this.withSpinnerPaused(() => this.writeRaw(data)))
  381. await this.frontendWriteLock
  382. }
  383. protected async writeRaw (data: string): Promise<void> {
  384. if (!this.frontend) {
  385. throw new Error('Frontend not ready')
  386. }
  387. if (this.config.store.terminal.detectProgress) {
  388. const percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
  389. if (!this.alternateScreenActive && percentageMatch) {
  390. const percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
  391. if (percentage > 0 && percentage <= 100) {
  392. this.setProgress(percentage)
  393. }
  394. } else {
  395. this.setProgress(null)
  396. }
  397. }
  398. await this.frontend.write(data)
  399. }
  400. async paste (): Promise<void> {
  401. let data = this.platform.readClipboard()
  402. if (this.hostApp.platform === Platform.Windows) {
  403. data = data.replaceAll('\r\n', '\r')
  404. } else {
  405. data = data.replaceAll('\n', '\r')
  406. }
  407. if (this.config.store.terminal.trimWhitespaceOnPaste && data.indexOf('\n') === data.length - 1) {
  408. // Ends with a newline and has no other line breaks
  409. data = data.substring(0, data.length - 1)
  410. }
  411. if (!this.alternateScreenActive) {
  412. if (data.includes('\r') && this.config.store.terminal.warnOnMultilinePaste) {
  413. const buttons = [
  414. this.translate.instant('Paste'),
  415. this.translate.instant('Cancel'),
  416. ]
  417. const result = (await this.platform.showMessageBox(
  418. {
  419. type: 'warning',
  420. detail: data.slice(0, 1000),
  421. message: this.translate.instant('Paste multiple lines?'),
  422. buttons,
  423. defaultId: 0,
  424. cancelId: 1,
  425. }
  426. )).response
  427. if (result === 1) {
  428. return
  429. }
  430. } else {
  431. if (this.config.store.terminal.trimWhitespaceOnPaste) {
  432. data = data.trim()
  433. }
  434. }
  435. }
  436. if (this.config.store.terminal.bracketedPaste && this.frontend?.supportsBracketedPaste()) {
  437. data = `\x1b[200~${data}\x1b[201~`
  438. }
  439. this.sendInput(data)
  440. }
  441. /**
  442. * Applies the user settings to the terminal
  443. */
  444. configure (): void {
  445. this.frontend?.configure()
  446. if (this.config.store.terminal.background === 'colorScheme') {
  447. if (this.config.store.terminal.colorScheme.background) {
  448. this.backgroundColor = this.config.store.terminal.colorScheme.background
  449. }
  450. } else {
  451. this.backgroundColor = null
  452. }
  453. }
  454. zoomIn (): void {
  455. this.zoom++
  456. this.frontend?.setZoom(this.zoom)
  457. }
  458. zoomOut (): void {
  459. this.zoom--
  460. this.frontend?.setZoom(this.zoom)
  461. }
  462. resetZoom (): void {
  463. this.zoom = 0
  464. this.frontend?.setZoom(this.zoom)
  465. }
  466. focusAllPanes (): void {
  467. if (this.allFocusModeSubscription) {
  468. return
  469. }
  470. if (this.parent instanceof SplitTabComponent) {
  471. const parent = this.parent
  472. parent._allFocusMode = true
  473. parent.layout()
  474. this.allFocusModeSubscription = this.frontend?.input$.subscribe(data => {
  475. for (const tab of parent.getAllTabs()) {
  476. if (tab !== this && tab instanceof BaseTerminalTabComponent) {
  477. tab.sendInput(data)
  478. }
  479. }
  480. }) ?? null
  481. }
  482. }
  483. cancelFocusAllPanes (): void {
  484. if (!this.allFocusModeSubscription) {
  485. return
  486. }
  487. if (this.parent instanceof SplitTabComponent) {
  488. this.allFocusModeSubscription.unsubscribe()
  489. this.allFocusModeSubscription = null
  490. this.parent._allFocusMode = false
  491. this.parent.layout()
  492. }
  493. }
  494. async copyCurrentPath (): Promise<void> {
  495. let cwd: string|null = null
  496. if (this.session?.supportsWorkingDirectory()) {
  497. cwd = await this.session.getWorkingDirectory()
  498. }
  499. if (cwd) {
  500. this.platform.setClipboard({ text: cwd })
  501. this.notifications.notice(this.translate.instant('Copied'))
  502. } else {
  503. this.notifications.error(this.translate.instant('Shell does not support current path detection'))
  504. }
  505. }
  506. /** @hidden */
  507. ngOnDestroy (): void {
  508. super.ngOnDestroy()
  509. }
  510. async destroy (): Promise<void> {
  511. this.frontend?.detach(this.content.nativeElement)
  512. this.frontend?.destroy()
  513. this.frontend = undefined
  514. this.content.nativeElement.remove()
  515. this.detachTermContainerHandlers()
  516. this.config.enabledServices(this.decorators).forEach(decorator => {
  517. try {
  518. decorator.detach(this)
  519. } catch (e) {
  520. this.logger.warn('Decorator attach() throws', e)
  521. }
  522. })
  523. this.output.complete()
  524. this.frontendReady.complete()
  525. super.destroy()
  526. if (this.session?.open) {
  527. await this.session.destroy()
  528. }
  529. }
  530. protected detachTermContainerHandlers (): void {
  531. this.termContainerSubscriptions.cancelAll()
  532. }
  533. private rightMouseDownTime = 0
  534. protected async handleRightMouseDown (event: MouseEvent): Promise<void> {
  535. event.preventDefault()
  536. event.stopPropagation()
  537. this.rightMouseDownTime = Date.now()
  538. if (this.config.store.terminal.rightClick === 'menu') {
  539. this.platform.popupContextMenu(await this.buildContextMenu(), event)
  540. }
  541. }
  542. protected async handleRightMouseUp (event: MouseEvent): Promise<void> {
  543. event.preventDefault()
  544. event.stopPropagation()
  545. if (this.config.store.terminal.rightClick === 'paste'
  546. || this.config.store.terminal.rightClick === 'clipboard') {
  547. const duration = Date.now() - this.rightMouseDownTime
  548. if (duration < 250) {
  549. if (this.config.store.terminal.rightClick === 'paste') {
  550. this.paste()
  551. } else if (this.config.store.terminal.rightClick === 'clipboard') {
  552. if (this.frontend?.getSelection()) {
  553. this.frontend.copySelection()
  554. this.frontend.clearSelection()
  555. } else {
  556. this.paste()
  557. }
  558. }
  559. } else {
  560. this.platform.popupContextMenu(await this.buildContextMenu(), event)
  561. }
  562. }
  563. }
  564. protected attachTermContainerHandlers (): void {
  565. this.detachTermContainerHandlers()
  566. if (!this.frontend) {
  567. throw new Error('Frontend not ready')
  568. }
  569. const maybeConfigure = () => {
  570. if (this.hasFocus) {
  571. setTimeout(() => this.configure(), 250)
  572. }
  573. }
  574. this.termContainerSubscriptions.subscribe(this.frontend.title$, title => this.zone.run(() => {
  575. if (!this.disableDynamicTitle) {
  576. this.setTitle(title)
  577. }
  578. }))
  579. this.termContainerSubscriptions.subscribe(this.focused$, () => this.frontend && (this.frontend.enableResizing = true))
  580. this.termContainerSubscriptions.subscribe(this.blurred$, () => this.frontend && (this.frontend.enableResizing = false))
  581. this.termContainerSubscriptions.subscribe(this.frontend.mouseEvent$, event => {
  582. if (event.type === 'mousedown') {
  583. if (event.which === 1) {
  584. this.cancelFocusAllPanes()
  585. }
  586. if (event.which === 2) {
  587. if (this.config.store.terminal.pasteOnMiddleClick) {
  588. this.paste()
  589. }
  590. event.preventDefault()
  591. event.stopPropagation()
  592. return
  593. }
  594. if (event.which === 3 || event.which === 1 && event.ctrlKey) {
  595. this.handleRightMouseDown(event)
  596. return
  597. }
  598. }
  599. if (event.type === 'mouseup') {
  600. if (event.which === 3 || event.which === 1 && event.ctrlKey) {
  601. this.handleRightMouseUp(event)
  602. return
  603. }
  604. }
  605. if (event.type === 'mousewheel') {
  606. let wheelDeltaY = 0
  607. if ('wheelDeltaY' in event) {
  608. wheelDeltaY = (event as WheelEvent)['wheelDeltaY']
  609. } else {
  610. wheelDeltaY = (event as WheelEvent).deltaY
  611. }
  612. if (event.altKey) {
  613. event.preventDefault()
  614. const delta = Math.round(wheelDeltaY / 50)
  615. this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
  616. }
  617. }
  618. })
  619. this.termContainerSubscriptions.subscribe(this.frontend.input$, data => {
  620. this.sendInput(data)
  621. })
  622. this.termContainerSubscriptions.subscribe(this.frontend.resize$.pipe(auditTime(100)), ({ columns, rows }) => {
  623. this.logger.debug(`Resizing to ${columns}x${rows}`)
  624. this.size = { columns, rows }
  625. this.zone.run(() => {
  626. if (this.session?.open) {
  627. this.session.resize(columns, rows)
  628. }
  629. })
  630. })
  631. this.termContainerSubscriptions.subscribe(this.platform.displayMetricsChanged$, maybeConfigure)
  632. this.termContainerSubscriptions.subscribe(this.hostWindow.windowMoved$, maybeConfigure)
  633. }
  634. setSession (session: BaseSession|null, destroyOnSessionClose = false): void {
  635. if (session) {
  636. if (this.session) {
  637. this.setSession(null)
  638. }
  639. this.detachSessionHandlers()
  640. this.session = session
  641. this.attachSessionHandlers(destroyOnSessionClose)
  642. } else {
  643. this.detachSessionHandlers()
  644. this.session = null
  645. }
  646. this.sessionChanged.next(session)
  647. }
  648. showToolbar (): void {
  649. this.revealToolbar = true
  650. this.toolbarRevealTimeout.clear()
  651. }
  652. hideToolbar (): void {
  653. this.toolbarRevealTimeout.set()
  654. }
  655. togglePinToolbar (): void {
  656. this.pinToolbar = !this.pinToolbar
  657. window.localStorage.pinTerminalToolbar = this.pinToolbar
  658. }
  659. @HostBinding('class.with-title-inset') get hasTitleInset (): boolean {
  660. return this.hostApp.platform === Platform.macOS && this.config.store.appearance.tabsLocation !== 'top' && this.config.store.appearance.frame === 'thin'
  661. }
  662. protected attachSessionHandler <T> (observable: Observable<T>, handler: (v: T) => void): void {
  663. this.sessionHandlers.subscribe(observable, handler)
  664. }
  665. protected attachSessionHandlers (destroyOnSessionClose = false): void {
  666. if (!this.session) {
  667. throw new Error('Session not set')
  668. }
  669. // this.session.output$.bufferTime(10).subscribe((datas) => {
  670. this.attachSessionHandler(this.session.output$, data => {
  671. if (this.enablePassthrough) {
  672. this.output.next(data)
  673. this.write(data)
  674. }
  675. })
  676. if (destroyOnSessionClose) {
  677. this.attachSessionHandler(this.session.closed$, () => {
  678. this.destroy()
  679. })
  680. }
  681. this.attachSessionHandler(this.session.destroyed$, () => {
  682. this.setSession(null)
  683. })
  684. this.attachSessionHandler(this.session.oscProcessor.copyRequested$, content => {
  685. this.platform.setClipboard({ text: content })
  686. this.notifications.notice(this.translate.instant('Copied'))
  687. })
  688. }
  689. protected detachSessionHandlers (): void {
  690. this.sessionHandlers.cancelAll()
  691. }
  692. protected startSpinner (text?: string): void {
  693. if (this.spinnerActive || this.spinnerPaused) {
  694. return
  695. }
  696. if (text) {
  697. this.spinner.text = text
  698. }
  699. this.spinner.setSpinnerString(6)
  700. this.spinnerActive = true
  701. this.zone.runOutsideAngular(() => {
  702. this.spinner.start()
  703. })
  704. }
  705. protected stopSpinner (): void {
  706. if (!this.spinnerActive) {
  707. return
  708. }
  709. this.spinner.stop(true)
  710. this.spinnerActive = false
  711. }
  712. protected async withSpinnerPaused (work: () => any): Promise<void> {
  713. this.spinnerPaused = true
  714. if (this.spinnerActive) {
  715. this.spinner.stop(true)
  716. }
  717. try {
  718. await work()
  719. } finally {
  720. this.spinnerPaused = false
  721. if (this.spinnerActive) {
  722. this.zone.runOutsideAngular(() => {
  723. this.spinner.start()
  724. })
  725. }
  726. }
  727. }
  728. protected forEachFocusedTerminalPane (cb: (tab: BaseTerminalTabComponent) => void): void {
  729. if (this.parent && this.parent instanceof SplitTabComponent && this.parent._allFocusMode) {
  730. for (const tab of this.parent.getAllTabs()) {
  731. if (tab instanceof BaseTerminalTabComponent) {
  732. cb(tab)
  733. }
  734. }
  735. } else {
  736. cb(this)
  737. }
  738. }
  739. }