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:
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
|
||||
|
|
80
README.md
80
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.
|
||||
|
|
|
@ -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):
|
||||
|
|
41
bebop/config.py
Normal file
41
bebop/config.py
Normal 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}")
|
|
@ -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
|
||||
|
|
Reference in a new issue