Terminal: Ctrl+C copies a selection (else ^C), Ctrl+V pastes, over http too; drop Log out everywhere

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 10:44:17 -07:00
co-authored by Claude Fable 5.1
parent d7ed4d8549
commit d3f2a83249
6 changed files with 28 additions and 28 deletions
-6
View File
@@ -124,12 +124,6 @@ export default function Settings({ me }: { me: Me }) {
</div>
</section>
<div className="dash-bar"><h2 className="section">Sessions</h2></div>
<section className="card">
<div className="muted small" style={{ marginBottom: 8 }}>Signs out every browser and phone that is logged in to this Ham Lab, this one included. Access tokens are not sessions and keep working.</div>
<Btn className="btn danger" confirm="Log out everywhere? Every device, including this one, will need the password again." onClick={async () => { try { await api.logoutAll() } finally { window.dispatchEvent(new Event('hamlab:unauthenticated')) } }}>Log out everywhere</Btn>
</section>
{making && <TokenDialog onClose={() => setMaking(false)} onMade={(t) => { setMaking(false); setFresh(t); loadTokens() }} />}
<div className="dash-bar"><h2 className="section">About</h2></div>
+20
View File
@@ -84,6 +84,26 @@ export default function TerminalPane({ id, active, register, onStatus, onExit }:
}
connect()
// Ctrl+C / Ctrl+V as on any desktop. Returning false tells xterm to leave the key alone,
// and *not* preventing the default lets the browser fire its own copy / paste event —
// which xterm answers with its selection, or pastes (bracketed, if the shell asked). That
// path needs no navigator.clipboard, so it works over plain http on the LAN as well.
term.attachCustomKeyEventHandler((e) => {
if (e.type !== 'keydown') return true
const k = e.key.toLowerCase(), ctrl = e.ctrlKey && !e.altKey && !e.metaKey
const copy = (ctrl && k === 'c' && (e.shiftKey || term.hasSelection())) || (ctrl && k === 'insert')
const paste = (ctrl && k === 'v') || (e.shiftKey && !ctrl && k === 'insert')
if (copy) {
if (!term.hasSelection()) { e.preventDefault(); return false } // Ctrl+Shift+C with nothing selected: not devtools, not ^C
if (e.shiftKey) { e.preventDefault(); document.execCommand('copy') } // Chrome keeps Ctrl+Shift+C for itself; copy by hand
if (navigator.clipboard) navigator.clipboard.writeText(term.getSelection()).catch(() => {})
window.setTimeout(() => term.clearSelection(), 50) // so the next Ctrl+C is an interrupt again
return false
}
if (paste) return false
return true
})
term.onData((d) => {
if (ctrlArmed && d.length === 1) {
ctrlArmed = false
-1
View File
@@ -182,7 +182,6 @@ export const api = {
changePassword: (current: string, password: string) => req<void>('POST', '/api/password', { current, password }, true),
tokens: () => req<{ name: string; created: string; scope?: string; sensors?: string[] }[]>('GET', '/api/tokens', undefined, true),
createReadToken: (name: string, sensors: string[] = []) => req<{ token: string; name: string }>('POST', '/api/tokens/read', { name, sensors }, true),
logoutAll: () => req<{ ended: number }>('POST', '/api/logout/all', undefined, true),
revokeToken: (name: string) => req<void>('DELETE', `/api/tokens/${encodeURIComponent(name)}`, undefined, true),
uiPrefs: () => req<{ theme?: string; refreshMs?: number }>('GET', '/api/prefs/ui', undefined, true),
saveUIPrefs: (p: { theme?: string; refreshMs?: number }) => req<void>('PUT', '/api/prefs/ui', p, true),