Files
2026-07-12 18:56:10 +05:45

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