@cueloop/client 0.1.0-alpha.58 → 0.1.0-alpha.59

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,86 @@
1
+ import { createTextAttributes } from "@opentui/core";
2
+ import type { DisplayBlock, StyleRun } from "../view-plan";
3
+ import type { Theme } from "../theme";
4
+
5
+ /** A cut block reads as removed: struck through and grayed, never red. */
6
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
7
+
8
+ /** Inline-emphasis attributes, reused per run so the render loop allocates none. */
9
+ const STRONG_ATTRIBUTES = createTextAttributes({ bold: true });
10
+ const EM_ATTRIBUTES = createTextAttributes({ italic: true });
11
+ const STRIKE_ATTRIBUTES = createTextAttributes({ strikethrough: true });
12
+ const LINK_ATTRIBUTES = createTextAttributes({ underline: true });
13
+ /** Block-level base attributes: headings read bold, blockquotes read italic. */
14
+ const HEADING_ATTRIBUTES = createTextAttributes({ bold: true });
15
+ const QUOTE_ATTRIBUTES = createTextAttributes({ italic: true });
16
+
17
+ interface BlockBaseStyle {
18
+ baseFg: string;
19
+ baseAttributes: number;
20
+ }
21
+
22
+ function isHeadingBlock(block: DisplayBlock): boolean {
23
+ return block.kind === "h1" || block.kind === "h2" || block.kind === "h3";
24
+ }
25
+
26
+ // headings are all bold; level reads from descending brightness alone (a
27
+ // terminal cannot scale font size), leaving the salmon accent to annotations
28
+ function headingForeground(block: DisplayBlock, tokens: Theme): string | undefined {
29
+ if (block.kind === "h1") return tokens.text;
30
+ if (block.kind === "h2") return tokens.textMuted;
31
+ if (block.kind === "h3") return tokens.textDim;
32
+ return undefined;
33
+ }
34
+
35
+ function blockBaseStyle(block: DisplayBlock, tokens: Theme): BlockBaseStyle {
36
+ const isHeading = isHeadingBlock(block);
37
+ const isQuote = block.kind === "quote";
38
+ const headingFg = headingForeground(block, tokens);
39
+ // block-level base: headings bold, quotes muted italic; inline roles compose on top
40
+ const baseFg = headingFg ?? (isQuote ? tokens.textMuted : tokens.text);
41
+ const baseAttributes = (isHeading ? HEADING_ATTRIBUTES : 0) | (isQuote ? QUOTE_ATTRIBUTES : 0);
42
+ return { baseFg, baseAttributes };
43
+ }
44
+
45
+ function inlineRoleStyle(
46
+ run: StyleRun,
47
+ base: BlockBaseStyle,
48
+ tokens: Theme,
49
+ ): { fg?: string; bg?: string; attributes?: number } {
50
+ const { baseFg, baseAttributes } = base;
51
+ const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
52
+ switch (run.role) {
53
+ case "ins":
54
+ return { fg: tokens.insertedForeground, attributes: baseAttributes || undefined };
55
+ case "del":
56
+ return { fg: tokens.deletedForeground };
57
+ case "mark-comment":
58
+ return { fg: tokens.text, bg: tokens.markCommentBackground };
59
+ case "mark-focus":
60
+ return { fg: tokens.accentInk, bg: tokens.accent };
61
+ case "kspan":
62
+ return { fg: tokens.accentInk, bg: tokens.accent };
63
+ case "strong":
64
+ return { fg: baseFg, attributes: withBase(STRONG_ATTRIBUTES) };
65
+ case "em":
66
+ return { fg: baseFg, attributes: withBase(EM_ATTRIBUTES) };
67
+ case "code":
68
+ return { fg: tokens.text, bg: tokens.elevated };
69
+ case "strike":
70
+ return { fg: tokens.textMuted, attributes: withBase(STRIKE_ATTRIBUTES) };
71
+ case "link":
72
+ return { fg: tokens.blue, attributes: withBase(LINK_ATTRIBUTES) };
73
+ default:
74
+ return { fg: baseFg, attributes: baseAttributes || undefined };
75
+ }
76
+ }
77
+
78
+ export function runStyle(
79
+ run: StyleRun,
80
+ block: DisplayBlock,
81
+ tokens: Theme,
82
+ ): { fg?: string; bg?: string; attributes?: number } {
83
+ // a cut block reads as removed: every run struck through and grayed, never red
84
+ if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
85
+ return inlineRoleStyle(run, blockBaseStyle(block, tokens), tokens);
86
+ }
package/src/config.ts CHANGED
@@ -160,6 +160,63 @@ function parseActions(raw: unknown): QuickAction[] | undefined {
160
160
  return actions.length ? actions : undefined;
161
161
  }
162
162
 
