Repos / mcross / b74362c954
commit b74362c954ba21b55f34597cd2682f1c1918b38e
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Thu Jun 18 16:39:50 2020 +0700

    unified conf file / cli args
    
    Config by priority: default > conf file > cli arg
    
    It supports long form and short form but quite limited:
    
    - Key-val style only, no on/off switch style e.g. `mcross --dark`
    - Isn't POSIX-compliant i.e. `-d1` doesn't work
    
    The Click library might help but I'm not sure if strict POSIX compliance
    is really worth complicating the dep tree...

diff --git a/README.md b/README.md
index c91d21e..c6b6474 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,22 @@ # Installation
 
 # Usage
 
-CLI arguments: `--textfont`, `--monofont`
+Run `mcross -h` to get a full list of CLI arguments. The same arguments can
+also be defined in a TOML config file: run `mcross-info` to know where this
+file should be for your OS. For example, running mcross like this:
+
+```sh
+mcross --background-color pink -t "Ubuntu"
+```
+
+is the same as putting this in `$HOME/.config/mcross/mcross.toml` for linux:
+
+```toml
+background-color = "pink"
+text-font = "Ubuntu"
+```
+
+The priority is CLI arg > config file > default.
 
 Keyboard shortcuts:
 
@@ -75,8 +90,9 @@ # Feature checklist
 - [x] non-blocking I/O using curio
 - [x] more visual indicators: waiting cursor, status bar
 - [x] parse gemini's advanced line types
-- [ ] properly handle mime types (gemini/plaintext/binary)
-- [ ] configurable document styling
+- [x] render `text/*` mime types with correct charset
+- [ ] handle `binary/*` mime types
+- [x] configurable document styling
 - [ ] human-friendly distribution
 - [ ] TOFU TLS (right now it always accepts self-signed certs)
 
diff --git a/poetry.lock b/poetry.lock
index 4262ef3..43ef576 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,11 @@
+[[package]]
+category = "main"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+name = "appdirs"
+optional = false
+python-versions = "*"
+version = "1.4.4"
+
 [[package]]
 category = "dev"
 description = "Disable App Nap on OS X 10.9"
@@ -260,6 +268,14 @@ optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 version = "1.14.0"
 
+[[package]]
+category = "main"
+description = "Python Library for Tom's Obvious, Minimal Language"
+name = "toml"
+optional = false
+python-versions = "*"
+version = "0.10.1"
+
 [[package]]
 category = "dev"
 description = "Traitlets Python config system"
@@ -309,10 +325,14 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
 testing = ["jaraco.itertools", "func-timeout"]
 
 [metadata]
-content-hash = "6aa72780c0ec9d3ed80370f35c09d1b1915c49675ab97732c49e2af47c5aa08f"
+content-hash = "4804febcb012c5490a8635fcf2e94dfb8e3db1e586c2a4534ee6bf806f74dc42"
 python-versions = "^3.7"
 
 [metadata.files]
