plugins: add basic scheme plugin support

This commit is contained in:
dece 2021-06-04 16:09:00 +02:00
parent af349f5ac2
commit b884aed3a8
4 changed files with 75 additions and 4 deletions

View file

@ -7,6 +7,7 @@ import logging
import os import os
import subprocess import subprocess
import tempfile import tempfile
from importlib import import_module
from math import inf from math import inf
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple from typing import Optional, Tuple
@ -86,6 +87,7 @@ class Browser:
self.last_download: Optional[Tuple[MimeType, Path]] = None self.last_download: Optional[Tuple[MimeType, Path]] = None
self.identities = {} self.identities = {}
self.search_res_lines = [] self.search_res_lines = []
self.plugins = []
self._current_url = "" self._current_url = ""
@property @property
@ -176,6 +178,12 @@ class Browser:
if not self.history.load(): if not self.history.load():
logging.warning("Could not load history file.") logging.warning("Could not load history file.")
# Load plugins.
self.load_plugins()
# If there has been any issue to load user files, show them instead of
# automatically moving forward. Else either open the URL requested or
# show the home page.
if failed_to_load: if failed_to_load:
error_msg = ( error_msg = (
f"Failed to open some local data: {', '.join(failed_to_load)}. " f"Failed to open some local data: {', '.join(failed_to_load)}. "
@ -455,7 +463,15 @@ class Browser:
self.set_status_error("Unknown page.") self.set_status_error("Unknown page.")
else: else:
self.set_status_error(f"Protocol '{scheme}' not supported.") from bebop.plugins import SchemePlugin
plugins = (p for p in self.plugins if isinstance(p, SchemePlugin))
plugin = next(filter(lambda p: p.scheme == scheme, plugins), None)
if plugin:
result_url = plugin.open_url(self, url)
if history and result_url:
self.history.push(result_url)
else:
self.set_status_error(f"Protocol '{scheme}' not supported.")
def load_page(self, page: Page): def load_page(self, page: Page):
"""Set this page as the current page and refresh appropriate windows.""" """Set this page as the current page and refresh appropriate windows."""
@ -828,3 +844,22 @@ class Browser:
self.set_status(f"Result {index}/{max_index}") self.set_status(f"Result {index}/{max_index}")
self.page_pad.current_line = next_line self.page_pad.current_line = next_line
self.refresh_windows() self.refresh_windows()
def load_plugins(self):
"""Load installed and configured plugins."""
for plugin_name in self.config["enabled_plugins"]:
module_name = f"bebop_{plugin_name}"
try:
module = import_module(module_name)
except ImportError as exc:
logging.error(f"Could not load module {module_name}: {exc}")
continue
try:
self.plugins.append(module.plugin) # type: ignore
except AttributeError:
logging.error(f"Module {module_name} does not export a plugin.")
continue
logging.info(f"Loaded plugin {plugin_name}.")

View file

@ -30,6 +30,7 @@ DEFAULT_CONFIG = {
], ],
"scroll_step": 3, "scroll_step": 3,
"persistent_history": False, "persistent_history": False,
"enabled_plugins": [],
} }
RENDER_MODES = ("fancy", "dumb") RENDER_MODES = ("fancy", "dumb")

View file

@ -64,6 +64,7 @@ Here are the available options:
* command_editor (see note 1): command to use for editing cli input. * command_editor (see note 1): command to use for editing cli input.
* connect_timeout (int): seconds before connection times out. * connect_timeout (int): seconds before connection times out.
* download_path (string): download path. * download_path (string): download path.
* enabled_plugins: (see note 4): plugin names to load.
* external_command_default (see note 1): default command to open files. * external_command_default (see note 1): default command to open files.
* external_commands (see note 2): commands to open various files. * external_commands (see note 2): commands to open various files.
* generate_client_cert_command (see note 3): command to generate a client cert. * generate_client_cert_command (see note 3): command to generate a client cert.
@ -77,11 +78,13 @@ Here are the available options:
Notes: Notes:
1: 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. 1: 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.
2: the external_commands dict maps MIME types to commands just as above. For example, if you want to open video files with VLC and audio files in Clementine, you can use the following dict: `{"audio": ["clementine"], "video": ["vlc"]}`. For now only "main" MIME types are supported, i.e. you cannot specify precise types like "audio/flac", just "audio". 2: The external_commands dict maps MIME types to commands just as above. For example, if you want to open video files with VLC and audio files in Clementine, you can use the following dict: `{"audio": ["clementine"], "video": ["vlc"]}`. For now only "main" MIME types are supported, i.e. you cannot specify precise types like "audio/flac", just "audio".
3: the generate_client_cert_command uses the same format as other commands (specified in note 1 above), with the exception that if the strings "{cert_path}", "{key_path}" or "{common_name}" are present in any string for the list, they will be replaced respectively by the certificate output path, the key output path and the CN to use. 3: The generate_client_cert_command uses the same format as other commands (specified in note 1 above), with the exception that if the strings "{cert_path}", "{key_path}" or "{common_name}" are present in any string for the list, they will be replaced respectively by the certificate output path, the key output path and the CN to use.
4: The enabled_plugins list contain plugin names to load. Plugins are available if they are installed Python packages that can be imported using the `bebop_<plugin-name>` package name.
Your current configuration is: Your current configuration is:

32
bebop/plugins.py Normal file
View file

@ -0,0 +1,32 @@
"""Plugin management.
Plugins are here to allow extending Bebop with additional features, potentially
requiring external libraries, without requiring users who just want a Gemini
browser to install anything.
Support for plugins is very simple right now: a plugin can only register an URL
scheme to handle.
"""
from abc import ABC, abstractmethod
from typing import Optional
from bebop.browser.browser import Browser
class SchemePlugin(ABC):
"""Plugin for URL scheme management."""
def __init__(self, scheme: str) -> None:
self.scheme = scheme
@abstractmethod
def open_url(self, browser: Browser, url: str) -> Optional[str]:
"""Handle an URL for this scheme.
Returns:
The properly handled URL at the end of this query, which may be
different from the url parameter if redirections happened, or None if an
error happened.
"""
raise NotImplementedError