reslop 0.0.1 → 0.1.0

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/lib/keys.js ADDED
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ const ACTIONS = [
4
+ { id: 'add', keys: ['a'], label: 'Add' },
5
+ { id: 'unstage', keys: ['u'], label: 'Unstage' },
6
+ { id: 'revert', keys: ['r'], label: 'Revert' },
7
+ { id: 'skip', keys: ['s'], label: 'Skip' },
8
+ { id: 'prev', keys: ['left', 'p'], hot: '←', label: 'Prev' },
9
+ { id: 'next', keys: ['right', 'n'], hot: '→', label: 'Next' },
10
+ { id: 'layout', keys: ['m'], label: 'Mode' },
11
+ { id: 'files', keys: ['l'], label: 'Files' },
12
+ { id: 'feedback', keys: ['f'], label: 'Feedback' },
13
+ { id: 'todo', keys: ['t'], label: 'Todo' },
14
+ { id: 'quit', keys: ['q', 'ctrl-c'], label: 'Quit' },
15
+ ];
16
+
17
+ const SCROLL = {
18
+ up: 'scrollUp',
19
+ k: 'scrollUp',
20
+ down: 'scrollDown',
21
+ j: 'scrollDown',
22
+ h: 'help',
23
+ '?': 'help',
24
+ enter: 'open',
25
+ backspace: 'removeTodo',
26
+ delete: 'removeTodo',
27
+ };
28
+
29
+ const KEY_TO_ACTION = Object.create(null);
30
+
31
+ for (const action of ACTIONS) {
32
+ for (const key of action.keys) KEY_TO_ACTION[key] = action.id;
33
+ }
34
+
35
+ for (const [key, id] of Object.entries(SCROLL)) KEY_TO_ACTION[key] = id;
36
+
37
+ const CSI_KEYS = {
38
+ A: 'up',
39
+ B: 'down',
40
+ C: 'right',
41
+ D: 'left',
42
+ H: 'home',
43
+ F: 'end',
44
+ };
45
+
46
+ const CSI_TILDE = Object.assign(Object.create(null), {
47
+ 1: 'home',
48
+ 3: 'delete',
49
+ 4: 'end',
50
+ });
51
+
52
+ const MOUSE_WHEEL = Object.assign(Object.create(null), {
53
+ 64: 'wheelUp',
54
+ 65: 'wheelDown',
55
+ });
56
+
57
+ const mouseKindFromButton = (button, press) => {
58
+ const wheel = MOUSE_WHEEL[button];
59
+ if (wheel) return wheel;
60
+ if (!press) return 'release';
61
+ if (button & 32) return 'drag';
62
+ return 'press';
63
+ };
64
+
65
+ const mouseKind = (event) => {
66
+ if (event.kind) return event.kind;
67
+ return mouseKindFromButton(event.button, event.press);
68
+ };
69
+
70
+ const parseMouse = (body, flag) => {
71
+ const parts = body.split(';');
72
+ if (parts.length < 3) return null;
73
+ const button = parseInt(parts[0], 10);
74
+ const x = parseInt(parts[1], 10);
75
+ const y = parseInt(parts[2], 10);
76
+ if (!Number.isFinite(button) || !Number.isFinite(x) || !Number.isFinite(y)) {
77
+ return null;
78
+ }
79
+ const press = flag === 'M';
80
+ const kind = mouseKindFromButton(button, press);
81
+ return { type: 'mouse', button, x, y, press, kind, btn: button & 3 };
82
+ };
83
+
84
+ const parseEsc = (text, start) => {
85
+ const rest = text.slice(start);
86
+ if (rest === '\x1b') return { needMore: true };
87
+ if (rest.startsWith('\x1b[') && rest.length === 2) return { needMore: true };
88
+ if (rest.startsWith('\x1b[<')) {
89
+ const endM = rest.search(/[Mm]/);
90
+ if (endM < 0) return { needMore: true };
91
+ const kind = rest[endM];
92
+ const body = rest.slice(3, endM);
93
+ const mouse = parseMouse(body, kind);
94
+ if (!mouse) {
95
+ return { event: { type: 'key', key: 'escape' }, end: start + endM + 1 };
96
+ }
97
+ return { event: mouse, end: start + endM + 1 };
98
+ }
99
+ if (rest.startsWith('\x1b[')) {
100
+ const code = rest[2];
101
+ if (code === undefined) return { needMore: true };
102
+ if (code >= '0' && code <= '9') {
103
+ const tilde = rest.indexOf('~');
104
+ if (tilde < 0) {
105
+ if (rest.length > 8) {
106
+ return { event: { type: 'key', key: 'escape' }, end: start + 1 };
107
+ }
108
+ return { needMore: true };
109
+ }
110
+ const num = rest.slice(2, tilde);
111
+ const named = CSI_TILDE[num];
112
+ if (named) {
113
+ return { event: { type: 'key', key: named }, end: start + tilde + 1 };
114
+ }
115
+ return { event: { type: 'key', key: 'escape' }, end: start + 1 };
116
+ }
117
+ const key = CSI_KEYS[code];
118
+ if (key) return { event: { type: 'key', key }, end: start + 3 };
119
+ return { event: { type: 'key', key: 'escape' }, end: start + 1 };
120
+ }
121
+ return { event: { type: 'key', key: 'escape' }, end: start + 1 };
122
+ };
123
+
124
+ const LITERAL_KEY = Object.assign(Object.create(null), {
125
+ '\x7f': 'backspace',
126
+ '\x08': 'backspace',
127
+ '\t': 'tab',
128
+ '\x03': 'ctrl-c',
129
+ '\r': 'enter',
130
+ '\n': 'enter',
131
+ });
132
+
133
+ const decodeChunk = (text, carry = '') => {
134
+ const source = carry + text;
135
+ const events = [];
136
+ let i = 0;
137
+ while (i < source.length) {
138
+ const ch = source[i];
139
+ if (ch === '\x1b') {
140
+ const parsed = parseEsc(source, i);
141
+ if (parsed.needMore) return { events, carry: source.slice(i) };
142
+ events.push(parsed.event);
143
+ i = parsed.end;
144
+ continue;
145
+ }
146
+ const named = LITERAL_KEY[ch];
147
+ if (named) {
148
+ events.push({ type: 'key', key: named });
149
+ i += 1;
150
+ continue;
151
+ }
152
+ const code = source.charCodeAt(i);
153
+ if (code >= 1 && code <= 26) {
154
+ const letter = String.fromCharCode(96 + code);
155
+ events.push({ type: 'key', key: `ctrl-${letter}` });
156
+ i += 1;
157
+ continue;
158
+ }
159
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < source.length) {
160
+ events.push({ type: 'key', key: source.slice(i, i + 2) });
161
+ i += 2;
162
+ continue;
163
+ }
164
+ events.push({ type: 'key', key: ch });
165
+ i += 1;
166
+ }
167
+ return { events, carry: '' };
168
+ };
169
+
170
+ const actionFromKey = (key) => KEY_TO_ACTION[key] ?? null;
171
+
172
+ const actionHot = (action) => action.hot ?? action.keys[0];
173
+
174
+ const actionLetter = (action) => {
175
+ if (action.hot) return action.hot;
176
+ const label = (action.label ?? '').toLowerCase();
177
+ for (const key of action.keys) {
178
+ if (key.length !== 1) continue;
179
+ if (label.includes(key)) return key;
180
+ }
181
+ return actionHot(action);
182
+ };
183
+
184
+ const buttonWord = (action) => {
185
+ const word = (action.label ?? '').toLowerCase();
186
+ const mark = actionLetter(action);
187
+ if (word.includes(mark)) return word;
188
+ return `${mark}${word}`;
189
+ };
190
+
191
+ const hitAction = (hits, x) => {
192
+ for (const hit of hits) {
193
+ if (x >= hit.x0 && x < hit.x1) return hit.id;
194
+ }
195
+ return null;
196
+ };
197
+
198
+ module.exports = {
199
+ ACTIONS,
200
+ KEY_TO_ACTION,
201
+ decodeChunk,
202
+ actionFromKey,
203
+ actionHot,
204
+ actionLetter,
205
+ buttonWord,
206
+ hitAction,
207
+ mouseKind,
208
+ };