← All tools
nebelwolfi ·v1.0.0 ·updated 7 days ago ·15 downloads
htop
VerifiedTool example for building a surface that the agent can reason about
No readme yet. The shows what it registers.
htop.lua · v1.0.0 · 27 KB · md5 54259f884ec1ee9e99b78a38263a1e98 Download
-- htop: the reference tool surface (docs/surfaces.md, TB-792).
--
-- A saved Lua tool with a live panel. Install it like any saved tool: copy
-- this file to ~/.girl/tools/htop.lua (every project; found with tool_search)
-- or to the project's tools folder (loaded at once), or hand its text to
-- `save_tool`. Then ask the model to "open htop" and the panel opens in the
-- girl app's side dock.
--
-- * `htop` (the tool the model calls) only OPENS the panel: it returns the
-- girl/surface display, and the registry stamps the surface, its digest,
-- title and placement.
-- * `list` (a handler, no permission: classified surface.read, so it runs at
-- once, even mid-turn) reads the process list.
-- * `kill` (a handler, permission process.kill) stops one process. It
-- prompts like any gated tool, waits for a safe point, and is shown to the
-- model as a "[Surface activity, by the user]" row once it settles.
--
-- The Windows list command is NOT the one the design record first named.
-- docs/design/surfaces.md asked for `tasklist /fo csv /v`; measured on the
-- machine this was built on (TB-792), that command takes 29.9 s per call,
-- against 0.28 s for the PowerShell Get-Process form below, so a panel that
-- refreshes every 2 s could never refresh with it. Do not "restore" it:
-- `tasklist /fo csv /nh` is as fast but has no CPU column. Get-Process
-- reports CUMULATIVE CPU seconds; the page shows them as "CPU s" and derives
-- "CPU % (last 2 s)" from the change between two refreshes - never an
-- instantaneous sample, and a dash when there is no earlier sample.
--
-- The page talks to girl only through `window.girl` (docs/surfaces.md, "SDK
-- reference"): girl.call for the two handlers, girl.state for the selection
-- the model can read with `surface {action="state", surface="htop"}`,
-- girl.attach + girl.compose for "Ask about this", and girl.on("event") for a
-- model-driven `highlight {pid}`.
--
-- The page builds its whole UI from its one script with a small DOM subset
-- (createElement, appendChild/removeChild/firstChild, textContent, className,
-- setAttribute, addEventListener, value, disabled, document.body,
-- document.visibilityState and the visibilitychange event), so
-- tests/app_servers_harness.js can execute it in node against a stub of
-- exactly that subset. Keep to it, or extend that stub with it.
local fs = require("platform.fs")
local WINDOWS = package.config:sub(1, 1) == "\\"
-- The two list commands and the two kill commands, exactly (a suite test reads
-- them from this file, so neither platform can drift silently).
local LIST_WINDOWS = "Get-Process | Select-Object Id,ProcessName,CPU,WorkingSet64 | ConvertTo-Csv -NoTypeInformation"
local LIST_POSIX = "ps -eo pid,comm,%cpu,rss"
local KILL_WINDOWS = "taskkill /PID %d /F"
local KILL_POSIX = "kill -TERM %d"
local ROWS_MAX = 500
local RESULT_BUDGET = 48 * 1024 -- the dispatcher refuses a result over 64 KiB
local PID_MAX = 2147483647
local NAME_MAX = 260
-- "exit code: N\n<output>" (the platform.fs house shape) -> output, N.
local function output_of(text, code)
text = tostring(text or "")
local body = text:gsub("^exit code: %-?%d+\r?\n?", "", 1)
return body, tonumber(code) or tonumber(text:match("^exit code: (%-?%d+)")) or 1
end
-- One CSV line, RFC 4180 quoting ("" inside quotes is a quote).
local function csv_fields(line)
local fields, i, n = {}, 1, #line
while i <= n + 1 do
local c = line:sub(i, i)
if c == '"' then
local value, j = {}, i + 1
while j <= n do
local d = line:sub(j, j)
if d == '"' then
if line:sub(j + 1, j + 1) == '"' then value[#value + 1] = '"'; j = j + 2
else j = j + 1; break end
else value[#value + 1] = d; j = j + 1 end
end
fields[#fields + 1] = table.concat(value)
i = j + 1 -- skip the comma
else
local comma = line:find(",", i, true) or (n + 1)
fields[#fields + 1] = line:sub(i, comma - 1)
i = comma + 1
end
if i > n + 1 then break end
end
return fields
end
-- A number that may use a decimal comma; nil for an empty field.
local function number_of(text)
text = tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "")
if text == "" then return nil end
return tonumber((text:gsub(",", ".")))
end
local function parse_windows(body)
local rows, header = {}, nil
body = body:gsub("^\239\187\191", "") -- a UTF-8 byte order mark, if the shell wrote one
for line in body:gmatch("[^\r\n]+") do
local f = csv_fields(line)
if not header then
header = f
else
local pid = tonumber(f[1])
if pid and pid > 0 and f[2] and f[2] ~= "" then
local mem = number_of(f[4])
rows[#rows + 1] = { pid = pid, name = f[2], cpu = number_of(f[3]),
mem_bytes = mem and math.floor(mem) or nil }
end
end
end
if not header or header[1] ~= "Id" then error("htop: Get-Process answered no process table", 0) end
return rows
end
local function parse_posix(body)
local rows, first = {}, true
for line in body:gmatch("[^\r\n]+") do
if first then
first = false
else
-- PID COMMAND %CPU RSS; a command may hold spaces.
local pid, rest = line:match("^%s*(%d+)%s+(.+)$")
if pid then
local name, cpu, rss = rest:match("^(.-)%s+(%S+)%s+(%S+)%s*$")
local kib = number_of(rss)
if name and name ~= "" then
rows[#rows + 1] = { pid = tonumber(pid), name = name, cpu = number_of(cpu),
mem_bytes = kib and math.floor(kib * 1024) or nil }
end
end
end
end
return rows
end
local function read_processes(context)
local text, code
if WINDOWS then
text, code = fs.run_shell("powershell", context.workspace, LIST_WINDOWS, { max_chars = 2000000 })
else
text, code = fs.run(context.workspace, LIST_POSIX, { max_chars = 2000000 })
end
local body, exit = output_of(text, code)
if exit ~= 0 then error("htop: the process list failed (exit " .. tostring(exit) .. "): " .. body:sub(1, 200), 0) end
return WINDOWS and parse_windows(body) or parse_posix(body)
end
local function by_cpu(a, b)
local x, y = a.cpu or -1, b.cpu or -1
if x ~= y then return x > y end
return a.pid < b.pid
end
local function list(_, context)
local rows = read_processes(context)
local total = #rows
table.sort(rows, by_cpu)
while #rows > ROWS_MAX do rows[#rows] = nil end
local json = require("platform.json")
-- Trim from the tail (the idlest processes) until the answer fits.
while #rows > 0 and #json.encode(rows) > RESULT_BUDGET do
for _ = 1, math.max(1, math.floor(#rows / 10)) do rows[#rows] = nil end
end
return { platform = WINDOWS and "windows" or "posix", cpu = WINDOWS and "seconds" or "percent",
at = os.time(), total = total, rows = rows }
end
-- The kill's arguments, checked BEFORE anything runs: a pid is an integer in
-- 1..2^31-1 and a name a non-empty single line of at most 260 bytes.
local function checked(args)
args = type(args) == "table" and args or {}
local pid, name = args.pid, args.name
if type(pid) ~= "number" or pid ~= math.floor(pid) or pid < 1 or pid > PID_MAX then
error("htop: pid must be a whole number between 1 and " .. PID_MAX, 0)
end
if type(name) ~= "string" or name == "" or #name > NAME_MAX or name:find("%c") then
error("htop: name must be the process name, one line of at most " .. NAME_MAX .. " bytes", 0)
end
return pid, name
end
local function shown(value)
local text = tostring(value == nil and "?" or value):gsub("%c", "?")
if #text > 80 then text = text:sub(1, 79) .. "~" end
return text
end
local function kill(args, context)
local pid, name = checked(args)
-- The pid must still be that process: a pid the OS handed to another
-- process since the list was read (or one that is gone) is never stopped.
local found
for _, row in ipairs(read_processes(context)) do
if row.pid == pid then found = row; break end
end
if not found then error("htop: pid " .. pid .. " is not running (it may have exited); nothing was stopped", 0) end
if found.name ~= name then
error("htop: pid " .. pid .. " is now " .. shown(found.name) .. ", not " .. shown(name) .. "; nothing was stopped", 0)
end
-- Only the integer reaches the shell, never the name.
local text, code
if WINDOWS then
text, code = fs.run(context.workspace, string.format(KILL_WINDOWS, pid))
else
text, code = fs.run(context.workspace, string.format(KILL_POSIX, pid))
end
local body, exit = output_of(text, code)
if exit ~= 0 then error("htop: stopping pid " .. pid .. " failed (exit " .. exit .. "): " .. body:sub(1, 200), 0) end
-- A zero exit only says the request was delivered: SIGTERM can be ignored,
-- and a process can linger for a moment. So look once more and say which.
local exited = true
for _, row in ipairs(read_processes(context)) do
if row.pid == pid and row.name == found.name then exited = false; break end
end
return { stopped = pid, name = found.name, exited = exited }
end
local HTML = [==[<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>htop</title>
<style>
.htop{display:flex;flex-direction:column;gap:8px;min-height:100%}
.htop-bar{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.htop-bar input{flex:1 1 140px;min-width:0;padding:4px 8px;border:1px solid var(--border,#888);border-radius:6px;background:var(--surface,transparent)}
.htop button{padding:4px 10px;border:1px solid var(--border,#888);border-radius:6px;background:var(--surface-2,transparent);cursor:pointer}
.htop button:hover{background:var(--hover,rgba(127,127,127,.15))}
.htop button:disabled{opacity:.5;cursor:default}
.htop button.htop-primary{background:var(--accent,#36c);color:var(--on-accent,#fff);border-color:var(--accent,#36c)}
.htop-wrap{overflow:auto;border:1px solid var(--border,#888);border-radius:6px}
.htop table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
.htop th{position:sticky;top:0;background:var(--surface,var(--bg));text-align:left;font-weight:600;white-space:nowrap}
.htop th button{all:unset;box-sizing:border-box;width:100%;text-align:inherit;cursor:pointer;display:block;padding:4px 8px}
.htop th button:focus-visible{outline:2px solid var(--accent,#36c);outline-offset:-2px}
.htop th[aria-sort="ascending"] button::after{content:" \25B2";font-size:.7em}
.htop th[aria-sort="descending"] button::after{content:" \25BC";font-size:.7em}
.htop td{padding:3px 8px;border-top:1px solid var(--line2,var(--border,#8884));white-space:nowrap}
.htop td.num,.htop th.num{text-align:right}
.htop td.name{max-width:22ch;overflow:hidden;text-overflow:ellipsis}
.htop tr.htop-row{cursor:pointer}
.htop tr.htop-row:hover{background:var(--hover,rgba(127,127,127,.12))}
.htop tr.htop-row.sel{background:var(--selected,var(--accent-soft,rgba(80,120,255,.25)))}
.htop-sel{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.htop-sel .htop-what{flex:1 1 160px;min-width:0;color:var(--muted,inherit)}
.htop-status{min-height:1.4em;color:var(--muted,inherit)}
.htop-status.bad{color:var(--error-text,var(--red,#c33))}
@media (max-width:520px){.htop .col-raw,.htop .col-pid{display:none}.htop td.name{max-width:16ch}}
</style>
</head>
<body>
<noscript>htop needs scripts: open it in the girl app.</noscript>
<script>
(function () {
'use strict';
var G = window.girl;
var REFRESH_MS = 2000;
var KEYS = { pid: 1, name: 1, cpu: 1, recent: 1, mem: 1 };
var S = { rows: [], prev: {}, prevAt: 0, unit: 'seconds', platform: '', total: 0,
sort: 'cpu', dir: -1, sel: null, filter: '', timer: null, busy: false, dead: false, gen: 0 };
var start = G && G.data && typeof G.data === 'object' ? G.data.state : null;
if (start && typeof start === 'object') {
if (KEYS[start.sort]) S.sort = start.sort;
if (start.dir === 1 || start.dir === -1) S.dir = start.dir;
var s0 = start.selected;
if (s0 && isPid(s0.pid) && typeof s0.name === 'string') S.sel = { pid: s0.pid, name: s0.name };
}
function isPid(v) { return typeof v === 'number' && v === Math.floor(v) && v > 0; }
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text !== undefined) e.textContent = text;
return e;
}
function clear(e) { while (e.firstChild) e.removeChild(e.firstChild); }
function bytes(n) {
if (typeof n !== 'number') return '\u2013';
var u = ['B', 'KiB', 'MiB', 'GiB', 'TiB'], i = 0;
while (n >= 1024 && i < u.length - 1) { n = n / 1024; i++; }
return (i === 0 ? String(n) : n.toFixed(n >= 100 ? 0 : 1)) + ' ' + u[i];
}
function num(v, digits) { return typeof v === 'number' ? v.toFixed(digits) : '\u2013'; }
// ── the UI, built here (no markup to parse) ──
var root = el('div', 'htop');
var bar = el('div', 'htop-bar');
var filter = el('input');
filter.setAttribute('type', 'search');
filter.setAttribute('placeholder', 'Filter by name or pid');
filter.setAttribute('aria-label', 'Filter processes by name or pid');
var refreshBtn = el('button', 'htop-refresh', 'Refresh');
refreshBtn.setAttribute('type', 'button');
bar.appendChild(filter); bar.appendChild(refreshBtn);
var wrap = el('div', 'htop-wrap');
var table = el('table');
var thead = el('thead');
var headRow = el('tr');
var tbody = el('tbody');
thead.appendChild(headRow); table.appendChild(thead); table.appendChild(tbody); wrap.appendChild(table);
var selBox = el('div', 'htop-sel');
var what = el('span', 'htop-what', 'Select a process.');
var askBtn = el('button', 'htop-ask htop-primary', 'Ask about this');
var killBtn = el('button', 'htop-kill', 'Kill');
askBtn.setAttribute('type', 'button'); killBtn.setAttribute('type', 'button');
selBox.appendChild(what); selBox.appendChild(askBtn); selBox.appendChild(killBtn);
var status = el('div', 'htop-status');
status.setAttribute('role', 'status');
root.appendChild(bar); root.appendChild(wrap); root.appendChild(selBox); root.appendChild(status);
document.body.appendChild(root);
function say(text, bad) { status.textContent = text || ''; status.className = 'htop-status' + (bad ? ' bad' : ''); }
function columns() {
var cols = [{ key: 'pid', label: 'PID', cls: 'num col-pid' }, { key: 'name', label: 'Name', cls: '' }];
if (S.unit === 'seconds') {
cols.push({ key: 'cpu', label: 'CPU s', cls: 'num col-raw',
tip: 'CPU time this process has used since it started, in seconds (Get-Process CPU).' });
cols.push({ key: 'recent', label: 'CPU % (last 2 s)', cls: 'num',
tip: 'The change in CPU seconds between the last two refreshes, divided by the time between them (one core = 100 %). Not an instantaneous sample; a dash until there are two samples.' });
} else {
cols.push({ key: 'cpu', label: 'CPU % (ps)', cls: 'num',
tip: 'As ps reports %cpu: the CPU time used divided by the time the process has run. Not an instantaneous sample.' });
}
cols.push({ key: 'mem', label: 'Memory', cls: 'num',
tip: (S.platform === 'windows' ? 'Working set' : 'Resident set') + ', in bytes (shown as KiB/MiB/GiB; hover a cell for the exact count).' });
return cols;
}
function value(r, key) {
if (key === 'pid') return r.pid;
if (key === 'name') return String(r.name).toLowerCase();
if (key === 'cpu') return typeof r.cpu === 'number' ? r.cpu : null;
if (key === 'recent') return typeof r.recent === 'number' ? r.recent : null;
return typeof r.mem_bytes === 'number' ? r.mem_bytes : null;
}
function compare(a, b) {
var k = S.sort;
if (k === 'cpu' && S.unit === 'seconds') {
// Busy now first, then busy over its life.
var x = value(a, 'recent'), y = value(b, 'recent');
if (x !== y) { if (x === null) return 1; if (y === null) return -1; return (x < y ? -1 : 1) * S.dir; }
}
var p = value(a, k), q = value(b, k);
if (p !== q) { if (p === null) return 1; if (q === null) return -1; return (p < q ? -1 : 1) * S.dir; }
return a.pid - b.pid;
}
function visible() {
var f = S.filter.toLowerCase();
var out = [];
for (var i = 0; i < S.rows.length; i++) {
var r = S.rows[i];
if (!f || String(r.name).toLowerCase().indexOf(f) >= 0 || String(r.pid) === f) out.push(r);
}
out.sort(compare);
return out;
}
function selectedRow() {
if (!S.sel) return null;
for (var i = 0; i < S.rows.length; i++) if (S.rows[i].pid === S.sel.pid) return S.rows[i];
return null;
}
function paintHead() {
clear(headRow);
var cols = columns();
for (var i = 0; i < cols.length; i++) (function (c) {
var th = el('th', c.cls);
th.setAttribute('scope', 'col');
th.setAttribute('aria-sort', S.sort === c.key ? (S.dir > 0 ? 'ascending' : 'descending') : 'none');
var b = el('button', 'htop-sort', c.label);
b.setAttribute('type', 'button');
b.setAttribute('data-sort', c.key);
if (c.tip) b.setAttribute('title', c.tip);
b.addEventListener('click', function () { sortBy(c.key); });
th.appendChild(b);
headRow.appendChild(th);
})(cols[i]);
}
function paintRows() {
clear(tbody);
var rows = visible();
var cols = columns();
for (var i = 0; i < rows.length; i++) (function (r) {
var tr = el('tr', 'htop-row' + (S.sel && S.sel.pid === r.pid ? ' sel' : ''));
tr.setAttribute('data-pid', String(r.pid));
tr.setAttribute('tabindex', '0');
tr.setAttribute('aria-selected', S.sel && S.sel.pid === r.pid ? 'true' : 'false');
for (var j = 0; j < cols.length; j++) {
var k = cols[j].key, td;
if (k === 'pid') td = el('td', 'num col-pid', String(r.pid));
else if (k === 'name') { td = el('td', 'name', String(r.name)); td.setAttribute('title', String(r.name)); }
else if (k === 'cpu') td = el('td', 'num' + (S.unit === 'seconds' ? ' col-raw' : ''), num(r.cpu, 1));
else if (k === 'recent') td = el('td', 'num', typeof r.recent === 'number' ? r.recent.toFixed(1) + ' %' : '\u2013');
else { td = el('td', 'num', bytes(r.mem_bytes)); if (typeof r.mem_bytes === 'number') td.setAttribute('title', r.mem_bytes + ' bytes'); }
tr.appendChild(td);
}
tr.addEventListener('click', function () { select(r.pid); });
tr.addEventListener('keydown', function (e) { if (e && (e.key === 'Enter' || e.key === ' ')) select(r.pid); });
tbody.appendChild(tr);
})(rows[i]);
paintSelection();
}
function paintSelection() {
var r = selectedRow();
if (r) what.textContent = r.name + ' (pid ' + r.pid + ')';
else if (S.sel && S.landed) what.textContent = S.sel.name + ' (pid ' + S.sel.pid + ') is no longer listed.';
else what.textContent = 'Select a process.';
askBtn.disabled = !r;
killBtn.disabled = !r;
}
function report() {
if (G && typeof G.state === 'function')
G.state({ sort: S.sort, dir: S.dir, selected: S.sel ? { pid: S.sel.pid, name: S.sel.name } : null });
}
function paint() { paintHead(); paintRows(); }
function sortBy(key) {
if (!KEYS[key]) return;
if (S.sort === key) S.dir = -S.dir;
else { S.sort = key; S.dir = (key === 'pid' || key === 'name') ? 1 : -1; }
paint(); report();
}
function select(pid) {
for (var i = 0; i < S.rows.length; i++) if (S.rows[i].pid === pid) {
S.sel = { pid: pid, name: S.rows[i].name };
paintRows(); report();
return true;
}
return false;
}
// ── data ──
function land(res, now) {
var rows = res && Array.isArray(res.rows) ? res.rows : [];
S.unit = res && res.cpu === 'percent' ? 'percent' : 'seconds';
S.platform = res && typeof res.platform === 'string' ? res.platform : '';
S.total = res && typeof res.total === 'number' ? res.total : rows.length;
var dt = S.prevAt ? (now - S.prevAt) / 1000 : 0;
var next = {};
var out = [];
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
if (!r || !isPid(r.pid)) continue;
var row = { pid: r.pid, name: String(r.name == null ? '' : r.name),
cpu: typeof r.cpu === 'number' ? r.cpu : null,
mem_bytes: typeof r.mem_bytes === 'number' ? r.mem_bytes : null, recent: null };
var before = S.prev[row.pid];
// A dash unless the same process has an earlier sample to compare with.
if (S.unit === 'seconds' && dt > 0 && before && before.name === row.name
&& row.cpu !== null && before.cpu !== null && row.cpu >= before.cpu)
row.recent = (row.cpu - before.cpu) / dt * 100;
next[row.pid] = { cpu: row.cpu, name: row.name };
out.push(row);
}
S.prev = next; S.prevAt = now; S.rows = out; S.landed = true;
paint();
}
// Every list carries a generation: a kill bumps it, so an answer that was
// already on its way (read before the kill) is thrown away and asked again.
function refresh() {
if (S.timer) { clearTimeout(S.timer); S.timer = null; }
if (S.busy || S.dead) return;
S.busy = true;
var gen = S.gen;
G.call('list', {}).then(function (res) {
S.busy = false;
if (gen !== S.gen) { refresh(); return; }
land(res, Date.now());
if (/^Could not read/.test(status.textContent)) say('');
schedule();
}, function (err) {
S.busy = false;
if (gen !== S.gen) { refresh(); return; }
say('Could not read the process list: ' + (err && err.message ? err.message : 'no answer'), true);
schedule();
});
}
// The next refresh starts only after this one settled, and only while the
// panel is on screen: a slow list never piles calls up.
function schedule() {
if (S.timer || S.dead) return;
if (document.visibilityState === 'hidden') return;
S.timer = setTimeout(function () { S.timer = null; refresh(); }, REFRESH_MS);
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') { if (S.timer) { clearTimeout(S.timer); S.timer = null; } }
else if (!S.timer && !S.busy) refresh();
});
// ── actions ──
filter.addEventListener('input', function () { S.filter = String(filter.value || '').trim(); paintRows(); });
refreshBtn.addEventListener('click', function () { refresh(); });
askBtn.addEventListener('click', function () {
var r = selectedRow();
if (!r) return;
var label = r.name + ' (pid ' + r.pid + ')';
G.attach({ label: label, data: { pid: r.pid, name: r.name, cpu: r.cpu, cpu_unit: S.unit, mem_bytes: r.mem_bytes } })
.then(function (a) {
if (!a || a.ok === false) { say('Not attached: ' + ((a && a.why) || 'refused'), true); return null; }
return G.compose('What is this process doing?');
})
.then(function (c) {
if (!c) return;
if (c.ok) say('Attached ' + label + ' and filled the message box: press Enter to send.');
else if (c.asked) say('Attached ' + label + '. Your draft was kept: choose Replace or Keep above the message box.');
else say('Attached ' + label + '. ' + (c.why || ''), true);
}, function (err) { say('Not attached: ' + (err && err.message ? err.message : 'refused'), true); });
});
killBtn.addEventListener('click', function () {
var r = selectedRow();
if (!r) return;
var label = r.name + ' (pid ' + r.pid + ')';
killBtn.disabled = true;
say('Waiting for your approval to stop ' + label + '\u2026');
G.call('kill', { pid: r.pid, name: r.name }).then(function (res) {
if (res && res.exited === false) say('Asked ' + label + ' to stop; it has not exited yet.');
else say('Stopped ' + label + '.');
if (res && res.stopped === r.pid && res.exited !== false) S.sel = null;
S.gen++;
refresh();
}, function (err) {
say('Not stopped: ' + (err && err.message ? err.message : 'refused'), true);
paintSelection();
});
});
// The model drives the panel: surface {action="event", name="highlight", payload={pid=N}}.
if (G && typeof G.on === 'function') G.on('event', function (ev) {
if (ev && ev.name === 'highlight' && ev.payload && isPid(ev.payload.pid)) {
if (!select(ev.payload.pid)) say('pid ' + ev.payload.pid + ' is not in the list.', true);
}
});
paint();
if (!G || typeof G.call !== 'function') { say('This panel needs the girl app.', true); return; }
refresh();
})();
</script>
</body>
</html>
]==]
return function(registry)
registry:register {
name = "htop",
description = "Open a live process list (htop) in the girl app's side panel. The user can sort it, select a "
.. "process, attach it to the chat with \"Ask about this\", and stop it (Kill asks for permission). "
.. "To see which process the user selected, call surface {action=\"state\", surface=\"htop\"}; "
.. "to point at one, call surface {action=\"event\", surface=\"htop\", name=\"highlight\", payload={pid=N}}.",
input_schema = {
type = "object",
properties = {
sort = { type = "string", enum = { "cpu", "mem", "pid", "name" },
description = "The column to sort by first (cpu unless asked)." },
},
additionalProperties = false,
},
-- Opening the panel has no effect of its own (it returns a display):
-- surface.read is the read-only class every posture allows.
permission = "surface.read",
resource = "process list panel",
surface = {
title = "htop",
placement = "dock",
html = HTML,
handlers = {
list = {
description = "The running processes: pid, name, CPU and memory.",
run = list,
},
kill = {
description = "Stop one process, by pid and the name it must still have.",
permission = "process.kill",
resource = function(args)
args = type(args) == "table" and args or {}
return "pid " .. shown(args.pid) .. " (" .. shown(args.name) .. ")"
end,
run = kill,
},
},
},
run = function(args)
local sort = type(args) == "table" and args.sort or nil
if sort ~= "cpu" and sort ~= "mem" and sort ~= "pid" and sort ~= "name" then sort = "cpu" end
local dir = (sort == "pid" or sort == "name") and 1 or -1
return "Opened htop in the girl app. Read the user's selection with surface {action=\"state\", surface=\"htop\"}.",
{ renderer = "girl/surface", data = { state = { sort = sort, dir = dir, selected = nil } } }
end,
}
end
- 1.0.0 7 days ago · 27 KB · download
md5 54259f884ec1ee9e99b78a38263a1e98