preferences: basic per-capsule preferences

For now only a per-path render mode is available.
This commit is contained in:
dece 2021-05-29 01:13:43 +02:00
parent f827ce3ee1
commit 2d493af64b
8 changed files with 171 additions and 6 deletions

View file

@ -17,9 +17,10 @@ from bebop.bookmarks import (
save_bookmark, save_bookmark,
) )
from bebop.colors import ColorPair, init_colors from bebop.colors import ColorPair, init_colors
from bebop.config import RENDER_MODES
from bebop.command_line import CommandLine from bebop.command_line import CommandLine
from bebop.external import open_external_program from bebop.external import open_external_program
from bebop.fs import get_identities_list_path from bebop.fs import get_capsule_prefs_path, get_identities_list_path
from bebop.help import get_help from bebop.help import get_help
from bebop.history import History from bebop.history import History
from bebop.identity import load_identities from bebop.identity import load_identities
@ -36,6 +37,7 @@ from bebop.navigation import (
) )
from bebop.page import Page from bebop.page import Page
from bebop.page_pad import PagePad from bebop.page_pad import PagePad
from bebop.preferences import load_capsule_prefs, save_capsule_prefs
from bebop.welcome import WELCOME_PAGE from bebop.welcome import WELCOME_PAGE
@ -153,6 +155,11 @@ class Browser:
failed_to_load.append("identities") failed_to_load.append("identities")
else: else:
self.identities = identities self.identities = identities
capsule_prefs = load_capsule_prefs(get_capsule_prefs_path())
if capsule_prefs is None:
failed_to_load.append("capsule preferences")
else:
self.capsule_prefs = capsule_prefs
if failed_to_load: if failed_to_load:
error_msg = ( error_msg = (
@ -324,6 +331,8 @@ class Browser:
elif command == "forget-certificate": elif command == "forget-certificate":
from bebop.browser.gemini import forget_certificate from bebop.browser.gemini import forget_certificate
forget_certificate(self, words[1]) forget_certificate(self, words[1])
elif command == "render":
self.set_render_mode(words[1])
def get_user_text_input(self, status_text, char, prefix="", strip=False): def get_user_text_input(self, status_text, char, prefix="", strip=False):
"""Get user input from the command-line.""" """Get user input from the command-line."""
@ -423,7 +432,7 @@ class Browser:
self.set_status_error(f"Protocol '{scheme}' not supported.") self.set_status_error(f"Protocol '{scheme}' not supported.")
def load_page(self, page: Page): def load_page(self, page: Page):
"""Load Gemtext data as the current page.""" """Set this page as the current page and refresh appropriate windows."""
old_pad_height = self.page_pad.dim[0] old_pad_height = self.page_pad.dim[0]
self.page_pad.show_page(page) self.page_pad.show_page(page)
if self.page_pad.dim[0] < old_pad_height: if self.page_pad.dim[0] < old_pad_height:
@ -676,3 +685,23 @@ class Browser:
size = f"{len(page.source)} chars" size = f"{len(page.source)} chars"
info = f"{mime} {encoding} {size}" info = f"{mime} {encoding} {size}"
self.set_status(info) self.set_status(info)
def set_render_mode(self, mode):
"""Set the render mode for the current path or capsule."""
if mode not in RENDER_MODES:
valid_modes = ", ".join(RENDER_MODES)
self.set_status_error("Valid render modes are: " + valid_modes)
return
url = self.get_user_text_input(
f"Set '{mode}' render mode for which URL (includes children)?",
CommandLine.CHAR_TEXT,
prefix=self.current_url,
strip=True
)
if not url:
return
prefs = self.capsule_prefs.get(url, {})
prefs["render_mode"] = mode
self.capsule_prefs[url] = prefs
save_capsule_prefs(self.capsule_prefs, get_capsule_prefs_path())
self.reload_page()

View file

@ -13,6 +13,7 @@ from bebop.identity import (
) )
from bebop.navigation import set_parameter from bebop.navigation import set_parameter
from bebop.page import Page from bebop.page import Page
from bebop.preferences import get_url_render_mode_pref
from bebop.protocol import Request, Response from bebop.protocol import Request, Response
from bebop.tofu import trust_fingerprint, untrust_fingerprint, WRONG_FP_ALERT from bebop.tofu import trust_fingerprint, untrust_fingerprint, WRONG_FP_ALERT
@ -213,7 +214,13 @@ def _handle_successful_response(browser: Browser, response: Response, url: str):
except LookupError: except LookupError:
error = f"Unknown encoding {encoding}." error = f"Unknown encoding {encoding}."
else: else:
page = Page.from_gemtext(text, browser.config["text_width"]) text_width = browser.config["text_width"]
render_mode = get_url_render_mode_pref(
browser.capsule_prefs,
url,
browser.config["render_mode"]
)
page = Page.from_gemtext(text, text_width, render=render_mode)
else: else:
encoding = "utf-8" encoding = "utf-8"
text = response.content.decode(encoding, errors="replace") text = response.content.decode(encoding, errors="replace")

View file

@ -15,8 +15,11 @@ DEFAULT_CONFIG = {
"external_commands": {}, "external_commands": {},
"external_command_default": ["xdg-open"], "external_command_default": ["xdg-open"],
"home": "bebop:welcome", "home": "bebop:welcome",
"render_mode": "fancy",
} }
RENDER_MODES = ("fancy", "dumb")
def load_config(config_path): def load_config(config_path):
if not os.path.isfile(config_path): if not os.path.isfile(config_path):

