@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.
@@ -0,0 +1,276 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { HTTP_METHODS } from '../types.js';
4
+ import { methodColor } from '../theme.js';
5
+ import { TextEditor } from './TextEditor.js';
6
+ import { headersToText, textToHeaders, queryToText, textToQuery } from '../lib/kv.js';
7
+ const BODY_MODES = ['none', 'raw', 'json', 'form'];
8
+ const AUTH_TYPES = ['none', 'bearer', 'basic'];
9
+ export function RequestEditor({ theme, request, isActive, onChange, onSend, onBack, }) {
10
+ const [focus, setFocus] = useState(0);
11
+ const [editingKey, setEditingKey] = useState(null);
12
+ const fields = buildFields(request, theme);
13
+ const editing = editingKey !== null;
14
+ useInput((input, key) => {
15
+ if (key.escape) {
16
+ onBack();
17
+ return;
18
+ }
19
+ if (key.upArrow || (key.shift && key.tab)) {
20
+ setFocus((f) => (f - 1 + fields.length) % fields.length);
21
+ return;
22
+ }
23
+ if (key.downArrow || key.tab) {
24
+ setFocus((f) => (f + 1) % fields.length);
25
+ return;
26
+ }
27
+ const field = fields[focus];
28
+ if (!field)
29
+ return;
30
+ if (key.leftArrow || key.rightArrow) {
31
+ const dir = key.leftArrow ? -1 : 1;
32
+ applyCycle(field.key, dir);
33
+ return;
34
+ }
35
+ if (key.return || input === ' ') {
36
+ activate(field);
37
+ return;
38
+ }
39
+ }, { isActive: isActive && !editing });
40
+ function update(patch) {
41
+ onChange({ ...request, ...patch, updatedAt: Date.now() });
42
+ }
43
+ function applyCycle(fieldKey, dir) {
44
+ if (fieldKey === 'method') {
45
+ const idx = HTTP_METHODS.indexOf(request.method);
46
+ const next = HTTP_METHODS[(idx + dir + HTTP_METHODS.length) % HTTP_METHODS.length];
47
+ update({ method: next });
48
+ }
49
+ else if (fieldKey === 'bodyMode') {
50
+ const idx = BODY_MODES.indexOf(request.bodyMode);
51
+ update({ bodyMode: BODY_MODES[(idx + dir + BODY_MODES.length) % BODY_MODES.length] });
52
+ }
53
+ else if (fieldKey === 'authType') {
54
+ const idx = AUTH_TYPES.indexOf(request.auth.type);
55
+ const next = AUTH_TYPES[(idx + dir + AUTH_TYPES.length) % AUTH_TYPES.length];
56
+ update({ auth: { ...request.auth, type: next } });
57
+ }
58
+ else if (fieldKey === 'followRedirects') {
59
+ update({ followRedirects: !request.followRedirects });
60
+ }
61
+ else if (fieldKey === 'rejectUnauthorized') {
62
+ update({ rejectUnauthorized: !request.rejectUnauthorized });
63
+ }
64
+ }
65
+ function activate(field) {
66
+ switch (field.type) {
67
+ case 'cycle':
68
+ applyCycle(field.key, 1);
69
+ break;
70
+ case 'toggle':
71
+ applyCycle(field.key, 1);
72
+ break;
73
+ case 'action':
74
+ if (field.key === 'send')
75
+ onSend();
76
+ break;
77
+ case 'text':
78
+ case 'masked':
79
+ case 'multiline':
80
+ setEditingKey(field.key);
81
+ break;
82
+ }
83
+ }
84
+ function commitEdit(fieldKey, value) {
85
+ switch (fieldKey) {
86
+ case 'name':
87
+ update({ name: value || 'Untitled request' });
88
+ break;
89
+ case 'url':
90
+ update({ url: value });
91
+ break;
92
+ case 'authToken':
93
+ update({ auth: { ...request.auth, token: value } });
94
+ break;
95
+ case 'authPassword':
96
+ update({ auth: { ...request.auth, password: value } });
97
+ break;
98
+ case 'body':
99
+ update({ body: value });
100
+ break;
101
+ case 'headers':
102
+ update({ headers: textToHeaders(value) });
103
+ break;
104
+ case 'query':
105
+ update({ query: textToQuery(value) });
106
+ break;
107
+ case 'timeout': {
108
+ const n = parseInt(value, 10);
109
+ update({ timeoutMs: Number.isFinite(n) && n >= 0 ? n : request.timeoutMs });
110
+ break;
111
+ }
112
+ }
113
+ setEditingKey(null);
114
+ }
115
+ if (editing) {
116
+ const ed = editorConfigFor(editingKey, request);
117
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1 },
118
+ React.createElement(TextEditor, { theme: theme, label: ed.label, initialValue: ed.value, multiline: ed.multiline, masked: ed.masked, onSubmit: (v) => commitEdit(editingKey, v), onCancel: () => setEditingKey(null) })));
119
+ }
120
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: isActive ? theme.borderActive : theme.border, paddingX: 1 },
121
+ React.createElement(Text, { color: theme.accentAlt, bold: true }, "Edit request"),
122
+ React.createElement(Box, { height: 1 }),
123
+ fields.map((f, i) => {
124
+ const focused = i === focus;
125
+ if (f.type === 'action') {
126
+ return (React.createElement(Box, { key: f.key, marginTop: 1 },
127
+ React.createElement(Text, { backgroundColor: focused ? theme.success : undefined, color: focused ? theme.bg : theme.success, bold: true },
128
+ focused ? ' ▶ ' : ' ',
129
+ f.label,
130
+ ' ')));
131
+ }
132
+ return (React.createElement(Box, { key: f.key },
133
+ React.createElement(Text, { color: focused ? theme.accent : theme.fgDim },
134
+ focused ? '❯ ' : ' ',
135
+ f.label.padEnd(13)),
136
+ React.createElement(Text, { color: f.color ?? (focused ? theme.fg : theme.fgDim), wrap: "truncate-end" }, f.display)));
137
+ }),
138
+ React.createElement(Box, { height: 1 }),
139
+ React.createElement(Text, { color: theme.muted }, "\u2191\u2193 move \u00B7 \u2190\u2192/Space change \u00B7 Enter edit \u00B7 Esc back")));
140
+ }
141
+ function buildFields(request, theme) {
142
+ const fields = [
143
+ { key: 'name', label: 'Name', type: 'text', display: request.name },
144
+ {
145
+ key: 'method',
146
+ label: 'Method',
147
+ type: 'cycle',
148
+ display: request.method,
149
+ color: methodColor(request.method, theme),
150
+ },
151
+ { key: 'url', label: 'URL', type: 'text', display: request.url || '(empty)' },
152
+ { key: 'authType', label: 'Auth', type: 'cycle', display: request.auth.type },
153
+ ];
154
+ if (request.auth.type === 'bearer') {
155
+ fields.push({
156
+ key: 'authToken',
157
+ label: 'Token',
158
+ type: 'masked',
159
+ display: mask(request.auth.token),
160
+ });
161
+ }
162
+ else if (request.auth.type === 'basic') {
163
+ fields.push({
164
+ key: 'authToken',
165
+ label: 'Username',
166
+ type: 'text',
167
+ display: request.auth.token || '(empty)',
168
+ });
169
+ fields.push({
170
+ key: 'authPassword',
171
+ label: 'Password',
172
+ type: 'masked',
173
+ display: mask(request.auth.password),
174
+ });
175
+ }
176
+ fields.push({
177
+ key: 'bodyMode',
178
+ label: 'Body type',
179
+ type: 'cycle',
180
+ display: request.bodyMode,
181
+ });
182
+ if (request.bodyMode !== 'none') {
183
+ fields.push({
184
+ key: 'body',
185
+ label: 'Body',
186
+ type: 'multiline',
187
+ display: preview(request.body),
188
+ });
189
+ }
190
+ fields.push({
191
+ key: 'headers',
192
+ label: 'Headers',
193
+ type: 'multiline',
194
+ display: preview(headersToText(request.headers)),
195
+ });
196
+ fields.push({
197
+ key: 'query',
198
+ label: 'Query',
199
+ type: 'multiline',
200
+ display: preview(queryToText(request.query)),
201
+ });
202
+ fields.push({
203
+ key: 'timeout',
204
+ label: 'Timeout ms',
205
+ type: 'text',
206
+ display: String(request.timeoutMs),
207
+ });
208
+ fields.push({
209
+ key: 'followRedirects',
210
+ label: 'Redirects',
211
+ type: 'toggle',
212
+ display: request.followRedirects ? 'follow' : 'manual',
213
+ });
214
+ fields.push({
215
+ key: 'rejectUnauthorized',
216
+ label: 'TLS verify',
217
+ type: 'toggle',
218
+ display: request.rejectUnauthorized ? 'on' : 'off (insecure)',
219
+ color: request.rejectUnauthorized ? undefined : theme.warning,
220
+ });
221
+ fields.push({ key: 'send', label: 'Send request ▶', type: 'action', display: '' });
222
+ return fields;
223
+ }
224
+ function editorConfigFor(key, request) {
225
+ switch (key) {
226
+ case 'name':
227
+ return { label: 'Name', value: request.name, multiline: false, masked: false };
228
+ case 'url':
229
+ return { label: 'URL', value: request.url, multiline: false, masked: false };
230
+ case 'authToken':
231
+ return {
232
+ label: request.auth.type === 'basic' ? 'Username' : 'Token',
233
+ value: request.auth.token,
234
+ multiline: false,
235
+ masked: request.auth.type === 'bearer',
236
+ };
237
+ case 'authPassword':
238
+ return { label: 'Password', value: request.auth.password, multiline: false, masked: true };
239
+ case 'body':
240
+ return { label: 'Body', value: request.body, multiline: true, masked: false };
241
+ case 'headers':
242
+ return {
243
+ label: 'Headers (Key: Value per line)',
244
+ value: headersToText(request.headers),
245
+ multiline: true,
246
+ masked: false,
247
+ };
248
+ case 'query':
249
+ return {
250
+ label: 'Query (key=value per line)',
251
+ value: queryToText(request.query),
252
+ multiline: true,
253
+ masked: false,
254
+ };
255
+ case 'timeout':
256
+ return {
257
+ label: 'Timeout (ms, 0 = none)',
258
+ value: String(request.timeoutMs),
259
+ multiline: false,
260
+ masked: false,
261
+ };
262
+ default:
263
+ return { label: key, value: '', multiline: false, masked: false };
264
+ }
265
+ }
266
+ function mask(s) {
267
+ return s ? '•'.repeat(Math.min(s.length, 24)) : '(empty)';
268
+ }
269
+ function preview(s) {
270
+ if (!s)
271
+ return '(empty)';
272
+ const firstLine = s.split('\n')[0];
273
+ const lines = s.split('\n').length;
274
+ const suffix = lines > 1 ? ` … +${lines - 1} lines` : '';
275
+ return (firstLine.length > 40 ? firstLine.slice(0, 39) + '…' : firstLine) + suffix;
276
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { methodColor } from '../theme.js';
4
+ export function RequestList({ theme, requests, selectedIndex }) {
5
+ return (React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
6
+ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, width: "45%", marginRight: 1 },
7
+ React.createElement(Text, { color: theme.accentAlt, bold: true },
8
+ "Saved requests (",
9
+ requests.length,
10
+ ")"),
11
+ React.createElement(Box, { height: 1 }),
12
+ requests.length === 0 ? (React.createElement(Text, { color: theme.muted }, "No requests yet. Press \"n\" to create one.")) : (requests.map((r, i) => {
13
+ const selected = i === selectedIndex;
14
+ return (React.createElement(Box, { key: r.id },
15
+ React.createElement(Text, { backgroundColor: selected ? theme.selectionBg : undefined, color: selected ? theme.fg : theme.fgDim },
16
+ selected ? '❯ ' : ' ',
17
+ React.createElement(Text, { color: methodColor(r.method, theme), bold: true }, r.method.padEnd(6)),
18
+ ' ',
19
+ truncate(r.name, 24))));
20
+ }))),
21
+ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, flexGrow: 1 }, requests[selectedIndex] ? (React.createElement(RequestDetail, { theme: theme, request: requests[selectedIndex] })) : (React.createElement(Text, { color: theme.muted }, "Select or create a request.")))));
22
+ }
23
+ function RequestDetail({ theme, request, }) {
24
+ const enabledHeaders = request.headers.filter((h) => h.enabled);
25
+ const enabledQuery = request.query.filter((q) => q.enabled);
26
+ return (React.createElement(Box, { flexDirection: "column" },
27
+ React.createElement(Text, { color: theme.fg, bold: true }, request.name),
28
+ React.createElement(Box, { height: 1 }),
29
+ React.createElement(Text, null,
30
+ React.createElement(Text, { color: methodColor(request.method, theme), bold: true }, request.method),
31
+ ' ',
32
+ React.createElement(Text, { color: theme.fg }, request.url || '(no url)')),
33
+ React.createElement(Box, { height: 1 }),
34
+ React.createElement(Field, { theme: theme, label: "Auth", value: request.auth.type }),
35
+ React.createElement(Field, { theme: theme, label: "Body", value: request.bodyMode }),
36
+ React.createElement(Field, { theme: theme, label: "Headers", value: String(enabledHeaders.length) }),
37
+ React.createElement(Field, { theme: theme, label: "Query", value: String(enabledQuery.length) }),
38
+ React.createElement(Field, { theme: theme, label: "Timeout", value: request.timeoutMs ? `${request.timeoutMs} ms` : 'none' }),
39
+ React.createElement(Field, { theme: theme, label: "Redirects", value: request.followRedirects ? 'follow' : 'manual' }),
40
+ React.createElement(Field, { theme: theme, label: "TLS verify", value: request.rejectUnauthorized ? 'on' : 'off (insecure)' })));
41
+ }
42
+ function Field({ theme, label, value }) {
43
+ return (React.createElement(Text, null,
44
+ React.createElement(Text, { color: theme.fgDim }, label.padEnd(11)),
45
+ React.createElement(Text, { color: theme.fg }, value)));
46
+ }
47
+ function truncate(s, n) {
48
+ return s.length > n ? s.slice(0, n - 1) + '…' : s;
49
+ }
@@ -0,0 +1,83 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { statusColor } from '../theme.js';
4
+ import { prettyBody, formatBytes, formatDuration, wrapLines } from '../lib/format.js';
5
+ export function ResponseViewer({ theme, response, isActive, onBack, width, height, }) {
6
+ const [tab, setTab] = useState('body');
7
+ const [scroll, setScroll] = useState(0);
8
+ const viewport = Math.max(4, height - 8);
9
+ const innerWidth = Math.max(20, width - 6);
10
+ const lines = useMemo(() => {
11
+ if (tab === 'headers') {
12
+ return Object.entries(response.headers).map(([k, v]) => `${k}: ${v}`);
13
+ }
14
+ const pretty = prettyBody(response.body, response.contentType);
15
+ return wrapLines(pretty, innerWidth);
16
+ }, [tab, response, innerWidth]);
17
+ const maxScroll = Math.max(0, lines.length - viewport);
18
+ const clampedScroll = Math.min(scroll, maxScroll);
19
+ useInput((input, key) => {
20
+ if (key.escape || input === 'q' || key.backspace) {
21
+ onBack();
22
+ return;
23
+ }
24
+ if (input === 'h' || input === 'b' || key.tab) {
25
+ setTab((t) => (t === 'body' ? 'headers' : 'body'));
26
+ setScroll(0);
27
+ return;
28
+ }
29
+ if (key.upArrow || input === 'k')
30
+ setScroll((s) => Math.max(0, s - 1));
31
+ if (key.downArrow || input === 'j')
32
+ setScroll((s) => Math.min(maxScroll, s + 1));
33
+ if (key.pageUp)
34
+ setScroll((s) => Math.max(0, s - viewport));
35
+ if (key.pageDown)
36
+ setScroll((s) => Math.min(maxScroll, s + viewport));
37
+ if (input === 'g')
38
+ setScroll(0);
39
+ if (input === 'G')
40
+ setScroll(maxScroll);
41
+ }, { isActive });
42
+ const visible = lines.slice(clampedScroll, clampedScroll + viewport);
43
+ const sc = statusColor(response.status, theme);
44
+ return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: isActive ? theme.borderActive : theme.border, paddingX: 1 },
45
+ response.error ? (React.createElement(Text, { color: theme.error, bold: true },
46
+ "\u2716 ",
47
+ response.error)) : (React.createElement(Box, { justifyContent: "space-between" },
48
+ React.createElement(Text, null,
49
+ React.createElement(Text, { color: sc, bold: true },
50
+ response.status,
51
+ " ",
52
+ response.statusText),
53
+ React.createElement(Text, { color: theme.fgDim },
54
+ ' ',
55
+ formatDuration(response.durationMs),
56
+ " \u00B7 ",
57
+ formatBytes(response.sizeBytes),
58
+ " \u00B7",
59
+ ' ',
60
+ response.contentType)),
61
+ React.createElement(Text, { color: theme.muted }, truncate(response.finalUrl, 40)))),
62
+ React.createElement(Box, { marginTop: 1 },
63
+ React.createElement(Tabs, { theme: theme, tab: tab, headerCount: Object.keys(response.headers).length })),
64
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, minHeight: viewport }, visible.length === 0 ? (React.createElement(Text, { color: theme.muted }, "(empty)")) : (visible.map((line, i) => (React.createElement(Text, { key: i, color: theme.fg }, line.length === 0 ? ' ' : line))))),
65
+ React.createElement(Box, { justifyContent: "space-between" },
66
+ React.createElement(Text, { color: theme.muted }, lines.length > viewport
67
+ ? `line ${clampedScroll + 1}-${Math.min(clampedScroll + viewport, lines.length)} / ${lines.length}`
68
+ : `${lines.length} lines`),
69
+ React.createElement(Text, { color: theme.muted }, "\u2191\u2193 scroll \u00B7 Tab headers/body \u00B7 g/G top/bottom \u00B7 Esc back"))));
70
+ }
71
+ function Tabs({ theme, tab, headerCount, }) {
72
+ const item = (id, label) => (React.createElement(Text, { backgroundColor: tab === id ? theme.selectionBg : undefined, color: tab === id ? theme.accent : theme.fgDim, bold: tab === id },
73
+ ' ',
74
+ label,
75
+ ' '));
76
+ return (React.createElement(Text, null,
77
+ item('body', 'Body'),
78
+ ' ',
79
+ item('headers', `Headers (${headerCount})`)));
80
+ }
81
+ function truncate(s, n) {
82
+ return s.length > n ? '…' + s.slice(s.length - n + 1) : s;
83
+ }
@@ -0,0 +1,13 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Text } from 'ink';
3
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
+ export function Spinner({ theme, label }) {
5
+ const [frame, setFrame] = useState(0);
6
+ useEffect(() => {
7
+ const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80);
8
+ return () => clearInterval(t);
9
+ }, []);
10
+ return (React.createElement(Text, { color: theme.accent },
11
+ FRAMES[frame],
12
+ label ? ` ${label}` : ''));
13
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ export function StatusBar({ theme, hints }) {
4
+ return (React.createElement(Box, { paddingX: 1, flexWrap: "wrap" }, hints.map((h, i) => (React.createElement(Text, { key: i },
5
+ React.createElement(Text, { color: theme.accent, bold: true }, h.key),
6
+ React.createElement(Text, { color: theme.fgDim }, ' ' + h.label),
7
+ i < hints.length - 1 ? React.createElement(Text, { color: theme.muted }, ' ') : null)))));
8
+ }
@@ -0,0 +1,90 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ /**
4
+ * A self-contained text editor supporting single- and multi-line input.
5
+ * Enter submits (single-line) or inserts a newline (multi-line).
6
+ * Ctrl+S submits, Esc cancels. Arrow keys move the cursor.
7
+ */
8
+ export function TextEditor({ initialValue, multiline = false, masked = false, theme, label, onSubmit, onCancel, }) {
9
+ const [value, setValue] = useState(initialValue);
10
+ const [cursor, setCursor] = useState(initialValue.length);
11
+ useInput((input, key) => {
12
+ if (key.escape) {
13
+ onCancel();
14
+ return;
15
+ }
16
+ // Ctrl+S always submits.
17
+ if (key.ctrl && input === 's') {
18
+ onSubmit(value);
19
+ return;
20
+ }
21
+ if (key.return) {
22
+ if (multiline) {
23
+ insert('\n');
24
+ }
25
+ else {
26
+ onSubmit(value);
27
+ }
28
+ return;
29
+ }
30
+ if (key.leftArrow) {
31
+ setCursor((c) => Math.max(0, c - 1));
32
+ return;
33
+ }
34
+ if (key.rightArrow) {
35
+ setCursor((c) => Math.min(value.length, c + 1));
36
+ return;
37
+ }
38
+ if (key.upArrow || key.downArrow) {
39
+ moveVertical(key.upArrow ? -1 : 1);
40
+ return;
41
+ }
42
+ if (key.backspace || key.delete) {
43
+ if (cursor > 0) {
44
+ setValue((v) => v.slice(0, cursor - 1) + v.slice(cursor));
45
+ setCursor((c) => Math.max(0, c - 1));
46
+ }
47
+ return;
48
+ }
49
+ // Ignore other control keys (tab, etc.).
50
+ if (key.tab || key.ctrl || key.meta)
51
+ return;
52
+ if (input)
53
+ insert(input);
54
+ });
55
+ function insert(text) {
56
+ setValue((v) => v.slice(0, cursor) + text + v.slice(cursor));
57
+ setCursor((c) => c + text.length);
58
+ }
59
+ function moveVertical(dir) {
60
+ const before = value.slice(0, cursor);
61
+ const lines = before.split('\n');
62
+ const col = lines[lines.length - 1].length;
63
+ const allLines = value.split('\n');
64
+ const curLineIdx = lines.length - 1;
65
+ const targetIdx = curLineIdx + dir;
66
+ if (targetIdx < 0 || targetIdx >= allLines.length)
67
+ return;
68
+ let offset = 0;
69
+ for (let i = 0; i < targetIdx; i++)
70
+ offset += allLines[i].length + 1;
71
+ offset += Math.min(col, allLines[targetIdx].length);
72
+ setCursor(offset);
73
+ }
74
+ const display = masked ? '•'.repeat(value.length) : value;
75
+ // Insert a visible cursor block.
76
+ const withCursor = display.slice(0, cursor) +
77
+ (cursor < display.length ? `█` : '█') +
78
+ display.slice(cursor + (cursor < display.length ? 1 : 0));
79
+ return (React.createElement(Box, { flexDirection: "column" },
80
+ React.createElement(Box, null,
81
+ React.createElement(Text, { color: theme.accent, bold: true },
82
+ '✎ ',
83
+ label)),
84
+ React.createElement(Box, { borderStyle: "round", borderColor: theme.borderActive, paddingX: 1, flexDirection: "column", minHeight: multiline ? 6 : 1 },
85
+ React.createElement(Text, { color: theme.fg }, withCursor.length ? withCursor : '█')),
86
+ React.createElement(Box, null,
87
+ React.createElement(Text, { color: theme.muted }, multiline
88
+ ? 'Enter: newline · Ctrl+S: save · Esc: cancel'
89
+ : 'Enter: save · Esc: cancel'))));
90
+ }