slash-port 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,16 @@
1
+ <p align="center">
2
+ <a href="https://github.com/Slash-ui">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-slashui-dark.svg" />
5
+ <img src="docs/assets/logo-slashui-light.svg" alt="Slash UI" width="159" height="65" />
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
1
10
  # slash-port
2
11
 
3
- <!-- release:badge -->[![github](https://img.shields.io/badge/github-v0.3.0-0f8b7d?logo=github&logoColor=white)](https://github.com/Slash-ui/slash-port/releases/tag/v0.3.0)<!-- /release:badge -->
12
+ <!-- release:badge -->
13
+ [![github](https://img.shields.io/badge/github-v0.4.0-0f8b7d?logo=github&logoColor=white)](https://github.com/Slash-ui/slash-port/releases/tag/v0.4.0)<!-- /release:badge -->
4
14
  [![npm](https://img.shields.io/npm/v/slash-port?logo=npm&logoColor=white&color=0f8b7d)](https://www.npmjs.com/package/slash-port)
5
15
  [![downloads](https://img.shields.io/npm/dm/slash-port?color=0f8b7d)](https://www.npmjs.com/package/slash-port)
6
16
  [![CI](https://github.com/Slash-ui/slash-port/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Slash-ui/slash-port/actions/workflows/ci.yml)
@@ -91,9 +101,8 @@ Requires Node 22 or newer. Works on Linux, macOS, and Windows.
91
101
 
92
102
  ### Updating
93
103
 
94
- The current release is
95
- <!-- release:version -->0.3.0<!-- /release:version -->. To see what you have,
96
- and what is published:
104
+ The current release is <!-- release:version -->0.4.0<!-- /release:version -->. To
105
+ see what you have, and what is published:
97
106
 
98
107
  ```sh
99
108
  slash-port --version # the one you are running
@@ -162,6 +171,20 @@ at any time, and `d` hides the panel in either.
162
171
  | Confirmation | Says what closing it costs and how to undo it | Names the signal |
163
172
  | Cost | One scan | One scan, plus a lookup for the row under the cursor |
164
173
 
174
+ Both modes on a working machine, where the list is forty-two ports long
175
+ rather than the tidy eight above:
176
+
177
+ ![Beginner mode on a real machine: mailpit and adminer in Docker, five
178
+ http.server processes on 8001 to 8005, Ollama, Figma, Visual Studio Code and
179
+ macOS Handoff, each with where to open it and whether closing it is a good
180
+ idea, and a panel explaining that port 8001 is a web server and safe to
181
+ close](docs/assets/screenshot-beginner.png)
182
+
183
+ ![The same machine in advanced mode: pid, user, process and address for every
184
+ row, and a panel with the parent process, open connections, uptime, memory,
185
+ working directory and the full command
186
+ line](docs/assets/screenshot-advanced.png)
187
+
165
188
  Advanced mode's extra facts are fetched for the selected row only. A machine
166
189
  with four hundred listening sockets would otherwise pay four hundred times over
167
190
  for facts you are reading one row at a time.
@@ -341,6 +364,16 @@ are fixed rather than configurable:
341
364
  - **A process that has already exited is never signalled**, because by then its
342
365
  pid may belong to something else.
343
366
 
367
+ The confirmation for a row that is safe to close names the process, says what
368
+ closing it costs, and keeps the polite close and the forced one on separate
369
+ keys:
370
+
371
+ ![The confirmation over the list: Close whatever is using port 8002, the
372
+ process named as Python http.server, pid 11575, user slashui, the verdict that
373
+ it is yours and as easy to start again as it was to start, and the keys y to
374
+ close it politely, f to force it, n to leave it
375
+ alone](docs/assets/screenshot-close.png)
376
+
344
377
  On Windows there is no signal delivery: SIGTERM becomes `TerminateProcess`,
345
378
  which a process cannot catch or ignore, so nothing there gets the chance to
346
379
  shut down cleanly. The confirmation still applies - but "terminate" and "force"
package/dist/ui/App.js CHANGED
@@ -7,7 +7,7 @@ import { ABSENT, formatAddresses, formatDescription, formatPid, formatPort, form
7
7
  import { inspectProcess } from '../inspect.js';
8
8
  import { killEntry } from '../kill.js';
9
9
  import { scan } from '../scan/index.js';
10
- import { cell, color, layout, truncate } from './theme.js';
10
+ import { cell, color, displayWidth, layout, truncate } from './theme.js';
11
11
  /**
12
12
  * Every row the interface spends on something other than the list, and what it
13
13
  * spends them on. These have to be exact rather than generous: the viewport is
@@ -337,9 +337,65 @@ export function App({ initialEntries, initialFilter = '', udp: initialUdp = fals
337
337
  address: formatAddresses(entry),
338
338
  });
339
339
  return (_jsxs(Box, { children: [_jsxs(Text, { inverse: isSelected, color: rowColor, children: [left, ' '] }), _jsx(Text, { inverse: isSelected, color: rowColor ?? color('accent'), children: cell(formatDescription(entry), columnLayout.description) }), columnLayout.url > 0 && (_jsxs(Text, { inverse: isSelected, color: rowColor ?? color('heading'), children: [' ', cell(entry.url ?? ABSENT, columnLayout.url)] })), columnLayout.risk > 0 && (_jsxs(Text, { inverse: isSelected, color: color(RISK_COLOR[entry.risk]), children: [' ', cell(riskWord(entry.risk), columnLayout.risk)] }))] }, entry.id));
340
- }), !loading && entries.length === 0 && (_jsx(Text, { color: color('muted'), children: truncate(`Nothing is listening${udp ? '' : ' on TCP'}. Press u to include UDP, r to rescan.`, columnLayout.total) })), !loading && entries.length > 0 && visible.length === 0 && (_jsx(Text, { color: color('muted'), children: truncate(`No port matches “${filter}”. Press Esc to clear the filter.`, columnLayout.total) })), showPanel && current && !dialog && (_jsx(DetailPanel, { entry: current, mode: mode, detail: detail, docker: docker, width: columnLayout.total })), dialog && (_jsx(ConfirmDialog, { entry: dialog, mode: mode, compact: compact, width: columnLayout.total })), !cramped && filtering && (_jsxs(Text, { children: [_jsx(Text, { color: color('heading'), children: "filter " }), _jsx(Text, { children: truncate(filter, Math.max(8, columnLayout.total - 8)) }), _jsx(Text, { color: color('muted'), children: "\u258E" })] })), !cramped && !filtering && filter !== '' && (_jsxs(Text, { color: color('muted'), children: ["filter: ", truncate(filter, Math.max(8, columnLayout.total - 10))] })), !cramped && status && (_jsx(Text, { color: color(STATUS_COLOR[status.kind]), children: truncate(status.text, columnLayout.total) })), !dialog && (_jsx(Text, { color: color('muted'), children: truncate(beginner
341
- ? '↑↓ move · / find · x close it · r refresh · u udp · d details · m advanced · q quit'
342
- : '↑↓/jk move · PgUp/PgDn/g/G jump · / filter · x kill · r rescan · u udp · d detail · m beginner · q quit', columnLayout.total) }))] }));
340
+ }), !loading && entries.length === 0 && (_jsx(Text, { color: color('muted'), children: truncate(`Nothing is listening${udp ? '' : ' on TCP'}. Press u to include UDP, r to rescan.`, columnLayout.total) })), !loading && entries.length > 0 && visible.length === 0 && (_jsx(Text, { color: color('muted'), children: truncate(`No port matches “${filter}”. Press Esc to clear the filter.`, columnLayout.total) })), showPanel && current && !dialog && (_jsx(DetailPanel, { entry: current, mode: mode, detail: detail, docker: docker, width: columnLayout.total })), dialog && (_jsx(ConfirmDialog, { entry: dialog, mode: mode, compact: compact, width: columnLayout.total })), !cramped && filtering && (_jsxs(Text, { children: [_jsx(Text, { color: color('heading'), children: "filter " }), _jsx(Text, { children: truncate(filter, Math.max(8, columnLayout.total - 8)) }), _jsx(Text, { color: color('muted'), children: "\u258E" })] })), !cramped && !filtering && filter !== '' && (_jsxs(Text, { color: color('muted'), children: ["filter: ", truncate(filter, Math.max(8, columnLayout.total - 10))] })), !cramped && status && (_jsx(Text, { color: color(STATUS_COLOR[status.kind]), children: truncate(status.text, columnLayout.total) })), !dialog && _jsx(HelpLine, { mode: mode, width: columnLayout.total })] }));
341
+ }
342
+ /**
343
+ * The keys, kept apart from what they do so the key can be given a colour of
344
+ * its own. In one colour the line reads as a sentence and the reader has to
345
+ * pick the keys back out of it, which is the opposite of what a help line is
346
+ * for.
347
+ */
348
+ const HELP = {
349
+ beginner: [
350
+ ['↑↓', 'move'],
351
+ ['/', 'find'],
352
+ ['x', 'close it'],
353
+ ['r', 'refresh'],
354
+ ['u', 'udp'],
355
+ ['d', 'details'],
356
+ ['m', 'advanced'],
357
+ ['q', 'quit'],
358
+ ],
359
+ advanced: [
360
+ ['↑↓/jk', 'move'],
361
+ ['PgUp/PgDn/g/G', 'jump'],
362
+ ['/', 'filter'],
363
+ ['x', 'kill'],
364
+ ['r', 'rescan'],
365
+ ['u', 'udp'],
366
+ ['d', 'detail'],
367
+ ['m', 'beginner'],
368
+ ['q', 'quit'],
369
+ ],
370
+ };
371
+ const HELP_SEPARATOR = ' · ';
372
+ /**
373
+ * Measured in cells and trimmed by dropping whole hints, because half a hint
374
+ * helps nobody and an ellipsis costs about what a key does.
375
+ *
376
+ * `q quit` is kept and the hints before it give way, since the one line a
377
+ * reader goes looking for is the one that tells them how to get out - and it
378
+ * is last, which is exactly where trimming from the end would have taken it.
379
+ *
380
+ * Always renders, even with nothing left to show: the row is one the height
381
+ * arithmetic has already paid for, and giving it back would move the list.
382
+ */
383
+ function HelpLine({ mode, width }) {
384
+ const hints = HELP[mode];
385
+ const quit = hints[hints.length - 1];
386
+ const gap = displayWidth(HELP_SEPARATOR);
387
+ const cost = ([key, label]) => displayWidth(key) + 1 + displayWidth(label);
388
+ const shown = [];
389
+ let used = cost(quit);
390
+ for (const hint of hints.slice(0, -1)) {
391
+ if (used + gap + cost(hint) > width)
392
+ break;
393
+ shown.push(hint);
394
+ used += gap + cost(hint);
395
+ }
396
+ if (used <= width)
397
+ shown.push(quit);
398
+ return (_jsx(Text, { color: color('muted'), children: shown.map(([key, label], index) => (_jsxs(Text, { children: [index === 0 ? '' : HELP_SEPARATOR, _jsx(Text, { color: color('heading'), children: key }), " ", label] }, key))) }));
343
399
  }
344
400
  /** A `label value` line, with the labels aligned down the panel. */
345
401
  function FieldLines({ fields, width }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slash-port",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "See what is listening on your ports, understand what it is, and kill it safely.",
5
5
  "keywords": [
6
6
  "port",