diff --git a/ui/Buffer.tsx b/ui/Buffer.tsx
index 65c368e..e2cc770 100644
--- a/ui/Buffer.tsx
+++ b/ui/Buffer.tsx
@@ -1,9 +1,12 @@
-import { Terminal, ITerminalOptions } from 'xterm';
+import { Terminal, ITerminalOptions, ITerminalInitOnlyOptions } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
+import { CanvasAddon } from 'xterm-addon-canvas';
import { debounce } from 'lodash';
import bel from './lib/bel';
import api from './api';
+const belAudio = new Audio(bel);
+
import {
pokeTask, pokeBelt
} from './lib/utils'
@@ -18,7 +21,7 @@ import { DEFAULT_SESSION, RESIZE_DEBOUNCE_MS, RESIZE_THRESHOLD_PX } from './cons
import { retry } from './lib/retry';
import { Belt } from 'lib/types';
-const termConfig: ITerminalOptions = {
+const termConfig: ITerminalOptions & ITerminalInitOnlyOptions = {
logLevel: 'warn',
//
convertEol: true,
@@ -28,12 +31,10 @@ const termConfig: ITerminalOptions = {
scrollback: 10000,
//
fontFamily: '"Source Code Pro", "Roboto mono", "Courier New", monospace',
+ fontSize: 16,
fontWeight: 400,
// NOTE theme colors configured dynamically
//
- bellStyle: 'sound',
- bellSound: bel,
- //
// allows text selection by holding modifier (option, or shift)
macOptionClickForcesSelection: true,
// prevent insertion of simulated arrow keys on-altclick
@@ -178,12 +179,18 @@ export default function Buffer({ name, selected, dark }: BufferProps) {
term.options.theme = makeTheme(dark);
const fit = new FitAddon();
term.loadAddon(fit);
+ try {
+ term.loadAddon(new CanvasAddon());
+ } catch (e) {
+ console.warn('canvas renderer unavailable, falling back to DOM', e);
+ }
fit.fit();
term.focus();
- // start mouse reporting
- //
- term.write(csi('?9h'));
+ // NOTE X10 mouse reporting (csi('?9h')) used to be enabled here
+ // unconditionally, but it makes xterm intercept clicks/drags so
+ // native selection can't initiate. dojo can re-enable it itself
+ // via blit if it ever wants click events.
const ses: Session = {
term,
@@ -211,6 +218,7 @@ export default function Buffer({ name, selected, dark }: BufferProps) {
});
term.onData(e => onInput(name, ses, e));
term.onBinary(e => onInput(name, ses, e));
+ term.onBell(() => { belAudio.play().catch(() => {}); });
// open subscription
//
@@ -291,6 +299,25 @@ export default function Buffer({ name, selected, dark }: BufferProps) {
}
}, [session, containerRef]);
+ // on touch devices, auto-copy whenever xterm's own selection changes
+ // (e.g. via double-tap word select) so you can paste elsewhere.
+ //
+ useEffect(() => {
+ if (!session) {
+ return;
+ }
+ if (!window.matchMedia('(pointer: coarse)').matches) {
+ return;
+ }
+ const sub = session.term.onSelectionChange(() => {
+ const sel = session.term.getSelection();
+ if (sel && navigator.clipboard?.writeText) {
+ navigator.clipboard.writeText(sel).catch(() => {});
+ }
+ });
+ return () => sub.dispose();
+ }, [session]);
+
// initialize resize listeners
//
useEffect(() => {
@@ -301,9 +328,11 @@ export default function Buffer({ name, selected, dark }: BufferProps) {
// TODO: use ResizeObserver for improved performance?
const debouncedResize = debounce(() => onResize(name, session), RESIZE_DEBOUNCE_MS);
window.addEventListener('resize', debouncedResize);
+ window.visualViewport?.addEventListener('resize', debouncedResize);
return () => {
window.removeEventListener('resize', debouncedResize);
+ window.visualViewport?.removeEventListener('resize', debouncedResize);
};
}, [session]);
diff --git a/ui/index.html b/ui/index.html
index 4086ccc..2d73b95 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -4,7 +4,7 @@
Terminal
+ content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, interactive-widget=resizes-content"/>
@@ -22,10 +22,20 @@
+
diff --git a/ui/lib/theme.ts b/ui/lib/theme.ts
index cf2beb1..2eefeb3 100644
--- a/ui/lib/theme.ts
+++ b/ui/lib/theme.ts
@@ -1,13 +1,15 @@
import { ITheme } from 'xterm';
export const makeTheme = (dark: boolean): ITheme => {
- let fg, bg: string;
+ let fg, bg, sel: string;
if (dark) {
fg = 'white';
bg = 'rgb(26,26,26)';
+ sel = 'rgba(255,255,255,0.3)';
} else {
fg = 'black';
bg = 'white';
+ sel = 'rgba(0,0,0,0.25)';
}
// TODO indigo colors.
// we can't pluck these from ThemeContext because they have transparency.
@@ -18,6 +20,6 @@ export const makeTheme = (dark: boolean): ITheme => {
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg,
cursorAccent: bg,
- selection: fg
+ selectionBackground: sel
};
};
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 08ede24..ce7ac27 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -25,8 +25,9 @@
"style-loader": "^1.3.0",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
- "xterm": "^4.15.0",
- "xterm-addon-fit": "^0.5.0",
+ "xterm": "^5.3.0",
+ "xterm-addon-canvas": "^0.5.0",
+ "xterm-addon-fit": "^0.8.0",
"zustand": "^3.5.0"
},
"devDependencies": {
@@ -19722,16 +19723,30 @@
}
},
"node_modules/xterm": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.15.0.tgz",
- "integrity": "sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw=="
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
+ "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
+ "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
+ "license": "MIT"
},
- "node_modules/xterm-addon-fit": {
+ "node_modules/xterm-addon-canvas": {
"version": "0.5.0",
- "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
- "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
+ "resolved": "https://registry.npmjs.org/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0.tgz",
+ "integrity": "sha512-QOo/eZCMrCleAgMimfdbaZCgmQRWOml63Ued6RwQ+UTPvQj3Av9QKx3xksmyYrDGRO/AVRXa9oNuzlYvLdmoLQ==",
+ "deprecated": "This package is now deprecated. Move to @xterm/addon-canvas instead.",
+ "license": "MIT",
"peerDependencies": {
- "xterm": "^4.0.0"
+ "xterm": "^5.0.0"
+ }
+ },
+ "node_modules/xterm-addon-fit": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
+ "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
+ "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.",
+ "license": "MIT",
+ "peerDependencies": {
+ "xterm": "^5.0.0"
}
},
"node_modules/y18n": {
@@ -34998,14 +35013,20 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"xterm": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.15.0.tgz",
- "integrity": "sha512-Ik1GoSq1yqKZQ2LF37RPS01kX9t4TP8gpamUYblD09yvWX5mEYuMK4CcqH6+plgiNEZduhTz/UrcaWs97gOlOw=="
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
+ "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
},
- "xterm-addon-fit": {
+ "xterm-addon-canvas": {
"version": "0.5.0",
- "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
- "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
+ "resolved": "https://registry.npmjs.org/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0.tgz",
+ "integrity": "sha512-QOo/eZCMrCleAgMimfdbaZCgmQRWOml63Ued6RwQ+UTPvQj3Av9QKx3xksmyYrDGRO/AVRXa9oNuzlYvLdmoLQ==",
+ "requires": {}
+ },
+ "xterm-addon-fit": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
+ "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
"requires": {}
},
"y18n": {
diff --git a/ui/package.json b/ui/package.json
index 729e3e6..6db9c9c 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -22,8 +22,9 @@
"style-loader": "^1.3.0",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
- "xterm": "^4.15.0",
- "xterm-addon-fit": "^0.5.0",
+ "xterm": "^5.3.0",
+ "xterm-addon-canvas": "^0.5.0",
+ "xterm-addon-fit": "^0.8.0",
"zustand": "^3.5.0"
},
"devDependencies": {
diff --git a/ui/state.ts b/ui/state.ts
index fa22e58..9eb1232 100644
--- a/ui/state.ts
+++ b/ui/state.ts
@@ -31,7 +31,7 @@ const useTermState = create((set, get) => ({
theme: 'auto',
// eslint-disable-next-line no-unused-vars
set: (f: (draft: TermState) => void) => {
- set(produce(f));
+ set(produce(f) as any);
}
} as TermState));