diff --git a/harness/ruview/mod/hooks/register.mjs b/harness/ruview/mod/hooks/register.mjs index 789cbdc23..ded6689ea 100644 --- a/harness/ruview/mod/hooks/register.mjs +++ b/harness/ruview/mod/hooks/register.mjs @@ -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.` }; } diff --git a/harness/ruview/mod/hooks/views.mjs b/harness/ruview/mod/hooks/views.mjs index 510ebf4a5..2a35713cf 100644 --- a/harness/ruview/mod/hooks/views.mjs +++ b/harness/ruview/mod/hooks/views.mjs @@ -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 }; } diff --git a/harness/ruview/test/mod-showcase.test.mjs b/harness/ruview/test/mod-showcase.test.mjs index fb1346d6b..a1546375b 100644 --- a/harness/ruview/test/mod-showcase.test.mjs +++ b/harness/ruview/test/mod-showcase.test.mjs @@ -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'); +});