tuigram 1.0.0-alpha.1

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.
Files changed (75) hide show
  1. package/README.md +5 -0
  2. package/application/(common)/LinkedList.js +90 -0
  3. package/application/(common)/Obj/pick.js +1 -0
  4. package/application/(common)/Random/uuid.js +1 -0
  5. package/application/(common)/Rate/debounce.js +7 -0
  6. package/application/(common)/risk.js +8 -0
  7. package/application/(common)/riskAsync.js +8 -0
  8. package/application/1-messenger/(common)/Message.js +8 -0
  9. package/application/1-messenger/(methods)/getDialogs.js +24 -0
  10. package/application/1-messenger/(methods)/getHistory.js +25 -0
  11. package/application/1-messenger/(methods)/on.js +3 -0
  12. package/application/1-messenger/(methods)/sendMessage.js +1 -0
  13. package/application/1-messenger/1-tg.js +4 -0
  14. package/application/1-messenger/2-dispatcher.js +2 -0
  15. package/application/1-messenger/cache/chats.js +15 -0
  16. package/application/1-messenger/cache/dialogs.js +13 -0
  17. package/application/2-nvim/(methods)/emit.js +2 -0
  18. package/application/2-nvim/(methods)/input.js +1 -0
  19. package/application/2-nvim/(methods)/on.js +2 -0
  20. package/application/2-nvim/1-client.js +6 -0
  21. package/application/2-nvim/2-events.js +22 -0
  22. package/application/3-ui/1-screen/1-renderer.js +1 -0
  23. package/application/3-ui/1-screen/2-wrapper.js +11 -0
  24. package/application/3-ui/2-components/Box.js +1 -0
  25. package/application/3-ui/2-components/ScrollBox.js +1 -0
  26. package/application/3-ui/2-components/Select.js +1 -0
  27. package/application/3-ui/2-components/Text.js +1 -0
  28. package/application/3-ui/2-components/Textarea.js +1 -0
  29. package/application/3-ui/3-sections/1-dialogs/(private)/loadMore.js +6 -0
  30. package/application/3-ui/3-sections/1-dialogs/(private)/toDialog.js +1 -0
  31. package/application/3-ui/3-sections/1-dialogs/(public)/blur.js +3 -0
  32. package/application/3-ui/3-sections/1-dialogs/(public)/focus.js +3 -0
  33. package/application/3-ui/3-sections/1-dialogs/(public)/on.js +3 -0
  34. package/application/3-ui/3-sections/1-dialogs/(public)/setLabel.js +1 -0
  35. package/application/3-ui/3-sections/1-dialogs/1-list.js +10 -0
  36. package/application/3-ui/3-sections/1-dialogs/2-component.js +9 -0
  37. package/application/3-ui/3-sections/2-chat/(private)/clear.js +4 -0
  38. package/application/3-ui/3-sections/2-chat/(private)/selectMessage.js +6 -0
  39. package/application/3-ui/3-sections/2-chat/(public)/addMessage.js +6 -0
  40. package/application/3-ui/3-sections/2-chat/(public)/addPendingMessage.js +5 -0
  41. package/application/3-ui/3-sections/2-chat/(public)/confirmMessage.js +4 -0
  42. package/application/3-ui/3-sections/2-chat/(public)/down.js +4 -0
  43. package/application/3-ui/3-sections/2-chat/(public)/focus.js +1 -0
  44. package/application/3-ui/3-sections/2-chat/(public)/key.js +5 -0
  45. package/application/3-ui/3-sections/2-chat/(public)/open.js +8 -0
  46. package/application/3-ui/3-sections/2-chat/(public)/setLabel.js +1 -0
  47. package/application/3-ui/3-sections/2-chat/(public)/up.js +4 -0
  48. package/application/3-ui/3-sections/2-chat/Bubble.js +15 -0
  49. package/application/3-ui/3-sections/2-chat/component.js +17 -0
  50. package/application/3-ui/3-sections/3-messagePrompt/(private)/emit.js +2 -0
  51. package/application/3-ui/3-sections/3-messagePrompt/(private)/send.js +1 -0
  52. package/application/3-ui/3-sections/3-messagePrompt/(public)/blur.js +3 -0
  53. package/application/3-ui/3-sections/3-messagePrompt/(public)/focus.js +4 -0
  54. package/application/3-ui/3-sections/3-messagePrompt/(public)/key.js +4 -0
  55. package/application/3-ui/3-sections/3-messagePrompt/(public)/on.js +2 -0
  56. package/application/3-ui/3-sections/3-messagePrompt/(public)/setLabel.js +1 -0
  57. package/application/3-ui/3-sections/3-messagePrompt/1-input.js +12 -0
  58. package/application/3-ui/3-sections/3-messagePrompt/2-component.js +8 -0
  59. package/application/3-ui/3-sections/3-messagePrompt/3-events.js +11 -0
  60. package/application/3-ui/4-layout/(private)/select.js +5 -0
  61. package/application/3-ui/4-layout/(subscriptions)/mode.js +1 -0
  62. package/application/3-ui/4-layout/(subscriptions)/newMessage.js +3 -0
  63. package/application/3-ui/4-layout/(subscriptions)/openChat.js +7 -0
  64. package/application/3-ui/4-layout/(subscriptions)/sendMessage.js +5 -0
  65. package/application/3-ui/4-layout/1-layout.js +6 -0
  66. package/application/3-ui/4-layout/2-mapping.js +18 -0
  67. package/application/3-ui/4-layout/3-key.js +7 -0
  68. package/application/3-ui/4-layout/5-render.js +21 -0
  69. package/bun.lock +361 -0
  70. package/eslint.config.js +42 -0
  71. package/package.json +30 -0
  72. package/prettier.config.js +10 -0
  73. package/tsconfig.json +13 -0
  74. package/tuigram.js +34 -0
  75. package/types/global.d.ts +26 -0
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # 📟 tuigram — Telegram Terminal UI Client
2
+
3
+ A lightweight, terminal-based Telegram client written in **Node.js** with a primary focus on **Vim motions**.
4
+
5
+ <img width="3840" height="2160" alt="obraz" src="https://github.com/user-attachments/assets/335b2934-ea6e-4c8b-9ae4-c8f3f89c78d0" />
@@ -0,0 +1,90 @@
1
+ const createNode = (value, prev = null, next = null) => ({ value, prev, next });
2
+
3
+ const buildOperations = (list) => ({
4
+ push: (value) => {
5
+ const node = createNode(value, list.tail);
6
+ if (list.tail !== null) list.tail.next = node;
7
+ else list.head = node;
8
+ list.tail = node;
9
+ list.size++;
10
+ },
11
+
12
+ unshift: (value) => {
13
+ const node = createNode(value, null, list.head);
14
+ if (list.head) list.head.prev = node;
15
+ else list.tail = node;
16
+ list.head = node;
17
+ list.size++;
18
+ },
19
+
20
+ pop: () => {
21
+ if (list.tail === null) return null;
22
+ const value = list.tail.value;
23
+ list.tail = list.tail.prev;
24
+ if (list.tail !== null) list.tail.next = null;
25
+ else list.head = null;
26
+ list.size--;
27
+ return value;
28
+ },
29
+
30
+ shift: () => {
31
+ if (list.head === null) return null;
32
+ const value = list.head.value;
33
+ list.head = list.head.next;
34
+ if (list.head !== null) list.head.prev = null;
35
+ else list.tail = null;
36
+ list.size--;
37
+ return value;
38
+ },
39
+
40
+ find: (predicate) => {
41
+ for (let node = list.head; node !== null; node = node.next) if (predicate(node.value)) return node.value;
42
+ return null;
43
+ },
44
+
45
+ removeNode: (node) => {
46
+ if (node.prev !== null) node.prev.next = node.next;
47
+ else list.head = node.next;
48
+ if (node.next !== null) node.next.prev = node.prev;
49
+ else list.tail = node.prev;
50
+ list.size--;
51
+ return node.value;
52
+ },
53
+ });
54
+
55
+ const buildIterator = (list) => () => {
56
+ let current = list.head;
57
+ return {
58
+ next: () => {
59
+ if (current === null) return { value: null, done: true };
60
+ const value = current.value;
61
+ current = current.next;
62
+ return { value, done: false };
63
+ },
64
+ };
65
+ };
66
+
67
+ const from = (values = []) => {
68
+ const list = { head: null, tail: null, size: 0 };
69
+
70
+ const operations = buildOperations(list);
71
+ const iterator = buildIterator(list);
72
+
73
+ for (const value of values) operations.push(value);
74
+
75
+ return {
76
+ get head() {
77
+ return list.head;
78
+ },
79
+ get tail() {
80
+ return list.tail;
81
+ },
82
+ get size() {
83
+ return list.size;
84
+ },
85
+ ...operations,
86
+ [Symbol.iterator]: iterator,
87
+ };
88
+ };
89
+
90
+ ({ from });
@@ -0,0 +1 @@
1
+ (obj, keys) => Object.fromEntries(keys.map((key) => [key, obj[key]]));
@@ -0,0 +1 @@
1
+ () => node.crypto.randomUUID();
@@ -0,0 +1,7 @@
1
+ (fn, delay = 0) => {
2
+ let timer = null;
3
+ return (...args) => {
4
+ node.timers.clearTimeout(timer);
5
+ timer = node.timers.setTimeout(() => fn(...args), delay);
6
+ };
7
+ };
@@ -0,0 +1,8 @@
1
+ (fn, ...args) => {
2
+ try {
3
+ const result = fn(...args);
4
+ return [null, result];
5
+ } catch (err) {
6
+ return [err, null];
7
+ }
8
+ };
@@ -0,0 +1,8 @@
1
+ async (fn, ...args) => {
2
+ try {
3
+ const result = await fn(...args);
4
+ return [null, result];
5
+ } catch (err) {
6
+ return [err, null];
7
+ }
8
+ };
@@ -0,0 +1,8 @@
1
+ ({
2
+ from: ({ id, text, sender, chat }) => ({
3
+ id,
4
+ text,
5
+ sender: Obj.pick(sender, ['isSelf']),
6
+ chatId: chat.id,
7
+ }),
8
+ });
@@ -0,0 +1,24 @@
1
+ (() =>
2
+ async function* (chunkSize) {
3
+ console.log('get dialogs from cache');
4
+
5
+ const cachedDialogs = self.cache.dialogs.read();
6
+ if (cachedDialogs.length > 0) yield cachedDialogs;
7
+
8
+ console.log('get dialogs from tg');
9
+ const iterator = messenger.tg.iterDialogs({ chunkSize });
10
+ let page = [];
11
+ for await (const dialog of iterator) {
12
+ page.push({
13
+ chatId: dialog.lastMessage.chat.id,
14
+ name: dialog.peer.displayName,
15
+ lastMessage: dialog.lastMessage.text,
16
+ });
17
+ if (page.length === chunkSize) {
18
+ self.cache.dialogs.append(page);
19
+ yield page;
20
+ page = [];
21
+ }
22
+ }
23
+ if (page.length > 0) yield page;
24
+ })();
@@ -0,0 +1,25 @@
1
+ (() =>
2
+ async function* (chatId, pageSize) {
3
+ console.log('get history from cache');
4
+ const cachedMessages = self.cache.chats.read(chatId);
5
+ if (cachedMessages.length > 0) yield cachedMessages;
6
+
7
+ let offset = undefined;
8
+
9
+ while (true) {
10
+ const params = { offset, limit: pageSize };
11
+
12
+ // the first message in the array is the most recent message in the chat
13
+ console.log('get history from tg');
14
+ const history = await messenger.tg.getHistory(chatId, params);
15
+
16
+ const messages = history.map(Message.from);
17
+
18
+ self.cache.chats.append(chatId, messages);
19
+
20
+ yield messages;
21
+
22
+ if (messages.next === undefined) return;
23
+ offset = messages.next;
24
+ }
25
+ })();
@@ -0,0 +1,3 @@
1
+ (event, handler) => {
2
+ if (event === 'message') messenger.dispatcher.onNewMessage((message) => handler(Message.from(message)));
3
+ };
@@ -0,0 +1 @@
1
+ (chatId, text) => messenger.tg.sendText(chatId, text);
@@ -0,0 +1,4 @@
1
+ const { TelegramClient } = npm['@mtcute/bun'];
2
+ const { API_ID, API_HASH } = process.env;
3
+ const tg = new TelegramClient({ apiId: API_ID, apiHash: API_HASH });
4
+ tg.start().then(() => tg);
@@ -0,0 +1,2 @@
1
+ const { Dispatcher } = npm['@mtcute/dispatcher'];
2
+ Dispatcher.for(messenger.tg);
@@ -0,0 +1,15 @@
1
+ ({
2
+ _getPath: (chatId) => node.path.join(__rootDir, `/cache/chats/${chatId}.json`),
3
+ read: (chatId) => {
4
+ const path = self._getPath(chatId);
5
+ const [err, json] = risk(node.fs.readFileSync, path, { encoding: 'utf-8' });
6
+ if (err !== null) return [];
7
+ return JSON.parse(json);
8
+ },
9
+ append: (chatId, messages) => {
10
+ const existing = self.read(chatId);
11
+ const updated = [...existing, ...messages];
12
+ const path = self._getPath(chatId);
13
+ node.fs.writeFileSync(path, JSON.stringify(updated));
14
+ },
15
+ });
@@ -0,0 +1,13 @@
1
+ ({
2
+ PATH: node.path.join(__rootDir, '/cache/dialogs.json'),
3
+ read: () => {
4
+ const [err, json] = risk(node.fs.readFileSync, self.PATH, { encoding: 'utf-8' });
5
+ if (err !== null) return [];
6
+ return JSON.parse(json);
7
+ },
8
+ append: (dialogs) => {
9
+ const existing = self.read();
10
+ const updated = [...existing, ...dialogs];
11
+ node.fs.writeFileSync(self.PATH, JSON.stringify(updated));
12
+ },
13
+ });
@@ -0,0 +1,2 @@
1
+ self.ee ??= new node.events.EventEmitter();
2
+ (event, ...args) => self.ee.emit(event, ...args);
@@ -0,0 +1 @@
1
+ (char) => nvim.client.input(char);
@@ -0,0 +1,2 @@
1
+ self.ee ??= new node.events.EventEmitter();
2
+ (event, handler) => self.ee.on(event, handler);
@@ -0,0 +1,6 @@
1
+ const { findNvim, attach } = npm.neovim;
2
+ const found = findNvim({ orderBy: 'desc', minVersion: '0.9.0' });
3
+ const path = found.matches[0].path;
4
+ const nvimProc = node.child_process.spawn(path, ['--embed']);
5
+ const client = attach({ proc: nvimProc, logger: console });
6
+ client.uiAttach(5, 5, {}).then(() => client);
@@ -0,0 +1,22 @@
1
+ const handlers = {
2
+ cursor_goto: async () => {
3
+ const [y, x] = await nvim.client.window.cursor;
4
+ nvim.emit('cursor', { x, y });
5
+ },
6
+ put: Rate.debounce(async () => {
7
+ const buffer = await nvim.client.buffer;
8
+ const lines = await buffer.lines;
9
+ const [y, x] = await nvim.client.window.cursor;
10
+ nvim.emit('lines', lines, { x, y });
11
+ }),
12
+ mode_change: ([[mode]]) => nvim.emit('mode', mode),
13
+ };
14
+
15
+ nvim.client.on('notification', (method, args) => {
16
+ if (method !== 'redraw') return;
17
+
18
+ for (const [event, ...updates] of args) {
19
+ const handler = handlers[event];
20
+ if (handler !== undefined) handler(updates);
21
+ }
22
+ });
@@ -0,0 +1 @@
1
+ tui.createCliRenderer({ exitOnCtrlC: true });
@@ -0,0 +1,11 @@
1
+ const wrapper = new tui.BoxRenderable(self.renderer, {
2
+ id: 'wrapper',
3
+ width: '100%',
4
+ height: '100%',
5
+ justifyContent: 'center',
6
+ alignItems: 'center',
7
+ });
8
+
9
+ self.renderer.root.add(wrapper);
10
+
11
+ wrapper;
@@ -0,0 +1 @@
1
+ (...options) => new tui.BoxRenderable(ui.screen.renderer, ...options);
@@ -0,0 +1 @@
1
+ (...options) => new tui.ScrollBoxRenderable(ui.screen.renderer, ...options);
@@ -0,0 +1 @@
1
+ (...options) => new tui.SelectRenderable(ui.screen.renderer, ...options);
@@ -0,0 +1 @@
1
+ (...options) => new tui.TextRenderable(ui.screen.renderer, ...options);
@@ -0,0 +1 @@
1
+ (...options) => new tui.TextareaRenderable(ui.screen.renderer, ...options);
@@ -0,0 +1,6 @@
1
+ ((iterator) => async () => {
2
+ //await node.timers.promises.setTimeout(5000);
3
+ const { value, done } = await iterator.next();
4
+ const moreDialogs = value.map(self.toDialog);
5
+ return done ? [] : moreDialogs;
6
+ })(messenger.getDialogs(9));
@@ -0,0 +1 @@
1
+ ({ chatId, name, lastMessage }) => ({ chatId, name, description: lastMessage });
@@ -0,0 +1,3 @@
1
+ () => {
2
+ self.list.blur();
3
+ };
@@ -0,0 +1,3 @@
1
+ () => {
2
+ self.list.focus();
3
+ };
@@ -0,0 +1,3 @@
1
+ (event, handler) => {
2
+ if (event === 'open') self.list.on(tui.SelectRenderableEvents.ITEM_SELECTED, (_, dialog) => handler(dialog));
3
+ };
@@ -0,0 +1 @@
1
+ (label) => (self.component.title = label);
@@ -0,0 +1,10 @@
1
+ self.loadMore().then((dialogs) =>
2
+ ui.components.Select({
3
+ id: 'dialogsList',
4
+ height: '100%',
5
+ options: dialogs,
6
+ selectedBackgroundColor: '#2d4f2d',
7
+ focusedBackgroundColor: '#1a1a1a',
8
+ showSelectionIndicator: false,
9
+ }),
10
+ );
@@ -0,0 +1,9 @@
1
+ const wrapper = ui.components.Box({
2
+ id: 'dialogsWrapper',
3
+ width: 30,
4
+ borderStyle: 'rounded',
5
+ });
6
+
7
+ wrapper.add(self.list);
8
+
9
+ wrapper;
@@ -0,0 +1,4 @@
1
+ () => {
2
+ self.messages = LinkedList.from([]);
3
+ for (const child of self.component.getChildren()) child.destroy();
4
+ };
@@ -0,0 +1,6 @@
1
+ (message) => {
2
+ self.selected = message;
3
+ const bubble = message.value.bubble;
4
+ self.component.scrollChildIntoView(bubble.id);
5
+ bubble.focus();
6
+ };
@@ -0,0 +1,6 @@
1
+ (message) => {
2
+ self.messages.push(message);
3
+ const bubble = self.Bubble(message);
4
+ self.component.add(bubble);
5
+ message.bubble = bubble;
6
+ };
@@ -0,0 +1,5 @@
1
+ (text) => {
2
+ const tempId = Random.uuid();
3
+ self.addMessage({ id: tempId, text, sender: { isSelf: true } });
4
+ return tempId;
5
+ };
@@ -0,0 +1,4 @@
1
+ (tempId, confirmedMessage) => {
2
+ const pendingMessage = self.messages.find(({ id }) => id === tempId);
3
+ pendingMessage.id = confirmedMessage.id;
4
+ };
@@ -0,0 +1,4 @@
1
+ () => {
2
+ if (self.selected === self.messages.tail) return;
3
+ self.selectMessage(self.selected.next);
4
+ };
@@ -0,0 +1 @@
1
+ () => self.selectMessage(self.selected);
@@ -0,0 +1,5 @@
1
+ (ch) =>
2
+ ({
3
+ k: self.up,
4
+ j: self.down,
5
+ })[ch]?.();
@@ -0,0 +1,8 @@
1
+ async (chatId) => {
2
+ self.clear();
3
+ self.iterator = messenger.getHistory(chatId, 20);
4
+ const { value } = await self.iterator.next();
5
+ const messages = value.toReversed();
6
+ for (const message of messages) self.addMessage(message);
7
+ self.selectMessage(self.messages.tail);
8
+ };
@@ -0,0 +1 @@
1
+ (label) => (self.component.title = label);
@@ -0,0 +1,4 @@
1
+ () => {
2
+ if (self.selected === self.messages.head) return;
3
+ self.selectMessage(self.selected.prev);
4
+ };
@@ -0,0 +1,15 @@
1
+ ({ text, sender }) => {
2
+ const box = ui.components.Box({
3
+ flexDirection: 'column',
4
+ borderStyle: 'rounded',
5
+ borderColor: sender.isSelf ? '#283457' : '#1f2335',
6
+ paddingX: 1,
7
+ alignSelf: sender.isSelf ? 'flex-end' : 'flex-start',
8
+ focusedBorderColor: 'green',
9
+ focusable: true,
10
+ });
11
+
12
+ box.add(ui.components.Text({ content: text, fg: '#c0caf5' }));
13
+
14
+ return box;
15
+ };
@@ -0,0 +1,17 @@
1
+ ui.components.ScrollBox({
2
+ flexGrow: 1,
3
+ borderStyle: 'rounded',
4
+ scrollY: true,
5
+ scrollX: false,
6
+ stickyScroll: true,
7
+ stickyStart: 'bottom',
8
+ focusable: true,
9
+ focusedBorderColor: '#42AAFF',
10
+ contentOptions: {
11
+ flexDirection: 'column',
12
+ gap: 1,
13
+ paddingX: 1,
14
+ paddingY: 1,
15
+ },
16
+ scrollbarOptions: { visible: false },
17
+ });
@@ -0,0 +1,2 @@
1
+ self.ee ??= new node.events.EventEmitter();
2
+ (event, ...args) => self.ee.emit(event, ...args);
@@ -0,0 +1 @@
1
+ () => self.emit('send', self.input.plainText);
@@ -0,0 +1,3 @@
1
+ () => {
2
+ self.focused = false;
3
+ };
@@ -0,0 +1,4 @@
1
+ () => {
2
+ self.input.focus();
3
+ self.focused = true;
4
+ };
@@ -0,0 +1,4 @@
1
+ (ch) => {
2
+ if (ch === 'x') self.send();
3
+ nvim.input(ch);
4
+ };
@@ -0,0 +1,2 @@
1
+ self.ee ??= new node.events.EventEmitter();
2
+ (event, handler) => self.ee.on(event, handler);
@@ -0,0 +1 @@
1
+ (label) => (self.component.title = label);
@@ -0,0 +1,12 @@
1
+ ui.components.Textarea({
2
+ width: '100%',
3
+ height: 1,
4
+ placeholder: 'Type a message...',
5
+ backgroundColor: '#1a1b26',
6
+ focusedBackgroundColor: '#24283b',
7
+ textColor: '#c0caf5',
8
+ focusedTextColor: '#c0caf5',
9
+ cursorColor: '#7aa2f7',
10
+ wrapMode: 'word',
11
+ cursorStyle: { blinking: true },
12
+ });
@@ -0,0 +1,8 @@
1
+ const wrapper = ui.components.Box({
2
+ borderStyle: 'rounded',
3
+ flexShrink: 0,
4
+ });
5
+
6
+ wrapper.add(self.input);
7
+
8
+ wrapper;
@@ -0,0 +1,11 @@
1
+ nvim.on('lines', (lines, { x, y }) => {
2
+ if (!self.focused) return;
3
+ self.input.replaceText(lines.join('\n'));
4
+ self.input.setCursor(y - 1, x);
5
+ self.input.height = lines.length > 0 ? lines.length : 1;
6
+ });
7
+
8
+ nvim.on('cursor', ({ x, y }) => {
9
+ if (!self.focused) return;
10
+ self.input.setCursor(y - 1, x);
11
+ });
@@ -0,0 +1,5 @@
1
+ (section) => {
2
+ ui.sections[self.selected].blur?.();
3
+ self.selected = section;
4
+ ui.sections[section].focus();
5
+ };
@@ -0,0 +1 @@
1
+ nvim.on('mode', (mode) => (self.selectable = mode === 'normal'));
@@ -0,0 +1,3 @@
1
+ messenger.on('message', (message) => {
2
+ if (self.openedChatId === message.chatId) ui.sections.chat.addMessage(message);
3
+ });
@@ -0,0 +1,7 @@
1
+ ui.sections.dialogs.on('open', (dialog) => {
2
+ const chatId = dialog.chatId;
3
+ if (self.openedChatId === chatId) return;
4
+ self.openedChatId = chatId;
5
+ ui.sections.chat.open(chatId);
6
+ self.select('chat');
7
+ });
@@ -0,0 +1,5 @@
1
+ ui.sections.messagePrompt.on('send', async (text) => {
2
+ const tempId = ui.sections.chat.addPendingMessage(text);
3
+ const message = await messenger.sendMessage(self.openedChatId, text);
4
+ ui.sections.chat.confirmMessage(tempId, message);
5
+ });
@@ -0,0 +1,6 @@
1
+ ({
2
+ selectable: true,
3
+ selected: 'dialogs',
4
+ reserved: ['q'],
5
+ openedChatId: undefined,
6
+ });
@@ -0,0 +1,18 @@
1
+ const mapping = {
2
+ 1: 'dialogs',
3
+ 2: 'chat',
4
+ 3: 'messagePrompt',
5
+ };
6
+
7
+ for (const [index, section] of Object.entries(mapping)) {
8
+ ui.sections[section].setLabel(` ${index} `);
9
+ }
10
+
11
+ ui.screen.renderer.keyInput.on('keypress', (key) => {
12
+ if (!self.selectable) return;
13
+
14
+ const section = mapping[key.name];
15
+ if (section !== undefined) self.select(section);
16
+ });
17
+
18
+ self.reserved.push(...Object.keys(mapping));
@@ -0,0 +1,7 @@
1
+ ui.screen.renderer.keyInput.on('keypress', (e) => {
2
+ const raw = e.raw;
3
+ if (self.selected !== 'dialogs') e.stopPropagation();
4
+
5
+ const skip = self.selectable && self.reserved.includes(raw);
6
+ if (!skip) ui.sections[self.selected].key?.(raw);
7
+ });
@@ -0,0 +1,21 @@
1
+ const layout = new tui.BoxRenderable(ui.screen.renderer, {
2
+ id: 'wrapper',
3
+ width: '100%',
4
+ height: '100%',
5
+ flexDirection: 'row',
6
+ });
7
+
8
+ ui.screen.wrapper.add(layout);
9
+
10
+ layout.add(ui.sections.dialogs.component);
11
+
12
+ const rightSide = new tui.BoxRenderable(ui.screen.renderer, {
13
+ id: 'rightSide',
14
+ flexDirection: 'column',
15
+ });
16
+
17
+ layout.add(rightSide);
18
+
19
+ rightSide.add(ui.sections.chat.component);
20
+ rightSide.add(ui.sections.messagePrompt.component);
21
+ ui.sections.dialogs.component.focus();