@beast01/tcurl 1.0.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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/app.js +170 -0
- package/dist/cli.js +81 -0
- package/dist/components/Header.js +13 -0
- package/dist/components/Help.js +53 -0
- package/dist/components/RequestEditor.js +276 -0
- package/dist/components/RequestList.js +49 -0
- package/dist/components/ResponseViewer.js +83 -0
- package/dist/components/Spinner.js +13 -0
- package/dist/components/StatusBar.js +8 -0
- package/dist/components/TextEditor.js +90 -0
- package/dist/http/client.js +190 -0
- package/dist/lib/curlgen.js +40 -0
- package/dist/lib/format.js +41 -0
- package/dist/lib/id.js +4 -0
- package/dist/lib/kv.js +54 -0
- package/dist/storage.js +78 -0
- package/dist/theme.js +73 -0
- package/dist/types.js +9 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# tcurl
|
|
2
|
+
|
|
3
|
+
An interactive terminal UI (TUI) for building, sending, and saving HTTP
|
|
4
|
+
requests — **curl, but comfortable.** Create requests of any method
|
|
5
|
+
(`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`), tweak headers,
|
|
6
|
+
query params, body and auth, fire them off, and browse the response — all
|
|
7
|
+
without leaving your terminal. Requests are saved to disk so you can reuse
|
|
8
|
+
them later.
|
|
9
|
+
|
|
10
|
+
Styled with the **Gruvbox** and **Ember** color themes. Works on macOS,
|
|
11
|
+
Linux, and Windows (any terminal with a TTY). Zero native dependencies.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌ tcurl — interactive HTTP client ──────────────── gruvbox ┐
|
|
15
|
+
│ ┌ Saved requests (2) ──┐ ┌ GET Get users ──────────────┐ │
|
|
16
|
+
│ │ ❯ GET Get users │ │ GET https://api.site/users │ │
|
|
17
|
+
│ │ POST Create user │ │ Auth bearer │ │
|
|
18
|
+
│ └──────────────────────┘ │ Headers 2 │ │
|
|
19
|
+
│ └──────────────────────────────┘ │
|
|
20
|
+
│ n new ↵ edit s send d delete y curl ? help │
|
|
21
|
+
└──────────────────────────────────────────────────────────┘
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g tcurl
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires **Node.js ≥ 18**.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
tcurl # launch the interactive TUI
|
|
36
|
+
tcurl https://api.example.com # launch with a new request prefilled
|
|
37
|
+
tcurl -X POST https://api.example.com/items
|
|
38
|
+
tcurl --theme ember # start with the Ember theme
|
|
39
|
+
tcurl --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Keyboard
|
|
43
|
+
|
|
44
|
+
**List view**
|
|
45
|
+
|
|
46
|
+
| Key | Action |
|
|
47
|
+
| ------------ | ------------------------------- |
|
|
48
|
+
| `↑` / `↓` | Move selection |
|
|
49
|
+
| `n` | New request |
|
|
50
|
+
| `Enter` / `e`| Edit request |
|
|
51
|
+
| `s` | Send request |
|
|
52
|
+
| `d` | Delete request |
|
|
53
|
+
| `y` | Copy as `curl` (prints on exit) |
|
|
54
|
+
| `t` | Cycle theme (gruvbox / ember) |
|
|
55
|
+
| `?` | Help |
|
|
56
|
+
| `q` / `Ctrl+C` | Quit |
|
|
57
|
+
|
|
58
|
+
**Editor**
|
|
59
|
+
|
|
60
|
+
| Key | Action |
|
|
61
|
+
| ------------------ | -------------------------------------------- |
|
|
62
|
+
| `↑` / `↓` / `Tab` | Move between fields |
|
|
63
|
+
| `←` / `→` / `Space`| Change method, body type, toggles |
|
|
64
|
+
| `Enter` | Edit the focused field |
|
|
65
|
+
| `Ctrl+S` | Save a multi-line field (body, headers…) |
|
|
66
|
+
| `Esc` | Back to list (auto-saves) |
|
|
67
|
+
|
|
68
|
+
**Response**
|
|
69
|
+
|
|
70
|
+
| Key | Action |
|
|
71
|
+
| --------------- | ------------------- |
|
|
72
|
+
| `↑`/`↓`/`j`/`k` | Scroll |
|
|
73
|
+
| `PgUp`/`PgDn` | Page scroll |
|
|
74
|
+
| `g` / `G` | Jump to top/bottom |
|
|
75
|
+
| `Tab` / `h` | Body ↔ Headers |
|
|
76
|
+
| `Esc` | Back |
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
- **All HTTP methods** — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.
|
|
81
|
+
- **Body modes** — none, raw text, JSON (auto content-type), and
|
|
82
|
+
URL-encoded form (`key=value` per line).
|
|
83
|
+
- **Headers & query params** — edited as simple `Key: Value` / `key=value`
|
|
84
|
+
lines.
|
|
85
|
+
- **Auth** — Bearer token or HTTP Basic (username / password).
|
|
86
|
+
- **Redirects** — follow (with method downgrade on 301/302/303) or manual.
|
|
87
|
+
- **TLS control** — toggle certificate verification for self-signed hosts.
|
|
88
|
+
- **Timeouts** — per-request, in milliseconds.
|
|
89
|
+
- **Pretty responses** — JSON is auto-formatted; scrollable body & headers
|
|
90
|
+
with status, timing, and size.
|
|
91
|
+
- **Persistence** — requests are stored as JSON and reused across sessions.
|
|
92
|
+
- **Export** — copy any saved request as an equivalent `curl` command.
|
|
93
|
+
|
|
94
|
+
## Where is my data?
|
|
95
|
+
|
|
96
|
+
Saved requests and your theme choice live in a single JSON file under your
|
|
97
|
+
platform's config directory (via [`env-paths`](https://www.npmjs.com/package/env-paths)):
|
|
98
|
+
|
|
99
|
+
- Linux: `~/.config/tcurl/store.json`
|
|
100
|
+
- macOS: `~/Library/Preferences/tcurl/store.json`
|
|
101
|
+
- Windows: `%APPDATA%\tcurl\Config\store.json`
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install
|
|
107
|
+
npm run build # compile TypeScript to dist/
|
|
108
|
+
npm run dev # watch-compile
|
|
109
|
+
node dist/cli.js # run locally
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
3
|
+
import { getTheme, themes } from './theme.js';
|
|
4
|
+
import { defaultRequest, saveStore } from './storage.js';
|
|
5
|
+
import { executeRequest } from './http/client.js';
|
|
6
|
+
import { toCurl } from './lib/curlgen.js';
|
|
7
|
+
import { Header } from './components/Header.js';
|
|
8
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
9
|
+
import { RequestList } from './components/RequestList.js';
|
|
10
|
+
import { RequestEditor } from './components/RequestEditor.js';
|
|
11
|
+
import { ResponseViewer } from './components/ResponseViewer.js';
|
|
12
|
+
import { Help } from './components/Help.js';
|
|
13
|
+
import { Spinner } from './components/Spinner.js';
|
|
14
|
+
function useDimensions() {
|
|
15
|
+
const { stdout } = useStdout();
|
|
16
|
+
const [dims, setDims] = useState({
|
|
17
|
+
cols: stdout?.columns ?? 80,
|
|
18
|
+
rows: stdout?.rows ?? 24,
|
|
19
|
+
});
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!stdout)
|
|
22
|
+
return;
|
|
23
|
+
const onResize = () => setDims({ cols: stdout.columns, rows: stdout.rows });
|
|
24
|
+
stdout.on('resize', onResize);
|
|
25
|
+
return () => {
|
|
26
|
+
stdout.off('resize', onResize);
|
|
27
|
+
};
|
|
28
|
+
}, [stdout]);
|
|
29
|
+
return dims;
|
|
30
|
+
}
|
|
31
|
+
export function App({ initialStore, exitState, }) {
|
|
32
|
+
const { exit } = useApp();
|
|
33
|
+
const { cols, rows } = useDimensions();
|
|
34
|
+
const [store, setStore] = useState(initialStore);
|
|
35
|
+
const [selected, setSelected] = useState(0);
|
|
36
|
+
const [mode, setMode] = useState('list');
|
|
37
|
+
const [working, setWorking] = useState(null);
|
|
38
|
+
const [response, setResponse] = useState(null);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [flash, setFlash] = useState(null);
|
|
41
|
+
const theme = getTheme(store.theme);
|
|
42
|
+
const requests = store.requests;
|
|
43
|
+
function persist(next) {
|
|
44
|
+
setStore(next);
|
|
45
|
+
saveStore(next).catch(() => {
|
|
46
|
+
/* non-fatal: keep running even if disk write fails */
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function flashMessage(msg) {
|
|
50
|
+
setFlash(msg);
|
|
51
|
+
setTimeout(() => setFlash(null), 2500);
|
|
52
|
+
}
|
|
53
|
+
function upsertWorking(r) {
|
|
54
|
+
const idx = requests.findIndex((x) => x.id === r.id);
|
|
55
|
+
const nextRequests = idx === -1 ? [...requests, r] : requests.map((x) => (x.id === r.id ? r : x));
|
|
56
|
+
persist({ ...store, requests: nextRequests });
|
|
57
|
+
}
|
|
58
|
+
async function send(config) {
|
|
59
|
+
if (!config.url.trim()) {
|
|
60
|
+
flashMessage('Set a URL first');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setLoading(true);
|
|
64
|
+
setResponse(null);
|
|
65
|
+
const result = await executeRequest(config);
|
|
66
|
+
setResponse(result);
|
|
67
|
+
setLoading(false);
|
|
68
|
+
setMode('response');
|
|
69
|
+
}
|
|
70
|
+
// Global keys — only in list mode and when idle.
|
|
71
|
+
useInput((input, key) => {
|
|
72
|
+
if (key.ctrl && input === 'c') {
|
|
73
|
+
exit();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (loading)
|
|
77
|
+
return;
|
|
78
|
+
if (input === '?') {
|
|
79
|
+
setMode('help');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (input === 'q') {
|
|
83
|
+
exit();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (input === 't') {
|
|
87
|
+
const names = Object.keys(themes);
|
|
88
|
+
const next = names[(names.indexOf(store.theme) + 1) % names.length];
|
|
89
|
+
persist({ ...store, theme: next });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (key.upArrow || input === 'k') {
|
|
93
|
+
setSelected((s) => Math.max(0, s - 1));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (key.downArrow || input === 'j') {
|
|
97
|
+
setSelected((s) => Math.min(requests.length - 1, s + 1));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (input === 'n') {
|
|
101
|
+
const r = defaultRequest();
|
|
102
|
+
setWorking(r);
|
|
103
|
+
setMode('editor');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if ((key.return || input === 'e') && requests[selected]) {
|
|
107
|
+
setWorking({ ...requests[selected] });
|
|
108
|
+
setMode('editor');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (input === 's' && requests[selected]) {
|
|
112
|
+
send(requests[selected]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (input === 'd' && requests[selected]) {
|
|
116
|
+
const next = requests.filter((_, i) => i !== selected);
|
|
117
|
+
persist({ ...store, requests: next });
|
|
118
|
+
setSelected((s) => Math.max(0, Math.min(s, next.length - 1)));
|
|
119
|
+
flashMessage('Deleted');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (input === 'y' && requests[selected]) {
|
|
123
|
+
exitState.curl = toCurl(requests[selected]);
|
|
124
|
+
flashMessage('curl command will print on exit');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}, { isActive: mode === 'list' && !loading });
|
|
128
|
+
// Editor callbacks
|
|
129
|
+
function onEditorChange(r) {
|
|
130
|
+
setWorking(r);
|
|
131
|
+
}
|
|
132
|
+
function onEditorBack() {
|
|
133
|
+
if (working) {
|
|
134
|
+
upsertWorking(working);
|
|
135
|
+
const idx = requests.findIndex((x) => x.id === working.id);
|
|
136
|
+
if (idx !== -1)
|
|
137
|
+
setSelected(idx);
|
|
138
|
+
else
|
|
139
|
+
setSelected(requests.length); // newly added goes last
|
|
140
|
+
}
|
|
141
|
+
setWorking(null);
|
|
142
|
+
setMode('list');
|
|
143
|
+
}
|
|
144
|
+
function onEditorSend() {
|
|
145
|
+
if (working) {
|
|
146
|
+
upsertWorking(working);
|
|
147
|
+
send(working);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const statusHints = mode === 'list'
|
|
151
|
+
? [
|
|
152
|
+
{ key: 'n', label: 'new' },
|
|
153
|
+
{ key: '↵', label: 'edit' },
|
|
154
|
+
{ key: 's', label: 'send' },
|
|
155
|
+
{ key: 'd', label: 'delete' },
|
|
156
|
+
{ key: 'y', label: 'curl' },
|
|
157
|
+
{ key: 't', label: 'theme' },
|
|
158
|
+
{ key: '?', label: 'help' },
|
|
159
|
+
{ key: 'q', label: 'quit' },
|
|
160
|
+
]
|
|
161
|
+
: [];
|
|
162
|
+
return (React.createElement(Box, { flexDirection: "column", width: cols, minHeight: rows - 1 },
|
|
163
|
+
React.createElement(Header, { theme: theme, subtitle: mode === 'list' ? `${requests.length} saved · ` : '' }),
|
|
164
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", marginTop: 1 }, loading ? (React.createElement(Box, { flexGrow: 1, borderStyle: "round", borderColor: theme.border, paddingX: 1, alignItems: "center", justifyContent: "center" },
|
|
165
|
+
React.createElement(Spinner, { theme: theme, label: "Sending request\u2026" }))) : mode === 'help' ? (React.createElement(Help, { theme: theme, onClose: () => setMode('list') })) : mode === 'editor' && working ? (React.createElement(RequestEditor, { theme: theme, request: working, isActive: mode === 'editor', onChange: onEditorChange, onSend: onEditorSend, onBack: onEditorBack })) : mode === 'response' && response ? (React.createElement(ResponseViewer, { theme: theme, response: response, isActive: mode === 'response', onBack: () => setMode('list'), width: cols, height: rows })) : (React.createElement(RequestList, { theme: theme, requests: requests, selectedIndex: selected }))),
|
|
166
|
+
React.createElement(Box, { justifyContent: "space-between" },
|
|
167
|
+
React.createElement(StatusBar, { theme: theme, hints: statusHints }),
|
|
168
|
+
flash ? (React.createElement(Box, { paddingX: 1 },
|
|
169
|
+
React.createElement(Text, { color: theme.info }, flash))) : null)));
|
|
170
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { App } from './app.js';
|
|
5
|
+
import { loadStore, saveStore, storeLocation } from './storage.js';
|
|
6
|
+
const VERSION = '1.0.0';
|
|
7
|
+
const HELP = `
|
|
8
|
+
tcurl — an interactive terminal UI for HTTP requests.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
tcurl Launch the interactive TUI
|
|
12
|
+
tcurl [url] Launch and prefill a new request with [url]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
-X, --method <m> Prefill method (used with a url)
|
|
16
|
+
--theme <name> Start with a theme (gruvbox | ember)
|
|
17
|
+
-h, --help Show this help
|
|
18
|
+
-v, --version Show version
|
|
19
|
+
|
|
20
|
+
Inside the TUI, press ? for the full keyboard reference.
|
|
21
|
+
Saved requests live at:
|
|
22
|
+
${storeLocation()}
|
|
23
|
+
`;
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (a === '-X' || a === '--method') {
|
|
29
|
+
out.method = (argv[++i] ?? '').toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
else if (a === '--theme') {
|
|
32
|
+
out.theme = argv[++i];
|
|
33
|
+
}
|
|
34
|
+
else if (!a.startsWith('-')) {
|
|
35
|
+
out.url = a;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
async function main() {
|
|
41
|
+
const argv = process.argv.slice(2);
|
|
42
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
43
|
+
process.stdout.write(HELP + '\n');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (argv.includes('-v') || argv.includes('--version')) {
|
|
47
|
+
process.stdout.write(`tcurl ${VERSION}\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
51
|
+
process.stderr.write('tcurl requires an interactive terminal (TTY). Run it directly in your terminal.\n');
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const args = parseArgs(argv);
|
|
56
|
+
const store = await loadStore();
|
|
57
|
+
if (args.theme && (args.theme === 'gruvbox' || args.theme === 'ember')) {
|
|
58
|
+
store.theme = args.theme;
|
|
59
|
+
}
|
|
60
|
+
// Prefill a new request from CLI args and persist it so it appears in the list.
|
|
61
|
+
if (args.url) {
|
|
62
|
+
const { defaultRequest } = await import('./storage.js');
|
|
63
|
+
const r = defaultRequest({
|
|
64
|
+
url: args.url,
|
|
65
|
+
name: args.url,
|
|
66
|
+
method: args.method || 'GET',
|
|
67
|
+
});
|
|
68
|
+
store.requests.push(r);
|
|
69
|
+
await saveStore(store);
|
|
70
|
+
}
|
|
71
|
+
const exitState = { curl: null };
|
|
72
|
+
const { waitUntilExit } = render(React.createElement(App, { initialStore: store, exitState: exitState }), { exitOnCtrlC: false });
|
|
73
|
+
await waitUntilExit();
|
|
74
|
+
if (exitState.curl) {
|
|
75
|
+
process.stdout.write('\n' + exitState.curl + '\n');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
main().catch((err) => {
|
|
79
|
+
process.stderr.write(`tcurl: ${err?.stack ?? err}\n`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function Header({ theme, subtitle }) {
|
|
4
|
+
return (React.createElement(Box, { borderStyle: "round", borderColor: theme.border, paddingX: 1, justifyContent: "space-between" },
|
|
5
|
+
React.createElement(Text, null,
|
|
6
|
+
React.createElement(Text, { color: theme.accent, bold: true }, ' tcurl '),
|
|
7
|
+
React.createElement(Text, { color: theme.fgDim }, "\u2014 interactive HTTP client")),
|
|
8
|
+
React.createElement(Text, { color: theme.muted },
|
|
9
|
+
subtitle ?? '',
|
|
10
|
+
React.createElement(Text, { color: theme.info },
|
|
11
|
+
" ",
|
|
12
|
+
theme.name))));
|
|
13
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
const SECTIONS = [
|
|
4
|
+
{
|
|
5
|
+
title: 'List view',
|
|
6
|
+
rows: [
|
|
7
|
+
['↑ / ↓', 'Move selection'],
|
|
8
|
+
['n', 'New request'],
|
|
9
|
+
['Enter / e', 'Edit request'],
|
|
10
|
+
['s', 'Send request'],
|
|
11
|
+
['d', 'Delete request'],
|
|
12
|
+
['y', 'Copy as curl (prints on exit)'],
|
|
13
|
+
['t', 'Cycle theme (gruvbox / ember)'],
|
|
14
|
+
['? ', 'Toggle this help'],
|
|
15
|
+
['q / Ctrl+C', 'Quit'],
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: 'Editor',
|
|
20
|
+
rows: [
|
|
21
|
+
['↑ / ↓ / Tab', 'Move between fields'],
|
|
22
|
+
['← / → / Space', 'Change method, body type, toggles'],
|
|
23
|
+
['Enter', 'Edit the focused field'],
|
|
24
|
+
['Ctrl+S', 'Save multi-line field'],
|
|
25
|
+
['Esc', 'Back to list (auto-saves)'],
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: 'Response',
|
|
30
|
+
rows: [
|
|
31
|
+
['↑ / ↓ / j / k', 'Scroll'],
|
|
32
|
+
['PgUp / PgDn', 'Page scroll'],
|
|
33
|
+
['g / G', 'Top / bottom'],
|
|
34
|
+
['Tab / h', 'Switch body / headers'],
|
|
35
|
+
['Esc', 'Back'],
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
export function Help({ theme, onClose }) {
|
|
40
|
+
useInput((input, key) => {
|
|
41
|
+
if (key.escape || input === '?' || input === 'q')
|
|
42
|
+
onClose();
|
|
43
|
+
});
|
|
44
|
+
return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, paddingX: 2, paddingY: 1, flexGrow: 1 },
|
|
45
|
+
React.createElement(Text, { color: theme.accent, bold: true }, "tcurl \u2014 keyboard reference"),
|
|
46
|
+
React.createElement(Box, { height: 1 }),
|
|
47
|
+
SECTIONS.map((section) => (React.createElement(Box, { key: section.title, flexDirection: "column", marginBottom: 1 },
|
|
48
|
+
React.createElement(Text, { color: theme.accentAlt, bold: true }, section.title),
|
|
49
|
+
section.rows.map(([k, desc]) => (React.createElement(Text, { key: k },
|
|
50
|
+
React.createElement(Text, { color: theme.accent }, k.padEnd(16)),
|
|
51
|
+
React.createElement(Text, { color: theme.fgDim }, desc))))))),
|
|
52
|
+
React.createElement(Text, { color: theme.muted }, "Press ? or Esc to close.")));
|
|
53
|
+
}
|