Skip to content
Merged
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,48 @@ app.render(({ ui, theme }) => {
await app.start();
```

## Copy summaries as Markdown

Enable copy icons for summary panes throughout an app:

```ts
const app = await createApp({
copyMarkdown: true,
markdownContext: () => `Host: ${hostname}\nReporting period: ${period}`,
});

app.render(({ ui }) => {
ui.panel({ title: "Status" }, (p) => {
p.keyValues([{ label: "Connection", value: "Ready" }]);
});
ui.copyButton({ markdown: () => "## Status\n\nReady\n", width: 6 });
});
```

Click **⧉ MD**, or Tab / Shift+Tab to focus a control and Enter / Space to copy.
Other navigation keys return focus to the app. ASCII terminals display `C MD`;
narrow panes show only the icon. A short notice confirms the clipboard request.

Automatic exports include the pane title, subtitle, context, text, labeled values,
meters, progress, and graph summaries (latest/min/max/sample count). Values are
captured before wrapping and clipping. Tables, lists, logs, trees, input fields,
and raw drawing callbacks are excluded. Panels containing only data rows have no
copy icon. Layout branches that the app does not build cannot be exported.

Set `copyMarkdown: true` on one panel or modal to enable it individually,
`copyMarkdown: false` to exclude it (including from parent exports), or provide
a Markdown string/callback for a custom summary. `ui.copyButton()` places the
same control in a status strip or custom layout. Custom Markdown is copied as
provided; `markdownText(value)` escapes plain values for interpolation.

The default clipboard writer uses OSC 52 through the terminal, including over
SSH, and wraps the sequence for tmux. Terminal clipboard support must be enabled;
“Markdown copy sent” confirms delivery of the request, since terminals do not
acknowledge clipboard writes. Oversized exports fail explicitly instead of being
truncated. Supply `clipboard: (text) => ...` on `createApp()` to use another writer.
No shell command is run. `renderToScreen()` records copies in `screen.copied` for
interaction tests without changing the clipboard.

## See it running

Ten screens. Real metrics on Linux, macOS and Windows, with no native dependencies.
Expand Down
7 changes: 6 additions & 1 deletion apps/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ npx --yes @profullstack/hqtui-demo@latest # Node 22.6+ works too

| Key | Action |
|---|---|
| `1`–`0`, `w`, `Tab` | Switch screens |
| `1`–`0`, `w` | Switch screens |
| `Tab`, `Shift+Tab` | Focus controls; Enter / Space copies a focused summary |
| `F1` | Help |
| `F2` | Cycle theme |
| `F3` | Filter processes |
Expand All @@ -81,6 +82,10 @@ npx --yes @profullstack/hqtui-demo@latest # Node 22.6+ works too

Mouse works too: click the tabs and buttons, scroll the process list.

Click **⧉ MD** in a summary pane to copy its facts as Markdown, with the host,
screen, metric source and update state. Process and journal rows are excluded.
The terminal must support OSC 52 clipboard writes (also usable over SSH).

The termination dialog sends **SIGTERM** by default, allowing the process to
clean up. **Force kill (-9 / SIGKILL)** starts unchecked each time. Use Tab,
Shift+Tab or the arrow keys to move between Yes, No and the checkbox; Space
Expand Down
4 changes: 2 additions & 2 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/hqtui-demo",
"version": "0.6.2",
"version": "0.6.3",
"description": "The HQTUI reference dashboard: a btop-grade terminal system monitor. Runs on real system metrics or a deterministic simulation.",
"license": "MIT",
"type": "module",
Expand All @@ -27,7 +27,7 @@
"audit:scroll": "bun scripts/scrollaudit.ts"
},
"dependencies": {
"@profullstack/hqtui": "^0.6.2"
"@profullstack/hqtui": "^0.6.3"
},
"publishConfig": {
"access": "public"
Expand Down
16 changes: 10 additions & 6 deletions apps/demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function parseArgs(argv: string[]): Options {
case "-h":
case "--help": printHelp(); process.exit(0);
case "-v":
case "--version": console.log("hqtui-demo 0.6.2"); process.exit(0);
case "--version": console.log("hqtui-demo 0.6.3"); process.exit(0);
}
}
return options;
Expand All @@ -76,7 +76,8 @@ Options:
-v, --version Show the version

Keys:
1-0/w / Tab screens F2 theme F3 filter F6 sort Ctrl+K palette
1-0/w screens F2 theme F3 filter F6 sort Ctrl+K palette
Tab focus / Enter copy summary
↑/↓ select Space pause F1 help q quit
Enter terminate selected process (SIGTERM; optional Force -9 in dialog)
`);
Expand Down Expand Up @@ -109,6 +110,8 @@ async function main(): Promise<void> {
state.themeIndex = Math.max(0, themeList.findIndex((t) => t.name === options.theme || t === (themes as never)[options.theme]));

const app = await createApp({
copyMarkdown: true,
markdownContext: () => `HQTUI demo · ${state.sample.system.hostname} · ${state.screen}\nSource: ${state.source} · ${state.paused ? "paused" : "live"}`,
theme: themeList[state.themeIndex] ?? themes.dark,
fps: options.fps,
title: "hqtui demo",
Expand Down Expand Up @@ -318,7 +321,7 @@ async function main(): Promise<void> {
{ key: "F6", label: `Sort: ${state.sort}`, onPress: () => press("f6") },
...(state.screen === "dashboard" ? [{ key: "Enter", label: "Kill", onPress: () => openKillDialog(state) }] : []),
{ key: "^K", label: "Palette", onPress: () => press("ctrl+k") },
{ key: "Tab", label: "Screen", onPress: () => press("tab") },
{ key: "Tab", label: "Focus", onPress: () => app.focusNext() },
{ key: "q", label: "Quit", onPress: () => press("q") },
],
right: [{ label: `${num(state.renderMs, 2)}ms ${state.changedCells} cells ${state.bytes}B` }],
Expand All @@ -330,7 +333,8 @@ async function main(): Promise<void> {
width: 62,
height: 20,
message:
"1-0, w or Tab switch screens; w is the clickable world map.\n" +
"1-0 or w switch screens; w is the clickable world map.\n" +
"⧉ MD copies a pane summary. Tab focuses, Enter copies.\n" +
"F2 cycles themes, F3 filters processes, F6 changes sort.\n" +
"c collapses adjacent panel borders into shared lines.\n" +
"Ctrl+K opens the command palette, Space pauses updates.\n" +
Expand All @@ -347,8 +351,8 @@ async function main(): Promise<void> {
"and the full journal. It does not add temperatures.\n" +
" sudo -E env \"PATH=$PATH\" bunx @profullstack/hqtui-demo")
: "All metrics available on this platform.") +
"\n\nPress any key to close.",
buttons: [{ label: "Close", focused: true, onPress: () => { state.showHelp = false; } }],
"\n\nEsc or Close dismisses help.",
buttons: [{ label: "Close", onPress: () => { state.showHelp = false; } }],
onDismiss: () => { state.showHelp = false; },
});
}
Expand Down
1 change: 1 addition & 0 deletions apps/demo/src/screens/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function inputScreen(ui: Container, state: DemoState, theme: Theme): void
p.list({ items: state.keyLog.slice(-20).reverse() });
});
row.panel({ title: "Try it" }, (p) => {
p.text("⧉ MD copies this pane as Markdown for people and agents.", { wrap: true });
p.text("Press any key — modifiers are normalized.", { fg: theme.foreground, size: 1 });
p.label("Arrows, Function keys, Ctrl/Alt/Shift combinations,", { size: 1 });
p.label("paste, focus, mouse move, click, drag and scroll.", { size: 1 });
Expand Down
26 changes: 26 additions & 0 deletions apps/demo/test/markdown-copy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { renderToScreen } from "@profullstack/hqtui/testing";
import { createSystemSimulation } from "../src/simulation.ts";
import { createState } from "../src/state.ts";
import { dashboardScreen } from "../src/screens/dashboard.ts";

test("demo summary panes export system facts without process or journal rows", () => {
const sample = createSystemSimulation({ seed: 1 }).current();
sample.system.hostname = "test-host";
sample.processes[0].command = "DO NOT EXPORT PROCESS ROW";
const state = createState(sample, "simulated", []);
const screen = renderToScreen(({ ui, theme }) => dashboardScreen(ui, state, theme), {
width: 180, height: 52, copyMarkdown: true,
markdownContext: "HQTUI demo · test-host · dashboard\nSource: simulated · live",
});
for (const r of screen.regions) screen.click(r.rect.x, r.rect.y);
const system = screen.copied.find((text) => text.startsWith("## System"));
assert.ok(system);
assert.ok(system.includes("**Hostname:** test-host"));
assert.ok(system.includes("**Source:** simulated"));
assert.ok(system.includes("HQTUI demo · test-host"));
assert.ok(screen.copied.some((text) => text.startsWith("## CPU Overview")));
assert.ok(!screen.copied.join("\n").includes("DO NOT EXPORT PROCESS ROW"));
assert.ok(!screen.copied.some((text) => /^## (Processes|Journal)/.test(text)));
});
2 changes: 1 addition & 1 deletion apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default async function Home() {
High Quality Terminal UI for TypeScript, Rust, Go, Python, Zig and C++
</p>
<Badge variant="secondary" className="mb-5 font-mono text-xs">
v0.6.2 · {COUNT} language demos · MIT
v0.6.3 · {COUNT} language demos · MIT
</Badge>
<p className="text-balance text-3xl font-bold tracking-tight sm:text-5xl">
Terminal dashboards that
Expand Down
8 changes: 4 additions & 4 deletions bun.lock

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

20 changes: 20 additions & 0 deletions docs/markdown-copy-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Markdown summary copy — 0.6.3

Summary panels and modals can export Markdown through a header copy icon.
Apps opt in with `copyMarkdown`; `markdownContext` carries the host, source and
reporting period. `copyButton` supports custom status strips. Tables, logs, trees,
lists, inputs and raw drawing callbacks are excluded from automatic summaries.

The TypeScript demo enables the feature. The corresponding Crawlproof and
CoinPay integrations require HQTUI 0.6.3 or later in the 0.6 series.

Release the HQTUI library before its demo and consumer packages, then regenerate
consumer lockfiles against the published tarball. Build the library and demo with
`bun run build`. Local testing can install all prepared package tarballs together
with `npm install --prefix <directory> <tarball-paths>`.

Validation covers semantic exports before clipping, nested panels and opt-outs,
ASCII and narrow headers, mouse and keyboard activation, modal dismissal,
clipboard failures, UTF-8 OSC 52 payloads and tmux wrapping. The installed demo
was also exercised in a PTY: Tab/Enter emitted a Markdown payload and q exited
cleanly. Terminal clipboard support must be enabled for OSC 52 delivery.
42 changes: 42 additions & 0 deletions packages/hqtui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,45 @@ gracefully on limited terminals (no mouse, quantized color, ASCII instead of Bra
## License

MIT.

## Copy summaries as Markdown

Enable copy icons for summary panes throughout an app:

```ts
const app = await createApp({
copyMarkdown: true,
markdownContext: () => `Host: ${hostname}\nReporting period: ${period}`,
});

app.render(({ ui }) => {
ui.panel({ title: "Status" }, (p) => {
p.keyValues([{ label: "Connection", value: "Ready" }]);
});
ui.copyButton({ markdown: () => "## Status\n\nReady\n", width: 6 });
});
```

Click **⧉ MD**, or Tab / Shift+Tab to focus a control and Enter / Space to copy.
Other navigation keys return focus to the app. ASCII terminals display `C MD`;
narrow panes show only the icon. A short notice confirms the clipboard request.

Automatic exports include the pane title, subtitle, context, text, labeled values,
meters, progress, and graph summaries (latest/min/max/sample count). Values are
captured before wrapping and clipping. Tables, lists, logs, trees, input fields,
and raw drawing callbacks are excluded. Panels containing only data rows have no
copy icon. Layout branches that the app does not build cannot be exported.

Set `copyMarkdown: true` on one panel or modal to enable it individually,
`copyMarkdown: false` to exclude it (including from parent exports), or provide
a Markdown string/callback for a custom summary. `ui.copyButton()` places the
same control in a status strip or custom layout. Custom Markdown is copied as
provided; `markdownText(value)` escapes plain values for interpolation.

The default clipboard writer uses OSC 52 through the terminal, including over
SSH, and wraps the sequence for tmux. Terminal clipboard support must be enabled;
“Markdown copy sent” confirms delivery of the request, since terminals do not
acknowledge clipboard writes. Oversized exports fail explicitly instead of being
truncated. Supply `clipboard: (text) => ...` on `createApp()` to use another writer.
No shell command is run. `renderToScreen()` records copies in `screen.copied` for
interaction tests without changing the clipboard.
2 changes: 1 addition & 1 deletion packages/hqtui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/hqtui",
"version": "0.6.2",
"version": "0.6.3",
"description": "High Quality Terminal UI for TypeScript. btop-grade dashboards with a one-import API, dark by default, zero runtime dependencies.",
"license": "MIT",
"type": "module",
Expand Down
Loading