diff --git a/demo/index.html b/demo/index.html index ee765c2057..daa60b6f5e 100644 --- a/demo/index.html +++ b/demo/index.html @@ -38,6 +38,17 @@

Options

+

+ +

diff --git a/demo/main.js b/demo/main.js index 9098c2f02d..cb346de2af 100644 --- a/demo/main.js +++ b/demo/main.js @@ -15,7 +15,8 @@ var terminalContainer = document.getElementById('terminal-container'), cursorBlink: document.querySelector('#option-cursor-blink'), cursorStyle: document.querySelector('#option-cursor-style'), scrollback: document.querySelector('#option-scrollback'), - tabstopwidth: document.querySelector('#option-tabstopwidth') + tabstopwidth: document.querySelector('#option-tabstopwidth'), + bellStyle: document.querySelector('#option-bell-style') }, colsElement = document.getElementById('cols'), rowsElement = document.getElementById('rows'); @@ -53,6 +54,9 @@ optionElements.cursorBlink.addEventListener('change', function () { optionElements.cursorStyle.addEventListener('change', function () { term.setOption('cursorStyle', optionElements.cursorStyle.value); }); +optionElements.bellStyle.addEventListener('change', function () { + term.setOption('bellStyle', optionElements.bellStyle.value); +}); optionElements.scrollback.addEventListener('change', function () { term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); }); diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 43ca18bc55..0e82f788f3 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + /// import { Terminal } from 'xterm'; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 61fe581e8b..f2d9775113 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -106,14 +106,7 @@ export class InputHandler implements IInputHandler { * Bell (Ctrl-G). */ public bell(): void { - if (!this._terminal.options.visualBell) { - return; - } - this._terminal.element.style.borderColor = 'white'; - setTimeout(() => this._terminal.element.style.borderColor = '', 10); - if (this._terminal.options.popOnBell) { - this._terminal.focus(); - } + this._terminal.bell(); } /** diff --git a/src/Interfaces.ts b/src/Interfaces.ts index e9db3f9c5e..943003aec3 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -87,6 +87,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { viewport: IViewport; selectionManager: ISelectionManager; + bell(): void; focus(): void; convertEol: boolean; updateRange(y: number): void; @@ -113,6 +114,8 @@ export interface IInputHandlingTerminal extends IEventEmitter { } export interface ITerminalOptions { + bellSound?: string; + bellStyle?: string; cancelEvents?: boolean; colors?: string[]; cols?: number; @@ -123,14 +126,12 @@ export interface ITerminalOptions { disableStdin?: boolean; geometry?: [number, number]; handler?: (data: string) => void; - popOnBell?: boolean; rows?: number; screenKeys?: boolean; scrollback?: number; tabStopWidth?: number; termName?: string; useFlowControl?: boolean; - visualBell?: boolean; } export interface IBuffer { diff --git a/src/Terminal.ts b/src/Terminal.ts index 32f9ea2311..ff42d13969 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -39,6 +39,7 @@ import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper } from './Interfaces'; +import { BellSound } from './utils/Sounds'; // Declare for RequireJS in loadAddon declare var define: any; @@ -147,8 +148,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { geometry: [80, 24], cursorBlink: false, cursorStyle: 'block', - visualBell: false, - popOnBell: false, + bellSound: BellSound, + bellStyle: null, scrollback: 1000, screenKeys: false, debug: false, @@ -178,6 +179,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private helperContainer: HTMLElement; private compositionView: HTMLElement; private charSizeStyleElement: HTMLStyleElement; + private bellAudioElement: HTMLAudioElement; + private visualBellTimer: number; public browser: IBrowser = Browser; @@ -470,6 +473,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport.syncScrollArea(); break; case 'tabStopWidth': this.setupStops(); break; + case 'bellStyle': this.preloadBellSound(); break; } } @@ -678,6 +682,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewportScrollArea.classList.add('xterm-scroll-area'); this.viewportElement.appendChild(this.viewportScrollArea); + // preload audio + this.preloadBellSound(); + // Create the selection container. this.selectionContainer = document.createElement('div'); this.selectionContainer.classList.add('xterm-selection'); @@ -1864,12 +1871,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Note: We could do sweet things with webaudio here */ public bell(): void { - if (!this.options.visualBell) return; - this.element.style.borderColor = 'white'; - setTimeout(() => { - this.element.style.borderColor = ''; - }, 10); - if (this.options.popOnBell) this.focus(); + this.emit('bell'); + if (this.soundBell()) this.bellAudioElement.play(); + + if (this.visualBell()) { + this.element.classList.add('visual-bell-active'); + clearTimeout(this.visualBellTimer); + this.visualBellTimer = window.setTimeout(() => { + this.element.classList.remove('visual-bell-active'); + }, 200); + } } /** @@ -2261,6 +2272,27 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return matchColorCache[hash] = li; } + + private visualBell(): boolean { + return this.options.bellStyle === 'visual' || + this.options.bellStyle === 'both'; + } + + private soundBell(): boolean { + return this.options.bellStyle === 'sound' || + this.options.bellStyle === 'both'; + } + + private preloadBellSound(): void { + if (this.soundBell()) { + this.bellAudioElement = document.createElement('audio'); + this.bellAudioElement.setAttribute('preload', 'auto'); + this.bellAudioElement.setAttribute('src', this.options.bellSound); + this.helperContainer.appendChild(this.bellAudioElement); + } else if (this.bellAudioElement) { + this.helperContainer.removeChild(this.bellAudioElement); + } + } } /** diff --git a/src/utils/Sounds.ts b/src/utils/Sounds.ts new file mode 100644 index 0000000000..6d20c67122 --- /dev/null +++ b/src/utils/Sounds.ts @@ -0,0 +1,9 @@ +/** + * @license MIT + */ + +// Source: https://freesound.org/people/altemark/sounds/45759/ +// This sound is released under the Creative Commons Attribution 3.0 Unported +// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been +// made, apart from the conversion to base64. +export const BellSound = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 8e18c1f47f..8d47556093 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -97,6 +97,10 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { throw new Error('Method not implemented.'); } convertEol: boolean; + bell(): void { + throw new Error('Method not implemented.'); + } + updateRange(y: number): void { throw new Error('Method not implemented.'); } diff --git a/src/xterm.css b/src/xterm.css index 89daf9e3e1..14b0a58d4e 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -91,6 +91,7 @@ .terminal .terminal-cursor { position: relative; + transition: opacity 150ms ease; } .terminal:not(.focus) .terminal-cursor { @@ -108,6 +109,7 @@ content: ''; position: absolute; background-color: #fff; + transition: opacity 150ms ease; } .terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before { @@ -124,6 +126,14 @@ height: 1px; } +.terminal.focus.xterm-cursor-style-block.visual-bell-active .terminal-cursor { + opacity: 0.5; +} +.terminal.focus.xterm-cursor-style-bar.visual-bell-active .terminal-cursor::before, +.terminal.focus.xterm-cursor-style-underline.visual-bell-active .terminal-cursor::before { + opacity: 0; +} + .terminal .composition-view { background: #000; color: #FFF; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6758b94fe1..ee7d68d87a 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -11,6 +11,16 @@ * An object containing start up options for the terminal. */ interface ITerminalOptions { + /** + * A data uri of the sound to use for the bell (needs bellStyle = 'sound'). + */ + bellSound?: string; + + /** + * The type of the bell notification the terminal will use. + */ + bellStyle?: 'none' | 'visual' | 'sound' | 'both'; + /** * The number of columns in the terminal. */