update(procmon): fix windowed table follow-scroll & row sizing` (#340)
* update(procmon): fix windowed table follow-scroll & row sizing` * Update plugin.toml * Update README.md * fix(procmon): always render list body as scroll to stop bottom clipping 0-result search switched the body from ui.scroll to a bare ui.row; when results returned the row->scroll switch left the flexGrow viewport stuck short, clipping bottom rows. Use ui.scroll with a shared key in both branches so the tree keeps one stable scroll node.
This commit is contained in:
+8
-4
@@ -25,6 +25,7 @@ usage (plus the process count). Click the widget to toggle the process panel:
|
|||||||
```sh
|
```sh
|
||||||
noctalia msg panel-toggle weinguyen/procmon:panel
|
noctalia msg panel-toggle weinguyen/procmon:panel
|
||||||
```
|
```
|
||||||
|
|
||||||
The panel shows CPU, RAM and swap bars with the 1/5/15-minute load averages,
|
The panel shows CPU, RAM and swap bars with the 1/5/15-minute load averages,
|
||||||
then a process table. Sort by the column dropdown (PID, CPU%, MEM%, RSS,
|
then a process table. Sort by the column dropdown (PID, CPU%, MEM%, RSS,
|
||||||
COMMAND) and flip asc/desc with the arrow button. Type in the filter box to
|
COMMAND) and flip asc/desc with the arrow button. Type in the filter box to
|
||||||
@@ -54,7 +55,10 @@ while it is open.
|
|||||||
- The bar widget only renders data the service publishes; it never runs
|
- The bar widget only renders data the service publishes; it never runs
|
||||||
commands. The panel runs the configured `kill_command` when a row's ✕ is
|
commands. The panel runs the configured `kill_command` when a row's ✕ is
|
||||||
clicked.
|
clicked.
|
||||||
- Requires `plugin_api = 13`. CPU%, RAM%, swap and the 1/5/15-minute load
|
- Requires `plugin_api = 13`. The panel renders a windowed slice of the
|
||||||
averages are sampled from `/proc` (`/proc/stat`, `/proc/meminfo`,
|
process table and follows the keyboard cursor with edge-follow scrolling
|
||||||
`/proc/loadavg`) by the service, so they work with no separate system-monitor
|
(window slides only when the cursor pushes past an edge, so scrolling up
|
||||||
dependency.
|
doesn't collapse the page until the top edge is reached). CPU%, RAM%, swap
|
||||||
|
and the 1/5/15-minute load averages are sampled from `/proc` (`/proc/stat`,
|
||||||
|
`/proc/meminfo`, `/proc/loadavg`) by the service, so they work with no
|
||||||
|
separate system-monitor dependency.
|
||||||
|
|||||||
+96
-7
@@ -26,6 +26,23 @@ local MAX_ROWS = 80
|
|||||||
-- selected. Ctrl+D kills the selected process directly.
|
-- selected. Ctrl+D kills the selected process directly.
|
||||||
local selectedIdx = 0
|
local selectedIdx = 0
|
||||||
|
|
||||||
|
-- Windowed table rendering, edge-follow (htop/btop style). The panel renders
|
||||||
|
-- only a VIEW_CAP-row slice [winStart..winEnd] that fits the table viewport.
|
||||||
|
-- Rows ~30px (kill button height=26 + paddingV 2), so 10 fill the 660px panel
|
||||||
|
-- without clipping the last one (tuned so no dead gap sits below the list).
|
||||||
|
-- The cursor moves freely INSIDE the window; the window slides only when the
|
||||||
|
-- cursor pushes past an edge — down at the bottom edge, up at the top edge. So
|
||||||
|
-- scrolling back up from the bottom does not collapse the page: the rows above
|
||||||
|
-- (6,7,8,9) stay in view while the highlight climbs, and only reaching the
|
||||||
|
-- window's top edge starts following up again. There is no host "scroll to row
|
||||||
|
-- N" (API 21 only jumps to absolute bottom), so the slice is what the panel
|
||||||
|
-- draws and the window IS the visible page.
|
||||||
|
-- ponytail: VIEW_CAP is tuned to the panel's table height; could derive it from
|
||||||
|
-- a scroll fill-measure if the API ever exposes available height.
|
||||||
|
local VIEW_CAP = 10
|
||||||
|
local winStart = 1
|
||||||
|
local winEnd = VIEW_CAP
|
||||||
|
|
||||||
-- Cheap fingerprint of everything the table shows, so the 1s tick only rebuilds
|
-- Cheap fingerprint of everything the table shows, so the 1s tick only rebuilds
|
||||||
-- the heavy UI tree when the data actually changed. Rebuilding 200 rows every
|
-- the heavy UI tree when the data actually changed. Rebuilding 200 rows every
|
||||||
-- second exceeded the panel update CPU budget; sampling the first SIG_SAMPLE
|
-- second exceeded the panel update CPU budget; sampling the first SIG_SAMPLE
|
||||||
@@ -253,12 +270,14 @@ local function dataRow(p, idx)
|
|||||||
color = selected and "primary" or "on_surface",
|
color = selected and "primary" or "on_surface",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
-- Kill button (fixed trailing width)
|
-- Kill button (fixed trailing width). Explicit height keeps the row short
|
||||||
|
-- enough that VIEW_CAP rows fit without clipping the last one.
|
||||||
table.insert(children, ui.button({
|
table.insert(children, ui.button({
|
||||||
glyph = "square-x",
|
glyph = "square-x",
|
||||||
glyphSize = 14,
|
glyphSize = 14,
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
controlSize = "sm",
|
controlSize = "sm",
|
||||||
|
height = 26,
|
||||||
width = KILL_W,
|
width = KILL_W,
|
||||||
tooltip = tr("panel.kill_tip", { pid = p.pid }),
|
tooltip = tr("panel.kill_tip", { pid = p.pid }),
|
||||||
onClick = function()
|
onClick = function()
|
||||||
@@ -376,6 +395,43 @@ local function visibleList()
|
|||||||
return list, #list
|
return list, #list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Keep the window [winStart..winEnd] covering the cursor and inside the list.
|
||||||
|
-- Called from render() after the cursor is clamped, so it also heals the window
|
||||||
|
-- when the list shrank (filter/sort/data) and left the cursor or window out of
|
||||||
|
-- bounds. The lazy up-follow during navigation lives in onKey, not here (this
|
||||||
|
-- only heals shrink/regrow).
|
||||||
|
local function clampWindow(n)
|
||||||
|
if n == 0 then
|
||||||
|
winStart, winEnd = 1, 0
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Cursor below the window (list regrew or follow-down persisted): extend.
|
||||||
|
if selectedIdx > winEnd then
|
||||||
|
winEnd = selectedIdx
|
||||||
|
winStart = math.max(1, winEnd - VIEW_CAP + 1)
|
||||||
|
-- Cursor above the window: the list shrank (a filter) or re-sorted and left the
|
||||||
|
-- cursor outside. Flatten back to the top page — cleared search shows row 1.
|
||||||
|
elseif selectedIdx < winStart then
|
||||||
|
winStart = 1
|
||||||
|
winEnd = math.min(VIEW_CAP, n)
|
||||||
|
end
|
||||||
|
-- Never let the window extend past the list nor below row 1.
|
||||||
|
if winEnd > n then
|
||||||
|
winEnd = n
|
||||||
|
winStart = math.max(1, winEnd - VIEW_CAP + 1)
|
||||||
|
end
|
||||||
|
if winStart < 1 then
|
||||||
|
winStart = 1
|
||||||
|
end
|
||||||
|
-- Re-expand a below-capacity window to a full page. Only fires when the list
|
||||||
|
-- shrank (filter) then regrew: otherwise the window would stay a few rows wide
|
||||||
|
-- and grow one row per Down press. Filling back to VIEW_CAP makes a cleared
|
||||||
|
-- search show the whole page again. No-op during normal navigation.
|
||||||
|
if n >= VIEW_CAP and winEnd - winStart + 1 < VIEW_CAP then
|
||||||
|
winEnd = math.min(winStart + VIEW_CAP - 1, n)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function render()
|
function render()
|
||||||
local wantsFocus = focusFilterOnRender
|
local wantsFocus = focusFilterOnRender
|
||||||
focusFilterOnRender = false
|
focusFilterOnRender = false
|
||||||
@@ -393,15 +449,28 @@ function render()
|
|||||||
|
|
||||||
local body
|
local body
|
||||||
if #shown == 0 then
|
if #shown == 0 then
|
||||||
body = ui.row({ align = "center", justify = "center", flexGrow = 1 }, {
|
-- Keep the body a ui.scroll in BOTH states so the flexGrow container keeps
|
||||||
ui.label({ text = tr("panel.no_processes"), color = "on_surface_variant" }),
|
-- a stable height. Rendering a bare ui.row here (and switching back to a
|
||||||
|
-- scroll once results return) left the scroll container stuck short, which
|
||||||
|
-- clipped the bottom of the list after a 0-result search.
|
||||||
|
winStart, winEnd = 1, 0
|
||||||
|
-- Same stable key as the results scroll so the UI tree reconciles this as
|
||||||
|
-- one persistent scroll node across the 0-result <-> results transition,
|
||||||
|
-- keeping the flexGrow viewport height stable (avoids bottom clipping).
|
||||||
|
body = ui.scroll({ key = "proc-list", flexGrow = 1 }, {
|
||||||
|
ui.row({ align = "center", justify = "center", flexGrow = 1 }, {
|
||||||
|
ui.label({ text = tr("panel.no_processes"), color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
|
clampWindow(#shown)
|
||||||
local itemRows = {}
|
local itemRows = {}
|
||||||
for i, p in ipairs(shown) do
|
for i = winStart, winEnd do
|
||||||
table.insert(itemRows, dataRow(p, i))
|
if shown[i] then
|
||||||
|
table.insert(itemRows, dataRow(shown[i], i))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
body = ui.scroll({ flexGrow = 1, gap = 2, align = "stretch" }, itemRows)
|
body = ui.scroll({ key = "proc-list", flexGrow = 1, gap = 2, align = "stretch" }, itemRows)
|
||||||
end
|
end
|
||||||
|
|
||||||
local refreshedAt = (st("refreshedAtMs") or 0) / 1000
|
local refreshedAt = (st("refreshedAtMs") or 0) / 1000
|
||||||
@@ -554,7 +623,25 @@ function onKey(chord, pressed)
|
|||||||
if chord == "up" or chord == "down" then
|
if chord == "up" or chord == "down" then
|
||||||
local list = visibleList()
|
local list = visibleList()
|
||||||
if #list > 0 then
|
if #list > 0 then
|
||||||
selectedIdx = math.max(1, math.min(#list, selectedIdx + (chord == "down" and 1 or -1)))
|
if chord == "down" then
|
||||||
|
selectedIdx = math.min(#list, selectedIdx + 1)
|
||||||
|
-- Cursor pushed past the bottom edge: extend the window one row and
|
||||||
|
-- slide its top so the new row is revealed below the cursor.
|
||||||
|
if selectedIdx > winEnd then
|
||||||
|
winEnd = selectedIdx
|
||||||
|
winStart = math.max(1, winEnd - VIEW_CAP + 1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
selectedIdx = math.max(1, selectedIdx - 1)
|
||||||
|
-- Cursor rose past the top edge: only now follow up — slide the window's
|
||||||
|
-- top to the cursor and refill down. Rows inside the window (6,7,8,9
|
||||||
|
-- while stepping down from 10) never shift the window; reaching below the
|
||||||
|
-- top edge (5) is what starts scrolling up again.
|
||||||
|
if selectedIdx < winStart then
|
||||||
|
winStart = selectedIdx
|
||||||
|
winEnd = math.min(winStart + VIEW_CAP - 1, #list)
|
||||||
|
end
|
||||||
|
end
|
||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
@@ -592,5 +679,7 @@ function onOpen(_context)
|
|||||||
-- keys and Ctrl+D work immediately (btop-style). Press Ctrl+F to start
|
-- keys and Ctrl+D work immediately (btop-style). Press Ctrl+F to start
|
||||||
-- typing a filter; the box is focused then.
|
-- typing a filter; the box is focused then.
|
||||||
selectedIdx = 0
|
selectedIdx = 0
|
||||||
|
winStart = 1
|
||||||
|
winEnd = VIEW_CAP
|
||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
id = "weinguyen/procmon"
|
id = "weinguyen/procmon"
|
||||||
name = "Process Monitor"
|
name = "Process Monitor"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
plugin_api = 13
|
plugin_api = 13
|
||||||
author = "weinguyen"
|
author = "weinguyen"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
Reference in New Issue
Block a user