ftreeview 0.1.4 → 0.1.5

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
@@ -13,6 +13,7 @@ A lightweight, interactive CLI file explorer with live monitoring and git awaren
13
13
  - **Colored File Types** - Syntax-highlighted file extensions (JS, TS, Python, Go, Rust, etc.)
14
14
  - **Smart Icons** - Nerd Font devicons (optional) with emoji fallback; ASCII mode supported
15
15
  - **Keyboard Navigation** - Full keyboard control with arrow keys or vim bindings
16
+ - **Optional Size Display** - Show selected file size in status bar when needed
16
17
  - **Handles Large Directories** - Efficient traversal with viewport scrolling
17
18
  - **Terminal Resize Support** - Adapts to terminal window size changes
18
19
  - **Clean Exit** - Graceful shutdown with proper terminal state restoration
@@ -70,6 +71,7 @@ ftreeview [path] [options]
70
71
  | `--icon-set <emoji\|nerd\|ascii>` | Choose icon set (default: emoji). You can also set `FTREE_ICON_SET`. |
71
72
  | `--theme <vscode>` | UI theme (default: vscode). You can also set `FTREE_THEME`. |
72
73
  | `--no-watch` | Disable file watching for static view |
74
+ | `--show-size` | Show selected file size in status bar (non-recursive, lightweight) |
73
75
 
74
76
  ### Examples
75
77
 
@@ -94,6 +96,9 @@ ftreeview --no-git
94
96
 
95
97
  # Static view (no live watching)
96
98
  ftreeview --no-watch
