preferences: basic per-capsule preferences
For now only a per-path render mode is available.
This commit is contained in:
parent
f827ce3ee1
commit
2d493af64b
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
11
bebop/fs.py
11
bebop/fs.py
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
59
bebop/preferences.py
Normal 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)
|
54
bebop/tests/test_preferences.py
Normal file
54
bebop/tests/test_preferences.py
Normal 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")
|
Reference in a new issue