🔧 chore: update code

This commit is contained in:
sankalp committed 2025-06-29 23:41:21 +05:45
1 parent 6d010ee65c
commit 0b652ff87a
19 files changed
+2336 -393

No files matched your search

+5
View File
@@ -18,6 +18,7 @@ from .battery import Battery
from .controls import Controls
from .indicators import Indicators
from .metrics import Metrics
from .music_player import MusicPlayer
from .workspaces import workspace
import gi
@@ -110,6 +111,7 @@ class DockComponents(Box):
)
self.controls = Controls()
self.indicators = Indicators()
self.music_player = MusicPlayer()
self.systray = SystemTray(
icon_size=18,
spacing=4,
@@ -122,6 +124,7 @@ class DockComponents(Box):
self.workspaces,
self.metrics,
self.controls,
self.music_player,
self.applications,
self.indicators,
self.battery,
@@ -155,6 +158,7 @@ class DockComponents(Box):
"date_time": self.date_time,
"controls": self.controls,
"indicators": self.indicators,
"music_player": self.music_player,
"systray": self.systray,
"applications": self.applications,
"language": self.language,
@@ -186,6 +190,7 @@ class DockComponents(Box):
"date_time": self.date_time,
"controls": self.controls,
"indicators": self.indicators,
"music_player": self.music_player,
"systray": self.systray,
"applications": self.applications,
"language": self.language,
+333
View File
@@ -0,0 +1,333 @@
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.circularprogressbar import CircularProgressBar
from fabric.widgets.eventbox import EventBox
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from gi.repository import Gdk
import config.data as data
import utils.icons as icons
from services.mpris import PlayerManager, PlayerService
from utils.wayland import WaylandWindow as Window
from player import PlayerContainer
class MusicPlayerPopup(Window):
"""Popup window that shows the full music player interface"""
def __init__(self, **kwargs):
# Get dock position for proper anchoring
dock_position = data.DOCK_POSITION
popup_anchor = self._get_popup_anchor(dock_position)
margin = self._get_popup_margin(dock_position)
super().__init__(
name="music-player-popup",
layer="top",
anchor=popup_anchor,
margin=margin,
exclusive=False,
keyboard_mode="on-demand",
visible=False,
all_visible=False,
**kwargs,
)
self.player_container = PlayerContainer()
self.add(self.player_container)
# Connect to escape key to close
self.connect("key-press-event", self.on_key_press)
self.connect("button-press-event", self.on_button_press)
self.set_can_focus(True)
def on_key_press(self, widget, event):
"""Handle key press events"""
if event.keyval == Gdk.KEY_Escape:
self.hide_popup()
return True
return False
def show_popup(self):
"""Show the music player popup"""
self.set_visible(True)
self.grab_focus()
def _get_popup_anchor(self, dock_position):
"""Get popup anchor based on dock position"""
anchor_map = {
"Top": "top",
"Bottom": "bottom",
"Left": "left",
"Right": "right",
}
return anchor_map.get(dock_position, "bottom")
def _get_popup_margin(self, dock_position):
"""Get popup margin based on dock position"""
margin_map = {
"Top": "60px 10px 10px 10px",
"Bottom": "10px 10px 60px 10px",
"Left": "10px 10px 10px 60px",
"Right": "10px 60px 10px 10px",
}
return margin_map.get(dock_position, "10px 10px 60px 10px")
def on_button_press(self, widget, event):
"""Handle button press events"""
return False
def hide_popup(self):
"""Hide the music player popup"""
self.set_visible(False)
class MusicPlayer(Box):
"""Music player component for the dock with circular progress bar"""
def __init__(self, **kwargs):
super().__init__(name="music-player", **kwargs)
# Initialize player manager
self.manager = PlayerManager()
self.manager.connect("new-player", self.on_new_player)
self.manager.connect("player-vanish", self.on_player_vanish)
# Current player tracking
self.current_player = None
self.current_player_service = None
self.duration = 0.0
self.position = 0.0
self._current_track_info = "Music Player"
# Create circular progress bar
self.progress_bar = CircularProgressBar(
name="player-circle",
size=28,
line_width=2,
start_angle=150,
end_angle=390,
value=0.0, # Initialize with 0 progress
)
# Create music icon label - always use generic music icon
self.music_label = Label(name="music-label", markup=icons.music)
# Create button for the music icon
self.music_button = Button(
on_clicked=self.on_music_clicked, child=self.music_label
)
# Create event box for scroll events (volume control)
self.event_box = EventBox(
events=["scroll", "smooth-scroll"],
child=Overlay(child=self.progress_bar, overlays=self.music_button),
)
# Connect scroll events for seeking
self.event_box.connect("scroll-event", self.on_scroll)
self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK)
self.add(self.event_box)
# Popup window will be created on-demand
self.popup = None
# Initialize players
self.manager.init_all_players()
# Set initial visibility
self.update_visibility()
# Connect to destroy signal for cleanup
self.connect("destroy", self.on_destroy)
def on_destroy(self, widget):
"""Clean up popup when component is destroyed"""
if self.popup:
self.popup.destroy()
self.popup = None
def on_new_player(self, manager, player):
"""Handle new player detection"""
print(f"Music dock component: New player detected - {player.props.player_name}")
# If this is our first player, make it current
if not self.current_player:
self.set_current_player(player)
self.update_visibility()
def on_player_vanish(self, manager, player):
"""Handle player disappearing"""
print(f"Music dock component: Player vanished - {player.props.player_name}")
# If the current player vanished, find another one
if (
self.current_player
and self.current_player.props.player_name == player.props.player_name
):
self.current_player = None
self.current_player_service = None
# Try to find another active player
# This is a simple approach - in a real implementation you might want more sophisticated logic
self.update_visibility()
def set_current_player(self, player):
"""Set the current active player"""
# Disconnect from previous player
if self.current_player_service:
try:
self.current_player_service.disconnect_by_func(self.on_track_position)
self.current_player_service.disconnect_by_func(self.on_play)
self.current_player_service.disconnect_by_func(self.on_pause)
self.current_player_service.disconnect_by_func(self.on_metadata)
except:
pass
self.current_player = player
self.current_player_service = PlayerService(player=player)
# Connect to player events
self.current_player_service.connect("track-position", self.on_track_position)
self.current_player_service.connect("play", self.on_play)
self.current_player_service.connect("pause", self.on_pause)
self.current_player_service.connect("meta-change", self.on_metadata)
# Always use generic music icon, not player-specific icons
self.music_label.set_markup(icons.music)
# Update initial state
self.update_playback_state()
def on_track_position(self, service, pos, dur):
"""Handle track position updates"""
self.position = pos
self.duration = dur
if dur > 0:
progress = pos / dur
# Use value property instead of set_percentage for CircularProgressBar
self.progress_bar.value = progress
# Update tooltip with timestamp info
pos_min = int(pos // 60)
pos_sec = int(pos % 60)
dur_min = int(dur // 60)
dur_sec = int(dur % 60)
timestamp_text = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
# Get current track info if available
if hasattr(self, '_current_track_info'):
tooltip = f"{self._current_track_info}\n{timestamp_text}"
else:
tooltip = f"Music Player\n{timestamp_text}"
self.event_box.set_tooltip_text(tooltip)
else:
self.progress_bar.value = 0
self.event_box.set_tooltip_text("Music Player")
def on_play(self, service):
"""Handle play state"""
self.music_label.set_markup(icons.music)
self.progress_bar.remove_style_class("paused")
# Update tooltip to show playing state
if hasattr(self, '_current_track_info'):
self.event_box.set_tooltip_text(f"{self._current_track_info} (Playing)")
def on_pause(self, service):
"""Handle pause state"""
# Always use generic music icon when paused
self.music_label.set_markup(icons.music)
self.progress_bar.add_style_class("paused")
# Update tooltip to show paused state
if hasattr(self, '_current_track_info'):
self.event_box.set_tooltip_text(f"{self._current_track_info} (Paused)")
def on_metadata(self, service, metadata, player):
"""Handle metadata changes"""
# Update tooltip with current track info
keys = metadata.keys()
if "xesam:artist" in keys and "xesam:title" in keys:
artist = (
metadata["xesam:artist"][0]
if metadata["xesam:artist"]
else "Unknown Artist"
)
title = metadata["xesam:title"]
self._current_track_info = f"{artist} - {title}"
# If we have position info, include timestamp
if hasattr(self, 'position') and hasattr(self, 'duration') and self.duration > 0:
pos_min = int(self.position // 60)
pos_sec = int(self.position % 60)
dur_min = int(self.duration // 60)
dur_sec = int(self.duration % 60)
timestamp_text = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
tooltip = f"{self._current_track_info}\n{timestamp_text}"
else:
tooltip = self._current_track_info
self.event_box.set_tooltip_text(tooltip)
else:
self._current_track_info = "Music Player"
self.event_box.set_tooltip_text("Music Player")
def update_playback_state(self):
"""Update the visual state based on current playback"""
if not self.current_player:
return
if (
self.current_player.props.playback_status.value_name
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
):
self.on_play(None)
else:
self.on_pause(None)
def on_music_clicked(self, button):
"""Handle music button click - show popup"""
if self.popup and self.popup.get_visible():
self.popup.hide_popup()
else:
# Create popup on-demand if it doesn't exist
if not self.popup:
self.popup = MusicPlayerPopup()
self.popup.show_popup()
def on_scroll(self, widget, event):
"""Handle scroll events for seeking"""
if not self.current_player or self.duration <= 0:
return False
# Calculate seek amount (5 seconds per scroll)
seek_amount = 5.0
if event.direction == Gdk.ScrollDirection.UP:
new_position = min(self.position + seek_amount, self.duration)
elif event.direction == Gdk.ScrollDirection.DOWN:
new_position = max(self.position - seek_amount, 0)
else:
return False
# Seek to new position
if self.current_player_service:
self.current_player_service.set_position(int(new_position))
return True
def update_visibility(self):
"""Update component visibility based on available players"""
# Show component only if there are active players
has_players = self.current_player is not None
self.set_visible(has_players)
# Hide in vertical mode if user preference is set
if data.VERTICAL and hasattr(data, "DOCK_COMPONENTS_VISIBILITY"):
music_visible = data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True)
if not music_visible:
self.set_visible(False)
+149 -208
View File
@@ -1,4 +1,3 @@
import base64
import json
import subprocess
import threading
@@ -11,13 +10,13 @@ from fabric.utils import get_relative_path
import utils.icons as icons
from modules.launcher.plugin_base import PluginBase
from modules.launcher.result import Result
try:
import pyotp
PYOTP_AVAILABLE = True
except ImportError:
PYOTP_AVAILABLE = False
from services.auth import (
generate_totp,
get_time_remaining_with_blink,
validate_base32_secret,
parse_otpauth_uri,
scan_qr_and_add_account,
)
class OTPPlugin(PluginBase):
@@ -40,11 +39,6 @@ class OTPPlugin(PluginBase):
def initialize(self):
"""Initialize the OTP plugin."""
if not PYOTP_AVAILABLE:
raise RuntimeError(
"pyotp is required for OTP functionality. Install with: pip install pyotp"
)
self.set_triggers(["otp"])
self._load_secrets()
self._ensure_config_file()
@@ -301,26 +295,11 @@ class OTPPlugin(PluginBase):
def _generate_totp(self, secret: str) -> Optional[str]:
"""Generate TOTP code from secret."""
try:
return pyotp.TOTP(secret).now()
except Exception as e:
print(f"Error generating TOTP: {e}")
return None
def _get_time_remaining(self) -> int:
"""Get seconds remaining until next token refresh."""
return 30 - (int(time.time()) % 30)
return generate_totp(secret)
def _get_time_remaining_with_blink(self) -> str:
"""Get time remaining with blinking effect."""
time_remaining = self._get_time_remaining()
current_second = int(time.time())
should_blink = current_second % 2 == 0
if should_blink:
return f"<span alpha='30%'>{time_remaining}s</span>"
else:
return f"{time_remaining}s"
return get_time_remaining_with_blink()
def query(self, query_string: str) -> List[Result]:
"""Process OTP queries."""
@@ -357,6 +336,13 @@ class OTPPlugin(PluginBase):
else:
remove_content = query[7:].strip()
return self._handle_remove_command(remove_content)
elif query_lower == "qr" or query_lower.startswith("qr "):
# Handle QR scanning command
if query_lower == "qr":
qr_content = ""
else:
qr_content = query[3:].strip()
return self._handle_qr_command(qr_content)
else:
return self._search_accounts(query)
@@ -413,7 +399,7 @@ class OTPPlugin(PluginBase):
results.append(
Result(
title="Available commands:",
subtitle="add <account> <secret> | remove <account>",
subtitle="add <account> <secret> | remove <account> | qr <account>",
icon_markup=icons.info,
action=lambda: None,
relevance=0.9,
@@ -435,7 +421,7 @@ class OTPPlugin(PluginBase):
results.append(
Result(
title=f"{totp_code}",
subtitle_markup=f"{display_name} • {time_display} remaining",
subtitle_markup=f"{display_name} • {time_display} remaining • Shift+Enter: remove",
icon_markup=icons.key,
action=lambda code=totp_code: self._copy_to_clipboard(code),
relevance=1.0,
@@ -444,6 +430,7 @@ class OTPPlugin(PluginBase):
"type": "totp",
"account": account_name,
"code": totp_code,
"alt_action": lambda acc=account_name: self._remove_account_and_refresh(acc),
},
)
)
@@ -492,7 +479,7 @@ class OTPPlugin(PluginBase):
title=f"{totp_code}",
subtitle_markup=f"{display_name} • {
time_display
} remaining",
} remaining • Shift+Enter: remove",
icon_markup=icons.key,
action=lambda code=totp_code: self._copy_to_clipboard(code),
relevance=1.0,
@@ -501,6 +488,7 @@ class OTPPlugin(PluginBase):
"type": "totp",
"account": account_name,
"code": totp_code,
"alt_action": lambda acc=account_name: self._remove_account_and_refresh(acc),
},
)
)
@@ -520,7 +508,7 @@ class OTPPlugin(PluginBase):
results.append(
Result(
title="Available commands:",
subtitle="add <account> <secret> | remove <account>",
subtitle="add <account> <secret> | remove <account> | qr <account>",
icon_markup=icons.info,
action=lambda: None,
relevance=0.4,
@@ -589,6 +577,66 @@ class OTPPlugin(PluginBase):
),
]
def _handle_qr_command(self, account_name: str) -> List[Result]:
"""Handle QR scanning command."""
if not account_name:
return [
Result(
title="Scan QR Code",
subtitle="Click to scan QR code from screen",
icon_markup=icons.connect,
action=lambda: self._scan_qr_and_add_account(""),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "qr_scan"},
),
Result(
title="QR Scan Instructions:",
subtitle="Use 'qr <account_name>' to specify account name",
icon_markup=icons.info,
action=lambda: None,
relevance=0.9,
plugin_name=self.display_name,
data={"type": "help", "keep_launcher_open": True},
),
]
else:
return [
Result(
title=f"Scan QR Code for '{account_name}'",
subtitle="Click to scan QR code from screen",
icon_markup=icons.connect,
action=lambda name=account_name: self._scan_qr_and_add_account(name),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "qr_scan"},
),
]
def _scan_qr_and_add_account(self, account_name: str):
"""Scan QR code and add OTP account."""
print(f"QR scan action called for account: '{account_name}'")
print("Starting QR scan process asynchronously...")
# Run QR scanning asynchronously so launcher closes immediately
import threading
thread = threading.Thread(target=self._scan_qr_async, args=(account_name,))
thread.daemon = True
thread.start()
def _scan_qr_async(self, account_name: str):
"""Async QR scanning process."""
result = scan_qr_and_add_account(account_name, str(self.secrets_file))
if result["success"]:
print(result["message"])
# Reload secrets from file
self._load_secrets()
# Trigger refresh to show the new account
self._trigger_refresh()
else:
print(f"QR scan failed: {result['error']}")
def _handle_remove_command(self, account_name: str) -> List[Result]:
"""Handle removal of OTP account."""
if not account_name:
@@ -636,18 +684,18 @@ class OTPPlugin(PluginBase):
results.append(
Result(
title=f"{totp_code}",
subtitle_markup=f"Type: remove {acc_name} • {
subtitle_markup=f"Press Enter to remove • {
time_display
} remaining",
icon_markup=icons.trash,
action=lambda: None, # No action - shows instruction only
action=lambda acc=acc_name: self._remove_account_and_refresh(acc),
relevance=0.9,
plugin_name=self.display_name,
data={
"type": "remove_instruction",
"account": acc_name,
"code": totp_code,
"keep_launcher_open": True,
"keep_launcher_open": True
},
)
)
@@ -655,9 +703,9 @@ class OTPPlugin(PluginBase):
results.append(
Result(
title=f"Error: {acc_name}",
subtitle=f"Type: remove {acc_name} (Invalid secret)",
subtitle=f"Press Enter to remove (Invalid secret)",
icon_markup=icons.trash,
action=lambda: None, # No action - shows instruction only
action=lambda acc=acc_name: self._remove_account_and_refresh(acc),
relevance=0.8,
plugin_name=self.display_name,
data={
@@ -689,114 +737,28 @@ class OTPPlugin(PluginBase):
issuer = account_data.get("issuer", "")
display_name = f"{issuer} - {account_name}" if issuer else account_name
# Remove the account
try:
del self.secrets[account_name]
self._save_secrets()
return [
Result(
title=f"✓ Removed '{display_name}'",
subtitle="OTP account removed successfully",
icon_markup=icons.check,
action=lambda: self._trigger_refresh(),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "success", "keep_launcher_open": True},
)
]
except Exception as e:
return [
Result(
title="Error removing account",
subtitle=f"Failed to remove account: {str(e)}",
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
plugin_name=self.display_name,
data={"type": "error", "keep_launcher_open": True},
)
]
# Show confirmation for removal
return [
Result(
title=f"Remove '{display_name}'?",
subtitle="Press Enter to confirm removal",
icon_markup=icons.trash,
action=lambda acc=account_name: self._remove_account_and_refresh(acc),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "remove_confirm", "account": account_name,"keep_launcher_open": True,},
)
]
def _handle_base32_secret(self, account_name: str, secret: str) -> List[Result]:
"""Handle raw Base32 secret."""
try:
# Clean up the secret - remove spaces, dashes, and convert to uppercase
clean_secret = (
secret.replace(" ", "").replace("-", "").replace("_", "").upper()
)
# Remove any non-base32 characters
import re
clean_secret = re.sub(r"[^A-Z2-7]", "", clean_secret)
# Add padding if needed (Base32 requires padding to multiple of 8)
while len(clean_secret) % 8 != 0:
clean_secret += "="
# Validate Base32 format
try:
base64.b32decode(clean_secret)
except Exception as e:
return [
Result(
title="Invalid Base32 secret",
subtitle=f"Error: {str(e)}. Please check the secret format",
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
plugin_name=self.display_name,
data={"type": "error", "keep_launcher_open": True},
)
]
# Test if the secret can generate a valid TOTP
try:
test_totp = pyotp.TOTP(clean_secret)
test_code = test_totp.now()
if not test_code or len(test_code) != 6:
raise ValueError("Generated invalid TOTP code")
except Exception as e:
return [
Result(
title="Invalid secret for TOTP",
subtitle=f"Cannot generate TOTP: {str(e)}",
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
plugin_name=self.display_name,
data={"type": "error", "keep_launcher_open": True},
)
]
self.secrets[account_name] = {
"secret": clean_secret,
"issuer": "",
"algorithm": "SHA1",
"digits": 6,
"period": 30,
}
self._save_secrets()
result = validate_base32_secret(secret)
if not result["success"]:
return [
Result(
title=f"✓ Added '{account_name}'",
subtitle=f"OTP account added successfully (secret: {
clean_secret[:4]
}...)",
icon_markup=icons.check,
action=lambda: self._trigger_refresh(),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "success", "keep_launcher_open": True},
)
]
except Exception as e:
return [
Result(
title="Error adding account",
subtitle=f"Unexpected error: {str(e)}",
title="Invalid Base32 secret",
subtitle=result["error"],
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
@@ -805,79 +767,36 @@ class OTPPlugin(PluginBase):
)
]
self.secrets[account_name] = {
"secret": result["secret"],
"issuer": "",
"algorithm": "SHA1",
"digits": 6,
"period": 30,
}
self._save_secrets()
return [
Result(
title=f"✓ Added '{account_name}'",
subtitle=f"OTP account added successfully (secret: {result['secret'][:4]}...)",
icon_markup=icons.check,
action=lambda: self._trigger_refresh(),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "success", "keep_launcher_open": True},
)
]
def _handle_otpauth_uri(self, account_name: str, uri: str) -> List[Result]:
"""Handle otpauth:// URI."""
try:
from urllib.parse import parse_qs, urlparse
result = parse_otpauth_uri(uri, account_name)
parsed = urlparse(uri)
if parsed.scheme != "otpauth" or parsed.netloc != "totp":
return [
Result(
title="Invalid otpauth URI",
subtitle="Only otpauth://totp/ URIs are supported",
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
plugin_name=self.display_name,
data={"type": "error", "keep_launcher_open": True},
)
]
if not account_name:
account_path = parsed.path.lstrip("/")
if ":" in account_path:
issuer, extracted_name = account_path.split(":", 1)
account_name = extracted_name
else:
account_name = account_path
params = parse_qs(parsed.query)
secret = params.get("secret", [""])[0]
issuer = params.get("issuer", [""])[0]
algorithm = params.get("algorithm", ["SHA1"])[0]
digits = int(params.get("digits", ["6"])[0])
period = int(params.get("period", ["30"])[0])
if not secret:
return [
Result(
title="No secret in URI",
subtitle="otpauth URI must contain a secret parameter",
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
plugin_name=self.display_name,
data={"type": "error", "keep_launcher_open": True},
)
]
self.secrets[account_name] = {
"secret": secret,
"issuer": issuer,
"algorithm": algorithm,
"digits": digits,
"period": period,
}
self._save_secrets()
display_name = f"{issuer} - {account_name}" if issuer else account_name
return [
Result(
title=f"✓ Added '{display_name}'",
subtitle="OTP account added from URI",
icon_markup=icons.check,
action=lambda: self._trigger_refresh(),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "success", "keep_launcher_open": True},
)
]
except Exception as e:
if not result["success"]:
return [
Result(
title="Error parsing otpauth URI",
subtitle=str(e),
subtitle=result["error"],
icon_markup=icons.cancel,
action=lambda: None,
relevance=0.5,
@@ -885,3 +804,25 @@ class OTPPlugin(PluginBase):
data={"type": "error", "keep_launcher_open": True},
)
]
self.secrets[result["account_name"]] = {
"secret": result["secret"],
"issuer": result["issuer"],
"algorithm": result["algorithm"],
"digits": result["digits"],
"period": result["period"],
}
self._save_secrets()
display_name = f"{result['issuer']} - {result['account_name']}" if result['issuer'] else result['account_name']
return [
Result(
title=f"✓ Added '{display_name}'",
subtitle="OTP account added from URI",
icon_markup=icons.check,
action=lambda: self._trigger_refresh(),
relevance=1.0,
plugin_name=self.display_name,
data={"type": "success", "keep_launcher_open": True},
)
]