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.
*/