diff --git a/tg/config.py b/tg/config.py index d283866..8ef1ce4 100644 --- a/tg/config.py +++ b/tg/config.py @@ -1,36 +1,60 @@ -import configparser -import mailcap -import mimetypes +""" +Every parameter (except for DEFAULT_CONFIG) can be +overwritten by external config file +""" import os +import platform +import runpy -DEFAULT_CONFIG = os.path.expanduser("~/.config/tg/tg.conf") +_os_name = platform.system() +_darwin = "Darwin" +_linux = "Linux" + + +DEFAULT_CONFIG = os.path.expanduser("~/.config/tg/conf.py") DEFAULT_FILES = os.path.expanduser("~/.cache/tg/") -max_download_size = "10MB" -record_cmd = "ffmpeg -f avfoundation -i ':0' -ar 22050 -b:a 32k '{file_path}'" -long_msg_cmd = "vim + -c 'startinsert' {file_path}" -editor = os.environ.get("EDITOR", "vi") +LOG_LEVEL = "INFO" + +API_ID = "559815" +API_HASH = "fd121358f59d764c57c55871aa0807ca" + +PHONE = None +ENC_KEY = None + +TDLIB_PATH = None +TDLIB_VERBOSITY = 0 + +MAX_DOWNLOAD_SIZE = "10MB" + +# TODO: check platform +NOTIFY_CMD = "/usr/local/bin/terminal-notifier -title '{title}' -subtitle '{subtitle}' -message '{msg}' -appIcon '{icon_path}'" +# TODO: check platform +if _os_name == _linux: + VOICE_RECORD_CMD = ( + "ffmpeg -f alsa -i default -ar 22050 -b:a 32k '{file_path}'" + ) +else: + VOICE_RECORD_CMD = ( + "ffmpeg -f avfoundation -i default -ar 22050 -b:a 32k '{file_path}'" + ) + +# TODO: use mailcap instead of editor +LONG_MSG_CMD = "vim + -c 'startinsert' {file_path}" +EDITOR = os.environ.get("EDITOR", "vi") + +if _os_name == _linux: + DEFAULT_OPEN = "xdg-open '{file_path}'" +else: + DEFAULT_OPEN = "open '{file_path}'" + +if _os_name == _linux: + DEFAULT_COPY = "xclip -selection c" +else: + DEFAULT_COPY = "pbcopy" -def get_cfg(config=DEFAULT_CONFIG): - cfg = configparser.ConfigParser() - cfg.read(config) - return cfg - - -def save_cfg(cfg, config=DEFAULT_CONFIG): - config_dir = os.path.dirname(config) - if not os.path.isdir(config_dir): - os.makedirs(config_dir) - with open(config, "w") as f: - cfg.write(f) - - -def get_file_handler(file_name, default=None): - mtype, _ = mimetypes.guess_type(file_name) - if not mtype: - return default - caps = mailcap.getcaps() - handler, view = mailcap.findmatch(caps, mtype, filename=file_name) - if not handler: - return None - return handler +if os.path.isfile(DEFAULT_CONFIG): + config_params = runpy.run_path(DEFAULT_CONFIG) + for param, value in config_params.items(): + if param.isupper(): + globals()[param] = value diff --git a/tg/controllers.py b/tg/controllers.py index 639c9d3..f96c945 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -4,7 +4,6 @@ import os import threading from datetime import datetime from functools import partial -from signal import SIGWINCH, signal from tempfile import NamedTemporaryFile from typing import Any, Callable, Dict, Optional @@ -25,15 +24,12 @@ from tg.views import View log = logging.getLogger(__name__) -MSGS_LEFT_SCROLL_THRESHOLD = 10 -REPLY_MSG_PREFIX = "# >" - # start scrolling to next page when number of the msgs left is less than value. # note, that setting high values could lead to situations when long msgs will # be removed from the display in order to achive scroll threshold. this could # cause blan areas on the msg display screen MSGS_LEFT_SCROLL_THRESHOLD = 2 - +REPLY_MSG_PREFIX = "# >" key_bind_handler_type = Callable[[Any], Any] @@ -51,7 +47,6 @@ class Controller: self.lock = threading.Lock() self.tg = tg self.chat_size = 0.5 - signal(SIGWINCH, self.resize_handler) self.chat_bindings: Dict[str, key_bind_handler_type] = { "q": lambda _: "QUIT", @@ -73,7 +68,9 @@ class Controller: self.msg_bindings: Dict[str, key_bind_handler_type] = { "q": lambda _: "QUIT", "h": lambda _: "BACK", + "bp": lambda _: self.breakpoint(), "^D": lambda _: "BACK", + # navigate msgs "]": self.next_chat, "[": self.prev_chat, "J": lambda _: self.next_msg(10), @@ -83,26 +80,28 @@ class Controller: "k": self.prev_msg, "^P": self.prev_msg, "G": lambda _: self.jump_bottom(), - "dd": lambda _: self.delete_msg(), - "D": lambda _: self.download_current_file(), - "l": lambda _: self.open_current_msg(), + # send files "sd": lambda _: self.send_file(self.tg.send_doc), "sp": lambda _: self.send_file(self.tg.send_photo), "sa": lambda _: self.send_file(self.tg.send_audio), "sv": lambda _: self.send_video(), "v": lambda _: self.send_voice(), + # manipulate msgs + "dd": lambda _: self.delete_msg(), + "D": lambda _: self.download_current_file(), + "l": lambda _: self.open_current_msg(), "e": lambda _: self.edit_msg(), "i": lambda _: self.write_short_msg(), "a": lambda _: self.write_short_msg(), "I": lambda _: self.write_long_msg(), "A": lambda _: self.write_long_msg(), + "p": lambda _: self.forward_msgs(), + "y": lambda _: self.copy_msgs(), "r": lambda _: self.reply_message(), "R": lambda _: self.reply_with_long_message(), - "bp": lambda _: self.breakpoint(), + # message selection " ": lambda _: self.toggle_select_msg(), "^[": lambda _: self.discard_selected_msgs(), # esc - "y": lambda _: self.copy_msgs(), - "p": lambda _: self.forward_msgs(), } def forward_msgs(self): @@ -230,7 +229,7 @@ class Controller: ) as s: f.write(insert_replied_msg(msg)) f.seek(0) - s.call(config.long_msg_cmd.format(file_path=f.name)) + s.call(config.LONG_MSG_CMD.format(file_path=f.name)) with open(f.name) as f: if msg := strip_replied_msg(f.read().strip()): self.model.reply_message(text=msg) @@ -272,7 +271,7 @@ class Controller: def send_voice(self): file_path = f"/tmp/voice-{datetime.now()}.oga" with suspend(self.view) as s: - s.call(config.record_cmd.format(file_path=file_path)) + s.call(config.VOICE_RECORD_CMD.format(file_path=file_path)) resp = self.view.status.get_input( f"Do you want to send recording: {file_path}? [Y/n]" ) @@ -354,7 +353,7 @@ class Controller: ) as s: f.write(msg.text_content) f.flush() - s.call(f"{config.editor} {f.name}") + s.call(f"{config.EDITOR} {f.name}") with open(f.name) as f: if text := f.read().strip(): self.model.edit_message(text=text) @@ -364,7 +363,7 @@ class Controller: with NamedTemporaryFile("r+", suffix=".txt") as f, suspend( self.view ) as s: - s.call(config.long_msg_cmd.format(file_path=f.name)) + s.call(config.LONG_MSG_CMD.format(file_path=f.name)) with open(f.name) as f: if msg := f.read().strip(): self.model.send_message(text=msg) diff --git a/tg/main.py b/tg/main.py index 385e4e4..2c69351 100644 --- a/tg/main.py +++ b/tg/main.py @@ -15,13 +15,24 @@ log = logging.getLogger(__name__) def run(tg: Tdlib, stdscr: window) -> None: - # run this function in thread? + + # handle ctrl+c, to avoid interrupting tg when subprocess is called + def interrupt_signal_handler(sig, frame): + # TODO: draw on status pane: to quite press + log.info("Interrupt signal is handled and ignored on purpose.") + + signal.signal(signal.SIGINT, interrupt_signal_handler) + model = Model(tg) status_view = StatusView(stdscr) msg_view = MsgView(stdscr, model.msgs, model, model.users) chat_view = ChatView(stdscr) view = View(stdscr, chat_view, msg_view, status_view) controller = Controller(model, view, tg) + + # hanlde resize of terminal correctly + signal.signal(signal.SIGWINCH, controller.resize_handler) + for msg_type, handler in update_handlers.handlers.items(): tg.add_update_handler(msg_type, partial(handler, controller)) @@ -31,28 +42,17 @@ def run(tg: Tdlib, stdscr: window) -> None: def main(): - def signal_handler(sig, frame): - log.info("You pressed Ctrl+C!") + utils.setup_log(config.LOG_LEVEL) - signal.signal(signal.SIGINT, signal_handler) - - cfg = config.get_cfg()["DEFAULT"] - utils.setup_log(cfg.get("level", "DEBUG")) - log.debug("#" * 64) tg = Tdlib( - api_id=cfg["api_id"], - api_hash=cfg["api_hash"], - phone=cfg["phone"], - database_encryption_key=cfg["enc_key"], - files_directory=cfg.get("files", config.DEFAULT_FILES), - tdlib_verbosity=cfg.get("tdlib_verbosity", 0), - library_path=cfg.get("library_path"), + api_id=config.API_ID, + api_hash=config.API_HASH, + phone=config.PHONE, + database_encryption_key=config.ENC_KEY, + files_directory=config.DEFAULT_FILES, + tdlib_verbosity=config.TDLIB_VERBOSITY, + library_path=config.TDLIB_PATH, ) - config.max_download_size = utils.parse_size( - cfg.get("max_download_size", config.max_download_size) - ) - config.record_cmd = cfg.get("record_cmd", config.record_cmd) - config.long_msg_cmd = cfg.get("long_msg_cmd", config.long_msg_cmd) tg.login() wrapper(partial(run, tg)) diff --git a/tg/update_handlers.py b/tg/update_handlers.py index f92adf2..bcb1f9f 100644 --- a/tg/update_handlers.py +++ b/tg/update_handlers.py @@ -2,7 +2,7 @@ import logging from functools import wraps from typing import Any, Callable, Dict, Optional -from tg import config +from tg import config, utils from tg.controllers import Controller from tg.msg import MsgProxy @@ -12,6 +12,8 @@ _update_handler_type = Callable[[Controller, Dict[str, Any]], None] handlers: Dict[str, _update_handler_type] = {} +max_download_size: int = utils.parse_size(config.MAX_DOWNLOAD_SIZE) + def update_handler(update_type): def decorator(fun): @@ -57,7 +59,7 @@ def update_new_msg(controller: Controller, update: Dict[str, Any]): ) if current_chat_id == msg.chat_id: controller.refresh_msgs() - if msg.file_id and msg.size <= config.max_download_size: + if msg.file_id and msg.size <= max_download_size: controller.download(msg.file_id, msg.chat_id, msg["id"]) controller._notify_for_message(msg.chat_id, msg) diff --git a/tg/utils.py b/tg/utils.py index f360409..a3af868 100644 --- a/tg/utils.py +++ b/tg/utils.py @@ -1,7 +1,9 @@ import base64 import curses import logging +import mailcap import math +import mimetypes import os import random import re @@ -30,7 +32,18 @@ emoji_pattern = re.compile( units = {"B": 1, "KB": 10 ** 3, "MB": 10 ** 6, "GB": 10 ** 9, "TB": 10 ** 12} -def parse_size(size): +def get_file_handler(file_name, default=None): + mtype, _ = mimetypes.guess_type(file_name) + if not mtype: + return default + caps = mailcap.getcaps() + handler, view = mailcap.findmatch(caps, mtype, filename=file_name) + if not handler: + return None + return handler + + +def parse_size(size: str) -> int: if size[-2].isalpha(): number, unit = size[:-2], size[-2:] else: @@ -107,10 +120,7 @@ def setup_log(level="DEBUG"): def notify( - msg, - subtitle="", - title="tg", - cmd=config.get_cfg()["DEFAULT"].get("notify_cmd"), + msg, subtitle="", title="tg", cmd=config.NOTIFY_CMD, ): if not cmd: return @@ -146,7 +156,7 @@ class suspend: subprocess.call(cmd, shell=True) def open_file(self, file_path): - cmd = config.get_file_handler(file_path) + cmd = get_file_handler(file_path) if not cmd: return self.call(cmd)