99
+
100
+ # Show selected file size in status bar
101
+ ftreeview --show-size
97
102
  ```
98
103
 
99
104
  ## Environment Variables
@@ -116,6 +121,7 @@ ftreeview --no-git
116
121
  | `g` | Jump to first item |
117
122
  | `G` | Jump to last item |
118
123
  | `r` | Refresh tree |
124
+ | `?` | Toggle keybinding hints |
119
125
  | `q` / `Ctrl+C` | Quit |
120
126
 
121
127
  ## Screenshots
package/dist/cli.js CHANGED
@@ -321,6 +321,50 @@ function formatPath(path, maxWidth) {
321
321
  }
322
322
  return truncatePath(ellipsisPath, maxWidth);
323
323
  }
324
+ function getEventMessageColor(eventType) {
325
+ if (eventType === "add" || eventType === "addDir") {
326
+ return "green";
327
+ }
328
+ if (eventType === "change") {
329
+ return "yellow";
330
+ }
331
+ if (eventType === "unlink" || eventType === "unlinkDir") {
332
+ return "magenta";
333
+ }
334
+ return "magenta";
335
+ }
336
+ function formatBytes(bytes) {
337
+ if (!Number.isFinite(bytes) || bytes < 0) {
338
+ return "";
339
+ }
340
+ if (bytes < 1024) {
341
+ return `${bytes}B`;
342
+ }
343
+ const units = ["KB", "MB", "GB", "TB"];
344
+ let value = bytes / 1024;
345
+ let unitIndex = 0;
346
+ while (value >= 1024 && unitIndex < units.length - 1) {
347
+ value /= 1024;
348
+ unitIndex++;
349
+ }
350
+ const decimals = value >= 10 ? 0 : 1;
351
+ return `${value.toFixed(decimals)}${units[unitIndex]}`;
352
+ }
353
+ function getWatchDisplay({ isWatching, watchMode, watchError, watchIcon }) {
354
+ if (watchMode === "off") {
355
+ return { text: `${watchIcon} Static`, tone: "muted" };
356
+ }
357
+ if (watchError) {
358
+ return { text: `${watchIcon} Paused`, tone: "warning" };
359
+ }
360
+ if (!isWatching) {
361
+ return { text: `${watchIcon} Starting`, tone: "muted" };
362
+ }
363
+ if (watchMode === "polling") {
364
+ return { text: `${watchIcon} Watching (poll)`, tone: "warning" };
365
+ }
366
+ return { text: `${watchIcon} Watching`, tone: "success" };
367
+ }
324
368
  function calculateGitStatus(statusMap) {
325
369
  if (!statusMap || statusMap.size === 0) {
326
370
  return { isRepo: false, summary: "" };
@@ -346,11 +390,18 @@ function StatusBar({
346
390
  fileCount = 0,
347
391
  dirCount = 0,
348
392
  isWatching = false,
393
+ watchMode = "native",
394
+ watchError = false,
349
395
  gitEnabled = false,
350
396
  isGitRepo = false,
351
397
  gitSummary = "",
352
398
  showHelp = true,
399
+ selectedPath = "",
400
+ showSize = false,
401
+ selectedIsDir = false,
402
+ selectedSize = void 0,
353
403
  eventMessage = "",
404
+ eventType = "",
354
405
  termWidth = 80,
355
406
  iconSet = "emoji",
356
407
  theme = VSCODE_THEME
@@ -384,9 +435,19 @@ function StatusBar({
384
435
  const paddingWidth = 2;
385
436
  const versionText = `ftreeview ${version}`;
386
437
  const countsText = `${barIcons.file} ${fileCount} ${barIcons.folder} ${dirCount}`;
387
- const watchText = `${barIcons.watch} ${isWatching ? "Watching" : "Static"}`;
438
+ const watchDisplay = getWatchDisplay({
439
+ isWatching,
440
+ watchMode,
441
+ watchError,
442
+ watchIcon: barIcons.watch
443
+ });
444
+ const watchText = watchDisplay.text;
445
+ const watchColor = watchDisplay.tone === "success" ? finalTheme.colors.success : watchDisplay.tone === "warning" ? finalTheme.colors.warning : finalTheme.colors.muted;
388
446
  const gitText = gitEnabled && isGitRepo ? `${barIcons.git} ${gitSummary || "clean"}` : "";
389
- const fixedSegments = [versionText, countsText, watchText].concat(gitText ? [gitText] : []);
447
+ const sizeValueText = !showSize ? "" : selectedIsDir ? "--" : formatBytes(selectedSize);
448
+ const showSizeText = Boolean(showSize && sizeValueText);
449
+ const sizeText = showSizeText ? `size ${sizeValueText}` : "";
450
+ const fixedSegments = [versionText, countsText, watchText].concat(showSizeText ? [sizeText] : []).concat(gitText ? [gitText] : []);
390
451
  const fixedWidth = fixedSegments.reduce((sum, part) => sum + part.length, 0) + fixedSegments.length * sep4.length + paddingWidth;
391
452
  const pathMaxWidth = Math.max(8, termWidth - fixedWidth);
392
453
  const displayPath = formatPath(rootPath, pathMaxWidth);
@@ -407,7 +468,11 @@ function StatusBar({
407
468
  /* @__PURE__ */ jsx3(Text3, { ...finalTheme.statusBar.separatorStyle, children: sep4 }),
408
469
  /* @__PURE__ */ jsx3(Text3, { color: finalTheme.colors.muted, children: countsText }),
409
470
  /* @__PURE__ */ jsx3(Text3, { ...finalTheme.statusBar.separatorStyle, children: sep4 }),
410
- /* @__PURE__ */ jsx3(Text3, { color: isWatching ? finalTheme.colors.success : finalTheme.colors.muted, children: watchText }),
471
+ /* @__PURE__ */ jsx3(Text3, { color: watchColor, children: watchText }),
472
+ showSizeText && /* @__PURE__ */ jsxs2(Fragment, { children: [
473
+ /* @__PURE__ */ jsx3(Text3, { ...finalTheme.statusBar.separatorStyle, children: sep4 }),
474
+ /* @__PURE__ */ jsx3(Text3, { color: finalTheme.colors.muted, children: sizeText })
475
+ ] }),
411
476
  gitEnabled && isGitRepo && /* @__PURE__ */ jsxs2(Fragment, { children: [
412
477
  /* @__PURE__ */ jsx3(Text3, { ...finalTheme.statusBar.separatorStyle, children: sep4 }),
413
478
  /* @__PURE__ */ jsx3(Text3, { color: gitSummary === "clean" ? finalTheme.colors.success : finalTheme.colors.warning, children: gitText })
@@ -420,7 +485,21 @@ function StatusBar({
420
485
  {
421
486
  width: termWidth,
422
487
  paddingX: 1,
423
- children: /* @__PURE__ */ jsx3(Text3, { ...finalTheme.statusBar.hintStyle, children: "q quit \u2191\u2193/jk move \u2190\u2192/hl expand space toggle r refresh" })
488
+ children: /* @__PURE__ */ jsx3(Text3, { ...finalTheme.statusBar.hintStyle, children: "q quit \u2191\u2193/jk move \u2190\u2192/hl expand space toggle r refresh ? help" })
489
+ }
490
+ ),
491
+ selectedPath && /* @__PURE__ */ jsxs2(
492
+ Box3,
493
+ {
494
+ width: termWidth,
495
+ paddingX: 1,
496
+ children: [
497
+ /* @__PURE__ */ jsxs2(Text3, { color: finalTheme.colors.muted, children: [
498
+ "selected",
499
+ " "
500
+ ] }),
501
+ /* @__PURE__ */ jsx3(Text3, { color: finalTheme.colors.fg, children: formatPath(selectedPath, Math.max(8, termWidth - 11)) })
502
+ ]
424
503
  }
425
504
  ),
426
505
  eventMessage && /* @__PURE__ */ jsx3(
@@ -428,7 +507,7 @@ function StatusBar({
428
507
  {
429
508
  width: termWidth,
430
509
  paddingX: 1,
431
- children: /* @__PURE__ */ jsx3(Text3, { color: "magenta", bold: true, children: eventMessage })
510
+ children: /* @__PURE__ */ jsx3(Text3, { color: getEventMessageColor(eventType), bold: true, children: eventMessage })
432
511
  }
433
512
  )
434
513
  ] });
@@ -930,7 +1009,7 @@ function useTree(rootPath, options = {}) {
930
1009
  import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
931
1010
  import { useInput, useApp } from "ink";
932
1011
  var VIEWPORT_MARGIN = 3;
933
- function useNavigation(flatList = [], rebuildTree, refreshFlatList = null, ensureChildrenLoaded = null) {
1012
+ function useNavigation(flatList = [], rebuildTree, refreshFlatList = null, ensureChildrenLoaded = null, onToggleHelp = null) {
934
1013
  const { exit } = useApp();
935
1014
  const viewportHeight = process.stdout.rows - 2;
936
1015
  const [cursor, setCursor] = useState2(0);
@@ -1034,6 +1113,8 @@ function useNavigation(flatList = [], rebuildTree, refreshFlatList = null, ensur
1034
1113
  jumpToBottom();
1035
1114
  } else if (input === "r") {
1036
1115
  refresh();
1116
+ } else if (input === "?" && typeof onToggleHelp === "function") {
1117
+ onToggleHelp();
1037
1118
  }
1038
1119
  });
1039
1120
  useEffect(() => {
@@ -1311,6 +1392,8 @@ function shouldUsePolling(rootPath) {
1311
1392
  function useWatcher(rootPath, onTreeChange, options = {}) {
1312
1393
  const debounceMs = options.debounceMs ?? DEFAULT_CONFIG.debounceDelay;
1313
1394
  const [isWatching, setIsWatching] = useState4(false);
1395
+ const [watchMode, setWatchMode] = useState4(options.noWatch ? "off" : "native");
1396
+ const [watchError, setWatchError] = useState4(false);
1314
1397
  const watcherRef = useRef3(null);
1315
1398
  const onTreeChangeRef = useRef3(onTreeChange);
1316
1399
  const mountedRef = useRef3(true);
@@ -1325,11 +1408,15 @@ function useWatcher(rootPath, onTreeChange, options = {}) {
1325
1408
  mountedRef.current = true;
1326
1409
  if (options.noWatch || !rootPath) {
1327
1410
  setIsWatching(false);
1411
+ setWatchMode("off");
1412
+ setWatchError(false);
1328
1413
  return () => {
1329
1414
  mountedRef.current = false;
1330
1415
  };
1331
1416
  }
1332
1417
  const usePolling = shouldUsePolling(rootPath);
1418
+ setWatchMode(usePolling ? "polling" : "native");
1419
+ setWatchError(false);
1333
1420
  const watcher = chokidar.watch(rootPath, {
1334
1421
  ignored: ignoredPathRef.current,
1335
1422
  persistent: true,
@@ -1372,9 +1459,11 @@ function useWatcher(rootPath, onTreeChange, options = {}) {
1372
1459
  debounceTimerRef.current = setTimeout(flushPendingChange, debounceMs);
1373
1460
  });
1374
1461
  watcher.on("error", () => {
1462
+ setWatchError(true);
1375
1463
  setIsWatching(false);
1376
1464
  });
1377
1465
  watcher.on("ready", () => {
1466
+ setWatchError(false);
1378
1467
  setIsWatching(true);
1379
1468
  });
1380
1469
  return () => {
@@ -1392,7 +1481,7 @@ function useWatcher(rootPath, onTreeChange, options = {}) {
1392
1481
  setIsWatching(false);
1393
1482
  };
1394
1483
  }, [rootPath, options.noWatch, debounceMs]);
1395
- return { isWatching };
1484
+ return { isWatching, watchMode, watchError };
1396
1485
  }
1397
1486
 
1398
1487
  // src/hooks/useChangedFiles.js
@@ -1500,17 +1589,23 @@ function App({ rootPath, options = {}, version }) {
1500
1589
  const theme = getTheme(resolveThemeName({ cliTheme: options.theme }));
1501
1590
  const [selectedPath, setSelectedPath] = useState6(null);
1502
1591
  const [recentEventMessage, setRecentEventMessage] = useState6("");
1592
+ const [recentEventType, setRecentEventType] = useState6("");
1593
+ const [showHelp, setShowHelp] = useState6(options.help !== false);
1503
1594
  useEffect5(() => setupCleanExit(), []);
1504
1595
  const { tree, flatList, refreshFlatList, rebuildTree, ensureChildrenLoaded } = useTree(rootPath, options);
1505
1596
  const { statusMap, pathStatusMap, isGitRepo, refresh: refreshGit } = useGitStatus(
1506
1597
  rootPath,
1507
1598
  !options.noGit
1508
1599
  );
1600
+ const toggleHelp = useCallback5(() => {
1601
+ setShowHelp((prev) => !prev);
1602
+ }, []);
1509
1603
  const { cursor, viewportStart, setCursor } = useNavigation(
1510
1604
  flatList,
1511
1605
  rebuildTree,
1512
1606
  refreshFlatList,
1513
- ensureChildrenLoaded
1607
+ ensureChildrenLoaded,
1608
+ toggleHelp
1514
1609
  );
1515
1610
  const cursorRef = useRef5(cursor);
1516
1611
  cursorRef.current = cursor;
@@ -1576,6 +1671,7 @@ function App({ rootPath, options = {}, version }) {
1576
1671
  const batch = Array.isArray(eventsOrEvent) ? eventsOrEvent : [{ event: eventsOrEvent, path: watchPath }];
1577
1672
  let hasStructuralChange = false;
1578
1673
  let latestEventMessage = "";
1674
+ let latestEventType = "";
1579
1675
  for (const item of batch) {
1580
1676
  if (!item) continue;
1581
1677
  if (item.event === "add" || item.event === "addDir" || item.event === "unlink" || item.event === "unlinkDir") {
@@ -1586,6 +1682,7 @@ function App({ rootPath, options = {}, version }) {
1586
1682
  const eventMessage = formatWatcherEventMessage(item.event, relativePath);
1587
1683
  if (eventMessage) {
1588
1684
  latestEventMessage = eventMessage;
1685
+ latestEventType = item.event;
1589
1686
  }
1590
1687
  if (changeStatus && relativePath) {
1591
1688
  markChanged(relativePath, changeStatus);
@@ -1593,6 +1690,7 @@ function App({ rootPath, options = {}, version }) {
1593
1690
  }
1594
1691
  if (latestEventMessage) {
1595
1692
  setRecentEventMessage(latestEventMessage);
1693
+ setRecentEventType(latestEventType);
1596
1694
  }
1597
1695
  if (hasStructuralChange) {
1598
1696
  rebuildTree(true);
@@ -1605,11 +1703,12 @@ function App({ rootPath, options = {}, version }) {
1605
1703
  }
1606
1704
  }
1607
1705
  }, [rebuildTree, isGitRepo, refreshGit, markChanged, toTreeRelativePath]);
1608
- const { isWatching } = useWatcher(rootPath, handleTreeChange, {
1706
+ const { isWatching, watchMode, watchError } = useWatcher(rootPath, handleTreeChange, {
1609
1707
  noWatch: options.noWatch
1610
1708
  });
1611
- const helpEnabled = options.help !== false;
1612
- const statusBarHeight = 1 + (helpEnabled ? 1 : 0) + (recentEventMessage ? 1 : 0);
1709
+ const selectedNode = cursor >= 0 && cursor < flatList.length ? flatList[cursor] : null;
1710
+ const selectedContextPath = selectedNode?.path || selectedPath || "";
1711
+ const statusBarHeight = 1 + (showHelp ? 1 : 0) + (selectedContextPath ? 1 : 0) + (recentEventMessage ? 1 : 0);
1613
1712
  const viewportHeight = termSize.height - statusBarHeight;
1614
1713
  const termWidth = termSize.width;
1615
1714
  const { fileCount, dirCount } = calculateCounts(flatList);
@@ -1635,11 +1734,18 @@ function App({ rootPath, options = {}, version }) {
1635
1734
  fileCount,
1636
1735
  dirCount,
1637
1736
  isWatching,
1737
+ watchMode,
1738
+ watchError,
1638
1739
  gitEnabled: !options.noGit,
1639
1740
  isGitRepo,
1640
1741
  gitSummary,
1641
- showHelp: helpEnabled,
1742
+ showHelp,
1743
+ selectedPath: selectedContextPath,
1744
+ showSize: Boolean(options.showSize),
1745
+ selectedIsDir: Boolean(selectedNode?.isDir),
1746
+ selectedSize: selectedNode?.size,
1642
1747
  eventMessage: recentEventMessage,
1748
+ eventType: recentEventType,
1643
1749
  termWidth,
1644
1750
  iconSet,
1645
1751
  theme
@@ -1668,6 +1774,7 @@ OPTIONS:
1668
1774
  --no-git Disable git integration
1669
1775
  --no-icons Use ASCII icons (disable emoji/nerd icons)
1670
1776
  --no-watch Disable file watching
1777
+ --show-size Show selected file size in the status bar (lightweight, non-recursive)
1671
1778
  --icon-set <emoji|nerd|ascii> Icon set (default: emoji). You can also set FTREE_ICON_SET.
1672
1779
  --theme <vscode> UI theme (default: vscode). You can also set FTREE_THEME.
1673
1780
 
@@ -1725,6 +1832,7 @@ function parseArgs() {
1725
1832
  noIcons: false,
1726
1833
  noGit: false,
1727
1834
  noWatch: false,
1835
+ showSize: false,
1728
1836
  help: true,
1729
1837
  // show help line by default
1730
1838
  iconSet: void 0,
@@ -1753,6 +1861,10 @@ function parseArgs() {
1753
1861
  result.options.noWatch = true;
1754
1862
  continue;
1755
1863
  }
1864
+ if (arg === "--show-size") {
1865
+ result.options.showSize = true;
1866
+ continue;
1867
+ }
1756
1868
  if (arg === "--no-help") {
1757
1869
  result.options.help = false;
1758
1870
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftreeview",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Terminal file explorer with git awareness and live updates",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/App.jsx CHANGED
@@ -77,6 +77,8 @@ export default function App({ rootPath, options = {}, version }) {
77
77
  // Track selected path to preserve cursor position across rebuilds
78
78
  const [selectedPath, setSelectedPath] = useState(null);
79
79
  const [recentEventMessage, setRecentEventMessage] = useState('');
80
+ const [recentEventType, setRecentEventType] = useState('');
81
+ const [showHelp, setShowHelp] = useState(options.help !== false);
80
82
 
81
83
  // Setup clean exit handler - ensures proper terminal state on exit
82
84
  useEffect(() => setupCleanExit(), []);
@@ -90,12 +92,17 @@ export default function App({ rootPath, options = {}, version }) {
90
92
  !options.noGit
91
93
  );
92
94
 
95
+ const toggleHelp = useCallback(() => {
96
+ setShowHelp((prev) => !prev);
97
+ }, []);
98
+
93
99
  // Get navigation state (cursor, viewport)
94
100
  const { cursor, viewportStart, setCursor } = useNavigation(
95
101
  flatList,
96
102
  rebuildTree,
97
103
  refreshFlatList,
98
- ensureChildrenLoaded
104
+ ensureChildrenLoaded,
105
+ toggleHelp
99
106
  );
100
107
  const cursorRef = useRef(cursor);
101
108
  cursorRef.current = cursor;
@@ -193,6 +200,7 @@ export default function App({ rootPath, options = {}, version }) {
193
200
 
194
201
  let hasStructuralChange = false;
195
202
  let latestEventMessage = '';
203
+ let latestEventType = '';
196
204
 
197
205
  for (const item of batch) {
198
206
  if (!item) continue;
@@ -204,6 +212,7 @@ export default function App({ rootPath, options = {}, version }) {
204
212
  const eventMessage = formatWatcherEventMessage(item.event, relativePath);
205
213
  if (eventMessage) {
206
214
  latestEventMessage = eventMessage;
215
+ latestEventType = item.event;
207
216
  }
208
217
 
209
218
  if (changeStatus && relativePath) {
@@ -213,6 +222,7 @@ export default function App({ rootPath, options = {}, version }) {
213
222
 
214
223
  if (latestEventMessage) {
215
224
  setRecentEventMessage(latestEventMessage);
225
+ setRecentEventType(latestEventType);
216
226
  }
217
227
 
218
228
  if (hasStructuralChange) {
@@ -229,14 +239,15 @@ export default function App({ rootPath, options = {}, version }) {
229
239
  }
230
240
  }, [rebuildTree, isGitRepo, refreshGit, markChanged, toTreeRelativePath]);
231
241
 
232
- const { isWatching } = useWatcher(rootPath, handleTreeChange, {
242
+ const { isWatching, watchMode, watchError } = useWatcher(rootPath, handleTreeChange, {
233
243
  noWatch: options.noWatch,
234
244
  });
235
245
 
236
246
  // Calculate viewport dimensions
237
- // Reserve space: 1 main status line + optional hints line + optional event line.
238
- const helpEnabled = options.help !== false; // Show help by default
239
- const statusBarHeight = 1 + (helpEnabled ? 1 : 0) + (recentEventMessage ? 1 : 0);
247
+ // Reserve space: 1 main status line + optional hints line + optional selection line + optional event line.
248
+ const selectedNode = (cursor >= 0 && cursor < flatList.length) ? flatList[cursor] : null;
249
+ const selectedContextPath = selectedNode?.path || selectedPath || '';
250
+ const statusBarHeight = 1 + (showHelp ? 1 : 0) + (selectedContextPath ? 1 : 0) + (recentEventMessage ? 1 : 0);
240
251
  const viewportHeight = termSize.height - statusBarHeight;
241
252
  const termWidth = termSize.width;
242
253
 
@@ -268,11 +279,18 @@ export default function App({ rootPath, options = {}, version }) {
268
279
  fileCount={fileCount}
269
280
  dirCount={dirCount}
270
281
  isWatching={isWatching}
282
+ watchMode={watchMode}
283
+ watchError={watchError}
271
284
  gitEnabled={!options.noGit}
272
285
  isGitRepo={isGitRepo}
273
286
  gitSummary={gitSummary}
274
- showHelp={helpEnabled}
287
+ showHelp={showHelp}
288
+ selectedPath={selectedContextPath}
289
+ showSize={Boolean(options.showSize)}
290
+ selectedIsDir={Boolean(selectedNode?.isDir)}
291
+ selectedSize={selectedNode?.size}
275
292
  eventMessage={recentEventMessage}
293
+ eventType={recentEventType}
276
294
  termWidth={termWidth}
277
295
  iconSet={iconSet}
278
296
  theme={theme}
package/src/cli.js CHANGED
@@ -39,6 +39,7 @@ OPTIONS:
39
39
  --no-git Disable git integration
40
40
  --no-icons Use ASCII icons (disable emoji/nerd icons)
41
41
  --no-watch Disable file watching
42
+ --show-size Show selected file size in the status bar (lightweight, non-recursive)
42
43
  --icon-set <emoji|nerd|ascii> Icon set (default: emoji). You can also set FTREE_ICON_SET.
43
44
  --theme <vscode> UI theme (default: vscode). You can also set FTREE_THEME.
44
45
 
@@ -118,6 +119,7 @@ function parseArgs() {
118
119
  noIcons: false,
119
120
  noGit: false,
120
121
  noWatch: false,
122
+ showSize: false,
121
123
  help: true, // show help line by default
122
124
  iconSet: undefined,
123
125
  theme: undefined,
@@ -148,6 +150,10 @@ function parseArgs() {
148
150
  result.options.noWatch = true;
149
151
  continue;
150
152
  }
153
+ if (arg === '--show-size') {
154
+ result.options.showSize = true;
155
+ continue;
156
+ }
151
157
  if (arg === '--no-help') {
152
158
  result.options.help = false;
153
159
  continue;
@@ -77,6 +77,53 @@ function formatPath(path, maxWidth) {
77
77
  return truncatePath(ellipsisPath, maxWidth);
78
78
  }
79
79
 
80
+ function getEventMessageColor(eventType) {
81
+ if (eventType === 'add' || eventType === 'addDir') {
82
+ return 'green';
83
+ }
84
+ if (eventType === 'change') {
85
+ return 'yellow';
86
+ }
87
+ if (eventType === 'unlink' || eventType === 'unlinkDir') {
88
+ return 'magenta';
89
+ }
90
+ return 'magenta';
91
+ }
92
+
93
+ function formatBytes(bytes) {
94
+ if (!Number.isFinite(bytes) || bytes < 0) {
95
+ return '';
96
+ }
97
+ if (bytes < 1024) {
98
+ return `${bytes}B`;
99
+ }
100
+ const units = ['KB', 'MB', 'GB', 'TB'];
101
+ let value = bytes / 1024;
102
+ let unitIndex = 0;
103
+ while (value >= 1024 && unitIndex < units.length - 1) {
104
+ value /= 1024;
105
+ unitIndex++;
106
+ }
107
+ const decimals = value >= 10 ? 0 : 1;
108
+ return `${value.toFixed(decimals)}${units[unitIndex]}`;
109
+ }
110
+
111
+ function getWatchDisplay({ isWatching, watchMode, watchError, watchIcon }) {
112
+ if (watchMode === 'off') {
113
+ return { text: `${watchIcon} Static`, tone: 'muted' };
114
+ }
115
+ if (watchError) {
116
+ return { text: `${watchIcon} Paused`, tone: 'warning' };
117
+ }
118
+ if (!isWatching) {
119
+ return { text: `${watchIcon} Starting`, tone: 'muted' };
120
+ }
121
+ if (watchMode === 'polling') {
122
+ return { text: `${watchIcon} Watching (poll)`, tone: 'warning' };
123
+ }
124
+ return { text: `${watchIcon} Watching`, tone: 'success' };
125
+ }
126
+
80
127
  /**
81
128
  * Calculate git status summary from statusMap
82
129
  * Returns a summary string like "clean" or "3M 1A"
@@ -116,11 +163,18 @@ export function calculateGitStatus(statusMap) {
116
163
  * @property {number} fileCount - Total number of files
117
164
  * @property {number} dirCount - Total number of directories
118
165
  * @property {boolean} isWatching - File watcher active status
166
+ * @property {'native'|'polling'|'off'} watchMode - File watcher mode
167
+ * @property {boolean} watchError - Watcher error state
119
168
  * @property {boolean} gitEnabled - Git integration enabled
120
169
  * @property {boolean} isGitRepo - Whether current directory is a git repo
121
170
  * @property {string} gitSummary - Git status summary (e.g., "clean" or "3M 1A")
122
171
  * @property {boolean} showHelp - Show keybind hints
172
+ * @property {string} selectedPath - Currently selected tree path
173
+ * @property {boolean} showSize - Show selected file size in status bar
174
+ * @property {boolean} selectedIsDir - Whether selected node is a directory
175
+ * @property {number|undefined} selectedSize - Selected node size in bytes
123
176
  * @property {string} eventMessage - Latest filesystem event message
177
+ * @property {string} eventType - Latest filesystem event type
124
178
  * @property {number} termWidth - Terminal width in columns
125
179
  */
126
180
  export function StatusBar({
@@ -129,11 +183,18 @@ export function StatusBar({
129
183
  fileCount = 0,
130
184
  dirCount = 0,
131
185
  isWatching = false,
186
+ watchMode = 'native',
187
+ watchError = false,
132
188
  gitEnabled = false,
133
189
  isGitRepo = false,
134
190
  gitSummary = '',
135
191
  showHelp = true,
192
+ selectedPath = '',
193
+ showSize = false,
194
+ selectedIsDir = false,
195
+ selectedSize = undefined,
136
196
  eventMessage = '',
197
+ eventType = '',
137
198
  termWidth = 80,
138
199
  iconSet = 'emoji',
139
200
  theme = VSCODE_THEME,
@@ -171,10 +232,26 @@ export function StatusBar({
171
232
 
172
233
  const versionText = `ftreeview ${version}`;
173
234
  const countsText = `${barIcons.file} ${fileCount} ${barIcons.folder} ${dirCount}`;
174
- const watchText = `${barIcons.watch} ${isWatching ? 'Watching' : 'Static'}`;
235
+ const watchDisplay = getWatchDisplay({
236
+ isWatching,
237
+ watchMode,
238
+ watchError,
239
+ watchIcon: barIcons.watch,
240
+ });
241
+ const watchText = watchDisplay.text;
242
+ const watchColor = watchDisplay.tone === 'success'
243
+ ? finalTheme.colors.success
244
+ : watchDisplay.tone === 'warning'
245
+ ? finalTheme.colors.warning
246
+ : finalTheme.colors.muted;
175
247
  const gitText = gitEnabled && isGitRepo ? `${barIcons.git} ${gitSummary || 'clean'}` : '';
248
+ const sizeValueText = !showSize ? '' : (selectedIsDir ? '--' : formatBytes(selectedSize));
249
+ const showSizeText = Boolean(showSize && sizeValueText);
250
+ const sizeText = showSizeText ? `size ${sizeValueText}` : '';
176
251
 
177
- const fixedSegments = [versionText, countsText, watchText].concat(gitText ? [gitText] : []);
252
+ const fixedSegments = [versionText, countsText, watchText]
253
+ .concat(showSizeText ? [sizeText] : [])
254
+ .concat(gitText ? [gitText] : []);
178
255
  const fixedWidth = fixedSegments.reduce((sum, part) => sum + part.length, 0)
179
256
  + (fixedSegments.length /* separators count incl. path */) * sep.length
180
257
  + paddingWidth;
@@ -216,10 +293,21 @@ export function StatusBar({
216
293
  <Text {...finalTheme.statusBar.separatorStyle}>
217
294
  {sep}
218
295
  </Text>
219
- <Text color={isWatching ? finalTheme.colors.success : finalTheme.colors.muted}>
296
+ <Text color={watchColor}>
220
297
  {watchText}
221
298
  </Text>
222
299
 
300
+ {showSizeText && (
301
+ <>
302
+ <Text {...finalTheme.statusBar.separatorStyle}>
303
+ {sep}
304
+ </Text>
305
+ <Text color={finalTheme.colors.muted}>
306
+ {sizeText}
307
+ </Text>
308
+ </>
309
+ )}
310
+
223
311
  {gitEnabled && isGitRepo && (
224
312
  <>
225
313
  <Text {...finalTheme.statusBar.separatorStyle}>
@@ -238,7 +326,21 @@ export function StatusBar({
238
326
  paddingX={1}
239
327
  >
240
328
  <Text {...finalTheme.statusBar.hintStyle}>
241
- q quit ↑↓/jk move ←→/hl expand space toggle r refresh
329
+ q quit ↑↓/jk move ←→/hl expand space toggle r refresh ? help
330
+ </Text>
331
+ </Box>
332
+ )}
333
+
334
+ {selectedPath && (
335
+ <Box
336
+ width={termWidth}
337
+ paddingX={1}
338
+ >
339
+ <Text color={finalTheme.colors.muted}>
340
+ selected{' '}
341
+ </Text>
342
+ <Text color={finalTheme.colors.fg}>
343
+ {formatPath(selectedPath, Math.max(8, termWidth - 11))}
242
344
  </Text>
243
345
  </Box>
244
346
  )}
@@ -248,7 +350,7 @@ export function StatusBar({
248
350
  width={termWidth}
249
351
  paddingX={1}
250
352
  >
251
- <Text color="magenta" bold>
353
+ <Text color={getEventMessageColor(eventType)} bold>
252
354
  {eventMessage}
253
355
  </Text>
254
356
  </Box>
@@ -20,9 +20,16 @@ const VIEWPORT_MARGIN = 3; // Keep cursor 3 lines from viewport edges
20
20
  * @param {Function} rebuildTree - Callback to rebuild tree with modified nodes
21
21
  * @param {Function|null} refreshFlatList - Callback to refresh visible list without full rebuild
22
22
  * @param {Function|null} ensureChildrenLoaded - Callback to lazy-load directory children when expanding
23
+ * @param {Function|null} onToggleHelp - Callback to toggle help hints
23
24
  * @returns {{cursor: number, viewportStart: number, exit: Function, setCursorByPath: Function, setCursor: Function}} Navigation state
24
25
  */
25
- export function useNavigation(flatList = [], rebuildTree, refreshFlatList = null, ensureChildrenLoaded = null) {
26
+ export function useNavigation(
27
+ flatList = [],
28
+ rebuildTree,
29
+ refreshFlatList = null,
30
+ ensureChildrenLoaded = null,
31
+ onToggleHelp = null
32
+ ) {
26
33
  const { exit } = useApp();
27
34
 
28
35
  // Calculate viewport height (terminal height minus header and status bar)
@@ -202,6 +209,8 @@ export function useNavigation(flatList = [], rebuildTree, refreshFlatList = null
202
209
  jumpToBottom();
203
210
  } else if (input === 'r') {
204
211
  refresh();
212
+ } else if (input === '?' && typeof onToggleHelp === 'function') {
213
+ onToggleHelp();
205
214
  }
206
215
  });
207
216
 
@@ -31,6 +31,8 @@ function shouldUsePolling(rootPath) {
31
31
  export function useWatcher(rootPath, onTreeChange, options = {}) {
32
32
  const debounceMs = options.debounceMs ?? DEFAULT_CONFIG.debounceDelay;
33
33
  const [isWatching, setIsWatching] = useState(false);
34
+ const [watchMode, setWatchMode] = useState(options.noWatch ? 'off' : 'native');
35
+ const [watchError, setWatchError] = useState(false);
34
36
  const watcherRef = useRef(null);
35
37
  const onTreeChangeRef = useRef(onTreeChange);
36
38
  const mountedRef = useRef(true);
@@ -50,12 +52,16 @@ export function useWatcher(rootPath, onTreeChange, options = {}) {
50
52
 
51
53
  if (options.noWatch || !rootPath) {
52
54
  setIsWatching(false);
55
+ setWatchMode('off');
56
+ setWatchError(false);
53
57
  return () => {
54
58
  mountedRef.current = false;
55
59
  };
56
60
  }
57
61
 
58
62
  const usePolling = shouldUsePolling(rootPath);
63
+ setWatchMode(usePolling ? 'polling' : 'native');
64
+ setWatchError(false);
59
65
  const watcher = chokidar.watch(rootPath, {
60
66
  ignored: ignoredPathRef.current,
61
67
  persistent: true,
@@ -107,10 +113,12 @@ export function useWatcher(rootPath, onTreeChange, options = {}) {
107
113
  });
108
114
 
109
115
  watcher.on('error', () => {
116
+ setWatchError(true);
110
117
  setIsWatching(false);
111
118
  });
112
119
 
113
120
  watcher.on('ready', () => {
121
+ setWatchError(false);
114
122
  setIsWatching(true);
115
123
  });
116
124
 
@@ -132,7 +140,7 @@ export function useWatcher(rootPath, onTreeChange, options = {}) {
132
140
  };
133
141
  }, [rootPath, options.noWatch, debounceMs]);
134
142
 
135
- return { isWatching };
143
+ return { isWatching, watchMode, watchError };
136
144
  }
137
145
 
138
146
  // Only default export to avoid conflicts with bundling