Merge pull request #53 from paul-nameless/config

Rewrite config
This commit is contained in:
Nameless 2020-05-29 17:12:43 +08:00 committed by GitHub
commit ddd57865a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 110 additions and 75 deletions

View file

@ -1,36 +1,60 @@
import configparser """
import mailcap Every parameter (except for DEFAULT_CONFIG) can be
import mimetypes overwritten by external config file
"""
import os 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/") DEFAULT_FILES = os.path.expanduser("~/.cache/tg/")
max_download_size = "10MB" LOG_LEVEL = "INFO"
record_cmd = "ffmpeg -f avfoundation -i ':0' -ar 22050 -b:a 32k '{file_path}'"
long_msg_cmd = "vim + -c 'startinsert' {file_path}" API_ID = "559815"
editor = os.environ.get("EDITOR", "vi") 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): if os.path.isfile(DEFAULT_CONFIG):
cfg = configparser.ConfigParser() config_params = runpy.run_path(DEFAULT_CONFIG)
cfg.read(config) for param, value in config_params.items():
return cfg if param.isupper():
globals()[param] = value
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

View file

@ -4,7 +4,6 @@ import os
import threading import threading
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
from signal import SIGWINCH, signal
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, Optional
@ -25,15 +24,12 @@ from tg.views import View
log = logging.getLogger(__name__) 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. # 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 # 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 # be removed from the display in order to achive scroll threshold. this could
# cause blan areas on the msg display screen # cause blan areas on the msg display screen
MSGS_LEFT_SCROLL_THRESHOLD = 2 MSGS_LEFT_SCROLL_THRESHOLD = 2
REPLY_MSG_PREFIX = "# >"
key_bind_handler_type = Callable[[Any], Any] key_bind_handler_type = Callable[[Any], Any]
@ -51,7 +47,6 @@ class Controller:
self.lock = threading.Lock() self.lock = threading.Lock()
self.tg = tg self.tg = tg
self.chat_size = 0.5 self.chat_size = 0.5
signal(SIGWINCH, self.resize_handler)
self.chat_bindings: Dict[str, key_bind_handler_type] = { self.chat_bindings: Dict[str, key_bind_handler_type] = {
"q": lambda _: "QUIT", "q": lambda _: "QUIT",
@ -73,7 +68,9 @@ class Controller:
self.msg_bindings: Dict[str, key_bind_handler_type] = { self.msg_bindings: Dict[str, key_bind_handler_type] = {
"q": lambda _: "QUIT", "q": lambda _: "QUIT",
"h": lambda _: "BACK", "h": lambda _: "BACK",
"bp": lambda _: self.breakpoint(),
"^D": lambda _: "BACK", "^D": lambda _: "BACK",
# navigate msgs
"]": self.next_chat, "]": self.next_chat,
"[": self.prev_chat, "[": self.prev_chat,
"J": lambda _: self.next_msg(10), "J": lambda _: self.next_msg(10),
@ -83,26 +80,28 @@ class Controller:
"k": self.prev_msg, "k": self.prev_msg,
"^P": self.prev_msg, "^P": self.prev_msg,
"G": lambda _: self.jump_bottom(), "G": lambda _: self.jump_bottom(),
"dd": lambda _: self.delete_msg(), # send files
"D": lambda _: self.download_current_file(),
"l": lambda _: self.open_current_msg(),
"sd": lambda _: self.send_file(self.tg.send_doc), "sd": lambda _: self.send_file(self.tg.send_doc),
"sp": lambda _: self.send_file(self.tg.send_photo), "sp": lambda _: self.send_file(self.tg.send_photo),
"sa": lambda _: self.send_file(self.tg.send_audio), "sa": lambda _: self.send_file(self.tg.send_audio),
"sv": lambda _: self.send_video(), "sv": lambda _: self.send_video(),
"v": lambda _: self.send_voice(), "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(), "e": lambda _: self.edit_msg(),
"i": lambda _: self.write_short_msg(), "i": lambda _: self.write_short_msg(),
"a": lambda _: self.write_short_msg(), "a": lambda _: self.write_short_msg(),
"I": lambda _: self.write_long_msg(), "I": lambda _: self.write_long_msg(),
"A": 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_message(),
"R": lambda _: self.reply_with_long_message(), "R": lambda _: self.reply_with_long_message(),
"bp": lambda _: self.breakpoint(), # message selection
" ": lambda _: self.toggle_select_msg(), " ": lambda _: self.toggle_select_msg(),
"^[": lambda _: self.discard_selected_msgs(), # esc "^[": lambda _: self.discard_selected_msgs(), # esc
"y": lambda _: self.copy_msgs(),
"p": lambda _: self.forward_msgs(),
} }
def forward_msgs(self): def forward_msgs(self):
@ -230,7 +229,7 @@ class Controller:
) as s: ) as s:
f.write(insert_replied_msg(msg)) f.write(insert_replied_msg(msg))
f.seek(0) 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: with open(f.name) as f:
if msg := strip_replied_msg(f.read().strip()): if msg := strip_replied_msg(f.read().strip()):
self.model.reply_message(text=msg) self.model.reply_message(text=msg)
@ -272,7 +271,7 @@ class Controller:
def send_voice(self): def send_voice(self):
file_path = f"/tmp/voice-{datetime.now()}.oga" file_path = f"/tmp/voice-{datetime.now()}.oga"
with suspend(self.view) as s: 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( resp = self.view.status.get_input(
f"Do you want to send recording: {file_path}? [Y/n]" f"Do you want to send recording: {file_path}? [Y/n]"
) )
@ -354,7 +353,7 @@ class Controller:
) as s: ) as s:
f.write(msg.text_content) f.write(msg.text_content)
f.flush() f.flush()
s.call(f"{config.editor} {f.name}") s.call(f"{config.EDITOR} {f.name}")
with open(f.name) as f: with open(f.name) as f:
if text := f.read().strip(): if text := f.read().strip():
self.model.edit_message(text=text) self.model.edit_message(text=text)
@ -364,7 +363,7 @@ class Controller:
with NamedTemporaryFile("r+", suffix=".txt") as f, suspend( with NamedTemporaryFile("r+", suffix=".txt") as f, suspend(
self.view self.view
) as s: ) 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: with open(f.name) as f:
if msg := f.read().strip(): if msg := f.read().strip():
self.model.send_message(text=msg) self.model.send_message(text=msg)

