command_line: add a prompt function

This commit is contained in:
dece 2021-04-19 00:28:20 +02:00
parent 396391ea80
commit 80ec71f30b
3 changed files with 69 additions and 26 deletions

View file

@ -253,11 +253,11 @@ class Browser:
def quick_command(self, command): def quick_command(self, command):
"""Shortcut method to take user input with a prefixed command string.""" """Shortcut method to take user input with a prefixed command string."""
prefix = f"{command} " if command else "" prefix = command + " " if command else ""
user_input = self.command_line.focus(":", prefix=prefix) text = self.command_line.focus(CommandLine.CHAR_COMMAND, prefix=prefix)
if not user_input: if not text:
return return
self.process_command(user_input) self.process_command(text)
def process_command(self, command_text: str): def process_command(self, command_text: str):
"""Handle a client command.""" """Handle a client command."""
@ -480,7 +480,10 @@ class Browser:
return return
self.set_status("Bookmark title?") self.set_status("Bookmark title?")
current_title = self.page_pad.current_page.title or "" current_title = self.page_pad.current_page.title or ""
title = self.command_line.focus(">", prefix=current_title) title = self.command_line.focus(
CommandLine.CHAR_TEXT,
prefix=current_title
)
if title: if title:
title = title.strip() title = title.strip()
if title: if title:
@ -525,3 +528,8 @@ class Browser:
"""Show the help page.""" """Show the help page."""
self.load_page(Page.from_gemtext(HELP_PAGE, self.config["text_width"])) self.load_page(Page.from_gemtext(HELP_PAGE, self.config["text_width"]))
self.current_url = "bebop://help" self.current_url = "bebop://help"
def prompt(self, text, keys):
"""Display the text and allow it to type one of the given keys."""
self.set_status(text)
return self.command_line.prompt_key(keys)

View file

