Files
dotfiles/ClipCascade/cli/tray.py
T
2026-07-23 16:48:58 +02:00

368 lines
12 KiB
Python
Executable File

import math
import os
import threading
import time
import webbrowser
from cli.echo import Echo
from cli.info import CustomDialog
from core.config import Config
from core.constants import *
from itertools import count
if PLATFORM != WINDOWS:
import subprocess
class TaskbarPanel:
def __init__(
self,
on_connect_callback: callable = None,
on_disconnect_callback: callable = None,
on_logoff_callback: callable = None,
new_version_available: list = None,
github_url: str = GITHUB_URL,
donation_url: str = None,
ws_interface=None, # type= interfaces.ws_interface.WSInterface
config: Config = None,
):
self.on_connect_callback = on_connect_callback
self.on_disconnect_callback = on_disconnect_callback
self.on_logoff_callback = on_logoff_callback
self.new_version_available = new_version_available
self.github_url = github_url
self.donation_url = donation_url
self.ws_interface = ws_interface
self.config = config
self.is_disconnecting = False
self.disconnecting_items = None
self.is_file_download_enabled = False
self.file_download_items = None
self.previous_stats: str = ""
self.previous_stats_items = None
# Initial state: Connected
self.is_connected = True
self.menu_items = None
self.numbered_menu = None
self.loop_terminate = False
self.update_stats() # Start the stats update thread
def run(self):
Echo("-" * 53)
self.menu_items, self.numbered_menu = self.create_menu()
while not self.loop_terminate:
Echo("\n")
self.display_menu()
# input
choice = input("Choice: \n")
Echo("Please wait...")
if choice == "": # refresh
continue
if choice.isdigit():
choice = int(choice)
else:
Echo("Invalid choice. Please enter a number.")
if choice in self.numbered_menu:
fun = self.numbered_menu[choice][1]
if fun is not None:
fun()
else:
Echo("Invalid choice.")
def get_max_width(self):
"""Determine the maximum width of menu item texts."""
menu_texts = [
item[0] if isinstance(item, tuple) else "" for item in self.menu_items
]
return max(len(text) for text in menu_texts) + 1
def display_menu(self):
counter = count(1)
max_width = self.get_max_width()
total_width = max_width + 8
# Box Top Border
print("\n╔" + "═" * (max_width + 6) + "╗")
print(f"║{' MAIN MENU ':^{max_width + 6}}║")
print("╠" + "═" * (max_width + 6) + "╣")
# Menu Items
for item in self.menu_items:
if isinstance(item, tuple):
width = max_width
if "disconnect" in item[0].lower() or "logs" in item[0].lower():
if "disconnecting" in item[0].lower():
width -= 1
else:
width += 1
else:
width -= 1
print(f"║ {next(counter):<2}. {item[0]:<{width}} ║")
else:
print(f"║ {'─' * (max_width + 4)} ║")
# Box Bottom Border
print("╚" + "═" * (max_width + 6) + "╝")
def create_menu(self, item_: tuple = None):
# Menu items
menu_items = [
"", # Menu Separator
("🗒️ Open Logs", self._open_logs),
("📂 Program Files", self._open_program_location),
"", # Menu Separator
("🏠 Homepage", self._open_homepage),
("❓ Help", self._open_help),
("💟 Donate", self._open_donate),
("🌐 GitHub", self._open_github),
"", # Menu Separator
("🔒 Logoff and Quit", self._on_logoff),
("❌ Quit", self._on_quit),
]
# Add connect/disconnect option (top of the menu - 0 index)
if not self.is_connected:
menu_items.insert(0, ("🔗 Connect", self._on_connect))
else:
if self.is_disconnecting and self.disconnecting_items is not None:
menu_items.insert(
self.disconnecting_items[1],
(self.disconnecting_items[0], self.disconnecting_items[2]),
)
else:
menu_items.insert(0, ("⛓️‍💥 Disconnect", self._on_disconnect))
# Add update option (before the last 3 items)
if self.new_version_available is not None and self.new_version_available[0]:
menu_items.insert(
len(menu_items) - 3,
(
f"🔄 Update ({self.new_version_available[2]}{self.new_version_available[1]})",
self._on_update,
),
)
# Add files download option (top of the menu - 0 index)
if self.is_file_download_enabled and self.file_download_items is not None:
menu_items.insert(
self.file_download_items[1],
(self.file_download_items[0], self.file_download_items[2]),
)
# Add stats option (top of the menu - 0 index)
if self.previous_stats_items is not None:
menu_items.insert(
self.previous_stats_items[1],
(self.previous_stats_items[0], self.previous_stats_items[2]),
)
# Create numbered menu
counter = count(1)
numbered_menu = {
next(counter): item for item in menu_items if isinstance(item, tuple)
}
return (menu_items, numbered_menu)
def update_menu(self, item_: tuple = None, no_dialog=False):
self.menu_items, self.numbered_menu = self.create_menu(item_=item_)
if not no_dialog:
CustomDialog(
"Menu Updated! Press Enter to Refresh. ⏎", msg_type="info"
).mainloop()
def update_stats(self):
threading.Thread(target=self._update_stats_thread, daemon=True).start()
def _update_stats_thread(self):
while True:
current_stats = self.ws_interface.get_stats()
if current_stats is not None and self.previous_stats != current_stats:
self.previous_stats = current_stats
self.previous_stats_items = (current_stats, 0, None)
self.update_menu(no_dialog=True)
time.sleep(1) # Sleep for 1 second
@staticmethod
def open_webbrowser(url):
try:
webbrowser.open(url)
except Exception as e:
CustomDialog(
f"Failed to open the browser. Here is the URL: {url}\nError: {e}",
msg_type="error",
).mainloop()
def _on_update(self):
TaskbarPanel.open_webbrowser(self.new_version_available[3])
def _on_connect(self):
if self.on_connect_callback:
try:
self.on_connect_callback()
except Exception as e:
pass
self.is_connected = True
self.update_menu(no_dialog=True)
def _on_disconnect(self):
if self.on_disconnect_callback:
self.on_disconnect_callback()
if self.ws_interface is not None and self.ws_interface.is_auto_reconnecting:
threading.Thread(target=self._wait_to_disconnect, daemon=True).start()
else:
self.is_connected = False
self.update_menu(no_dialog=True)
def _wait_to_disconnect(self):
"""
Wait for the auto-reconnect timer to expire before disconnecting.
"""
if self.ws_interface is None:
return
timeout = math.ceil(self.ws_interface.get_total_timeout() / 1000)
while timeout > 0:
self.is_disconnecting = True
self.disconnecting_items = (
f"⏳ Disconnecting... ({timeout} sec)",
0,
None,
) # text, location, callback
self.update_menu(no_dialog=True)
time.sleep(1) # seconds
timeout -= 1
self.is_disconnecting = False
self.disconnecting_items = None
self.ws_interface.is_auto_reconnecting = False
self.is_connected = False
self.update_menu()
def _open_homepage(self):
TaskbarPanel.open_webbrowser(self.config.data["server_url"])
def _open_github(self):
TaskbarPanel.open_webbrowser(self.github_url)
def _open_help(self):
TaskbarPanel.open_webbrowser(HELP_URL)
def _open_donate(self):
if self.donation_url is not None:
TaskbarPanel.open_webbrowser(self.donation_url)
def open_location(self, path):
if PLATFORM == WINDOWS:
os.startfile(path)
elif PLATFORM == MACOS:
subprocess.run(["open", path])
elif PLATFORM.startswith(LINUX):
subprocess.run(["xdg-open", path])
def _open_logs(self):
log_file_path = os.path.join(get_program_files_directory(), LOG_FILE_NAME)
if os.path.exists(log_file_path):
try:
self.open_location(log_file_path)
except Exception as e:
CustomDialog(
f"Failed to open the log file '{log_file_path}'.\nError: {e}",
msg_type="error",
).mainloop()
else:
CustomDialog(
f"Log file not found at '{log_file_path}'.", msg_type="error"
).mainloop()
def _open_program_location(self):
try:
program_location = get_program_files_directory()
self.open_location(program_location)
except Exception as e:
CustomDialog(
f"Failed to open the program location '{program_location}'.\nError: {e}",
msg_type="error",
).mainloop()
def enable_files_download(self, files):
self.is_file_download_enabled = True
self.file_download_items = (
"📥 Download File(s)",
0,
lambda: self._on_download(files),
)
self.update_menu()
def disable_files_download(self):
self.is_file_download_enabled = False
self.file_download_items = None
self.update_menu()
def _on_download(self, files):
try:
if self.config.data["default_file_download_location"] != "":
target_directory = self.config.data["default_file_download_location"]
else:
target_directory = input("Enter the location to save file(s): ").strip()
if not target_directory:
CustomDialog(
"No input provided. Please enter a valid directory path.",
msg_type="error",
).mainloop()
return
if not os.path.isdir(target_directory):
CustomDialog("Invalid directory path.", msg_type="error").mainloop()
return
if not os.access(target_directory, os.W_OK):
CustomDialog(
"Insufficient permissions to write to the directory.",
msg_type="error",
).mainloop()
return
CustomDialog(
f"Saving files to: {target_directory}", msg_type="info"
).mainloop()
# Save each file to the chosen directory
for filename, file_obj in files.items():
file_path = os.path.join(target_directory, filename)
with open(file_path, "wb") as f:
f.write(file_obj.getvalue())
logging.debug(f"Saved: {file_path}")
CustomDialog("Done.", msg_type="success").mainloop()
except Exception as e:
msg = f"An error occurred while downloading files. Error: {e}"
logging.error(msg)
CustomDialog(
msg,
msg_type="error",
).mainloop()
def _on_logoff(self):
try:
if self.on_logoff_callback:
self.on_logoff_callback()
self._on_quit()
except Exception as e:
CustomDialog(
f"An error occurred while logging off: {e}", msg_type="error"
).mainloop()
def _on_quit(self):
self.loop_terminate = True