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/
exec
dece 3 years ago
parent 4fbfa37937
commit 3a818812a9

@ -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

@ -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.

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

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

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

@ -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):

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

@ -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."""

@ -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

@ -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

Loading…
Cancel
Save