@ -3,6 +3,7 @@
from pathlib import Path from pathlib import Path
from bebop.browser.browser import Browser from bebop.browser.browser import Browser
from bebop.command_line import CommandLine
from bebop.fs import get_downloads_path from bebop.fs import get_downloads_path
from bebop.navigation import set_parameter from bebop.navigation import set_parameter
from bebop.page import Page from bebop.page import Page
@ -42,7 +43,7 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
error = f"Certificate has been changed ({url})." error = f"Certificate has been changed ({url})."
# TODO propose the user ways to handle this. # TODO propose the user ways to handle this.
elif req.state == Request.STATE_CONNECTION_FAILED: elif req.state == Request.STATE_CONNECTION_FAILED:
error_details = f": {req.error}" if req.error else "." error_details = ": " + req.error if req.error else "."
error = f"Connection failed ({url})" + error_details error = f"Connection failed ({url})" + error_details
else: else:
error = f"Connection failed ({url})." error = f"Connection failed ({url})."
@ -67,21 +68,27 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
browser.set_status_error(f"Server response parsing failed ({url}).") browser.set_status_error(f"Server response parsing failed ({url}).")
return return
_handle_response(browser, response, url, redirects, history)
def _handle_response(browser: Browser, response: Response, url: str,
redirects: int, history: bool):
"""Handle a response from a Gemini server."""
if response.code == 20: if response.code == 20:
handle_response_content(browser, url, response, history) _handle_successful_response(browser, response, url, history)
elif response.generic_code == 30 and response.meta: elif response.generic_code == 30 and response.meta:
browser.open_url(response.meta, base_url=url, redirects=redirects + 1) browser.open_url(response.meta, base_url=url, 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:
handle_input_request(browser, url, response.meta) _handle_input_request(browser, url, response.meta)
else: else:
error = f"Unhandled response code {response.code}" error = f"Unhandled response code {response.code}"
browser.set_status_error(error) browser.set_status_error(error)
def handle_response_content(browser: Browser, url: str, response: Response, def _handle_successful_response(browser: Browser, response: Response, url: str,
history: bool): history: bool):
"""Handle a successful response content from a Gemini server. """Handle a successful response content from a Gemini server.
@ -116,7 +123,7 @@ def handle_response_content(browser: Browser, url: str, response: Response,
text = response.content.decode("utf-8", errors="replace") text = response.content.decode("utf-8", errors="replace")
page = Page.from_text(text) page = Page.from_text(text)
else: else:
filepath = get_download_path(url) filepath = _get_download_path(url)
if page: if page:
browser.load_page(page) browser.load_page(page)
@ -137,7 +144,7 @@ def handle_response_content(browser: Browser, url: str, response: Response,
browser.set_status_error(error) browser.set_status_error(error)
def get_download_path(url: str) -> Path: def _get_download_path(url: str) -> Path:
"""Try to find the best download file path possible from this URL.""" """Try to find the best download file path possible from this URL."""
download_dir = get_downloads_path() download_dir = get_downloads_path()
url_parts = url.rsplit("/", maxsplit=1) url_parts = url.rsplit("/", maxsplit=1)
@ -149,13 +156,13 @@ def get_download_path(url: str) -> Path:
return download_dir / filename return download_dir / filename
def handle_input_request(browser: Browser, from_url: str, message: str =None): def _handle_input_request(browser: Browser, from_url: str, message: str =None):
"""Focus command-line to pass input to the server.""" """Focus command-line to pass input to the server."""
if message: if message:
browser.set_status(f"Input needed: {message}") browser.set_status(f"Input needed: {message}")
else: else:
browser.set_status("Input needed:") browser.set_status("Input needed:")
user_input = browser.command_line.focus("?") user_input = browser.command_line.focus(CommandLine.CHAR_TEXT)
if not user_input: if not user_input:
return return
url = set_parameter(from_url, user_input) url = set_parameter(from_url, user_input)

View file

@ -17,18 +17,35 @@ class CommandLine:
the window's right border when writing more content than the width allows. the window's right border when writing more content than the width allows.
Therefore I just added the M-e keybind to call an external editor and use Therefore I just added the M-e keybind to call an external editor and use
its content as result. its content as result.
Attributes:
- window: curses window to use for the command line and Textbox.
- editor_command: external command to use to edit content externally.
- textbox: Textbox object handling user input.
""" """
CHAR_COMMAND = ":"
CHAR_DIGIT = "&"
CHAR_TEXT = ">"
def __init__(self, window, editor_command): def __init__(self, window, editor_command):
self.window = window self.window = window
self.editor_command = editor_command self.editor_command = editor_command
self.textbox = None self.textbox = curses.textpad.Textbox(self.window)
def clear(self): def clear(self):
"""Clear command-line contents.""" """Clear command-line contents."""
self.window.clear() self.window.clear()
self.window.refresh() self.window.refresh()
def gather(self):
"""Return the string currently written by the user in command line.
This doesn't count the command char used, but it includes then prefix.
Trailing whitespace is trimmed.
"""
return self.textbox.gather()[1:].rstrip()
def focus(self, command_char, validator=None, prefix=""): def focus(self, command_char, validator=None, prefix=""):
"""Give user focus to the command bar. """Give user focus to the command bar.
@ -46,13 +63,13 @@ class CommandLine:
User input as string. The string will be empty if the validator raised User input as string. The string will be empty if the validator raised
an EscapeInterrupt. an EscapeInterrupt.
""" """
validator = validator or self._validate_common_input
self.window.clear() self.window.clear()
self.window.refresh() self.window.refresh()
self.textbox = curses.textpad.Textbox(self.window)
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 or self.validate_common_input) command = self.textbox.edit(validator)
except EscapeCommandInterrupt: except EscapeCommandInterrupt:
command = "" command = ""
except TerminateCommandInterrupt as exc: except TerminateCommandInterrupt as exc:
@ -63,11 +80,7 @@ class CommandLine:
self.clear() self.clear()
return command return command
def gather(self): def _validate_common_input(self, ch: int):
"""Return the string currently written by the user in command line."""
return self.textbox.gather()[1:].rstrip()
def validate_common_input(self, ch: int):
"""Generic input validator, handles a few more cases than default. """Generic input validator, handles a few more cases than default.
This validator can be used as a default validator as it handles, on top This validator can be used as a default validator as it handles, on top
@ -136,8 +149,8 @@ class CommandLine:
if len(candidates) == 1: if len(candidates) == 1:
return 0, candidates[0] return 0, candidates[0]
# Else, focus the command line to let the user input more digits. # Else, focus the command line to let the user input more digits.
validator = lambda ch: self.validate_link_digit(ch, links, max_digits) validator = lambda ch: self._validate_link_digit(ch, links, max_digits)
link_input = self.focus("&", validator, digit) link_input = self.focus(CommandLine.CHAR_DIGIT, validator, digit)
if not link_input: if not link_input:
return 1, None return 1, None
try: try:
@ -146,10 +159,10 @@ class CommandLine:
return 2, f"Invalid link ID {link_input}." return 2, f"Invalid link ID {link_input}."
return 0, link_id return 0, link_id
def validate_link_digit(self, ch: int, links: Links, max_digits: int): def _validate_link_digit(self, ch: int, links: Links, max_digits: int):
"""Handle input chars to be used as link ID.""" """Handle input chars to be used as link ID."""
# Handle common chars. # Handle common chars.
ch = self.validate_common_input(ch) ch = self._validate_common_input(ch)
# Only accept digits. If we reach the amount of required digits, open # Only accept digits. If we reach the amount of required digits, open
# link now and leave command line. Else just process it. # link now and leave command line. Else just process it.
if curses.ascii.isdigit(ch): if curses.ascii.isdigit(ch):
@ -185,6 +198,21 @@ class CommandLine:
return return
raise TerminateCommandInterrupt(content) raise TerminateCommandInterrupt(content)
def prompt_key(self, keys):
"""Focus the command line and wait for the user """
validator = lambda ch: self._validate_prompt(ch, keys)
key = self.focus(CommandLine.CHAR_TEXT, validator)
return key if key in keys else ""
def _validate_prompt(self, ch: int, keys):
"""Handle input chars and raise a terminate interrupt on a valid key."""
# Handle common keys.
ch = self._validate_common_input(ch)
char = chr(ch)
if char in keys:
raise TerminateCommandInterrupt(char)
return 0
class EscapeCommandInterrupt(Exception): class EscapeCommandInterrupt(Exception):
"""Signal that ESC has been pressed during command line.""" """Signal that ESC has been pressed during command line."""