View file

@ -15,13 +15,24 @@ log = logging.getLogger(__name__)
def run(tg: Tdlib, stdscr: window) -> None: 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 <q>
log.info("Interrupt signal is handled and ignored on purpose.")
signal.signal(signal.SIGINT, interrupt_signal_handler)
model = Model(tg) model = Model(tg)
status_view = StatusView(stdscr) status_view = StatusView(stdscr)
msg_view = MsgView(stdscr, model.msgs, model, model.users) msg_view = MsgView(stdscr, model.msgs, model, model.users)
chat_view = ChatView(stdscr) chat_view = ChatView(stdscr)
view = View(stdscr, chat_view, msg_view, status_view) view = View(stdscr, chat_view, msg_view, status_view)
controller = Controller(model, view, tg) 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(): for msg_type, handler in update_handlers.handlers.items():
tg.add_update_handler(msg_type, partial(handler, controller)) tg.add_update_handler(msg_type, partial(handler, controller))
@ -31,28 +42,17 @@ def run(tg: Tdlib, stdscr: window) -> None:
def main(): def main():
def signal_handler(sig, frame): utils.setup_log(config.LOG_LEVEL)
log.info("You pressed Ctrl+C!")
signal.signal(signal.SIGINT, signal_handler)
cfg = config.get_cfg()["DEFAULT"]
utils.setup_log(cfg.get("level", "DEBUG"))
log.debug("#" * 64)
tg = Tdlib( tg = Tdlib(
api_id=cfg["api_id"], api_id=config.API_ID,
api_hash=cfg["api_hash"], api_hash=config.API_HASH,
phone=cfg["phone"], phone=config.PHONE,
database_encryption_key=cfg["enc_key"], database_encryption_key=config.ENC_KEY,
files_directory=cfg.get("files", config.DEFAULT_FILES), files_directory=config.DEFAULT_FILES,
tdlib_verbosity=cfg.get("tdlib_verbosity", 0), tdlib_verbosity=config.TDLIB_VERBOSITY,
library_path=cfg.get("library_path"), 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() tg.login()
wrapper(partial(run, tg)) wrapper(partial(run, tg))

View file

@ -2,7 +2,7 @@ import logging
from functools import wraps from functools import wraps
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, Optional
from tg import config from tg import config, utils
from tg.controllers import Controller from tg.controllers import Controller
from tg.msg import MsgProxy from tg.msg import MsgProxy
@ -12,6 +12,8 @@ _update_handler_type = Callable[[Controller, Dict[str, Any]], None]
handlers: Dict[str, _update_handler_type] = {} handlers: Dict[str, _update_handler_type] = {}
max_download_size: int = utils.parse_size(config.MAX_DOWNLOAD_SIZE)
def update_handler(update_type): def update_handler(update_type):
def decorator(fun): def decorator(fun):
@ -57,7 +59,7 @@ def update_new_msg(controller: Controller, update: Dict[str, Any]):
) )
if current_chat_id == msg.chat_id: if current_chat_id == msg.chat_id:
controller.refresh_msgs() 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.download(msg.file_id, msg.chat_id, msg["id"])
controller._notify_for_message(msg.chat_id, msg) controller._notify_for_message(msg.chat_id, msg)

View file

@ -1,7 +1,9 @@
import base64 import base64
import curses import curses
import logging import logging
import mailcap
import math import math
import mimetypes
import os import os
import random import random
import re 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} 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(): if size[-2].isalpha():
number, unit = size[:-2], size[-2:] number, unit = size[:-2], size[-2:]
else: else:
@ -107,10 +120,7 @@ def setup_log(level="DEBUG"):
def notify( def notify(
msg, msg, subtitle="", title="tg", cmd=config.NOTIFY_CMD,
subtitle="",
title="tg",
cmd=config.get_cfg()["DEFAULT"].get("notify_cmd"),
): ):
if not cmd: if not cmd:
return return
@ -146,7 +156,7 @@ class suspend:
subprocess.call(cmd, shell=True) subprocess.call(cmd, shell=True)
def open_file(self, file_path): def open_file(self, file_path):
cmd = config.get_file_handler(file_path) cmd = get_file_handler(file_path)
if not cmd: if not cmd:
return return
self.call(cmd) self.call(cmd)