204 files changed
+51505
No files matched your search
@@ -0,0 +1,690 @@
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fabric.utils import get_relative_path
|
||||
from modules.launcher.plugin_base import PluginBase
|
||||
from modules.launcher.result import Result
|
||||
|
||||
|
||||
class PasswordManager:
|
||||
"""Simple password manager with basic encryption and caching."""
|
||||
|
||||
def __init__(self, storage_file: Path):
|
||||
self.storage_file = storage_file
|
||||
self.passwords: Dict[str, Dict] = {}
|
||||
self._cache_lock = threading.Lock()
|
||||
self._last_loaded = 0
|
||||
self._cache_ttl = 30 # Cache for 30 seconds
|
||||
self._load_passwords()
|
||||
|
||||
def _simple_encrypt(self, text: str, key: str = "modus_pass") -> str:
|
||||
"""Simple encryption using XOR with base64 encoding."""
|
||||
key_bytes = key.encode("utf-8")
|
||||
text_bytes = text.encode("utf-8")
|
||||
|
||||
# XOR encryption
|
||||
encrypted = bytearray()
|
||||
for i, byte in enumerate(text_bytes):
|
||||
encrypted.append(byte ^ key_bytes[i % len(key_bytes)])
|
||||
|
||||
# Base64 encode
|
||||
return base64.b64encode(encrypted).decode("utf-8")
|
||||
|
||||
def _simple_decrypt(self, encrypted_text: str, key: str = "modus_pass") -> str:
|
||||
"""Simple decryption using XOR with base64 decoding."""
|
||||
try:
|
||||
key_bytes = key.encode("utf-8")
|
||||
|
||||
# Base64 decode
|
||||
encrypted_bytes = base64.b64decode(encrypted_text.encode("utf-8"))
|
||||
|
||||
# XOR decryption
|
||||
decrypted = bytearray()
|
||||
for i, byte in enumerate(encrypted_bytes):
|
||||
decrypted.append(byte ^ key_bytes[i % len(key_bytes)])
|
||||
|
||||
return decrypted.decode("utf-8")
|
||||
except Exception:
|
||||
return encrypted_text # Return as-is if decryption fails
|
||||
|
||||
def _load_passwords(self):
|
||||
"""Load passwords 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.passwords:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.storage_file.exists():
|
||||
with open(self.storage_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
self.passwords = data.get("passwords", {})
|
||||
else:
|
||||
self.passwords = {}
|
||||
|
||||
self._last_loaded = current_time
|
||||
except Exception as e:
|
||||
print(f"Error loading passwords: {e}")
|
||||
self.passwords = {}
|
||||
|
||||
def _save_passwords(self):
|
||||
"""Save passwords to JSON file."""
|
||||
with self._cache_lock:
|
||||
try:
|
||||
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = {
|
||||
"passwords": self.passwords,
|
||||
"last_modified": datetime.now().isoformat(),
|
||||
}
|
||||
with open(self.storage_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Update cache timestamp
|
||||
self._last_loaded = time.time()
|
||||
except Exception as e:
|
||||
print(f"Error saving passwords: {e}")
|
||||
|
||||
def add_password(self, name: str, password: str, description: str = "") -> bool:
|
||||
"""Add a new password entry."""
|
||||
try:
|
||||
encrypted_password = self._simple_encrypt(password)
|
||||
self.passwords[name] = {
|
||||
"password": encrypted_password,
|
||||
"description": description,
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": None,
|
||||
}
|
||||
self._save_passwords()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error adding password: {e}")
|
||||
return False
|
||||
|
||||
def get_password(self, name: str, update_access_time: bool = True) -> Optional[str]:
|
||||
"""Get decrypted password by name."""
|
||||
# Ensure we have fresh data
|
||||
self._load_passwords()
|
||||
|
||||
if name in self.passwords:
|
||||
try:
|
||||
encrypted = self.passwords[name]["password"]
|
||||
decrypted = self._simple_decrypt(encrypted)
|
||||
|
||||
# Update last accessed time only if requested (to avoid frequent saves)
|
||||
if update_access_time:
|
||||
self.passwords[name]["last_accessed"] = datetime.now().isoformat()
|
||||
# Don't save immediately - batch saves for better performance
|
||||
|
||||
return decrypted
|
||||
except Exception as e:
|
||||
print(f"Error decrypting password: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def remove_password(self, name: str) -> bool:
|
||||
"""Remove a password entry."""
|
||||
if name in self.passwords:
|
||||
del self.passwords[name]
|
||||
self._save_passwords()
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_passwords(self) -> List[str]:
|
||||
"""Get list of all password names."""
|
||||
# Ensure we have fresh data
|
||||
self._load_passwords()
|
||||
return list(self.passwords.keys())
|
||||
|
||||
def get_password_info(self, name: str) -> Optional[Dict]:
|
||||
"""Get password metadata without decrypting."""
|
||||
if name in self.passwords:
|
||||
info = self.passwords[name].copy()
|
||||
info.pop("password", None) # Remove encrypted password
|
||||
return info
|
||||
return None
|
||||
|
||||
|
||||
class PasswordPlugin(PluginBase):
|
||||
"""
|
||||
Password manager plugin for the launcher.
|
||||
Stores passwords securely and allows easy access.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.display_name = "Password Manager"
|
||||
self.description = "Secure password storage and management"
|
||||
|
||||
# Initialize password manager
|
||||
self.password_file = Path(
|
||||
get_relative_path("../../../config/assets/passwords.json")
|
||||
)
|
||||
self.password_manager = PasswordManager(self.password_file)
|
||||
|
||||
# State for password visibility
|
||||
self.revealed_passwords: Dict[str, str] = {}
|
||||
|
||||
# Cache for results to avoid repeated queries
|
||||
self._results_cache: Dict[str, List[Result]] = {}
|
||||
self._cache_timestamps: Dict[str, float] = {}
|
||||
self._cache_ttl = 5 # Cache results for 5 seconds
|
||||
|
||||
# Track launcher state for auto-hiding passwords
|
||||
self._launcher_instance = None
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the password plugin."""
|
||||
self.set_triggers(["pass"])
|
||||
self._setup_launcher_hooks()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the password plugin."""
|
||||
self.revealed_passwords.clear()
|
||||
self._results_cache.clear()
|
||||
self._cache_timestamps.clear()
|
||||
self._cleanup_launcher_hooks()
|
||||
|
||||
def query(self, query_string: str) -> List[Result]:
|
||||
"""Process password manager queries with caching."""
|
||||
query_key = query_string.strip()
|
||||
current_time = time.time()
|
||||
|
||||
# Check cache first (except for add/remove commands which should always execute)
|
||||
if (
|
||||
not query_key.startswith(("add ", "remove ", "delete "))
|
||||
and query_key in self._results_cache
|
||||
and (current_time - self._cache_timestamps.get(query_key, 0))
|
||||
< self._cache_ttl
|
||||
):
|
||||
return self._results_cache[query_key]
|
||||
|
||||
results = []
|
||||
query = query_key.lower()
|
||||
|
||||
# Handle different commands
|
||||
if not query:
|
||||
# Show all passwords
|
||||
results.extend(self._list_all_passwords())
|
||||
elif query.startswith("add "):
|
||||
# Add new password (don't cache)
|
||||
results.extend(self._handle_add_command(query_string))
|
||||
elif query.startswith("remove ") or query.startswith("delete "):
|
||||
# Remove password (don't cache)
|
||||
results.extend(self._handle_remove_command(query_string))
|
||||
else:
|
||||
# Search for specific password
|
||||
results.extend(self._search_passwords(query))
|
||||
|
||||
# Cache results (except for add/remove commands)
|
||||
if not query.startswith(("add ", "remove ", "delete ")):
|
||||
self._results_cache[query_key] = results
|
||||
self._cache_timestamps[query_key] = current_time
|
||||
|
||||
return results
|
||||
|
||||
def _list_all_passwords(self) -> List[Result]:
|
||||
"""List all stored passwords."""
|
||||
results = []
|
||||
password_names = self.password_manager.list_passwords()
|
||||
|
||||
if not password_names:
|
||||
results.append(
|
||||
Result(
|
||||
title="No passwords stored",
|
||||
subtitle="Use 'pass add <name> <password>' to add your first password",
|
||||
icon_name="password-symbolic",
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "empty", "keep_launcher_open": True},
|
||||
)
|
||||
)
|
||||
results.append(
|
||||
Result(
|
||||
title="Available commands:",
|
||||
subtitle="add <name> <password> | remove <name> | <name> (to search)",
|
||||
icon_name="info",
|
||||
action=lambda: None,
|
||||
relevance=0.9,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "help", "keep_launcher_open": True},
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
# Sort passwords alphabetically
|
||||
password_names.sort()
|
||||
|
||||
for name in password_names:
|
||||
info = self.password_manager.get_password_info(name)
|
||||
description = info.get("description", "") if info else ""
|
||||
|
||||
# Check if password is revealed
|
||||
if name in self.revealed_passwords:
|
||||
title = f"{name}: {self.revealed_passwords[name]}"
|
||||
subtitle = "Password revealed - Enter: copy | Shift+Enter: hide"
|
||||
else:
|
||||
title = f"{name}: {'*' * 8}"
|
||||
subtitle = "Enter: copy | Shift+Enter: reveal password"
|
||||
|
||||
if description:
|
||||
subtitle += f" | {description}"
|
||||
|
||||
results.append(
|
||||
Result(
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
icon_name="key",
|
||||
action=lambda n=name: self._copy_password_to_clipboard(n),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={
|
||||
"type": "password",
|
||||
"name": name,
|
||||
"keep_launcher_open": False,
|
||||
"alt_action": lambda n=name: self._toggle_password_visibility(
|
||||
n
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def _handle_add_command(self, query_string: str) -> List[Result]:
|
||||
"""Handle add password command."""
|
||||
results = []
|
||||
parts = query_string.strip().split(" ", 3)
|
||||
|
||||
if len(parts) < 3:
|
||||
results.append(
|
||||
Result(
|
||||
title="Add Password - Invalid format",
|
||||
subtitle="Usage: add <name> <password> [description]",
|
||||
icon_name="cancel",
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
name = parts[1]
|
||||
password = parts[2]
|
||||
description = parts[3] if len(parts) > 3 else ""
|
||||
|
||||
# Check if password already exists
|
||||
if name in self.password_manager.list_passwords():
|
||||
results.append(
|
||||
Result(
|
||||
title=f"Update password for '{name}'?",
|
||||
subtitle="Password already exists. Click to update it.",
|
||||
icon_name="key",
|
||||
action=lambda: self._add_password_action(
|
||||
name, password, description, update=True
|
||||
),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "update", "name": name, "keep_launcher_open": False},
|
||||
)
|
||||
)
|
||||
else:
|
||||
results.append(
|
||||
Result(
|
||||
title=f"Add password for '{name}'",
|
||||
subtitle="Click to save password"
|
||||
+ (f" | {description}" if description else ""),
|
||||
icon_name="plus",
|
||||
action=lambda: self._add_password_action(
|
||||
name, password, description
|
||||
),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "add", "name": name, "keep_launcher_open": False},
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def _handle_remove_command(self, query_string: str) -> List[Result]:
|
||||
"""Handle remove password command."""
|
||||
results = []
|
||||
parts = query_string.strip().split(" ", 1)
|
||||
|
||||
if len(parts) < 2:
|
||||
results.append(
|
||||
Result(
|
||||
title="Remove Password - Invalid format",
|
||||
subtitle="Usage: remove <name>",
|
||||
icon_name="cancel",
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
name = parts[1]
|
||||
|
||||
if name not in self.password_manager.list_passwords():
|
||||
results.append(
|
||||
Result(
|
||||
title=f"Password '{name}' not found",
|
||||
subtitle="Check the name and try again",
|
||||
icon_name="cancel",
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
)
|
||||
else:
|
||||
results.append(
|
||||
Result(
|
||||
title=f"Remove password '{name}'?",
|
||||
subtitle="Click to confirm deletion (this cannot be undone)",
|
||||
icon_name="trash",
|
||||
action=lambda: self._remove_password_action(name),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "remove", "name": name, "keep_launcher_open": False},
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def _search_passwords(self, query: str) -> List[Result]:
|
||||
"""Search for passwords by name."""
|
||||
results = []
|
||||
password_names = self.password_manager.list_passwords()
|
||||
|
||||
# Filter passwords that match the query
|
||||
matching_passwords = [
|
||||
name for name in password_names if query.lower() in name.lower()
|
||||
]
|
||||
|
||||
if not matching_passwords:
|
||||
results.append(
|
||||
Result(
|
||||
title=f"No passwords found matching '{query}'",
|
||||
subtitle="Try a different search term or use 'pass' to see all passwords",
|
||||
icon_name="magnifier",
|
||||
action=lambda: None,
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "no_results", "keep_launcher_open": True},
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
# Sort by relevance (exact match first, then starts with, then contains)
|
||||
def get_relevance(name: str) -> float:
|
||||
name_lower = name.lower()
|
||||
query_lower = query.lower()
|
||||
|
||||
if name_lower == query_lower:
|
||||
return 1.0
|
||||
elif name_lower.startswith(query_lower):
|
||||
return 0.9
|
||||
else:
|
||||
return 0.7
|
||||
|
||||
matching_passwords.sort(key=get_relevance, reverse=True)
|
||||
|
||||
for name in matching_passwords:
|
||||
info = self.password_manager.get_password_info(name)
|
||||
description = info.get("description", "") if info else ""
|
||||
|
||||
# Check if password is revealed
|
||||
if name in self.revealed_passwords:
|
||||
title = f"{name}: {self.revealed_passwords[name]}"
|
||||
subtitle = "Password revealed - Enter: copy | Shift+Enter: hide"
|
||||
else:
|
||||
title = f"{name}: {'*' * 8}"
|
||||
subtitle = "Enter: copy | Shift+Enter: reveal password"
|
||||
|
||||
if description:
|
||||
subtitle += f" | {description}"
|
||||
|
||||
results.append(
|
||||
Result(
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
icon_name="key",
|
||||
action=lambda n=name: self._copy_password_to_clipboard(n),
|
||||
relevance=get_relevance(name),
|
||||
plugin_name=self.display_name,
|
||||
data={
|
||||
"type": "password",
|
||||
"name": name,
|
||||
"keep_launcher_open": False,
|
||||
"alt_action": lambda n=name: self._toggle_password_visibility(
|
||||
n
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def _add_password_action(
|
||||
self, name: str, password: str, description: str = "", update: bool = False
|
||||
):
|
||||
"""Action to add/update a password."""
|
||||
try:
|
||||
success = self.password_manager.add_password(name, password, description)
|
||||
if success:
|
||||
action_word = "updated" if update else "added"
|
||||
# Clear cache to force refresh
|
||||
self._results_cache.clear()
|
||||
|
||||
# Send notification if available (non-blocking)
|
||||
try:
|
||||
subprocess.Popen(
|
||||
[
|
||||
"notify-send",
|
||||
"Password Manager",
|
||||
f"Password '{name}' {action_word} successfully",
|
||||
]
|
||||
)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
print(f"Failed to add password '{name}'")
|
||||
except Exception as e:
|
||||
print(f"Error adding password: {e}")
|
||||
|
||||
def _remove_password_action(self, name: str):
|
||||
"""Action to remove a password."""
|
||||
try:
|
||||
success = self.password_manager.remove_password(name)
|
||||
if success:
|
||||
# Remove from revealed passwords if present
|
||||
self.revealed_passwords.pop(name, None)
|
||||
|
||||
# Clear cache to force refresh
|
||||
self._results_cache.clear()
|
||||
|
||||
# Send notification if available (non-blocking)
|
||||
try:
|
||||
subprocess.Popen(
|
||||
[
|
||||
"notify-send",
|
||||
"Password Manager",
|
||||
f"Password '{name}' removed successfully",
|
||||
]
|
||||
)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
print(f"Failed to remove password '{name}'")
|
||||
except Exception as e:
|
||||
print(f"Error removing password: {e}")
|
||||
|
||||
def _copy_password_to_clipboard(self, name: str):
|
||||
"""Copy password to clipboard and reveal it temporarily."""
|
||||
try:
|
||||
password = self.password_manager.get_password(
|
||||
name, update_access_time=False
|
||||
)
|
||||
if password:
|
||||
# Copy to clipboard (use timeout to avoid hanging)
|
||||
try:
|
||||
subprocess.run(
|
||||
["wl-copy"], input=password.encode(), check=True, timeout=2
|
||||
)
|
||||
except subprocess.SubprocessError:
|
||||
# Fall back to X11
|
||||
subprocess.run(
|
||||
["xclip", "-selection", "clipboard"],
|
||||
input=password.encode(),
|
||||
check=True,
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
# Reveal password temporarily
|
||||
self.revealed_passwords[name] = password
|
||||
|
||||
# Send notification if available (non-blocking)
|
||||
try:
|
||||
subprocess.Popen(
|
||||
[
|
||||
"notify-send",
|
||||
"Password Manager",
|
||||
f"Password for '{name}' copied to clipboard",
|
||||
]
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Clear cache to force refresh
|
||||
self._results_cache.clear()
|
||||
|
||||
else:
|
||||
print(f"Failed to retrieve password for '{name}'")
|
||||
except Exception as e:
|
||||
print(f"Error copying password: {e}")
|
||||
|
||||
def _reveal_password(self, name: str):
|
||||
"""Reveal password without copying to clipboard."""
|
||||
try:
|
||||
password = self.password_manager.get_password(
|
||||
name, update_access_time=False
|
||||
)
|
||||
if password:
|
||||
self.revealed_passwords[name] = password
|
||||
# Clear cache to force refresh with revealed password
|
||||
self._results_cache.clear()
|
||||
else:
|
||||
print(f"Failed to retrieve password for '{name}'")
|
||||
except Exception as e:
|
||||
print(f"Error revealing password: {e}")
|
||||
|
||||
def _hide_password(self, name: str):
|
||||
"""Hide revealed password."""
|
||||
self.revealed_passwords.pop(name, None)
|
||||
|
||||
def _hide_all_passwords(self):
|
||||
"""Hide all revealed passwords."""
|
||||
if self.revealed_passwords:
|
||||
self.revealed_passwords.clear()
|
||||
# Clear cache to force refresh with hidden passwords
|
||||
self._results_cache.clear()
|
||||
|
||||
def _setup_launcher_hooks(self):
|
||||
"""Setup hooks to monitor launcher state."""
|
||||
try:
|
||||
# Try to find the launcher instance
|
||||
import gc
|
||||
|
||||
for obj in gc.get_objects():
|
||||
if (
|
||||
hasattr(obj, "__class__")
|
||||
and obj.__class__.__name__ == "Launcher"
|
||||
and hasattr(obj, "close_launcher")
|
||||
):
|
||||
self._launcher_instance = obj
|
||||
# Store original close_launcher method
|
||||
self._original_close_launcher = obj.close_launcher
|
||||
# Replace with our wrapper
|
||||
obj.close_launcher = self._wrapped_close_launcher
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not setup launcher hooks: {e}")
|
||||
|
||||
def _cleanup_launcher_hooks(self):
|
||||
"""Cleanup launcher hooks."""
|
||||
try:
|
||||
if self._launcher_instance and hasattr(self, "_original_close_launcher"):
|
||||
# Restore original close_launcher method
|
||||
self._launcher_instance.close_launcher = self._original_close_launcher
|
||||
self._launcher_instance = None
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not cleanup launcher hooks: {e}")
|
||||
|
||||
def _wrapped_close_launcher(self):
|
||||
"""Wrapper for launcher close that hides passwords."""
|
||||
# Hide all passwords when launcher closes
|
||||
self._hide_all_passwords()
|
||||
# Call original close_launcher method
|
||||
if hasattr(self, "_original_close_launcher"):
|
||||
self._original_close_launcher()
|
||||
|
||||
def _toggle_password_visibility(self, name: str):
|
||||
"""Toggle password visibility when Shift+Enter is pressed."""
|
||||
if name in self.revealed_passwords:
|
||||
# Hide password
|
||||
self.revealed_passwords.pop(name, None)
|
||||
self._results_cache.clear()
|
||||
else:
|
||||
# Reveal password
|
||||
try:
|
||||
password = self.password_manager.get_password(
|
||||
name, update_access_time=False
|
||||
)
|
||||
if password:
|
||||
self.revealed_passwords[name] = password
|
||||
self._results_cache.clear()
|
||||
else:
|
||||
print(f"Failed to retrieve password for '{name}'")
|
||||
except Exception as e:
|
||||
print(f"Error revealing password: {e}")
|
||||
|
||||
# Force refresh of the launcher to show updated state
|
||||
self._force_launcher_refresh()
|
||||
|
||||
def _force_launcher_refresh(self):
|
||||
"""Force the launcher to refresh and show updated results."""
|
||||
try:
|
||||
if self._launcher_instance and hasattr(
|
||||
self._launcher_instance, "_perform_search"
|
||||
):
|
||||
# Get current search text
|
||||
current_text = ""
|
||||
if hasattr(self._launcher_instance, "search_entry"):
|
||||
current_text = self._launcher_instance.search_entry.get_text()
|
||||
|
||||
# Trigger a search to refresh results
|
||||
try:
|
||||
from gi.repository import GLib
|
||||
|
||||
def refresh():
|
||||
self._launcher_instance._perform_search(current_text)
|
||||
return False
|
||||
|
||||
GLib.timeout_add(50, refresh)
|
||||
except ImportError:
|
||||
# Fallback: direct call if GLib not available
|
||||
self._launcher_instance._perform_search(current_text)
|
||||
except Exception as e:
|
||||
print(f"Could not force launcher refresh: {e}")
|
||||
Reference in new issue
Block a user