+appdirs = [
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
 appnope = [
     {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"},
     {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"},
@@ -391,6 +411,10 @@ six = [
     {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
     {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
 ]
+toml = [
+    {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
+    {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
+]
 traitlets = [
     {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"},
     {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"},
diff --git a/pyproject.toml b/pyproject.toml
index b5f935a..1a3544d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "mcross"
-version = "0.5.16"
+version = "0.5.17"
 description = "Do you remember www?"
 authors = ["nhanb <hi@imnhan.com>"]
 license = "MIT"
@@ -13,10 +13,13 @@ repository = "https://git.sr.ht/~nhanb/mcross"
 
 [tool.poetry.scripts]
 mcross = "mcross:run"
+mcross-info = "mcross:info"
 
 [tool.poetry.dependencies]
 python = "^3.7"
 curio = "^1.2"
+appdirs = "^1.4.4"
+toml = "^0.10.1"
 
 [tool.poetry.dev-dependencies]
 python-language-server = "^0.31.10"
diff --git a/src/mcross/__init__.py b/src/mcross/__init__.py
index eb8be0e..c99ebbe 100644
--- a/src/mcross/__init__.py
+++ b/src/mcross/__init__.py
@@ -1,17 +1,20 @@
-import argparse
-
-from .gui.controller import Controller
-
-
 def run():
+    from . import conf
+    from .gui.controller import Controller
 
-    # Parse CLI arguments
-    argparser = argparse.ArgumentParser()
-    argparser.add_argument("--textfont")
-    argparser.add_argument("--monofont")
-    argparser.add_argument("--dark", action="store_true")
-    args = argparser.parse_args()
+    conf.init()
 
     # Actually start the program
-    c = Controller(args)
+    c = Controller()
     c.run()
+
+
+def info():
+    from . import conf
+    from pprint import pprint
+
+    conf.init()
+
+    print("Config file:", conf.CONFIG_FILE)
+    print("Config:")
+    pprint(conf._conf)
diff --git a/src/mcross/conf.py b/src/mcross/conf.py
new file mode 100644
index 0000000..f37c43a
--- /dev/null
+++ b/src/mcross/conf.py
@@ -0,0 +1,78 @@
+import argparse
+import os
+from collections import namedtuple
+from pathlib import Path
+
+import toml
+from appdirs import user_config_dir
+
+CONFIG_DIR = Path(user_config_dir("mcross", False))
+CONFIG_FILE = CONFIG_DIR / "mcross.toml"
+
+_conf = None
+
+ConfDef = namedtuple("ConfDef", ["name", "short_name", "type", "default"])
+# argparse's add_argument() will ensure name/short_name uniqueness for free
+conf_definitions = [
+    ConfDef("text-font", "f", str, "Source Serif Pro"),
+    ConfDef("mono-font", "m", str, "Ubuntu Mono"),
+    ConfDef("background-color", "b", str, "#fff8dc"),
+    ConfDef("text-color", "t", str, "black"),
+    ConfDef("link-color", "l", str, "brown"),
+    ConfDef("list-item-color", "i", str, "#044604"),
+]
+
+
+def init():
+    default_conf = load_default_conf()
+    file_conf = load_conf_file()
+    cli_conf = parse_conf_args()
+
+    global _conf
+    _conf = {**default_conf, **file_conf, **cli_conf}
+    return _conf
+
+
+def load_default_conf():
+    return {confdef.name: confdef.default for confdef in conf_definitions}
+
+
+def load_conf_file():
+    if not CONFIG_DIR.is_dir():
+        os.mkdir(CONFIG_DIR)
+
+    if not CONFIG_FILE.is_file():
+        return {}
+
+    try:
+        data = toml.load(CONFIG_FILE)
+        return {
+            confdef.name: data[confdef.name]
+            for confdef in conf_definitions
+            if confdef.name in data
+        }
+    except Exception as e:
+        print("Unexpected error reading config file:", str(e))
+        return {}
+
+
+def parse_conf_args():
+    argparser = argparse.ArgumentParser()
+    for confdef in conf_definitions:
+        argparser.add_argument(
+            f"-{confdef.short_name}", f"--{confdef.name}", type=confdef.type,
+        )
+    args = argparser.parse_args()
+    return {key.replace("_", "-"): val for key, val in vars(args).items() if val}
+
+
+def get(key):
+    return _conf[key]
+
+
+if __name__ == "__main__":
+    init()
+    import pprint
+
+    print("Final conf:")
+    pprint.pprint(_conf)
diff --git a/src/mcross/gui/controller.py b/src/mcross/gui/controller.py
index de350c2..6397404 100644
--- a/src/mcross/gui/controller.py
+++ b/src/mcross/gui/controller.py
@@ -21,13 +21,11 @@
 
 
 class Controller:
-    def __init__(self, args):
+    def __init__(self):
         self.root = Tk()
         self.root.alt_shortcuts = set()
         self.model = Model()
-        self.view = View(
-            self.root, self.model, fonts=(args.textfont, args.monofont), dark=args.dark
-        )
+        self.view = View(self.root, self.model)
         self.root.title("McRoss Browser")
         self.root.geometry("800x600")
 
diff --git a/src/mcross/gui/view.py b/src/mcross/gui/view.py
index 06fdd1f..bd0fa34 100644
--- a/src/mcross/gui/view.py
+++ b/src/mcross/gui/view.py
@@ -2,6 +2,7 @@
 import sys
 from tkinter import Text, Tk, font, ttk
 
+from .. import conf
 from ..document import (
     GeminiNode,
     H1Node,
@@ -71,7 +72,7 @@ class View:
     back_callback = None
     forward_callback = None
 
-    def __init__(self, root: Tk, model: Model, fonts=(None, None), dark=False):
+    def __init__(self, root: Tk, model: Model):
         self.model = model
 
         # first row - address bar + buttons
@@ -142,31 +143,28 @@ def on_ctrl_l(ev):
         text = ReadOnlyText(row2, wrap="word")
         self.text = text
         self.render_page()
-        if fonts[0] is None:
-            text_font = pick_font(
-                [
-                    "Charis SIL",
-                    "Source Serif Pro",
-                    "Cambria",
-                    "Georgia",
-                    "DejaVu Serif",
-                    "Times New Roman",
-                    "Times",
-                    "TkTextFont",
-                ]
-            )
-        else:
-            text_font = fonts[0]
+        text_font = pick_font(
+            [
+                conf.get("text-font"),
+                "Charis SIL",
+                "Source Serif Pro",
+                "Cambria",
+                "Georgia",
+                "DejaVu Serif",
+                "Times New Roman",
+                "Times",
+                "TkTextFont",
+            ]
+        )
 
-        if fonts[1] is None:
-            mono_font = pick_font(["Ubuntu Mono", "Consolas", "Courier", "TkFixedFont"])
-        else:
-            mono_font = fonts[1]
+        mono_font = pick_font(
+            [conf.get("mono-font"), "Ubuntu Mono", "Consolas", "Courier", "TkFixedFont"]
+        )
 
         text.config(
             font=(text_font, 13),
-            bg="#212121" if dark else "#fff8dc",
-            fg="#eee" if dark else "black",
+            bg=conf.get("background-color"),
+            fg=conf.get("text-color"),
             padx=5,
             pady=5,
             # hide blinking insertion cursor:
@@ -176,13 +174,13 @@ def on_ctrl_l(ev):
             height=1,
         )
         text.pack(side="left", fill="both", expand=True)
-        text.tag_config("link", foreground="#ff8a65" if dark else "brown")
+        text.tag_config("link", foreground=conf.get("link-color"))
         text.tag_bind("link", "<Enter>", self._on_link_enter)
         text.tag_bind("link", "<Leave>", self._on_link_leave)
         text.tag_bind("link", "<Button-1>", self._on_link_click)
         text.tag_config("pre", font=(mono_font, 13))
         text.tag_config("plaintext", font=(mono_font, 13))
-        text.tag_config("listitem", foreground="#64c664" if dark else "#044604")
+        text.tag_config("listitem", foreground=conf.get("list-item-color"))
 
         base_heading_font = font.Font(font=text["font"])
         base_heading_font.config(weight="bold")