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):
"""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:
prefix = command + " " if command else ""
text = self.command_line.focus(CommandLine.CHAR_COMMAND, prefix=prefix)
if not text:
return
self.process_command(user_input)
self.process_command(text)
def process_command(self, command_text: str):
"""Handle a client command."""
@ -480,7 +480,10 @@ class Browser:
return
self.set_status("Bookmark title?")
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:
title = title.strip()
if title:
@ -525,3 +528,8 @@ class Browser:
"""Show the help page."""
self.load_page(Page.from_gemtext(HELP_PAGE, self.config["text_width"]))
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 bebop.browser.browser import Browser
from bebop.command_line import CommandLine
from bebop.fs import get_downloads_path
from bebop.navigation import set_parameter
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})."
# TODO propose the user ways to handle this.
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
else:
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}).")
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:
handle_response_content(browser, url, response, history)
_handle_successful_response(browser, response, url, history)
elif response.generic_code == 30 and response.meta:
browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
elif response.generic_code in (40, 50):
error = f"Server error: {response.meta or Response.code.name}"
browser.set_status_error(error)
elif response.generic_code == 10:
handle_input_request(browser, url, response.meta)
_handle_input_request(browser, url, response.meta)
else:
error = f"Unhandled response code {response.code}"
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):
"""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")
page = Page.from_text(text)
else:
filepath = get_download_path(url)
filepath = _get_download_path(url)
if page:
browser.load_page(page)
@ -137,7 +144,7 @@ def handle_response_content(browser: Browser, url: str, response: Response,
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."""
download_dir = get_downloads_path()
url_parts = url.rsplit("/", maxsplit=1)
@ -149,13 +156,13 @@ def get_download_path(url: str) -> Path:
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."""
if message:
browser.set_status(f"Input needed: {message}")
else:
browser.set_status("Input needed:")
user_input = browser.command_line.focus("?")
user_input = browser.command_line.focus(CommandLine.CHAR_TEXT)
if not user_input:
return
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.
Therefore I just added the M-e keybind to call an external editor and use
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):
self.window = window
self.editor_command = editor_command
self.textbox = None
self.textbox = curses.textpad.Textbox(self.window)
def clear(self):
"""Clear command-line contents."""
self.window.clear()
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=""):
"""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
an EscapeInterrupt.
"""
validator = validator or self._validate_common_input
self.window.clear()
self.window.refresh()
self.textbox = curses.textpad.Textbox(self.window)
self.window.addstr(command_char + prefix)
curses.curs_set(1)
try:
command = self.textbox.edit(validator or self.validate_common_input)
command = self.textbox.edit(validator)
except EscapeCommandInterrupt:
command = ""
except TerminateCommandInterrupt as exc:
@ -63,11 +80,7 @@ class CommandLine:
self.clear()
return command
def gather(self):
"""Return the string currently written by the user in command line."""
return self.textbox.gather()[1:].rstrip()
def validate_common_input(self, ch: int):
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
@ -136,8 +149,8 @@ class CommandLine:
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)
validator = lambda ch: self._validate_link_digit(ch, links, max_digits)
link_input = self.focus(CommandLine.CHAR_DIGIT, validator, digit)
if not link_input:
return 1, None
try:
@ -146,10 +159,10 @@ class CommandLine:
return 2, f"Invalid link ID {link_input}."
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 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
# link now and leave command line. Else just process it.
if curses.ascii.isdigit(ch):
@ -185,6 +198,21 @@ class CommandLine:
return
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):
"""Signal that ESC has been pressed during command line."""