arcanechat-tui 0.11.0__py3-none-any.whl
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.
- arcanechat_tui/__init__.py +3 -0
- arcanechat_tui/__main__.py +5 -0
- arcanechat_tui/_version.py +21 -0
- arcanechat_tui/application.py +165 -0
- arcanechat_tui/cards_widget.py +35 -0
- arcanechat_tui/chatlist.py +104 -0
- arcanechat_tui/cli.py +149 -0
- arcanechat_tui/composer.py +86 -0
- arcanechat_tui/container.py +31 -0
- arcanechat_tui/conversation.py +127 -0
- arcanechat_tui/eventcenter.py +45 -0
- arcanechat_tui/lazylistwaker.py +33 -0
- arcanechat_tui/logger.py +29 -0
- arcanechat_tui/main.py +64 -0
- arcanechat_tui/util.py +86 -0
- arcanechat_tui/welcome_widget.py +10 -0
- arcanechat_tui-0.11.0.dist-info/LICENSE +674 -0
- arcanechat_tui-0.11.0.dist-info/METADATA +84 -0
- arcanechat_tui-0.11.0.dist-info/RECORD +22 -0
- arcanechat_tui-0.11.0.dist-info/WHEEL +5 -0
- arcanechat_tui-0.11.0.dist-info/entry_points.txt +3 -0
- arcanechat_tui-0.11.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.11.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 11, 0)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Main UI program"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import urwid
|
|
8
|
+
from deltachat2 import Client
|
|
9
|
+
|
|
10
|
+
from .cards_widget import CardsWidget
|
|
11
|
+
from .chatlist import CHAT_SELECTED, ChatListWidget
|
|
12
|
+
from .composer import SENDING_MSG_FAILED, ComposerWidget
|
|
13
|
+
from .container import Container
|
|
14
|
+
from .conversation import ConversationWidget
|
|
15
|
+
from .eventcenter import CHATLIST_CHANGED, MESSAGES_CHANGED, EventCenter
|
|
16
|
+
from .util import shorten_text
|
|
17
|
+
from .welcome_widget import WelcomeWidget
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Application:
|
|
21
|
+
"""Main application UI"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, client: Client, keymap: dict, theme: dict) -> None:
|
|
24
|
+
self.client = client
|
|
25
|
+
self.keymap = keymap
|
|
26
|
+
self.accid: Optional[int] = None
|
|
27
|
+
eventcenter = EventCenter()
|
|
28
|
+
|
|
29
|
+
self.chatlist = ChatListWidget(client)
|
|
30
|
+
urwid.connect_signal(eventcenter, CHATLIST_CHANGED, self.chatlist.chatlist_changed)
|
|
31
|
+
chatlist_cont = Container(self.chatlist, self._chatlist_keypress)
|
|
32
|
+
|
|
33
|
+
conversation = ConversationWidget(client, theme["background"][-1])
|
|
34
|
+
urwid.connect_signal(self.chatlist, CHAT_SELECTED, conversation.set_chat)
|
|
35
|
+
urwid.connect_signal(eventcenter, MESSAGES_CHANGED, conversation.messages_changed)
|
|
36
|
+
conversation_cont = Container(conversation, self._conversation_keypress)
|
|
37
|
+
|
|
38
|
+
composer = ComposerWidget(client, keymap)
|
|
39
|
+
urwid.connect_signal(self.chatlist, CHAT_SELECTED, composer.set_chat)
|
|
40
|
+
composer_cont = Container(composer, self._composer_keypress)
|
|
41
|
+
|
|
42
|
+
self.cards = CardsWidget()
|
|
43
|
+
self.cards.add("welcome", WelcomeWidget())
|
|
44
|
+
self.cards.add("conversation", conversation_cont)
|
|
45
|
+
self.cards.show("welcome")
|
|
46
|
+
|
|
47
|
+
self.right_side = urwid.Pile([self.cards, (urwid.PACK, composer_cont)])
|
|
48
|
+
|
|
49
|
+
vsep = urwid.AttrMap(urwid.Filler(urwid.Columns([])), "separator")
|
|
50
|
+
|
|
51
|
+
# layout root
|
|
52
|
+
self.main_columns = urwid.Columns(
|
|
53
|
+
[("weight", 1, chatlist_cont), (1, vsep), ("weight", 4, self.right_side)]
|
|
54
|
+
)
|
|
55
|
+
self.frame = urwid.Frame(self.main_columns)
|
|
56
|
+
|
|
57
|
+
self.loop = urwid.MainLoop(
|
|
58
|
+
urwid.AttrMap(self.frame, "background"),
|
|
59
|
+
[(key, *value) for key, value in theme.items()],
|
|
60
|
+
unhandled_input=self._unhandled_keypress,
|
|
61
|
+
)
|
|
62
|
+
self.loop.screen.set_terminal_properties(colors=256)
|
|
63
|
+
|
|
64
|
+
urwid.connect_signal(self.chatlist, CHAT_SELECTED, self.chat_selected)
|
|
65
|
+
urwid.connect_signal(composer, SENDING_MSG_FAILED, self.sending_msg_failed)
|
|
66
|
+
urwid.connect_signal(eventcenter, CHATLIST_CHANGED, self.chatlist_changed)
|
|
67
|
+
urwid.connect_signal(eventcenter, MESSAGES_CHANGED, self.messages_changed)
|
|
68
|
+
client.add_hook(eventcenter.process_core_event)
|
|
69
|
+
|
|
70
|
+
def _print_title(self) -> None:
|
|
71
|
+
badge = 0
|
|
72
|
+
for acc in self.client.rpc.get_all_account_ids():
|
|
73
|
+
badge += len(self.client.rpc.get_fresh_msgs(acc))
|
|
74
|
+
name = self.client.rpc.get_config(self.accid, "displayname")
|
|
75
|
+
if not name:
|
|
76
|
+
name = self.client.rpc.get_config(self.accid, "configured_addr")
|
|
77
|
+
name = shorten_text(name, 30)
|
|
78
|
+
if badge > 0:
|
|
79
|
+
text = f"\x1b]2;({badge if badge < 999 else '+999'}) {name}\x07"
|
|
80
|
+
else:
|
|
81
|
+
text = f"\x1b]2;{name}\x07"
|
|
82
|
+
sys.stdout.write(text)
|
|
83
|
+
|
|
84
|
+
def exit(self) -> None:
|
|
85
|
+
sys.stdout.write("\x1b]2;\x07")
|
|
86
|
+
raise urwid.ExitMainLoop
|
|
87
|
+
|
|
88
|
+
def chat_selected(self, chat: Optional[Tuple[int, int]]) -> None:
|
|
89
|
+
if chat:
|
|
90
|
+
self.cards.show("conversation")
|
|
91
|
+
# focus composer
|
|
92
|
+
self.main_columns.focus_position = 2
|
|
93
|
+
self.right_side.focus_position = 1
|
|
94
|
+
else:
|
|
95
|
+
self.cards.show("welcome")
|
|
96
|
+
# focus chatlist
|
|
97
|
+
self.main_columns.focus_position = 0
|
|
98
|
+
|
|
99
|
+
def messages_changed(self, _client: Client, accid: int, chatid: int, _msgid: int) -> None:
|
|
100
|
+
chat = self.chatlist.selected_chat
|
|
101
|
+
if accid == self.accid and chat and chatid in (chat[1], 0):
|
|
102
|
+
self.loop.draw_screen()
|
|
103
|
+
|
|
104
|
+
def chatlist_changed(self, _client: Client, accid: int) -> None:
|
|
105
|
+
self._print_title()
|
|
106
|
+
if accid == self.accid:
|
|
107
|
+
self.loop.draw_screen()
|
|
108
|
+
|
|
109
|
+
def sending_msg_failed(self, error: str) -> None:
|
|
110
|
+
self.toast(urwid.AttrMap(urwid.Text([" Error: ", error]), "failed"), 5)
|
|
111
|
+
|
|
112
|
+
def toast(self, element: urwid.Widget, duration: int) -> None:
|
|
113
|
+
def reset_footer(*_) -> None:
|
|
114
|
+
if self.frame.footer == element:
|
|
115
|
+
self.frame.footer = None
|
|
116
|
+
|
|
117
|
+
self.frame.footer = element
|
|
118
|
+
self.loop.set_alarm_in(duration, reset_footer)
|
|
119
|
+
|
|
120
|
+
def _unhandled_keypress(self, key: str) -> None:
|
|
121
|
+
if key == self.keymap["quit"]:
|
|
122
|
+
self.exit()
|
|
123
|
+
|
|
124
|
+
def _chatlist_keypress(self, _size: list, key: str) -> Optional[str]:
|
|
125
|
+
if key in ("right", "tab"):
|
|
126
|
+
# give focus to the composer area
|
|
127
|
+
self.main_columns.focus_position = 2
|
|
128
|
+
self.right_side.focus_position = 1
|
|
129
|
+
return None
|
|
130
|
+
return key
|
|
131
|
+
|
|
132
|
+
def _conversation_keypress(self, _size: list, key: str) -> Optional[str]:
|
|
133
|
+
if key in ("tab", "esc"):
|
|
134
|
+
# give focus to the composer area
|
|
135
|
+
self.right_side.focus_position = 1
|
|
136
|
+
return None
|
|
137
|
+
return key
|
|
138
|
+
|
|
139
|
+
def _composer_keypress(self, _size: list, key: str) -> Optional[str]:
|
|
140
|
+
if key == "esc":
|
|
141
|
+
self.chatlist.select_chat(None)
|
|
142
|
+
return None
|
|
143
|
+
return key
|
|
144
|
+
|
|
145
|
+
def run(self, accid: int = 0) -> None:
|
|
146
|
+
rpc = self.client.rpc
|
|
147
|
+
self.accid = accid or self.client.rpc.get_selected_account_id()
|
|
148
|
+
if not self.accid:
|
|
149
|
+
accounts = [rpc.is_configured(accid) for accid in rpc.get_all_account_ids()]
|
|
150
|
+
if accounts:
|
|
151
|
+
self.accid = accounts[0]
|
|
152
|
+
else:
|
|
153
|
+
print("Error: No account configured yet")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
elif not rpc.is_configured(self.accid):
|
|
156
|
+
print("Error: Account configured yet")
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
|
|
159
|
+
self.chatlist.set_account(self.accid)
|
|
160
|
+
self._print_title()
|
|
161
|
+
Thread(target=self.client.run_forever, daemon=True).start()
|
|
162
|
+
try:
|
|
163
|
+
self.loop.run()
|
|
164
|
+
except KeyboardInterrupt:
|
|
165
|
+
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Cards layout widget, a stack of widgets, only one is visible at a time"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
import urwid
|
|
6
|
+
|
|
7
|
+
EMPTY = urwid.Widget()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CardsWidget(urwid.WidgetPlaceholder):
|
|
11
|
+
"""Cards layout widget, a stack of widgets, only one of them is visible at a time"""
|
|
12
|
+
|
|
13
|
+
original_widget: urwid.Widget
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__(EMPTY)
|
|
17
|
+
self.cards: Dict[str, urwid.Widget] = {}
|
|
18
|
+
|
|
19
|
+
def add(self, name: str, widget: urwid.Widget) -> None:
|
|
20
|
+
"""Add a new widget to the stack.
|
|
21
|
+
|
|
22
|
+
The name must be an unique string ID to identify the item.
|
|
23
|
+
"""
|
|
24
|
+
self.cards[name] = widget
|
|
25
|
+
|
|
26
|
+
def remove(self, name: str) -> urwid.Widget:
|
|
27
|
+
"""Remove an item from the stack corresponding to the given name."""
|
|
28
|
+
widget = self.cards.pop(name)
|
|
29
|
+
if widget is self.original_widget:
|
|
30
|
+
self.original_widget = EMPTY
|
|
31
|
+
return widget
|
|
32
|
+
|
|
33
|
+
def show(self, name: str) -> None:
|
|
34
|
+
"""Show the widget corresponding to the given name."""
|
|
35
|
+
self.original_widget = self.cards[name]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Chat list widget"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import urwid
|
|
6
|
+
from deltachat2 import ChatlistFlag, Client
|
|
7
|
+
|
|
8
|
+
from .lazylistwaker import LazyListWalker
|
|
9
|
+
from .util import shorten_text
|
|
10
|
+
|
|
11
|
+
CHAT_SELECTED = "chat_selected"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChatListItem(urwid.Button):
|
|
15
|
+
"""A single chatlist item"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
accid: int,
|
|
20
|
+
item: Any,
|
|
21
|
+
selected: bool,
|
|
22
|
+
callback: Callable[["ChatListItem"], None],
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__("", callback)
|
|
25
|
+
self.accid = accid
|
|
26
|
+
self.id = item.id
|
|
27
|
+
elements: list = []
|
|
28
|
+
|
|
29
|
+
if item.is_self_talk:
|
|
30
|
+
color = "#0af"
|
|
31
|
+
icon = "*"
|
|
32
|
+
elif item.is_device_talk:
|
|
33
|
+
color = "#888"
|
|
34
|
+
icon = "i"
|
|
35
|
+
else:
|
|
36
|
+
color = item.color
|
|
37
|
+
icon = "@" if item.dm_chat_contact else "#"
|
|
38
|
+
elements.append((urwid.AttrSpec("#fff", color), f" {icon} "))
|
|
39
|
+
|
|
40
|
+
if item.is_pinned:
|
|
41
|
+
elements.append("*")
|
|
42
|
+
else:
|
|
43
|
+
elements.append(" ")
|
|
44
|
+
|
|
45
|
+
name = shorten_text(item.name, 40)
|
|
46
|
+
elements.append(("selected_chat", name) if selected else name)
|
|
47
|
+
|
|
48
|
+
if item.fresh_message_counter > 0:
|
|
49
|
+
elements.append(" ")
|
|
50
|
+
style = "unread_badge_muted" if item.is_muted else "unread_badge"
|
|
51
|
+
elements.append((style, f"({item.fresh_message_counter})"))
|
|
52
|
+
|
|
53
|
+
self._w = urwid.AttrMap(urwid.SelectableIcon(elements, 3), None, focus_map="focused_item")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ChatListWidget(urwid.ListBox):
|
|
57
|
+
"""Display a list of chats"""
|
|
58
|
+
|
|
59
|
+
signals = [CHAT_SELECTED]
|
|
60
|
+
|
|
61
|
+
def __init__(self, client: Client) -> None:
|
|
62
|
+
self.client = client
|
|
63
|
+
self.accid: Optional[int] = None
|
|
64
|
+
self.selected_chat: Optional[Tuple[int, int]] = None
|
|
65
|
+
super().__init__(LazyListWalker([], self._create_chatlist_item))
|
|
66
|
+
|
|
67
|
+
def set_account(self, accid: Optional[int]) -> None:
|
|
68
|
+
self.accid = accid
|
|
69
|
+
if self.selected_chat and accid != self.selected_chat[0]:
|
|
70
|
+
self._select_chat(None)
|
|
71
|
+
self.chatlist_changed(self.client, accid)
|
|
72
|
+
|
|
73
|
+
def select_chat(self, chat: Optional[Tuple[int, int]]) -> None:
|
|
74
|
+
self._select_chat(chat)
|
|
75
|
+
self.chatlist_changed(self.client, self.accid)
|
|
76
|
+
|
|
77
|
+
def chatlist_changed(self, client: Client, accid: Optional[int]) -> None:
|
|
78
|
+
if not self.accid:
|
|
79
|
+
self.body.clear_cache()
|
|
80
|
+
self.body.clear()
|
|
81
|
+
elif accid == self.accid:
|
|
82
|
+
entries = client.rpc.get_chatlist_entries(accid, ChatlistFlag.NO_SPECIALS, None, None)
|
|
83
|
+
item = self.focus
|
|
84
|
+
self.body.clear_cache()
|
|
85
|
+
self.body[:] = [(accid, chatid) for chatid in entries]
|
|
86
|
+
try:
|
|
87
|
+
index = max(entries.index(item.id), 0) if item else 0
|
|
88
|
+
except ValueError:
|
|
89
|
+
pass
|
|
90
|
+
else:
|
|
91
|
+
if entries:
|
|
92
|
+
self.set_focus(index)
|
|
93
|
+
|
|
94
|
+
def _create_chatlist_item(self, chat: Tuple[int, int]) -> urwid.Widget:
|
|
95
|
+
item = self.client.rpc.get_chatlist_items_by_entries(chat[0], [chat[1]])[str(chat[1])]
|
|
96
|
+
return ChatListItem(chat[0], item, self.selected_chat == chat, self._on_item_clicked)
|
|
97
|
+
|
|
98
|
+
def _on_item_clicked(self, item: ChatListItem) -> None:
|
|
99
|
+
self._select_chat((item.accid, item.id))
|
|
100
|
+
self.chatlist_changed(self.client, self.accid) # so the selected chat widget is updated
|
|
101
|
+
|
|
102
|
+
def _select_chat(self, chat: Optional[Tuple[int, int]]) -> None:
|
|
103
|
+
self.selected_chat = chat
|
|
104
|
+
urwid.emit_signal(self, CHAT_SELECTED, self.selected_chat)
|
arcanechat_tui/cli.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Command line arguments parsing"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from argparse import ArgumentParser, Namespace
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from appdirs import user_config_dir
|
|
8
|
+
from deltachat2 import Client, JsonRpcError
|
|
9
|
+
|
|
10
|
+
from . import APP_NAME
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
from .util import abspath, get_account, get_or_create_account, parse_docstring
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Cli:
|
|
16
|
+
"""Command line argument parser"""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._parser = ArgumentParser()
|
|
20
|
+
self._subcommands = self._parser.add_subparsers(title="subcommands")
|
|
21
|
+
|
|
22
|
+
self._parser.add_argument("-v", "--version", action="version", version=__version__)
|
|
23
|
+
|
|
24
|
+
self._parser.add_argument(
|
|
25
|
+
"--program-folder",
|
|
26
|
+
"-f",
|
|
27
|
+
help="program configuration folder (default: %(default)s)",
|
|
28
|
+
metavar="PATH",
|
|
29
|
+
default=user_config_dir(APP_NAME),
|
|
30
|
+
type=abspath,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
self._parser.add_argument(
|
|
34
|
+
"--account",
|
|
35
|
+
"-a",
|
|
36
|
+
help="operate only over the given account when running any subcommand",
|
|
37
|
+
metavar="ADDR",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self._parser.add_argument(
|
|
41
|
+
"--log",
|
|
42
|
+
help=(
|
|
43
|
+
"set the severity level of what should be saved in the log file"
|
|
44
|
+
" (default: %(default)s)"
|
|
45
|
+
),
|
|
46
|
+
choices=["debug", "info", "warning", "error", "disabled"],
|
|
47
|
+
default="warning",
|
|
48
|
+
type=str.lower,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
init_parser = self.add_subcommand(init_cmd, name="init")
|
|
52
|
+
init_parser.add_argument("addr", help="your e-mail address")
|
|
53
|
+
init_parser.add_argument("password", help="your password")
|
|
54
|
+
|
|
55
|
+
import_parser = self.add_subcommand(import_cmd, name="import")
|
|
56
|
+
import_parser.add_argument("path", help="path to the backup file to import")
|
|
57
|
+
|
|
58
|
+
config_parser = self.add_subcommand(config_cmd, name="config")
|
|
59
|
+
config_parser.add_argument("option", help="option name", nargs="?")
|
|
60
|
+
config_parser.add_argument("value", help="option value to set", nargs="?")
|
|
61
|
+
|
|
62
|
+
def add_subcommand(
|
|
63
|
+
self,
|
|
64
|
+
func: Callable[["Cli", Namespace], None],
|
|
65
|
+
**kwargs,
|
|
66
|
+
) -> ArgumentParser:
|
|
67
|
+
"""Add a subcommand to the CLI."""
|
|
68
|
+
if not kwargs.get("name"):
|
|
69
|
+
kwargs["name"] = func.__name__
|
|
70
|
+
if not kwargs.get("help") and not kwargs.get("description"):
|
|
71
|
+
kwargs["help"], kwargs["description"] = parse_docstring(func.__doc__)
|
|
72
|
+
subparser = self._subcommands.add_parser(**kwargs)
|
|
73
|
+
subparser.set_defaults(cmd=func)
|
|
74
|
+
return subparser
|
|
75
|
+
|
|
76
|
+
def parse_args(self) -> Namespace:
|
|
77
|
+
"""Parse command line arguments"""
|
|
78
|
+
return self._parser.parse_args()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def init_cmd(client: Client, args: Namespace) -> None:
|
|
82
|
+
"""initialize an account"""
|
|
83
|
+
if args.account:
|
|
84
|
+
accid = get_account(client.rpc, args.account)
|
|
85
|
+
if not accid or accid not in client.rpc.get_all_account_ids():
|
|
86
|
+
print(f"Error: unknown account: {args.account!r}")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
else:
|
|
89
|
+
accid = get_or_create_account(client.rpc, args.addr)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
client.configure(accid, email=args.addr, password=args.password)
|
|
93
|
+
print("Account configured successfully.")
|
|
94
|
+
except JsonRpcError as err:
|
|
95
|
+
print("ERROR: Configuration failed:", err)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def import_cmd(client: Client, args: Namespace) -> None:
|
|
100
|
+
"""import account from backup file"""
|
|
101
|
+
if args.account:
|
|
102
|
+
print("Error: -a/--account can't be used with this subcommand")
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
client.rpc.import_backup(client.rpc.add_account(), args.path, None)
|
|
107
|
+
print("Backup imported successfully.")
|
|
108
|
+
except JsonRpcError as err:
|
|
109
|
+
print("ERROR: Failed to import backup:", err)
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def config_cmd(client: Client, args: Namespace) -> None:
|
|
114
|
+
"""set or get account configuration values"""
|
|
115
|
+
accounts = client.rpc.get_all_account_ids()
|
|
116
|
+
if not args.account and len(accounts) == 1:
|
|
117
|
+
args.account = accounts[0]
|
|
118
|
+
|
|
119
|
+
if args.account:
|
|
120
|
+
accid = get_account(client.rpc, args.account)
|
|
121
|
+
if not accid or accid not in accounts:
|
|
122
|
+
print(f"Error: unknown account: {args.account!r}")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
keys = (client.rpc.get_config(accid, "sys.config_keys") or "").split()
|
|
126
|
+
if args.option and not args.option.startswith("ui.") and args.option not in keys:
|
|
127
|
+
print(f"Error: unknown configuration option: {args.option}")
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
if args.value:
|
|
131
|
+
client.rpc.set_config(accid, args.option, args.value)
|
|
132
|
+
|
|
133
|
+
if args.option:
|
|
134
|
+
try:
|
|
135
|
+
value = client.rpc.get_config(accid, args.option)
|
|
136
|
+
print(f"{args.option}={value!r}")
|
|
137
|
+
except JsonRpcError:
|
|
138
|
+
print(f"Error: unknown configuration option: {args.option}")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
else:
|
|
141
|
+
for key in keys:
|
|
142
|
+
value = client.rpc.get_config(accid, key)
|
|
143
|
+
print(f"{key}={value!r}")
|
|
144
|
+
else:
|
|
145
|
+
print(
|
|
146
|
+
"Error: you must use --account option to set what account to set/get"
|
|
147
|
+
" configuration values"
|
|
148
|
+
)
|
|
149
|
+
sys.exit(1)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Composer area widget"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import urwid
|
|
6
|
+
import urwid_readline
|
|
7
|
+
from deltachat2 import Client, JsonRpcError, MsgData
|
|
8
|
+
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .util import get_subtitle, shorten_text
|
|
11
|
+
|
|
12
|
+
SENDING_MSG_FAILED = "sending_msg_failed"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReadlineEdit2(urwid_readline.ReadlineEdit):
|
|
16
|
+
"""Edit widget"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, insert_new_line_key: str) -> None:
|
|
19
|
+
super().__init__(multiline=True)
|
|
20
|
+
del self.keymap["enter"]
|
|
21
|
+
self.keymap[insert_new_line_key] = self.insert_new_line
|
|
22
|
+
|
|
23
|
+
def previous_line(self):
|
|
24
|
+
"""Patch bug: https://github.com/rr-/urwid_readline/issues/24"""
|
|
25
|
+
x, y = self.get_cursor_coords(self.size)
|
|
26
|
+
return self.move_cursor_to_coords(self.size, x, y - 1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ComposerWidget(urwid.Filler):
|
|
30
|
+
"""Composer area and chat status bar"""
|
|
31
|
+
|
|
32
|
+
signals = [SENDING_MSG_FAILED]
|
|
33
|
+
|
|
34
|
+
def __init__(self, client: Client, keymap: Dict[str, str]) -> None:
|
|
35
|
+
self.client = client
|
|
36
|
+
self.keymap = keymap
|
|
37
|
+
self.chat: Optional[Tuple[int, int]] = None
|
|
38
|
+
self.status_bar = urwid.Text(("status_bar", ""), align="left")
|
|
39
|
+
self.edit_widget = ReadlineEdit2(keymap["insert_new_line"])
|
|
40
|
+
prompt = urwid.Columns([(urwid.PACK, urwid.Text("> ")), self.edit_widget])
|
|
41
|
+
super().__init__(urwid.Pile([urwid.AttrMap(self.status_bar, "status_bar"), prompt]))
|
|
42
|
+
self._update_status_bar(None)
|
|
43
|
+
|
|
44
|
+
def set_chat(self, chat: Optional[Tuple[int, int]]) -> None:
|
|
45
|
+
self.chat = chat
|
|
46
|
+
self.edit_widget.set_edit_text("")
|
|
47
|
+
self.edit_widget.set_edit_pos(0)
|
|
48
|
+
self._update_status_bar(chat)
|
|
49
|
+
|
|
50
|
+
def _send_message(self, text) -> None:
|
|
51
|
+
accid, chatid = self.chat or (0, 0)
|
|
52
|
+
if accid:
|
|
53
|
+
chat = self.client.rpc.get_basic_chat_info(accid, chatid)
|
|
54
|
+
if chat.is_contact_request or chat.is_protection_broken:
|
|
55
|
+
# accept contact requests automatically on sending
|
|
56
|
+
self.client.rpc.accept_chat(accid, chatid)
|
|
57
|
+
try:
|
|
58
|
+
self.client.rpc.send_msg(accid, chatid, MsgData(text=text))
|
|
59
|
+
except JsonRpcError:
|
|
60
|
+
errmsg = "Message could not be sent, are you a member of the chat?"
|
|
61
|
+
urwid.emit_signal(self, SENDING_MSG_FAILED, errmsg)
|
|
62
|
+
else:
|
|
63
|
+
urwid.emit_signal(self, SENDING_MSG_FAILED, "No chat selected")
|
|
64
|
+
|
|
65
|
+
def _update_status_bar(self, chat: Optional[Tuple[int, int]]) -> None:
|
|
66
|
+
if chat:
|
|
67
|
+
info = self.client.rpc.get_basic_chat_info(*chat)
|
|
68
|
+
verified = "✓ " if info.is_protected or info.is_device_chat else ""
|
|
69
|
+
muted = " (muted)" if info.is_muted else ""
|
|
70
|
+
name = shorten_text(info.name, 40)
|
|
71
|
+
|
|
72
|
+
subtitle = shorten_text(get_subtitle(self.client.rpc, chat[0], info), 40)
|
|
73
|
+
text = f" {verified}[ {name} ]{muted} -- {subtitle}"
|
|
74
|
+
else:
|
|
75
|
+
text = f" Cursed Delta {__version__}"
|
|
76
|
+
|
|
77
|
+
self.status_bar.set_text(text)
|
|
78
|
+
|
|
79
|
+
def keypress(self, size: list, key: str) -> Optional[str]:
|
|
80
|
+
if key == self.keymap["send_msg"]:
|
|
81
|
+
text = self.edit_widget.get_edit_text().strip()
|
|
82
|
+
if text:
|
|
83
|
+
self.edit_widget.set_edit_text("")
|
|
84
|
+
self._send_message(text)
|
|
85
|
+
return None
|
|
86
|
+
return super().keypress(size, key)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Widget container that handles key presses in the contained widget."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
|
|
5
|
+
import urwid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Container(urwid.WidgetPlaceholder):
|
|
9
|
+
"""Widget container that handles key presses in the contained widget."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
widget: urwid.Widget,
|
|
14
|
+
callback: Callable[[list, str], Optional[str]],
|
|
15
|
+
unhandled_only: bool = True,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
:param widget: the contained widget
|
|
19
|
+
:param callback: the callback to call on key presses unhandled by the contained widget
|
|
20
|
+
:param unhandled_only: if set to False call callback first and if it didn't handle
|
|
21
|
+
the key press then pass it to the contained widget. If set to True
|
|
22
|
+
only call callback on unhandled key presses
|
|
23
|
+
"""
|
|
24
|
+
self._callback = callback
|
|
25
|
+
self._unhandled_only = unhandled_only
|
|
26
|
+
super().__init__(widget)
|
|
27
|
+
|
|
28
|
+
def keypress(self, size: list, key: str) -> Optional[str]:
|
|
29
|
+
if self._unhandled_only:
|
|
30
|
+
return super().keypress(size, key) and self._callback(size, key)
|
|
31
|
+
return self._callback(size, key) and super().keypress(size, key)
|