ftreeview 0.1.1

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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared helpers for added/modified visual status indicators.
3
+ * Status values are intentionally limited to:
4
+ * - '' (no indicator)
5
+ * - 'A' (added)
6
+ * - 'M' (modified)
7
+ */
8
+
9
+ export const CHANGE_STATUS = {
10
+ ADDED: 'A',
11
+ MODIFIED: 'M',
12
+ };
13
+
14
+ /**
15
+ * Normalize any input into a supported change status.
16
+ *
17
+ * @param {string} status - Raw status value
18
+ * @returns {''|'A'|'M'} Normalized status
19
+ */
20
+ export function normalizeChangeStatus(status = '') {
21
+ if (status === CHANGE_STATUS.MODIFIED) {
22
+ return CHANGE_STATUS.MODIFIED;
23
+ }
24
+ if (status === CHANGE_STATUS.ADDED) {
25
+ return CHANGE_STATUS.ADDED;
26
+ }
27
+ return '';
28
+ }
29
+
30
+ /**
31
+ * Merge two statuses with priority: M > A > clean.
32
+ *
33
+ * @param {string} first - First status
34
+ * @param {string} second - Second status
35
+ * @returns {''|'A'|'M'} Merged status
36
+ */
37
+ export function mergeChangeStatus(first = '', second = '') {
38
+ const left = normalizeChangeStatus(first);
39
+ const right = normalizeChangeStatus(second);
40
+
41
+ if (left === CHANGE_STATUS.MODIFIED || right === CHANGE_STATUS.MODIFIED) {
42
+ return CHANGE_STATUS.MODIFIED;
43
+ }
44
+ if (left === CHANGE_STATUS.ADDED || right === CHANGE_STATUS.ADDED) {
45
+ return CHANGE_STATUS.ADDED;
46
+ }
47
+ return '';
48
+ }
49
+
50
+ /**
51
+ * Convert git status into a change indicator status.
52
+ * Only A/M are considered valid for this indicator.
53
+ *
54
+ * @param {string} gitStatus - Single-character git status
55
+ * @returns {''|'A'|'M'} Indicator status
56
+ */
57
+ export function mapGitStatusToChangeStatus(gitStatus = '') {
58
+ // Treat "untracked" as "added" for the A/M indicator so new files show up.
59
+ if (gitStatus === 'U') {
60
+ return CHANGE_STATUS.ADDED;
61
+ }
62
+ return normalizeChangeStatus(gitStatus);
63
+ }
64
+
65
+ /**
66
+ * Convert chokidar watcher event into a change indicator status.
67
+ *
68
+ * @param {string} event - Chokidar event name
69
+ * @returns {''|'A'|'M'} Indicator status
70
+ */
71
+ export function mapWatcherEventToChangeStatus(event = '') {
72
+ if (event === 'add' || event === 'addDir') {
73
+ return CHANGE_STATUS.ADDED;
74
+ }
75
+ if (event === 'change') {
76
+ return CHANGE_STATUS.MODIFIED;
77
+ }
78
+ return '';
79
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Tree Connector Logic for ftree
3
+ *
4
+ * Generates the prefix strings for tree visualization using box-drawing characters.
5
+ * Handles nested directory structures with proper continuation lines.
6
+ */
7
+
8
+ /**
9
+ * Box-drawing characters for tree connectors
10
+ */
11
+ const CONNECTORS = {
12
+ /** Middle child connector: "├── " */
13
+ MIDDLE: '├── ',
14
+
15
+ /** Last child connector: "└── " */
16
+ LAST: '└── ',
17
+
18
+ /** Vertical continuation for parents: "│ " */
19
+ VERTICAL: '│ ',
20
+
21
+ /** Empty space (no continuation): " " */
22
+ EMPTY: ' ',
23
+ };
24
+
25
+ /**
26
+ * Computes the prefix string for a tree node based on its position in the tree.
27
+ *
28
+ * The prefix consists of 4-character segments for each level of depth:
29
+ * - For levels above the current: vertical continuation if the ancestor is not a last child
30
+ * - For the current level: middle or last connector (or nothing for root)
31
+ *
32
+ * @param {object} node - Current FileNode with depth property
33
+ * @param {boolean} isLast - True if this node is the last child of its parent
34
+ * @param {boolean[]} parentPrefixes - Array indicating if each ancestor is a last child
35
+ * @returns {string} The complete prefix string for this node
36
+ *
37
+ * @example
38
+ * // Root node (depth 0)
39
+ * getPrefix({ depth: 0 }, false, [])
40
+ * // => ""
41
+ *
42
+ * // Depth 1, middle child
43
+ * getPrefix({ depth: 1 }, false, [])
44
+ * // => "├── "
45
+ *
46
+ * // Depth 1, last child
47
+ * getPrefix({ depth: 1 }, true, [])
48
+ * // => "└── "
49
+ *
50
+ * // Depth 2, under a middle parent
51
+ * getPrefix({ depth: 2 }, false, [false])
52
+ * // => "│ ├── "
53
+ *
54
+ * // Depth 2, under a last parent
55
+ * getPrefix({ depth: 2 }, true, [true])
56
+ * // => " └── "
57
+ *
58
+ * // Depth 3, complex case
59
+ * getPrefix({ depth: 3 }, false, [false, true])
60
+ * // => "│ ├── "
61
+ */
62
+ export function getPrefix(node, isLast, parentPrefixes) {
63
+ if (!node || node.depth === 0) {
64
+ return '';
65
+ }
66
+
67
+ const segments = [];
68
+
69
+ // Build prefix segments for each ancestor level
70
+ for (let level = 0; level < node.depth; level++) {
71
+ if (level === node.depth - 1) {
72
+ // Current level: use middle or last connector
73
+ segments.push(isLast ? CONNECTORS.LAST : CONNECTORS.MIDDLE);
74
+ } else {
75
+ // Parent level: vertical continuation if not last child, empty otherwise
76
+ const ancestorIsLast = parentPrefixes[level] || false;
77
+ segments.push(ancestorIsLast ? CONNECTORS.EMPTY : CONNECTORS.VERTICAL);
78
+ }
79
+ }
80
+
81
+ return segments.join('');
82
+ }
83
+
84
+ /**
85
+ * Computes which ancestors are "last children" for a given node in the flattened list.
86
+ *
87
+ * This function walks up the tree structure by examining parent relationships
88
+ * in the original hierarchical tree to determine the prefix continuation pattern.
89
+ *
90
+ * @param {object[]} flatList - Flattened list of FileNodes from flattenTree()
91
+ * @param {number} index - Index of the current node in the flattened list
92
+ * @returns {boolean[]} Array where each index indicates if the ancestor at that level is a last child
93
+ *
94
+ * @example
95
+ * // Given a tree structure:
96
+ * // root/
97
+ * // ├── src/
98
+ * // │ ├── utils.js
99
+ * // │ └── helper.js <-- index points here
100
+ * // └── tests/
101
+ *
102
+ * // For helper.js:
103
+ * // parent (src) is not last (tests comes after)
104
+ * // root is... (need to check if src is root's last child)
105
+ * computeAncestorPrefixes(flatList, indexOfHelperJs)
106
+ * // => [false] indicates src is not a last child
107
+ *
108
+ * @example
109
+ * // For a deeply nested node:
110
+ * // root/
111
+ * // └── src/
112
+ * // └── lib/
113
+ * // └── core.js <-- index points here
114
+ * // Returns: [true, true] - both src and lib are last children
115
+ */
116
+ export function computeAncestorPrefixes(flatList, index) {
117
+ if (!flatList || index < 0 || index >= flatList.length) {
118
+ return [];
119
+ }
120
+
121
+ const currentNode = flatList[index];
122
+ const ancestorIsLast = [];
123
+
124
+ // For each ancestor level (from direct parent up to root-adjacent),
125
+ // determine if that ancestor is a last child of its parent.
126
+ // We iterate (currentNode.depth - 1) times because:
127
+ // - depth 0 node: no ancestors
128
+ // - depth 1 node: 1 ancestor at index 0 (the root)
129
+ // - depth 2 node: 2 ancestors (root, parent)
130
+ let targetNode = currentNode;
131
+ let targetIndex = index;
132
+
133
+ for (let level = 0; level < currentNode.depth; level++) {
134
+ const parentInfo = findParentInfo(flatList, targetIndex, targetNode.depth);
135
+
136
+ if (!parentInfo) {
137
+ // Reached root's parent or invalid tree structure
138
+ break;
139
+ }
140
+
141
+ // Stop if we've reached root (depth 0) - we only care about ancestors of root
142
+ if (parentInfo.node.depth === 0) {
143
+ break;
144
+ }
145
+
146
+ // Check if the parent node is a last child
147
+ const isParentLast = isLastChild(flatList, parentInfo.index);
148
+
149
+ ancestorIsLast.push(isParentLast);
150
+
151
+ // Move up to the parent for next iteration
152
+ targetNode = parentInfo.node;
153
+ targetIndex = parentInfo.index;
154
+ }
155
+
156
+ // The array is built from direct parent to root-adjacent,
157
+ // so reverse it to get root-adjacent to direct parent order.
158
+ // Example: for node at depth 3 under [root(not last), parent(last)]
159
+ // we want [false, true] not [true, false]
160
+ return ancestorIsLast.reverse();
161
+ }
162
+
163
+ /**
164
+ * Finds the parent node and its index in the flattened list.
165
+ *
166
+ * @param {object[]} flatList - Flattened list of FileNodes
167
+ * @param {number} childIndex - Index of the child node
168
+ * @param {number} childDepth - Depth of the child node
169
+ * @returns {{node: object, index: number}|null} Parent info or null if not found
170
+ */
171
+ function findParentInfo(flatList, childIndex, childDepth) {
172
+ if (childDepth === 0) {
173
+ return null;
174
+ }
175
+
176
+ // Parent has depth = childDepth - 1
177
+ // Search backwards from childIndex to find the parent
178
+ for (let i = childIndex - 1; i >= 0; i--) {
179
+ const node = flatList[i];
180
+ if (node.depth === childDepth - 1) {
181
+ return { node, index: i };
182
+ }
183
+ // If we've gone to a shallower depth, we've passed the parent
184
+ if (node.depth < childDepth - 1) {
185
+ break;
186
+ }
187
+ }
188
+
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * Determines if a node at the given index is a last child of its parent.
194
+ *
195
+ * A node is a last child if there are no siblings following it in the
196
+ * flattened list before the depth decreases (indicating we've moved to
197
+ * a different parent).
198
+ *
199
+ * @param {object[]} flatList - Flattened list of FileNodes
200
+ * @param {number} index - Index of the node to check
201
+ * @returns {boolean} True if this node is the last child of its parent
202
+ */
203
+ function isLastChild(flatList, index) {
204
+ if (index >= flatList.length - 1) {
205
+ return true;
206
+ }
207
+
208
+ const currentNode = flatList[index];
209
+ const nextNode = flatList[index + 1];
210
+
211
+ // If next node is shallower or at same depth (not a child), this is a last child
212
+ // If next node is deeper (a child), this is NOT a last child
213
+ return nextNode.depth <= currentNode.depth;
214
+ }
215
+
216
+ /**
217
+ * Computes ancestor prefixes for all nodes in a flattened list.
218
+ * Useful for batch processing in TreeView component.
219
+ *
220
+ * @param {object[]} flatList - Flattened list of FileNodes from flattenTree()
221
+ * @returns {Map<number, boolean[]>} Map from node index to ancestor prefix array
222
+ */
223
+ export function computeAllPrefixes(flatList) {
224
+ const prefixesMap = new Map();
225
+
226
+ for (let i = 0; i < flatList.length; i++) {
227
+ prefixesMap.set(i, computeAncestorPrefixes(flatList, i));
228
+ }
229
+
230
+ return prefixesMap;
231
+ }
232
+
233
+ export { CONNECTORS };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Default configuration values for ftree
3
+ */
4
+
5
+ export const DEFAULT_CONFIG = {
6
+ /** Maximum depth to traverse the directory tree */
7
+ maxDepth: 10,
8
+
9
+ /** Show hidden files (starting with .) */
10
+ showHidden: false,
11
+
12
+ /** Show ignored files (--no-ignore flag) */
13
+ showIgnored: false,
14
+
15
+ /** Files/directories to ignore (gitignore-style patterns) */
16
+ ignorePatterns: [
17
+ 'node_modules/**',
18
+ '.git/**',
19
+ 'dist/**',
20
+ 'build/**',
21
+ '*.log',
22
+ ],
23
+
24
+ /** Watch for file system changes and update tree */
25
+ watch: true,
26
+
27
+ /** Show git status (modified, added, deleted, untracked files) */
28
+ showGitStatus: true,
29
+
30
+ /** Number of concurrent file system operations */
31
+ concurrency: 50,
32
+
33
+ /** Update interval for git status (ms) */
34
+ gitStatusInterval: 2000,
35
+
36
+ /** Debounce delay for file system events (ms) */
37
+ debounceDelay: 100,
38
+ };
39
+
40
+ export const GIT_STATUS = {
41
+ /** Modified but not staged */
42
+ MODIFIED: 'M',
43
+
44
+ /** Staged for commit */
45
+ STAGED: 'A',
46
+
47
+ /** Deleted but not staged */
48
+ DELETED: 'D',
49
+
50
+ /** Untracked files */
51
+ UNTRACKED: '?',
52
+
53
+ /** Renamed */
54
+ RENAMED: 'R',
55
+
56
+ /** Conflicted */
57
+ CONFLICTED: 'C',
58
+ };
59
+
60
+ export const FILE_TYPE = {
61
+ DIRECTORY: 'directory',
62
+ FILE: 'file',
63
+ SYMLINK: 'symlink',
64
+ };