blots-editor 0.0.1 → 0.0.3

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 ADDED
@@ -0,0 +1,25 @@
1
+ # blots-editor
2
+
3
+ > A lightweight Markdown editor for the terminal — built on [ink-cartridge](https://github.com/BAIGAOa/ink-cartridge).
4
+
5
+ ## About
6
+
7
+ blots-editor is both a **real product** and a **real-world testbed** for ink-cartridge. Every screen, keyboard shortcut, and overlay exercises the framework's screen management and keyboard engine in a concrete application — catching edge cases that unit tests alone can't.
8
+
9
+ Built with [Ink](https://github.com/vadimdemedes/ink) + [React](https://react.dev).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g blots-editor
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ blots-editor
21
+ ```
22
+
23
+ ## License
24
+
25
+ MIT
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ export type EditorContext = {
3
+ /**
4
+ * Used to externally get all current text, etc.
5
+ */
6
+ onChance: () => void;
7
+ /**
8
+ * Original text, including some special symbols
9
+ */
10
+ value: string;
11
+ /**
12
+ * What is the right margin of the left line number,
13
+ * which also affects the cursor position
14
+ */
15
+ lineNumberRightSpacing?: number;
16
+ /**
17
+ * Cursor offset. The default value is 1.
18
+ * It is also recommended to be 1. Otherwise,
19
+ * it will cause the cursor visual position to exceed the limit.
20
+ */
21
+ cursorOffset?: number;
22
+ /**
23
+ * Cursor height offset, default is 2
24
+ */
25
+ cursorHeightOffset?: number;
26
+ /**
27
+ * Number of indentation spaces
28
+ * default is 2
29
+ */
30
+ numberOfIndentationSpaces?: number;
31
+ };
32
+ export type CursorCoordinates = {
33
+ /**
34
+ * How many rows are currently in
35
+ */
36
+ line: number;
37
+ /**
38
+ * How many columns are currently in
39
+ */
40
+ column: number;
41
+ };
42
+ export declare function Editor({ value: line, lineNumberRightSpacing, cursorOffset, cursorHeightOffset, numberOfIndentationSpaces, }: EditorContext): React.JSX.Element;
@@ -0,0 +1,192 @@
1
+ import { Box, Text, useCursor } from "ink";
2
+ import { useKeyboard } from "ink-cartridge";
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ import stringWidth from "string-width";
5
+ function splitTextByLines(text) {
6
+ if (!text)
7
+ return [];
8
+ return text.split(/\r?\n/);
9
+ }
10
+ function insertStr(original, index, insertText) {
11
+ return original.slice(0, index) + insertText + original.slice(index);
12
+ }
13
+ function deleteCharAt(original, index, length = 1) {
14
+ return original.slice(0, index) + original.slice(index + length);
15
+ }
16
+ function splitString(string, index) {
17
+ return [string.slice(0, index), string.slice(index)];
18
+ }
19
+ export function Editor({ value: line, lineNumberRightSpacing = 1, cursorOffset = 1, cursorHeightOffset = 2, numberOfIndentationSpaces = 2, }) {
20
+ const [value, setValue] = useState(() => {
21
+ const lines = splitTextByLines(line);
22
+ return lines;
23
+ });
24
+ const [cursor, setCursor] = useState({
25
+ line: 0,
26
+ column: 0,
27
+ });
28
+ const checkedLineNumberRightSpacing = useMemo(() => {
29
+ return Math.max(0, lineNumberRightSpacing);
30
+ }, [lineNumberRightSpacing]);
31
+ const currentLineNumberLength = useMemo(() => {
32
+ return stringWidth(String(value.length - 1));
33
+ }, [stringWidth, cursor.line]);
34
+ const { setCursorPosition } = useCursor();
35
+ const boxRef = useRef(null);
36
+ const { boundKeyboard, enableWildcardPriority } = useKeyboard();
37
+ useEffect(() => {
38
+ const removeWildcardPrecedence = enableWildcardPriority();
39
+ const unBinds = [];
40
+ unBinds.push(boundKeyboard(["*"], (input, _key) => {
41
+ const cur = cursor;
42
+ setValue((prev) => {
43
+ const newValue = [...prev];
44
+ const newStr = insertStr(newValue[cur.line], cur.column, input);
45
+ newValue[cur.line] = newStr;
46
+ return newValue;
47
+ });
48
+ setCursor((prev) => {
49
+ return {
50
+ line: prev.line,
51
+ column: prev.column + input.length,
52
+ };
53
+ });
54
+ }));
55
+ unBinds.push(boundKeyboard(["return"], () => {
56
+ const cur = cursor;
57
+ setValue((prev) => {
58
+ const newLine = [...prev];
59
+ const newStr = splitString(newLine[cur.line], cur.column);
60
+ newLine.splice(cur.line, 1);
61
+ newLine.splice(cur.line, 0, newStr[0]);
62
+ newLine.splice(cur.line + 1, 0, newStr[1]);
63
+ return newLine;
64
+ });
65
+ setCursor((prev) => {
66
+ return {
67
+ line: prev.line + 1,
68
+ column: 0,
69
+ };
70
+ });
71
+ }));
72
+ unBinds.push(boundKeyboard(["right"], () => {
73
+ setCursor((prev) => ({
74
+ line: prev.line,
75
+ column: Math.min(prev.column + 1, value[cursor.line].length),
76
+ }));
77
+ }));
78
+ unBinds.push(boundKeyboard(["left"], () => {
79
+ setCursor((prev) => ({
80
+ line: prev.line,
81
+ column: Math.max(prev.column - 1, 0),
82
+ }));
83
+ }));
84
+ unBinds.push(boundKeyboard(["backspace", "delete"], () => {
85
+ let wrap = false;
86
+ let lastLineLenth = null;
87
+ setValue((prev) => {
88
+ const newValue = [...prev];
89
+ if (cursor.column === 0 && cursor.line !== 0) {
90
+ wrap = true;
91
+ lastLineLenth = newValue[cursor.line - 1].length;
92
+ const newStr = insertStr(newValue[cursor.line - 1], newValue[cursor.line - 1].length, newValue[cursor.line]);
93
+ newValue[cursor.line - 1] = newStr;
94
+ newValue.splice(cursor.line, 1);
95
+ }
96
+ else {
97
+ const newStr = cursor.column === 0
98
+ ? newValue[cursor.line]
99
+ : deleteCharAt(newValue[cursor.line], cursor.column - 1);
100
+ newValue[cursor.line] = newStr;
101
+ }
102
+ return newValue;
103
+ });
104
+ setCursor((prev) => {
105
+ return {
106
+ line: wrap ? prev.line - 1 : prev.line,
107
+ column: lastLineLenth
108
+ ? lastLineLenth
109
+ : Math.max(prev.column - 1, 0),
110
+ };
111
+ });
112
+ }));
113
+ unBinds.push(boundKeyboard(["up"], () => {
114
+ setCursor((prev) => {
115
+ return {
116
+ line: Math.max(prev.line - 1, 0),
117
+ column: Math.min(value[Math.max(prev.line - 1, 0)].length, prev.column),
118
+ };
119
+ });
120
+ }));
121
+ unBinds.push(boundKeyboard(["down"], () => {
122
+ setCursor((prev) => {
123
+ return {
124
+ line: Math.min(prev.line + 1, value.length - 1),
125
+ column: Math.min(value[Math.min(prev.line + 1, value.length - 1)].length, prev.column),
126
+ };
127
+ });
128
+ }));
129
+ unBinds.push(boundKeyboard(["tab"], () => {
130
+ setValue((prev) => {
131
+ const newValue = [...prev];
132
+ newValue[cursor.line] = insertStr(newValue[cursor.line], cursor.column, " ".repeat(numberOfIndentationSpaces));
133
+ return newValue;
134
+ });
135
+ setCursor((prev) => ({
136
+ line: prev.line,
137
+ column: prev.column + numberOfIndentationSpaces,
138
+ }));
139
+ }));
140
+ unBinds.push(boundKeyboard(["shift+tab"], () => {
141
+ setValue((prev) => {
142
+ const newValue = [...prev];
143
+ const currentLine = newValue[cursor.line];
144
+ const leadingSpaces = currentLine.match(/^ */)?.[0].length || 0;
145
+ if (leadingSpaces >= numberOfIndentationSpaces) {
146
+ newValue[cursor.line] = deleteCharAt(currentLine, 0, numberOfIndentationSpaces);
147
+ setCursor((prevCursor) => ({
148
+ line: prevCursor.line,
149
+ column: Math.max(0, prevCursor.column - numberOfIndentationSpaces),
150
+ }));
151
+ }
152
+ else if (leadingSpaces > 0) {
153
+ newValue[cursor.line] = deleteCharAt(currentLine, 0, leadingSpaces);
154
+ setCursor((prevCursor) => ({
155
+ line: prevCursor.line,
156
+ column: Math.max(0, prevCursor.column - leadingSpaces),
157
+ }));
158
+ }
159
+ return newValue;
160
+ });
161
+ }));
162
+ return () => {
163
+ removeWildcardPrecedence();
164
+ unBinds.forEach((each) => each());
165
+ };
166
+ }, [boundKeyboard, cursor.column, cursor.line, enableWildcardPriority]);
167
+ const currentLineText = useMemo(() => value[cursor.line] ?? "", [value, cursor.line]);
168
+ const clampedColumn = useMemo(() => Math.max(0, Math.min(currentLineText.length, cursor.column)), [cursor.column, currentLineText]);
169
+ const visualColumn = useMemo(() => stringWidth(currentLineText.slice(0, clampedColumn)), [stringWidth, currentLineText, clampedColumn]);
170
+ const x = useMemo(() => visualColumn +
171
+ currentLineNumberLength +
172
+ checkedLineNumberRightSpacing +
173
+ cursorOffset, [
174
+ visualColumn,
175
+ currentLineNumberLength,
176
+ checkedLineNumberRightSpacing,
177
+ cursorOffset,
178
+ ]);
179
+ const y = useMemo(() => cursor.line + cursorHeightOffset, [cursor.line, cursorHeightOffset]);
180
+ // Testing has revealed a flaw in the cursor operations provided by ink.
181
+ // It could also be due to a specific device.
182
+ // On arm64-based devices—such as when running in Termux—this causes cursor misalignment.
183
+ // The cursor works normally everywhere else.
184
+ // I also don't know how to solve this problem elegantly.
185
+ setCursorPosition({ x: x, y: y });
186
+ return (React.createElement(Box, { ref: boxRef, height: "100%", width: "100%", backgroundColor: "#1e1e1e", flexDirection: "column" }, value.map((each, number) => {
187
+ return (React.createElement(Box, { key: number, flexDirection: "row" },
188
+ React.createElement(Box, { width: Math.max(currentLineNumberLength - 1, 1), justifyContent: "flex-end", marginRight: checkedLineNumberRightSpacing },
189
+ React.createElement(Text, { bold: true }, number)),
190
+ React.createElement(Text, null, each)));
191
+ })));
192
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ export type Information = {
3
+ /**
4
+ * current mode
5
+ */
6
+ mode: string;
7
+ };
8
+ export declare function InformationBar({ mode }: Information): React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ export function InformationBar({ mode }) {
4
+ return React.createElement(Box, { width: "100%", height: 1, backgroundColor: "blue", flexDirection: 'row', paddingLeft: 2 },
5
+ React.createElement(Text, { color: 'white', bold: true }, mode));
6
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.1";
1
+ export {};
package/dist/index.js CHANGED
@@ -1,3 +1,16 @@
1
- // blots-editor — a lightweight Markdown editor for the terminal.
2
- // Built on ink-cartridge's screen and keyboard systems.
3
- export const VERSION = '0.0.1';
1
+ import { Box, render } from "ink";
2
+ import React from "react";
3
+ import { InformationBar } from "./core/view/information-bar.js";
4
+ import { CurrentScreen, KeyboardProvider, registerComponent, ScenarioManagementProvider, } from "ink-cartridge";
5
+ import { Editor } from "./core/view/editor.js";
6
+ function App() {
7
+ return (React.createElement(Box, { flexDirection: "column", height: "100%", width: "100%" },
8
+ React.createElement(Box, { flexGrow: 1 },
9
+ React.createElement(InformationBar, { mode: "NONE" })),
10
+ React.createElement(Box, { height: "100%", width: "100%" },
11
+ React.createElement(Editor, { value: "\u4F60\u597D", onChance: () => { } }))));
12
+ }
13
+ registerComponent(App, {});
14
+ render(React.createElement(ScenarioManagementProvider, { defaultScreen: App, fullScreen: true },
15
+ React.createElement(KeyboardProvider, { autoTab: false },
16
+ React.createElement(CurrentScreen, null))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blots-editor",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A lightweight Markdown editor for the terminal, built on ink-cartridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsc",
28
+ "start": "tsx src/index.tsx",
28
29
  "clean": "rm -rf dist",
29
30
  "test": "vitest run",
30
31
  "test:watch": "vitest"
@@ -49,7 +50,8 @@
49
50
  "react": ">=18"
50
51
  },
51
52
  "dependencies": {
52
- "ink-cartridge": "*"
53
+ "ink-cartridge": "*",
54
+ "string-width": "^8.2.2"
53
55
  },
54
56
  "devDependencies": {
55
57
  "@types/node": "^20.19.39",