oxlint-tui 1.2.0 → 1.4.0
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 +9 -10
- package/dist/index.mjs +3 -0
- package/dist/rule-descriptions.br +0 -0
- package/package.json +12 -11
- package/dist/index.js +0 -360
- package/dist/rendering.js +0 -166
- package/dist/rule-descriptions.json +0 -693
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
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.
|
|
4
4
|
|
|
5
|
-
It automatically loads your local configuration to show you the status of
|
|
5
|
+
It automatically loads your local configuration (if one exists) to show you the current status of your project. You can then toggle rules on the fly to see how they affect your codebase without altering your actual configuration file.
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**It serves as a playground:** Toggle rules in memory, run the linter, and see the results immediately.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## Why?
|
|
12
12
|
|
|
13
|
-
Configuring linters often involves jumping between your editor, a massive JSON file, and web documentation. `oxlint-tui` tries to make the process easier by giving you an **interactive dashboard** right in your terminal.
|
|
13
|
+
Configuring linters often involves jumping between your editor, a massive JSON file, and web documentation. `oxlint-tui` tries to make the process easier by giving you an **interactive dashboard** right in your terminal. It allows you to "try before you buy"—enabling strict rules temporarily to see how many errors they would produce.
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
17
|
+
- **Non-Destructive**: Toggling rules happens entirely in memory. No changes are written to disk, making it safe to experiment without messing up your config or comments.
|
|
18
|
+
- **Config Aware**: Automatically reads `.oxlintrc.json` to initialize the state, but works perfectly even if no config file exists.
|
|
19
|
+
- **Details**: View category, scope, fix, default, and type-aware rule parameters at a glance.
|
|
19
20
|
- **View Docs**: Press <kbd>ENTER</kbd> on any rule to open its official documentation in your browser.
|
|
20
21
|
- **Zero Dependencies**: Written in pure Node.js without any heavy TUI libraries.
|
|
21
22
|
|
|
@@ -31,7 +32,7 @@ npx oxlint-tui
|
|
|
31
32
|
|
|
32
33
|
### Custom Config Path
|
|
33
34
|
|
|
34
|
-
If
|
|
35
|
+
If you want to load an initial state from a specific config file:
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
npx oxlint-tui ./configs/oxlint.json
|
|
@@ -68,9 +69,7 @@ oxlint-tui
|
|
|
68
69
|
- Node.js >= 16
|
|
69
70
|
- `oxlint` (The tool runs `npx oxlint --rules --format=json` internally to fetch definitions)
|
|
70
71
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
The goal is to build this into a tool that not only reads the information provided by the oxlint CLI and your configuration file - but also allows to fully customize the configuration. Oxlint provides a lot more flexibility than just toggling rules on/off, so making this fully functional is going to require more work.
|
|
72
|
+
## Contributing
|
|
74
73
|
|
|
75
74
|
If you're willing and able, please feel free to [contribute to this project](https://github.com/holoflash/oxlint-tui/blob/main/CONTRIBUTING.md).
|
|
76
75
|
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import e from"node:fs";import t from"node:zlib";import{fileURLToPath as n}from"node:url";import r from"node:path";import i from"readline";import{execSync as a,spawn as o}from"node:child_process";import{argv as s,exit as c,platform as l,stdin as u,stdout as d}from"node:process";const f={reset:`\x1B[0m`,dim:`\x1B[38;5;242m`,highlight:`\x1B[38;5;111m`,selectedBg:`\x1B[48;5;24m\x1B[38;5;255m\x1B[1m`,borderActive:`\x1B[38;5;111m`,borderInactive:`\x1B[38;5;237m`,error:`\x1B[38;5;203m`,warn:`\x1B[38;5;215m`,success:`\x1B[38;5;114m`,info:`\x1B[38;5;75m`};function p(e,t){if(!e)return[];let n=Math.ceil(e.length/t),r=Array(n);for(let i=0;i<n;i++)r[i]=e.substring(i*t,(i+1)*t);return r}function m(e,t,n,r,i,a,o,s,c,l){let u=l?f.borderActive:f.borderInactive,d=`${u}┌─ ${a.length>r-6?a.substring(0,r-7)+`…`:a} `.padEnd(r+u.length-1,`─`);e.push(`\x1b[${n};${t}H${d}┐${f.reset}`);for(let a=1;a<i-1;a++)e.push(`\x1b[${n+a};${t}H${u}│${` `.repeat(r-2)}│${f.reset}`);e.push(`\x1b[${n+i-1};${t}H${u}└${`─`.repeat(r-2)}┘${f.reset}`);let p=i-2;o.slice(c,c+p).forEach((i,a)=>{let o=c+a,u=typeof i!=`string`,d=u?i.value:i,p=d.length>r-4?d.substring(0,r-5)+`…`:d.padEnd(r-4),m=f.dim;if(u){let e=i;e.configStatus===`error`?m=f.error:e.configStatus===`warn`?m=f.warn:e.isActive&&(m=f.success)}e.push(`\x1b[${n+1+a};${t+2}H`),o===s?e.push(l?`${f.selectedBg}${p}${f.reset}`:`${f.dim}\x1b[7m${p}${f.reset}`):e.push(`${m}${p}${f.reset}`)})}function h(e,t,n,r,i,a){let o=f.borderInactive,s=`${o}┌─ STATS `.padEnd(r+o.length-1,`─`);e.push(`\x1b[${n};${t}H${s}┐${f.reset}`);for(let a=1;a<i-1;a++)e.push(`\x1b[${n+a};${t}H${o}│${` `.repeat(r-2)}│${f.reset}`);e.push(`\x1b[${n+i-1};${t}H${o}└${`─`.repeat(r-2)}┘${f.reset}`);let c={error:0,warn:0,off:0};a.forEach(e=>{e.configStatus===`error`?c.error++:e.configStatus===`warn`?c.warn++:c.off++}),[{label:`Error`,count:c.error,color:f.error},{label:`Warn`,count:c.warn,color:f.warn},{label:`Off`,count:c.off,color:f.dim}].forEach((a,o)=>{if(o<i-2){let i=String(a.count).padStart(3),s=a.label.padEnd(r-8);e.push(`\x1b[${n+1+o};${t+2}H${a.color}${s}${i}${f.reset}`)}})}function g(e,t,n,r,i,a,o){let s=o?f.borderActive:f.borderInactive,c=`${s}┌─ DETAILS `.padEnd(r+s.length-1,`─`);e.push(`\x1b[${n};${t}H${c}┐${f.reset}`);for(let a=1;a<i-1;a++)e.push(`\x1b[${n+a};${t}H${s}│${` `.repeat(r-2)}│${f.reset}`);if(e.push(`\x1b[${n+i-1};${t}H${s}└${`─`.repeat(r-2)}┘${f.reset}`),!a)return;let l=a.configStatus.toUpperCase();l=a.configStatus===`error`?`${f.error}${l}${f.reset}`:a.configStatus===`warn`?`${f.warn}${l}${f.reset}`:`${f.dim}${l}${f.reset}`;let u=[[`Name`,a.value],[`Status`,l],[`Category`,a.category],[`Scope`,a.scope],[`Fix`,a.fix||`N/A`],[`Default`,a.default?`Yes`:`No`],[`Type-aware`,a.type_aware?`Yes`:`No`]],d=0;u.forEach(([r,a])=>{d<i-2&&(e.push(`\x1b[${n+1+d};${t+2}H${f.highlight}${r.padEnd(12)} ${f.reset}${a}`),d++)}),d<i-3&&d++,d<i-2&&(e.push(`\x1b[${n+1+d};${t+2}H${f.reset}Description:${f.reset}`),d++,p((a.description??`N/A`).replace(/\s+/g,` `).trim(),r-6).forEach(r=>{d<i-2&&(e.push(`\x1b[${n+1+d};${t+2}H${f.dim}${r}${f.reset}`),d++)}));let m=Math.max(d+1,i-3);m<i-1&&e.push(`\x1b[${n+1+m};${t+2}HHit ${f.highlight}ENTER${f.reset} to open docs`)}function _(e,t,n){return e<t?e:e>=t+n?e-n+1:t}function v(){if(!w||!w.categories)return;let{columns:e=80,rows:t=24}=d,n=w.categories[w.selectedCategoryIndex],r=w.rulesByCategory[n]||[],i=r[w.selectedRuleIndex],a=t-5,o=Math.floor(e*.2),s=Math.floor(e*.3),c=e-o-s-2,l=a-6,u=[`\x1B[H\x1B[J`];m(u,1,1,o,l,`CATEGORIES`,w.categories,w.selectedCategoryIndex,w.categoryScroll,w.activePane===0),h(u,1,1+l,o,6,r),m(u,o+1,1,s,a,`RULES (${r.length})`,r,w.selectedRuleIndex,w.ruleScroll,w.activePane===1),g(u,o+s+1,1,c,a,i,w.activePane===2);let p=f[w.messageType]||f.reset;u.push(`\x1b[${t-3};2H${p}● ${w.message}${f.reset}`);let _=w.configPath?`Config: ${w.configPath}`:`No config loaded`;u.push(`\x1b[${t-1};2H${f.dim}Arrows/HJKL: Nav | 1-3: Status | R: Lint | X: Run rule | Enter: Docs | Q: Quit | ${_}${f.reset}`),d.write(u.join(``))}const y=`1.42.0`,b={k:{type:`MOVE_UP`},up:{type:`MOVE_UP`},down:{type:`MOVE_DOWN`},j:{type:`MOVE_DOWN`},left:{type:`MOVE_LEFT`},h:{type:`MOVE_LEFT`},right:{type:`MOVE_RIGHT`},l:{type:`MOVE_RIGHT`},return:{type:`OPEN_DOCS`},enter:{type:`OPEN_DOCS`},1:{type:`SET_STATUS`,value:`off`},2:{type:`SET_STATUS`,value:`warn`},3:{type:`SET_STATUS`,value:`error`},q:{type:`EXIT`},r:{type:`RUN_LINT`},x:{type:`RUN_SINGLE_RULE`}},x=r.dirname(n(import.meta.url));function S(){let n=r.join(x,`rule-descriptions.br`);if(e.existsSync(n))return JSON.parse(t.brotliDecompressSync(e.readFileSync(n)).toString())}const C=S();let w={activePane:0,selectedCategoryIndex:0,selectedRuleIndex:0,categoryScroll:0,ruleScroll:0,isLintInProgress:!1,message:`oxlint-tui`,messageType:`dim`,...A()};function T(e,t){w.config||={rules:{}},w.config.rules||(w.config.rules={});try{let n=e.value,r=e.scope===`oxc2`||e.scope===`eslint`?n:`${e.scope}/${n}`,i=w.config.rules,a=Object.keys(i).find(e=>e===r||e===n||e.endsWith(`/${n}`))||r;i[a]=t}catch{w.message=`Failed to update internal state`,w.messageType=`error`}}function E({rule:e=null}={}){if(w.isLintInProgress)return;w.isLintInProgress=!0;let t=e?`${e.scope}/${e.value}`:null,n=e?e.type_aware:Object.values(w.rulesByCategory).flat().some(e=>e.isActive&&e.type_aware===!0);w.message=`Linting`,t&&(w.message+=` [${t}]`),n&&(w.message+=` with --type-aware`),w.message+=`...`,w.messageType=`info`,v();let r=l===`win32`?`npx.cmd`:`npx`,i=[`-q`,`--yes`,`--package`,`oxlint@${y}`];n&&i.push(`--package`,`oxlint-tsgolint@0.11.4`),i.push(`--`,`oxlint`),n&&i.push(`--type-aware`),t?i.push(`-A`,`all`,`-D`,t):w.config&&w.config.rules&&Object.entries(w.config.rules).forEach(([e,t])=>{let n=Array.isArray(t)?t[0]:t;n===`error`?i.push(`-D`,e):n===`warn`?i.push(`-W`,e):n===`off`&&i.push(`-A`,e)});let a=o(r,i),s=``,c=``;a.stdout.on(`data`,e=>{s+=e}),a.stderr.on(`data`,e=>{c+=e}),a.on(`close`,e=>{w.isLintInProgress=!1;let n=(s+c).match(/Found (\d+) warnings? and (\d+) errors?/i);if(n){let e=parseInt(n[2]);w.message=t?`[${t}] Found ${e} issue${e===1?``:`s`}`:n[0],w.messageType=e>0?`error`:`warn`}else if(s.toLowerCase().includes(`finished`)||e===0&&s.length<200)w.message=`Linting passed! 0 issues found.`,t&&(w.message=`[${t}] ${w.message}`),w.messageType=`success`;else{let e=c.split(`
|
|
3
|
+
`).filter(e=>!e.includes(`experimental`)&&!e.includes(`Breaking changes`)&&e.trim()!==``).join(` `);w.message=e?`Error: ${e.substring(0,50)}...`:`Lint failed`,w.messageType=`error`}v()})}function D(e){if(!e)return;let{categories:t,rulesByCategory:n,selectedCategoryIndex:r,selectedRuleIndex:i,activePane:a}=w,o=t[r],s=n[o]||[],l=d.rows-8,u=l-7;switch(e.type){case`EXIT`:P(),c(0);return;case`RUN_LINT`:E();return;case`RUN_SINGLE_RULE`:{let e=s[i];e&&E({rule:e});return}case`OPEN_DOCS`:if(a===1){let e=s[i];e&&j(e.docs_url||e.url)}return;case`SET_STATUS`:{if(a!==1||!e.value)return;let t=s[i];if(!t)return;T(t,e.value);let r=[...s];r[i]={...t,configStatus:e.value,isActive:e.value===`error`||e.value===`warn`},w={...w,message:`Rule '${t.value}' set to: ${e.value}`,messageType:`info`,rulesByCategory:{...n,[o]:r}},v();return}case`MOVE_RIGHT`:a!==1&&(w={...w,activePane:a+1},v());return;case`MOVE_LEFT`:a!==0&&(w={...w,activePane:a-1},v());return;case`MOVE_UP`:if(a===0){let e=r===0?t.length-1:r-1;w={...w,selectedCategoryIndex:e,selectedRuleIndex:0,ruleScroll:0,categoryScroll:_(e,w.categoryScroll,u)}}else if(a===1){let e=i===0?s.length-1:i-1;w={...w,selectedRuleIndex:e,ruleScroll:_(e,w.ruleScroll,l)}}v();return;case`MOVE_DOWN`:if(a===0){let e=r===t.length-1?0:r+1;w={...w,selectedCategoryIndex:e,selectedRuleIndex:0,ruleScroll:0,categoryScroll:_(e,w.categoryScroll,u)}}else if(a===1){let e=i===s.length-1?0:i+1;w={...w,selectedRuleIndex:e,ruleScroll:_(e,w.ruleScroll,l)}}v();return}}function O(e,t,n){if(n.rules){let t=n.rules[e];if(t===void 0){let r=Object.keys(n.rules).find(t=>t.endsWith(`/${e}`));r&&(t=n.rules[r])}if(t!==void 0)return Array.isArray(t)?t[0]:t}return n.categories&&n.categories[t]?n.categories[t]:`off`}function k(e){return e.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,(e,t)=>t?``:e)}function A(){let t=[],n={rules:{},categories:{}},r=null,i=C;i||(w.message=`Error: Couldn't find description.`,w.messageType=`error`);try{let e=a(`npx -q --yes oxlint@${y} --rules --format=json`,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]});t=JSON.parse(e)}catch{w.message=`Error: Couldn't run 'npx oxlint'.`,w.messageType=`error`,c(1)}let o=s[2];if(o&&e.existsSync(o)?r=o:e.existsSync(`.oxlintrc.json`)&&(r=`.oxlintrc.json`),r)try{n=JSON.parse(k(e.readFileSync(r,`utf8`)))}catch{w.message=`Error: Couldn't parse config.`,w.messageType=`error`}let l={};return t.forEach(e=>{let t=e.category||`Uncategorized`;l[t]||(l[t]=[]);let r=O(e.value,t,n),a=i[e.scope]?.[e.value];l[t].push({...e,description:a,configStatus:r,isActive:r===`error`||r===`warn`})}),{categories:Object.keys(l).toSorted(),rulesByCategory:l,config:n,configPath:r}}function j(e){e&&o(l===`darwin`?`open`:l===`win32`?`explorer`:`xdg-open`,[e],{detached:!0,stdio:`ignore`}).unref()}const M=e=>d.write(e),N=()=>M(`\x1B[?1049h\x1B[?25l`),P=()=>M(`\x1B[?1049l\x1B[?25h`);i.emitKeypressEvents(u),u.isTTY&&u.setRawMode(!0),u.on(`keypress`,(e,t)=>{D(b[t.name]||(t.ctrl&&t.name===`c`?{type:`EXIT`}:b[t.sequence]||null))}),d.on(`resize`,v),N(),v();export{w as state};
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A Node TUI Oxlint rules and configuration browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "holoflash",
|
|
15
15
|
"bin": {
|
|
16
|
-
"oxlint-tui": "dist/index.
|
|
16
|
+
"oxlint-tui": "./dist/index.mjs"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist"
|
|
@@ -21,22 +21,23 @@
|
|
|
21
21
|
"type": "module",
|
|
22
22
|
"scripts": {
|
|
23
23
|
"prepare": "husky",
|
|
24
|
-
"dev": "tsx src/index.ts
|
|
25
|
-
"generate-rule-descriptions": "tsx src/generate-rule-descriptions.ts",
|
|
24
|
+
"dev": "tsx src/index.ts",
|
|
26
25
|
"lint": "oxlint --type-aware --format=stylish",
|
|
27
26
|
"type-check": "tsgo --noEmit",
|
|
28
27
|
"format": "oxfmt",
|
|
29
28
|
"format-check": "oxfmt --check",
|
|
30
|
-
"
|
|
31
|
-
"build
|
|
29
|
+
"gen": "tsx scripts/generate-rule-descriptions.ts",
|
|
30
|
+
"build": "tsdown && cp src/rule-descriptions.br dist/",
|
|
31
|
+
"build-run": "npm run gen && npm run build && node dist/index.mjs"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "25.0
|
|
35
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
34
|
+
"@types/node": "25.2.0",
|
|
35
|
+
"@typescript/native-preview": "7.0.0-dev.20260202.1",
|
|
36
36
|
"husky": "9.1.7",
|
|
37
|
-
"oxfmt": "0.
|
|
38
|
-
"oxlint": "1.
|
|
39
|
-
"oxlint-tsgolint": "0.11.
|
|
37
|
+
"oxfmt": "0.28.0",
|
|
38
|
+
"oxlint": "1.43.0",
|
|
39
|
+
"oxlint-tsgolint": "0.11.4",
|
|
40
|
+
"tsdown": "0.20.1",
|
|
40
41
|
"tsx": "4.21.0",
|
|
41
42
|
"typescript": "5.9.3"
|
|
42
43
|
},
|
package/dist/index.js
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import readline from "readline";
|
|
4
|
-
import { execSync, spawn } from "node:child_process";
|
|
5
|
-
import { stdout, stdin, exit, platform, argv } from "node:process";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { render } from "./rendering.js";
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
11
|
-
const OXLINT_VERSION = "1.41.0";
|
|
12
|
-
const TSGOLINT_VERSION = "0.11.1";
|
|
13
|
-
const KEY_MAP = {
|
|
14
|
-
k: { type: "MOVE_UP" },
|
|
15
|
-
up: { type: "MOVE_UP" },
|
|
16
|
-
down: { type: "MOVE_DOWN" },
|
|
17
|
-
j: { type: "MOVE_DOWN" },
|
|
18
|
-
left: { type: "MOVE_LEFT" },
|
|
19
|
-
h: { type: "MOVE_LEFT" },
|
|
20
|
-
right: { type: "MOVE_RIGHT" },
|
|
21
|
-
l: { type: "MOVE_RIGHT" },
|
|
22
|
-
return: { type: "OPEN_DOCS" },
|
|
23
|
-
enter: { type: "OPEN_DOCS" },
|
|
24
|
-
"1": { type: "SET_STATUS", value: "off" },
|
|
25
|
-
"2": { type: "SET_STATUS", value: "warn" },
|
|
26
|
-
"3": { type: "SET_STATUS", value: "error" },
|
|
27
|
-
q: { type: "EXIT" },
|
|
28
|
-
r: { type: "RUN_LINT" },
|
|
29
|
-
x: { type: "RUN_SINGLE_RULE" },
|
|
30
|
-
};
|
|
31
|
-
export let state = {
|
|
32
|
-
activePane: 0,
|
|
33
|
-
selectedCategoryIndex: 0,
|
|
34
|
-
selectedRuleIndex: 0,
|
|
35
|
-
categoryScroll: 0,
|
|
36
|
-
ruleScroll: 0,
|
|
37
|
-
isLintInProgress: false,
|
|
38
|
-
message: "oxlint-tui",
|
|
39
|
-
messageType: "dim",
|
|
40
|
-
...loadRules(),
|
|
41
|
-
};
|
|
42
|
-
function updateConfig(rule, newStatus) {
|
|
43
|
-
if (!state.configPath || !state.config)
|
|
44
|
-
return;
|
|
45
|
-
try {
|
|
46
|
-
if (!state.config.rules)
|
|
47
|
-
state.config.rules = {};
|
|
48
|
-
const ruleName = rule.value;
|
|
49
|
-
const canonicalKey = rule.scope === "oxc2" || rule.scope === "eslint" ? ruleName : `${rule.scope}/${ruleName}`;
|
|
50
|
-
const rules = state.config.rules;
|
|
51
|
-
const existingKey = Object.keys(rules).find((key) => key === canonicalKey || key === ruleName || key.endsWith(`/${ruleName}`));
|
|
52
|
-
const targetKey = existingKey || canonicalKey;
|
|
53
|
-
rules[targetKey] = newStatus;
|
|
54
|
-
fs.writeFileSync(state.configPath, JSON.stringify(state.config, null, 2), "utf8");
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
state.message = "Failed to write config file";
|
|
58
|
-
state.messageType = "error";
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function runLint({ rule = null } = {}) {
|
|
62
|
-
if (state.isLintInProgress)
|
|
63
|
-
return;
|
|
64
|
-
state.isLintInProgress = true;
|
|
65
|
-
let ruleName = rule ? `${rule.scope}/${rule.value}` : null;
|
|
66
|
-
const typeAware = rule
|
|
67
|
-
? rule.type_aware
|
|
68
|
-
: Object.values(state.rulesByCategory)
|
|
69
|
-
.flat()
|
|
70
|
-
.some((ruleItem) => ruleItem.isActive && ruleItem.type_aware === true);
|
|
71
|
-
state.message = "Linting";
|
|
72
|
-
if (ruleName)
|
|
73
|
-
state.message += ` [${ruleName}]`;
|
|
74
|
-
if (typeAware)
|
|
75
|
-
state.message += " with --type-aware";
|
|
76
|
-
state.message += "...";
|
|
77
|
-
state.messageType = "info";
|
|
78
|
-
render();
|
|
79
|
-
const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
80
|
-
const args = ["-q", "--yes", "--package", `oxlint@${OXLINT_VERSION}`];
|
|
81
|
-
if (typeAware) {
|
|
82
|
-
args.push("--package", `oxlint-tsgolint@${TSGOLINT_VERSION}`);
|
|
83
|
-
}
|
|
84
|
-
args.push("--", "oxlint");
|
|
85
|
-
if (typeAware) {
|
|
86
|
-
args.push("--type-aware");
|
|
87
|
-
}
|
|
88
|
-
if (ruleName) {
|
|
89
|
-
args.push("-A", "all", "-D", ruleName);
|
|
90
|
-
}
|
|
91
|
-
const child = spawn(npxCmd, args);
|
|
92
|
-
let stdoutData = "";
|
|
93
|
-
let stderrData = "";
|
|
94
|
-
child.stdout.on("data", (data) => {
|
|
95
|
-
stdoutData += data;
|
|
96
|
-
});
|
|
97
|
-
child.stderr.on("data", (data) => {
|
|
98
|
-
stderrData += data;
|
|
99
|
-
});
|
|
100
|
-
child.on("close", (code) => {
|
|
101
|
-
state.isLintInProgress = false;
|
|
102
|
-
const fullOutput = stdoutData + stderrData;
|
|
103
|
-
const summaryMatch = fullOutput.match(/Found (\d+) warnings? and (\d+) errors?/i);
|
|
104
|
-
if (summaryMatch) {
|
|
105
|
-
const errors = parseInt(summaryMatch[2]);
|
|
106
|
-
state.message = ruleName
|
|
107
|
-
? `[${ruleName}] Found ${errors} issue${errors === 1 ? "" : "s"}`
|
|
108
|
-
: summaryMatch[0];
|
|
109
|
-
state.messageType = errors > 0 ? "error" : "warn";
|
|
110
|
-
}
|
|
111
|
-
else if (stdoutData.toLowerCase().includes("finished") ||
|
|
112
|
-
(code === 0 && stdoutData.length < 200)) {
|
|
113
|
-
state.message = "Linting passed! 0 issues found.";
|
|
114
|
-
if (ruleName)
|
|
115
|
-
state.message = `[${ruleName}] ${state.message}`;
|
|
116
|
-
state.messageType = "success";
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
const cleanError = stderrData
|
|
120
|
-
.split("\n")
|
|
121
|
-
.filter((l) => !l.includes("experimental") && !l.includes("Breaking changes") && l.trim() !== "")
|
|
122
|
-
.join(" ");
|
|
123
|
-
state.message = cleanError ? `Error: ${cleanError.substring(0, 50)}...` : "Lint failed";
|
|
124
|
-
state.messageType = "error";
|
|
125
|
-
}
|
|
126
|
-
render();
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
function execute(action) {
|
|
130
|
-
if (!action)
|
|
131
|
-
return;
|
|
132
|
-
const { categories, rulesByCategory, selectedCategoryIndex, selectedRuleIndex, activePane } = state;
|
|
133
|
-
const currentCategory = categories[selectedCategoryIndex];
|
|
134
|
-
const currentCategoryRules = rulesByCategory[currentCategory] || [];
|
|
135
|
-
const viewportHeight = stdout.rows - 8;
|
|
136
|
-
const statsBoxHeight = 7;
|
|
137
|
-
const categoryListHeight = viewportHeight - statsBoxHeight;
|
|
138
|
-
switch (action.type) {
|
|
139
|
-
case "EXIT":
|
|
140
|
-
exitAltScreen();
|
|
141
|
-
exit(0);
|
|
142
|
-
return;
|
|
143
|
-
case "RUN_LINT":
|
|
144
|
-
runLint();
|
|
145
|
-
return;
|
|
146
|
-
case "RUN_SINGLE_RULE": {
|
|
147
|
-
const rule = currentCategoryRules[selectedRuleIndex];
|
|
148
|
-
if (rule)
|
|
149
|
-
runLint({ rule });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
case "OPEN_DOCS": {
|
|
153
|
-
if (activePane === 1) {
|
|
154
|
-
const rule = currentCategoryRules[selectedRuleIndex];
|
|
155
|
-
if (rule)
|
|
156
|
-
openUrl(rule.docs_url || rule.url);
|
|
157
|
-
}
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
case "SET_STATUS": {
|
|
161
|
-
if (activePane !== 1 || !action.value)
|
|
162
|
-
return;
|
|
163
|
-
const rule = currentCategoryRules[selectedRuleIndex];
|
|
164
|
-
if (!rule)
|
|
165
|
-
return;
|
|
166
|
-
updateConfig(rule, action.value);
|
|
167
|
-
const updatedRules = [...currentCategoryRules];
|
|
168
|
-
updatedRules[selectedRuleIndex] = {
|
|
169
|
-
...rule,
|
|
170
|
-
configStatus: action.value,
|
|
171
|
-
isActive: action.value === "error" || action.value === "warn",
|
|
172
|
-
};
|
|
173
|
-
state = {
|
|
174
|
-
...state,
|
|
175
|
-
message: `Rule '${rule.value}' set to: ${action.value}`,
|
|
176
|
-
messageType: "info",
|
|
177
|
-
rulesByCategory: {
|
|
178
|
-
...rulesByCategory,
|
|
179
|
-
[currentCategory]: updatedRules,
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
render();
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
case "MOVE_RIGHT":
|
|
186
|
-
if (activePane !== 1) {
|
|
187
|
-
state = { ...state, activePane: activePane + 1 };
|
|
188
|
-
render();
|
|
189
|
-
}
|
|
190
|
-
return;
|
|
191
|
-
case "MOVE_LEFT":
|
|
192
|
-
if (activePane !== 0) {
|
|
193
|
-
state = { ...state, activePane: activePane - 1 };
|
|
194
|
-
render();
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
case "MOVE_UP":
|
|
198
|
-
if (activePane === 0) {
|
|
199
|
-
const nextIndex = selectedCategoryIndex === 0 ? categories.length - 1 : selectedCategoryIndex - 1;
|
|
200
|
-
state = {
|
|
201
|
-
...state,
|
|
202
|
-
selectedCategoryIndex: nextIndex,
|
|
203
|
-
selectedRuleIndex: 0,
|
|
204
|
-
ruleScroll: 0,
|
|
205
|
-
categoryScroll: updateScroll(nextIndex, state.categoryScroll, categoryListHeight),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
else if (activePane === 1) {
|
|
209
|
-
const nextIndex = selectedRuleIndex === 0 ? currentCategoryRules.length - 1 : selectedRuleIndex - 1;
|
|
210
|
-
state = {
|
|
211
|
-
...state,
|
|
212
|
-
selectedRuleIndex: nextIndex,
|
|
213
|
-
ruleScroll: updateScroll(nextIndex, state.ruleScroll, viewportHeight),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
render();
|
|
217
|
-
return;
|
|
218
|
-
case "MOVE_DOWN":
|
|
219
|
-
if (activePane === 0) {
|
|
220
|
-
const nextIndex = selectedCategoryIndex === categories.length - 1 ? 0 : selectedCategoryIndex + 1;
|
|
221
|
-
state = {
|
|
222
|
-
...state,
|
|
223
|
-
selectedCategoryIndex: nextIndex,
|
|
224
|
-
selectedRuleIndex: 0,
|
|
225
|
-
ruleScroll: 0,
|
|
226
|
-
categoryScroll: updateScroll(nextIndex, state.categoryScroll, categoryListHeight),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
else if (activePane === 1) {
|
|
230
|
-
const nextIndex = selectedRuleIndex === currentCategoryRules.length - 1 ? 0 : selectedRuleIndex + 1;
|
|
231
|
-
state = {
|
|
232
|
-
...state,
|
|
233
|
-
selectedRuleIndex: nextIndex,
|
|
234
|
-
ruleScroll: updateScroll(nextIndex, state.ruleScroll, viewportHeight),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
render();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function getRuleStatus(ruleName, category, config) {
|
|
242
|
-
if (config.rules) {
|
|
243
|
-
let val = config.rules[ruleName];
|
|
244
|
-
if (val === undefined) {
|
|
245
|
-
const foundKey = Object.keys(config.rules).find((key) => key.endsWith(`/${ruleName}`));
|
|
246
|
-
if (foundKey)
|
|
247
|
-
val = config.rules[foundKey];
|
|
248
|
-
}
|
|
249
|
-
if (val !== undefined) {
|
|
250
|
-
const status = Array.isArray(val) ? val[0] : val;
|
|
251
|
-
return status;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (config.categories && config.categories[category]) {
|
|
255
|
-
return config.categories[category];
|
|
256
|
-
}
|
|
257
|
-
return "off";
|
|
258
|
-
}
|
|
259
|
-
function stripJsonComments(json) {
|
|
260
|
-
return json.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m));
|
|
261
|
-
}
|
|
262
|
-
function loadRules() {
|
|
263
|
-
let rulesData = [];
|
|
264
|
-
let config = {
|
|
265
|
-
rules: {},
|
|
266
|
-
categories: {},
|
|
267
|
-
};
|
|
268
|
-
let configPath = null;
|
|
269
|
-
let descriptions = {};
|
|
270
|
-
const descriptionsPath = join(__dirname, "./", "rule-descriptions.json");
|
|
271
|
-
try {
|
|
272
|
-
if (fs.existsSync(descriptionsPath)) {
|
|
273
|
-
descriptions = JSON.parse(fs.readFileSync(descriptionsPath, "utf8"));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
state.message = "Error: Couldn't find description.";
|
|
278
|
-
state.messageType = "error";
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
const raw = execSync(`npx -q --yes oxlint@${OXLINT_VERSION} --rules --format=json`, {
|
|
282
|
-
encoding: "utf8",
|
|
283
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
284
|
-
});
|
|
285
|
-
rulesData = JSON.parse(raw);
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
state.message = "Error: Couldn't run 'npx oxlint'.";
|
|
289
|
-
state.messageType = "error";
|
|
290
|
-
exit(1);
|
|
291
|
-
}
|
|
292
|
-
const userConfigPath = argv[2];
|
|
293
|
-
if (userConfigPath && fs.existsSync(userConfigPath)) {
|
|
294
|
-
configPath = userConfigPath;
|
|
295
|
-
}
|
|
296
|
-
else if (fs.existsSync(".oxlintrc.json")) {
|
|
297
|
-
configPath = ".oxlintrc.json";
|
|
298
|
-
}
|
|
299
|
-
if (configPath) {
|
|
300
|
-
try {
|
|
301
|
-
config = JSON.parse(stripJsonComments(fs.readFileSync(configPath, "utf8")));
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
state.message = "Error: Couldn't parse config.";
|
|
305
|
-
state.messageType = "error";
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
const map = {};
|
|
309
|
-
rulesData.forEach((rule) => {
|
|
310
|
-
const cat = rule.category || "Uncategorized";
|
|
311
|
-
if (!map[cat])
|
|
312
|
-
map[cat] = [];
|
|
313
|
-
const status = getRuleStatus(rule.value, cat, config);
|
|
314
|
-
const description = descriptions[rule.scope]?.[rule.value];
|
|
315
|
-
map[cat].push({
|
|
316
|
-
...rule,
|
|
317
|
-
description,
|
|
318
|
-
configStatus: status,
|
|
319
|
-
isActive: status === "error" || status === "warn",
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
const categories = Object.keys(map).toSorted();
|
|
323
|
-
return {
|
|
324
|
-
categories,
|
|
325
|
-
rulesByCategory: map,
|
|
326
|
-
config,
|
|
327
|
-
configPath,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
function updateScroll(idx, currentScroll, viewHeight) {
|
|
331
|
-
if (idx < currentScroll)
|
|
332
|
-
return idx;
|
|
333
|
-
if (idx >= currentScroll + viewHeight)
|
|
334
|
-
return idx - viewHeight + 1;
|
|
335
|
-
return currentScroll;
|
|
336
|
-
}
|
|
337
|
-
function openUrl(url) {
|
|
338
|
-
if (!url)
|
|
339
|
-
return;
|
|
340
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "explorer" : "xdg-open";
|
|
341
|
-
const process = spawn(cmd, [url], {
|
|
342
|
-
detached: true,
|
|
343
|
-
stdio: "ignore",
|
|
344
|
-
});
|
|
345
|
-
process.unref();
|
|
346
|
-
}
|
|
347
|
-
const write = (str) => stdout.write(str);
|
|
348
|
-
const enterAltScreen = () => write("\x1b[?1049h\x1b[?25l");
|
|
349
|
-
const exitAltScreen = () => write("\x1b[?1049l\x1b[?25h");
|
|
350
|
-
readline.emitKeypressEvents(stdin);
|
|
351
|
-
if (stdin.isTTY)
|
|
352
|
-
stdin.setRawMode(true);
|
|
353
|
-
stdin.on("keypress", (_, key) => {
|
|
354
|
-
const action = KEY_MAP[key.name] ||
|
|
355
|
-
(key.ctrl && key.name === "c" ? { type: "EXIT" } : KEY_MAP[key.sequence] || null);
|
|
356
|
-
execute(action);
|
|
357
|
-
});
|
|
358
|
-
stdout.on("resize", render);
|
|
359
|
-
enterAltScreen();
|
|
360
|
-
render();
|
package/dist/rendering.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { state } from "./index.js";
|
|
2
|
-
import { stdout } from "node:process";
|
|
3
|
-
export const COLORS = {
|
|
4
|
-
reset: "\x1b[0m",
|
|
5
|
-
dim: "\x1b[38;5;242m",
|
|
6
|
-
highlight: "\x1b[38;5;111m",
|
|
7
|
-
selectedBg: "\x1b[48;5;24m\x1b[38;5;255m\x1b[1m",
|
|
8
|
-
borderActive: "\x1b[38;5;111m",
|
|
9
|
-
borderInactive: "\x1b[38;5;237m",
|
|
10
|
-
error: "\x1b[38;5;203m",
|
|
11
|
-
warn: "\x1b[38;5;215m",
|
|
12
|
-
success: "\x1b[38;5;114m",
|
|
13
|
-
info: "\x1b[38;5;75m",
|
|
14
|
-
};
|
|
15
|
-
function chunkString(str, len) {
|
|
16
|
-
if (!str)
|
|
17
|
-
return [];
|
|
18
|
-
const size = Math.ceil(str.length / len);
|
|
19
|
-
const r = Array(size);
|
|
20
|
-
for (let i = 0; i < size; i++)
|
|
21
|
-
r[i] = str.substring(i * len, (i + 1) * len);
|
|
22
|
-
return r;
|
|
23
|
-
}
|
|
24
|
-
function drawBox(buffer, x, y, width, height, title, items, selectedIndex, scrollOffset, isActive) {
|
|
25
|
-
const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
|
|
26
|
-
const titleClean = title.length > width - 6 ? title.substring(0, width - 7) + "…" : title;
|
|
27
|
-
const topBorder = `${borderColor}┌─ ${titleClean} `.padEnd(width + borderColor.length - 1, "─");
|
|
28
|
-
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
29
|
-
for (let i = 1; i < height - 1; i++) {
|
|
30
|
-
buffer.push(`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`);
|
|
31
|
-
}
|
|
32
|
-
buffer.push(`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`);
|
|
33
|
-
const innerHeight = height - 2;
|
|
34
|
-
items.slice(scrollOffset, scrollOffset + innerHeight).forEach((item, i) => {
|
|
35
|
-
const absoluteIndex = scrollOffset + i;
|
|
36
|
-
const isRule = typeof item !== "string";
|
|
37
|
-
const rawText = isRule ? item.value : item;
|
|
38
|
-
let display = rawText.length > width - 4
|
|
39
|
-
? rawText.substring(0, width - 5) + "…"
|
|
40
|
-
: rawText.padEnd(width - 4);
|
|
41
|
-
let itemColor = COLORS.dim;
|
|
42
|
-
if (isRule) {
|
|
43
|
-
const ruleItem = item;
|
|
44
|
-
if (ruleItem.configStatus === "error")
|
|
45
|
-
itemColor = COLORS.error;
|
|
46
|
-
else if (ruleItem.configStatus === "warn")
|
|
47
|
-
itemColor = COLORS.warn;
|
|
48
|
-
else if (ruleItem.isActive)
|
|
49
|
-
itemColor = COLORS.success;
|
|
50
|
-
}
|
|
51
|
-
buffer.push(`\x1b[${y + 1 + i};${x + 2}H`);
|
|
52
|
-
if (absoluteIndex === selectedIndex) {
|
|
53
|
-
buffer.push(isActive
|
|
54
|
-
? `${COLORS.selectedBg}${display}${COLORS.reset}`
|
|
55
|
-
: `${COLORS.dim}\x1b[7m${display}${COLORS.reset}`);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
buffer.push(`${itemColor}${display}${COLORS.reset}`);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function drawStats(buffer, x, y, width, height, rules) {
|
|
63
|
-
const borderColor = COLORS.borderInactive;
|
|
64
|
-
const topBorder = `${borderColor}┌─ STATS `.padEnd(width + borderColor.length - 1, "─");
|
|
65
|
-
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
66
|
-
for (let i = 1; i < height - 1; i++)
|
|
67
|
-
buffer.push(`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`);
|
|
68
|
-
buffer.push(`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`);
|
|
69
|
-
let counts = { error: 0, warn: 0, off: 0 };
|
|
70
|
-
rules.forEach((ruleItem) => {
|
|
71
|
-
if (ruleItem.configStatus === "error")
|
|
72
|
-
counts.error++;
|
|
73
|
-
else if (ruleItem.configStatus === "warn")
|
|
74
|
-
counts.warn++;
|
|
75
|
-
else
|
|
76
|
-
counts.off++;
|
|
77
|
-
});
|
|
78
|
-
const lines = [
|
|
79
|
-
{ label: "Error", count: counts.error, color: COLORS.error },
|
|
80
|
-
{ label: "Warn", count: counts.warn, color: COLORS.warn },
|
|
81
|
-
{ label: "Off", count: counts.off, color: COLORS.dim },
|
|
82
|
-
];
|
|
83
|
-
lines.forEach((line, i) => {
|
|
84
|
-
if (i < height - 2) {
|
|
85
|
-
const numStr = String(line.count).padStart(3);
|
|
86
|
-
const labelStr = line.label.padEnd(width - 8);
|
|
87
|
-
buffer.push(`\x1b[${y + 1 + i};${x + 2}H${line.color}${labelStr}${numStr}${COLORS.reset}`);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
function drawDetails(buffer, x, y, width, height, rule, isActive) {
|
|
92
|
-
const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
|
|
93
|
-
const topBorder = `${borderColor}┌─ DETAILS `.padEnd(width + borderColor.length - 1, "─");
|
|
94
|
-
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
95
|
-
for (let i = 1; i < height - 1; i++)
|
|
96
|
-
buffer.push(`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`);
|
|
97
|
-
buffer.push(`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`);
|
|
98
|
-
if (!rule)
|
|
99
|
-
return;
|
|
100
|
-
let statusDisplay = rule.configStatus.toUpperCase();
|
|
101
|
-
if (rule.configStatus === "error")
|
|
102
|
-
statusDisplay = `${COLORS.error}${statusDisplay}${COLORS.reset}`;
|
|
103
|
-
else if (rule.configStatus === "warn")
|
|
104
|
-
statusDisplay = `${COLORS.warn}${statusDisplay}${COLORS.reset}`;
|
|
105
|
-
else
|
|
106
|
-
statusDisplay = `${COLORS.dim}${statusDisplay}${COLORS.reset}`;
|
|
107
|
-
const metadata = [
|
|
108
|
-
["Name", rule.value],
|
|
109
|
-
["Status", statusDisplay],
|
|
110
|
-
["Category", rule.category],
|
|
111
|
-
["Scope", rule.scope],
|
|
112
|
-
["Fix", rule.fix || "N/A"],
|
|
113
|
-
["Default", rule.default ? "Yes" : "No"],
|
|
114
|
-
["Type-aware", rule.type_aware ? "Yes" : "No"],
|
|
115
|
-
];
|
|
116
|
-
let line = 0;
|
|
117
|
-
metadata.forEach(([lbl, val]) => {
|
|
118
|
-
if (line < height - 2) {
|
|
119
|
-
buffer.push(`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(12)} ${COLORS.reset}${val}`);
|
|
120
|
-
line++;
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
if (line < height - 3)
|
|
124
|
-
line++;
|
|
125
|
-
if (line < height - 2) {
|
|
126
|
-
buffer.push(`\x1b[${y + 1 + line};${x + 2}H${COLORS.reset}Description:${COLORS.reset}`);
|
|
127
|
-
line++;
|
|
128
|
-
const cleanDesc = (rule.description ?? "N/A").replace(/\s+/g, " ").trim();
|
|
129
|
-
const chunks = chunkString(cleanDesc, width - 6);
|
|
130
|
-
chunks.forEach((chunk) => {
|
|
131
|
-
if (line < height - 2) {
|
|
132
|
-
buffer.push(`\x1b[${y + 1 + line};${x + 2}H${COLORS.dim}${chunk}${COLORS.reset}`);
|
|
133
|
-
line++;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
const footerLine = Math.max(line + 1, height - 3);
|
|
138
|
-
if (footerLine < height - 1) {
|
|
139
|
-
buffer.push(`\x1b[${y + 1 + footerLine};${x + 2}HHit ${COLORS.highlight}ENTER${COLORS.reset} to open docs`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
export function render() {
|
|
143
|
-
if (!state || !state.categories) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const { columns = 80, rows = 24 } = stdout;
|
|
147
|
-
const currentCategory = state.categories[state.selectedCategoryIndex];
|
|
148
|
-
const rules = state.rulesByCategory[currentCategory] || [];
|
|
149
|
-
const rule = rules[state.selectedRuleIndex];
|
|
150
|
-
const boxHeight = rows - 5;
|
|
151
|
-
const categoriesColumnWidth = Math.floor(columns * 0.2);
|
|
152
|
-
const rulesColumnWidth = Math.floor(columns * 0.3);
|
|
153
|
-
const detailsColumnWidth = columns - categoriesColumnWidth - rulesColumnWidth - 2;
|
|
154
|
-
const statsHeight = 6;
|
|
155
|
-
const categoryListHeight = boxHeight - statsHeight;
|
|
156
|
-
const buffer = ["\x1b[H\x1b[J"];
|
|
157
|
-
drawBox(buffer, 1, 1, categoriesColumnWidth, categoryListHeight, "CATEGORIES", state.categories, state.selectedCategoryIndex, state.categoryScroll, state.activePane === 0);
|
|
158
|
-
drawStats(buffer, 1, 1 + categoryListHeight, categoriesColumnWidth, statsHeight, rules);
|
|
159
|
-
drawBox(buffer, categoriesColumnWidth + 1, 1, rulesColumnWidth, boxHeight, `RULES (${rules.length})`, rules, state.selectedRuleIndex, state.ruleScroll, state.activePane === 1);
|
|
160
|
-
drawDetails(buffer, categoriesColumnWidth + rulesColumnWidth + 1, 1, detailsColumnWidth, boxHeight, rule, state.activePane === 2);
|
|
161
|
-
const msgColor = COLORS[state.messageType] || COLORS.reset;
|
|
162
|
-
buffer.push(`\x1b[${rows - 3};2H${msgColor}● ${state.message}${COLORS.reset}`);
|
|
163
|
-
const footerConfig = state.configPath ? `Config: ${state.configPath}` : "No config loaded";
|
|
164
|
-
buffer.push(`\x1b[${rows - 1};2H${COLORS.dim}Arrows/HJKL: Nav | 1-3: Status | R: Lint | X: Run rule | Enter: Docs | Q: Quit | ${footerConfig}${COLORS.reset}`);
|
|
165
|
-
stdout.write(buffer.join(""));
|
|
166
|
-
}
|