bookmarks: basic bookmark management

This commit is contained in:
dece 2021-03-18 01:56:24 +01:00
parent d6bcd1f706
commit 5b3e91336f
2 changed files with 117 additions and 7 deletions

60
bebop/bookmarks.py Normal file
View file

@ -0,0 +1,60 @@
import io
from pathlib import Path
from bebop.fs import get_user_data_path
TEMPLATE = """\
# Bookmarks
Welcome to your bookmark page! This file has been created in "{original_path}" \
and you can edit it as you wish. New bookmarks will be added on a new \
line at the end. Always keep an empty line at the end!
"""
def get_bookmarks_path() -> Path:
"""Return the path to the bookmarks file."""
return get_user_data_path() / "bookmarks.gmi"
def init_bookmarks(filepath):
"""Create the bookmarks file and return its initial content.
Raises OSError if the file could not be written.
"""
content = TEMPLATE.format(original_path=filepath)
with open(filepath, "wt") as bookmark_file:
bookmark_file.write(content)
return content
def get_bookmarks_document():
"""Return the bookmarks content, or None or failure.
If no bookmarks file exist yet, it is created. If accessing or creating the
file fails, or if it is unreadable, return None.
"""
filepath = get_bookmarks_path()
try:
if not filepath.exists():
content = init_bookmarks(filepath)
else:
with open(filepath, "rt") as bookmark_file:
content = bookmark_file.read()
except OSError:
return None
return content
def save_bookmark(url, title):
"""Append this URL/title pair to the bookmarks, return True on success."""
filepath = get_bookmarks_path()
try:
if not filepath.exists():
init_bookmarks(filepath)
with open(filepath, "at") as bookmark_file:
bookmark_file.write(f"=> {url} {title}\n")
except OSError:
return False
return True

View file

@ -7,6 +7,7 @@ import os
import webbrowser import webbrowser
from math import inf from math import inf
from bebop.bookmarks import get_bookmarks_document, save_bookmark
from bebop.colors import ColorPair, init_colors from bebop.colors import ColorPair, init_colors
from bebop.command_line import CommandLine from bebop.command_line import CommandLine
from bebop.history import History from bebop.history import History
@ -30,9 +31,9 @@ class Browser:
self.command_line = None self.command_line = None
self.running = True self.running = True
self.status_data = ("", 0, 0) self.status_data = ("", 0, 0)
self.current_url = ""
self.history = History() self.history = History()
self.cache = {} self.cache = {}
self._current_url = ""
@property @property
def h(self): def h(self):
@ -42,6 +43,17 @@ class Browser:
def w(self): def w(self):
return self.dim[1] return self.dim[1]
@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)
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
"""Use curses' wrapper around _run.""" """Use curses' wrapper around _run."""
os.environ.setdefault("ESCDELAY", "25") os.environ.setdefault("ESCDELAY", "25")
@ -116,6 +128,10 @@ class Browser:
self.go_to_parent_page() self.go_to_parent_page()
elif char == ord("U"): elif char == ord("U"):
self.go_to_root_page() self.go_to_root_page()
elif char == ord("b"):
self.open_bookmarks()
elif char == ord("B"):
self.add_bookmark()
elif curses.ascii.isdigit(char): elif curses.ascii.isdigit(char):
self.handle_digit_input(char) self.handle_digit_input(char)
elif char == curses.KEY_MOUSE: elif char == curses.KEY_MOUSE:
@ -126,7 +142,7 @@ class Browser:
self.screen.nodelay(True) self.screen.nodelay(True)
char = self.screen.getch() char = self.screen.getch()
if char == -1: if char == -1:
self.set_status(self.current_url) self.reset_status()
else: # ALT keybinds. else: # ALT keybinds.
if char == ord("h"): if char == ord("h"):
self.scroll_page_horizontally(-1) self.scroll_page_horizontally(-1)
@ -182,6 +198,10 @@ class Browser:
self.status_data = text, ColorPair.NORMAL, curses.A_ITALIC self.status_data = text, ColorPair.NORMAL, curses.A_ITALIC
self.refresh_status_line() self.refresh_status_line()
def reset_status(self):
"""Reset status line, e.g. after a cancelled action."""
self.set_status(self.current_url)
def set_status_error(self, text): def set_status_error(self, text):
"""Set an error message in the status bar.""" """Set an error message in the status bar."""
self.status_data = text, ColorPair.ERROR, 0 self.status_data = text, ColorPair.ERROR, 0
@ -241,11 +261,15 @@ class Browser:
# If there is no netloc, this is a relative URL. # If there is no netloc, this is a relative URL.
if join or base_url: if join or base_url:
url = join_url(base_url or self.current_url, url) url = join_url(base_url or self.current_url, url)
self.open_gemini_url(sanitize_url(url), redirects) self.open_gemini_url(sanitize_url(url), redirects=redirects,
history=history, use_cache=use_cache)
elif parts.scheme.startswith("http"): elif parts.scheme.startswith("http"):
self.open_web_url(url) self.open_web_url(url)
elif parts.scheme == "file": elif parts.scheme == "file":
self.open_file(parts.path) self.open_file(parts.path, history=history)
elif parts.scheme == "bebop":
if parts.netloc == "bookmarks":
self.open_bookmarks()
else: else:
self.set_status_error(f"Protocol {parts.scheme} not supported.") self.set_status_error(f"Protocol {parts.scheme} not supported.")
@ -464,7 +488,7 @@ class Browser:
def reload_page(self): def reload_page(self):
"""Reload the page, if one has been previously loaded.""" """Reload the page, if one has been previously loaded."""
if self.current_url: if self.current_url:
self.open_gemini_url( self.open_url(
self.current_url, self.current_url,
history=False, history=False,
use_cache=False use_cache=False
@ -473,7 +497,7 @@ class Browser:
def go_back(self): def go_back(self):
"""Go back in history if possible.""" """Go back in history if possible."""
if self.history.has_links(): if self.history.has_links():
self.open_gemini_url(self.history.pop(), history=False) self.open_url(self.history.pop(), history=False)
def go_to_parent_page(self): def go_to_parent_page(self):
"""Go to the parent URL if possible.""" """Go to the parent URL if possible."""
@ -490,7 +514,7 @@ class Browser:
self.set_status(f"Opening {url}") self.set_status(f"Opening {url}")
webbrowser.open_new_tab(url) webbrowser.open_new_tab(url)
def open_file(self, filepath, encoding="utf-8"): def open_file(self, filepath, encoding="utf-8", history=True):
"""Open a file and render it. """Open a file and render it.
This should be used only on Gemtext files or at least text files. This should be used only on Gemtext files or at least text files.
@ -505,3 +529,29 @@ class Browser:
self.set_status_error(f"Failed to open file: {exc}") self.set_status_error(f"Failed to open file: {exc}")
return return
self.load_page(Page.from_gemtext(text)) self.load_page(Page.from_gemtext(text))
file_url = "file://" + filepath
if history:
self.history.push(file_url)
self.current_url = file_url
def open_bookmarks(self):
"""Open bookmarks."""
content = get_bookmarks_document()
if content is None:
self.set_status_error("Failed to open bookmarks.")
return
self.load_page(Page.from_gemtext(content))
self.current_url = "bebop://bookmarks"
def add_bookmark(self):
"""Add the current URL as bookmark."""
if not self.current_url:
return
self.set_status("Title?")
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()