fix(ruview-live): open as a dialog that takes the keys, sized per view

Hotkeys reach a pane only while it holds the keyboard, and `focus` alone
was not taking it. The engine's documented dialog form does: open with
focus + closeOnEscape + holdToasts (Escape closes, as originally). Each
open also requests `rows` for its view (overview 16, waterfall 30,
radar 26) so the inline pane opens tall enough for the bordered design
instead of a third of the screen; switching views re-requests the size,
and picture heights leave room for the chrome (rows - 15).

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv committed 2026-10-01 18:35:19 -04:00
1 parent bf21c8c7a9
commit c4f9d95e11
3 files changed
+32 -10

No files matched your search

+20 -7
View File
@@ -20,6 +20,17 @@ export { ANIMATED, picturesOf, sizesOf, viewOf } from './views.mjs';
export const PANE_ID = 'ruview-live';
export const COMMAND = 'ruview';
const TICK_MS = 5000;
/** Body rows each view asks for when the pane sits inline above the prompt. */
export const VIEW_ROWS = Object.freeze({ overview: 16, waterfall: 30, radar: 26 });
/**
* The open request: a dialog (focus + closeOnEscape + holdToasts) takes the
* keyboard, so the view keys work at once; `rows` sizes the inline pane to
* the view instead of a third of the screen (the dock ignores it).
*/
export const openArgsOf = (mode) => ({
id: PANE_ID, title: 'RuView', focus: true, closeOnEscape: true, holdToasts: true, rows: VIEW_ROWS[mode] ?? VIEW_ROWS.overview,
});
/** Animation frame period: ~12 fps, well inside blit's 60 shown a second. */
export const FRAME_MS = 80;
@@ -138,15 +149,17 @@ export function register(on, options = {}) {
function setMode(next) {
if (!MODES.includes(next) || next === mode) return;
mode = next;
if (isOpen && host) { stop(); startPolling(); } else host?.invalidate();
if (isOpen && host) {
// Each open sets the size anew: re-request rows for this view, keeping the keys.
void host.open(openArgsOf(mode)).catch(() => undefined);
stop();
startPolling();
} else host?.invalidate();
}
async function open(initialMode) {
if (MODES.includes(initialMode)) mode = initialMode;
// Ask for the keyboard so the view keys (1/2/3, r, c) work at once; the
// surface grants it only over an empty prompt. No closeOnEscape: Escape
// hands the keys back without closing the pane.
await host.open({ id: PANE_ID, title: 'RuView', focus: true });
await host.open(openArgsOf(mode));
stop();
startPolling();
}
@@ -180,8 +193,8 @@ export function register(on, options = {}) {
if (arg === 'refresh') { await refresh(); return { text: statusOf(model) }; }
if (MODES.includes(arg)) {
if (isOpen) {
setMode(arg);
await host.open({ id: PANE_ID, title: 'RuView', focus: true }).catch(() => undefined);
if (arg === mode) await host.open(openArgsOf(mode)).catch(() => undefined);
else setMode(arg);
} else await open(arg);
return { text: `RuView pane: ${arg} view.` };
}
+1 -1
View File
@@ -16,7 +16,7 @@ export const ANIMATED = Object.freeze(['shimmer', 'waterfall', 'fan', 'pulse']);
/** Picture sizes that fit the pane body. */
export function sizesOf(columns = 80, rows = 30) {
const width = Math.max(24, Math.min(128, columns - 6));
const height = Math.max(6, Math.min(26, rows - 12));
const height = Math.max(6, Math.min(26, rows - 15)); // the header, tabs, card chrome and footer take ~15 rows
return { width, height };
}
+11 -2
View File
@@ -4,7 +4,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
ANIMATED, Braille, colormap, commandsOf, DEFAULT, drain, Grid, lineChart, modelOf, phaseOf, picturesOf, pingOf, pulse,
ANIMATED, Braille, colormap, openArgsOf, VIEW_ROWS, commandsOf, DEFAULT, drain, Grid, lineChart, modelOf, phaseOf, picturesOf, pingOf, pulse,
radarFan, rgb, settingsOf, shimmer, shownFrames, spectrumOf, spectrumWith, toBase64, viewOf, waterfall,
} from '../mod/hooks/register.mjs';
@@ -150,7 +150,7 @@ test('radar view: fan, pulse and charts, with the honesty labels', () => {
});
test('spectrum requests, merging across captures, and live commands', () => {
assert.deepEqual(spectrumOf(120, 34), { bins: 114, frames: 44 });
assert.deepEqual(spectrumOf(120, 34), { bins: 114, frames: 38 });
assert.deepEqual(spectrumOf(10, 5), { bins: 24, frames: 12 });
const c = commandsOf(settingsOf({ radarHost: 'kit.local' }), { live: true, spectrum: { bins: 64, frames: 48 } });
assert.deepEqual(c.capture, ['esp32', '--seconds', '2', '--udp-port', '5005', '--json', '--spectrum', '--spectrum-bins', '64', '--spectrum-frames', '48']);
@@ -167,3 +167,12 @@ test('an unfocused pane says how to give it the keys; a focused one does not', (
assert.match(JSON.stringify(viewOf(UI, model, { ...base, focused: false })), /ctrl\+x tab to use the keys/);
assert.doesNotMatch(JSON.stringify(viewOf(UI, model, { ...base, focused: true })), /ctrl\+x tab/);
});
test('the pane opens as a dialog that takes the keys, sized to the view', () => {
for (const mode of ['overview', 'waterfall', 'radar']) {
const a = openArgsOf(mode);
assert.deepEqual([a.id, a.focus, a.closeOnEscape, a.holdToasts], ['ruview-live', true, true, true], 'focus + closeOnEscape + holdToasts = a dialog that takes the keys');
assert.equal(a.rows, VIEW_ROWS[mode]);
}
assert.ok(VIEW_ROWS.waterfall > VIEW_ROWS.overview, 'the live views ask for more room');
});