@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.
@@ -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 { colors as c } from "./theme.mjs";
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 split = width >= 78,
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
- clip(
111
- `${marked ? "●" : " "}${String(number).padStart(4)} ${graphemes(text.replaceAll("\t", " ")).slice(horizontal).join("")}`,
112
- codeWidth,
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) / 4)),
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 * 4, first * 4 + height - header)
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
- // One palette for the workspace, file picker and review panes.
2
- export const colors = Object.freeze({
3
- bg: "#0b1020",
4
- panel: "#11182b",
5
- line: "#303d60",
6
- muted: "#8e9ab8",
7
- text: "#e5eaff",
8
- blue: "#86baff",
9
- purple: "#b5a0ff",
10
- green: "#7bdcbd",
11
- red: "#ffa0b4",
12
- selected: "#29365b",
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("/posts/" + route.id + "?reply_page=" + page);
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 {