@tenyokjs/devdevian-cli 0.4.0-alpha.1 → 0.5.0-alpha.1
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 +26 -0
- package/VERSIONS.md +71 -0
- package/bin/devdevian.mjs +51 -26
- package/lib/appearance.mjs +179 -0
- package/lib/client.mjs +12 -4
- package/lib/external-editor.mjs +111 -0
- package/lib/picker.mjs +6 -5
- package/lib/problems.mjs +29 -0
- package/lib/review-workspace.mjs +17 -8
- package/lib/rich-text.mjs +96 -0
- package/lib/theme.mjs +66 -13
- package/lib/tui-data.mjs +44 -1
- package/lib/tui-model.mjs +2 -1
- package/lib/tui.mjs +528 -163
- package/package.json +2 -1
package/lib/review-workspace.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { clip, graphemes } from "./tui-model.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { syntaxSpans } from "./rich-text.mjs";
|
|
5
|
+
import { useTheme } from "./theme.mjs";
|
|
5
6
|
const h = React.createElement;
|
|
6
7
|
export function codeRange(line, anchor = null) {
|
|
7
8
|
return {
|
|
@@ -31,8 +32,11 @@ export function ReviewWorkspace({
|
|
|
31
32
|
anchor,
|
|
32
33
|
horizontal = 0,
|
|
33
34
|
rows,
|
|
35
|
+
layout = "auto",
|
|
36
|
+
stride = 4,
|
|
34
37
|
}) {
|
|
35
|
-
const
|
|
38
|
+
const c = useTheme();
|
|
39
|
+
const split = width >= 78 && layout !== "stacked",
|
|
36
40
|
codeWidth = split ? Math.floor(width * 0.55) : width;
|
|
37
41
|
const selectedNote = note || items[index];
|
|
38
42
|
const excerpt = reviewExcerpt(post, focus === "code" ? null : selectedNote),
|
|
@@ -96,6 +100,10 @@ export function ReviewWorkspace({
|
|
|
96
100
|
number >= selectedNote.start_line &&
|
|
97
101
|
number <= selectedNote.end_line);
|
|
98
102
|
const text = lines[n];
|
|
103
|
+
const rendered = clip(
|
|
104
|
+
`${marked ? "●" : " "}${String(number).padStart(4)} ${graphemes(text.replaceAll("\t", " ")).slice(horizontal).join("")}`,
|
|
105
|
+
codeWidth,
|
|
106
|
+
);
|
|
99
107
|
return h(
|
|
100
108
|
Text,
|
|
101
109
|
{
|
|
@@ -107,15 +115,16 @@ export function ReviewWorkspace({
|
|
|
107
115
|
? c.red
|
|
108
116
|
: c.text,
|
|
109
117
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
...(text.startsWith("+") || text.startsWith("-")
|
|
119
|
+
? [rendered]
|
|
120
|
+
: syntaxSpans(rendered, c).map((span, j) =>
|
|
121
|
+
h(Text, { key: j, color: span.color }, span.text),
|
|
122
|
+
)),
|
|
114
123
|
);
|
|
115
124
|
}),
|
|
116
125
|
);
|
|
117
126
|
const notesWidth = split ? width - codeWidth - 2 : width;
|
|
118
|
-
const visible = Math.max(1, Math.floor((height - 2) /
|
|
127
|
+
const visible = Math.max(1, Math.floor((height - 2) / stride)),
|
|
119
128
|
first = Math.max(0, index - visible + 1);
|
|
120
129
|
const notes = h(
|
|
121
130
|
Box,
|
|
@@ -134,7 +143,7 @@ export function ReviewWorkspace({
|
|
|
134
143
|
: null,
|
|
135
144
|
...(items.length
|
|
136
145
|
? rows(notesWidth)
|
|
137
|
-
.slice(first *
|
|
146
|
+
.slice(first * stride, first * stride + height - header)
|
|
138
147
|
.map((row, i) =>
|
|
139
148
|
h(
|
|
140
149
|
Text,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { clean } from "./client.mjs";
|
|
2
|
+
import { wrap } from "./tui-model.mjs";
|
|
3
|
+
import { colors } from "./theme.mjs";
|
|
4
|
+
|
|
5
|
+
// Small, predictable terminal renderer. HTML and terminal hyperlinks are never executed.
|
|
6
|
+
export function syntaxSpans(text, c = colors) {
|
|
7
|
+
const pattern =
|
|
8
|
+
/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[^`]*`|\/\/.*$|#.*$|\b(?:async|await|function|return|const|let|var|if|else|throw|new|class|import|from|export|def|None|True|False|true|false|null|try|catch|for|while)\b|\b\d+(?:\.\d+)?\b)/g;
|
|
9
|
+
const parts = [];
|
|
10
|
+
let start = 0;
|
|
11
|
+
for (const match of text.matchAll(pattern)) {
|
|
12
|
+
if (match.index > start)
|
|
13
|
+
parts.push({ text: text.slice(start, match.index) });
|
|
14
|
+
const word = match[0];
|
|
15
|
+
parts.push({
|
|
16
|
+
text: word,
|
|
17
|
+
color: /^(\/\/|#)/.test(word)
|
|
18
|
+
? c.muted
|
|
19
|
+
: /^["'`]/.test(word)
|
|
20
|
+
? c.green
|
|
21
|
+
: /^\d/.test(word)
|
|
22
|
+
? c.blue
|
|
23
|
+
: c.purple,
|
|
24
|
+
});
|
|
25
|
+
start = match.index + word.length;
|
|
26
|
+
}
|
|
27
|
+
if (start < text.length) parts.push({ text: text.slice(start) });
|
|
28
|
+
return parts;
|
|
29
|
+
}
|
|
30
|
+
export function markdownRows(value, width, c = colors) {
|
|
31
|
+
let fence = null,
|
|
32
|
+
number = 0;
|
|
33
|
+
const rows = [];
|
|
34
|
+
for (const raw of clean(value).replace(/\t/g, " ").split("\n")) {
|
|
35
|
+
const marker = raw.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
36
|
+
if (
|
|
37
|
+
marker &&
|
|
38
|
+
(!fence ||
|
|
39
|
+
(marker[1][0] === fence[0] &&
|
|
40
|
+
marker[1].length >= fence.length &&
|
|
41
|
+
!marker[2].trim()))
|
|
42
|
+
) {
|
|
43
|
+
if (fence) {
|
|
44
|
+
fence = null;
|
|
45
|
+
rows.push({ text: "", color: c.muted });
|
|
46
|
+
} else {
|
|
47
|
+
fence = marker[1];
|
|
48
|
+
number = 0;
|
|
49
|
+
rows.push({
|
|
50
|
+
text:
|
|
51
|
+
"CODE SNIPPET" + (marker[2].trim() ? " · " + marker[2].trim() : ""),
|
|
52
|
+
color: c.blue,
|
|
53
|
+
bold: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (fence) {
|
|
59
|
+
const prefix = String(++number).padStart(4) + " ";
|
|
60
|
+
for (const [i, line] of wrap(raw, Math.max(1, width - 6)).entries()) {
|
|
61
|
+
const text = (i ? " " : prefix) + line;
|
|
62
|
+
rows.push({ text, spans: syntaxSpans(text, c) });
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const heading = /^#{1,6}\s+/.test(raw),
|
|
67
|
+
quote = /^>\s?/.test(raw);
|
|
68
|
+
const text = raw
|
|
69
|
+
.replace(/^#{1,6}\s+/, "")
|
|
70
|
+
.replace(/^>\s?/, "│ ")
|
|
71
|
+
.replace(/^\s*[-*+]\s+/, "• ")
|
|
72
|
+
.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1 <$2>");
|
|
73
|
+
for (const line of wrap(text, width)) {
|
|
74
|
+
const parts = [];
|
|
75
|
+
let start = 0;
|
|
76
|
+
for (const match of line.matchAll(/\*\*([^*]+)\*\*|`([^`]+)`/g)) {
|
|
77
|
+
if (match.index > start)
|
|
78
|
+
parts.push({ text: line.slice(start, match.index) });
|
|
79
|
+
parts.push({
|
|
80
|
+
text: match[1] || match[2],
|
|
81
|
+
bold: !!match[1],
|
|
82
|
+
color: match[2] ? c.blue : undefined,
|
|
83
|
+
});
|
|
84
|
+
start = match.index + match[0].length;
|
|
85
|
+
}
|
|
86
|
+
parts.push({ text: line.slice(start) });
|
|
87
|
+
rows.push({
|
|
88
|
+
text: parts.map((p) => p.text).join(""),
|
|
89
|
+
spans: parts,
|
|
90
|
+
bold: heading,
|
|
91
|
+
color: heading ? c.purple : quote ? c.muted : c.text,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
package/lib/theme.mjs
CHANGED
|
@@ -1,13 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import { normalizeAppearance } from "./appearance.mjs";
|
|
3
|
+
const presets = {
|
|
4
|
+
midnight: {
|
|
5
|
+
bg: "#0b1020",
|
|
6
|
+
panel: "#11182b",
|
|
7
|
+
line: "#303d60",
|
|
8
|
+
muted: "#96a3c1",
|
|
9
|
+
text: "#e5eaff",
|
|
10
|
+
green: "#7bdcbd",
|
|
11
|
+
red: "#ffa0b4",
|
|
12
|
+
selected: "#29365b",
|
|
13
|
+
},
|
|
14
|
+
nebula: {
|
|
15
|
+
bg: "#100d20",
|
|
16
|
+
panel: "#1a1530",
|
|
17
|
+
line: "#4a3b68",
|
|
18
|
+
muted: "#a49ab9",
|
|
19
|
+
text: "#f0eaff",
|
|
20
|
+
green: "#86ddc3",
|
|
21
|
+
red: "#ffa6bd",
|
|
22
|
+
selected: "#3c2c59",
|
|
23
|
+
},
|
|
24
|
+
daylight: {
|
|
25
|
+
bg: "#f2f5ff",
|
|
26
|
+
panel: "#ffffff",
|
|
27
|
+
line: "#b4c0dc",
|
|
28
|
+
muted: "#53617a",
|
|
29
|
+
text: "#1b2440",
|
|
30
|
+
green: "#16634d",
|
|
31
|
+
red: "#a12444",
|
|
32
|
+
selected: "#dce5ff",
|
|
33
|
+
},
|
|
34
|
+
contrast: {
|
|
35
|
+
bg: "#000000",
|
|
36
|
+
panel: "#080e1c",
|
|
37
|
+
line: "#899cc6",
|
|
38
|
+
muted: "#becae2",
|
|
39
|
+
text: "#ffffff",
|
|
40
|
+
green: "#8cf4c7",
|
|
41
|
+
red: "#ffb0c4",
|
|
42
|
+
selected: "#24345c",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export function themeFor(value = {}) {
|
|
46
|
+
const settings = normalizeAppearance(value),
|
|
47
|
+
light = settings.theme === "daylight";
|
|
48
|
+
const accents = light
|
|
49
|
+
? {
|
|
50
|
+
blue: ["#3154b8", "#6b36b6"],
|
|
51
|
+
violet: ["#6b36b6", "#3154b8"],
|
|
52
|
+
cyan: ["#006b80", "#6b36b6"],
|
|
53
|
+
rose: ["#9b246c", "#3154b8"],
|
|
54
|
+
}
|
|
55
|
+
: {
|
|
56
|
+
blue: ["#86baff", "#b5a0ff"],
|
|
57
|
+
violet: ["#c0a6ff", "#86baff"],
|
|
58
|
+
cyan: ["#74d5ed", "#b5a0ff"],
|
|
59
|
+
rose: ["#f4a6cf", "#86baff"],
|
|
60
|
+
};
|
|
61
|
+
const [blue, purple] = accents[settings.accent];
|
|
62
|
+
return { ...presets[settings.theme], blue, purple, frame: settings.frame };
|
|
63
|
+
}
|
|
64
|
+
export const colors = Object.freeze(themeFor());
|
|
65
|
+
export const ThemeContext = createContext(colors);
|
|
66
|
+
export const useTheme = () => useContext(ThemeContext);
|
package/lib/tui-data.mjs
CHANGED
|
@@ -19,8 +19,42 @@ export const navigation = [
|
|
|
19
19
|
["communities", "Communities", "Your private spaces"],
|
|
20
20
|
["search", "Search", "Find a conversation"],
|
|
21
21
|
["devices", "Connected terminals", "Manage CLI access"],
|
|
22
|
+
["appearance", "Appearance", "Themes & layout"],
|
|
22
23
|
["help", "Help", "Keys & commands"],
|
|
23
24
|
];
|
|
25
|
+
export async function notificationRoute(api, item) {
|
|
26
|
+
if (item.code_review_id)
|
|
27
|
+
return {
|
|
28
|
+
view: "feedback",
|
|
29
|
+
postId: item.post_id,
|
|
30
|
+
id: item.code_review_id,
|
|
31
|
+
page: 1,
|
|
32
|
+
targetReply: item.feedback_reply_id || null,
|
|
33
|
+
};
|
|
34
|
+
if (!item.reply_id)
|
|
35
|
+
return { view: "post", id: item.post_id, tab: "overview", page: 1 };
|
|
36
|
+
const post = await api(`/posts/${item.post_id}?reply_id=${item.reply_id}`);
|
|
37
|
+
const target = post.reply_path?.at(-1);
|
|
38
|
+
if (!target || post.reply_target_missing)
|
|
39
|
+
throw Object.assign(Error("The linked reply is no longer available."), {
|
|
40
|
+
status: 404,
|
|
41
|
+
});
|
|
42
|
+
return target.parent_id
|
|
43
|
+
? {
|
|
44
|
+
view: "thread",
|
|
45
|
+
postId: item.post_id,
|
|
46
|
+
parent: { id: target.parent_id },
|
|
47
|
+
page: target.page,
|
|
48
|
+
targetReply: item.reply_id,
|
|
49
|
+
}
|
|
50
|
+
: {
|
|
51
|
+
view: "post",
|
|
52
|
+
id: item.post_id,
|
|
53
|
+
tab: "replies",
|
|
54
|
+
page: target.page,
|
|
55
|
+
targetReply: item.reply_id,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
24
58
|
export async function loadPage(api, route, signal) {
|
|
25
59
|
const get = (path) => api(path, { signal }),
|
|
26
60
|
page = route.page || 1;
|
|
@@ -85,7 +119,13 @@ export async function loadPage(api, route, signal) {
|
|
|
85
119
|
};
|
|
86
120
|
}
|
|
87
121
|
if (route.view === "post") {
|
|
88
|
-
const post = await get(
|
|
122
|
+
const post = await get(
|
|
123
|
+
"/posts/" +
|
|
124
|
+
route.id +
|
|
125
|
+
(route.targetReply
|
|
126
|
+
? "?reply_id=" + route.targetReply
|
|
127
|
+
: "?reply_page=" + page),
|
|
128
|
+
);
|
|
89
129
|
let items = post.replies,
|
|
90
130
|
pagination = post.reply_pagination;
|
|
91
131
|
let snippet_hash;
|
|
@@ -194,9 +234,12 @@ export function contentLines(route, data) {
|
|
|
194
234
|
"E / D Edit / delete your own feedback or reply",
|
|
195
235
|
"O Open this conversation in your browser",
|
|
196
236
|
"/ Focus the command / search bar",
|
|
237
|
+
"Ctrl+E Edit a multiline field in VISUAL / EDITOR",
|
|
238
|
+
"Ctrl+L Save a draft and reconnect an expired session",
|
|
197
239
|
"Ctrl+S Preview the current form",
|
|
198
240
|
"Ctrl+O / Ctrl+G Select a code file / Git diff in a review form",
|
|
199
241
|
"/drafts Restore locally saved drafts for this account/project",
|
|
242
|
+
"/appearance Preview and save local themes and layout",
|
|
200
243
|
"Ctrl+C Exit and restore the original terminal",
|
|
201
244
|
"",
|
|
202
245
|
"Commands: /feed /inbox /search words /open 142",
|
package/lib/tui-model.mjs
CHANGED
|
@@ -129,6 +129,7 @@ export function parseCommand(text) {
|
|
|
129
129
|
"help",
|
|
130
130
|
"devices",
|
|
131
131
|
"drafts",
|
|
132
|
+
"appearance",
|
|
132
133
|
].includes(cmd)
|
|
133
134
|
)
|
|
134
135
|
return { command: cmd, arg };
|
|
@@ -140,7 +141,7 @@ export function parseCommand(text) {
|
|
|
140
141
|
return { command: cmd, arg: Number(arg) };
|
|
141
142
|
if (["quit", "logout"].includes(cmd)) return { command: cmd };
|
|
142
143
|
throw Error(
|
|
143
|
-
"Use /feed, /inbox, /search words, /open ID, /ask, /review, /communities, /devices, /drafts, /help or /quit.",
|
|
144
|
+
"Use /feed, /inbox, /search words, /open ID, /ask, /review, /communities, /devices, /drafts, /appearance, /help or /quit.",
|
|
144
145
|
);
|
|
145
146
|
}
|
|
146
147
|
export class PageCache {
|