From 3a818812a94f5b4541b059e9116098eaed2f825a Mon Sep 17 00:00:00 2001 From: dece Date: Sun, 18 Apr 2021 02:27:05 +0200 Subject: [PATCH] config: add config management A config file is now created and used, and a few hardcoded things in the source have been moved to the config file \o/ --- BOARD.txt | 8 +++- README.md | 80 ++++++++++++++++++++++++++++++++++++---- bebop/__main__.py | 8 +++- bebop/browser/browser.py | 14 ++++--- bebop/browser/gemini.py | 5 ++- bebop/command_line.py | 3 +- bebop/config.py | 41 ++++++++++++++++++++ bebop/fs.py | 7 ++++ bebop/page.py | 4 +- bebop/protocol.py | 4 +- 10 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 bebop/config.py diff --git a/BOARD.txt b/BOARD.txt index 7e4d9fe..ee9f168 100644 --- a/BOARD.txt +++ b/BOARD.txt @@ -11,18 +11,22 @@ TODO DONE bookmarks view/edit sources downloads + configuration open last download -non shit command-line +actual TOFU home page media files view history identity management -configuration +help page for keybinds -------------------------------------------------------------------------------- BACKLOG +click on links to open them download to disk, not in memory +does encoding really work? cf. egsam margins / centering pre blocks folding buffers (tabs) handle soft-hyphens on wrapping bug: combining chars reduce lengths +non shit command-line diff --git a/README.md b/README.md index 7db6c3a..2e3ea75 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,85 @@ It passes the Conman's client test but not Egsam's for now. Features -------- -### What works +Why use Bebop instead of something else? -Common basic browsing features work: go to URL, scrolling, follow links, -redirections, page encoding. +### Lightweight -Bebop also provide these neat features: +It only uses a single dependency, [asn1crypto][asn1crypto], to delegate +parsing certificates. Everything else including NCurses or TLS is done using +Python's standard library. + +[asn1crypto]: https://github.com/wbond/asn1crypto + +### Fun + +Link navigation is done by entering the link ID with automatic validation: if +there are less than 10 links on a page, pressing the link ID will take you to +the page directly. If there are 30 links, pressing "1" will wait for another +digit. If there are 1000 links but you wish to visit link #5, pressing 5 and +enter will do! + +Of course this is based on my own perception of what exactly makes a client +"fun" to use, but give it a shot! + +### And more! + +It does not try to do many things. Common basic browsing features work: go to +URL, scrolling, follow links, redirections, page encodings, etc. + +It also provide these features: - History - Caching - Bookmarks: it's just a text file with bindings. +- Downloads Check out [this board](BOARD.txt) for what's done and coming next. -### What is not planned for now -- 256-colors mode and themes. -- Subscriptions. I have no need for them as I prefer to use a browser-agnostic - aggregator at the moment. + +Configuration +------------- + +Bebop uses a JSON file (usually in `~/.config`). It is created with default +values on first start. It is never written to afterwards: you can edit it when +you want, just restart Bebop to take changes into account. + +Here are the available options: + +| Key | Type | Default | Description | +|-------------------|-------------|----------|---------------------------------------| +| `connect_timeout` | int | 10 | Seconds before connection times out. | +| `text_width` | int | 80 | Rendered line length. | +| `source_editor` | string list | `["vi"]` | Command to use for editing sources. | +| `command_editor` | string list | `["vi"]` | Command to use for editing CLI input. | + +Note: for the "command" parameters such as `source_editor` and `command_editor`, +a string list is used to separate the different program arguments, e.g. if you +wish to use `vim -c 'startinsert'`, you should write the list `["vim", "-c", +"startinsert"]`. In both case, a temporary or regular file name will be appended +to this command when run. + + + +FAQ +--- + +### Can I change the colors? + +I do not plan to allow modifying the colors or elements style for now. Configure +a nice palette for your terminal so that Bebop fits nicely in there! + +### Will Bebop implement subscriptions? + +I have no need for them as I prefer to use a browser-agnostic aggregator at the +moment, so no. + +### WTF is wrong with the command line? + +I don't understand how you're supposed to create a fine input field in pure +curses without the form extension library, which is not available from Python. +Or, I think I understand but it's way too hard for the limited time I have to +work on Bebop. So the command line is based on the very limited Textbox class; +it's fine for entering a simple URL or a bookmark title but if you need to type +more than the window's width, press `M-e` (ALT + e) to open an editor. diff --git a/bebop/__main__.py b/bebop/__main__.py index 1ffd547..c69d392 100644 --- a/bebop/__main__.py +++ b/bebop/__main__.py @@ -1,7 +1,8 @@ import argparse from bebop.browser.browser import Browser -from bebop.fs import get_user_data_path +from bebop.config import load_config +from bebop.fs import get_config_path, get_user_data_path from bebop.tofu import load_cert_stash, save_cert_stash @@ -15,6 +16,9 @@ def main(): else: start_url = None + config_path = get_config_path() + config = load_config(config_path) + user_data_path = get_user_data_path() if not user_data_path.exists(): user_data_path.mkdir() @@ -22,7 +26,7 @@ def main(): cert_stash_path = user_data_path / "known_hosts.txt" cert_stash = load_cert_stash(cert_stash_path) or {} try: - Browser(cert_stash).run(start_url=start_url) + Browser(config, cert_stash).run(start_url=start_url) finally: save_cert_stash(cert_stash, cert_stash_path) diff --git a/bebop/browser/browser.py b/bebop/browser/browser.py index fcdee1f..54b9133 100644 --- a/bebop/browser/browser.py +++ b/bebop/browser/browser.py @@ -25,7 +25,8 @@ from bebop.page_pad import PagePad class Browser: """Manage the events, inputs and rendering.""" - def __init__(self, cert_stash): + def __init__(self, config, cert_stash): + self.config = config self.stash = cert_stash or {} self.screen = None self.dim = (0, 0) @@ -82,7 +83,10 @@ class Browser: *self.line_dim, *self.command_line_pos, ) - self.command_line = CommandLine(command_line_window) + self.command_line = CommandLine( + command_line_window, + self.config["command_editor"] + ) if start_url: self.open_url(start_url, assume_absolute=True) @@ -429,7 +433,7 @@ class Browser: if content is None: self.set_status_error("Failed to open bookmarks.") return - self.load_page(Page.from_gemtext(content)) + self.load_page(Page.from_gemtext(content, self.config["text_width"])) self.current_url = "bebop://bookmarks" def add_bookmark(self): @@ -453,9 +457,7 @@ class Browser: needs it, if needed. Internal pages, e.g. the bookmarks page, are loaded directly from their location on disk. """ - command = ["vi"] delete_source_after = False - special_pages = { "bebop://bookmarks": str(get_bookmarks_path()) } @@ -470,7 +472,7 @@ class Browser: source_filename = source_file.name delete_source_after = True - command.append(source_filename) + command = self.config["source_editor"] + [source_filename] open_external_program(command) if delete_source_after: os.unlink(source_filename) diff --git a/bebop/browser/gemini.py b/bebop/browser/gemini.py index 772b452..a545960 100644 --- a/bebop/browser/gemini.py +++ b/bebop/browser/gemini.py @@ -33,7 +33,8 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True, return req = Request(url, browser.stash) - connected = req.connect() + connect_timeout = browser.config["connect_timeout"] + connected = req.connect(connect_timeout) if not connected: if req.state == Request.STATE_ERROR_CERT: error = f"Certificate was missing or corrupt ({url})." @@ -110,7 +111,7 @@ def handle_response_content(browser: Browser, url: str, response: Response, except LookupError: error = f"Unknown encoding {encoding}." else: - page = Page.from_gemtext(text) + page = Page.from_gemtext(text, browser.config["text_width"]) else: text = response.content.decode("utf-8", errors="replace") page = Page.from_text(text) diff --git a/bebop/command_line.py b/bebop/command_line.py index 73dd1cc..79ec5c1 100644 --- a/bebop/command_line.py +++ b/bebop/command_line.py @@ -19,8 +19,9 @@ class CommandLine: its content as result. """ - def __init__(self, window): + def __init__(self, window, editor_command): self.window = window + self.editor_command = editor_command self.textbox = None def clear(self): diff --git a/bebop/config.py b/bebop/config.py new file mode 100644 index 0000000..8c04cdb --- /dev/null +++ b/bebop/config.py @@ -0,0 +1,41 @@ +"""Config management.""" + +import json +import os.path + + +DEFAULT_CONFIG = { + "connect_timeout": 10, + "text_width": 80, + "source_editor": ["vi"], + "command_editor": ["vi"], +} + + +def load_config(config_path): + if not os.path.isfile(config_path): + create_default_config(config_path) + return DEFAULT_CONFIG + + try: + with open(config_path, "rt") as config_file: + config = json.load(config_file) + except OSError as exc: + print(f"Could not read config file {config_path}: {exc}") + except ValueError as exc: + print(f"Could not parse config file {config_path}: {exc}") + else: + # Fill missing values with defaults. + for key, value in DEFAULT_CONFIG.items(): + if key not in config: + config[key] = value + return config + return DEFAULT_CONFIG + + +def create_default_config(config_path): + try: + with open(config_path, "wt") as config_file: + json.dump(DEFAULT_CONFIG, config_file, indent=2) + except OSError as exc: + print(f"Could not create config file {config_path}: {exc}") diff --git a/bebop/fs.py b/bebop/fs.py index 87ac441..8ee01ad 100644 --- a/bebop/fs.py +++ b/bebop/fs.py @@ -13,6 +13,13 @@ from pathlib import Path APP_NAME = "bebop" +@lru_cache(None) +def get_config_path() -> Path: + """Return the user config file path.""" + config_dir = Path(getenv("XDG_CONFIG_HOME", expanduser("~/.config"))) + return config_dir / (APP_NAME + ".json") + + @lru_cache(None) def get_user_data_path() -> Path: """Return the user data directory path.""" diff --git a/bebop/page.py b/bebop/page.py index a1c62cb..e6200a5 100644 --- a/bebop/page.py +++ b/bebop/page.py @@ -24,10 +24,10 @@ class Page: title: str = "" @staticmethod - def from_gemtext(gemtext: str): + def from_gemtext(gemtext: str, wrap_at: int): """Produce a Page from a Gemtext file or string.""" elements, links, title = parse_gemtext(gemtext) - metalines = generate_metalines(elements, 80) + metalines = generate_metalines(elements, wrap_at) return Page(gemtext, metalines, links, title) @staticmethod diff --git a/bebop/protocol.py b/bebop/protocol.py index 47f2503..faeaff6 100644 --- a/bebop/protocol.py +++ b/bebop/protocol.py @@ -61,7 +61,7 @@ class Request: self.cert_status = None self.error = "" - def connect(self): + def connect(self, timeout): """Connect to a Gemini server and return a RequestEventType. Return True if the connection is established. The caller has to verify @@ -95,7 +95,7 @@ class Request: self.payload += LINE_TERM try: - sock = socket.create_connection((hostname, port), timeout=10) + sock = socket.create_connection((hostname, port), timeout=timeout) except OSError as exc: self.state = Request.STATE_CONNECTION_FAILED self.error = exc.strerror