163
+ function mergeAuthors(target: Record<string, string>, raw: unknown): void {
164
+ const authors = raw as Record<string, unknown> | undefined;
165
+ if (!authors) return;
166
+ for (const [id, value] of Object.entries(authors)) {
167
+ if (typeof value === "string") target[id] = value;
168
+ }
169
+ }
170
+
171
+ function mergeKeys(target: Record<string, string[]>, raw: unknown): void {
172
+ const keys = raw as KeymapConfig | undefined;
173
+ if (!keys) return;
174
+ for (const [action, combo] of Object.entries(keys)) {
175
+ target[action] = Array.isArray(combo) ? combo : [combo];
176
+ }
177
+ }
178
+
179
+ function mergeAutoClose(target: CueloopConfig["ui"], value: unknown): void {
180
+ if (value === undefined) return;
181
+ if (value === "off") target.autoClose = "off";
182
+ else if (typeof value === "number" && value >= 0) target.autoClose = value;
183
+ }
184
+
185
+ function mergeUi(target: CueloopConfig["ui"], raw: unknown): void {
186
+ const ui = raw as
187
+ | { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
188
+ | undefined;
189
+ if (!ui) return;
190
+ mergeAutoClose(target, ui.auto_close);
191
+ if (typeof ui.editor === "string" && ui.editor.trim()) target.editor = ui.editor.trim();
192
+ if (typeof ui.review_width === "number" && Number.isFinite(ui.review_width)) {
193
+ target.reviewWidth = clampWidth(ui.review_width);
194
+ }
195
+ if (
196
+ ui.review_state === "expanded" ||
197
+ ui.review_state === "compact" ||
198
+ ui.review_state === "hidden"
199
+ ) {
200
+ target.reviewState = ui.review_state;
201
+ }
202
+ }
203
+
204
+ function mergeObsidian(target: ObsidianConfig, raw: unknown): void {
205
+ const integrations = raw as Record<string, unknown> | undefined;
206
+ const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
207
+ if (!obsidian) return;
208
+ if (typeof obsidian["vault"] === "string") target.vault = obsidian["vault"];
209
+ if (typeof obsidian["folder"] === "string") target.folder = obsidian["folder"];
210
+ if (typeof obsidian["filenameFormat"] === "string")
211
+ target.filenameFormat = obsidian["filenameFormat"];
212
+ const separator = obsidian["separator"];
213
+ if (separator === "space" || separator === "dash" || separator === "underscore")
214
+ target.separator = separator;
215
+ const exportOn = obsidian["exportOn"];
216
+ if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
217
+ target.exportOn = exportOn;
218
+ }
219
+
163
220
  function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig {
164
221
  const out: CueloopConfig = {
165
222
  keys: { ...base.keys },
@@ -172,53 +229,10 @@ function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig
172
229
  };
173
230
  const actions = parseActions(raw["actions"]);
174
231
  if (actions) out.actions = actions;
175
- const authors = raw["authors"] as Record<string, unknown> | undefined;
176
- if (authors) {
177
- for (const [id, value] of Object.entries(authors)) {
178
- if (typeof value === "string") out.authors[id] = value;
179
- }
180
- }
181
- const keys = raw["keys"] as KeymapConfig | undefined;
182
- if (keys) {
183
- for (const [action, combo] of Object.entries(keys)) {
184
- out.keys[action] = Array.isArray(combo) ? combo : [combo];
185
- }
186
- }
187
- const ui = raw["ui"] as
188
- | { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
189
- | undefined;
190
- if (ui && ui.auto_close !== undefined) {
191
- if (ui.auto_close === "off") out.ui.autoClose = "off";
192
- else if (typeof ui.auto_close === "number" && ui.auto_close >= 0)
193
- out.ui.autoClose = ui.auto_close;
194
- }
195
- if (ui && typeof ui.editor === "string" && ui.editor.trim()) out.ui.editor = ui.editor.trim();
196
- if (ui && typeof ui.review_width === "number" && Number.isFinite(ui.review_width)) {
197
- out.ui.reviewWidth = clampWidth(ui.review_width);
198
- }
199
- if (
200
- ui &&
201
- (ui.review_state === "expanded" ||
202
- ui.review_state === "compact" ||
203
- ui.review_state === "hidden")
204
- ) {
205
- out.ui.reviewState = ui.review_state;
206
- }
207
- const integrations = raw["integrations"] as Record<string, unknown> | undefined;
208
- const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
209
- if (obsidian) {
210
- const obsidianConfig = out.integrations.obsidian;
211
- if (typeof obsidian["vault"] === "string") obsidianConfig.vault = obsidian["vault"];
212
- if (typeof obsidian["folder"] === "string") obsidianConfig.folder = obsidian["folder"];
213
- if (typeof obsidian["filenameFormat"] === "string")
214
- obsidianConfig.filenameFormat = obsidian["filenameFormat"];
215
- const separator = obsidian["separator"];
216
- if (separator === "space" || separator === "dash" || separator === "underscore")
217
- obsidianConfig.separator = separator;
218
- const exportOn = obsidian["exportOn"];
219
- if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
220
- obsidianConfig.exportOn = exportOn;
221
- }
232
+ mergeAuthors(out.authors, raw["authors"]);
233
+ mergeKeys(out.keys, raw["keys"]);
234
+ mergeUi(out.ui, raw["ui"]);
235
+ mergeObsidian(out.integrations.obsidian, raw["integrations"]);
222
236
  return out;
223
237
  }
224
238