Compare commits

...

3 commits

Author SHA1 Message Date
dece f687a80dba WIP 2021-11-27 11:36:11 +01:00
dece 1604456bc9 browser: add quit info in the C-c message 2021-09-20 08:17:47 +02:00
dece 2ccb056756 browser: add i keybind to show page info 2021-09-20 08:16:32 +02:00
6 changed files with 127 additions and 12 deletions

View file

@ -29,11 +29,13 @@ handle big files (e.g. gemini://tilde.team/~tomasino/irc/log.txt)
allow encoding overrides (useful for gopher i guess) allow encoding overrides (useful for gopher i guess)
config for web browser, default to webbrowser module config for web browser, default to webbrowser module
use pubkeys instead of the whole DER hash for TOFU use pubkeys instead of the whole DER hash for TOFU
reload after forgetting old cert
specify external commands interface (exec, run, pipes) specify external commands interface (exec, run, pipes)
table of contents table of contents
better default editor than vim better default editor than vim
search engine search engine
auto-open some media types auto-open some media types
use about scheme instead of bebop

View file

@ -19,7 +19,7 @@ from bebop.bookmarks import (
) )
from bebop.colors import A_ITALIC, ColorPair, init_colors from bebop.colors import A_ITALIC, ColorPair, init_colors
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, substitute_external_command
from bebop.fs import get_capsule_prefs_path, 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
@ -203,7 +203,7 @@ class Browser:
try: try:
self.handle_inputs() self.handle_inputs()
except KeyboardInterrupt: except KeyboardInterrupt:
self.set_status("Cancelled.") self.set_status("Operation cancelled (to quit, type :q).")
if self.config["persistent_history"]: if self.config["persistent_history"]:
self.history.save() self.history.save()
@ -266,6 +266,10 @@ class Browser:
self.move_to_search_result(Browser.SEARCH_NEXT) self.move_to_search_result(Browser.SEARCH_NEXT)
elif char == ord("N"): elif char == ord("N"):
self.move_to_search_result(Browser.SEARCH_PREVIOUS) self.move_to_search_result(Browser.SEARCH_PREVIOUS)
elif char == ord("i"):
self.show_page_info()
elif char == ord("!"):
self.quick_command("exec")
elif curses.ascii.isdigit(char): elif curses.ascii.isdigit(char):
self.handle_digit_input(char) self.handle_digit_input(char)
elif char == curses.KEY_MOUSE: elif char == curses.KEY_MOUSE:
@ -382,6 +386,8 @@ class Browser:
else: else:
if command in ("o", "open"): if command in ("o", "open"):
self.open_url(words[1]) self.open_url(words[1])
elif command == "exec":
self.exec_external_command(" ".join(words[1:]))
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])
@ -786,7 +792,7 @@ class Browser:
encoding = page.encoding or "unk. encoding" encoding = page.encoding or "unk. encoding"
size = f"{len(page.source)} chars" size = f"{len(page.source)} chars"
lines = f"{len(page.metalines)} lines" lines = f"{len(page.metalines)} lines"
info = f"{mime} {encoding} {size} {lines}" info = f"{mime}; {encoding}; {size}; {lines}"
self.set_status(info) self.set_status(info)
def set_render_mode(self, mode): def set_render_mode(self, mode):
@ -887,3 +893,16 @@ class Browser:
continue continue
logging.info(f"Loaded plugin {plugin_name}.") logging.info(f"Loaded plugin {plugin_name}.")
def exec_external_command(self, command):
if not self.current_page:
return
command = substitute_external_command(
command, self.current_url, self.current_page)
self.set_status(f"Launching `{command}`…")
success = open_external_program(command)
if success:
self.reset_status()
else:
self.set_status_error("Command failed.")
self.refresh_windows()

View file

@ -166,7 +166,7 @@ def _handle_response(
redirects=redirects + 1 redirects=redirects + 1
) )
elif response.generic_code in (40, 50): elif response.generic_code in (40, 50):
error = f"Server error: {response.meta or Response.code.name}" error = f"Server error: {response.meta or response.code.name}"
browser.set_status_error(error) browser.set_status_error(error)
elif response.generic_code == 10: elif response.generic_code == 10:
return _handle_input_request(browser, url, response.meta) return _handle_input_request(browser, url, response.meta)

