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/
This commit is contained in:
dece 2021-04-18 02:27:05 +02:00
parent 4fbfa37937
commit 3a818812a9
10 changed files with 149 additions and 25 deletions

View file

@ -11,18 +11,22 @@ TODO DONE
bookmarks bookmarks
view/edit sources view/edit sources
downloads downloads
configuration
open last download open last download
non shit command-line actual TOFU
home page home page
media files media files
view history view history
identity management identity management
configuration help page for keybinds
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
BACKLOG BACKLOG
click on links to open them
download to disk, not in memory download to disk, not in memory
does encoding really work? cf. egsam
margins / centering margins / centering
pre blocks folding pre blocks folding
buffers (tabs) buffers (tabs)
handle soft-hyphens on wrapping handle soft-hyphens on wrapping
bug: combining chars reduce lengths bug: combining chars reduce lengths
non shit command-line

View file

@ -25,21 +25,85 @@ It passes the Conman's client test but not Egsam's for now.
Features Features
-------- --------
### What works Why use Bebop instead of something else?
Common basic browsing features work: go to URL, scrolling, follow links, ### Lightweight
redirections, page encoding.
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 - History
- Caching - Caching
- Bookmarks: it's just a text file with bindings. - Bookmarks: it's just a text file with bindings.
- Downloads
Check out [this board](BOARD.txt) for what's done and coming next. 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 Configuration
aggregator at the moment. -------------
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.

View file

@ -1,7 +1,8 @@
import argparse import argparse
from bebop.browser.browser import Browser 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 from bebop.tofu import load_cert_stash, save_cert_stash
@ -15,6 +16,9 @@ def main():
else: else:
start_url = None start_url = None
config_path = get_config_path()
config = load_config(config_path)
user_data_path = get_user_data_path() user_data_path = get_user_data_path()
if not user_data_path.exists(): if not user_data_path.exists():
user_data_path.mkdir() user_data_path.mkdir()
@ -22,7 +26,7 @@ def main():
cert_stash_path = user_data_path / "known_hosts.txt" cert_stash_path = user_data_path / "known_hosts.txt"
cert_stash = load_cert_stash(cert_stash_path) or {} cert_stash = load_cert_stash(cert_stash_path) or {}
try: try:
Browser(cert_stash).run(start_url=start_url) Browser(config, cert_stash).run(start_url=start_url)
finally: finally:
save_cert_stash(cert_stash, cert_stash_path) save_cert_stash(cert_stash, cert_stash_path)

View file

@ -25,7 +25,8 @@ from bebop.page_pad import PagePad
class Browser: class Browser:
"""Manage the events, inputs and rendering.""" """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.stash = cert_stash or {}
self.screen = None self.screen = None
self.dim = (0, 0) self.dim = (0, 0)
@ -82,7 +83,10 @@ class Browser:
*self.line_dim, *self.line_dim,
*self.command_line_pos, *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: if start_url:
self.open_url(start_url, assume_absolute=True) self.open_url(start_url, assume_absolute=True)
@ -429,7 +433,7 @@ class Browser:
if content is None: if content is None:
self.set_status_error("Failed to open bookmarks.") self.set_status_error("Failed to open bookmarks.")
return return
self.load_page(Page.from_gemtext(content)) self.load_page(Page.from_gemtext(content, self.config["text_width"]))
self.current_url = "bebop://bookmarks" self.current_url = "bebop://bookmarks"
def add_bookmark(self): def add_bookmark(self):
@ -453,9 +457,7 @@ class Browser:
needs it, if needed. Internal pages, e.g. the bookmarks page, are loaded needs it, if needed. Internal pages, e.g. the bookmarks page, are loaded
directly from their location on disk. directly from their location on disk.
""" """
command = ["vi"]
delete_source_after = False delete_source_after = False
special_pages = { special_pages = {
"bebop://bookmarks": str(get_bookmarks_path()) "bebop://bookmarks": str(get_bookmarks_path())
} }
@ -470,7 +472,7 @@ class Browser:
source_filename = source_file.name source_filename = source_file.name
delete_source_after = True delete_source_after = True
command.append(source_filename) command = self.config["source_editor"] + [source_filename]
open_external_program(command) open_external_program(command)
if delete_source_after: if delete_source_after:
os.unlink(source_filename) os.unlink(source_filename)

View file

@ -33,7 +33,8 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
return return
req = Request(url, browser.stash) req = Request(url, browser.stash)
connected = req.connect() connect_timeout = browser.config["connect_timeout"]
connected = req.connect(connect_timeout)
if not connected: if not connected:
if req.state == Request.STATE_ERROR_CERT: if req.state == Request.STATE_ERROR_CERT:
error = f"Certificate was missing or corrupt ({url})." error = f"Certificate was missing or corrupt ({url})."
@ -110,7 +111,7 @@ def handle_response_content(browser: Browser, url: str, response: Response,
except LookupError: except LookupError:
error = f"Unknown encoding {encoding}." error = f"Unknown encoding {encoding}."
else: else:
page = Page.from_gemtext(text) page = Page.from_gemtext(text, browser.config["text_width"])
else: else:
text = response.content.decode("utf-8", errors="replace") text = response.content.decode("utf-8", errors="replace")
page = Page.from_text(text) page = Page.from_text(text)

View file

@ -19,8 +19,9 @@ class CommandLine:
its content as result. its content as result.
""" """
def __init__(self, window): def __init__(self, window, editor_command):
self.window = window self.window = window
self.editor_command = editor_command
self.textbox = None self.textbox = None
def clear(self): def clear(self):

41
bebop/config.py Normal file
View file

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

View file

@ -13,6 +13,13 @@ from pathlib import Path
APP_NAME = "bebop" 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) @lru_cache(None)
def get_user_data_path() -> Path: def get_user_data_path() -> Path:
"""Return the user data directory path.""" """Return the user data directory path."""

View file

@ -24,10 +24,10 @@ class Page:
title: str = "" title: str = ""
@staticmethod @staticmethod
def from_gemtext(gemtext: str): def from_gemtext(gemtext: str, wrap_at: int):
"""Produce a Page from a Gemtext file or string.""" """Produce a Page from a Gemtext file or string."""
elements, links, title = parse_gemtext(gemtext) elements, links, title = parse_gemtext(gemtext)
metalines = generate_metalines(elements, 80) metalines = generate_metalines(elements, wrap_at)
return Page(gemtext, metalines, links, title) return Page(gemtext, metalines, links, title)
@staticmethod @staticmethod

View file

@ -61,7 +61,7 @@ class Request:
self.cert_status = None self.cert_status = None
self.error = "" self.error = ""
def connect(self): def connect(self, timeout):
"""Connect to a Gemini server and return a RequestEventType. """Connect to a Gemini server and return a RequestEventType.
Return True if the connection is established. The caller has to verify Return True if the connection is established. The caller has to verify
@ -95,7 +95,7 @@ class Request:
self.payload += LINE_TERM self.payload += LINE_TERM
try: try:
sock = socket.create_connection((hostname, port), timeout=10) sock = socket.create_connection((hostname, port), timeout=timeout)
except OSError as exc: except OSError as exc:
self.state = Request.STATE_CONNECTION_FAILED self.state = Request.STATE_CONNECTION_FAILED
self.error = exc.strerror self.error = exc.strerror