command_line: move input validation there
This absolutely un-atomic commit also introduces the Links class to hold methods regarding link lookups on top of the standard dict class.
This commit is contained in:
parent
83db26ada7
commit
0b79bd9e9e
173
bebop/browser.py
173
bebop/browser.py
|
@ -11,6 +11,7 @@ from bebop.colors import ColorPair, init_colors
|
||||||
from bebop.command_line import (CommandLine, EscapeCommandInterrupt,
|
from bebop.command_line import (CommandLine, EscapeCommandInterrupt,
|
||||||
TerminateCommandInterrupt)
|
TerminateCommandInterrupt)
|
||||||
from bebop.history import History
|
from bebop.history import History
|
||||||
|
from bebop.links import Links
|
||||||
from bebop.mouse import ButtonState
|
from bebop.mouse import ButtonState
|
||||||
from bebop.navigation import join_url, parse_url, sanitize_url, set_parameter
|
from bebop.navigation import join_url, parse_url, sanitize_url, set_parameter
|
||||||
from bebop.page import Page
|
from bebop.page import Page
|
||||||
|
@ -21,10 +22,10 @@ class Browser:
|
||||||
"""Manage the events, inputs and rendering."""
|
"""Manage the events, inputs and rendering."""
|
||||||
|
|
||||||
def __init__(self, cert_stash):
|
def __init__(self, cert_stash):
|
||||||
self.stash = cert_stash
|
self.stash = cert_stash or {}
|
||||||
self.screen = None
|
self.screen = None
|
||||||
self.dim = (0, 0)
|
self.dim = (0, 0)
|
||||||
self.tab = None
|
self.page = None
|
||||||
self.status_line = None
|
self.status_line = None
|
||||||
self.command_line = None
|
self.command_line = None
|
||||||
self.status_data = ("", 0, 0)
|
self.status_data = ("", 0, 0)
|
||||||
|
@ -172,6 +173,28 @@ class Browser:
|
||||||
self.status_data = text, ColorPair.ERROR, 0
|
self.status_data = text, ColorPair.ERROR, 0
|
||||||
self.refresh_status_line()
|
self.refresh_status_line()
|
||||||
|
|
||||||
|
def quick_command(self, command):
|
||||||
|
"""Shortcut method to take user input with a prefixed command string."""
|
||||||
|
prefix = f"{command} " if command else ""
|
||||||
|
user_input = self.command_line.focus(":", prefix=prefix)
|
||||||
|
if not user_input:
|
||||||
|
return
|
||||||
|
self.process_command(user_input)
|
||||||
|
|
||||||
|
def process_command(self, command_text: str):
|
||||||
|
"""Handle a client command."""
|
||||||
|
words = command_text.split()
|
||||||
|
num_words = len(words)
|
||||||
|
if num_words == 0:
|
||||||
|
return
|
||||||
|
command = words[0]
|
||||||
|
if num_words == 1:
|
||||||
|
if command in ("q", "quit"):
|
||||||
|
self.running = False
|
||||||
|
return
|
||||||
|
if command in ("o", "open"):
|
||||||
|
self.open_url(words[1], assume_absolute=True)
|
||||||
|
|
||||||
def open_url(self, url, base_url=None, redirects=0, assume_absolute=False):
|
def open_url(self, url, base_url=None, redirects=0, assume_absolute=False):
|
||||||
"""Try to open an URL.
|
"""Try to open an URL.
|
||||||
|
|
||||||
|
@ -210,7 +233,10 @@ class Browser:
|
||||||
self.set_status_error(f"Protocol {parts.scheme} not supported.")
|
self.set_status_error(f"Protocol {parts.scheme} not supported.")
|
||||||
|
|
||||||
def open_gemini_url(self, url, redirects=0, history=True):
|
def open_gemini_url(self, url, redirects=0, history=True):
|
||||||
"""Open a Gemini URL and set the formatted response as content."""
|
"""Open a Gemini URL and set the formatted response as content.
|
||||||
|
|
||||||
|
After initiating the connection, TODO
|
||||||
|
"""
|
||||||
self.set_status(f"Loading {url}")
|
self.set_status(f"Loading {url}")
|
||||||
req = Request(url, self.stash)
|
req = Request(url, self.stash)
|
||||||
connected = req.connect()
|
connected = req.connect()
|
||||||
|
@ -274,130 +300,18 @@ class Browser:
|
||||||
else:
|
else:
|
||||||
self.refresh_page()
|
self.refresh_page()
|
||||||
|
|
||||||
def take_user_input(self, type_char: str =":", prefix: str =""):
|
|
||||||
"""Focus command line to let the user type something."""
|
|
||||||
return self.command_line.focus(
|
|
||||||
type_char,
|
|
||||||
validator=self.validate_common_char,
|
|
||||||
prefix=prefix,
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_common_char(self, ch: int):
|
|
||||||
"""Generic input validator, handles a few more cases than default.
|
|
||||||
|
|
||||||
This validator can be used as a default validator as it handles, on top
|
|
||||||
of the Textbox defaults:
|
|
||||||
- Erasing the first command char, i.e. clearing the line, cancels the
|
|
||||||
command input.
|
|
||||||
- Pressing ESC also cancels the input.
|
|
||||||
|
|
||||||
This validator can be safely called at the beginning of other validators
|
|
||||||
to handle the keys above.
|
|
||||||
"""
|
|
||||||
if ch == curses.KEY_BACKSPACE: # Cancel input if all line is cleaned.
|
|
||||||
text = self.command_line.gather()
|
|
||||||
if len(text) == 0:
|
|
||||||
raise EscapeCommandInterrupt()
|
|
||||||
elif ch == curses.ascii.ESC: # Could be ESC or ALT
|
|
||||||
self.screen.nodelay(True)
|
|
||||||
ch = self.screen.getch()
|
|
||||||
if ch == -1:
|
|
||||||
raise EscapeCommandInterrupt()
|
|
||||||
self.screen.nodelay(False)
|
|
||||||
return ch
|
|
||||||
|
|
||||||
def quick_command(self, command):
|
|
||||||
"""Shortcut method to take user input with a prefixed command string."""
|
|
||||||
prefix = f"{command} " if command else ""
|
|
||||||
user_input = self.take_user_input(prefix=prefix)
|
|
||||||
if not user_input:
|
|
||||||
return
|
|
||||||
self.process_command(user_input)
|
|
||||||
|
|
||||||
def process_command(self, command_text: str):
|
|
||||||
words = command_text.split()
|
|
||||||
command = words[0]
|
|
||||||
if command in ("o", "open"):
|
|
||||||
self.open_url(words[1], assume_absolute=True)
|
|
||||||
elif command in ("q", "quit"):
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def handle_digit_input(self, init_char: int):
|
def handle_digit_input(self, init_char: int):
|
||||||
"""Handle a initial digit input by the user.
|
"""Focus command-line to select the link ID to follow."""
|
||||||
|
if not self.page or self.page.links is None:
|
||||||
When a digit key is pressed, the user intents to visit a link (or
|
return
|
||||||
dropped something on the numpad). To reduce the number of key types
|
|
||||||
needed, Bebop uses the following algorithm:
|
|
||||||
- If the current user input identifies a link without ambiguity, it is
|
|
||||||
used directly.
|
|
||||||
- If it is ambiguous, the user either inputs as many digits required
|
|
||||||
to disambiguate the link ID, or press enter to validate her input.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- I have 3 links. Pressing "2" takes me to link 2.
|
|
||||||
- I have 15 links. Pressing "3" takes me to link 3 (no ambiguity).
|
|
||||||
- I have 15 links. Pressing "1" and "2" takes me to link 12.
|
|
||||||
- I have 456 links. Pressing "1", "2" and Enter takes me to link 12.
|
|
||||||
- I have 456 links. Pressing "1", "2" and "6" takes me to link 126.
|
|
||||||
"""
|
|
||||||
digit = init_char & 0xf
|
|
||||||
links = self.page.links
|
links = self.page.links
|
||||||
num_links = len(links)
|
err, val = self.command_line.focus_for_link_navigation(init_char, links)
|
||||||
# If there are less than 10 links, just open it now.
|
if err == 0:
|
||||||
if num_links < 10:
|
self.open_link(links, val) # type: ignore
|
||||||
self.open_link(links, digit)
|
elif err == 2:
|
||||||
return
|
self.set_status_error(val)
|
||||||
# Else check if the digit alone is sufficient.
|
|
||||||
digit = chr(init_char)
|
|
||||||
max_digits = 0
|
|
||||||
while num_links:
|
|
||||||
max_digits += 1
|
|
||||||
num_links //= 10
|
|
||||||
disambiguous = self.disambiguate_link_id(digit, links, max_digits)
|
|
||||||
if disambiguous is not None:
|
|
||||||
self.open_link(links, disambiguous)
|
|
||||||
return
|
|
||||||
# Else, focus the command line to let the user input more digits.
|
|
||||||
validator = lambda ch: self._validate_link_digit(ch, links, max_digits)
|
|
||||||
link_input = self.command_line.focus("&", validator, digit)
|
|
||||||
if not link_input:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
link_id = int(link_input)
|
|
||||||
except ValueError as exc:
|
|
||||||
self.set_status_error(f"Invalid link ID {link_input}.")
|
|
||||||
return
|
|
||||||
self.open_link(links, link_id)
|
|
||||||
|
|
||||||
def _validate_link_digit(self, ch: int, links, max_digits: int):
|
def open_link(self, links: Links, link_id: int):
|
||||||
"""Handle input chars to be used as link ID."""
|
|
||||||
# Handle common chars.
|
|
||||||
ch = self.validate_common_char(ch)
|
|
||||||
# Only accept digits. If we reach the amount of required digits, open
|
|
||||||
# link now and leave command line. Else just process it.
|
|
||||||
if curses.ascii.isdigit(ch):
|
|
||||||
digits = self.command_line.gather() + chr(ch)
|
|
||||||
disambiguous = self.disambiguate_link_id(digits, links, max_digits)
|
|
||||||
if disambiguous is not None:
|
|
||||||
raise TerminateCommandInterrupt(disambiguous)
|
|
||||||
return ch
|
|
||||||
# If not a digit but a printable character, ignore it.
|
|
||||||
if curses.ascii.isprint(ch):
|
|
||||||
return 0
|
|
||||||
# Everything else could be a control character and should be processed.
|
|
||||||
return ch
|
|
||||||
|
|
||||||
def disambiguate_link_id(self, digits: str, links, max_digits: int):
|
|
||||||
"""Return the only possible link ID as str, or None on ambiguities."""
|
|
||||||
if len(digits) == max_digits:
|
|
||||||
return int(digits)
|
|
||||||
candidates = [
|
|
||||||
link_id for link_id, url in links.items()
|
|
||||||
if str(link_id).startswith(digits)
|
|
||||||
]
|
|
||||||
return candidates[0] if len(candidates) == 1 else None
|
|
||||||
|
|
||||||
def open_link(self, links, link_id: int):
|
|
||||||
"""Open the link with this link ID."""
|
"""Open the link with this link ID."""
|
||||||
if not link_id in links:
|
if not link_id in links:
|
||||||
self.set_status_error(f"Unknown link ID {link_id}.")
|
self.set_status_error(f"Unknown link ID {link_id}.")
|
||||||
|
@ -405,11 +319,12 @@ class Browser:
|
||||||
self.open_url(links[link_id])
|
self.open_url(links[link_id])
|
||||||
|
|
||||||
def handle_input_request(self, from_url: str, response: Response):
|
def handle_input_request(self, from_url: str, response: Response):
|
||||||
|
"""Focus command-line to pass input to the server."""
|
||||||
if response.meta:
|
if response.meta:
|
||||||
self.set_status(f"Input needed: {response.meta}")
|
self.set_status(f"Input needed: {response.meta}")
|
||||||
else:
|
else:
|
||||||
self.set_status("Input needed:")
|
self.set_status("Input needed:")
|
||||||
user_input = self.take_user_input("?")
|
user_input = self.command_line.focus("?")
|
||||||
if user_input:
|
if user_input:
|
||||||
url = set_parameter(from_url, user_input)
|
url = set_parameter(from_url, user_input)
|
||||||
self.open_gemini_url(url)
|
self.open_gemini_url(url)
|
||||||
|
@ -451,6 +366,12 @@ class Browser:
|
||||||
self.refresh_windows()
|
self.refresh_windows()
|
||||||
|
|
||||||
def scroll_page_vertically(self, by_lines):
|
def scroll_page_vertically(self, by_lines):
|
||||||
|
"""Scroll page vertically.
|
||||||
|
|
||||||
|
If `by_lines` is an integer (positive or negative), scroll the page by
|
||||||
|
this amount of lines. If `by_lines` is one of the floats inf and -inf,
|
||||||
|
go to the end of file and beginning of file, respectively.
|
||||||
|
"""
|
||||||
window_height = self.h - 2
|
window_height = self.h - 2
|
||||||
require_refresh = False
|
require_refresh = False
|
||||||
if by_lines == inf:
|
if by_lines == inf:
|
||||||
|
@ -463,10 +384,12 @@ class Browser:
|
||||||
self.refresh_page()
|
self.refresh_page()
|
||||||
|
|
||||||
def scroll_page_horizontally(self, by_columns):
|
def scroll_page_horizontally(self, by_columns):
|
||||||
|
"""Scroll page horizontally."""
|
||||||
if self.page.scroll_h(by_columns, self.w):
|
if self.page.scroll_h(by_columns, self.w):
|
||||||
self.refresh_page()
|
self.refresh_page()
|
||||||
|
|
||||||
def reload_page(self):
|
def reload_page(self):
|
||||||
|
"""Reload the page, if one has been previously loaded."""
|
||||||
if self.current_url:
|
if self.current_url:
|
||||||
self.open_gemini_url(self.current_url, history=False)
|
self.open_gemini_url(self.current_url, history=False)
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
"""Integrated command-line implementation."""
|
"""Integrated command-line implementation."""
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
import curses.ascii
|
||||||
import curses.textpad
|
import curses.textpad
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from bebop.links import Links
|
||||||
|
|
||||||
|
|
||||||
class CommandLine:
|
class CommandLine:
|
||||||
|
"""Basic and flaky command-line à la Vim, using curses module's Textbox."""
|
||||||
|
|
||||||
def __init__(self, window):
|
def __init__(self, window):
|
||||||
self.window = window
|
self.window = window
|
||||||
self.textbox = None
|
self.textbox = None
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Clear command-line contents."""
|
||||||
self.window.clear()
|
self.window.clear()
|
||||||
self.window.refresh()
|
self.window.refresh()
|
||||||
|
|
||||||
|
@ -20,8 +27,10 @@ class CommandLine:
|
||||||
validator function is passed to the textbox.
|
validator function is passed to the textbox.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
- command_char: char to display before the command line.
|
- command_char: char to display before the command line; it must be an
|
||||||
- validator: function to use to validate the input chars.
|
str of length 1, else the return value of `gather` might be wrong.
|
||||||
|
- validator: function to use to validate the input chars; if omitted,
|
||||||
|
`validate_common_input` is used.
|
||||||
- prefix: string to insert before the cursor in the command line.
|
- prefix: string to insert before the cursor in the command line.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -34,20 +43,114 @@ class CommandLine:
|
||||||
self.window.addstr(command_char + prefix)
|
self.window.addstr(command_char + prefix)
|
||||||
curses.curs_set(1)
|
curses.curs_set(1)
|
||||||
try:
|
try:
|
||||||
command = self.textbox.edit(validator)[1:].strip()
|
command = self.textbox.edit(validator or self.validate_common_input)
|
||||||
except EscapeCommandInterrupt:
|
except EscapeCommandInterrupt:
|
||||||
command = ""
|
command = ""
|
||||||
except TerminateCommandInterrupt as exc:
|
except TerminateCommandInterrupt as exc:
|
||||||
command = exc.command
|
command = exc.command
|
||||||
|
command = command[1:].rstrip()
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
self.window.clear()
|
self.clear()
|
||||||
self.window.refresh()
|
|
||||||
return command
|
return command
|
||||||
|
|
||||||
def gather(self):
|
def gather(self):
|
||||||
"""Return the string currently written by the user in command line."""
|
"""Return the string currently written by the user in command line."""
|
||||||
return self.textbox.gather()[1:].rstrip()
|
return self.textbox.gather()[1:].rstrip()
|
||||||
|
|
||||||
|
def validate_common_input(self, ch: int):
|
||||||
|
"""Generic input validator, handles a few more cases than default.
|
||||||
|
|
||||||
|
This validator can be used as a default validator as it handles, on top
|
||||||
|
of the Textbox defaults:
|
||||||
|
- Erasing the first command char, i.e. clearing the line, cancels the
|
||||||
|
command input.
|
||||||
|
- Pressing ESC also cancels the input.
|
||||||
|
|
||||||
|
This validator can be safely called at the beginning of other validators
|
||||||
|
to handle the keys above.
|
||||||
|
"""
|
||||||
|
if ch == curses.KEY_BACKSPACE: # Cancel input if all line is cleaned.
|
||||||
|
text = self.gather()
|
||||||
|
if len(text) == 0:
|
||||||
|
raise EscapeCommandInterrupt()
|
||||||
|
elif ch == curses.ascii.ESC: # Could be ESC or ALT
|
||||||
|
self.window.nodelay(True)
|
||||||
|
ch = self.window.getch()
|
||||||
|
if ch == -1:
|
||||||
|
raise EscapeCommandInterrupt()
|
||||||
|
self.window.nodelay(False)
|
||||||
|
return ch
|
||||||
|
|
||||||
|
def focus_for_link_navigation(self, init_char: int, links: Links):
|
||||||
|
"""Handle a initial digit input by the user.
|
||||||
|
|
||||||
|
When a digit key is pressed, the user intents to visit a link (or
|
||||||
|
dropped something on the numpad). To reduce the number of key types
|
||||||
|
needed, Bebop uses the following algorithm:
|
||||||
|
- If the current user input identifies a link without ambiguity, it is
|
||||||
|
used directly.
|
||||||
|
- If it is ambiguous, the user either inputs as many digits required
|
||||||
|
to disambiguate the link ID, or press enter to validate her input.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- I have 3 links. Pressing "2" takes me to link 2.
|
||||||
|
- I have 15 links. Pressing "3" takes me to link 3 (no ambiguity).
|
||||||
|
- I have 15 links. Pressing "1" and "2" takes me to link 12.
|
||||||
|
- I have 456 links. Pressing "1", "2" and Enter takes me to link 12.
|
||||||
|
- I have 456 links. Pressing "1", "2" and "6" takes me to link 126.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- init_char: the first char (code) being pressed.
|
||||||
|
- links: accessible Links.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tuple (error, value); if error is 0, value is the link ID to use; if
|
||||||
|
error is 1, discard value and do nothing; if error is 2, value is an
|
||||||
|
error than can be showed to the user.
|
||||||
|
"""
|
||||||
|
digit = init_char & 0xf
|
||||||
|
num_links = len(links)
|
||||||
|
# If there are less than 10 links, just open it now.
|
||||||
|
if num_links < 10:
|
||||||
|
return 0, digit
|
||||||
|
# Else check if the digit alone is sufficient.
|
||||||
|
digit = chr(init_char)
|
||||||
|
max_digits = 0
|
||||||
|
while num_links:
|
||||||
|
max_digits += 1
|
||||||
|
num_links //= 10
|
||||||
|
candidates = links.disambiguate(digit, max_digits)
|
||||||
|
if len(candidates) == 1:
|
||||||
|
return 0, candidates[0]
|
||||||
|
# Else, focus the command line to let the user input more digits.
|
||||||
|
validator = lambda ch: self.validate_link_digit(ch, links, max_digits)
|
||||||
|
link_input = self.focus("&", validator, digit)
|
||||||
|
if not link_input:
|
||||||
|
return 1, None
|
||||||
|
try:
|
||||||
|
link_id = int(link_input)
|
||||||
|
except ValueError as exc:
|
||||||
|
return 2, f"Invalid link ID {link_input}."
|
||||||
|
return 0, link_id
|
||||||
|
|
||||||
|
def validate_link_digit(self, ch: int, links: Links, max_digits: int):
|
||||||
|
"""Handle input chars to be used as link ID."""
|
||||||
|
# Handle common chars.
|
||||||
|
ch = self.validate_common_input(ch)
|
||||||
|
# Only accept digits. If we reach the amount of required digits, open
|
||||||
|
# link now and leave command line. Else just process it.
|
||||||
|
if curses.ascii.isdigit(ch):
|
||||||
|
digits = self.gather() + chr(ch)
|
||||||
|
candidates = links.disambiguate(digits, max_digits)
|
||||||
|
if len(candidates) == 1:
|
||||||
|
raise TerminateCommandInterrupt(candidates)
|
||||||
|
return ch
|
||||||
|
# If not a digit but a printable character, ignore it.
|
||||||
|
if curses.ascii.isprint(ch):
|
||||||
|
return 0
|
||||||
|
# Everything else could be a control character and should be processed.
|
||||||
|
return ch
|
||||||
|
|
||||||
|
|
||||||
class EscapeCommandInterrupt(Exception):
|
class EscapeCommandInterrupt(Exception):
|
||||||
"""Signal that ESC has been pressed during command line."""
|
"""Signal that ESC has been pressed during command line."""
|
||||||
|
|
10
bebop/fs.py
10
bebop/fs.py
|
@ -6,13 +6,13 @@ https://github.com/ActiveState/appdirs/blob/master/appdirs.py
|
||||||
|
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from os.path import expanduser, join
|
from os.path import expanduser, join
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
APP_NAME = "bebop"
|
APP_NAME = "bebop"
|
||||||
|
|
||||||
|
|
||||||
def get_user_data_dir():
|
def get_user_data_path() -> Path:
|
||||||
"""Return the user data directory."""
|
"""Return the user data directory path."""
|
||||||
path = getenv("XDG_DATA_HOME", expanduser("~/.local/share"))
|
path = Path(getenv("XDG_DATA_HOME", expanduser("~/.local/share")))
|
||||||
path = join(path, APP_NAME)
|
return path / APP_NAME
|
||||||
return path
|
|
||||||
|
|
13
bebop/links.py
Normal file
13
bebop/links.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
"""Links manager."""
|
||||||
|
|
||||||
|
|
||||||
|
class Links(dict):
|
||||||
|
|
||||||
|
def disambiguate(self, digits: str, max_digits: int):
|
||||||
|
"""Return the list of possible candidates for those digits."""
|
||||||
|
if len(digits) == max_digits:
|
||||||
|
return [int(digits)]
|
||||||
|
return [
|
||||||
|
link_id for link_id, url in self.items()
|
||||||
|
if str(link_id).startswith(digits)
|
||||||
|
]
|
|
@ -3,6 +3,7 @@
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
from bebop.gemtext import parse_gemtext
|
from bebop.gemtext import parse_gemtext
|
||||||
|
from bebop.links import Links
|
||||||
from bebop.rendering import format_elements, render_lines
|
from bebop.rendering import format_elements, render_lines
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,21 +20,23 @@ class Page:
|
||||||
self.metalines = []
|
self.metalines = []
|
||||||
self.current_line = 0
|
self.current_line = 0
|
||||||
self.current_column = 0
|
self.current_column = 0
|
||||||
self.links = {}
|
self.links = Links()
|
||||||
|
|
||||||
def show_gemtext(self, gemtext: bytes):
|
def show_gemtext(self, gemtext: bytes):
|
||||||
"""Render Gemtext data in the content pad."""
|
"""Render Gemtext data in the content pad."""
|
||||||
|
# Parse and format Gemtext.
|
||||||
elements = parse_gemtext(gemtext)
|
elements = parse_gemtext(gemtext)
|
||||||
self.metalines = format_elements(elements, 80)
|
self.metalines = format_elements(elements, 80)
|
||||||
self.links = {
|
# Render metalines.
|
||||||
meta["link_id"]: meta["url"]
|
|
||||||
for meta, _ in self.metalines
|
|
||||||
if "link_id" in meta and "url" in meta
|
|
||||||
}
|
|
||||||
self.pad.clear()
|
self.pad.clear()
|
||||||
self.dim = render_lines(self.metalines, self.pad, Page.MAX_COLS)
|
self.dim = render_lines(self.metalines, self.pad, Page.MAX_COLS)
|
||||||
self.current_line = 0
|
self.current_line = 0
|
||||||
self.current_column = 0
|
self.current_column = 0
|
||||||
|
# Aggregate links for navigation.
|
||||||
|
self.links = Links()
|
||||||
|
for meta, _ in self.metalines:
|
||||||
|
if "link_id" in meta and "url" in meta:
|
||||||
|
self.links[meta["link_id"]] = meta["url"]
|
||||||
|
|
||||||
def refresh_content(self, x, y):
|
def refresh_content(self, x, y):
|
||||||
"""Refresh content pad's view using the current line/column."""
|
"""Refresh content pad's view using the current line/column."""
|
||||||
|
|
|
@ -11,6 +11,7 @@ from enum import Enum
|
||||||
|
|
||||||
import asn1crypto.x509
|
import asn1crypto.x509
|
||||||
|
|
||||||
|
|
||||||
STASH_LINE_RE = re.compile(r"(\S+) (\S+) (\S+) (\d+)")
|
STASH_LINE_RE = re.compile(r"(\S+) (\S+) (\S+) (\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in a new issue