numux 1.8.0 → 1.9.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.
Files changed (3) hide show
  1. package/README.md +1 -16
  2. package/dist/numux.js +76 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -265,22 +265,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
265
265
 
266
266
  ## Keybindings
267
267
 
268
- | Key | Action |
269
- |-----|--------|
270
- | `Ctrl+C` | Quit (graceful shutdown) |
271
- | `R` | Restart active process |
272
- | `Shift+R` | Restart all processes |
273
- | `S` | Stop/start active process |
274
- | `L` | Clear active pane output |
275
- | `F` | Search in active pane output |
276
- | `1`–`9` | Jump to tab |
277
- | `Left/Right` | Cycle tabs |
278
- | `PageUp/PageDown` | Scroll output by page |
279
- | `Home/End` | Scroll to top/bottom |
280
-
281
- While searching: type to filter, `Enter`/`Shift+Enter` to navigate matches, `Escape` to close.
282
-
283
- Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
268
+ Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set `interactive: true` on processes that need stdin (REPLs, shells, etc.).
284
269
 
285
270
  ## Tab icons
286
271
 
package/dist/numux.js CHANGED
@@ -22,7 +22,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
22
22
  var require_package = __commonJS((exports, module) => {
23
23
  module.exports = {
24
24
  name: "numux",
25
- version: "1.8.0",
25
+ version: "1.9.0",
26
26
  description: "Terminal multiplexer with dependency orchestration",
27
27
  type: "module",
28
28
  license: "MIT",
@@ -1549,6 +1549,26 @@ function evaluateCondition(condition) {
1549
1549
  // src/ui/app.ts
1550
1550
  import { BoxRenderable, createCliRenderer } from "@opentui/core";
1551
1551
 
1552
+ // src/ui/keybindings.ts
1553
+ var SHORTCUTS = {
1554
+ restartAll: { key: "r", label: "Shift+R", description: "restart all", shift: true },
1555
+ copy: { key: "y", label: "Y", description: "copy" },
1556
+ search: { key: "f", label: "F", description: "search" },
1557
+ restart: { key: "r", label: "R", description: "restart" },
1558
+ stopStart: { key: "s", label: "S", description: "stop/start" },
1559
+ clear: { key: "l", label: "L", description: "clear" }
1560
+ };
1561
+ var STATUS_HINTS = [
1562
+ ["\u2190\u2192/1-9", "tabs"],
1563
+ [SHORTCUTS.restart.label, SHORTCUTS.restart.description],
1564
+ [SHORTCUTS.stopStart.label, SHORTCUTS.stopStart.description],
1565
+ [SHORTCUTS.search.label, SHORTCUTS.search.description],
1566
+ [SHORTCUTS.copy.label, SHORTCUTS.copy.description],
1567
+ [SHORTCUTS.clear.label, SHORTCUTS.clear.description],
1568
+ ["Ctrl+C", "quit"]
1569
+ ];
1570
+ var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
1571
+
1552
1572
  // src/ui/pane.ts
1553
1573
  import { ScrollBoxRenderable } from "@opentui/core";
1554
1574
  import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
@@ -1558,6 +1578,7 @@ class Pane {
1558
1578
  terminal;
1559
1579
  decoder = new TextDecoder;
1560
1580
  _onScroll = null;
1581
+ _onCopy = null;
1561
1582
  constructor(renderer, name, cols, rows, interactive = false) {
1562
1583
  this.scrollBox = new ScrollBoxRenderable(renderer, {
1563
1584
  id: `pane-${name}`,
@@ -1576,6 +1597,18 @@ class Pane {
1576
1597
  showCursor: interactive,
1577
1598
  trimEnd: true
1578
1599
  });
1600
+ const origOnSelectionChanged = this.terminal.onSelectionChanged.bind(this.terminal);
1601
+ this.terminal.onSelectionChanged = (selection) => {
1602
+ const result = origOnSelectionChanged(selection);
1603
+ if (selection?.isActive && !selection.isDragging) {
1604
+ const text = selection.getSelectedText();
1605
+ if (text) {
1606
+ renderer.copyToClipboardOSC52(text);
1607
+ this._onCopy?.(text);
1608
+ }
1609
+ }
1610
+ return result;
1611
+ };
1579
1612
  this.scrollBox.add(this.terminal);
1580
1613
  }
1581
1614
  feed(data) {
@@ -1604,6 +1637,9 @@ class Pane {
1604
1637
  onScroll(handler) {
1605
1638
  this._onScroll = handler;
1606
1639
  }
1640
+ onCopy(handler) {
1641
+ this._onCopy = handler;
1642
+ }
1607
1643
  show() {
1608
1644
  this.scrollBox.visible = true;
1609
1645
  }
@@ -1667,6 +1703,8 @@ class StatusBar {
1667
1703
  _searchQuery = "";
1668
1704
  _searchMatchCount = 0;
1669
1705
  _searchCurrentIndex = -1;
1706
+ _tempMessage = null;
1707
+ _tempTimer = null;
1670
1708
  constructor(renderer) {
1671
1709
  this.renderable = new TextRenderable(renderer, {
1672
1710
  id: "status-bar",
@@ -1684,13 +1722,25 @@ class StatusBar {
1684
1722
  this._searchCurrentIndex = currentIndex;
1685
1723
  this.renderable.content = this.buildContent();
1686
1724
  }
1725
+ showTemporaryMessage(message, duration = 2000) {
1726
+ if (this._tempTimer)
1727
+ clearTimeout(this._tempTimer);
1728
+ this._tempMessage = message;
1729
+ this.renderable.content = this.buildContent();
1730
+ this._tempTimer = setTimeout(() => {
1731
+ this._tempMessage = null;
1732
+ this._tempTimer = null;
1733
+ this.renderable.content = this.buildContent();
1734
+ }, duration);
1735
+ }
1687
1736
  buildContent() {
1737
+ if (this._tempMessage) {
1738
+ return new StyledText([cyan(this._tempMessage)]);
1739
+ }
1688
1740
  if (this._searchMode) {
1689
1741
  return this.buildSearchContent();
1690
1742
  }
1691
- return new StyledText([
1692
- plain("\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit")
1693
- ]);
1743
+ return new StyledText([plain(STATUS_BAR_TEXT)]);
1694
1744
  }
1695
1745
  buildSearchContent() {
1696
1746
  const chunks = [];
@@ -1997,6 +2047,7 @@ class App {
1997
2047
  for (const name of this.names) {
1998
2048
  const interactive = this.config.processes[name].interactive === true;
1999
2049
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
2050
+ pane.onCopy(() => this.statusBar.showTemporaryMessage("Copied!"));
2000
2051
  this.panes.set(name, pane);
2001
2052
  paneContainer.add(pane.scrollBox);
2002
2053
  }
@@ -2060,19 +2111,23 @@ class App {
2060
2111
  const isInteractive = this.config.processes[this.activePane]?.interactive === true;
2061
2112
  if (!isInteractive) {
2062
2113
  const name = key.name.toLowerCase();
2063
- if (key.shift && name === "r") {
2114
+ if (key.shift && name === SHORTCUTS.restartAll.key) {
2064
2115
  this.manager.restartAll(this.termCols, this.termRows);
2065
2116
  return;
2066
2117
  }
2067
- if (name === "f") {
2118
+ if (name === SHORTCUTS.copy.key) {
2119
+ this.copySelection();
2120
+ return;
2121
+ }
2122
+ if (name === SHORTCUTS.search.key) {
2068
2123
  this.enterSearch();
2069
2124
  return;
2070
2125
  }
2071
- if (name === "r") {
2126
+ if (name === SHORTCUTS.restart.key) {
2072
2127
  this.manager.restart(this.activePane, this.termCols, this.termRows);
2073
2128
  return;
2074
2129
  }
2075
- if (name === "s") {
2130
+ if (name === SHORTCUTS.stopStart.key) {
2076
2131
  const state = this.manager.getState(this.activePane);
2077
2132
  if (state?.status === "stopped" || state?.status === "finished" || state?.status === "failed") {
2078
2133
  this.manager.start(this.activePane, this.termCols, this.termRows);
@@ -2081,7 +2136,7 @@ class App {
2081
2136
  }
2082
2137
  return;
2083
2138
  }
2084
- if (name === "l") {
2139
+ if (name === SHORTCUTS.clear.key) {
2085
2140
  this.panes.get(this.activePane)?.clear();
2086
2141
  return;
2087
2142
  }
@@ -2169,6 +2224,18 @@ class App {
2169
2224
  this.tabBar.setInputWaiting(name, false);
2170
2225
  }
2171
2226
  }
2227
+ copySelection() {
2228
+ const selection = this.renderer.getSelection();
2229
+ if (!selection?.isActive)
2230
+ return false;
2231
+ const text = selection.getSelectedText();
2232
+ if (!text)
2233
+ return false;
2234
+ this.renderer.copyToClipboardOSC52(text);
2235
+ this.renderer.clearSelection();
2236
+ this.statusBar.showTemporaryMessage("Copied!");
2237
+ return true;
2238
+ }
2172
2239
  enterSearch() {
2173
2240
  this.searchMode = true;
2174
2241
  this.searchQuery = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",