blots-editor 0.0.16 → 0.0.17
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/asset/language/en.json +0 -1
- package/asset/language/zh.json +0 -1
- package/dist/core/document/document.d.ts +11 -3
- package/dist/core/document/document.js +20 -7
- package/dist/core/editor-controller.js +8 -2
- package/dist/core/io/file-tree-model.d.ts +7 -2
- package/dist/core/io/file-tree-model.js +9 -2
- package/dist/index.js +2 -1
- package/dist/view/editor/command-bar.js +2 -1
- package/dist/view/editor/editor-setting.js +2 -1
- package/dist/view/editor/file-tree.d.ts +0 -4
- package/dist/view/editor/file-tree.js +4 -7
- package/dist/view/editor/tool-bar.js +2 -1
- package/dist/view/page/editor.js +54 -4
- package/dist/view/page/main-menu.js +2 -1
- package/dist/view/page/settings.js +2 -1
- package/dist/view/utils/sensitivity-bar.js +2 -1
- package/package.json +2 -1
package/asset/language/en.json
CHANGED
package/asset/language/zh.json
CHANGED
|
@@ -131,9 +131,17 @@ export declare class Document {
|
|
|
131
131
|
visualAtLogical(line: number, logical: number): number;
|
|
132
132
|
moveLeft(): void;
|
|
133
133
|
moveRight(): void;
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Move up by `count` visual lines (default 1), aligning by visual column so
|
|
136
|
+
* wide-character lines do not drift. Overshooting the top clamps to the
|
|
137
|
+
* first visual line.
|
|
138
|
+
*/
|
|
139
|
+
moveUp(count?: number): void;
|
|
140
|
+
/**
|
|
141
|
+
* Move down by `count` visual lines (default 1), aligning by visual column.
|
|
142
|
+
* Overshooting the bottom clamps to the last visual line.
|
|
143
|
+
*/
|
|
144
|
+
moveDown(count?: number): void;
|
|
137
145
|
moveToLineStart(): void;
|
|
138
146
|
moveToLineEnd(): void;
|
|
139
147
|
moveToDocumentStart(): void;
|
|
@@ -325,10 +325,15 @@ export class Document {
|
|
|
325
325
|
this.setCursor(line, target);
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
|
-
/**
|
|
329
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Move up by `count` visual lines (default 1), aligning by visual column so
|
|
330
|
+
* wide-character lines do not drift. Overshooting the top clamps to the
|
|
331
|
+
* first visual line.
|
|
332
|
+
*/
|
|
333
|
+
moveUp(count = 1) {
|
|
330
334
|
const { line, logical } = this._cursor;
|
|
331
|
-
|
|
335
|
+
const current = this.cursorVisualLine;
|
|
336
|
+
if (current === 0) {
|
|
332
337
|
// Already on the first visual line: still move to the start of the
|
|
333
338
|
// current segment (like up-to-line-start in vim).
|
|
334
339
|
const segs = this.lineSegments(line);
|
|
@@ -339,17 +344,25 @@ export class Document {
|
|
|
339
344
|
this.setCursor(line, segs[idx].start);
|
|
340
345
|
return;
|
|
341
346
|
}
|
|
342
|
-
const
|
|
347
|
+
const step = Math.max(1, Math.round(count));
|
|
348
|
+
const prev = this.visualLineAt(Math.max(0, current - step));
|
|
343
349
|
if (!prev) {
|
|
344
350
|
return;
|
|
345
351
|
}
|
|
346
352
|
this.moveToSegment(prev);
|
|
347
353
|
}
|
|
348
|
-
|
|
349
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Move down by `count` visual lines (default 1), aligning by visual column.
|
|
356
|
+
* Overshooting the bottom clamps to the last visual line.
|
|
357
|
+
*/
|
|
358
|
+
moveDown(count = 1) {
|
|
359
|
+
const current = this.cursorVisualLine;
|
|
360
|
+
const last = this.visualLineCount - 1;
|
|
361
|
+
if (current >= last) {
|
|
350
362
|
return;
|
|
351
363
|
}
|
|
352
|
-
const
|
|
364
|
+
const step = Math.max(1, Math.round(count));
|
|
365
|
+
const next = this.visualLineAt(Math.min(last, current + step));
|
|
353
366
|
if (!next) {
|
|
354
367
|
return;
|
|
355
368
|
}
|
|
@@ -64,8 +64,14 @@ export class EditorController {
|
|
|
64
64
|
});
|
|
65
65
|
this.defineCommand("cursor.moveLeft", (doc) => doc.moveLeft());
|
|
66
66
|
this.defineCommand("cursor.moveRight", (doc) => doc.moveRight());
|
|
67
|
-
this.defineCommand("cursor.moveUp", (doc) =>
|
|
68
|
-
|
|
67
|
+
this.defineCommand("cursor.moveUp", (doc, args) => {
|
|
68
|
+
const count = typeof args?.count === "number" ? args.count : 1;
|
|
69
|
+
doc.moveUp(count);
|
|
70
|
+
});
|
|
71
|
+
this.defineCommand("cursor.moveDown", (doc, args) => {
|
|
72
|
+
const count = typeof args?.count === "number" ? args.count : 1;
|
|
73
|
+
doc.moveDown(count);
|
|
74
|
+
});
|
|
69
75
|
this.defineCommand("cursor.lineStart", (doc) => doc.moveToLineStart());
|
|
70
76
|
this.defineCommand("cursor.lineEnd", (doc) => doc.moveToLineEnd());
|
|
71
77
|
this.defineCommand("cursor.wordForward", (doc) => doc.moveWordForward());
|
|
@@ -24,8 +24,13 @@ export type FileNode = {
|
|
|
24
24
|
};
|
|
25
25
|
/** Expand a leading `~` to the user's home directory. */
|
|
26
26
|
export declare function expandHome(path: string): string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the file-tree root from settings: the custom path, or the startup
|
|
29
|
+
* directory. A custom mode with a blank path falls back to the startup
|
|
30
|
+
* directory — the picker lets you select custom before typing anything, and an
|
|
31
|
+
* empty root should not read as "no directory".
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveFileTreeRoot(settings: FileTreeSettings, startupDir: string): string;
|
|
29
34
|
/**
|
|
30
35
|
* Recursively scan a directory into a tree. Symlinks are never followed
|
|
31
36
|
* (they can loop), dotfiles and known dependency/VCS dirs are skipped.
|
|
@@ -43,11 +43,18 @@ const isHidden = (name) => name.startsWith(".");
|
|
|
43
43
|
export function expandHome(path) {
|
|
44
44
|
return path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the file-tree root from settings: the custom path, or the startup
|
|
48
|
+
* directory. A custom mode with a blank path falls back to the startup
|
|
49
|
+
* directory — the picker lets you select custom before typing anything, and an
|
|
50
|
+
* empty root should not read as "no directory".
|
|
51
|
+
*/
|
|
47
52
|
export function resolveFileTreeRoot(settings, startupDir) {
|
|
48
53
|
if (settings.root === "custom") {
|
|
49
54
|
const custom = expandHome(settings.customPath.trim());
|
|
50
|
-
|
|
55
|
+
if (custom.length > 0) {
|
|
56
|
+
return custom;
|
|
57
|
+
}
|
|
51
58
|
}
|
|
52
59
|
return startupDir;
|
|
53
60
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { LanguageProvider } from "@cartridge-engine/i18n";
|
|
5
|
+
import { CurrentScreen, KeyboardProvider, registerComponent, ScenarioManagementProvider, } from "ink-cartridge";
|
|
5
6
|
import { resources } from "./utils/view/i18n-resources.js";
|
|
6
7
|
import { settingsStore } from "./core/settings/useSettings.js";
|
|
7
8
|
import { MainMenu } from "./view/page/main-menu.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
2
|
-
import { back, ModalLayerElementContext,
|
|
3
|
+
import { back, ModalLayerElementContext, useKeyboard, useScreenSystem, } from "ink-cartridge";
|
|
3
4
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
4
5
|
/**
|
|
5
6
|
* Modal command bar (like Helix's `:` prompt). Rendered as a modal layer
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
2
|
-
import { applyElement, back, closeLayer, LayerElementContext, openLayer,
|
|
3
|
+
import { applyElement, back, closeLayer, LayerElementContext, openLayer, useKeyboard, useMouseRegion, } from "ink-cartridge";
|
|
3
4
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
4
5
|
import { SettingsEntries } from "../page/settings.js";
|
|
5
6
|
import { ModalFrame } from "../utils/modal-frame.js";
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { FileTreeState } from "../../core/io/file-tree-model.js";
|
|
3
2
|
import type { EditorSession } from "../../core/io/session.js";
|
|
4
3
|
export type FileTreeProps = {
|
|
5
4
|
/** The shared file session; clicking a file opens it here. */
|
|
6
5
|
session: EditorSession;
|
|
7
6
|
};
|
|
8
|
-
export type FileTreeDirState = FileTreeState | {
|
|
9
|
-
noDir?: boolean;
|
|
10
|
-
};
|
|
11
7
|
/**
|
|
12
8
|
* VSCode-style file tree pinned to the right edge of the terminal (regular
|
|
13
9
|
* layer element). Recursively scans the configured root directory once per
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
2
|
-
import { applyElementToModalLayer, ModalLayerElementContext, openModalLayer,
|
|
3
|
+
import { applyElementToModalLayer, ModalLayerElementContext, openModalLayer, useKeyboard, useMouseRegion, useScreenSystem, } from "ink-cartridge";
|
|
3
4
|
import React, { useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
4
5
|
import { basename } from "node:path";
|
|
5
6
|
import stringWidth from "string-width";
|
|
@@ -36,10 +37,7 @@ export function FileTree({ session }) {
|
|
|
36
37
|
setRoot({ scanning: true });
|
|
37
38
|
(async () => {
|
|
38
39
|
const resolved = resolveFileTreeRoot(settings.fileTree, process.cwd());
|
|
39
|
-
|
|
40
|
-
? await scanDirectoryCached(resolved)
|
|
41
|
-
: { noDir: true };
|
|
42
|
-
setRoot(tree);
|
|
40
|
+
setRoot(await scanDirectoryCached(resolved));
|
|
43
41
|
})();
|
|
44
42
|
}, [settings.fileTree, scanTick]);
|
|
45
43
|
// A new scan (root changed) starts collapsed at the top — reset during
|
|
@@ -114,8 +112,7 @@ export function FileTree({ session }) {
|
|
|
114
112
|
React.createElement(Box, { ref: refreshRef, marginLeft: 1 },
|
|
115
113
|
React.createElement(Text, { color: refreshHovered ? "green" : "gray" }, "\u21BB"))),
|
|
116
114
|
"scanning" in root ? (React.createElement(Box, { paddingLeft: 1 },
|
|
117
|
-
React.createElement(Text, { dimColor: true }, t("fileTree.scanning")))) : "
|
|
118
|
-
React.createElement(Text, { dimColor: true }, t("fileTree.noDir")))) : "fail" in root ? (React.createElement(Box, { paddingLeft: 1 },
|
|
115
|
+
React.createElement(Text, { dimColor: true }, t("fileTree.scanning")))) : "fail" in root ? (React.createElement(Box, { paddingLeft: 1 },
|
|
119
116
|
React.createElement(Text, { dimColor: true }, t("fileTree.fail")))) : (visible.map((row) => (React.createElement(TreeRow, { key: row.node.path, node: row.node, depth: row.depth, expanded: expanded.has(row.node.path), hovered: hoveredPath === row.node.path, onEnter: () => setHoveredPath(row.node.path), onLeave: () => setHoveredPath((h) => (h === row.node.path ? null : h)), onWheel: handleWheel, onClick: () => {
|
|
120
117
|
if (row.node.isDir) {
|
|
121
118
|
toggleDir(row.node.path);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useI18n
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
2
|
+
import { useKeyboard, useMouseRegion } from "ink-cartridge";
|
|
2
3
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { Box, measureElement, Text, useWindowSize } from "ink";
|
package/dist/view/page/editor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text, useBoxMetrics, useCursor } from "ink";
|
|
2
|
-
import { applyElement, applyElementToModalLayer, closeLayer, eraseElement, openLayer, openModalLayer,
|
|
3
|
+
import { applyElement, applyElementToModalLayer, closeLayer, eraseElement, openLayer, openModalLayer, useKeyboard, useMouseRegion, } from "ink-cartridge";
|
|
3
4
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { EditorSession } from "../../core/io/session.js";
|
|
5
6
|
import { useSettings } from "../../core/settings/useSettings.js";
|
|
@@ -104,7 +105,7 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
104
105
|
session,
|
|
105
106
|
openFileTree: () => setFileTreeOpen((open) => !open),
|
|
106
107
|
openSettings: () => setSettingsOpen((open) => !open),
|
|
107
|
-
fileTreeOpen
|
|
108
|
+
fileTreeOpen,
|
|
108
109
|
},
|
|
109
110
|
});
|
|
110
111
|
}, 0);
|
|
@@ -158,7 +159,9 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
158
159
|
unbinds.push(boundKeyboard(keys, handler, options));
|
|
159
160
|
};
|
|
160
161
|
// insert mode: classic editing
|
|
161
|
-
bind(["*"], (input) => controller.execute("editor.insertText", { text: input }), {
|
|
162
|
+
bind(["*"], (input) => controller.execute("editor.insertText", { text: input }), {
|
|
163
|
+
mode: "insert",
|
|
164
|
+
});
|
|
162
165
|
bind(["return"], () => controller.execute("editor.splitLine"), { mode: "insert" });
|
|
163
166
|
bind(["right"], () => controller.execute("cursor.moveRight"), { mode: "insert" });
|
|
164
167
|
bind(["left"], () => controller.execute("cursor.moveLeft"), { mode: "insert" });
|
|
@@ -201,7 +204,54 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
201
204
|
return { ...ctx, lastFlag: null };
|
|
202
205
|
},
|
|
203
206
|
});
|
|
207
|
+
// Vim-style count prefix: digits accumulate in the chain's `value`, then a
|
|
208
|
+
// direction key spends it (`5j`, `12k`). Composition runs ahead of the
|
|
209
|
+
// screen bindings, so a bare digit arms a chain instead of moving.
|
|
210
|
+
const countKeys = [];
|
|
211
|
+
for (const digit of "1234567890") {
|
|
212
|
+
countKeys.push(digit);
|
|
213
|
+
registryCompositionKey({
|
|
214
|
+
key: digit,
|
|
215
|
+
flags: [],
|
|
216
|
+
alternativeFlag: "number",
|
|
217
|
+
needs: ["number"],
|
|
218
|
+
// `0` cannot open a count, so a bare `0` keeps meaning line-start
|
|
219
|
+
// via its binding below; it may still extend one (`10j`).
|
|
220
|
+
optional: digit !== "0",
|
|
221
|
+
mode: "normal",
|
|
222
|
+
execute: (ctx) => ({
|
|
223
|
+
...ctx,
|
|
224
|
+
value: (typeof ctx.value === "number" ? ctx.value : 0) * 10 + Number(digit),
|
|
225
|
+
lastFlag: "number",
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Counts only pay off on the vertical keys; h/l/left/right stay untouched.
|
|
230
|
+
const directions = [
|
|
231
|
+
["k", "cursor.moveUp"],
|
|
232
|
+
["up", "cursor.moveUp"],
|
|
233
|
+
["j", "cursor.moveDown"],
|
|
234
|
+
["down", "cursor.moveDown"],
|
|
235
|
+
];
|
|
236
|
+
for (const [key, command] of directions) {
|
|
237
|
+
countKeys.push(key);
|
|
238
|
+
registryCompositionKey({
|
|
239
|
+
key,
|
|
240
|
+
flags: [],
|
|
241
|
+
alternativeFlag: "direction",
|
|
242
|
+
needs: ["number"],
|
|
243
|
+
mode: "normal",
|
|
244
|
+
// The count is spent here and the chain ends, so swallow the key —
|
|
245
|
+
// otherwise the plain binding below would move one extra line.
|
|
246
|
+
KeyReleaseWhenChainInterrupted: true,
|
|
247
|
+
execute: (ctx) => {
|
|
248
|
+
controller.execute(command, { count: ctx.value });
|
|
249
|
+
return null;
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
204
253
|
unbinds.push(() => removeCompositionKey("g"));
|
|
254
|
+
unbinds.push(() => countKeys.forEach((key) => removeCompositionKey(key)));
|
|
205
255
|
// Ink marks "G" as shift+g, so the normalized key name is "shift+G".
|
|
206
256
|
bind(["shift+G"], () => controller.execute("cursor.documentEnd"), { mode: "normal" });
|
|
207
257
|
bind(["i"], () => switchMode("insert"), { mode: "normal" });
|
|
@@ -281,7 +331,7 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
281
331
|
return null;
|
|
282
332
|
}
|
|
283
333
|
return (React.createElement(Box, { key: vline, flexDirection: "row" },
|
|
284
|
-
React.createElement(Box, { width: lineNumberWidth, justifyContent: "flex-end", marginRight: lineNumberRightSpacing }, seg.first ?
|
|
334
|
+
React.createElement(Box, { width: lineNumberWidth, justifyContent: "flex-end", marginRight: lineNumberRightSpacing }, seg.first ? React.createElement(Text, { bold: cursor.line === seg.line }, seg.line) : null),
|
|
285
335
|
React.createElement(Text, null, seg.text)));
|
|
286
336
|
})))));
|
|
287
337
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
2
|
-
import {
|
|
3
|
+
import { useKeyboard, useMouseRegion } from "ink-cartridge";
|
|
3
4
|
import React, { useEffect, useState } from "react";
|
|
4
5
|
import { Editor } from "./editor.js";
|
|
5
6
|
import { Settings } from "./settings.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
2
|
-
import { back, ModalLayerElementContext,
|
|
3
|
+
import { back, ModalLayerElementContext, useKeyboard, useMouseRegion, useScreenSystem, } from "ink-cartridge";
|
|
3
4
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
5
|
import { useSettings } from "../../core/settings/useSettings.js";
|
|
5
6
|
import { ModalFrame } from "../utils/modal-frame.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useI18n } from "@cartridge-engine/i18n";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
2
|
-
import {
|
|
3
|
+
import { useMouseRegion } from "ink-cartridge";
|
|
3
4
|
import React from "react";
|
|
4
5
|
export const SENSITIVITY_MIN = 1;
|
|
5
6
|
export const SENSITIVITY_MAX = 10;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blots-editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "A lightweight Markdown editor for the terminal, built on ink-cartridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"react": ">=18"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@cartridge-engine/i18n": "*",
|
|
54
55
|
"cfonts": "^3.3.1",
|
|
55
56
|
"chalk": "^4.1.2",
|
|
56
57
|
"gradient-string": "^2.0.2",
|