diff --git a/docs/src/content/docs/dev/reference/schema.mdx b/docs/src/content/docs/dev/reference/schema.mdx
index 590adf3b..b0e711d5 100644
--- a/docs/src/content/docs/dev/reference/schema.mdx
+++ b/docs/src/content/docs/dev/reference/schema.mdx
@@ -38,6 +38,10 @@ description: config schema humanified
- [1.18.2. Property `Rovr Config > interface > compact_mode > panels`](#interface_compact_mode_panels)
- [1.19. Property `Rovr Config > interface > mime_rules`](#interface_mime_rules)
- [1.19.1. Property `Rovr Config > interface > mime_rules > additionalProperties`](#interface_mime_rules_additionalProperties)
+ - [1.20. Property `Rovr Config > interface > pins_places`](#interface_pins_places)
+ - [1.20.1. Rovr Config > interface > pins_places > pins_places items](#interface_pins_places_items)
+ - [1.21. Property `Rovr Config > interface > pins_bookmarks`](#interface_pins_bookmarks)
+ - [1.21.1. Rovr Config > interface > pins_bookmarks > pins_bookmarks items](#interface_pins_bookmarks_items)
- [2. Property `Rovr Config > settings`](#settings)
- [2.1. Property `Rovr Config > settings > use_recycle_bin`](#settings_use_recycle_bin)
- [2.2. Property `Rovr Config > settings > copy_includes_metadata`](#settings_copy_includes_metadata)
@@ -352,27 +356,29 @@ description: config schema humanified
**Description:** Settings related to the user interface and experience
-| Property | Type | Title/Description |
-| --------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [tooltips](#interface_tooltips) | boolean | Show tooltips when your mouse is over a tooltip supported button.
This is not hot reloaded. |
-| [nerd_font](#interface_nerd_font) | boolean | Use nerd font for rendering icons instead of weird characters and stuff.
Not properly hot-reloaded. |
-| [use_reactive_layout](#interface_use_reactive_layout) | boolean | Hide certain elements based on the width and height of the terminal. |
-| [show_progress_eta](#interface_show_progress_eta) | boolean | When copying or deleting files, show an ETA for when the action will be completed. |
-| [show_progress_percentage](#interface_show_progress_percentage) | boolean | When copying or deleting files, show a percentage of how much had been completed. |
-| [truncate_progress_file_path](#interface_truncate_progress_file_path) | boolean | When the process container is using file paths, truncate the file path to only view the first and last names of the path. |
-| [show_line_numbers](#interface_show_line_numbers) | boolean | Add line numbers to the left gutter if you are viewing a text file. |
-| [scrolloff](#interface_scrolloff) | integer | The number of files to keep above and below the cursor when moving through the file list. |
-| [show_hidden_files](#interface_show_hidden_files) | boolean | Show hidden files and folders (those starting with a dot on Unix, or explicitly hidden on Windows/MacOS). |
-| [image_viewer](#interface_image_viewer) | object | Settings related to the image viewer used in the preview sidebar |
-| [font_preview](#interface_font_preview) | object | Settings related to the font preview used in the preview sidebar |
-| [allow_tab_nav](#interface_allow_tab_nav) | boolean | Allow navigating the main app screen with \`tab\` and \`shift+tab\` |
-| [append_new_tabs](#interface_append_new_tabs) | boolean | Choose whether or not to append new tabs instead of inserting them.
\`true\` => Append to the end of the tab list.
\`false\` => Insert after the active tab. |
-| [double_click_delay](#interface_double_click_delay) | number | The delay between two consecutive clicks to enter into a directory, or open a file. |
-| [drive_watcher_frequency](#interface_drive_watcher_frequency) | number | How often (in seconds) to check for changes in mounted drives in the sidebar. |
-| [clock](#interface_clock) | object | |
-| [preview_text](#interface_preview_text) | object | |
-| [compact_mode](#interface_compact_mode) | object | |
-| [mime_rules](#interface_mime_rules) | object | Map MIME type patterns to preview types. Uses regex patterns. Valid preview types: text, image, pdf, archive, folder, resvg, font, remime.
-> Use 'remime' if you want a more accurate description from file(1) |
+| Property | Type | Title/Description |
+| --------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [tooltips](#interface_tooltips) | boolean | Show tooltips when your mouse is over a tooltip supported button.
This is not hot reloaded. |
+| [nerd_font](#interface_nerd_font) | boolean | Use nerd font for rendering icons instead of weird characters and stuff.
Not properly hot-reloaded. |
+| [use_reactive_layout](#interface_use_reactive_layout) | boolean | Hide certain elements based on the width and height of the terminal. |
+| [show_progress_eta](#interface_show_progress_eta) | boolean | When copying or deleting files, show an ETA for when the action will be completed. |
+| [show_progress_percentage](#interface_show_progress_percentage) | boolean | When copying or deleting files, show a percentage of how much had been completed. |
+| [truncate_progress_file_path](#interface_truncate_progress_file_path) | boolean | When the process container is using file paths, truncate the file path to only view the first and last names of the path. |
+| [show_line_numbers](#interface_show_line_numbers) | boolean | Add line numbers to the left gutter if you are viewing a text file. |
+| [scrolloff](#interface_scrolloff) | integer | The number of files to keep above and below the cursor when moving through the file list. |
+| [show_hidden_files](#interface_show_hidden_files) | boolean | Show hidden files and folders (those starting with a dot on Unix, or explicitly hidden on Windows/MacOS). |
+| [image_viewer](#interface_image_viewer) | object | Settings related to the image viewer used in the preview sidebar |
+| [font_preview](#interface_font_preview) | object | Settings related to the font preview used in the preview sidebar |
+| [allow_tab_nav](#interface_allow_tab_nav) | boolean | Allow navigating the main app screen with \`tab\` and \`shift+tab\` |
+| [append_new_tabs](#interface_append_new_tabs) | boolean | Choose whether or not to append new tabs instead of inserting them.
\`true\` => Append to the end of the tab list.
\`false\` => Insert after the active tab. |
+| [double_click_delay](#interface_double_click_delay) | number | The delay between two consecutive clicks to enter into a directory, or open a file. |
+| [drive_watcher_frequency](#interface_drive_watcher_frequency) | number | How often (in seconds) to check for changes in mounted drives in the sidebar. |
+| [clock](#interface_clock) | object | |
+| [preview_text](#interface_preview_text) | object | |
+| [compact_mode](#interface_compact_mode) | object | |
+| [mime_rules](#interface_mime_rules) | object | Map MIME type patterns to preview types. Uses regex patterns. Valid preview types: text, image, pdf, archive, folder, resvg, font, remime.
-> Use 'remime' if you want a more accurate description from file(1) |
+| [pins_places](#interface_pins_places) | array of enum (of string) | Provider for the pinned places in the side pane. |
+| [pins_bookmarks](#interface_pins_bookmarks) | array of enum (of string) | Provider for the pinned bookmarks in the side pane. |
### 1.1. Property `Rovr Config > interface > tooltips`
@@ -805,6 +811,77 @@ Must be one of:
- "resvg"
- "font"
+### 1.20. Property `Rovr Config > interface > pins_places`
+
+| | |
+| ------------ | --------------------------- |
+| **Type** | `array of enum (of string)` |
+| **Required** | No |
+| **Default** | `["rovr"]` |
+
+**Description:** Provider for the pinned places in the side pane.
+
+| | Array restrictions |
+| -------------------- | ------------------ |
+| **Min items** | 1 |
+| **Max items** | N/A |
+| **Items unicity** | False |
+| **Additional items** | False |
+| **Tuple validation** | See below |
+
+| Each item of this array must be | Description |
+| ------------------------------------------------- | ----------- |
+| [pins_places items](#interface_pins_places_items) | |
+
+#### 1.20.1. Rovr Config > interface > pins_places > pins_places items
+
+| | |
+| ------------ | ------------------ |
+| **Type** | `enum (of string)` |
+| **Required** | No |
+
+Must be one of:
+
+- "empty"
+- "default"
+- "rovr"
+
+### 1.21. Property `Rovr Config > interface > pins_bookmarks`
+
+| | |
+| ------------ | --------------------------- |
+| **Type** | `array of enum (of string)` |
+| **Required** | No |
+| **Default** | `["rovr"]` |
+
+**Description:** Provider for the pinned bookmarks in the side pane.
+
+| | Array restrictions |
+| -------------------- | ------------------ |
+| **Min items** | 1 |
+| **Max items** | N/A |
+| **Items unicity** | False |
+| **Additional items** | False |
+| **Tuple validation** | See below |
+
+| Each item of this array must be | Description |
+| ------------------------------------------------------- | ----------- |
+| [pins_bookmarks items](#interface_pins_bookmarks_items) | |
+
+#### 1.21.1. Rovr Config > interface > pins_bookmarks > pins_bookmarks items
+
+| | |
+| ------------ | ------------------ |
+| **Type** | `enum (of string)` |
+| **Required** | No |
+
+Must be one of:
+
+- "empty"
+- "rovr"
+- "gtk"
+- "kde"
+
## 2. Property `Rovr Config > settings`
| | |
diff --git a/src/rovr/config/schema.json b/src/rovr/config/schema.json
index 41b1ea73..9a84d653 100644
--- a/src/rovr/config/schema.json
+++ b/src/rovr/config/schema.json
@@ -227,6 +227,26 @@
"application/x-font-.*": "font"
},
"description": "Map MIME type patterns to preview types. Uses regex patterns. Valid preview types: text, image, pdf, archive, folder, resvg, font, remime.\n-> Use 'remime' if you want a more accurate description from file(1)"
+ },
+ "pins_places": {
+ "type": "array",
+ "default": ["rovr"],
+ "description": "Provider for the pinned places in the side pane.",
+ "minItems": 1,
+ "items": {
+ "type": "string",
+ "enum": ["empty", "default", "rovr"]
+ }
+ },
+ "pins_bookmarks": {
+ "type": "array",
+ "default": ["rovr"],
+ "description": "Provider for the pinned bookmarks in the side pane.",
+ "minItems": 1,
+ "items": {
+ "type": "string",
+ "enum": ["empty", "rovr", "gtk", "kde"]
+ }
}
}
},
diff --git a/src/rovr/functions/pins.py b/src/rovr/functions/pins.py
index 4a76a322..8431dd3d 100644
--- a/src/rovr/functions/pins.py
+++ b/src/rovr/functions/pins.py
@@ -1,14 +1,33 @@
import json
from copy import deepcopy
from os import makedirs, path
-from typing import NotRequired, TypedDict, cast
+from typing import NotRequired, TypedDict, cast, Protocol
+from pathlib import Path
+import xml.etree.ElementTree as ET
from rovr.variables.maps import RovrVars
+from rovr.variables.constants import config
from .path import dump_exc, normalise
pins = {}
PIN_PATH = path.join(RovrVars.ROVRCONFIG, "pins.json")
+_places_providers = {}
+_bookmarks_providers = {}
+
+def _register(name, bucket):
+ def decorator(cls):
+ bucket[name] = cls
+ return cls
+ return decorator
+
+
+def register_places(name):
+ return _register(name, _places_providers)
+
+
+def register_bookmarks(name):
+ return _register(name, _bookmarks_providers)
class PinItem(TypedDict):
@@ -24,58 +43,34 @@ class PinsDict(TypedDict):
"Other added folders"
-def load_pins() -> PinsDict:
- """
- Load the pinned files from a JSON file in the user's config directory.
- Returns:
- dict: A dictionary with the default values, and the custom added pins.
- Raises:
- ValueError: If the config is of the wrong type
- """
- # I'm not entirely sure why the pins break when
- # pins isn't set global, I can't be bothered for now
- # until an issue gets raised in the future
- global pins
- _pins: PinsDict
+class PinProvider(Protocol):
+ @classmethod
+ def load_pins(cls) -> list[PinItem]: ...
- if not path.exists(PIN_PATH):
- _pins = {
- "default": [
- {"name": "Home", "path": "$HOME"},
- {"name": "Downloads", "path": "$DOWNLOADS"},
- {"name": "Documents", "path": "$DOCUMENTS"},
- {"name": "Desktop", "path": "$DESKTOP"},
- {"name": "Pictures", "path": "$PICTURES"},
- {"name": "Videos", "path": "$VIDEOS"},
- {"name": "Music", "path": "$MUSIC"},
- ],
- "pins": [],
- }
- else:
- try:
- with open(PIN_PATH, "r") as f:
- loaded = json.load(f)
- if not isinstance(loaded, dict):
- raise ValueError()
- _pins = cast(PinsDict, loaded)
- except (IOError, ValueError, json.JSONDecodeError):
- # Reset pins on corrupt or something else happened
- _pins = {
- "default": [
- {"name": "Home", "path": "$HOME"},
- {"name": "Downloads", "path": "$DOWNLOADS"},
- {"name": "Documents", "path": "$DOCUMENTS"},
- {"name": "Desktop", "path": "$DESKTOP"},
- {"name": "Pictures", "path": "$PICTURES"},
- {"name": "Videos", "path": "$VIDEOS"},
- {"name": "Music", "path": "$MUSIC"},
- ],
- "pins": [],
- }
-
- # If list died
- if "default" not in _pins or not isinstance(_pins["default"], list):
- _pins["default"] = [
+ @classmethod
+ def add_pin(cls, pin_name: str, pin_path: str | bytes) -> None: ...
+
+ @classmethod
+ def remove_pin(cls, pin_path: str | bytes) -> None: ...
+
+ @classmethod
+ def toggle_pin(cls, pin_name: str, pin_path: str) -> None: ...
+
+
+@register_bookmarks("empty")
+@register_places("empty")
+class EmptyPinProvider():
+ @classmethod
+ def load_pins(cls) -> list[PinItem]:
+ return []
+
+
+@register_places("default")
+class DefaultPlaces():
+
+ @classmethod
+ def load_pins(cls) -> list[PinItem]:
+ return _sanitize([
{"name": "Home", "path": "$HOME"},
{"name": "Downloads", "path": "$DOWNLOADS"},
{"name": "Documents", "path": "$DOCUMENTS"},
@@ -83,26 +78,114 @@ def load_pins() -> PinsDict:
{"name": "Pictures", "path": "$PICTURES"},
{"name": "Videos", "path": "$VIDEOS"},
{"name": "Music", "path": "$MUSIC"},
- ]
- if "pins" not in _pins or not isinstance(_pins["pins"], list):
- _pins["pins"] = []
+ ])
- for section_key in ["default", "pins"]:
- # again, screw you ty, `section_key` can never be unknown
- # but i dont know how to assert that to you
- for item in _pins[section_key]: # ty: ignore[invalid-key]
- # no i will not use isinstance, ty screams at me
- # because of the replace code a few lines below
- if type(item) is dict and "path" in item and type(item["path"]) is str:
- # Expand variables
- for var in RovrVars.slots:
- item["path"] = item["path"].replace(
- f"${var}", getattr(RovrVars, var)
- )
- # Normalize to forward slashes
- item["path"] = normalise(str(item["path"]))
- pins = _pins
- return _pins
+
+@register_places("rovr")
+class RovrPinedPlaces():
+
+ @classmethod
+ def load_pins(cls) -> list[PinItem]:
+ try:
+ places = []
+ with open(PIN_PATH, "r") as f:
+ loaded_pins = cast(PinsDict, json.load(f))
+ # for place in loaded_pins["default"]:
+ # place["path"]=normalise(_expand_vars(place["path"]))
+ # places.append(place)
+ places = _sanitize(loaded_pins["default"])
+ except (IOError, ValueError, json.decoder.JSONDecodeError):
+ places = DefaultPlaces.load_pins()
+ return places
+
+
+@register_bookmarks("rovr")
+class RovrPinedBookmarks():
+
+ @classmethod
+ def load_pins(cls) -> list[PinItem]:
+ try:
+ bookmarks = []
+ with open(PIN_PATH, "r") as f:
+ loaded_pins = cast(PinsDict, json.load(f))
+ # for bookmark in loaded_pins["pins"]:
+ # bookmark["path"]=normalise(_expand_vars(bookmark["path"]))
+ # bookmarks.append(bookmark)
+ bookmarks = _sanitize(loaded_pins["pins"])
+ except (IOError, ValueError, json.decoder.JSONDecodeError):
+ bookmarks = []
+ return bookmarks
+
+
+@register_bookmarks("gtk")
+class GTKBookmarks():
+ bookmarks_path = "~/.config/gtk-3.0/bookmarks"
+
+ @classmethod
+ def load_pins(cls) -> list[PinItem]:
+ bookmarks = []
+ try:
+ with open(cls.bookmarks_path) as bookmarks_file:
+ for line in bookmarks_file.readlines():
+ try:
+ path, name = line.strip().split(" ", 1)
+ bookmarks.append({"name": name, "path": str(Path.from_uri(path))})
+ except ValueError:
+ pass
+ except OSError:
+ pass
+ return bookmarks
+
+
+@register_bookmarks("kde")
+class KDEBookmarks():
+ bookmarks_path = "~/.local/share/kfile/bookmarks.xml"
+
+ @classmethod
+ def load_pins(cls) -> list[PinItem]:
+ bookmarks = []
+ try:
+ root = ET.parse(cls.bookmarks_path).getroot()
+ for elem in root.iter("bookmark"):
+ try:
+ title_elem = elem.find("title")
+ name = title_elem.text.strip()
+ path = str(Path.from_uri(elem.get("href", "").strip()))
+ bookmarks.append({"name": name, "path": path})
+ except ValueError:
+ pass
+ except OSError:
+ pass
+
+ return bookmarks
+
+def _expand_vars(path: str) -> str:
+ for var in RovrVars.slots:
+ path = path.replace(f"${var}", getattr(RovrVars, var))
+ return path
+
+
+def _sanitize(pins: list[PinItem]) -> list[PinItem]:
+ out = []
+ for pin in pins:
+ pin["path"] = normalise(_expand_vars(pin["path"]))
+ out.append(pin)
+ return out
+
+
+def load_pins() -> PinsDict:
+ _places = []
+ _bookmarks = []
+
+ for provider in config["interface"]["pins_places"]:
+ _places.extend(_places_providers[provider].load_pins())
+ for provider in config["interface"]["pins_bookmarks"]:
+ _bookmarks.extend(_bookmarks_providers[provider].load_pins())
+
+ return {
+ "default": _places,
+ "pins": _bookmarks,
+ }
def add_pin(pin_name: str, pin_path: str | bytes) -> None: