diff --git a/bebop/bookmarks.py b/bebop/bookmarks.py new file mode 100644 index 0000000..00be956 --- /dev/null +++ b/bebop/bookmarks.py @@ -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 diff --git a/bebop/browser.py b/bebop/browser.py index 6993bcc..e8ddef2 100644 --- a/bebop/browser.py +++ b/bebop/browser.py @@ -7,6 +7,7 @@ import os import webbrowser from math import inf +from bebop.bookmarks import get_bookmarks_document, save_bookmark from bebop.colors import ColorPair, init_colors from bebop.command_line import CommandLine from bebop.history import History @@ -30,9 +31,9 @@ class Browser: self.command_line = None self.running = True self.status_data = ("", 0, 0) - self.current_url = "" self.history = History() self.cache = {} + self._current_url = "" @property def h(self): @@ -42,6 +43,17 @@ class Browser: def w(self): 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): """Use curses' wrapper around _run.""" os.environ.setdefault("ESCDELAY", "25") @@ -116,6 +128,10 @@ class Browser: self.go_to_parent_page() elif char == ord("U"): self.go_to_root_page() + elif char == ord("b"): + self.open_bookmarks() + elif char == ord("B"): + self.add_bookmark() elif curses.ascii.isdigit(char): self.handle_digit_input(char) elif char == curses.KEY_MOUSE: @@ -126,7 +142,7 @@ class Browser: self.screen.nodelay(True) char = self.screen.getch() if char == -1: - self.set_status(self.current_url) + self.reset_status() else: # ALT keybinds. if char == ord("h"): self.scroll_page_horizontally(-1) @@ -182,6 +198,10 @@ class Browser: self.status_data = text, ColorPair.NORMAL, curses.A_ITALIC 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): """Set an error message in the status bar.""" self.status_data = text, ColorPair.ERROR, 0 @@ -241,11 +261,15 @@ class Browser: # If there is no netloc, this is a relative URL. if join or base_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"): self.open_web_url(url) 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: self.set_status_error(f"Protocol {parts.scheme} not supported.") @@ -464,7 +488,7 @@ class Browser: def reload_page(self): """Reload the page, if one has been previously loaded.""" if self.current_url: - self.open_gemini_url( + self.open_url( self.current_url, history=False, use_cache=False @@ -473,7 +497,7 @@ class Browser: def go_back(self): """Go back in history if possible.""" 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): """Go to the parent URL if possible.""" @@ -490,7 +514,7 @@ class Browser: self.set_status(f"Opening {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. 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}") return 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()