View file

@ -2,7 +2,24 @@
import curses import curses
import logging import logging
import re
import subprocess import subprocess
import tempfile
from bebop.page import Page
def _pre_exec():
curses.nocbreak()
curses.echo()
curses.curs_set(1)
def _post_exec():
curses.mousemask(curses.ALL_MOUSE_EVENTS)
curses.curs_set(0)
curses.noecho()
curses.cbreak()
def open_external_program(command): def open_external_program(command):
@ -14,17 +31,48 @@ def open_external_program(command):
Returns: Returns:
True if no exception occured. True if no exception occured.
""" """
curses.nocbreak() _pre_exec()
curses.echo()
curses.curs_set(1)
result = True result = True
try: try:
subprocess.run(command) subprocess.run(command)
except OSError as exc: except OSError as exc:
logging.error(f"Failed to run '{command}': {exc}") logging.error(f"Failed to run '{command}': {exc}")
result = False result = False
curses.mousemask(curses.ALL_MOUSE_EVENTS) _post_exec()
curses.curs_set(0)
curses.noecho()
curses.cbreak()
return result return result
SUB_URL_RE = re.compile(r"(?<!\$)\$u")
SUB_SRC_RE = re.compile(r"(?<!\$)\$s")
SUB_LINK_RE = re.compile(r"(?<!\$)\$(\d+)")
SUB_LITERAL_RE = re.compile(r"\$\$")
def substitute_external_command(command: str, url: str, page: Page):
"""Substitute "$" parts of the command with corresponding values.
Valid substitutions are:
- $u = current url
- $n (with n any positive number) = link url
- $s = current page source temp file
- $$ = $
Returns:
The command with all the template parts replaced with the corresponding
strings.
Raises:
ValueError if a substitution is wrong, e.g. a link ID which does not exist.
"""
# URL substitution.
command = SUB_URL_RE.sub(url, command)
# Source file substitution.
if SUB_SRC_RE.search(command):
with tempfile.NamedTemporaryFile("wt", delete=False) as source_file:
source_file.write(page.source)
command = SUB_SRC_RE.sub(source_file.name, command)
# Link ID substitution.
command = SUB_LINK_RE.sub(lambda m: page.links[int(m.group(1))], command)
# Literal dollar sign.
command = SUB_LITERAL_RE.sub("$", command)
return command

View file

@ -102,6 +102,7 @@ def parse_gemtext(text: str, dumb=False) -> ParsedGemtext:
match = Blockquote.RE.match(line) match = Blockquote.RE.match(line)
if match: if match:
text = match.groups()[0] text = match.groups()[0]
if text or dumb:
elements.append(Blockquote(text)) elements.append(Blockquote(text))
continue continue

View file

@ -0,0 +1,45 @@
from bebop.metalines import RenderOptions
import unittest
from ..external import substitute_external_command
from ..page import Page
URL = "gemini://example.com"
GEMTEXT = """\
# Test page
Blablabla
=> gemini://example.com/index.gmi Link to index
=> gemini://example.com/sub/gemlog.gmi Link to gemlog
"""
PAGE = Page.from_gemtext(GEMTEXT, RenderOptions(80, "fancy", "- "))
class TestExternal(unittest.TestCase):
def test_substitute_external_command(self):
# Replace URLs occurences.
command = "gmni $u | grep $u" # Grep for a page's own URL.
result = substitute_external_command(command, URL, PAGE)
self.assertEqual(result, "gmni {u} | grep {u}".format(u=URL))
# Replace link ID's with the target URL.
command = "gmni $1 && gmni $2" # Get both links
result = substitute_external_command(command, URL, PAGE)
expected = (
"gmni gemini://example.com/index.gmi"
" && gmni gemini://example.com/sub/gemlog.gmi"
)
self.assertEqual(result, expected)
# Invalid link ID raise a ValueError.
command = "gmni $8"
with self.assertRaises(Exception):
substitute_external_command(command, URL, PAGE)
# Replace escaped $$ with literal $.
command = "grep ^iamaregex$$ | echo dollar $" # Do nothing with last.
result = substitute_external_command(command, URL, PAGE)
self.assertEqual(result, "grep ^iamaregex$ | echo dollar $")