View file

@ -59,6 +59,12 @@ def get_identities_path():
return get_user_data_path() / "identities" return get_user_data_path() / "identities"
@lru_cache(None)
def get_capsule_prefs_path():
"""Return the directory where identities are stored."""
return get_user_data_path() / "capsule_prefs.json"
def ensure_bebop_files_exist() -> Optional[str]: def ensure_bebop_files_exist() -> Optional[str]:
"""Ensure various Bebop's files or directories are present. """Ensure various Bebop's files or directories are present.
@ -78,5 +84,10 @@ def ensure_bebop_files_exist() -> Optional[str]:
identities_path = get_identities_path() identities_path = get_identities_path()
if not identities_path.exists(): if not identities_path.exists():
identities_path.mkdir(parents=True) identities_path.mkdir(parents=True)
# Ensure the capsule preferences file exists.
capsule_prefs_path = get_capsule_prefs_path()
if not capsule_prefs_path.exists():
with open(capsule_prefs_path, "wt") as prefs_file:
prefs_file.write("{}")
except OSError as exc: except OSError as exc:
return str(exc) return str(exc)

View file

@ -65,6 +65,7 @@ Here are the available options:
* external_commands (see note 2): commands to open various files. * external_commands (see note 2): commands to open various files.
* external_command_default (see note 1): default command to open files. * external_command_default (see note 1): default command to open files.
* home (string): home page. * home (string): home page.
* render_mode (string): default render mode to use ("fancy" or "dumb").
Notes: Notes:

View file

@ -28,10 +28,11 @@ class Page:
encoding: str = "" encoding: str = ""
@staticmethod @staticmethod
def from_gemtext(gemtext: str, wrap_at: int, dumb: bool =False): def from_gemtext(gemtext: str, wrap_at: int, render: str ="fancy"):
"""Produce a Page from a Gemtext file or string.""" """Produce a Page from a Gemtext file or string."""
elements, links, title = parse_gemtext(gemtext, dumb=dumb) dumb_mode = render == "dumb"
metalines = generate_metalines(elements, wrap_at, dumb=dumb) elements, links, title = parse_gemtext(gemtext, dumb=dumb_mode)
metalines = generate_metalines(elements, wrap_at, dumb=dumb_mode)
return Page(gemtext, metalines, links, title) return Page(gemtext, metalines, links, title)
@staticmethod @staticmethod

59
bebop/preferences.py Normal file
View file

@ -0,0 +1,59 @@
"""Per-capsule preferences.
Currently contains only overrides for render modes, per URL path. In the future
it may be interesting to move a few things from the config to here, such as the
text width.
This is a map from an URL to dicts. The only used key is render_mode.
"""
import json
import logging
from pathlib import Path
from typing import Optional
def load_capsule_prefs(prefs_path: Path) -> Optional[dict]:
"""Return saved capsule preferences or None on error."""
prefs = {}
try:
with open(prefs_path, "rt") as prefs_file:
prefs = json.load(prefs_file)
except (OSError, ValueError) as exc:
logging.error(f"Failed to load capsule prefs '{prefs_path}': {exc}")
return None
return prefs
def save_capsule_prefs(prefs: dict, prefs_path: Path):
"""Save the capsule preferences. Return True on success."""
try:
with open(prefs_path, "wt") as prefs_file:
json.dump(prefs, prefs_file, indent=2)
except (OSError, ValueError) as exc:
logging.error(f"Failed to save capsule prefs '{prefs_path}': {exc}")
return False
return True
def get_url_render_mode_pref(prefs: dict, url: str, default: str):
"""Return the desired render mode for this URL.
If the preferences contain the URL or a parent URL, the corresponding render
mode is used. If several URLs are prefixes of the `url` argument, the
longest one is used to get the matching preference.
Arguments:
- prefs: current capsule preferences.
- url: URL about to be rendered.
- default: default render mode if no user preferences match.
"""
prefix_urls = []
for key in prefs:
if url.startswith(key):
prefix_urls.append(key)
if not prefix_urls:
return default
key = max(prefix_urls, key=len)
preference = prefs[key]
return preference.get("render_mode", default)

View file

@ -0,0 +1,54 @@
import unittest
from ..preferences import get_url_render_mode_pref
class TestPreferences(unittest.TestCase):
def test_get_url_render_mode_pref(self):
prefs = {}
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com",
"default"
), "default")
prefs["gemini://example.com"] = {}
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com",
"default"
), "default")
prefs["gemini://example.com"] = {"render_mode": "test"}
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com",
"default"
), "test")
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com/path",
"default"
), "test")
prefs["gemini://example.com/specific/subdir"] = {"render_mode": "test2"}
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com/path",
"default"
), "test")
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com/specific",
"default"
), "test")
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com/specific/subdir",
"default"
), "test2")
self.assertEqual(get_url_render_mode_pref(
prefs,
"gemini://example.com/specific/subdir/subsubdir",
"default"
), "test2")