jrnl/jrnl/util.py
2019-11-04 16:36:16 +01:00

144 lines
4.2 KiB
Python

#!/usr/bin/env python
import sys
import os
import getpass as gp
import yaml
if "win32" in sys.platform:
import colorama
colorama.init()
import re
import tempfile
import subprocess
import unicodedata
import shlex
import logging
log = logging.getLogger(__name__)
WARNING_COLOR = "\033[33m"
ERROR_COLOR = "\033[31m"
RESET_COLOR = "\033[0m"
# Based on Segtok by Florian Leitner
# https://github.com/fnl/segtok
SENTENCE_SPLITTER = re.compile(r"""
( # A sentence ends at one of two sequences:
[.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal,
[\'\u2019\"\u201D]? # an optional right quote,
[\]\)]* # optional closing brackets and
\s+ # a sequence of required spaces.
| # Otherwise,
\n # a sentence also terminates newlines.
)""", re.VERBOSE)
class UserAbort(Exception):
pass
getpass = gp.getpass
def get_password(validator, keychain=None, max_attempts=3):
pwd_from_keychain = keychain and get_keychain(keychain)
password = pwd_from_keychain or getpass()
result = validator(password)
# Password is bad:
if result is None and pwd_from_keychain:
set_keychain(keychain, None)
attempt = 1
while result is None and attempt < max_attempts:
print("Wrong password, try again.", file=sys.stderr)
password = gp.getpass()
result = validator(password)
attempt += 1
if result is not None:
return result
else:
print("Extremely wrong password.", file=sys.stderr)
sys.exit(1)
def get_keychain(journal_name):
import keyring
return keyring.get_password('jrnl', journal_name)
def set_keychain(journal_name, password):
import keyring
if password is None:
try:
keyring.delete_password('jrnl', journal_name)
except RuntimeError:
pass
else:
keyring.set_password('jrnl', journal_name, password)
def yesno(prompt, default=True):
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
response = input(prompt)
return {"y": True, "n": False}.get(response.lower(), default)
def load_config(config_path):
"""Tries to load a config file from YAML.
"""
with open(config_path) as f:
return yaml.load(f, Loader=yaml.FullLoader)
def scope_config(config, journal_name):
if journal_name not in config['journals']:
return config
config = config.copy()
journal_conf = config['journals'].get(journal_name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
log.debug('Updating configuration with specific journal overrides %s', journal_conf)
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
config.pop('journals')
return config
def get_text_from_editor(config, template=""):
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
with open(tmpfile, 'w', encoding="utf-8") as f:
if template:
f.write(template)
try:
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
except AttributeError:
subprocess.call(config['editor'] + [tmpfile])
with open(tmpfile, "r", encoding="utf-8") as f:
raw = f.read()
os.close(filehandle)
os.remove(tmpfile)
if not raw:
print('[Nothing saved to file]', file=sys.stderr)
return raw
def colorize(string):
"""Returns the string wrapped in cyan ANSI escape"""
return f"\033[36m{string}\033[39m"
def slugify(string):
"""Slugifies a string.
Based on public domain code from https://github.com/zacharyvoase/slugify
"""
normalized_string = str(unicodedata.normalize('NFKD', string))
no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
slug = re.sub(r'[-\s]+', '-', no_punctuation)
return slug
def split_title(text):
"""Splits the first sentence off from a text."""
punkt = SENTENCE_SPLITTER.search(text)
if not punkt:
return text, ""
return text[:punkt.end()].strip(), text[punkt.end():].strip()