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.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/cli.js +1619 -0
- package/package.json +58 -0
- package/src/App.jsx +243 -0
- package/src/cli.js +228 -0
- package/src/components/StatusBar.jsx +270 -0
- package/src/components/TreeLine.jsx +190 -0
- package/src/components/TreeView.jsx +129 -0
- package/src/hooks/useChangedFiles.js +82 -0
- package/src/hooks/useGitStatus.js +347 -0
- package/src/hooks/useIgnore.js +182 -0
- package/src/hooks/useNavigation.js +247 -0
- package/src/hooks/useTree.js +508 -0
- package/src/hooks/useWatcher.js +129 -0
- package/src/index.js +22 -0
- package/src/lib/changeStatus.js +79 -0
- package/src/lib/connectors.js +233 -0
- package/src/lib/constants.js +64 -0
- package/src/lib/icons.js +658 -0
- package/src/lib/theme.js +102 -0
|
@@ -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
|
+
};
|