@emaxe/tuigram 1.2.0 → 1.3.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/CHANGELOG.md +19 -1
- package/CHANGELOG.ru.md +19 -1
- package/README.md +23 -0
- package/README.ru.md +23 -0
- package/package.json +1 -1
- package/src/ui/app.js +115 -2
- package/src/ui/components/chatList.js +62 -0
- package/src/ui/components/chatView.js +65 -8
- package/src/ui/components/header.js +24 -1
- package/src/ui/components/inputBox.js +40 -1
- package/src/ui/components/modals/actionModal.js +31 -0
- package/src/ui/components/modals/chatInfoModal.js +19 -0
- package/src/ui/components/modals/confirmModal.js +18 -0
- package/src/ui/components/modals/fileModal.js +33 -1
- package/src/ui/components/modals/filePickerModal.js +19 -0
- package/src/ui/components/modals/helpModal.js +24 -0
- package/src/ui/components/statusBar.js +53 -1
- package/src/ui/screen.js +52 -3
- package/src/utils/mouse.js +236 -0
|
@@ -2,6 +2,8 @@ import blessed from "neo-blessed";
|
|
|
2
2
|
import { escapeBlessed } from "../../telegram/formatter.js";
|
|
3
3
|
import { fg, badge } from "../theme.js";
|
|
4
4
|
|
|
5
|
+
import { getInputContextActionAt } from "../../utils/mouse.js";
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Создаёт компонент поля ввода сообщения (нижняя панель).
|
|
7
9
|
* @param {blessed.Widgets.Screen} screen
|
|
@@ -10,8 +12,16 @@ import { fg, badge } from "../theme.js";
|
|
|
10
12
|
* @param {(text: string, context: { mode: string|null, target: object|null }) => void} callbacks.onSubmit
|
|
11
13
|
* @param {() => void} [callbacks.onCancelContext]
|
|
12
14
|
* @param {(command: string, args: string[]) => void} [callbacks.onSlashCommand]
|
|
15
|
+
* @param {() => void} [callbacks.onReplyLast]
|
|
16
|
+
* @param {() => void} [callbacks.onEditLast]
|
|
13
17
|
*/
|
|
14
|
-
export function createInputBox(screen, theme, {
|
|
18
|
+
export function createInputBox(screen, theme, {
|
|
19
|
+
onSubmit,
|
|
20
|
+
onCancelContext,
|
|
21
|
+
onSlashCommand,
|
|
22
|
+
onReplyLast,
|
|
23
|
+
onEditLast,
|
|
24
|
+
} = {}) {
|
|
15
25
|
// Высота 5 = рамка (2) + контекстная плашка (1) + две строки ввода (2).
|
|
16
26
|
// При autoPadding у blessed рамка съедает по строке сверху и снизу, поэтому
|
|
17
27
|
// меньшая высота оставляет textarea нулевую высоту и вводимый текст не виден.
|
|
@@ -21,6 +31,7 @@ export function createInputBox(screen, theme, { onSubmit, onCancelContext, onSla
|
|
|
21
31
|
left: "35%",
|
|
22
32
|
right: 0,
|
|
23
33
|
height: 5,
|
|
34
|
+
mouse: true,
|
|
24
35
|
border: {
|
|
25
36
|
type: "line",
|
|
26
37
|
},
|
|
@@ -41,6 +52,8 @@ export function createInputBox(screen, theme, { onSubmit, onCancelContext, onSla
|
|
|
41
52
|
right: 0,
|
|
42
53
|
height: 1,
|
|
43
54
|
tags: true,
|
|
55
|
+
mouse: true,
|
|
56
|
+
clickable: true,
|
|
44
57
|
style: {
|
|
45
58
|
bg: theme.input.contextBg,
|
|
46
59
|
fg: theme.input.contextFg,
|
|
@@ -110,6 +123,32 @@ export function createInputBox(screen, theme, { onSubmit, onCancelContext, onSla
|
|
|
110
123
|
screen.render();
|
|
111
124
|
}
|
|
112
125
|
|
|
126
|
+
contextBar.on("click", (data) => {
|
|
127
|
+
const relX = data.x - (contextBar.aleft || 0);
|
|
128
|
+
const action = getInputContextActionAt(relX, currentMode);
|
|
129
|
+
if (action === "cancel") {
|
|
130
|
+
if (currentMode) {
|
|
131
|
+
currentMode = null;
|
|
132
|
+
currentTarget = null;
|
|
133
|
+
renderContext();
|
|
134
|
+
onCancelContext?.();
|
|
135
|
+
}
|
|
136
|
+
} else if (action === "reply") {
|
|
137
|
+
onReplyLast?.();
|
|
138
|
+
} else if (action === "edit") {
|
|
139
|
+
onEditLast?.();
|
|
140
|
+
} else if (action === "commands") {
|
|
141
|
+
textarea.setValue("/");
|
|
142
|
+
textarea.focus();
|
|
143
|
+
screen.render();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
textarea.on("click", () => {
|
|
148
|
+
textarea.focus();
|
|
149
|
+
screen.render();
|
|
150
|
+
});
|
|
151
|
+
|
|
113
152
|
textarea.key(["enter"], () => {
|
|
114
153
|
const value = textarea.getValue().trim();
|
|
115
154
|
if (!value) {
|
|
@@ -2,6 +2,8 @@ import blessed from "neo-blessed";
|
|
|
2
2
|
import { escapeBlessed } from "../../../telegram/formatter.js";
|
|
3
3
|
import { fg } from "../../theme.js";
|
|
4
4
|
|
|
5
|
+
import { isInsideBox } from "../../../utils/mouse.js";
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Создаёт модальное окно контекстных действий над сообщением.
|
|
7
9
|
* @param {blessed.Widgets.Screen} screen
|
|
@@ -19,6 +21,7 @@ export function createActionModal(screen, theme, { onAction } = {}) {
|
|
|
19
21
|
height: "55%",
|
|
20
22
|
hidden: true,
|
|
21
23
|
tags: true,
|
|
24
|
+
mouse: true,
|
|
22
25
|
border: {
|
|
23
26
|
type: "line",
|
|
24
27
|
},
|
|
@@ -66,6 +69,20 @@ export function createActionModal(screen, theme, { onAction } = {}) {
|
|
|
66
69
|
},
|
|
67
70
|
});
|
|
68
71
|
|
|
72
|
+
const baseCreateItem = list.createItem.bind(list);
|
|
73
|
+
list.createItem = (content) => {
|
|
74
|
+
const item = baseCreateItem(content);
|
|
75
|
+
item.on("click", () => {
|
|
76
|
+
const index = list.getItemIndex(item);
|
|
77
|
+
const action = currentActions[index];
|
|
78
|
+
if (action && currentMsg) {
|
|
79
|
+
hide();
|
|
80
|
+
onAction?.(action.id, currentMsg);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return item;
|
|
84
|
+
};
|
|
85
|
+
|
|
69
86
|
let currentMsg = null;
|
|
70
87
|
let currentActions = [];
|
|
71
88
|
let previousFocus = null;
|
|
@@ -128,6 +145,20 @@ export function createActionModal(screen, theme, { onAction } = {}) {
|
|
|
128
145
|
// Фокус получает список — на нём и живут клавиши закрытия.
|
|
129
146
|
list.key(["escape", "q"], hide);
|
|
130
147
|
|
|
148
|
+
// Закрытие при клике мышью мимо модального окна
|
|
149
|
+
screen.on("click", (data) => {
|
|
150
|
+
if (!modal.visible) return;
|
|
151
|
+
const inside = isInsideBox(data.x, data.y, {
|
|
152
|
+
left: modal.aleft,
|
|
153
|
+
top: modal.atop,
|
|
154
|
+
width: modal.width,
|
|
155
|
+
height: modal.height,
|
|
156
|
+
});
|
|
157
|
+
if (!inside) {
|
|
158
|
+
hide();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
131
162
|
return {
|
|
132
163
|
modal,
|
|
133
164
|
show,
|
|
@@ -3,6 +3,8 @@ import { escapeBlessed } from "../../../telegram/formatter.js";
|
|
|
3
3
|
import { idToString } from "../../../telegram/entities.js";
|
|
4
4
|
import { fg } from "../../theme.js";
|
|
5
5
|
|
|
6
|
+
import { isInsideBox } from "../../../utils/mouse.js";
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Создаёт модальное окно информации о текущем чате.
|
|
8
10
|
* @param {blessed.Widgets.Screen} screen
|
|
@@ -18,6 +20,7 @@ export function createChatInfoModal(screen, theme) {
|
|
|
18
20
|
height: "60%",
|
|
19
21
|
hidden: true,
|
|
20
22
|
tags: true,
|
|
23
|
+
mouse: true,
|
|
21
24
|
border: {
|
|
22
25
|
type: "line",
|
|
23
26
|
},
|
|
@@ -38,6 +41,7 @@ export function createChatInfoModal(screen, theme) {
|
|
|
38
41
|
right: 2,
|
|
39
42
|
bottom: 3,
|
|
40
43
|
tags: true,
|
|
44
|
+
mouse: true,
|
|
41
45
|
scrollable: true,
|
|
42
46
|
style: {
|
|
43
47
|
bg: theme.modal.bg,
|
|
@@ -114,9 +118,24 @@ export function createChatInfoModal(screen, theme) {
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
closeBtn.on("press", hide);
|
|
121
|
+
closeBtn.on("click", hide);
|
|
117
122
|
// Клавиши вешаем на кнопку: blessed отдаёт события только сфокусированному элементу.
|
|
118
123
|
closeBtn.key(["escape", "q"], hide);
|
|
119
124
|
|
|
125
|
+
// Закрытие при клике мышью мимо модального окна
|
|
126
|
+
screen.on("click", (data) => {
|
|
127
|
+
if (!modal.visible) return;
|
|
128
|
+
const inside = isInsideBox(data.x, data.y, {
|
|
129
|
+
left: modal.aleft,
|
|
130
|
+
top: modal.atop,
|
|
131
|
+
width: modal.width,
|
|
132
|
+
height: modal.height,
|
|
133
|
+
});
|
|
134
|
+
if (!inside) {
|
|
135
|
+
hide();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
120
139
|
return {
|
|
121
140
|
modal,
|
|
122
141
|
show,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import blessed from "neo-blessed";
|
|
2
2
|
import { fg } from "../../theme.js";
|
|
3
|
+
import { isInsideBox } from "../../../utils/mouse.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Создаёт модальное окно подтверждения действия (Да / Нет).
|
|
@@ -19,6 +20,7 @@ export function createConfirmModal(screen, theme) {
|
|
|
19
20
|
height: 8,
|
|
20
21
|
hidden: true,
|
|
21
22
|
tags: true,
|
|
23
|
+
mouse: true,
|
|
22
24
|
border: {
|
|
23
25
|
type: "line",
|
|
24
26
|
},
|
|
@@ -113,7 +115,9 @@ export function createConfirmModal(screen, theme) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
yesBtn.on("press", confirm);
|
|
118
|
+
yesBtn.on("click", confirm);
|
|
116
119
|
noBtn.on("press", hide);
|
|
120
|
+
noBtn.on("click", hide);
|
|
117
121
|
|
|
118
122
|
// Клавиши вешаем на обе кнопки — активна всегда одна из них.
|
|
119
123
|
for (const btn of [yesBtn, noBtn]) {
|
|
@@ -125,6 +129,20 @@ export function createConfirmModal(screen, theme) {
|
|
|
125
129
|
});
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
// Закрытие при клике мышью мимо модального окна
|
|
133
|
+
screen.on("click", (data) => {
|
|
134
|
+
if (!modal.visible) return;
|
|
135
|
+
const inside = isInsideBox(data.x, data.y, {
|
|
136
|
+
left: modal.aleft,
|
|
137
|
+
top: modal.atop,
|
|
138
|
+
width: modal.width,
|
|
139
|
+
height: modal.height,
|
|
140
|
+
});
|
|
141
|
+
if (!inside) {
|
|
142
|
+
hide();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
128
146
|
return {
|
|
129
147
|
modal,
|
|
130
148
|
ask: (text, onConfirm) => {
|
|
@@ -4,6 +4,7 @@ import { inspectLocalFile } from "../../../utils/storage.js";
|
|
|
4
4
|
import { formatFileSize } from "../../../utils/time.js";
|
|
5
5
|
import { createFilePickerModal } from "./filePickerModal.js";
|
|
6
6
|
import { fg } from "../../theme.js";
|
|
7
|
+
import { isInsideBox } from "../../../utils/mouse.js";
|
|
7
8
|
|
|
8
9
|
/** Разделитель нескольких путей в поле ввода. */
|
|
9
10
|
const PATH_SEPARATOR = "|";
|
|
@@ -27,6 +28,7 @@ export function createFileModal(screen, theme, { onSendFile } = {}) {
|
|
|
27
28
|
height: 14,
|
|
28
29
|
hidden: true,
|
|
29
30
|
tags: true,
|
|
31
|
+
mouse: true,
|
|
30
32
|
border: {
|
|
31
33
|
type: "line",
|
|
32
34
|
},
|
|
@@ -51,13 +53,15 @@ export function createFileModal(screen, theme, { onSendFile } = {}) {
|
|
|
51
53
|
style: { bg: theme.modal.bg, fg: theme.modal.fg },
|
|
52
54
|
});
|
|
53
55
|
|
|
54
|
-
blessed.box({
|
|
56
|
+
const browseBar = blessed.box({
|
|
55
57
|
parent: modal,
|
|
56
58
|
top: 2,
|
|
57
59
|
left: 2,
|
|
58
60
|
right: 2,
|
|
59
61
|
height: 1,
|
|
60
62
|
tags: true,
|
|
63
|
+
mouse: true,
|
|
64
|
+
clickable: true,
|
|
61
65
|
content: `Путь к файлу ${fg(theme.modal.hintFg, `(несколько — через ${PATH_SEPARATOR})`)}{|}${fg(theme.accent, "[Ctrl+F] Обзор")} `,
|
|
62
66
|
style: { bg: theme.modal.bg, fg: theme.modal.fg },
|
|
63
67
|
});
|
|
@@ -69,6 +73,7 @@ export function createFileModal(screen, theme, { onSendFile } = {}) {
|
|
|
69
73
|
right: 2,
|
|
70
74
|
height: 1,
|
|
71
75
|
inputOnFocus: true,
|
|
76
|
+
mouse: true,
|
|
72
77
|
style: {
|
|
73
78
|
bg: theme.modal.inputBg,
|
|
74
79
|
fg: theme.modal.inputFg,
|
|
@@ -93,6 +98,7 @@ export function createFileModal(screen, theme, { onSendFile } = {}) {
|
|
|
93
98
|
right: 2,
|
|
94
99
|
height: 1,
|
|
95
100
|
inputOnFocus: true,
|
|
101
|
+
mouse: true,
|
|
96
102
|
style: {
|
|
97
103
|
bg: theme.modal.inputBg,
|
|
98
104
|
fg: theme.modal.inputFg,
|
|
@@ -338,10 +344,36 @@ export function createFileModal(screen, theme, { onSendFile } = {}) {
|
|
|
338
344
|
});
|
|
339
345
|
captionInput.on("submit", submit);
|
|
340
346
|
sendBtn.on("press", submit);
|
|
347
|
+
sendBtn.on("click", submit);
|
|
341
348
|
cancelBtn.on("press", hide);
|
|
349
|
+
cancelBtn.on("click", hide);
|
|
342
350
|
asDocumentCheck.on("check", () => validate({ quiet: true }));
|
|
343
351
|
asDocumentCheck.on("uncheck", () => validate({ quiet: true }));
|
|
344
352
|
|
|
353
|
+
browseBar.on("click", openPicker);
|
|
354
|
+
pathInput.on("click", () => {
|
|
355
|
+
pathInput.focus();
|
|
356
|
+
screen.render();
|
|
357
|
+
});
|
|
358
|
+
captionInput.on("click", () => {
|
|
359
|
+
captionInput.focus();
|
|
360
|
+
screen.render();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Закрытие при клике мышью мимо модального окна
|
|
364
|
+
screen.on("click", (data) => {
|
|
365
|
+
if (!modal.visible || picker.modal.visible) return;
|
|
366
|
+
const inside = isInsideBox(data.x, data.y, {
|
|
367
|
+
left: modal.aleft,
|
|
368
|
+
top: modal.atop,
|
|
369
|
+
width: modal.width,
|
|
370
|
+
height: modal.height,
|
|
371
|
+
});
|
|
372
|
+
if (!inside) {
|
|
373
|
+
hide();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
345
377
|
return {
|
|
346
378
|
modal,
|
|
347
379
|
pathInput,
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { formatFileSize } from "../../../utils/time.js";
|
|
6
6
|
import { fg } from "../../theme.js";
|
|
7
|
+
import { isInsideBox } from "../../../utils/mouse.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Модальное окно выбора локального файла (обёртка над blessed.filemanager).
|
|
@@ -23,6 +24,7 @@ export function createFilePickerModal(screen, theme) {
|
|
|
23
24
|
height: "60%",
|
|
24
25
|
hidden: true,
|
|
25
26
|
tags: true,
|
|
27
|
+
mouse: true,
|
|
26
28
|
border: {
|
|
27
29
|
type: "line",
|
|
28
30
|
},
|
|
@@ -78,6 +80,8 @@ export function createFilePickerModal(screen, theme) {
|
|
|
78
80
|
right: 1,
|
|
79
81
|
height: 2,
|
|
80
82
|
tags: true,
|
|
83
|
+
mouse: true,
|
|
84
|
+
clickable: true,
|
|
81
85
|
style: { bg: theme.modal.bg, fg: theme.modal.fg },
|
|
82
86
|
});
|
|
83
87
|
|
|
@@ -187,6 +191,21 @@ export function createFilePickerModal(screen, theme) {
|
|
|
187
191
|
// filemanager сам отдаёт "cancel" по Escape (list.js), но у него нет hide()
|
|
188
192
|
manager.key(["escape", "q"], hide);
|
|
189
193
|
manager.on("cancel", hide);
|
|
194
|
+
footer.on("click", hide);
|
|
195
|
+
|
|
196
|
+
// Закрытие при клике мышью мимо модального окна
|
|
197
|
+
screen.on("click", (data) => {
|
|
198
|
+
if (!modal.visible) return;
|
|
199
|
+
const inside = isInsideBox(data.x, data.y, {
|
|
200
|
+
left: modal.aleft,
|
|
201
|
+
top: modal.atop,
|
|
202
|
+
width: modal.width,
|
|
203
|
+
height: modal.height,
|
|
204
|
+
});
|
|
205
|
+
if (!inside) {
|
|
206
|
+
hide();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
190
209
|
|
|
191
210
|
return {
|
|
192
211
|
modal,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import blessed from "neo-blessed";
|
|
2
|
+
import { isInsideBox } from "../../../utils/mouse.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Создаёт модальное окно справки по всем горячим клавишам и возможностям.
|
|
@@ -15,6 +16,8 @@ export function createHelpModal(screen, theme) {
|
|
|
15
16
|
height: "80%",
|
|
16
17
|
hidden: true,
|
|
17
18
|
tags: true,
|
|
19
|
+
mouse: true,
|
|
20
|
+
scrollable: true,
|
|
18
21
|
border: {
|
|
19
22
|
type: "line",
|
|
20
23
|
},
|
|
@@ -46,6 +49,12 @@ export function createHelpModal(screen, theme) {
|
|
|
46
49
|
${K_CYAN}[PageUp] / [Ctrl+U]${K_END} Прокрутка сообщений вверх / Подгрузка старой истории
|
|
47
50
|
${K_CYAN}[PageDown] / [Ctrl+D]${K_END}Прокрутка сообщений вниз
|
|
48
51
|
|
|
52
|
+
{bold}Управление мышью:{/bold}
|
|
53
|
+
${K_CYAN}Клик по диалогу${K_END} Мгновенно открыть чат
|
|
54
|
+
${K_CYAN}Колесо мыши${K_END} Прокрутка списка чатов и сообщений (вверх — подгрузка истории)
|
|
55
|
+
${K_CYAN}Клик по сообщению${K_END} Открыть меню действий над сообщением
|
|
56
|
+
${K_CYAN}Клик по вкладкам/кнопкам${K_END} Переключение фильтров и вызов действий
|
|
57
|
+
|
|
49
58
|
{bold}Вкладки фильтрации диалогов (нажмите цифру в списке чатов):{/bold}
|
|
50
59
|
${K_YELLOW}[1]${K_END} Все чаты ${K_YELLOW}[2]${K_END} Личные (ЛС) ${K_YELLOW}[3]${K_END} Группы
|
|
51
60
|
${K_YELLOW}[4]${K_END} Каналы ${K_YELLOW}[5]${K_END} Боты ${K_YELLOW}[6]${K_END} Непрочитанные
|
|
@@ -120,9 +129,24 @@ export function createHelpModal(screen, theme) {
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
closeBtn.on("press", hide);
|
|
132
|
+
closeBtn.on("click", hide);
|
|
123
133
|
// Клавиши вешаем на кнопку: blessed отдаёт события только сфокусированному элементу.
|
|
124
134
|
closeBtn.key(["escape", "q", "f1"], hide);
|
|
125
135
|
|
|
136
|
+
// Закрытие при клике мышью мимо модального окна
|
|
137
|
+
screen.on("click", (data) => {
|
|
138
|
+
if (!modal.visible) return;
|
|
139
|
+
const inside = isInsideBox(data.x, data.y, {
|
|
140
|
+
left: modal.aleft,
|
|
141
|
+
top: modal.atop,
|
|
142
|
+
width: modal.width,
|
|
143
|
+
height: modal.height,
|
|
144
|
+
});
|
|
145
|
+
if (!inside) {
|
|
146
|
+
hide();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
126
150
|
return {
|
|
127
151
|
modal,
|
|
128
152
|
show,
|
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import blessed from "neo-blessed";
|
|
2
2
|
import { fg } from "../theme.js";
|
|
3
3
|
|
|
4
|
+
import { getStatusBarActionAt } from "../../utils/mouse.js";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Создаёт нижнюю строку состояния (Status Bar) с подсказками и временными тостами.
|
|
6
8
|
* @param {blessed.Widgets.Screen} screen
|
|
7
9
|
* @param {object} theme
|
|
10
|
+
* @param {object} [callbacks]
|
|
11
|
+
* @param {() => void} [callbacks.onFocusNext]
|
|
12
|
+
* @param {() => void} [callbacks.onSelectOrSubmit]
|
|
13
|
+
* @param {() => void} [callbacks.onFilterTabs]
|
|
14
|
+
* @param {() => void} [callbacks.onSearch]
|
|
15
|
+
* @param {() => void} [callbacks.onHelp]
|
|
16
|
+
* @param {() => void} [callbacks.onAction]
|
|
17
|
+
* @param {() => void} [callbacks.onChatInfo]
|
|
18
|
+
* @param {() => void} [callbacks.onQuit]
|
|
8
19
|
* @returns {blessed.Widgets.BoxElement & { showMessage: (text: string, type?: string, duration?: number) => void }}
|
|
9
20
|
*/
|
|
10
|
-
export function createStatusBar(screen, theme
|
|
21
|
+
export function createStatusBar(screen, theme, {
|
|
22
|
+
onFocusNext,
|
|
23
|
+
onSelectOrSubmit,
|
|
24
|
+
onFilterTabs,
|
|
25
|
+
onSearch,
|
|
26
|
+
onHelp,
|
|
27
|
+
onAction,
|
|
28
|
+
onChatInfo,
|
|
29
|
+
onQuit,
|
|
30
|
+
} = {}) {
|
|
11
31
|
const statusBar = blessed.box({
|
|
12
32
|
parent: screen,
|
|
13
33
|
bottom: 0,
|
|
@@ -15,12 +35,44 @@ export function createStatusBar(screen, theme) {
|
|
|
15
35
|
width: "100%",
|
|
16
36
|
height: 1,
|
|
17
37
|
tags: true,
|
|
38
|
+
mouse: true,
|
|
18
39
|
style: {
|
|
19
40
|
bg: theme.status.bg,
|
|
20
41
|
fg: theme.status.fg,
|
|
21
42
|
},
|
|
22
43
|
});
|
|
23
44
|
|
|
45
|
+
statusBar.on("click", (data) => {
|
|
46
|
+
const relX = data.x - (statusBar.aleft || 0);
|
|
47
|
+
const action = getStatusBarActionAt(relX, statusBar.width || 120);
|
|
48
|
+
switch (action) {
|
|
49
|
+
case "focus":
|
|
50
|
+
onFocusNext?.();
|
|
51
|
+
break;
|
|
52
|
+
case "select":
|
|
53
|
+
onSelectOrSubmit?.();
|
|
54
|
+
break;
|
|
55
|
+
case "tabs":
|
|
56
|
+
onFilterTabs?.();
|
|
57
|
+
break;
|
|
58
|
+
case "search":
|
|
59
|
+
onSearch?.();
|
|
60
|
+
break;
|
|
61
|
+
case "help":
|
|
62
|
+
onHelp?.();
|
|
63
|
+
break;
|
|
64
|
+
case "actions":
|
|
65
|
+
onAction?.();
|
|
66
|
+
break;
|
|
67
|
+
case "info":
|
|
68
|
+
onChatInfo?.();
|
|
69
|
+
break;
|
|
70
|
+
case "quit":
|
|
71
|
+
onQuit?.();
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
24
76
|
let currentTimeout = null;
|
|
25
77
|
|
|
26
78
|
const key = (label) => `{bold}${fg(theme.accent, label)}{/bold}`;
|
package/src/ui/screen.js
CHANGED
|
@@ -27,11 +27,15 @@ export const GLOBAL_KEYS = [
|
|
|
27
27
|
* @returns {blessed.Widgets.Screen}
|
|
28
28
|
*/
|
|
29
29
|
export function createScreen({ theme, onExit } = {}) {
|
|
30
|
+
// Форсируем поддержку SGR и CellMotion режимов в neo-blessed для современных терминалов
|
|
31
|
+
process.env.BLESSED_FORCE_MODES = "SGRMOUSE=1,CELLMOTION=1,VT200MOUSE=1,UTFMOUSE=0,ALLMOTION=0,URXVTMOUSE=1";
|
|
32
|
+
|
|
30
33
|
const screen = blessed.screen({
|
|
31
34
|
smartCSR: true,
|
|
32
35
|
title: "TuiGram - Telegram Terminal Client",
|
|
33
36
|
fullUnicode: true,
|
|
34
37
|
dockBorders: true,
|
|
38
|
+
sendFocus: true,
|
|
35
39
|
cursor: {
|
|
36
40
|
synthetic: true,
|
|
37
41
|
blink: true,
|
|
@@ -40,21 +44,66 @@ export function createScreen({ theme, onExit } = {}) {
|
|
|
40
44
|
style: {
|
|
41
45
|
bg: theme?.bg ?? "black",
|
|
42
46
|
fg: theme?.fg ?? "white",
|
|
43
|
-
}
|
|
47
|
+
},
|
|
44
48
|
});
|
|
45
49
|
|
|
46
50
|
screen.ignoreLocked = [...GLOBAL_KEYS];
|
|
47
51
|
|
|
52
|
+
// Настраиваем режимы мыши в program:
|
|
53
|
+
// SGR (1006) — стандарт для macOS Terminal, iTerm2, Alacritty, Kitty, Windows Terminal
|
|
54
|
+
// CellMotion (1002) — клики, зажатия и скролл
|
|
55
|
+
// VT200 (1000) — базовая поддержка кнопок
|
|
56
|
+
// urxvt (1015) — fallback
|
|
57
|
+
screen.program.setMouse({
|
|
58
|
+
vt200Mouse: true,
|
|
59
|
+
cellMotion: true,
|
|
60
|
+
allMotion: false,
|
|
61
|
+
sgrMouse: true,
|
|
62
|
+
urxvtMouse: true,
|
|
63
|
+
utfMouse: false,
|
|
64
|
+
}, true);
|
|
65
|
+
|
|
66
|
+
const enableMouseSeq = "\x1b[?1003l\x1b[?1005l\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1015h";
|
|
67
|
+
const disableMouseSeq = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l";
|
|
68
|
+
|
|
69
|
+
function sendEnableMouse() {
|
|
70
|
+
try {
|
|
71
|
+
if (screen.program && screen.program.output && typeof screen.program.output.write === "function") {
|
|
72
|
+
screen.program.output.write(enableMouseSeq);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Игнорируем в headless/тестах
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
sendEnableMouse();
|
|
80
|
+
screen.enableMouse();
|
|
81
|
+
|
|
82
|
+
// Отправляем escape-последовательности повторно при перерендере/ресайзе экрана
|
|
83
|
+
screen.on("render", sendEnableMouse);
|
|
84
|
+
screen.on("resize", sendEnableMouse);
|
|
85
|
+
|
|
86
|
+
// Пробрасываем событие click на уровне экрана при mouseup
|
|
87
|
+
screen.on("mouseup", (data) => {
|
|
88
|
+
screen.emit("click", data);
|
|
89
|
+
});
|
|
90
|
+
|
|
48
91
|
// Обработка закрытия терминала или аварийного прерывания
|
|
49
|
-
|
|
92
|
+
function cleanExit() {
|
|
50
93
|
try {
|
|
94
|
+
if (screen.program && screen.program.output && typeof screen.program.output.write === "function") {
|
|
95
|
+
screen.program.output.write(disableMouseSeq);
|
|
96
|
+
}
|
|
97
|
+
screen.program.disableMouse();
|
|
51
98
|
onExit?.();
|
|
52
99
|
} catch {
|
|
53
100
|
// Выходим в любом случае
|
|
54
101
|
}
|
|
55
102
|
screen.destroy();
|
|
56
103
|
process.exit(0);
|
|
57
|
-
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
screen.key(["C-c"], cleanExit);
|
|
58
107
|
|
|
59
108
|
return screen;
|
|
60
109
|
}
|