Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions ui/Buffer.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
//
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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]);

Expand Down
45 changes: 43 additions & 2 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title>Terminal</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, interactive-widget=resizes-content"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
Expand All @@ -22,10 +22,20 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
<style>
html {
height: 100%;
overflow: hidden;
}

body, #root {
height: 99vh; /* prevent scrollbar on outer frame */
position: fixed;
inset: 0;
height: 100vh;
height: 100dvh;
height: var(--app-height, 100dvh); /* tracks visualViewport so soft keyboard pushes content up */
margin: 0;
padding: 0;
overflow: hidden;
}

.buffer-container {
Expand Down Expand Up @@ -98,6 +108,20 @@
cursor: pointer;
}

.xterm-viewport {
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}

/* allow native text selection (long-press on touch, click-drag on desktop) */
.xterm-screen,
.xterm-screen .xterm-rows,
.xterm-screen .xterm-rows * {
-webkit-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
}

@media (prefers-color-scheme: dark) {
html {
background-color: rgb(26,26,26);
Expand Down Expand Up @@ -131,6 +155,23 @@
}
}
</style>
<script>
// Track visualViewport so the soft keyboard shrinks the layout
// instead of overlapping the terminal.
(function () {
var setHeight = function () {
var h = (window.visualViewport && window.visualViewport.height) || window.innerHeight;
document.documentElement.style.setProperty('--app-height', h + 'px');
};
setHeight();
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', setHeight);
window.visualViewport.addEventListener('scroll', setHeight);
}
window.addEventListener('resize', setHeight);
window.addEventListener('orientationchange', setHeight);
})();
</script>
<script type="module" src="/index.jsx"></script>
</head>
<body>
Expand Down
6 changes: 4 additions & 2 deletions ui/lib/theme.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,6 +20,6 @@ export const makeTheme = (dark: boolean): ITheme => {
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg,
cursorAccent: bg,
selection: fg
selectionBackground: sel
};
};
51 changes: 36 additions & 15 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion ui/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const useTermState = create<TermState>((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));

Expand Down