oxlint-tui 1.0.7 → 1.0.9
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 +5 -1
- package/package.json +4 -1
- package/tui.js +337 -186
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# oxlint-tui
|
|
2
2
|
|
|
3
|
-
A lightweight, dependency-free Node.js Terminal User Interface (TUI) for browsing, toggling and visualizing [oxlint](https://github.com/oxc-project/oxc) rules.
|
|
3
|
+
A lightweight, dependency-free Node.js Terminal User Interface (TUI) for browsing, toggling and visualizing [oxlint](https://github.com/oxc-project/oxc) rules and the number of warnings/errors they produce when toggled ON.
|
|
4
4
|
|
|
5
5
|
It automatically loads your local configuration to show you the status of the rules toggled in your project and allows you to toggle them by selecting a rule in the Rules pane and pressing <kbd>1</kbd>, <kbd>2</kbd>, or <kbd>3</kbd>. The config file is modified in real-time.
|
|
6
6
|
|
|
7
|
+
Pressing <kbd>r</kbd> will lint the project using "oxlint". Pressing <kbd>t</kbd> will lint include the oxlint-tsgolint package and lint the project using "oxlint --type-aware". The results are presented directly in the interface.
|
|
8
|
+
|
|
7
9
|
**NOTE**: At the moment, comments will be erased from your configuration file when adding or toggling rules.
|
|
8
10
|
|
|
9
11
|

|
|
@@ -58,6 +60,8 @@ oxlint-tui
|
|
|
58
60
|
| **1** | Set selected rule to "off" |
|
|
59
61
|
| **2** | Set selected rule to "warn" |
|
|
60
62
|
| **3** | Set selected rule to "error" |
|
|
63
|
+
| **r** | Run the linter with "oxlint" |
|
|
64
|
+
| **t** | Run the linter with "-p oxlint-tsgolint@latest oxlint --type-aware" |
|
|
61
65
|
| **Enter** | Open Rule Documentation in Browser |
|
|
62
66
|
| **q** / **Esc** | Quit |
|
|
63
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-tui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "A Node TUI Oxlint rules and configuration browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"tui",
|
|
20
20
|
"cli"
|
|
21
21
|
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"oxlint-tsgolint": "0.11.1"
|
|
24
|
+
},
|
|
22
25
|
"author": "holoflash",
|
|
23
26
|
"license": "MIT",
|
|
24
27
|
"engines": {
|
package/tui.js
CHANGED
|
@@ -1,150 +1,207 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync, exec } from
|
|
4
|
-
import { stdout, stdin, exit, platform, argv } from
|
|
5
|
-
import readline from
|
|
6
|
-
import fs from
|
|
3
|
+
import { execSync, exec } from "node:child_process";
|
|
4
|
+
import { stdout, stdin, exit, platform, argv } from "node:process";
|
|
5
|
+
import readline from "readline";
|
|
6
|
+
import fs from "node:fs";
|
|
7
7
|
|
|
8
|
-
const OXLINT_VERSION = "1.41.0"
|
|
8
|
+
const OXLINT_VERSION = "1.41.0";
|
|
9
9
|
|
|
10
10
|
const COLORS = {
|
|
11
|
-
reset:
|
|
12
|
-
dim:
|
|
13
|
-
highlight:
|
|
14
|
-
selectedBg:
|
|
15
|
-
borderActive:
|
|
16
|
-
borderInactive:
|
|
17
|
-
error:
|
|
18
|
-
warn:
|
|
19
|
-
success:
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
dim: "\x1b[90m",
|
|
13
|
+
highlight: "\x1b[38;5;110m",
|
|
14
|
+
selectedBg: "\x1b[47m\x1b[30m",
|
|
15
|
+
borderActive: "\x1b[36m",
|
|
16
|
+
borderInactive: "\x1b[90m",
|
|
17
|
+
error: "\x1b[31m",
|
|
18
|
+
warn: "\x1b[33m",
|
|
19
|
+
success: "\x1b[32m",
|
|
20
|
+
info: "\x1b[34m",
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
const KEY_MAP = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
k: { type: "MOVE_UP" },
|
|
25
|
+
up: { type: "MOVE_UP" },
|
|
26
|
+
down: { type: "MOVE_DOWN" },
|
|
27
|
+
j: { type: "MOVE_DOWN" },
|
|
28
|
+
left: { type: "MOVE_LEFT" },
|
|
29
|
+
h: { type: "MOVE_LEFT" },
|
|
30
|
+
right: { type: "MOVE_RIGHT" },
|
|
31
|
+
l: { type: "MOVE_RIGHT" },
|
|
32
|
+
return: { type: "OPEN_DOCS" },
|
|
33
|
+
enter: { type: "OPEN_DOCS" },
|
|
34
|
+
1: { type: "SET_STATUS", value: "off" },
|
|
35
|
+
2: { type: "SET_STATUS", value: "warn" },
|
|
36
|
+
3: { type: "SET_STATUS", value: "error" },
|
|
37
|
+
q: { type: "EXIT" },
|
|
38
|
+
r: { type: "RUN_LINT" },
|
|
39
|
+
t: { type: "RUN_TYPE_AWARE_LINT" },
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
let state = {
|
|
43
|
+
activePane: 0,
|
|
44
|
+
selectedCatIdx: 0,
|
|
45
|
+
selectedRuleIdx: 0,
|
|
46
|
+
scrollCat: 0,
|
|
47
|
+
scrollRule: 0,
|
|
48
|
+
isLinting: false,
|
|
49
|
+
message: "oxlint-tui",
|
|
50
|
+
messageType: "dim",
|
|
51
|
+
...loadRules(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// TODO: Nukes comments in the json file. Find the most minimal way to avoid this, while preserving formatting
|
|
41
55
|
function updateConfig(rule, newStatus) {
|
|
42
56
|
if (!state.configPath) return;
|
|
43
|
-
|
|
44
57
|
try {
|
|
45
58
|
if (!state.config.rules) state.config.rules = {};
|
|
46
59
|
const ruleName = rule.value;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
key
|
|
53
|
-
|
|
60
|
+
const canonicalKey =
|
|
61
|
+
rule.scope === "oxc2" || rule.scope === "eslint"
|
|
62
|
+
? ruleName
|
|
63
|
+
: `${rule.scope}/${ruleName}`;
|
|
64
|
+
const existingKey = Object.keys(state.config.rules).find(
|
|
65
|
+
(key) =>
|
|
66
|
+
key === canonicalKey ||
|
|
67
|
+
key === ruleName ||
|
|
68
|
+
key.endsWith(`/${ruleName}`),
|
|
54
69
|
);
|
|
55
|
-
|
|
56
70
|
const targetKey = existingKey || canonicalKey;
|
|
57
71
|
state.config.rules[targetKey] = newStatus;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
state.configPath,
|
|
74
|
+
JSON.stringify(state.config, null, 2),
|
|
75
|
+
"utf8",
|
|
76
|
+
);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
state.message = "Failed to write config file";
|
|
79
|
+
state.messageType = "error";
|
|
62
80
|
}
|
|
63
81
|
}
|
|
64
82
|
|
|
83
|
+
function runLint(type_aware) {
|
|
84
|
+
if (state.isLinting) return;
|
|
85
|
+
|
|
86
|
+
state.isLinting = true;
|
|
87
|
+
state.message = type_aware
|
|
88
|
+
? "Running linter with --type-aware..."
|
|
89
|
+
: "Running linter...";
|
|
90
|
+
state.messageType = "info";
|
|
91
|
+
render();
|
|
92
|
+
|
|
93
|
+
const cmd = `npx -p oxlint@${OXLINT_VERSION} ${type_aware ? "-p oxlint-tsgolint@latest oxlint --type-aware" : "oxlint"}`;
|
|
94
|
+
|
|
95
|
+
exec(cmd, (error, stdout, stderr) => {
|
|
96
|
+
const fullOutput = stdout + stderr;
|
|
97
|
+
const summaryMatch = fullOutput.match(
|
|
98
|
+
/Found (\d+) warnings? and (\d+) errors?/,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
state.isLinting = false;
|
|
102
|
+
if (summaryMatch) {
|
|
103
|
+
state.message = summaryMatch[0];
|
|
104
|
+
const errors = parseInt(summaryMatch[2]);
|
|
105
|
+
state.messageType = errors > 0 ? "error" : "warn";
|
|
106
|
+
} else if (
|
|
107
|
+
fullOutput.includes("Finished") ||
|
|
108
|
+
(!error && fullOutput.length === 0)
|
|
109
|
+
) {
|
|
110
|
+
state.message = "Linting passed! 0 issues found.";
|
|
111
|
+
state.messageType = "success";
|
|
112
|
+
} else {
|
|
113
|
+
state.message = "Something went wrong";
|
|
114
|
+
state.messageType = "error";
|
|
115
|
+
}
|
|
116
|
+
render();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
65
120
|
function reducer(state, action) {
|
|
66
|
-
const {
|
|
121
|
+
const {
|
|
122
|
+
categories,
|
|
123
|
+
rulesByCategory,
|
|
124
|
+
selectedCatIdx,
|
|
125
|
+
selectedRuleIdx,
|
|
126
|
+
activePane,
|
|
127
|
+
} = state;
|
|
67
128
|
const currentCat = categories[selectedCatIdx];
|
|
68
129
|
const currentRules = rulesByCategory[currentCat] || [];
|
|
69
|
-
const viewHeight = stdout.rows -
|
|
70
|
-
|
|
130
|
+
const viewHeight = stdout.rows - 8;
|
|
71
131
|
const statsHeight = 7;
|
|
72
132
|
const catViewHeight = viewHeight - statsHeight;
|
|
73
133
|
|
|
74
134
|
switch (action.type) {
|
|
75
|
-
case
|
|
135
|
+
case "SET_STATUS": {
|
|
76
136
|
if (activePane !== 1) return state;
|
|
77
137
|
const rule = currentRules[selectedRuleIdx];
|
|
78
138
|
if (!rule) return state;
|
|
79
|
-
|
|
80
139
|
updateConfig(rule, action.value);
|
|
81
|
-
|
|
82
140
|
const updatedRules = [...currentRules];
|
|
83
141
|
updatedRules[selectedRuleIdx] = {
|
|
84
142
|
...rule,
|
|
85
143
|
configStatus: action.value,
|
|
86
|
-
isActive: action.value ===
|
|
144
|
+
isActive: action.value === "error" || action.value === "warn",
|
|
87
145
|
};
|
|
88
|
-
|
|
89
146
|
return {
|
|
90
147
|
...state,
|
|
148
|
+
message: `Rule '${rule.value}' set to: ${action.value}`,
|
|
149
|
+
messageType: "info",
|
|
91
150
|
rulesByCategory: {
|
|
92
151
|
...rulesByCategory,
|
|
93
|
-
[currentCat]: updatedRules
|
|
94
|
-
}
|
|
152
|
+
[currentCat]: updatedRules,
|
|
153
|
+
},
|
|
95
154
|
};
|
|
96
155
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (activePane !== 1)
|
|
100
|
-
return { ...state, activePane: activePane + 1 };
|
|
156
|
+
case "MOVE_RIGHT":
|
|
157
|
+
if (activePane !== 1) return { ...state, activePane: activePane + 1 };
|
|
101
158
|
return state;
|
|
102
159
|
|
|
103
|
-
case
|
|
104
|
-
if (activePane !== 0)
|
|
105
|
-
return { ...state, activePane: activePane - 1 };
|
|
160
|
+
case "MOVE_LEFT":
|
|
161
|
+
if (activePane !== 0) return { ...state, activePane: activePane - 1 };
|
|
106
162
|
return state;
|
|
107
|
-
|
|
108
|
-
case 'MOVE_UP':
|
|
163
|
+
case "MOVE_UP":
|
|
109
164
|
if (activePane === 0) {
|
|
110
|
-
const nextIdx =
|
|
165
|
+
const nextIdx =
|
|
166
|
+
selectedCatIdx === 0 ? categories.length - 1 : selectedCatIdx - 1;
|
|
111
167
|
return {
|
|
112
168
|
...state,
|
|
113
169
|
selectedCatIdx: nextIdx,
|
|
114
170
|
selectedRuleIdx: 0,
|
|
115
171
|
scrollRule: 0,
|
|
116
|
-
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight)
|
|
172
|
+
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
117
173
|
};
|
|
118
174
|
} else if (activePane === 1) {
|
|
119
|
-
const nextIdx =
|
|
175
|
+
const nextIdx =
|
|
176
|
+
selectedRuleIdx === 0 ? currentRules.length - 1 : selectedRuleIdx - 1;
|
|
120
177
|
return {
|
|
121
178
|
...state,
|
|
122
179
|
selectedRuleIdx: nextIdx,
|
|
123
|
-
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight)
|
|
180
|
+
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
124
181
|
};
|
|
125
182
|
}
|
|
126
183
|
return state;
|
|
127
|
-
|
|
128
|
-
case 'MOVE_DOWN':
|
|
184
|
+
case "MOVE_DOWN":
|
|
129
185
|
if (activePane === 0) {
|
|
130
|
-
const nextIdx =
|
|
186
|
+
const nextIdx =
|
|
187
|
+
selectedCatIdx === categories.length - 1 ? 0 : selectedCatIdx + 1;
|
|
131
188
|
return {
|
|
132
189
|
...state,
|
|
133
190
|
selectedCatIdx: nextIdx,
|
|
134
191
|
selectedRuleIdx: 0,
|
|
135
192
|
scrollRule: 0,
|
|
136
|
-
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight)
|
|
193
|
+
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
137
194
|
};
|
|
138
195
|
} else if (activePane === 1) {
|
|
139
|
-
const nextIdx =
|
|
196
|
+
const nextIdx =
|
|
197
|
+
selectedRuleIdx === currentRules.length - 1 ? 0 : selectedRuleIdx + 1;
|
|
140
198
|
return {
|
|
141
199
|
...state,
|
|
142
200
|
selectedRuleIdx: nextIdx,
|
|
143
|
-
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight)
|
|
201
|
+
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
144
202
|
};
|
|
145
203
|
}
|
|
146
204
|
return state;
|
|
147
|
-
|
|
148
205
|
default:
|
|
149
206
|
return state;
|
|
150
207
|
}
|
|
@@ -153,83 +210,82 @@ function reducer(state, action) {
|
|
|
153
210
|
function getRuleStatus(ruleName, category, config) {
|
|
154
211
|
if (config.rules) {
|
|
155
212
|
let val = config.rules[ruleName];
|
|
156
|
-
|
|
157
213
|
if (val === undefined) {
|
|
158
|
-
const foundKey = Object.keys(config.rules).find(key =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (val !== undefined) {
|
|
165
|
-
if (Array.isArray(val)) return val[0];
|
|
166
|
-
return val;
|
|
214
|
+
const foundKey = Object.keys(config.rules).find((key) =>
|
|
215
|
+
key.endsWith(`/${ruleName}`),
|
|
216
|
+
);
|
|
217
|
+
if (foundKey) val = config.rules[foundKey];
|
|
167
218
|
}
|
|
219
|
+
if (val !== undefined) return Array.isArray(val) ? val[0] : val;
|
|
168
220
|
}
|
|
169
|
-
|
|
170
|
-
if (config.categories && config.categories[category]) {
|
|
171
|
-
return config.categories[category];
|
|
172
|
-
}
|
|
173
|
-
return 'off';
|
|
221
|
+
return (config.categories && config.categories[category]) || "off";
|
|
174
222
|
}
|
|
175
223
|
|
|
176
224
|
function stripJsonComments(json) {
|
|
177
|
-
return json.replace(
|
|
225
|
+
return json.replace(
|
|
226
|
+
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
|
|
227
|
+
(m, g) => (g ? "" : m),
|
|
228
|
+
);
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
function loadRules() {
|
|
181
232
|
let rulesData;
|
|
182
|
-
let config = {
|
|
233
|
+
let config = {
|
|
234
|
+
rules: {},
|
|
235
|
+
categories: {},
|
|
236
|
+
};
|
|
183
237
|
let configPath = null;
|
|
184
238
|
|
|
185
239
|
try {
|
|
186
|
-
const raw = execSync(
|
|
240
|
+
const raw = execSync(
|
|
241
|
+
`npx --yes oxlint@${OXLINT_VERSION} --rules --format=json`,
|
|
242
|
+
{
|
|
243
|
+
encoding: "utf8",
|
|
244
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
245
|
+
},
|
|
246
|
+
);
|
|
187
247
|
rulesData = JSON.parse(raw);
|
|
188
248
|
} catch (e) {
|
|
189
|
-
console.error(
|
|
249
|
+
console.error(
|
|
250
|
+
`${COLORS.error}Error: Could not run 'npx oxlint'.${COLORS.reset}`,
|
|
251
|
+
);
|
|
190
252
|
exit(1);
|
|
191
253
|
}
|
|
192
254
|
|
|
193
255
|
const userConfigPath = argv[2];
|
|
194
|
-
if (userConfigPath) {
|
|
195
|
-
if (!fs.existsSync(userConfigPath)) {
|
|
196
|
-
console.error(`${COLORS.error}Error: Config file '${userConfigPath}' not found.${COLORS.reset}`);
|
|
197
|
-
exit(1);
|
|
198
|
-
}
|
|
256
|
+
if (userConfigPath && fs.existsSync(userConfigPath)) {
|
|
199
257
|
configPath = userConfigPath;
|
|
200
|
-
} else if (fs.existsSync(
|
|
201
|
-
configPath =
|
|
258
|
+
} else if (fs.existsSync(".oxlintrc.json")) {
|
|
259
|
+
configPath = ".oxlintrc.json";
|
|
202
260
|
}
|
|
203
261
|
|
|
204
262
|
if (configPath) {
|
|
205
263
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} catch (e) {
|
|
210
|
-
console.error(`${COLORS.error}Error: Failed to parse '${configPath}'.${COLORS.reset}`);
|
|
211
|
-
exit(1);
|
|
212
|
-
}
|
|
264
|
+
config = JSON.parse(
|
|
265
|
+
stripJsonComments(fs.readFileSync(configPath, "utf8")),
|
|
266
|
+
);
|
|
267
|
+
} catch (e) { }
|
|
213
268
|
}
|
|
214
269
|
|
|
215
270
|
const map = {};
|
|
216
|
-
rulesData.forEach(rule => {
|
|
217
|
-
const cat = rule.category ||
|
|
271
|
+
rulesData.forEach((rule) => {
|
|
272
|
+
const cat = rule.category || "Uncategorized";
|
|
218
273
|
if (!map[cat]) map[cat] = [];
|
|
219
274
|
const status = getRuleStatus(rule.value, cat, config);
|
|
220
|
-
map[cat].push({
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
categories.forEach(c => {
|
|
225
|
-
map[c].sort((a, b) => {
|
|
226
|
-
if (a.isActive && !b.isActive) return -1;
|
|
227
|
-
if (!a.isActive && b.isActive) return 1;
|
|
228
|
-
return a.value.localeCompare(b.value);
|
|
275
|
+
map[cat].push({
|
|
276
|
+
...rule,
|
|
277
|
+
configStatus: status,
|
|
278
|
+
isActive: status === "error" || status === "warn",
|
|
229
279
|
});
|
|
230
280
|
});
|
|
231
281
|
|
|
232
|
-
|
|
282
|
+
const categories = Object.keys(map).sort();
|
|
283
|
+
return {
|
|
284
|
+
categories,
|
|
285
|
+
rulesByCategory: map,
|
|
286
|
+
config,
|
|
287
|
+
configPath,
|
|
288
|
+
};
|
|
233
289
|
}
|
|
234
290
|
|
|
235
291
|
function updateScroll(idx, currentScroll, viewHeight) {
|
|
@@ -240,47 +296,77 @@ function updateScroll(idx, currentScroll, viewHeight) {
|
|
|
240
296
|
|
|
241
297
|
function openUrl(url) {
|
|
242
298
|
if (!url) return;
|
|
243
|
-
const cmd =
|
|
299
|
+
const cmd =
|
|
300
|
+
platform === "darwin"
|
|
301
|
+
? "open"
|
|
302
|
+
: platform === "win32"
|
|
303
|
+
? "start"
|
|
304
|
+
: "xdg-open";
|
|
244
305
|
exec(`${cmd} "${url}"`);
|
|
245
306
|
}
|
|
246
307
|
|
|
247
308
|
function chunkString(str, len) {
|
|
309
|
+
if (!str) return [];
|
|
248
310
|
const size = Math.ceil(str.length / len);
|
|
249
311
|
const r = Array(size);
|
|
250
|
-
for (let i = 0; i < size; i++)
|
|
251
|
-
r[i] = str.substring(i * len, (i + 1) * len);
|
|
252
|
-
}
|
|
312
|
+
for (let i = 0; i < size; i++) r[i] = str.substring(i * len, (i + 1) * len);
|
|
253
313
|
return r;
|
|
254
314
|
}
|
|
255
315
|
|
|
256
316
|
const write = (str) => stdout.write(str);
|
|
257
|
-
const enterAltScreen = () => write(
|
|
258
|
-
const exitAltScreen = () => write(
|
|
259
|
-
|
|
260
|
-
function drawBox(
|
|
317
|
+
const enterAltScreen = () => write("\x1b[?1049h\x1b[?25l");
|
|
318
|
+
const exitAltScreen = () => write("\x1b[?1049l\x1b[?25h");
|
|
319
|
+
|
|
320
|
+
function drawBox(
|
|
321
|
+
buffer,
|
|
322
|
+
x,
|
|
323
|
+
y,
|
|
324
|
+
width,
|
|
325
|
+
height,
|
|
326
|
+
title,
|
|
327
|
+
items,
|
|
328
|
+
selectedIdx,
|
|
329
|
+
scrollOffset,
|
|
330
|
+
isActive,
|
|
331
|
+
) {
|
|
261
332
|
const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
|
|
262
|
-
const titleClean =
|
|
263
|
-
|
|
333
|
+
const titleClean =
|
|
334
|
+
title.length > width - 6 ? title.substring(0, width - 7) + "…" : title;
|
|
335
|
+
const topBorder = `${borderColor}┌─ ${titleClean} `.padEnd(
|
|
336
|
+
width + borderColor.length - 1,
|
|
337
|
+
"─",
|
|
338
|
+
);
|
|
264
339
|
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
265
340
|
|
|
266
341
|
for (let i = 1; i < height - 1; i++) {
|
|
267
|
-
buffer.push(
|
|
342
|
+
buffer.push(
|
|
343
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
344
|
+
);
|
|
268
345
|
}
|
|
269
|
-
buffer.push(
|
|
346
|
+
buffer.push(
|
|
347
|
+
`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`,
|
|
348
|
+
);
|
|
270
349
|
|
|
271
350
|
const innerHeight = height - 2;
|
|
272
351
|
items.slice(scrollOffset, scrollOffset + innerHeight).forEach((item, i) => {
|
|
273
352
|
const absIdx = scrollOffset + i;
|
|
274
353
|
const rawText = (item.value || item).toString();
|
|
275
|
-
let display =
|
|
354
|
+
let display =
|
|
355
|
+
rawText.length > width - 4
|
|
356
|
+
? rawText.substring(0, width - 5) + "…"
|
|
357
|
+
: rawText.padEnd(width - 4);
|
|
276
358
|
let itemColor = COLORS.dim;
|
|
277
|
-
if (item.configStatus ===
|
|
278
|
-
else if (item.configStatus ===
|
|
359
|
+
if (item.configStatus === "error") itemColor = COLORS.error;
|
|
360
|
+
else if (item.configStatus === "warn") itemColor = COLORS.warn;
|
|
279
361
|
else if (item.isActive) itemColor = COLORS.success;
|
|
280
362
|
|
|
281
363
|
buffer.push(`\x1b[${y + 1 + i};${x + 2}H`);
|
|
282
364
|
if (absIdx === selectedIdx) {
|
|
283
|
-
buffer.push(
|
|
365
|
+
buffer.push(
|
|
366
|
+
isActive
|
|
367
|
+
? `${COLORS.selectedBg}${display}${COLORS.reset}`
|
|
368
|
+
: `${COLORS.dim}\x1b[7m${display}${COLORS.reset}`,
|
|
369
|
+
);
|
|
284
370
|
} else {
|
|
285
371
|
buffer.push(`${itemColor}${display}${COLORS.reset}`);
|
|
286
372
|
}
|
|
@@ -289,69 +375,93 @@ function drawBox(buffer, x, y, width, height, title, items, selectedIdx, scrollO
|
|
|
289
375
|
|
|
290
376
|
function drawStats(buffer, x, y, width, height, rules) {
|
|
291
377
|
const borderColor = COLORS.borderInactive;
|
|
292
|
-
const topBorder = `${borderColor}┌─ STATS `.padEnd(
|
|
378
|
+
const topBorder = `${borderColor}┌─ STATS `.padEnd(
|
|
379
|
+
width + borderColor.length - 1,
|
|
380
|
+
"─",
|
|
381
|
+
);
|
|
293
382
|
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
294
|
-
for (let i = 1; i < height - 1; i++)
|
|
295
|
-
|
|
383
|
+
for (let i = 1; i < height - 1; i++)
|
|
384
|
+
buffer.push(
|
|
385
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
386
|
+
);
|
|
387
|
+
buffer.push(
|
|
388
|
+
`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`,
|
|
389
|
+
);
|
|
296
390
|
|
|
297
391
|
let counts = { error: 0, warn: 0, off: 0 };
|
|
298
|
-
rules.forEach(r => {
|
|
299
|
-
if (r.configStatus ===
|
|
300
|
-
else if (r.configStatus ===
|
|
392
|
+
rules.forEach((r) => {
|
|
393
|
+
if (r.configStatus === "error") counts.error++;
|
|
394
|
+
else if (r.configStatus === "warn") counts.warn++;
|
|
301
395
|
else counts.off++;
|
|
302
396
|
});
|
|
303
397
|
|
|
304
398
|
const lines = [
|
|
305
|
-
{ label:
|
|
306
|
-
{ label:
|
|
307
|
-
{ label:
|
|
399
|
+
{ label: "Error", count: counts.error, color: COLORS.error },
|
|
400
|
+
{ label: "Warn", count: counts.warn, color: COLORS.warn },
|
|
401
|
+
{ label: "Off", count: counts.off, color: COLORS.dim },
|
|
308
402
|
];
|
|
309
403
|
|
|
310
404
|
lines.forEach((line, i) => {
|
|
311
405
|
if (i < height - 2) {
|
|
312
406
|
const numStr = String(line.count).padStart(3);
|
|
313
407
|
const labelStr = line.label.padEnd(width - 8);
|
|
314
|
-
buffer.push(
|
|
408
|
+
buffer.push(
|
|
409
|
+
`\x1b[${y + 1 + i};${x + 2}H${line.color}${labelStr}${numStr}${COLORS.reset}`,
|
|
410
|
+
);
|
|
315
411
|
}
|
|
316
412
|
});
|
|
317
413
|
}
|
|
318
414
|
|
|
319
415
|
function drawDetails(buffer, x, y, width, height, rule, isActive) {
|
|
320
416
|
const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
|
|
321
|
-
const topBorder = `${borderColor}┌─ DETAILS `.padEnd(
|
|
417
|
+
const topBorder = `${borderColor}┌─ DETAILS `.padEnd(
|
|
418
|
+
width + borderColor.length - 1,
|
|
419
|
+
"─",
|
|
420
|
+
);
|
|
322
421
|
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
323
|
-
for (let i = 1; i < height - 1; i++)
|
|
324
|
-
|
|
422
|
+
for (let i = 1; i < height - 1; i++)
|
|
423
|
+
buffer.push(
|
|
424
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
425
|
+
);
|
|
426
|
+
buffer.push(
|
|
427
|
+
`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`,
|
|
428
|
+
);
|
|
325
429
|
|
|
326
430
|
if (!rule) return;
|
|
327
431
|
|
|
328
432
|
let statusDisplay = rule.configStatus.toUpperCase();
|
|
329
|
-
if (rule.configStatus ===
|
|
330
|
-
|
|
433
|
+
if (rule.configStatus === "error")
|
|
434
|
+
statusDisplay = `${COLORS.error}${statusDisplay}${COLORS.reset}`;
|
|
435
|
+
else if (rule.configStatus === "warn")
|
|
436
|
+
statusDisplay = `${COLORS.warn}${statusDisplay}${COLORS.reset}`;
|
|
331
437
|
else statusDisplay = `${COLORS.dim}${statusDisplay}${COLORS.reset}`;
|
|
332
438
|
|
|
333
439
|
const labels = [
|
|
334
|
-
[
|
|
335
|
-
[
|
|
336
|
-
[
|
|
337
|
-
[
|
|
338
|
-
[
|
|
339
|
-
[
|
|
340
|
-
[
|
|
341
|
-
[
|
|
440
|
+
["Name", rule.value],
|
|
441
|
+
["Status", statusDisplay],
|
|
442
|
+
["Category", rule.category],
|
|
443
|
+
["Scope", rule.scope],
|
|
444
|
+
["Fix", rule.fix],
|
|
445
|
+
["Default", rule.default ? "Yes" : "No"],
|
|
446
|
+
["Type-aware", rule.type_aware ? "Yes" : "No"],
|
|
447
|
+
["Docs", `Hit ${COLORS.highlight}ENTER${COLORS.reset} to open docs`],
|
|
342
448
|
];
|
|
343
449
|
|
|
344
450
|
let line = 0;
|
|
345
451
|
labels.forEach(([lbl, val]) => {
|
|
346
|
-
if (lbl ===
|
|
347
|
-
buffer.push(
|
|
452
|
+
if (lbl === "Status" && line < height - 2) {
|
|
453
|
+
buffer.push(
|
|
454
|
+
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${val}`,
|
|
455
|
+
);
|
|
348
456
|
line++;
|
|
349
457
|
return;
|
|
350
458
|
}
|
|
351
|
-
const chunks = chunkString(String(val ||
|
|
459
|
+
const chunks = chunkString(String(val || "N/A"), width - 15);
|
|
352
460
|
chunks.forEach((chunk) => {
|
|
353
461
|
if (line < height - 2) {
|
|
354
|
-
buffer.push(
|
|
462
|
+
buffer.push(
|
|
463
|
+
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${chunk}`,
|
|
464
|
+
);
|
|
355
465
|
line++;
|
|
356
466
|
}
|
|
357
467
|
});
|
|
@@ -363,46 +473,87 @@ function render() {
|
|
|
363
473
|
const currentCat = state.categories[state.selectedCatIdx];
|
|
364
474
|
const rules = state.rulesByCategory[currentCat] || [];
|
|
365
475
|
const rule = rules[state.selectedRuleIdx];
|
|
366
|
-
const boxHeight = rows -
|
|
476
|
+
const boxHeight = rows - 5;
|
|
367
477
|
const col1W = Math.floor(columns * 0.2);
|
|
368
478
|
const col2W = Math.floor(columns * 0.3);
|
|
369
479
|
const col3W = columns - col1W - col2W - 2;
|
|
370
480
|
const statsHeight = 6;
|
|
371
481
|
const catListHeight = boxHeight - statsHeight;
|
|
372
482
|
|
|
373
|
-
const buffer = [
|
|
374
|
-
drawBox(
|
|
483
|
+
const buffer = ["\x1b[H\x1b[J"];
|
|
484
|
+
drawBox(
|
|
485
|
+
buffer,
|
|
486
|
+
1,
|
|
487
|
+
1,
|
|
488
|
+
col1W,
|
|
489
|
+
catListHeight,
|
|
490
|
+
"CATEGORIES",
|
|
491
|
+
state.categories,
|
|
492
|
+
state.selectedCatIdx,
|
|
493
|
+
state.scrollCat,
|
|
494
|
+
state.activePane === 0,
|
|
495
|
+
);
|
|
375
496
|
drawStats(buffer, 1, 1 + catListHeight, col1W, statsHeight, rules);
|
|
376
|
-
drawBox(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
497
|
+
drawBox(
|
|
498
|
+
buffer,
|
|
499
|
+
col1W + 1,
|
|
500
|
+
1,
|
|
501
|
+
col2W,
|
|
502
|
+
boxHeight,
|
|
503
|
+
`RULES (${rules.length})`,
|
|
504
|
+
rules,
|
|
505
|
+
state.selectedRuleIdx,
|
|
506
|
+
state.scrollRule,
|
|
507
|
+
state.activePane === 1,
|
|
508
|
+
);
|
|
509
|
+
drawDetails(
|
|
510
|
+
buffer,
|
|
511
|
+
col1W + col2W + 1,
|
|
512
|
+
1,
|
|
513
|
+
col3W,
|
|
514
|
+
boxHeight,
|
|
515
|
+
rule,
|
|
516
|
+
state.activePane === 2,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const msgColor = COLORS[state.messageType] || COLORS.reset;
|
|
520
|
+
buffer.push(
|
|
521
|
+
`\x1b[${rows - 3};2H${msgColor}● ${state.message}${COLORS.reset}`,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const footerConfig = state.configPath
|
|
525
|
+
? `Config: ${state.configPath}`
|
|
526
|
+
: "No config loaded";
|
|
527
|
+
buffer.push(
|
|
528
|
+
`\x1b[${rows - 1};2H${COLORS.dim}Arrows/HJKL: Nav | 1-3: Status | R: Lint | T: Lint with --type-aware | Enter: Docs | Q: Quit | ${footerConfig}${COLORS.reset}`,
|
|
529
|
+
);
|
|
530
|
+
write(buffer.join(""));
|
|
382
531
|
}
|
|
383
532
|
|
|
384
|
-
let state = {
|
|
385
|
-
activePane: 0,
|
|
386
|
-
selectedCatIdx: 0,
|
|
387
|
-
selectedRuleIdx: 0,
|
|
388
|
-
scrollCat: 0,
|
|
389
|
-
scrollRule: 0,
|
|
390
|
-
...loadRules()
|
|
391
|
-
};
|
|
392
|
-
|
|
393
533
|
readline.emitKeypressEvents(stdin);
|
|
394
534
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
395
535
|
|
|
396
|
-
stdin.on(
|
|
397
|
-
const action =
|
|
536
|
+
stdin.on("keypress", (_, key) => {
|
|
537
|
+
const action =
|
|
538
|
+
KEY_MAP[key.name] ||
|
|
539
|
+
(key.ctrl && key.name === "c"
|
|
540
|
+
? { type: "EXIT" }
|
|
541
|
+
: KEY_MAP[key.sequence] || null);
|
|
398
542
|
if (!action) return;
|
|
399
543
|
|
|
400
|
-
if (action.type ===
|
|
544
|
+
if (action.type === "EXIT") {
|
|
401
545
|
exitAltScreen();
|
|
402
546
|
exit(0);
|
|
403
547
|
}
|
|
404
|
-
|
|
405
|
-
|
|
548
|
+
if (action.type === "RUN_LINT") {
|
|
549
|
+
runLint();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (action.type === "RUN_TYPE_AWARE_LINT") {
|
|
553
|
+
runLint(true);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (action.type === "OPEN_DOCS" && state.activePane === 1) {
|
|
406
557
|
const currentCat = state.categories[state.selectedCatIdx];
|
|
407
558
|
const rule = state.rulesByCategory[currentCat]?.[state.selectedRuleIdx];
|
|
408
559
|
if (rule) openUrl(rule.docs_url || rule.url);
|
|
@@ -413,6 +564,6 @@ stdin.on('keypress', (_, key) => {
|
|
|
413
564
|
render();
|
|
414
565
|
});
|
|
415
566
|
|
|
416
|
-
stdout.on(
|
|
567
|
+
stdout.on("resize", render);
|
|
417
568
|
enterAltScreen();
|
|
418
|
-
render();
|
|
569
|
+
render();
|