numux 1.16.0 → 1.17.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 -1
  2. package/dist/numux.js +116 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -300,7 +300,7 @@ Falsy values: unset, empty string, `"0"`, `"false"`, `"no"`, `"off"` (case-insen
300
300
 
301
301
  ### Dependency orchestration
302
302
 
303
- Processes are grouped into tiers by topological sort. Each tier starts after the previous tier is ready. If a process fails, its dependents are skipped.
303
+ Each process starts as soon as its declared `dependsOn` dependencies are ready it does not wait for unrelated processes. If a process fails, its dependents are skipped.
304
304
 
305
305
  A process becomes **ready** when:
306
306
  - **persistent + readyPattern** — the pattern matches in stdout
package/dist/numux.js CHANGED
@@ -36,7 +36,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
36
36
  var require_package = __commonJS((exports, module) => {
37
37
  module.exports = {
38
38
  name: "numux",
39
- version: "1.16.0",
39
+ version: "1.17.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -1691,38 +1691,49 @@ class ProcessManager {
1691
1691
  log("Starting all processes");
1692
1692
  this.lastCols = cols;
1693
1693
  this.lastRows = rows;
1694
- for (const tier of this.tiers) {
1695
- const readyPromises = [];
1696
- for (const name of tier) {
1697
- const proc = this.config.processes[name];
1698
- if (proc.condition && !evaluateCondition(proc.condition)) {
1699
- log(`Skipping ${name}: condition "${proc.condition}" not met`);
1700
- this.updateStatus(name, "skipped");
1701
- continue;
1702
- }
1703
- const deps = proc.dependsOn ?? [];
1704
- const failedDep = deps.find((d) => {
1705
- const s = this.states.get(d).status;
1706
- return s === "failed" || s === "skipped";
1707
- });
1708
- if (failedDep) {
1709
- log(`Skipping ${name}: dependency ${failedDep} failed`);
1710
- this.updateStatus(name, "skipped");
1711
- continue;
1712
- }
1713
- const { promise, resolve: resolve8 } = Promise.withResolvers();
1714
- readyPromises.push(promise);
1715
- this.pendingReadyResolvers.set(name, resolve8);
1716
- this.createRunner(name, () => {
1717
- this.pendingReadyResolvers.delete(name);
1718
- resolve8();
1719
- });
1720
- this.startProcess(name, cols, rows);
1694
+ const readyPromises = new Map;
1695
+ const readyResolvers = new Map;
1696
+ for (const name of this.tiers.flat()) {
1697
+ const { promise, resolve: resolve8 } = Promise.withResolvers();
1698
+ readyPromises.set(name, promise);
1699
+ readyResolvers.set(name, resolve8);
1700
+ }
1701
+ const launches = this.tiers.flat().map(async (name) => {
1702
+ const proc = this.config.processes[name];
1703
+ const resolve8 = readyResolvers.get(name);
1704
+ const deps = proc.dependsOn ?? [];
1705
+ if (deps.length > 0) {
1706
+ await Promise.all(deps.map((d) => readyPromises.get(d)));
1721
1707
  }
1722
- if (readyPromises.length > 0) {
1723
- await Promise.all(readyPromises);
1708
+ if (this.stopping) {
1709
+ resolve8();
1710
+ return;
1724
1711
  }
1725
- }
1712
+ if (proc.condition && !evaluateCondition(proc.condition)) {
1713
+ log(`Skipping ${name}: condition "${proc.condition}" not met`);
1714
+ this.updateStatus(name, "skipped");
1715
+ resolve8();
1716
+ return;
1717
+ }
1718
+ const failedDep = deps.find((d) => {
1719
+ const s = this.states.get(d).status;
1720
+ return s === "failed" || s === "skipped";
1721
+ });
1722
+ if (failedDep) {
1723
+ log(`Skipping ${name}: dependency ${failedDep} failed`);
1724
+ this.updateStatus(name, "skipped");
1725
+ resolve8();
1726
+ return;
1727
+ }
1728
+ this.pendingReadyResolvers.set(name, resolve8);
1729
+ this.createRunner(name, () => {
1730
+ this.pendingReadyResolvers.delete(name);
1731
+ resolve8();
1732
+ });
1733
+ this.startProcess(name, cols, rows);
1734
+ await readyPromises.get(name);
1735
+ });
1736
+ await Promise.all(launches);
1726
1737
  this.setupWatchers();
1727
1738
  }
1728
1739
  startProcess(name, cols, rows) {
@@ -2008,6 +2019,7 @@ var STATUS_HINTS = [
2008
2019
  [SHORTCUTS.search.label, SHORTCUTS.search.description],
2009
2020
  [SHORTCUTS.copy.label, SHORTCUTS.copy.description],
2010
2021
  [SHORTCUTS.clear.label, SHORTCUTS.clear.description],
2022
+ ["Ctrl+Click", "open link"],
2011
2023
  ["Ctrl+C", "quit"]
2012
2024
  ];
2013
2025
  var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
@@ -2016,12 +2028,57 @@ var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
2016
2028
  import { ScrollBoxRenderable } from "@opentui/core";
2017
2029
  import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
2018
2030
 
2031
+ // src/ui/url-handler.ts
2032
+ var URL_RE = /https?:\/\/[^\s<>"'`)\]},;]+/g;
2033
+ var FILE_PATH_RE = /(?:\.\.?\/|\/)[^\s:]+(?::(\d+)(?::(\d+))?)?/g;
2034
+ var TRAILING_PUNCT = /[.,;:!?)>\]'"]+$/;
2035
+ function findLinksInLine(line) {
2036
+ const links = [];
2037
+ for (const m of line.matchAll(URL_RE)) {
2038
+ const url = m[0].replace(TRAILING_PUNCT, "");
2039
+ links.push({
2040
+ url,
2041
+ start: m.index,
2042
+ end: m.index + url.length,
2043
+ type: "url"
2044
+ });
2045
+ }
2046
+ for (const m of line.matchAll(FILE_PATH_RE)) {
2047
+ const start = m.index;
2048
+ const end = m.index + m[0].length;
2049
+ if (links.some((l) => start < l.end && end > l.start))
2050
+ continue;
2051
+ links.push({
2052
+ url: m[0],
2053
+ start,
2054
+ end,
2055
+ type: "file"
2056
+ });
2057
+ }
2058
+ return links.sort((a, b) => a.start - b.start);
2059
+ }
2060
+ function findLinkAtPosition(line, col) {
2061
+ const links = findLinksInLine(line);
2062
+ return links.find((l) => col >= l.start && col < l.end) ?? null;
2063
+ }
2064
+ function openLink(link) {
2065
+ const opener = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
2066
+ if (!opener)
2067
+ return;
2068
+ try {
2069
+ const proc = Bun.spawn([opener, link.url]);
2070
+ proc.exited.catch(() => {});
2071
+ } catch {}
2072
+ }
2073
+
2074
+ // src/ui/pane.ts
2019
2075
  class Pane {
2020
2076
  scrollBox;
2021
2077
  terminal;
2022
2078
  decoder = new TextDecoder;
2023
2079
  _onScroll = null;
2024
2080
  _onCopy = null;
2081
+ _onLinkClick = null;
2025
2082
  _textLines = null;
2026
2083
  _textLinesLower = null;
2027
2084
  constructor(renderer, name, cols, rows, interactive = false) {
@@ -2056,6 +2113,15 @@ class Pane {
2056
2113
  }
2057
2114
  return result;
2058
2115
  };
2116
+ this.terminal.onMouseDown = (event) => {
2117
+ if (event.modifiers.ctrl && event.button === 0) {
2118
+ const link = this.getLinkAtMouse(event.x, event.y);
2119
+ if (link) {
2120
+ event.stopPropagation();
2121
+ this._onLinkClick?.(link);
2122
+ }
2123
+ }
2124
+ };
2059
2125
  this.scrollBox.add(this.terminal);
2060
2126
  }
2061
2127
  feed(data) {
@@ -2094,6 +2160,21 @@ class Pane {
2094
2160
  onCopy(handler) {
2095
2161
  this._onCopy = handler;
2096
2162
  }
2163
+ onLinkClick(handler) {
2164
+ this._onLinkClick = handler;
2165
+ }
2166
+ getLinkAtMouse(localX, localY) {
2167
+ if (!this._textLines) {
2168
+ const text = this.terminal.getText();
2169
+ this._textLines = text.split(`
2170
+ `);
2171
+ this._textLinesLower = this._textLines.map((l) => l.toLowerCase());
2172
+ }
2173
+ const lineIndex = Math.floor(this.scrollBox.scrollTop) + localY;
2174
+ if (lineIndex < 0 || lineIndex >= this._textLines.length)
2175
+ return null;
2176
+ return findLinkAtPosition(this._textLines[lineIndex], localX);
2177
+ }
2097
2178
  show() {
2098
2179
  this.scrollBox.visible = true;
2099
2180
  }
@@ -2543,6 +2624,10 @@ class App {
2543
2624
  this.copyToClipboard(text);
2544
2625
  this.statusBar.showTemporaryMessage("Copied!");
2545
2626
  });
2627
+ pane.onLinkClick((link) => {
2628
+ openLink(link);
2629
+ this.statusBar.showTemporaryMessage(`Opening ${link.url}`);
2630
+ });
2546
2631
  pane.onScroll(() => {
2547
2632
  if (this.searchMode && this.searchMatches.length > 0 && this.activePane === name) {
2548
2633
  this.updateSearchHighlights();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",