leet-tui 0.1.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 +149 -0
- package/bin/leet-tui +45 -0
- package/package.json +37 -0
- package/scripts/postinstall.js +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# LeetCode TUI
|
|
2
|
+
|
|
3
|
+
A terminal user interface (TUI) application for solving LeetCode problems with an embedded Neovim editor.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Home page with problem list**: Browse all available LeetCode problems with difficulty indicators
|
|
8
|
+
- **Split-screen interface**: 1/3 for problem description, 2/3 for Neovim editor
|
|
9
|
+
- **Automatic boilerplate generation**: Each problem gets custom boilerplate code and test cases
|
|
10
|
+
- **Run tests locally**: Press Ctrl+R to run your solution against test cases with instant feedback
|
|
11
|
+
- **Color-coded results**: Test output shows PASSED in green, FAILED in red
|
|
12
|
+
- **Embedded Neovim**: Full Neovim functionality within the TUI
|
|
13
|
+
- **JavaScript solutions**: Default language is JavaScript (other languages coming soon)
|
|
14
|
+
- **Organized solution files**: Solutions saved to `~/.local/share/leet-tui/solutions/`
|
|
15
|
+
- **Proper window sizing**: Neovim automatically fits the available space
|
|
16
|
+
- **Focus switching**: Toggle between question pane and editor pane
|
|
17
|
+
- **Scrollable question view**: Navigate through problem descriptions
|
|
18
|
+
- **Proper input forwarding**: All keyboard shortcuts work in Neovim (arrows, Ctrl combinations, etc.)
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
The application uses:
|
|
23
|
+
- **ratatui**: Terminal UI framework
|
|
24
|
+
- **portable-pty**: PTY system for spawning Neovim
|
|
25
|
+
- **vt100**: Terminal emulator parser
|
|
26
|
+
- **tui-term**: Widget for rendering terminal output
|
|
27
|
+
- **crossterm**: Cross-platform terminal manipulation
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### Via npm (recommended)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g leet-tui
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will automatically download the correct binary for your platform.
|
|
38
|
+
|
|
39
|
+
### From source
|
|
40
|
+
|
|
41
|
+
Prerequisites:
|
|
42
|
+
- Rust (1.70+)
|
|
43
|
+
- Neovim installed and available in PATH
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cargo build --release
|
|
47
|
+
./target/release/leet-tui
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Running
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
leet-tui
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Keyboard Shortcuts
|
|
57
|
+
|
|
58
|
+
### Home Page
|
|
59
|
+
- **Up/Down**: Navigate problem list
|
|
60
|
+
- **Enter**: Select a problem
|
|
61
|
+
- **Ctrl+C**: Quit application
|
|
62
|
+
|
|
63
|
+
### Question View
|
|
64
|
+
- **Ctrl+R**: Save file and run tests (auto-saves before testing)
|
|
65
|
+
- **Ctrl+Q**: Switch focus between question pane and editor pane
|
|
66
|
+
- **Ctrl+H**: Back to home page
|
|
67
|
+
- **Ctrl+C**: Quit application
|
|
68
|
+
- **Esc**: Close test results popup
|
|
69
|
+
- **Up/Down**: Scroll question (when question pane is focused)
|
|
70
|
+
- **PageUp/PageDown**: Fast scroll question (when question pane is focused)
|
|
71
|
+
- **Home**: Jump to top of question (when question pane is focused)
|
|
72
|
+
- **All Neovim shortcuts**: Work when editor pane is focused
|
|
73
|
+
|
|
74
|
+
## Project Structure
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
src/
|
|
78
|
+
├── main.rs # Main application logic and UI rendering
|
|
79
|
+
├── pty.rs # PTY manager for Neovim integration
|
|
80
|
+
├── input.rs # Keyboard input handling and ANSI escape sequence mapping
|
|
81
|
+
└── leetcode.rs # LeetCode API integration (currently with sample data)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
### PTY Integration
|
|
87
|
+
|
|
88
|
+
The application spawns Neovim in a pseudo-terminal (PTY) and renders its output in a Ratatui widget. A background thread continuously reads from the PTY and updates a VT100 parser, which maintains the terminal state.
|
|
89
|
+
|
|
90
|
+
### Input Forwarding
|
|
91
|
+
|
|
92
|
+
When the editor pane is focused, all keyboard events are converted to ANSI escape sequences and sent to the PTY. This includes:
|
|
93
|
+
- Regular characters
|
|
94
|
+
- Control combinations (Ctrl+A, Ctrl+W, etc.)
|
|
95
|
+
- Arrow keys and function keys
|
|
96
|
+
- Special keys (Home, End, PageUp, PageDown, etc.)
|
|
97
|
+
|
|
98
|
+
### Focus Management
|
|
99
|
+
|
|
100
|
+
The application maintains a focus state that determines which pane receives input:
|
|
101
|
+
- **Question pane**: Arrow keys scroll the problem description
|
|
102
|
+
- **Editor pane**: All input is forwarded to Neovim
|
|
103
|
+
|
|
104
|
+
## Extending
|
|
105
|
+
|
|
106
|
+
### Adding Real LeetCode API Integration
|
|
107
|
+
|
|
108
|
+
Currently, the app displays a sample problem. To integrate with the real LeetCode API:
|
|
109
|
+
|
|
110
|
+
1. Update `src/leetcode.rs` to make GraphQL requests to LeetCode
|
|
111
|
+
2. Add authentication support
|
|
112
|
+
3. Implement problem selection UI
|
|
113
|
+
|
|
114
|
+
### Customizing Neovim
|
|
115
|
+
|
|
116
|
+
You can customize the Neovim instance by:
|
|
117
|
+
- Setting environment variables (e.g., `NVIM_APPNAME` for different configs)
|
|
118
|
+
- Passing command-line arguments in `src/pty.rs:25`
|
|
119
|
+
- Creating a custom init file for the TUI context
|
|
120
|
+
|
|
121
|
+
The app currently opens `solution.js` by default. To support other languages, modify the file extension in `src/pty.rs:25`.
|
|
122
|
+
|
|
123
|
+
## Troubleshooting
|
|
124
|
+
|
|
125
|
+
### Keys not working in Neovim
|
|
126
|
+
|
|
127
|
+
If certain key combinations don't work, you may need to add them to the `key_to_bytes` function in `src/input.rs`.
|
|
128
|
+
|
|
129
|
+
### Terminal size issues
|
|
130
|
+
|
|
131
|
+
The application handles resize events and updates the PTY size accordingly. If you experience rendering issues, try resizing your terminal.
|
|
132
|
+
|
|
133
|
+
### Neovim colors look wrong
|
|
134
|
+
|
|
135
|
+
The VT100 parser supports basic colors. If you use a complex Neovim theme, some colors may not render perfectly in the embedded terminal.
|
|
136
|
+
|
|
137
|
+
## Future Enhancements
|
|
138
|
+
|
|
139
|
+
- [ ] Real LeetCode API integration
|
|
140
|
+
- [ ] Problem selection menu
|
|
141
|
+
- [ ] Multiple test case support
|
|
142
|
+
- [ ] Submit solution functionality
|
|
143
|
+
- [ ] History of attempted problems
|
|
144
|
+
- [ ] Custom Neovim configuration per problem type
|
|
145
|
+
- [ ] Split terminal for running tests
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/bin/leet-tui
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execFileSync, spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const binDir = __dirname;
|
|
8
|
+
const binName = process.platform === 'win32' ? 'leet-tui.exe' : 'leet-tui-binary';
|
|
9
|
+
const binaryPath = path.join(binDir, binName);
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(binaryPath)) {
|
|
12
|
+
console.error('[leet-tui] Binary not found. Running postinstall...');
|
|
13
|
+
try {
|
|
14
|
+
execFileSync('node', [path.join(__dirname, '..', 'scripts', 'postinstall.js')], {
|
|
15
|
+
stdio: 'inherit',
|
|
16
|
+
});
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('[leet-tui] Failed to install binary.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(binaryPath)) {
|
|
24
|
+
console.error('[leet-tui] Binary still not found after postinstall.');
|
|
25
|
+
console.error('Please try reinstalling: npm install -g leet-tui');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use spawn with inherit to properly handle TTY
|
|
30
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on('error', (err) => {
|
|
36
|
+
console.error(`[leet-tui] Failed to start: ${err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
child.on('exit', (code, signal) => {
|
|
41
|
+
if (signal) {
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
process.exit(code ?? 0);
|
|
45
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "leet-tui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A terminal UI for practicing LeetCode Blind 75 problems with embedded Neovim",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/trevor-ofarrell/leet-tui"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/trevor-ofarrell/leet-tui#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/trevor-ofarrell/leet-tui/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"leetcode",
|
|
16
|
+
"tui",
|
|
17
|
+
"terminal",
|
|
18
|
+
"neovim",
|
|
19
|
+
"coding",
|
|
20
|
+
"interview",
|
|
21
|
+
"practice",
|
|
22
|
+
"blind75"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"leet-tui": "bin/leet-tui"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"scripts"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"postinstall": "node scripts/postinstall.js"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const REPO = 'trevor-ofarrell/leet-tui';
|
|
9
|
+
const VERSION = require('../package.json').version;
|
|
10
|
+
|
|
11
|
+
const PLATFORMS = {
|
|
12
|
+
'darwin-arm64': 'leet-tui-darwin-arm64',
|
|
13
|
+
'darwin-x64': 'leet-tui-darwin-x64',
|
|
14
|
+
'linux-x64': 'leet-tui-linux-x64',
|
|
15
|
+
'linux-arm64': 'leet-tui-linux-arm64',
|
|
16
|
+
'win32-x64': 'leet-tui-win32-x64.exe',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
20
|
+
const binaryName = PLATFORMS[platformKey];
|
|
21
|
+
|
|
22
|
+
if (!binaryName) {
|
|
23
|
+
console.warn(`\n[leet-tui] No prebuilt binary for ${platformKey}`);
|
|
24
|
+
console.warn(`Supported: ${Object.keys(PLATFORMS).join(', ')}`);
|
|
25
|
+
console.warn('Build from source: https://github.com/' + REPO + '\n');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const binDir = path.join(__dirname, '..', 'bin');
|
|
30
|
+
const binPath = path.join(binDir, process.platform === 'win32' ? 'leet-tui.exe' : 'leet-tui-binary');
|
|
31
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`;
|
|
32
|
+
|
|
33
|
+
function download(url, dest) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const file = fs.createWriteStream(dest);
|
|
36
|
+
|
|
37
|
+
https.get(url, (response) => {
|
|
38
|
+
// Handle redirects (GitHub releases redirect to S3)
|
|
39
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
40
|
+
file.close();
|
|
41
|
+
fs.unlinkSync(dest);
|
|
42
|
+
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (response.statusCode !== 200) {
|
|
46
|
+
file.close();
|
|
47
|
+
fs.unlinkSync(dest);
|
|
48
|
+
reject(new Error(`Download failed: HTTP ${response.statusCode}`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const total = parseInt(response.headers['content-length'], 10);
|
|
53
|
+
let downloaded = 0;
|
|
54
|
+
|
|
55
|
+
response.on('data', (chunk) => {
|
|
56
|
+
downloaded += chunk.length;
|
|
57
|
+
if (total) {
|
|
58
|
+
const pct = Math.round((downloaded / total) * 100);
|
|
59
|
+
process.stdout.write(`\r[leet-tui] Downloading... ${pct}%`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
response.pipe(file);
|
|
64
|
+
|
|
65
|
+
file.on('finish', () => {
|
|
66
|
+
file.close();
|
|
67
|
+
console.log('\r[leet-tui] Download complete. ');
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
}).on('error', (err) => {
|
|
71
|
+
fs.unlink(dest, () => {});
|
|
72
|
+
reject(err);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function main() {
|
|
78
|
+
console.log(`[leet-tui] Installing binary for ${platformKey}...`);
|
|
79
|
+
|
|
80
|
+
// Ensure bin directory exists
|
|
81
|
+
if (!fs.existsSync(binDir)) {
|
|
82
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await download(url, binPath);
|
|
87
|
+
|
|
88
|
+
// Make executable on Unix
|
|
89
|
+
if (process.platform !== 'win32') {
|
|
90
|
+
fs.chmodSync(binPath, 0o755);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('[leet-tui] Installation successful!');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`\n[leet-tui] Failed to download binary: ${err.message}`);
|
|
96
|
+
console.error(`URL: ${url}`);
|
|
97
|
+
console.error('\nYou can build from source:');
|
|
98
|
+
console.error(' git clone https://github.com/' + REPO);
|
|
99
|
+
console.error(' cd leet-tui && cargo build --release');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main();
|