tg 0.20.0__py2.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.
- tg/__init__.py +5 -0
- tg/__main__.py +78 -0
- tg/colors.py +53 -0
- tg/config.py +99 -0
- tg/controllers.py +890 -0
- tg/models.py +823 -0
- tg/msg.py +247 -0
- tg/resources/tg.png +0 -0
- tg/tdlib.py +448 -0
- tg/update_handlers.py +305 -0
- tg/utils.py +315 -0
- tg/views.py +691 -0
- tg-0.20.0.dist-info/METADATA +318 -0
- tg-0.20.0.dist-info/RECORD +15 -0
- tg-0.20.0.dist-info/WHEEL +4 -0
tg/__init__.py
ADDED
tg/__main__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging.handlers
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
from curses import window, wrapper
|
|
5
|
+
from functools import partial
|
|
6
|
+
from types import FrameType
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from tg import config, update_handlers, utils
|
|
10
|
+
from tg.controllers import Controller
|
|
11
|
+
from tg.models import Model
|
|
12
|
+
from tg.tdlib import Tdlib
|
|
13
|
+
from tg.views import ChatView, MsgView, StatusView, View
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(tg: Tdlib, stdscr: window) -> None:
|
|
19
|
+
# handle ctrl+c, to avoid interrupting tg when subprocess is called
|
|
20
|
+
def interrupt_signal_handler(sig: int, frame: Optional[FrameType]) -> None:
|
|
21
|
+
# TODO: draw on status pane: to quite press <q>
|
|
22
|
+
log.info("Interrupt signal is handled and ignored on purpose.")
|
|
23
|
+
|
|
24
|
+
signal.signal(signal.SIGINT, interrupt_signal_handler)
|
|
25
|
+
|
|
26
|
+
model = Model(tg)
|
|
27
|
+
status_view = StatusView(stdscr)
|
|
28
|
+
msg_view = MsgView(stdscr, model)
|
|
29
|
+
chat_view = ChatView(stdscr, model)
|
|
30
|
+
view = View(stdscr, chat_view, msg_view, status_view)
|
|
31
|
+
controller = Controller(model, view, tg)
|
|
32
|
+
|
|
33
|
+
# hanlde resize of terminal correctly
|
|
34
|
+
signal.signal(signal.SIGWINCH, controller.resize_handler)
|
|
35
|
+
|
|
36
|
+
for msg_type, handler in update_handlers.handlers.items():
|
|
37
|
+
tg.add_update_handler(msg_type, partial(handler, controller))
|
|
38
|
+
|
|
39
|
+
thread = threading.Thread(target=controller.run)
|
|
40
|
+
thread.daemon = True
|
|
41
|
+
thread.start()
|
|
42
|
+
|
|
43
|
+
controller.draw()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_args() -> None:
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-v", "--version"):
|
|
50
|
+
import tg
|
|
51
|
+
|
|
52
|
+
print("Terminal Telegram client")
|
|
53
|
+
print("Version:", tg.__version__)
|
|
54
|
+
exit(0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
parse_args()
|
|
59
|
+
utils.cleanup_cache()
|
|
60
|
+
tg = Tdlib(
|
|
61
|
+
api_id=config.API_ID,
|
|
62
|
+
api_hash=config.API_HASH,
|
|
63
|
+
phone=config.PHONE,
|
|
64
|
+
database_encryption_key=config.ENC_KEY,
|
|
65
|
+
files_directory=config.FILES_DIR,
|
|
66
|
+
tdlib_verbosity=config.TDLIB_VERBOSITY,
|
|
67
|
+
library_path=config.TDLIB_PATH,
|
|
68
|
+
)
|
|
69
|
+
tg.login()
|
|
70
|
+
|
|
71
|
+
utils.setup_log()
|
|
72
|
+
utils.set_shorter_esc_delay()
|
|
73
|
+
|
|
74
|
+
wrapper(partial(run, tg))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
tg/colors.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import curses
|
|
2
|
+
|
|
3
|
+
DEFAULT_FG = curses.COLOR_WHITE
|
|
4
|
+
DEFAULT_BG = curses.COLOR_BLACK
|
|
5
|
+
COLOR_PAIRS = {(10, 10): 0}
|
|
6
|
+
COLOR_PAIRS = {}
|
|
7
|
+
|
|
8
|
+
# colors
|
|
9
|
+
black = curses.COLOR_BLACK
|
|
10
|
+
blue = curses.COLOR_BLUE
|
|
11
|
+
cyan = curses.COLOR_CYAN
|
|
12
|
+
green = curses.COLOR_GREEN
|
|
13
|
+
magenta = curses.COLOR_MAGENTA
|
|
14
|
+
red = curses.COLOR_RED
|
|
15
|
+
white = curses.COLOR_WHITE
|
|
16
|
+
yellow = curses.COLOR_YELLOW
|
|
17
|
+
default = -1
|
|
18
|
+
|
|
19
|
+
# modes
|
|
20
|
+
normal = curses.A_NORMAL
|
|
21
|
+
bold = curses.A_BOLD
|
|
22
|
+
blink = curses.A_BLINK
|
|
23
|
+
reverse = curses.A_REVERSE
|
|
24
|
+
underline = curses.A_UNDERLINE
|
|
25
|
+
invisible = curses.A_INVIS
|
|
26
|
+
dim = curses.A_DIM
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_color(fg: int, bg: int) -> int:
|
|
30
|
+
"""Returns the curses color pair for the given fg/bg combination."""
|
|
31
|
+
|
|
32
|
+
key = (fg, bg)
|
|
33
|
+
if key not in COLOR_PAIRS:
|
|
34
|
+
size = len(COLOR_PAIRS)
|
|
35
|
+
try:
|
|
36
|
+
curses.init_pair(size, fg, bg)
|
|
37
|
+
except curses.error:
|
|
38
|
+
# If curses.use_default_colors() failed during the initialization
|
|
39
|
+
# of curses, then using -1 as fg or bg will fail as well, which
|
|
40
|
+
# we need to handle with fallback-defaults:
|
|
41
|
+
if fg == -1: # -1 is the "default" color
|
|
42
|
+
fg = DEFAULT_FG
|
|
43
|
+
if bg == -1: # -1 is the "default" color
|
|
44
|
+
bg = DEFAULT_BG
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
curses.init_pair(size, fg, bg)
|
|
48
|
+
except curses.error:
|
|
49
|
+
# If this fails too, colors are probably not supported
|
|
50
|
+
pass
|
|
51
|
+
COLOR_PAIRS[key] = size
|
|
52
|
+
|
|
53
|
+
return curses.color_pair(COLOR_PAIRS[key])
|
tg/config.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Every parameter (except for CONFIG_FILE) can be
|
|
3
|
+
overwritten by external config file
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import runpy
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
_os_name = platform.system()
|
|
12
|
+
_darwin = "Darwin"
|
|
13
|
+
_linux = "Linux"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CONFIG_DIR = os.path.expanduser("~/.config/tg/")
|
|
17
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "conf.py")
|
|
18
|
+
FILES_DIR = os.path.expanduser("~/.cache/tg/")
|
|
19
|
+
MAILCAP_FILE: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
LOG_LEVEL = "INFO"
|
|
22
|
+
LOG_PATH = os.path.expanduser("~/.local/share/tg/")
|
|
23
|
+
|
|
24
|
+
API_ID = 559815
|
|
25
|
+
API_HASH = "fd121358f59d764c57c55871aa0807ca"
|
|
26
|
+
|
|
27
|
+
PHONE = None
|
|
28
|
+
ENC_KEY = ""
|
|
29
|
+
|
|
30
|
+
TDLIB_PATH = None
|
|
31
|
+
TDLIB_VERBOSITY = 0
|
|
32
|
+
|
|
33
|
+
MAX_DOWNLOAD_SIZE = "10MB"
|
|
34
|
+
|
|
35
|
+
# TODO: check platform
|
|
36
|
+
NOTIFY_CMD = "/usr/local/bin/terminal-notifier -title {title} -subtitle {subtitle} -message {msg} -appIcon {icon_path}"
|
|
37
|
+
|
|
38
|
+
VIEW_TEXT_CMD = "less"
|
|
39
|
+
FZF = "fzf"
|
|
40
|
+
|
|
41
|
+
if _os_name == _linux:
|
|
42
|
+
# for more info see https://trac.ffmpeg.org/wiki/Capture/ALSA
|
|
43
|
+
VOICE_RECORD_CMD = "ffmpeg -f alsa -i hw:0 -c:a libopus -b:a 32k {file_path}"
|
|
44
|
+
else:
|
|
45
|
+
VOICE_RECORD_CMD = (
|
|
46
|
+
"ffmpeg -f avfoundation -i ':0' -c:a libopus -b:a 32k {file_path}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# TODO: use mailcap instead of editor
|
|
50
|
+
LONG_MSG_CMD = "vim + -c 'startinsert' {file_path}"
|
|
51
|
+
EDITOR = os.environ.get("EDITOR", "vi")
|
|
52
|
+
|
|
53
|
+
if _os_name == _linux:
|
|
54
|
+
DEFAULT_OPEN = "xdg-open {file_path}"
|
|
55
|
+
else:
|
|
56
|
+
DEFAULT_OPEN = "open {file_path}"
|
|
57
|
+
|
|
58
|
+
if _os_name == _linux:
|
|
59
|
+
if os.environ.get("WAYLAND_DISPLAY"):
|
|
60
|
+
COPY_CMD = "wl-copy"
|
|
61
|
+
else:
|
|
62
|
+
COPY_CMD = "xclip -selection c"
|
|
63
|
+
else:
|
|
64
|
+
COPY_CMD = "pbcopy"
|
|
65
|
+
|
|
66
|
+
CHAT_FLAGS: Dict[str, str] = {}
|
|
67
|
+
|
|
68
|
+
MSG_FLAGS: Dict[str, str] = {}
|
|
69
|
+
|
|
70
|
+
ICON_PATH = os.path.join(os.path.dirname(__file__), "resources", "tg.png")
|
|
71
|
+
|
|
72
|
+
URL_VIEW = "urlview"
|
|
73
|
+
|
|
74
|
+
USERS_COLORS = tuple(range(2, 16))
|
|
75
|
+
|
|
76
|
+
KEEP_MEDIA = 7
|
|
77
|
+
|
|
78
|
+
FILE_PICKER_CMD = "ranger --choosefile={file_path}"
|
|
79
|
+
|
|
80
|
+
DOWNLOAD_DIR = os.path.expanduser("~/Downloads/")
|
|
81
|
+
|
|
82
|
+
if os.path.isfile(CONFIG_FILE):
|
|
83
|
+
config_params = runpy.run_path(CONFIG_FILE)
|
|
84
|
+
for param, value in config_params.items():
|
|
85
|
+
if param.isupper():
|
|
86
|
+
globals()[param] = value
|
|
87
|
+
else:
|
|
88
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
if not PHONE:
|
|
91
|
+
print(
|
|
92
|
+
"Enter your phone number in international format (including country code)"
|
|
93
|
+
)
|
|
94
|
+
PHONE = input("phone> ")
|
|
95
|
+
if not PHONE.startswith("+"):
|
|
96
|
+
PHONE = "+" + PHONE
|
|
97
|
+
|
|
98
|
+
with open(CONFIG_FILE, "w") as f:
|
|
99
|
+
f.write(f"PHONE = '{PHONE}'\n")
|