378 lines
12 KiB
Python
378 lines
12 KiB
Python
from collections.abc import Iterator
|
|
|
|
from fabric.utils import Gdk, idle_add, remove_handler
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.button import Button
|
|
from fabric.widgets.label import Label
|
|
|
|
from modules.launcher.base import LauncherPlugin
|
|
from services import BookmarkManager
|
|
from utils.functions import copy_text
|
|
|
|
|
|
class BookmarkPlugin(LauncherPlugin):
|
|
@property
|
|
def name(self) -> str:
|
|
return "bookmarks"
|
|
|
|
@property
|
|
def icon(self) -> str:
|
|
return "user-bookmarks-symbolic"
|
|
|
|
@property
|
|
def keywords(self) -> list[str]:
|
|
return ["bm"]
|
|
|
|
def __init__(self, handler):
|
|
super().__init__(handler)
|
|
self._query_handler: int = 0
|
|
self.current_slots: list = []
|
|
self.bookmarks: dict = {}
|
|
self.bookmark_manager = BookmarkManager()
|
|
|
|
def on_search(self, text: str) -> None:
|
|
self.query_bookmarks(text)
|
|
|
|
def stop(self):
|
|
if self._query_handler:
|
|
remove_handler(self._query_handler)
|
|
self._query_handler = 0
|
|
self.current_slots.clear()
|
|
|
|
def query_bookmarks(self, text: str) -> None:
|
|
self.handler.start("bookmarks")
|
|
self.current_slots.clear()
|
|
text_stripped = text.strip()
|
|
|
|
commands = {
|
|
"add": self._handle_add_command,
|
|
"remove": self._handle_remove_command,
|
|
"update": self._handle_update_command,
|
|
"help": lambda t: self._show_command_help(),
|
|
}
|
|
|
|
for prefix, handler in commands.items():
|
|
if text_stripped.startswith(prefix):
|
|
handler(text)
|
|
return
|
|
|
|
self._load_and_display_bookmarks(text)
|
|
|
|
def _load_and_display_bookmarks(self, text: str) -> None:
|
|
result = self.bookmark_manager.list_bookmarks()
|
|
if not result["success"]:
|
|
self._show_error_message(f"Failed to load bookmarks: {result['error']}")
|
|
return
|
|
|
|
self.bookmarks = result["data"]
|
|
|
|
if not self.bookmarks:
|
|
self._show_no_bookmarks()
|
|
return
|
|
|
|
filtered_bookmarks = self._filter_bookmarks(text)
|
|
if not filtered_bookmarks:
|
|
self._show_no_results()
|
|
return
|
|
|
|
self._query_handler = idle_add(
|
|
self._bake_next_bookmark_slot, iter(filtered_bookmarks), pin=True
|
|
)
|
|
|
|
def _filter_bookmarks(self, text: str) -> Iterator[tuple[str, dict]]:
|
|
if not text.strip():
|
|
return iter(self.bookmarks.items())
|
|
|
|
text_lower = text.lower()
|
|
return iter(
|
|
(name, data)
|
|
for name, data in self.bookmarks.items()
|
|
if (
|
|
text_lower in name.lower()
|
|
or text_lower in data.get("content", "").lower()
|
|
)
|
|
)
|
|
|
|
def _show_no_bookmarks(self) -> None:
|
|
self.handler.slot_ready(
|
|
Button(
|
|
style_classes="app-slot bookmark-no-bookmarks",
|
|
child=Box(
|
|
orientation="h",
|
|
spacing=12,
|
|
children=[
|
|
Label(
|
|
label="No bookmarks found",
|
|
style_classes="bookmark-no-bookmarks-text",
|
|
h_align="center",
|
|
),
|
|
],
|
|
),
|
|
tooltip_text="Use 'add name content' to add bookmarks",
|
|
),
|
|
"bookmarks",
|
|
)
|
|
self.handler.done()
|
|
|
|
def _show_no_results(self) -> None:
|
|
self.handler.slot_ready(
|
|
Button(
|
|
style_classes="app-slot bookmark-no-results",
|
|
child=Box(
|
|
orientation="h",
|
|
spacing=12,
|
|
children=[
|
|
Label(
|
|
label="No bookmarks match your search",
|
|
style_classes="bookmark-no-results-text",
|
|
h_align="center",
|
|
),
|
|
],
|
|
),
|
|
),
|
|
"bookmarks",
|
|
)
|
|
self.handler.done()
|
|
|
|
def _bake_next_bookmark_slot(self, iterator: Iterator[tuple[str, dict]]) -> bool:
|
|
try:
|
|
bookmark_name, bookmark_data = next(iterator)
|
|
except StopIteration:
|
|
idle_add(self.handler.done)
|
|
return False
|
|
|
|
self._create_bookmark_slot(bookmark_name, bookmark_data)
|
|
return True
|
|
|
|
def _create_bookmark_slot(self, bookmark_name: str, bookmark_data: dict) -> None:
|
|
content = bookmark_data.get("content", "")
|
|
|
|
name_label = Label(
|
|
label=bookmark_name,
|
|
style_classes="bookmark-name",
|
|
h_align="start",
|
|
max_chars_width=25,
|
|
ellipsization="end",
|
|
)
|
|
|
|
content_label = Label(
|
|
label=content if content else "No content",
|
|
style_classes="bookmark-content",
|
|
h_align="start",
|
|
max_chars_width=40,
|
|
ellipsization="end",
|
|
)
|
|
|
|
slot_widget = Button(
|
|
style_classes="app-slot bookmark-slot",
|
|
child=Box(
|
|
orientation="v",
|
|
spacing=4,
|
|
children=[
|
|
name_label,
|
|
content_label,
|
|
],
|
|
),
|
|
tooltip_text=f"Click to copy content\nContent: {content}",
|
|
on_clicked=lambda *_: self._copy_bookmark(bookmark_name),
|
|
)
|
|
|
|
slot_widget.connect(
|
|
"key-press-event",
|
|
lambda widget, event, bm_name=bookmark_name: self._on_slot_key_press(
|
|
widget, event, bm_name
|
|
),
|
|
)
|
|
|
|
slot_info = {
|
|
"widget": slot_widget,
|
|
"name_label": name_label,
|
|
"content_label": content_label,
|
|
"bookmark_name": bookmark_name,
|
|
"bookmark_data": bookmark_data,
|
|
}
|
|
self.current_slots.append(slot_info)
|
|
self.handler.slot_ready(slot_widget, "bookmarks")
|
|
|
|
def _copy_bookmark(self, bookmark_name: str) -> None:
|
|
"""Copy bookmark content to clipboard"""
|
|
result = self.bookmark_manager.get_bookmark(bookmark_name)
|
|
if not result["success"]:
|
|
self._show_error_message(f"Failed to get bookmark: {result['error']}")
|
|
return
|
|
|
|
content = result["data"]["content"]
|
|
self.handler.launched()
|
|
|
|
if not copy_text(content):
|
|
self._show_error_message("Failed to copy content to clipboard")
|
|
|
|
def _on_slot_key_press(self, widget, event, bookmark_name: str) -> bool:
|
|
if not event:
|
|
return False
|
|
return (
|
|
(event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter)
|
|
and event.state & Gdk.ModifierType.SHIFT_MASK
|
|
and (self._handle_remove_command(f"remove {bookmark_name}") or True)
|
|
)
|
|
|
|
def _handle_add_command(self, text: str) -> None:
|
|
if text.strip() == "add":
|
|
self._load_and_display_bookmarks("")
|
|
return
|
|
|
|
if not text.strip().endswith(";"):
|
|
self._show_error_message(
|
|
"Bookmark command must end with ';'\n\nUsage: add name content;"
|
|
)
|
|
return
|
|
|
|
command_text = text.strip()[:-1]
|
|
parts = command_text.split()
|
|
if len(parts) < 3:
|
|
self._show_command_help("add")
|
|
return
|
|
|
|
name = parts[1]
|
|
content = parts[2]
|
|
result = self.bookmark_manager.add_bookmark(name, content)
|
|
if result["success"]:
|
|
if hasattr(self.handler, "clear_input"):
|
|
self.handler.clear_input()
|
|
self._refresh_bookmark_list()
|
|
else:
|
|
self._show_error_message(f"Error adding bookmark: {result['error']}")
|
|
|
|
def _handle_remove_command(self, text: str) -> None:
|
|
if text.strip() == "remove":
|
|
self._load_and_display_bookmarks("")
|
|
return
|
|
|
|
if not text.strip().endswith(";"):
|
|
self._show_error_message(
|
|
"Bookmark command must end with ';'\n\nUsage: remove name;"
|
|
)
|
|
return
|
|
|
|
command_text = text.strip()[:-1]
|
|
parts = command_text.split()
|
|
if len(parts) < 2:
|
|
self._show_command_help("remove")
|
|
return
|
|
|
|
bookmark_name = parts[1]
|
|
result = self.bookmark_manager.delete_bookmark(bookmark_name)
|
|
if result["success"]:
|
|
if hasattr(self.handler, "clear_input"):
|
|
self.handler.clear_input()
|
|
self._refresh_bookmark_list()
|
|
else:
|
|
self._show_error_message(f"Error removing bookmark: {result['error']}")
|
|
|
|
def _handle_update_command(self, text: str) -> None:
|
|
if text.strip() == "update":
|
|
self._load_and_display_bookmarks("")
|
|
return
|
|
|
|
if not text.strip().endswith(";"):
|
|
self._show_error_message(
|
|
"Bookmark command must end with ';'\n\nUsage: update name content new_value;"
|
|
)
|
|
return
|
|
|
|
command_text = text.strip()[:-1]
|
|
parts = command_text.split()
|
|
if len(parts) < 4:
|
|
self._show_command_help("update")
|
|
return
|
|
|
|
bookmark_name = parts[1]
|
|
field = parts[2]
|
|
if field != "content":
|
|
self._show_error_message("Invalid field. Only 'content' is supported")
|
|
return
|
|
|
|
new_value = " ".join(parts[3:])
|
|
|
|
result = self.bookmark_manager.update_bookmark(bookmark_name, content=new_value)
|
|
if result["success"]:
|
|
if hasattr(self.handler, "clear_input"):
|
|
self.handler.clear_input()
|
|
self._refresh_bookmark_list()
|
|
else:
|
|
self._show_error_message(f"Error updating bookmark: {result['error']}")
|
|
|
|
def _show_command_help(self, command: str = None) -> None:
|
|
help_texts = {
|
|
"add": "Usage: add name content;\nExample: add github https://github.com;\nContent can be URLs, file paths, commands, or any text!",
|
|
"remove": "Usage: remove name;\nExample: remove github;",
|
|
"update": "Usage: update name content new_value;\nExample: update github content https://github.com/new;",
|
|
}
|
|
|
|
help_text = help_texts.get(
|
|
command,
|
|
"""Available commands:
|
|
• add name content; (Add new bookmark)
|
|
• remove name; (Remove bookmark)
|
|
• update name content new_value; (Update bookmark)
|
|
• help (Show this help)
|
|
|
|
Commands with arguments must end with semicolon (;)
|
|
Content can be URLs, file paths, commands, or any text!
|
|
Click on any bookmark to copy it to clipboard!""",
|
|
)
|
|
|
|
self.handler.slot_ready(
|
|
Button(
|
|
style_classes="app-slot bookmark-help",
|
|
child=Box(
|
|
orientation="h",
|
|
spacing=12,
|
|
children=[
|
|
Label(
|
|
label=help_text,
|
|
style_classes="bookmark-help-text",
|
|
h_align="center",
|
|
),
|
|
],
|
|
),
|
|
),
|
|
"bookmarks",
|
|
)
|
|
self.handler.done()
|
|
|
|
def _show_error_message(self, message: str) -> None:
|
|
self.handler.slot_ready(
|
|
Button(
|
|
style_classes="app-slot bookmark-error",
|
|
child=Box(
|
|
orientation="h",
|
|
spacing=12,
|
|
children=[
|
|
Label(
|
|
label=message,
|
|
style_classes="bookmark-error-text",
|
|
h_align="center",
|
|
),
|
|
],
|
|
),
|
|
on_clicked=lambda *_: self.handler.launched(),
|
|
),
|
|
"bookmarks",
|
|
)
|
|
self.handler.done()
|
|
|
|
def _refresh_bookmark_list(self) -> None:
|
|
self.current_slots.clear()
|
|
if hasattr(self.handler, "clear_viewport"):
|
|
self.handler.clear_viewport()
|
|
idle_add(self._show_bookmark_list)
|
|
|
|
def _show_bookmark_list(self) -> None:
|
|
self.handler.start("bookmarks")
|
|
self._load_and_display_bookmarks("")
|
|
|
|
def handle_external(self, command: str, args: str) -> None:
|
|
# Map external commands if needed
|
|
pass
|