~nhanb/mcross

b74362c954ba21b55f34597cd2682f1c1918b38e — Bùi Thành Nhân 5 months ago c55e6f4 config 0.5.17
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...
M README.md => README.md +19 -3
@@ 31,7 31,22 @@ Maybe it's finally time to try nuitka?

# 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 @@ necessarily agree with its "plaintext or nothing" stance.
- [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)


M poetry.lock => poetry.lock +25 -1
@@ 1,4 1,12 @@
[[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"
marker = "python_version >= \"3.4\" and sys_platform == \"darwin\""


@@ 261,6 269,14 @@ 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"
marker = "python_version >= \"3.4\""


@@ 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"},

M pyproject.toml => pyproject.toml +4 -1
@@ 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"

M src/mcross/__init__.py => src/mcross/__init__.py +15 -12
@@ 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)

A src/mcross/conf.py => src/mcross/conf.py +78 -0
@@ 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)

M src/mcross/gui/controller.py => src/mcross/gui/controller.py +2 -4
@@ 21,13 21,11 @@ statusbar_logger = logging.getLogger("statusbar")


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")


M src/mcross/gui/view.py => src/mcross/gui/view.py +22 -24
@@ 2,6 2,7 @@ import logging
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 @@ class View:
        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 @@ class View:
            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")