startall 0.0.9 → 0.0.11

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 (2) hide show
  1. package/index.js +153 -76
  2. package/package.json +3 -2
package/index.js CHANGED
@@ -357,6 +357,10 @@ class ProcessManager {
357
357
  this.splitMode = false; // Whether waiting for split command after Ctrl+b
358
358
  this.showSplitMenu = false; // Whether to show the command palette
359
359
  this.splitMenuIndex = 0; // Selected item in split menu
360
+ this.paneScrollPositions = new Map(); // Store scroll positions per pane ID
361
+ this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID
362
+ this.paneFilterState = new Map(); // Track filter state per pane to detect changes
363
+ this.paneLineCount = new Map(); // Track how many lines we've rendered per pane
360
364
 
361
365
  // Assign colors to each script
362
366
  this.processColors = new Map();
@@ -1811,9 +1815,94 @@ class ProcessManager {
1811
1815
  }
1812
1816
 
1813
1817
  updateRunningUI() {
1814
- // Just rebuild the entire UI - simpler and more reliable
1815
- // OpenTUI doesn't have great incremental update support anyway
1816
- this.buildRunningUI();
1818
+ // Update existing panes instead of rebuilding everything
1819
+ if (this.paneScrollBoxes.size > 0) {
1820
+ // Update each pane's content
1821
+ for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
1822
+ const pane = findPaneById(this.paneRoot, paneId);
1823
+ if (pane && scrollBox && scrollBox.content) {
1824
+ // Check if filter state changed or if paused (requires rebuild)
1825
+ const currentFilterState = JSON.stringify({
1826
+ filter: pane.filter || '',
1827
+ hidden: pane.hidden || [],
1828
+ processes: pane.processes || [],
1829
+ colorFilter: pane.colorFilter || null,
1830
+ });
1831
+ const previousFilterState = this.paneFilterState.get(paneId);
1832
+ const filterChanged = currentFilterState !== previousFilterState;
1833
+ const needsRebuild = filterChanged || this.isPaused;
1834
+
1835
+ if (needsRebuild) {
1836
+ // Filter changed - need to rebuild all content
1837
+ this.paneFilterState.set(paneId, currentFilterState);
1838
+
1839
+ // Remove all children
1840
+ if (scrollBox.content.children) {
1841
+ while (scrollBox.content.children.length > 0) {
1842
+ const child = scrollBox.content.children[0];
1843
+ if (child && child.id) {
1844
+ scrollBox.content.remove(child.id);
1845
+ } else {
1846
+ break;
1847
+ }
1848
+ }
1849
+ }
1850
+
1851
+ // Rebuild all content
1852
+ const height = scrollBox.height || this.renderer.height - 6;
1853
+ this.buildPaneOutput(pane, scrollBox.content, height);
1854
+
1855
+ // Update line count after rebuild
1856
+ const lines = this.getOutputLinesForPane(pane);
1857
+ this.paneLineCount.set(paneId, lines.length);
1858
+
1859
+ // Auto-scroll to bottom after filter change
1860
+ if (!this.isPaused) {
1861
+ scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
1862
+ }
1863
+ } else {
1864
+ // No filter change - just append new lines
1865
+ const lines = this.getOutputLinesForPane(pane);
1866
+ const lastRenderedCount = this.paneLineCount.get(paneId) || 0;
1867
+
1868
+ if (lines.length > lastRenderedCount) {
1869
+ const newLines = lines.slice(lastRenderedCount);
1870
+
1871
+ for (let i = 0; i < newLines.length; i++) {
1872
+ const line = newLines[i];
1873
+ const lineIndex = lastRenderedCount + i;
1874
+ const processColor = this.processColors.get(line.process) || COLORS.text;
1875
+ const trimmedText = line.text.trim();
1876
+
1877
+ const outputLine = new TextRenderable(this.renderer, {
1878
+ id: `output-${pane.id}-${lineIndex}`,
1879
+ content: t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`,
1880
+ bg: '#000000',
1881
+ });
1882
+
1883
+ scrollBox.content.add(outputLine);
1884
+ }
1885
+
1886
+ // Update line count
1887
+ this.paneLineCount.set(paneId, lines.length);
1888
+
1889
+ // Auto-scroll to bottom if not paused
1890
+ if (!this.isPaused) {
1891
+ scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
1892
+ }
1893
+ }
1894
+ }
1895
+
1896
+ // Update scrollbar visibility based on pause state
1897
+ if (scrollBox.verticalScrollBar) {
1898
+ scrollBox.verticalScrollBar.width = this.isPaused ? 1 : 0;
1899
+ }
1900
+ }
1901
+ }
1902
+ } else {
1903
+ // First time or no panes exist - do full rebuild
1904
+ this.buildRunningUI();
1905
+ }
1817
1906
  }
1818
1907
 
1819
1908
  // Build a single pane's output area
@@ -1821,79 +1910,24 @@ class ProcessManager {
1821
1910
  const isFocused = pane.id === this.focusedPaneId;
1822
1911
  const lines = this.getOutputLinesForPane(pane);
1823
1912
 
1824
- // Calculate visible lines - use global pause state
1825
- const outputHeight = Math.max(3, height - 2);
1826
- let linesToShow = this.isPaused ? lines : lines.slice(-outputHeight);
1827
-
1828
- // Use full terminal width for padding - simpler and ensures complete clearing
1829
- // Each pane will pad to full width, excess will be clipped by container
1830
- const approxPaneWidth = this.renderer.width;
1913
+ // Don't slice - show all lines and let ScrollBox handle scrolling
1914
+ const linesToShow = lines;
1831
1915
 
1832
- // Add lines in reverse order (newest first)
1833
- for (let i = linesToShow.length - 1; i >= 0; i--) {
1916
+ // Add lines (oldest first, so newest is at bottom)
1917
+ for (let i = 0; i < linesToShow.length; i++) {
1834
1918
  const line = linesToShow[i];
1835
1919
  const processColor = this.processColors.get(line.process) || COLORS.text;
1836
1920
 
1837
- const maxWidth = Math.max(20, this.renderer.width / 2 - line.process.length - 10);
1838
- const visibleLength = stripAnsi(line.text).length;
1839
- let truncatedText = line.text;
1840
- if (visibleLength > maxWidth) {
1841
- let visible = 0;
1842
- const ansiRegex = /\x1b\[[0-9;]*m/g;
1843
- let lastIndex = 0;
1844
- let result = '';
1845
- let match;
1846
- const text = line.text;
1847
- while ((match = ansiRegex.exec(text)) !== null) {
1848
- const before = text.slice(lastIndex, match.index);
1849
- for (const char of before) {
1850
- if (visible >= maxWidth - 3) break;
1851
- result += char;
1852
- visible++;
1853
- }
1854
- if (visible >= maxWidth - 3) break;
1855
- result += match[0];
1856
- lastIndex = ansiRegex.lastIndex;
1857
- }
1858
- if (visible < maxWidth - 3) {
1859
- const remaining = text.slice(lastIndex);
1860
- for (const char of remaining) {
1861
- if (visible >= maxWidth - 3) break;
1862
- result += char;
1863
- visible++;
1864
- }
1865
- }
1866
- truncatedText = result + '\x1b[0m...';
1867
- }
1868
-
1869
- // Get visible length of current line and pad to fill width
1870
- const linePrefix = `[${line.process}] `;
1871
- const currentLength = linePrefix.length + stripAnsi(truncatedText).length;
1872
-
1873
- // Pad with spaces to fill the width
1874
- const paddingNeeded = Math.max(0, approxPaneWidth - currentLength);
1875
- const padding = ' '.repeat(paddingNeeded);
1876
-
1921
+ // Trim whitespace and let text wrap naturally - ScrollBox will handle overflow
1922
+ const trimmedText = line.text.trim();
1877
1923
  const outputLine = new TextRenderable(this.renderer, {
1878
1924
  id: `output-${pane.id}-${i}`,
1879
- content: t`${fg(processColor)(`[${line.process}]`)} ${truncatedText}${padding}`,
1925
+ content: t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`,
1880
1926
  bg: '#000000', // Black background for pane content
1881
1927
  });
1882
1928
 
1883
1929
  container.add(outputLine);
1884
1930
  }
1885
-
1886
- // Fill remaining vertical space with blank lines
1887
- const emptyLinesNeeded = Math.max(0, outputHeight - linesToShow.length);
1888
- for (let j = 0; j < emptyLinesNeeded; j++) {
1889
- const emptyLine = new TextRenderable(this.renderer, {
1890
- id: `empty-${pane.id}-${j}`,
1891
- content: ' '.repeat(approxPaneWidth), // Fill entire width with spaces
1892
- bg: '#000000', // Black background for empty lines
1893
- });
1894
-
1895
- container.add(emptyLine);
1896
- }
1897
1931
  }
1898
1932
 
1899
1933
  // Count how many vertical panes exist (for width calculation)
@@ -1943,22 +1977,54 @@ class ProcessManager {
1943
1977
  backgroundColor: '#000000', // Black background for pane container
1944
1978
  });
1945
1979
 
1946
- // Output content - use BoxRenderable that fills remaining space
1980
+ // Output content - use ScrollBox to handle text wrapping properly
1947
1981
  // Use passed height or calculate default for line count calculation
1948
1982
  const height = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
1949
1983
 
1950
- const outputBox = new BoxRenderable(this.renderer, {
1984
+ const outputBox = new ScrollBoxRenderable(this.renderer, {
1951
1985
  id: `pane-output-${pane.id}`,
1952
- flexDirection: 'column',
1953
- flexGrow: 1,
1954
- flexShrink: 1,
1955
- flexBasis: 0,
1956
- overflow: 'hidden',
1957
- paddingLeft: 1,
1958
- backgroundColor: '#000000', // Black background for pane
1986
+ height: height,
1987
+ scrollX: false, // Disable horizontal scrollbar entirely
1988
+ scrollY: true, // Enable vertical scrolling
1989
+ focusable: true, // Enable mouse interactions and keyboard scrolling
1990
+ style: {
1991
+ rootOptions: {
1992
+ flexGrow: 1,
1993
+ flexShrink: 1,
1994
+ flexBasis: 0,
1995
+ paddingLeft: 1,
1996
+ backgroundColor: '#000000',
1997
+ },
1998
+ contentOptions: {
1999
+ backgroundColor: '#000000',
2000
+ width: '100%', // Fill container width for proper text wrapping
2001
+ },
2002
+ },
1959
2003
  });
1960
2004
 
1961
- this.buildPaneOutput(pane, outputBox, height);
2005
+ // Show scrollbar when paused, hide when not paused
2006
+ if (outputBox.verticalScrollBar) {
2007
+ outputBox.verticalScrollBar.width = this.isPaused ? 1 : 0;
2008
+ }
2009
+
2010
+ // Store ScrollBox reference for this pane
2011
+ this.paneScrollBoxes.set(pane.id, outputBox);
2012
+
2013
+ this.buildPaneOutput(pane, outputBox.content, height);
2014
+
2015
+ // Restore or set scroll position
2016
+ setTimeout(() => {
2017
+ if (!outputBox || !outputBox.scrollTo) return;
2018
+
2019
+ if (this.isPaused && this.paneScrollPositions.has(pane.id)) {
2020
+ // Restore saved scroll position when paused
2021
+ const savedPos = this.paneScrollPositions.get(pane.id);
2022
+ outputBox.scrollTo(savedPos);
2023
+ } else if (!this.isPaused) {
2024
+ // Auto-scroll to bottom when not paused
2025
+ outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2026
+ }
2027
+ }, 0);
1962
2028
 
1963
2029
  paneContainer.add(outputBox);
1964
2030
  return paneContainer;
@@ -2054,6 +2120,16 @@ class ProcessManager {
2054
2120
  }
2055
2121
 
2056
2122
  buildRunningUI() {
2123
+ // Save scroll positions before destroying
2124
+ for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
2125
+ if (scrollBox) {
2126
+ this.paneScrollPositions.set(paneId, {
2127
+ x: scrollBox.scrollLeft || 0,
2128
+ y: scrollBox.scrollTop || 0,
2129
+ });
2130
+ }
2131
+ }
2132
+
2057
2133
  // Remove old containers if they exist - use destroyRecursively to clean up all children
2058
2134
  if (this.selectionContainer) {
2059
2135
  this.renderer.root.remove(this.selectionContainer);
@@ -2072,8 +2148,9 @@ class ProcessManager {
2072
2148
  this.runningContainer.destroyRecursively();
2073
2149
  this.runningContainer = null;
2074
2150
  }
2075
- // Clear outputBox reference since it was destroyed with runningContainer
2151
+ // Clear outputBox reference and scrollbox map since they were destroyed
2076
2152
  this.outputBox = null;
2153
+ this.paneScrollBoxes.clear();
2077
2154
 
2078
2155
  // Create main container - full screen with black background
2079
2156
  const mainContainer = new BoxRenderable(this.renderer, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "startall",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "demo:worker": "node -e \"setInterval(() => console.log('Processing jobs...'), 500)\"",
15
15
  "demo:worker2": "node -e \"setInterval(() => console.log('Processing jobs...'), 100)\"",
16
16
  "demo:errors": "node -e \"setInterval(() => { console.log('\\x1b[32mOK: All good\\x1b[0m'); console.log('\\x1b[33mWARN: Deprecated API used\\x1b[0m'); console.log('\\x1b[31mERROR: Connection failed\\x1b[0m'); console.log('Normal log message'); }, 2000)\"",
17
- "demo:build": "node -e \"let i=0; setInterval(() => { i++; if(i%5===0) console.log('\\x1b[31mError: Type mismatch in file.ts:42\\x1b[0m'); else if(i%3===0) console.log('\\x1b[33mWarning: Unused variable\\x1b[0m'); else console.log('\\x1b[36mCompiling module ' + i + '...\\x1b[0m'); }, 800)\""
17
+ "demo:build": "node -e \"let i=0; setInterval(() => { i++; if(i%5===0) console.log('\\x1b[31mError: Type mismatch in file.ts:42\\x1b[0m'); else if(i%3===0) console.log('\\x1b[33mWarning: Unused variable\\x1b[0m'); else console.log('\\x1b[36mCompiling module ' + i + '...\\x1b[0m'); }, 800)\"",
18
+ "demo:longtext": "node -e \"setInterval(() => { console.log('\\x1b[36m[2024-01-20T12:34:56.789Z] Executing SQL query: SELECT users.id, users.name, users.email, orders.order_id, orders.total, products.name FROM users INNER JOIN orders ON users.id = orders.user_id LEFT JOIN products ON orders.product_id = products.id WHERE users.created_at > NOW() - INTERVAL 30 DAY AND orders.status = \\'completed\\' ORDER BY orders.total DESC LIMIT 100\\x1b[0m'); console.log('Fetched 42 records in 127ms'); }, 1500)\""
18
19
  },
19
20
  "keywords": [],
20
21
  "author": "",