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