command_line: add a prompt function
This commit is contained in:
parent
396391ea80
commit
80ec71f30b
|
@ -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)
|
||||
|
|
|
@ -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,22 +68,28 @@ 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,
|
||||
history: bool):
|
||||
def _handle_successful_response(browser: Browser, response: Response, url: str,
|
||||
history: bool):
|
||||
"""Handle a successful response content from a Gemini server.
|
||||
|
||||
According to the MIME type received or inferred, the response is either
|
||||
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
Reference in a new issue