2021-03-11 19:16:15 +01:00
|
|
|
"""Main browser logic."""
|
|
|
|
|
2021-02-12 19:01:42 +01:00
|
|
|
import curses
|
|
|
|
import curses.ascii
|
|
|
|
import curses.textpad
|
|
|
|
import os
|
2021-04-16 19:30:33 +02:00
|
|
|
import tempfile
|
2021-02-18 19:01:28 +01:00
|
|
|
from math import inf
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-04-16 19:30:33 +02:00
|
|
|
from bebop.bookmarks import (
|
|
|
|
get_bookmarks_path, get_bookmarks_document, save_bookmark
|
|
|
|
)
|
2021-02-12 19:01:42 +01:00
|
|
|
from bebop.colors import ColorPair, init_colors
|
2021-03-13 20:37:13 +01:00
|
|
|
from bebop.command_line import CommandLine
|
2021-04-17 21:54:11 +02:00
|
|
|
from bebop.external import open_external_program
|
2021-02-18 01:40:05 +01:00
|
|
|
from bebop.history import History
|
2021-03-13 16:31:11 +01:00
|
|
|
from bebop.links import Links
|
2021-02-12 19:01:42 +01:00
|
|
|
from bebop.mouse import ButtonState
|
2021-03-28 18:28:35 +02:00
|
|
|
from bebop.navigation import (
|
|
|
|
get_parent_url, get_root_url, join_url, parse_url, sanitize_url)
|
2021-03-18 01:55:31 +01:00
|
|
|
from bebop.page import Page
|
|
|
|
from bebop.page_pad import PagePad
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
|
2021-02-16 21:22:49 +01:00
|
|
|
class Browser:
|
|
|
|
"""Manage the events, inputs and rendering."""
|
2021-03-28 18:28:35 +02:00
|
|
|
|
2021-04-18 02:27:05 +02:00
|
|
|
def __init__(self, config, cert_stash):
|
|
|
|
self.config = config
|
2021-03-13 16:31:11 +01:00
|
|
|
self.stash = cert_stash or {}
|
2021-02-12 19:01:42 +01:00
|
|
|
self.screen = None
|
|
|
|
self.dim = (0, 0)
|
2021-03-13 20:37:13 +01:00
|
|
|
self.page_pad = None
|
2021-02-12 23:29:51 +01:00
|
|
|
self.status_line = None
|
|
|
|
self.command_line = None
|
2021-03-13 20:37:13 +01:00
|
|
|
self.running = True
|
2021-03-09 00:44:54 +01:00
|
|
|
self.status_data = ("", 0, 0)
|
2021-02-18 01:40:05 +01:00
|
|
|
self.history = History()
|
2021-03-14 00:05:22 +01:00
|
|
|
self.cache = {}
|
2021-03-18 01:56:24 +01:00
|
|
|
self._current_url = ""
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def h(self):
|
|
|
|
return self.dim[0]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def w(self):
|
|
|
|
return self.dim[1]
|
|
|
|
|
2021-03-18 01:56:24 +01:00
|
|
|
@property
|
|
|
|
def current_url(self):
|
|
|
|
"""Return the current URL."""
|
|
|
|
return self._current_url
|
|
|
|
|
|
|
|
@current_url.setter
|
|
|
|
def current_url(self, url):
|
|
|
|
"""Set the current URL and show it in the status line."""
|
|
|
|
self._current_url = url
|
|
|
|
self.set_status(url)
|
|
|
|
|
2021-02-12 19:01:42 +01:00
|
|
|
def run(self, *args, **kwargs):
|
|
|
|
"""Use curses' wrapper around _run."""
|
|
|
|
os.environ.setdefault("ESCDELAY", "25")
|
|
|
|
curses.wrapper(self._run, *args, **kwargs)
|
|
|
|
|
|
|
|
def _run(self, stdscr, start_url=None):
|
|
|
|
"""Start displaying content and handling events."""
|
|
|
|
self.screen = stdscr
|
|
|
|
self.screen.clear()
|
|
|
|
self.screen.refresh()
|
2021-02-12 23:29:51 +01:00
|
|
|
|
2021-02-12 19:01:42 +01:00
|
|
|
curses.mousemask(curses.ALL_MOUSE_EVENTS)
|
2021-02-12 23:29:51 +01:00
|
|
|
curses.curs_set(0)
|
|
|
|
init_colors()
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
self.dim = self.screen.getmaxyx()
|
2021-03-13 20:37:13 +01:00
|
|
|
self.page_pad = PagePad(self.h - 2)
|
2021-02-12 23:29:51 +01:00
|
|
|
self.status_line = self.screen.subwin(
|
2021-02-12 19:01:42 +01:00
|
|
|
*self.line_dim,
|
2021-02-12 23:29:51 +01:00
|
|
|
*self.status_line_pos,
|
2021-02-12 19:01:42 +01:00
|
|
|
)
|
2021-02-12 23:29:51 +01:00
|
|
|
command_line_window = self.screen.subwin(
|
2021-02-12 19:01:42 +01:00
|
|
|
*self.line_dim,
|
2021-02-12 23:29:51 +01:00
|
|
|
*self.command_line_pos,
|
2021-02-12 19:01:42 +01:00
|
|
|
)
|
2021-04-18 02:27:05 +02:00
|
|
|
self.command_line = CommandLine(
|
|
|
|
command_line_window,
|
|
|
|
self.config["command_editor"]
|
|
|
|
)
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-03-08 23:40:03 +01:00
|
|
|
if start_url:
|
|
|
|
self.open_url(start_url, assume_absolute=True)
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-03-08 23:40:03 +01:00
|
|
|
while self.running:
|
2021-03-09 00:44:42 +01:00
|
|
|
try:
|
|
|
|
self.handle_inputs()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
self.set_status("Cancelled.")
|
|
|
|
|
|
|
|
def handle_inputs(self):
|
|
|
|
char = self.screen.getch()
|
|
|
|
if char == ord(":"):
|
|
|
|
self.quick_command("")
|
|
|
|
elif char == ord("r"):
|
|
|
|
self.reload_page()
|
|
|
|
elif char == ord("h"):
|
|
|
|
self.scroll_page_horizontally(-3)
|
2021-03-13 18:58:02 +01:00
|
|
|
elif char == ord("H"):
|
2021-04-17 22:02:06 +02:00
|
|
|
self.scroll_whole_page_left()
|
2021-03-09 00:44:42 +01:00
|
|
|
elif char == ord("j"):
|
|
|
|
self.scroll_page_vertically(3)
|
2021-03-13 18:58:02 +01:00
|
|
|
elif char == ord("J"):
|
|
|
|
self.scroll_whole_page_down()
|
2021-03-09 00:44:42 +01:00
|
|
|
elif char == ord("k"):
|
|
|
|
self.scroll_page_vertically(-3)
|
2021-03-13 18:58:02 +01:00
|
|
|
elif char == ord("K"):
|
|
|
|
self.scroll_whole_page_up()
|
2021-03-09 00:44:42 +01:00
|
|
|
elif char == ord("l"):
|
|
|
|
self.scroll_page_horizontally(3)
|
2021-03-13 18:58:02 +01:00
|
|
|
elif char == ord("L"):
|
2021-04-17 22:02:06 +02:00
|
|
|
self.scroll_whole_page_right()
|
2021-03-13 18:58:02 +01:00
|
|
|
elif char == ord("^"):
|
2021-04-17 22:02:06 +02:00
|
|
|
self.scroll_page_horizontally(-inf)
|
2021-03-09 00:44:42 +01:00
|
|
|
elif char == ord("g"):
|
2021-02-12 19:01:42 +01:00
|
|
|
char = self.screen.getch()
|
2021-03-09 00:44:42 +01:00
|
|
|
if char == ord("g"):
|
|
|
|
self.scroll_page_vertically(-inf)
|
|
|
|
elif char == ord("G"):
|
|
|
|
self.scroll_page_vertically(inf)
|
2021-03-13 18:58:02 +01:00
|
|
|
elif char == ord("o"):
|
|
|
|
self.quick_command("open")
|
|
|
|
elif char == ord("p"):
|
|
|
|
self.go_back()
|
2021-03-14 02:05:42 +01:00
|
|
|
elif char == ord("u"):
|
|
|
|
self.go_to_parent_page()
|
2021-03-16 19:38:11 +01:00
|
|
|
elif char == ord("U"):
|
|
|
|
self.go_to_root_page()
|
2021-03-18 01:56:24 +01:00
|
|
|
elif char == ord("b"):
|
|
|
|
self.open_bookmarks()
|
|
|
|
elif char == ord("B"):
|
|
|
|
self.add_bookmark()
|
2021-04-16 19:30:33 +02:00
|
|
|
elif char == ord("e"):
|
|
|
|
self.edit_page()
|
2021-03-09 00:44:42 +01:00
|
|
|
elif curses.ascii.isdigit(char):
|
|
|
|
self.handle_digit_input(char)
|
|
|
|
elif char == curses.KEY_MOUSE:
|
|
|
|
self.handle_mouse(*curses.getmouse())
|
|
|
|
elif char == curses.KEY_RESIZE:
|
|
|
|
self.handle_resize()
|
|
|
|
elif char == curses.ascii.ESC: # Can be ESC or ALT char.
|
|
|
|
self.screen.nodelay(True)
|
2021-03-13 18:58:02 +01:00
|
|
|
char = self.screen.getch()
|
|
|
|
if char == -1:
|
2021-03-18 01:56:24 +01:00
|
|
|
self.reset_status()
|
2021-03-13 18:58:02 +01:00
|
|
|
else: # ALT keybinds.
|
|
|
|
if char == ord("h"):
|
|
|
|
self.scroll_page_horizontally(-1)
|
|
|
|
elif char == ord("j"):
|
|
|
|
self.scroll_page_vertically(1)
|
|
|
|
elif char == ord("k"):
|
|
|
|
self.scroll_page_vertically(-1)
|
|
|
|
elif char == ord("l"):
|
|
|
|
self.scroll_page_horizontally(1)
|
2021-03-09 00:44:42 +01:00
|
|
|
self.screen.nodelay(False)
|
2021-03-14 00:04:55 +01:00
|
|
|
# else:
|
|
|
|
# unctrled = curses.unctrl(char)
|
|
|
|
# if unctrled == b"^T":
|
|
|
|
# self.set_status("test!")
|
2021-03-13 18:58:02 +01:00
|
|
|
|
2021-02-12 19:01:42 +01:00
|
|
|
@property
|
2021-02-12 23:29:51 +01:00
|
|
|
def page_pad_size(self):
|
2021-02-12 19:01:42 +01:00
|
|
|
return self.h - 3, self.w - 1
|
|
|
|
|
|
|
|
@property
|
2021-02-12 23:29:51 +01:00
|
|
|
def status_line_pos(self):
|
2021-02-12 19:01:42 +01:00
|
|
|
return self.h - 2, 0
|
|
|
|
|
|
|
|
@property
|
2021-02-12 23:29:51 +01:00
|
|
|
def command_line_pos(self):
|
2021-02-12 19:01:42 +01:00
|
|
|
return self.h - 1, 0
|
|
|
|
|
|
|
|
@property
|
|
|
|
def line_dim(self):
|
|
|
|
return 1, self.w
|
|
|
|
|
|
|
|
def refresh_windows(self):
|
2021-02-16 21:22:49 +01:00
|
|
|
"""Refresh all windows and clear command line."""
|
2021-02-12 23:29:51 +01:00
|
|
|
self.refresh_page()
|
|
|
|
self.refresh_status_line()
|
|
|
|
self.command_line.clear()
|
|
|
|
|
|
|
|
def refresh_page(self):
|
2021-02-16 21:22:49 +01:00
|
|
|
"""Refresh the current page pad; it does not reload the page."""
|
2021-03-13 20:37:13 +01:00
|
|
|
self.page_pad.refresh_content(*self.page_pad_size)
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-02-12 23:29:51 +01:00
|
|
|
def refresh_status_line(self):
|
2021-02-12 19:01:42 +01:00
|
|
|
"""Refresh status line contents."""
|
2021-03-09 00:44:54 +01:00
|
|
|
text, pair, attributes = self.status_data
|
2021-02-12 19:01:42 +01:00
|
|
|
text = text[:self.w - 1]
|
2021-03-09 00:44:54 +01:00
|
|
|
color = curses.color_pair(pair)
|
|
|
|
self.status_line.addstr(0, 0, text, color | attributes)
|
2021-02-12 23:29:51 +01:00
|
|
|
self.status_line.clrtoeol()
|
|
|
|
self.status_line.refresh()
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
def set_status(self, text):
|
|
|
|
"""Set a regular message in the status bar."""
|
2021-03-09 00:44:54 +01:00
|
|
|
self.status_data = text, ColorPair.NORMAL, curses.A_ITALIC
|
2021-02-12 23:29:51 +01:00
|
|
|
self.refresh_status_line()
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-03-18 01:56:24 +01:00
|
|
|
def reset_status(self):
|
|
|
|
"""Reset status line, e.g. after a cancelled action."""
|
|
|
|
self.set_status(self.current_url)
|
|
|
|
|
2021-02-12 19:01:42 +01:00
|
|
|
def set_status_error(self, text):
|
|
|
|
"""Set an error message in the status bar."""
|
2021-03-09 00:44:54 +01:00
|
|
|
self.status_data = text, ColorPair.ERROR, 0
|
2021-02-12 23:29:51 +01:00
|
|
|
self.refresh_status_line()
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-03-13 16:31:11 +01:00
|
|
|
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)
|
|
|
|
|
2021-03-18 01:55:31 +01:00
|
|
|
def open_url(self, url, base_url=None, redirects=0, assume_absolute=False,
|
|
|
|
history=True, use_cache=True):
|
2021-02-12 19:01:42 +01:00
|
|
|
"""Try to open an URL.
|
|
|
|
|
2021-02-16 21:22:49 +01:00
|
|
|
This function assumes that the URL can be from an user and thus tries a
|
|
|
|
few things to make it work.
|
|
|
|
|
|
|
|
If there is no current URL (e.g. we just started) or `assume_absolute`
|
|
|
|
is True, assume it is an absolute URL. In other cases, parse it normally
|
|
|
|
and later check if it has to be used relatively to the current URL.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
- url: an URL string, may not be completely compliant.
|
2021-02-26 17:41:16 +01:00
|
|
|
- base_url: an URL string to use as base in case `url` is relative.
|
2021-02-16 21:22:49 +01:00
|
|
|
- redirections: number of redirections we did yet for the same request.
|
|
|
|
- assume_absolute: assume we intended to use an absolute URL if True.
|
2021-03-18 01:55:31 +01:00
|
|
|
- history: whether the URL should be pushed to history on success.
|
|
|
|
- use_cache: whether we should look for an already cached document.
|
2021-02-12 19:01:42 +01:00
|
|
|
"""
|
2021-02-26 17:41:16 +01:00
|
|
|
if redirects > 5:
|
2021-02-15 19:57:49 +01:00
|
|
|
self.set_status_error(f"Too many redirections ({url}).")
|
2021-02-13 23:34:45 +01:00
|
|
|
return
|
2021-03-28 18:28:35 +02:00
|
|
|
|
2021-02-16 21:22:49 +01:00
|
|
|
if assume_absolute or not self.current_url:
|
2021-02-12 19:01:42 +01:00
|
|
|
parts = parse_url(url, absolute=True)
|
2021-02-17 01:31:24 +01:00
|
|
|
join = False
|
2021-02-16 21:22:49 +01:00
|
|
|
else:
|
|
|
|
parts = parse_url(url)
|
2021-02-17 01:31:24 +01:00
|
|
|
join = True
|
2021-03-28 18:28:35 +02:00
|
|
|
|
2021-02-12 19:01:42 +01:00
|
|
|
if parts.scheme == "gemini":
|
2021-03-28 18:28:35 +02:00
|
|
|
from bebop.browser.gemini import open_gemini_url
|
2021-02-13 23:34:45 +01:00
|
|
|
# If there is no netloc, this is a relative URL.
|
2021-02-26 17:41:16 +01:00
|
|
|
if join or base_url:
|
|
|
|
url = join_url(base_url or self.current_url, url)
|
2021-03-28 18:28:35 +02:00
|
|
|
open_gemini_url(
|
|
|
|
self,
|
|
|
|
sanitize_url(url),
|
|
|
|
redirects=redirects,
|
|
|
|
history=history,
|
|
|
|
use_cache=use_cache
|
|
|
|
)
|
2021-02-26 15:36:56 +01:00
|
|
|
elif parts.scheme.startswith("http"):
|
2021-03-28 18:28:35 +02:00
|
|
|
from bebop.browser.web import open_web_url
|
|
|
|
open_web_url(self, url)
|
2021-03-08 23:40:03 +01:00
|
|
|
elif parts.scheme == "file":
|
2021-03-28 18:28:35 +02:00
|
|
|
from bebop.browser.file import open_file
|
|
|
|
open_file(self, parts.path, history=history)
|
2021-03-18 01:56:24 +01:00
|
|
|
elif parts.scheme == "bebop":
|
|
|
|
if parts.netloc == "bookmarks":
|
|
|
|
self.open_bookmarks()
|
2021-02-12 19:01:42 +01:00
|
|
|
else:
|
2021-02-15 19:57:49 +01:00
|
|
|
self.set_status_error(f"Protocol {parts.scheme} not supported.")
|
2021-02-12 19:01:42 +01:00
|
|
|
|
2021-03-13 20:37:13 +01:00
|
|
|
def load_page(self, page: Page):
|
2021-02-12 23:29:51 +01:00
|
|
|
"""Load Gemtext data as the current page."""
|
2021-03-13 20:37:13 +01:00
|
|
|
old_pad_height = self.page_pad.dim[0]
|
|
|
|
self.page_pad.show_page(page)
|
|
|
|
if self.page_pad.dim[0] < old_pad_height:
|
2021-02-12 23:29:51 +01:00
|
|
|
self.screen.clear()
|
|
|
|
self.screen.refresh()
|
|
|
|
self.refresh_windows()
|
|
|
|
else:
|
|
|
|
self.refresh_page()
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
def handle_digit_input(self, init_char: int):
|
2021-03-13 16:31:11 +01:00
|
|
|
"""Focus command-line to select the link ID to follow."""
|
2021-04-16 19:30:14 +02:00
|
|
|
if self.page_pad.current_page is None:
|
2021-02-18 01:41:14 +01:00
|
|
|
return
|
2021-03-13 20:37:13 +01:00
|
|
|
links = self.page_pad.current_page.links
|
2021-04-16 19:30:14 +02:00
|
|
|
if links is None:
|
|
|
|
return
|
2021-03-13 16:31:11 +01:00
|
|
|
err, val = self.command_line.focus_for_link_navigation(init_char, links)
|
|
|
|
if err == 0:
|
|
|
|
self.open_link(links, val) # type: ignore
|
|
|
|
elif err == 2:
|
|
|
|
self.set_status_error(val)
|
|
|
|
|
|
|
|
def open_link(self, links: Links, link_id: int):
|
2021-02-12 19:01:42 +01:00
|
|
|
"""Open the link with this link ID."""
|
2021-02-12 23:29:51 +01:00
|
|
|
if not link_id in links:
|
2021-02-15 19:57:49 +01:00
|
|
|
self.set_status_error(f"Unknown link ID {link_id}.")
|
2021-02-12 19:01:42 +01:00
|
|
|
return
|
2021-02-12 23:29:51 +01:00
|
|
|
self.open_url(links[link_id])
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
def handle_mouse(self, mouse_id: int, x: int, y: int, z: int, bstate: int):
|
|
|
|
"""Handle mouse events.
|
|
|
|
|
|
|
|
Right now, only vertical scrolling is handled.
|
|
|
|
"""
|
|
|
|
if bstate & ButtonState.SCROLL_UP:
|
2021-02-15 18:51:45 +01:00
|
|
|
self.scroll_page_vertically(-3)
|
2021-02-12 19:01:42 +01:00
|
|
|
elif bstate & ButtonState.SCROLL_DOWN:
|
2021-02-15 18:51:45 +01:00
|
|
|
self.scroll_page_vertically(3)
|
2021-02-12 19:01:42 +01:00
|
|
|
|
|
|
|
def handle_resize(self):
|
|
|
|
"""Try to not make everything collapse on resizes."""
|
|
|
|
# Refresh the whole screen before changing windows to avoid random
|
|
|
|
# blank screens.
|
|
|
|
self.screen.refresh()
|
|
|
|
old_dim = self.dim
|
|
|
|
self.dim = self.screen.getmaxyx()
|
|
|
|
# Avoid work if the resizing does not impact us.
|
|
|
|
if self.dim == old_dim:
|
|
|
|
return
|
|
|
|
# Resize windows to fit the new dimensions. Content pad will be updated
|
|
|
|
# on its own at the end of the function.
|
2021-02-12 23:29:51 +01:00
|
|
|
self.status_line.resize(*self.line_dim)
|
|
|
|
self.command_line.window.resize(*self.line_dim)
|
2021-02-12 19:01:42 +01:00
|
|
|
# Move the windows to their new position if that's still possible.
|
2021-02-12 23:29:51 +01:00
|
|
|
if self.status_line_pos[0] >= 0:
|
|
|
|
self.status_line.mvwin(*self.status_line_pos)
|
|
|
|
if self.command_line_pos[0] >= 0:
|
|
|
|
self.command_line.window.mvwin(*self.command_line_pos)
|
2021-02-12 19:01:42 +01:00
|
|
|
# If the content pad does not fit its whole place, we have to clean the
|
|
|
|
# gap between it and the status line. Refresh all screen.
|
2021-03-13 20:37:13 +01:00
|
|
|
if self.page_pad.dim[0] < self.h - 2:
|
2021-02-12 19:01:42 +01:00
|
|
|
self.screen.clear()
|
|
|
|
self.screen.refresh()
|
|
|
|
self.refresh_windows()
|
2021-02-13 23:58:49 +01:00
|
|
|
|
2021-02-18 19:01:28 +01:00
|
|
|
def scroll_page_vertically(self, by_lines):
|
2021-03-13 16:31:11 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2021-02-18 19:01:28 +01:00
|
|
|
window_height = self.h - 2
|
|
|
|
require_refresh = False
|
|
|
|
if by_lines == inf:
|
2021-03-13 20:37:13 +01:00
|
|
|
require_refresh = self.page_pad.go_to_end(window_height)
|
2021-02-18 19:01:28 +01:00
|
|
|
elif by_lines == -inf:
|
2021-03-13 20:37:13 +01:00
|
|
|
require_refresh = self.page_pad.go_to_beginning()
|
2021-02-18 19:01:28 +01:00
|
|
|
else:
|
2021-03-13 20:37:13 +01:00
|
|
|
require_refresh = self.page_pad.scroll_v(by_lines, window_height)
|
2021-02-18 19:01:28 +01:00
|
|
|
if require_refresh:
|
2021-02-15 18:51:45 +01:00
|
|
|
self.refresh_page()
|
|
|
|
|
2021-03-13 18:58:02 +01:00
|
|
|
def scroll_whole_page_down(self):
|
|
|
|
"""Scroll down by a whole page."""
|
|
|
|
self.scroll_page_vertically(self.page_pad_size[0])
|
|
|
|
|
|
|
|
def scroll_whole_page_up(self):
|
|
|
|
"""Scroll up by a whole page."""
|
|
|
|
self.scroll_page_vertically(-self.page_pad_size[0])
|
|
|
|
|
2021-02-18 19:01:28 +01:00
|
|
|
def scroll_page_horizontally(self, by_columns):
|
2021-04-17 22:02:06 +02:00
|
|
|
"""Scroll page horizontally.
|
|
|
|
|
|
|
|
If `by_lines` is an integer (positive or negative), scroll the page by
|
|
|
|
this amount of columns. If `by_lines` is -inf, scroll back to the first
|
|
|
|
column. Scrolling to the right-most column is not supported.
|
|
|
|
"""
|
|
|
|
if by_columns == -inf:
|
|
|
|
require_refresh = self.page_pad.go_to_first_column()
|
|
|
|
else:
|
|
|
|
require_refresh = self.page_pad.scroll_h(by_columns, self.w)
|
|
|
|
if require_refresh:
|
2021-02-15 18:51:45 +01:00
|
|
|
self.refresh_page()
|
|
|
|
|
2021-04-17 22:02:06 +02:00
|
|
|
def scroll_whole_page_left(self):
|
|
|
|
"""Scroll left by a whole page."""
|
|
|
|
self.scroll_page_horizontally(-self.page_pad_size[1])
|
|
|
|
|
|
|
|
def scroll_whole_page_right(self):
|
|
|
|
"""Scroll right by a whole page."""
|
|
|
|
self.scroll_page_horizontally(self.page_pad_size[1])
|
|
|
|
|
2021-02-18 01:41:02 +01:00
|
|
|
def reload_page(self):
|
2021-03-13 16:31:11 +01:00
|
|
|
"""Reload the page, if one has been previously loaded."""
|
2021-02-18 01:41:02 +01:00
|
|
|
if self.current_url:
|
2021-03-28 18:28:35 +02:00
|
|
|
self.open_url(self.current_url, history=False, use_cache=False)
|
2021-02-18 01:41:02 +01:00
|
|
|
|
2021-02-13 23:58:49 +01:00
|
|
|
def go_back(self):
|
|
|
|
"""Go back in history if possible."""
|
2021-02-18 01:40:05 +01:00
|
|
|
if self.history.has_links():
|
2021-03-18 01:56:24 +01:00
|
|
|
self.open_url(self.history.pop(), history=False)
|
2021-02-26 15:36:56 +01:00
|
|
|
|
2021-03-14 02:05:42 +01:00
|
|
|
def go_to_parent_page(self):
|
|
|
|
"""Go to the parent URL if possible."""
|
|
|
|
if self.current_url:
|
2021-03-28 18:28:35 +02:00
|
|
|
self.open_url(get_parent_url(self.current_url))
|
2021-03-14 02:05:42 +01:00
|
|
|
|
2021-03-16 19:38:11 +01:00
|
|
|
def go_to_root_page(self):
|
|
|
|
"""Go to the root URL if possible."""
|
|
|
|
if self.current_url:
|
2021-03-28 18:28:35 +02:00
|
|
|
self.open_url(get_root_url(self.current_url))
|
2021-03-18 01:56:24 +01:00
|
|
|
|
|
|
|
def open_bookmarks(self):
|
|
|
|
"""Open bookmarks."""
|
|
|
|
content = get_bookmarks_document()
|
|
|
|
if content is None:
|
|
|
|
self.set_status_error("Failed to open bookmarks.")
|
|
|
|
return
|
2021-04-18 02:27:05 +02:00
|
|
|
self.load_page(Page.from_gemtext(content, self.config["text_width"]))
|
2021-03-18 01:56:24 +01:00
|
|
|
self.current_url = "bebop://bookmarks"
|
|
|
|
|
|
|
|
def add_bookmark(self):
|
|
|
|
"""Add the current URL as bookmark."""
|
|
|
|
if not self.current_url:
|
|
|
|
return
|
2021-04-16 19:30:14 +02:00
|
|
|
self.set_status("Bookmark title?")
|
2021-03-18 01:56:24 +01:00
|
|
|
current_title = self.page_pad.current_page.title or ""
|
|
|
|
title = self.command_line.focus(">", prefix=current_title)
|
|
|
|
if title:
|
|
|
|
title = title.strip()
|
|
|
|
if title:
|
|
|
|
save_bookmark(self.current_url, title)
|
|
|
|
self.reset_status()
|
2021-04-16 19:30:33 +02:00
|
|
|
|
|
|
|
def edit_page(self):
|
|
|
|
"""Open a text editor to edit the page source.
|
|
|
|
|
|
|
|
For external pages, the source is written in a temporary file, opened in
|
|
|
|
its editor of choice and so it's up to the user to save it where she
|
|
|
|
needs it, if needed. Internal pages, e.g. the bookmarks page, are loaded
|
|
|
|
directly from their location on disk.
|
|
|
|
"""
|
|
|
|
delete_source_after = False
|
|
|
|
special_pages = {
|
|
|
|
"bebop://bookmarks": str(get_bookmarks_path())
|
|
|
|
}
|
|
|
|
if self.current_url in special_pages:
|
|
|
|
source_filename = special_pages[self.current_url]
|
|
|
|
else:
|
|
|
|
if not self.page_pad.current_page:
|
|
|
|
return
|
|
|
|
source = self.page_pad.current_page.source
|
|
|
|
with tempfile.NamedTemporaryFile("wt", delete=False) as source_file:
|
|
|
|
source_file.write(source)
|
|
|
|
source_filename = source_file.name
|
|
|
|
delete_source_after = True
|
|
|
|
|
2021-04-18 02:27:05 +02:00
|
|
|
command = self.config["source_editor"] + [source_filename]
|
2021-04-17 21:54:11 +02:00
|
|
|
open_external_program(command)
|
2021-04-16 19:30:33 +02:00
|
|
|
if delete_source_after:
|
|
|
|
os.unlink(source_filename)
|
2021-04-17 21:54:11 +02:00
|
|
|
self.refresh_windows()
|