diff --git a/.gitignore b/.gitignore
index 1f2408c..6c9cc9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ config/json/config.json
config/json/dock.json
config/json/accounts.json
config/json/passwords.json
+config/json/bookmarks.json
config/hypr/modus.conf
config/hypr/colors.conf
styles/colors.css
diff --git a/modules/launcher/plugin_manager.py b/modules/launcher/plugin_manager.py
index 7260565..4e2794d 100644
--- a/modules/launcher/plugin_manager.py
+++ b/modules/launcher/plugin_manager.py
@@ -95,7 +95,8 @@ class PluginManager:
"bluetooth",
"otp",
"notifications",
- "password"
+ "password",
+ "bookmarks"
]
for plugin_name in default_plugins:
diff --git a/modules/launcher/plugins/bookmarks.py b/modules/launcher/plugins/bookmarks.py
new file mode 100644
index 0000000..28a8605
--- /dev/null
+++ b/modules/launcher/plugins/bookmarks.py
@@ -0,0 +1,540 @@
+import json
+import os
+import subprocess
+import threading
+import time
+from pathlib import Path
+from typing import Dict, List, Optional
+from urllib.parse import urlparse
+
+from thefuzz import fuzz
+from fabric.utils.helpers import get_relative_path
+
+import utils.icons as icons
+from modules.launcher.plugin_base import PluginBase
+from modules.launcher.result import Result
+
+
+class BookmarkManager:
+ """Manages user's custom bookmarks."""
+
+ def __init__(self, storage_file: Path):
+ self.storage_file = storage_file
+ self.bookmarks: List[Dict] = []
+ self.cache_lock = threading.Lock()
+ self.last_loaded = 0
+ self.cache_ttl = 30 # Cache for 30 seconds
+ self._load_bookmarks()
+
+ def _get_favicon_url(self, url: str) -> str:
+ """Generate favicon URL for a given website URL."""
+ try:
+ parsed = urlparse(url)
+ return f"{parsed.scheme}://{parsed.netloc}/favicon.ico"
+ except:
+ return ""
+
+ def _extract_domain(self, url: str) -> str:
+ """Extract domain from URL."""
+ try:
+ parsed = urlparse(url)
+ domain = parsed.netloc
+ # Remove www. prefix
+ if domain.startswith("www."):
+ domain = domain[4:]
+ return domain
+ except:
+ return url
+
+ def _normalize_url(self, url: str) -> str:
+ """Normalize URL by adding protocol if missing."""
+ url = url.strip()
+ if not url.startswith(("http://", "https://")):
+ if url.startswith("www."):
+ url = "https://" + url
+ else:
+ url = "https://" + url
+ return url
+
+ def _load_bookmarks(self):
+ """Load bookmarks from JSON file with caching."""
+ with self.cache_lock:
+ current_time = time.time()
+
+ # Check if cache is still valid
+ if (current_time - self.last_loaded) < self.cache_ttl and self.bookmarks:
+ return
+
+ try:
+ if self.storage_file.exists():
+ with open(self.storage_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ self.bookmarks = data.get("bookmarks", [])
+ else:
+ self.bookmarks = []
+
+ self.last_loaded = current_time
+ except Exception as e:
+ print(f"Error loading bookmarks: {e}")
+ self.bookmarks = []
+
+ def get_bookmarks(self) -> List[Dict]:
+ """Get bookmarks, loading from file if needed."""
+ self._load_bookmarks()
+ return self.bookmarks
+
+
+
+ def _save_bookmarks_unlocked(self):
+ """Save bookmarks to JSON file without acquiring lock."""
+ try:
+ self.storage_file.parent.mkdir(parents=True, exist_ok=True)
+ data = {
+ "bookmarks": self.bookmarks,
+ "last_updated": time.time(),
+ }
+ with open(self.storage_file, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+
+ # Update cache timestamp
+ self.last_loaded = time.time()
+ except Exception as e:
+ print(f"Error saving bookmarks: {e}")
+
+ def _save_bookmarks(self):
+ """Save bookmarks to JSON file."""
+ with self.cache_lock:
+ self._save_bookmarks_unlocked()
+
+ def add_bookmark(self, title: str, url: str, description: str = "", tags: List[str] = None) -> bool:
+ """Add a new bookmark."""
+ try:
+ url = self._normalize_url(url)
+
+ # Check if bookmark already exists
+ for bookmark in self.bookmarks:
+ if bookmark["url"] == url:
+ return False # Already exists
+
+ new_bookmark = {
+ "title": title.strip(),
+ "url": url,
+ "description": description.strip(),
+ "tags": tags or [],
+ "created": time.time(),
+ "accessed": 0
+ }
+
+ self.bookmarks.append(new_bookmark)
+ self._save_bookmarks()
+
+ # Clear cache to force reload
+ self.last_loaded = 0
+
+ return True
+
+ except Exception as e:
+ print(f"Error adding bookmark: {e}")
+ return False
+
+ def remove_bookmark(self, identifier: str) -> bool:
+ """Remove a bookmark by title or URL."""
+ try:
+ identifier = identifier.lower().strip()
+
+ for i, bookmark in enumerate(self.bookmarks):
+ if (bookmark["title"].lower() == identifier or
+ bookmark["url"].lower() == identifier or
+ self._extract_domain(bookmark["url"]).lower() == identifier):
+
+ self.bookmarks.pop(i)
+ self._save_bookmarks()
+
+ # Clear cache to force reload
+ self.last_loaded = 0
+
+ return True
+
+ return False
+
+ except Exception as e:
+ print(f"Error removing bookmark: {e}")
+ return False
+
+ def update_access_time(self, url: str):
+ """Update the last accessed time for a bookmark."""
+ try:
+ for bookmark in self.bookmarks:
+ if bookmark["url"] == url:
+ bookmark["accessed"] = time.time()
+ self._save_bookmarks()
+ break
+ except Exception as e:
+ print(f"Error updating access time: {e}")
+
+ def get_bookmark_count(self) -> int:
+ """Get total number of bookmarks."""
+ return len(self.get_bookmarks())
+
+
+class BookmarksPlugin(PluginBase):
+ """
+ User bookmarks plugin for the launcher.
+ Allows users to add, remove, and search their own bookmarks.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.display_name = "Bookmarks"
+ self.description = "Manage and search your personal bookmarks"
+
+ # Initialize bookmark manager with storage file
+ self.bookmark_file = Path(get_relative_path("../../../config/json/bookmarks.json"))
+ self.bookmark_manager = BookmarkManager(self.bookmark_file)
+ self.max_results = 15
+
+ def initialize(self):
+ """Initialize the bookmarks plugin."""
+ self.set_triggers(["bookmark", "bm"])
+
+ def cleanup(self):
+ """Cleanup the bookmarks plugin."""
+ pass
+
+ def query(self, query_string: str) -> List[Result]:
+ """Process bookmark queries with add/remove commands."""
+ query = query_string.strip()
+
+ if not query:
+ # Show recent/popular bookmarks when no query
+ return self._get_recent_bookmarks()
+
+ # Handle add command
+ if query.startswith("add "):
+ return self._handle_add_command(query[4:].strip())
+
+ # Handle remove command
+ if query.startswith(("remove ", "delete ", "rm ")):
+ command_parts = query.split(" ", 1)
+ if len(command_parts) > 1:
+ return self._handle_remove_command(command_parts[1].strip())
+ else:
+ return self._show_remove_help()
+
+ # Search bookmarks
+ return self._search_bookmarks(query.lower())
+
+ def _search_bookmarks(self, query: str) -> List[Result]:
+ """Search through bookmarks."""
+ bookmarks = self.bookmark_manager.get_bookmarks()
+
+ if not bookmarks:
+ return [
+ Result(
+ title="No Bookmarks Found",
+ subtitle="Use 'add
' to add your first bookmark",
+ icon_markup=icons.info,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "info", "keep_launcher_open": True}
+ )
+ ]
+
+ # Search bookmarks
+ results = []
+ for bookmark in bookmarks:
+ relevance = self._calculate_relevance(bookmark, query)
+ if relevance > 0.3: # Only show relevant results
+ result = self._create_bookmark_result(bookmark, relevance)
+ if result:
+ results.append(result)
+
+ # Sort by relevance and limit results
+ results.sort(key=lambda r: r.relevance, reverse=True)
+ return results[:self.max_results]
+
+ def _get_recent_bookmarks(self) -> List[Result]:
+ """Get recent/popular bookmarks when no query is provided."""
+ bookmarks = self.bookmark_manager.get_bookmarks()
+
+ if not bookmarks:
+ return [
+ Result(
+ title="No Bookmarks Yet",
+ subtitle="Use 'add ' to add your first bookmark",
+ icon_markup=icons.info,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "help", "keep_launcher_open": True}
+ ),
+ Result(
+ title="Example: Add Google",
+ subtitle="add Google https://google.com",
+ icon_markup=icons.bulb,
+ action=lambda: None,
+ relevance=0.9,
+ plugin_name=self.display_name,
+ data={"type": "example", "keep_launcher_open": True}
+ )
+ ]
+
+ # Sort by access time (most recent first) and show top 10
+ sorted_bookmarks = sorted(bookmarks, key=lambda b: b.get("accessed", 0), reverse=True)
+ results = []
+ for bookmark in sorted_bookmarks[:10]:
+ result = self._create_bookmark_result(bookmark, 0.8)
+ if result:
+ results.append(result)
+
+ return results
+
+ def _handle_add_command(self, args: str) -> List[Result]:
+ """Handle add bookmark command."""
+ if not args:
+ return [
+ Result(
+ title="Add Bookmark",
+ subtitle="Usage: add [description]",
+ icon_markup=icons.info,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "help", "keep_launcher_open": True}
+ ),
+ Result(
+ title="Example",
+ subtitle="add Google https://google.com Search engine",
+ icon_markup=icons.bulb,
+ action=lambda: None,
+ relevance=0.9,
+ plugin_name=self.display_name,
+ data={"type": "example", "keep_launcher_open": True}
+ )
+ ]
+
+ # Parse arguments: title url [description]
+ parts = args.split()
+ if len(parts) < 2:
+ return [
+ Result(
+ title="Invalid Format",
+ subtitle="Usage: add [description]",
+ icon_markup=icons.alert,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "error", "keep_launcher_open": True}
+ )
+ ]
+
+ title = parts[0]
+ url = parts[1]
+ description = " ".join(parts[2:]) if len(parts) > 2 else ""
+
+ # Add the bookmark
+ success = self.bookmark_manager.add_bookmark(title, url, description)
+
+ if success:
+ return [
+ Result(
+ title=f"✓ Added '{title}'",
+ subtitle=f"Bookmark saved: {url}",
+ icon_markup=icons.check,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "success"}
+ )
+ ]
+ else:
+ return [
+ Result(
+ title="Bookmark Already Exists",
+ subtitle=f"A bookmark with URL '{url}' already exists",
+ icon_markup=icons.alert,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "error", "keep_launcher_open": True}
+ )
+ ]
+
+ def _handle_remove_command(self, identifier: str) -> List[Result]:
+ """Handle remove bookmark command."""
+ if not identifier:
+ return self._show_remove_help()
+
+ success = self.bookmark_manager.remove_bookmark(identifier)
+
+ if success:
+ return [
+ Result(
+ title=f"✓ Removed Bookmark",
+ subtitle=f"Bookmark '{identifier}' has been removed",
+ icon_markup=icons.check,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "success"}
+ )
+ ]
+ else:
+ return [
+ Result(
+ title="Bookmark Not Found",
+ subtitle=f"No bookmark found matching '{identifier}'",
+ icon_markup=icons.alert,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "error", "keep_launcher_open": True}
+ )
+ ]
+
+ def _show_remove_help(self) -> List[Result]:
+ """Show help for remove command."""
+ bookmarks = self.bookmark_manager.get_bookmarks()
+ results = [
+ Result(
+ title="Remove Bookmark",
+ subtitle="Usage: remove ",
+ icon_markup=icons.info,
+ action=lambda: None,
+ relevance=1.0,
+ plugin_name=self.display_name,
+ data={"type": "help", "keep_launcher_open": True}
+ )
+ ]
+
+ # Show available bookmarks to remove
+ if bookmarks:
+ results.append(
+ Result(
+ title="Available Bookmarks:",
+ subtitle=f"{len(bookmarks)} bookmarks available to remove",
+ icon_markup=icons.bookmark,
+ action=lambda: None,
+ relevance=0.9,
+ plugin_name=self.display_name,
+ data={"type": "info", "keep_launcher_open": True}
+ )
+ )
+
+ # Show first few bookmarks as examples
+ for bookmark in bookmarks[:3]:
+ title = bookmark.get("title", "Untitled")
+ domain = self._extract_domain(bookmark.get("url", ""))
+ results.append(
+ Result(
+ title=f"remove {title}",
+ subtitle=f"Remove: {title} ({domain})",
+ icon_markup=icons.trash,
+ action=lambda t=title: self.bookmark_manager.remove_bookmark(t),
+ relevance=0.8,
+ plugin_name=self.display_name,
+ data={"type": "remove_option", "bookmark": bookmark}
+ )
+ )
+
+ return results
+
+ def _calculate_relevance(self, bookmark: Dict, query: str) -> float:
+ """Calculate relevance score for a bookmark."""
+ title = bookmark.get("title", "").lower()
+ url = bookmark.get("url", "").lower()
+ description = bookmark.get("description", "").lower()
+
+ # Exact title match
+ if query == title:
+ return 1.0
+
+ # Title starts with query
+ if title.startswith(query):
+ return 0.95
+
+ # Query in title
+ if query in title:
+ position = title.index(query)
+ position_score = 1.0 - (position / len(title))
+ return 0.8 + (position_score * 0.1)
+
+ # Query in URL
+ if query in url:
+ return 0.7
+
+ # Query in description
+ if query in description:
+ return 0.6
+
+ # Fuzzy matching for title
+ if len(query) >= 3:
+ fuzzy_score = fuzz.partial_ratio(query, title) / 100.0
+ if fuzzy_score >= 0.7:
+ return fuzzy_score * 0.6
+
+ return 0.0
+
+ def _create_bookmark_result(self, bookmark: Dict, relevance: float) -> Optional[Result]:
+ """Create a Result object for a bookmark."""
+ try:
+ title = bookmark.get("title", "Untitled")
+ url = bookmark.get("url", "")
+ description = bookmark.get("description", "")
+
+ # Truncate long titles
+ if len(title) > 60:
+ title = title[:57] + "..."
+
+ # Create subtitle with domain and description
+ domain = self._extract_domain(url)
+ if description:
+ subtitle = f"{domain} • {description}"
+ else:
+ subtitle = domain
+
+ return Result(
+ title=title,
+ subtitle=subtitle,
+ icon_markup=icons.bookmark,
+ action=lambda u=url: self._open_bookmark(u),
+ relevance=relevance,
+ plugin_name=self.display_name,
+ data={
+ "type": "bookmark",
+ "url": url,
+ "domain": domain,
+ "description": description
+ }
+ )
+ except Exception as e:
+ print(f"Error creating bookmark result: {e}")
+ return None
+
+ def _extract_domain(self, url: str) -> str:
+ """Extract domain from URL."""
+ try:
+ parsed = urlparse(url)
+ domain = parsed.netloc
+ # Remove www. prefix
+ if domain.startswith("www."):
+ domain = domain[4:]
+ return domain
+ except:
+ return url
+
+ def _open_bookmark(self, url: str):
+ """Open bookmark URL in default browser and update access time."""
+ try:
+ # Update access time
+ self.bookmark_manager.update_access_time(url)
+
+ # Open URL
+ subprocess.Popen(
+ ["xdg-open", url],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+ except Exception as e:
+ print(f"Failed to open bookmark: {e}")
diff --git a/modules/launcher/plugins/password.py b/modules/launcher/plugins/password.py
index 1a69d51..7b03a65 100644
--- a/modules/launcher/plugins/password.py
+++ b/modules/launcher/plugins/password.py
@@ -486,8 +486,6 @@ class PasswordPlugin(PluginBase):
success = self.password_manager.add_password(name, password, description)
if success:
action_word = "updated" if update else "added"
- print(f"Password '{name}' {action_word} successfully")
-
# Clear cache to force refresh
self._results_cache.clear()
@@ -512,7 +510,6 @@ class PasswordPlugin(PluginBase):
try:
success = self.password_manager.remove_password(name)
if success:
- print(f"Password '{name}' removed successfully")
# Remove from revealed passwords if present
self.revealed_passwords.pop(name, None)
@@ -560,8 +557,6 @@ class PasswordPlugin(PluginBase):
# Reveal password temporarily
self.revealed_passwords[name] = password
- print(f"Password for '{name}' copied to clipboard")
-
# Send notification if available (non-blocking)
try:
subprocess.Popen(
@@ -607,7 +602,6 @@ class PasswordPlugin(PluginBase):
self.revealed_passwords.clear()
# Clear cache to force refresh with hidden passwords
self._results_cache.clear()
- print("🔒 All passwords hidden")
def _setup_launcher_hooks(self):
"""Setup hooks to monitor launcher state."""
@@ -626,7 +620,6 @@ class PasswordPlugin(PluginBase):
self._original_close_launcher = obj.close_launcher
# Replace with our wrapper
obj.close_launcher = self._wrapped_close_launcher
- print("🔗 Password plugin hooked into launcher close event")
break
except Exception as e:
print(f"Warning: Could not setup launcher hooks: {e}")
@@ -638,7 +631,6 @@ class PasswordPlugin(PluginBase):
# Restore original close_launcher method
self._launcher_instance.close_launcher = self._original_close_launcher
self._launcher_instance = None
- print("🔗 Password plugin hooks cleaned up")
except Exception as e:
print(f"Warning: Could not cleanup launcher hooks: {e}")
@@ -656,7 +648,6 @@ class PasswordPlugin(PluginBase):
# Hide password
self.revealed_passwords.pop(name, None)
self._results_cache.clear()
- print(f"🔒 Password for '{name}' hidden (Shift+Enter)")
else:
# Reveal password
try:
@@ -666,7 +657,6 @@ class PasswordPlugin(PluginBase):
if password:
self.revealed_passwords[name] = password
self._results_cache.clear()
- print(f"🔓 Password for '{name}' revealed (Shift+Enter)")
else:
print(f"Failed to retrieve password for '{name}'")
except Exception as e:
diff --git a/modules/launcher/plugins/pomodoro.py b/modules/launcher/plugins/pomodoro.py
index d8d4e44..7c2a121 100644
--- a/modules/launcher/plugins/pomodoro.py
+++ b/modules/launcher/plugins/pomodoro.py
@@ -230,9 +230,7 @@ class PomodoroTimer:
else:
period_type = f"Work (Cycle {self.current_cycle})"
- self._cached_status = f"{period_type} - {remaining_minutes:02d}:{
- remaining_seconds:02d
- } | Total: {self.total_cycles_completed}"
+ self._cached_status = f"{period_type} - {remaining_minutes:02d}:{remaining_seconds:02d} | Total: {self.total_cycles_completed}"
self._last_status_update = current_time
def get_status(self):
@@ -357,11 +355,7 @@ class PomodoroPlugin(PluginBase):
results.append(
Result(
title="Start Pomodoro",
- subtitle=f"{self.DEFAULT_POMODORO_DURATION} min work, {
- self.DEFAULT_BREAK_DURATION
- } min break, {
- self.DEFAULT_LONG_BREAK_DURATION
- } min long break, {self.DEFAULT_POMODORO_COUNT} cycles",
+ subtitle=f"{self.DEFAULT_POMODORO_DURATION} min work, {self.DEFAULT_BREAK_DURATION} min break, {self.DEFAULT_LONG_BREAK_DURATION} min long break, {self.DEFAULT_POMODORO_COUNT} cycles",
icon_markup=icons.timer_on,
action=start_default_and_refresh,
relevance=1.0,
@@ -416,9 +410,7 @@ class PomodoroPlugin(PluginBase):
results.append(
Result(
title="Start Pomodoro",
- subtitle=f"{p} min work, {b} min break, {
- lb
- } min long break, {c} cycles",
+ subtitle=f"{p} min work, {b} min break, {lb} min long break, {c} cycles",
icon_markup=icons.timer_on,
action=start_custom_and_refresh,
relevance=1.0,
diff --git a/utils/icons.py b/utils/icons.py
index a3a77a4..f64b9de 100644
--- a/utils/icons.py
+++ b/utils/icons.py
@@ -183,6 +183,8 @@ calendar: str = ""
connect: str = ""
disconnect: str = "ﴖ"
auth: str = ""
+bookmark: str = ""
+star: str = ""
# otp