update
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Ax-Shell utilities package.
|
||||
Contains helper functions and utility classes.
|
||||
"""
|
||||
@@ -0,0 +1,180 @@
|
||||
from typing import cast
|
||||
|
||||
import fabric
|
||||
from fabric import Property, Service, Signal
|
||||
from gi.repository import GLib, Gtk
|
||||
|
||||
|
||||
class Animator(Service):
|
||||
@Signal
|
||||
def finished(self) -> None: ...
|
||||
|
||||
@Property(tuple[float, float, float, float], "read-write")
|
||||
def bezier_curve(self) -> tuple[float, float, float, float]:
|
||||
return self._bezier_curve
|
||||
|
||||
@bezier_curve.setter
|
||||
def bezier_curve(self, value: tuple[float, float, float, float]):
|
||||
self._bezier_curve = value
|
||||
return
|
||||
|
||||
@Property(float, "read-write")
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: float):
|
||||
self._value = value
|
||||
return
|
||||
|
||||
@Property(float, "read-write")
|
||||
def max_value(self):
|
||||
return self._max_value
|
||||
|
||||
@max_value.setter
|
||||
def max_value(self, value: float):
|
||||
self._max_value = value
|
||||
return
|
||||
|
||||
@Property(float, "read-write")
|
||||
def min_value(self):
|
||||
return self._min_value
|
||||
|
||||
@min_value.setter
|
||||
def min_value(self, value: float):
|
||||
self._min_value = value
|
||||
return
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def playing(self):
|
||||
return self._playing
|
||||
|
||||
@playing.setter
|
||||
def playing(self, value: bool):
|
||||
self._playing = value
|
||||
return
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def repeat(self):
|
||||
return self._repeat
|
||||
|
||||
@repeat.setter
|
||||
def repeat(self, value: bool):
|
||||
self._repeat = value
|
||||
return
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bezier_curve: tuple[float, float, float, float],
|
||||
duration: float,
|
||||
min_value: float = 0.0,
|
||||
max_value: float = 1.0,
|
||||
repeat: bool = False,
|
||||
tick_widget: Gtk.Widget | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self._bezier_curve = (1, 0, 1, 1)
|
||||
self._duration = 5
|
||||
self._value = 0.0
|
||||
self._min_value = 0.0
|
||||
self._max_value = 1.0
|
||||
self._repeat = False
|
||||
|
||||
self.bezier_curve = bezier_curve
|
||||
self.duration = duration
|
||||
self.value = min_value
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.repeat = repeat
|
||||
|
||||
self.playing = False
|
||||
self._start_time = None
|
||||
self._tick_handler = None
|
||||
self._timeline_pos = 0
|
||||
self._tick_widget = tick_widget
|
||||
|
||||
def do_get_time_now(self):
|
||||
return GLib.get_monotonic_time() / 1_000_000
|
||||
|
||||
def do_lerp(self, start: float, end: float, time: float) -> float:
|
||||
return start + (end - start) * time
|
||||
|
||||
def do_interpolate_cubic_bezier(self, time: float) -> float:
|
||||
y_points = (0, self.bezier_curve[1], self.bezier_curve[3], 1)
|
||||
return (
|
||||
(1 - time) ** 3 * y_points[0]
|
||||
+ 3 * (1 - time) ** 2 * time * y_points[1]
|
||||
+ 3 * (1 - time) * time**2 * y_points[2]
|
||||
+ time**3 * y_points[3]
|
||||
)
|
||||
|
||||
def do_ease(self, time: float) -> float:
|
||||
return self.do_lerp(
|
||||
self.min_value, self.max_value, self.do_interpolate_cubic_bezier(time)
|
||||
)
|
||||
|
||||
def do_update_value(self, delta_time: float):
|
||||
if not self.playing:
|
||||
return
|
||||
|
||||
elapsed_time = delta_time - cast(float, self._start_time)
|
||||
|
||||
self._timeline_pos = min(1, elapsed_time / self.duration)
|
||||
|
||||
self.value = self.do_ease(self._timeline_pos)
|
||||
|
||||
if not self._timeline_pos >= 1:
|
||||
return
|
||||
|
||||
if not self.repeat:
|
||||
self.value = self.max_value
|
||||
self.finished()
|
||||
self.pause()
|
||||
return
|
||||
|
||||
self._start_time = delta_time
|
||||
self._timeline_pos = 0
|
||||
return
|
||||
|
||||
def do_handle_tick(self, *_):
|
||||
current_time = self.do_get_time_now()
|
||||
self.do_update_value(current_time)
|
||||
return True
|
||||
|
||||
def do_remove_tick_handlers(self):
|
||||
if self._tick_handler:
|
||||
if self._tick_widget:
|
||||
self._tick_widget.remove_tick_callback(self._tick_handler)
|
||||
else:
|
||||
GLib.source_remove(self._tick_handler)
|
||||
self._tick_handler = None
|
||||
return
|
||||
|
||||
def play(self):
|
||||
if self.playing:
|
||||
return
|
||||
|
||||
self._start_time = self.do_get_time_now()
|
||||
|
||||
if not self._tick_handler:
|
||||
if self._tick_widget:
|
||||
self._tick_handler = self._tick_widget.add_tick_callback(
|
||||
self.do_handle_tick
|
||||
)
|
||||
else:
|
||||
self._tick_handler = GLib.timeout_add(16, self.do_handle_tick)
|
||||
|
||||
self.playing = True
|
||||
return
|
||||
|
||||
def pause(self):
|
||||
self.playing = False
|
||||
return self.do_remove_tick_handlers()
|
||||
|
||||
def stop(self):
|
||||
if not self._tick_handler:
|
||||
self._timeline_pos = 0
|
||||
self.playing = False
|
||||
return
|
||||
return self.do_remove_tick_handlers()
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Utility functions for running subprocess operations asynchronously without blocking the UI.
|
||||
This module provides helper functions to prevent UI freezes when executing external processes.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from typing import Callable, List, Optional, Union
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
def run_async_subprocess(
|
||||
command: Union[str, List[str]],
|
||||
on_success: Optional[Callable] = None,
|
||||
on_error: Optional[Callable[[Exception], None]] = None,
|
||||
on_complete: Optional[Callable[[], None]] = None,
|
||||
thread_name: str = "async-subprocess"
|
||||
) -> None:
|
||||
"""
|
||||
Run a subprocess command asynchronously in a background thread.
|
||||
|
||||
Args:
|
||||
command: Command to execute (string or list of strings)
|
||||
on_success: Callback function to call on successful completion
|
||||
on_error: Callback function to call when an error occurs (receives exception)
|
||||
on_complete: Callback function to call when operation completes (success or error)
|
||||
thread_name: Name for the background thread
|
||||
"""
|
||||
def worker_thread(user_data):
|
||||
"""Background thread worker function"""
|
||||
try:
|
||||
if isinstance(command, str):
|
||||
subprocess.run(command, shell=True, check=True)
|
||||
else:
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
# Schedule success callback on main thread
|
||||
if on_success:
|
||||
GLib.idle_add(lambda: (on_success(), False))
|
||||
|
||||
except Exception as e:
|
||||
# Schedule error callback on main thread
|
||||
if on_error:
|
||||
GLib.idle_add(lambda: (on_error(e), False))
|
||||
finally:
|
||||
# Schedule completion callback on main thread
|
||||
if on_complete:
|
||||
GLib.idle_add(lambda: (on_complete(), False))
|
||||
|
||||
GLib.Thread.new(thread_name, worker_thread, None)
|
||||
|
||||
|
||||
def check_process_async(
|
||||
process_name: str,
|
||||
on_running: Optional[Callable[[], None]] = None,
|
||||
on_not_running: Optional[Callable[[], None]] = None,
|
||||
on_error: Optional[Callable[[Exception], None]] = None,
|
||||
thread_name: str = "check-process"
|
||||
) -> None:
|
||||
"""
|
||||
Check if a process is running asynchronously.
|
||||
|
||||
Args:
|
||||
process_name: Name of the process to check (used with pgrep)
|
||||
on_running: Callback function to call if process is running
|
||||
on_not_running: Callback function to call if process is not running
|
||||
on_error: Callback function to call when an error occurs
|
||||
thread_name: Name for the background thread
|
||||
"""
|
||||
def worker_thread(user_data):
|
||||
"""Background thread worker function"""
|
||||
try:
|
||||
subprocess.check_output(["pgrep", process_name])
|
||||
# Process is running
|
||||
if on_running:
|
||||
GLib.idle_add(lambda: (on_running(), False))
|
||||
except subprocess.CalledProcessError:
|
||||
# Process is not running
|
||||
if on_not_running:
|
||||
GLib.idle_add(lambda: (on_not_running(), False))
|
||||
except Exception as e:
|
||||
# Other error occurred
|
||||
if on_error:
|
||||
GLib.idle_add(lambda: (on_error(e), False))
|
||||
|
||||
GLib.Thread.new(thread_name, worker_thread, None)
|
||||
|
||||
|
||||
def run_command_with_output_async(
|
||||
command: Union[str, List[str]],
|
||||
on_success: Optional[Callable[[bytes], None]] = None,
|
||||
on_error: Optional[Callable[[Exception], None]] = None,
|
||||
thread_name: str = "command-output"
|
||||
) -> None:
|
||||
"""
|
||||
Run a command and capture its output asynchronously.
|
||||
|
||||
Args:
|
||||
command: Command to execute (string or list of strings)
|
||||
on_success: Callback function to call with command output on success
|
||||
on_error: Callback function to call when an error occurs
|
||||
thread_name: Name for the background thread
|
||||
"""
|
||||
def worker_thread(user_data):
|
||||
"""Background thread worker function"""
|
||||
try:
|
||||
if isinstance(command, str):
|
||||
result = subprocess.run(command, shell=True, capture_output=True, check=True)
|
||||
else:
|
||||
result = subprocess.run(command, capture_output=True, check=True)
|
||||
|
||||
# Schedule success callback with output on main thread
|
||||
if on_success:
|
||||
GLib.idle_add(lambda: (on_success(result.stdout), False))
|
||||
|
||||
except Exception as e:
|
||||
# Schedule error callback on main thread
|
||||
if on_error:
|
||||
GLib.idle_add(lambda: (on_error(e), False))
|
||||
|
||||
GLib.Thread.new(thread_name, worker_thread, None)
|
||||
@@ -0,0 +1,14 @@
|
||||
class Colors:
|
||||
"""Class to define colors for terminal output"""
|
||||
|
||||
# Reference: https://stackoverflow.com/questions/287871/print-in-terminal-with-colors-using-python
|
||||
HEADER = "\033[95m"
|
||||
INFO = "\033[94m"
|
||||
OKCYAN = "\033[96m"
|
||||
OKGREEN = "\033[92m"
|
||||
WARNING = "\033[93m"
|
||||
ERROR = "\033[91m"
|
||||
ENDC = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
UNDERLINE = "\033[4m"
|
||||
RESET = "\033[0m"
|
||||
@@ -0,0 +1,449 @@
|
||||
import requests
|
||||
|
||||
|
||||
class Units():
|
||||
def __init__(self):
|
||||
self.WEIGHT_CHART: dict[str, tuple[float, float]] = {
|
||||
"kilogram": (1, 1),
|
||||
"kg": (1, 1),
|
||||
"tonne": (1000, 0.001),
|
||||
"ton": (1000, 0.001),
|
||||
"gram": (1e-3, 1e3),
|
||||
"g": (1e-3, 1e3),
|
||||
"milligram": (1e-6, 1e6),
|
||||
"mg": (1e-6, 1e6),
|
||||
"metric-ton": (1000, 0.001),
|
||||
"metric-tonne": (1000, 0.001),
|
||||
"long-ton": (1016.04608, 0.0009842073),
|
||||
"short-ton": (907.184, 0.0011023122),
|
||||
"pound": (0.453592, 2.2046244202),
|
||||
"lb": (0.453592, 2.2046244202),
|
||||
"stone": (6.35029, 0.1574731728),
|
||||
"st": (6.35029, 0.1574731728),
|
||||
"ounce": (0.0283495, 35.273990723),
|
||||
"oz": (0.0283495, 35.273990723),
|
||||
"carrat": (0.0002, 5000),
|
||||
"ct": (0.0002, 5000),
|
||||
"atomic-mass-unit": (1.660540199e-27, 6.022136652e26),
|
||||
}
|
||||
|
||||
self.LENGTH_CHART: dict[str, float] = {
|
||||
# meter
|
||||
"m": 1,
|
||||
"M": 1,
|
||||
"meter": 1,
|
||||
# kilometer
|
||||
"km": 1e3,
|
||||
"KM": 1e3,
|
||||
"kilometer": 1e3,
|
||||
# centimeter
|
||||
"cm": 1e-2,
|
||||
"CM": 1e-2,
|
||||
"centimeter": 1e-2,
|
||||
# millimeter
|
||||
"mm": 1e-3,
|
||||
"MM": 1e-3,
|
||||
"millimeter": 1e-3,
|
||||
# micrometer
|
||||
"um": 1e-6,
|
||||
"UM": 1e-6,
|
||||
"micrometer": 1e-6,
|
||||
# nanometer
|
||||
"nm": 1e-9,
|
||||
"NM": 1e-9,
|
||||
"nanometer": 1e-9,
|
||||
# mile
|
||||
"mi": 1609.344,
|
||||
"MI": 1609.344,
|
||||
"mile": 1609.344,
|
||||
# yard
|
||||
"yd": 0.9144,
|
||||
"YD": 0.9144,
|
||||
"yard": 0.9144,
|
||||
# foot
|
||||
"ft": 0.3048,
|
||||
"FT": 0.3048,
|
||||
"foot": 0.3048,
|
||||
"feet": 0.3048,
|
||||
# inch
|
||||
"in": 0.0254,
|
||||
"IN": 0.0254,
|
||||
"inch": 0.0254,
|
||||
"inches": 0.0254,
|
||||
# nautical mile
|
||||
"nmi": 1852,
|
||||
"NMI": 1852,
|
||||
"nautical-mile": 1852,
|
||||
}
|
||||
|
||||
self.STORAGE_TYPE_CHART: dict[str, float] = {
|
||||
"bit": 1,
|
||||
"byte": 8,
|
||||
"B": 8,
|
||||
"kilobyte": 8192,
|
||||
"KB": 8192,
|
||||
"megabyte": 8388608,
|
||||
"MB": 8388608,
|
||||
"gigabyte": 8589934592,
|
||||
"GB": 8589934592,
|
||||
"terabyte": 8796093022208,
|
||||
"TB": 8796093022208,
|
||||
"petabyte": 9007199254740992,
|
||||
"PB": 9007199254740992,
|
||||
"exabyte": 9223372036854775808,
|
||||
"EB": 9223372036854775808,
|
||||
}
|
||||
|
||||
self.TEMPERATURE_CHART = {
|
||||
"celsius": (lambda v: v + 273.15, lambda v: v - 273.15),
|
||||
"c": (lambda v: v + 273.15, lambda v: v - 273.15),
|
||||
"fahrenheit": (lambda v: (v - 32) * 5/9 + 273.15, lambda v: (v - 273.15) * 9/5 + 32),
|
||||
"f": (lambda v: (v - 32) * 5/9 + 273.15, lambda v: (v - 273.15) * 9/5 + 32),
|
||||
"kelvin": (lambda v: v, lambda v: v),
|
||||
"k": (lambda v: v, lambda v: v),
|
||||
"rankine": (lambda v: v * 5/9, lambda v: v * 9/5),
|
||||
"reaumur": (lambda v: v * 5/4 + 273.15, lambda v: (v - 273.15) * 4/5),
|
||||
}
|
||||
|
||||
self.TIME_CHART: dict[str, float] = {
|
||||
"second": 1,
|
||||
"s": 1,
|
||||
"minute": 60,
|
||||
"min": 60,
|
||||
"m": 60,
|
||||
"hour": 3600,
|
||||
"h": 3600,
|
||||
"milisecond": 1e-3,
|
||||
"ms": 1e-3,
|
||||
"day": 86400,
|
||||
"d": 86400,
|
||||
"week": 604800,
|
||||
"w": 604800,
|
||||
"fortnight": 1209600,
|
||||
"month": 2628000, # Approximation (30.44 days)
|
||||
"mo": 2628000, # Approximation (30.44 days)
|
||||
"year": 31536000, # Approximation (365 days)
|
||||
"yr": 31536000, # Approximation (365 days)
|
||||
"decade": 315360000, # Approximation (10 years)
|
||||
"dec": 315360000, # Approximation (10 years)
|
||||
"century": 3153600000, # Approximation (100 years)
|
||||
"cent": 3153600000, # Approximation (100 years)
|
||||
"millennium": 31536000000, # Approximation (1000 years)
|
||||
"millenia": 31536000000, # Approximation (1000 years)
|
||||
}
|
||||
|
||||
self.LIQUID_VOLUME_CHART: dict[str, float] = {
|
||||
"liter": 1,
|
||||
"l": 1,
|
||||
"milliliter": 1e-3,
|
||||
"ml": 1e-3,
|
||||
"gallon": 3.78541,
|
||||
"quart": 0.946353,
|
||||
"pint": 0.473176,
|
||||
"fluid-ounce": 0.0295735,
|
||||
"fl-oz": 0.0295735,
|
||||
"oz": 0.0295735,
|
||||
"ounce": 0.0295735,
|
||||
"cup": 0.236588,
|
||||
"tablespoon": 0.0147868,
|
||||
"tbsp": 0.0147868,
|
||||
"teaspoon": 0.00492892,
|
||||
"tsp": 0.00492892,
|
||||
}
|
||||
|
||||
self.ANGLE_CHART: dict[str, float] = {
|
||||
"degree": 1,
|
||||
"deg": 1,
|
||||
"radian": 57.2958,
|
||||
"rad": 57.2958,
|
||||
"gradian": 0.9,
|
||||
"gon": 0.9,
|
||||
}
|
||||
|
||||
self.ENERGY_CHART: dict[str, float] = {
|
||||
"joule": 1,
|
||||
"j": 1,
|
||||
"kilojoule": 1000,
|
||||
"kj": 1000,
|
||||
"calorie": 4.184,
|
||||
"cal": 4.184,
|
||||
"kilocalorie": 4184,
|
||||
"kcal": 4184,
|
||||
"watt-hour": 3600,
|
||||
"wh": 3600,
|
||||
"kilowatt-hour": 3.6e6,
|
||||
"kwh": 3.6e6,
|
||||
}
|
||||
|
||||
self.SPEED_CHART: dict[str, float] = {
|
||||
"mps": 1,
|
||||
"kmph": 0.277778,
|
||||
"mph": 0.44704,
|
||||
"fps": 0.3048,
|
||||
"knot": 0.514444,
|
||||
}
|
||||
|
||||
self.PRESSURE_CHART: dict[str, float] = {
|
||||
"pascal": 1,
|
||||
"Pa": 1,
|
||||
"bar": 100000,
|
||||
"atm": 101325,
|
||||
"torr": 133.322,
|
||||
"mmHg": 133.322,
|
||||
"psi": 6894.76,
|
||||
}
|
||||
|
||||
self.FORCE_CHART: dict[str, float] = {
|
||||
"newton": 1,
|
||||
"N": 1,
|
||||
"kilonewton": 1000,
|
||||
"kN": 1000,
|
||||
"pound-force": 4.44822,
|
||||
"lbf": 4.44822,
|
||||
"dyne": 1e-5,
|
||||
}
|
||||
|
||||
self.POWER_CHART: dict[str, float] = {
|
||||
"watt": 1,
|
||||
"W": 1,
|
||||
"kilowatt": 1000,
|
||||
"kW": 1000,
|
||||
"horsepower": 745.7,
|
||||
"hp": 745.7,
|
||||
"megawatt": 1e6,
|
||||
"MW": 1e6,
|
||||
}
|
||||
|
||||
self.VOLTAGE_CHART: dict[str, float] = {
|
||||
"volt": 1,
|
||||
"V": 1,
|
||||
"millivolt": 1e-3,
|
||||
"mV": 1e-3,
|
||||
"kilovolt": 1000,
|
||||
"kV": 1000,
|
||||
"megavolt": 1e6,
|
||||
"MV": 1e6,
|
||||
}
|
||||
|
||||
self.CURRENT_CHART: dict[str, float] = {
|
||||
"ampere": 1,
|
||||
"A": 1,
|
||||
"milliampere": 1e-3,
|
||||
"mA": 1e-3,
|
||||
"microampere": 1e-6,
|
||||
"μA": 1e-6,
|
||||
}
|
||||
|
||||
self.RESISTANCE_CHART: dict[str, float] = {
|
||||
"ohm": 1,
|
||||
"Ω": 1,
|
||||
"kilohm": 1000,
|
||||
"kΩ": 1000,
|
||||
"megohm": 1e6,
|
||||
"MΩ": 1e6,
|
||||
}
|
||||
|
||||
self.CAPACITANCE_CHART: dict[str, float] = {
|
||||
"farad": 1,
|
||||
"F": 1,
|
||||
"millifarad": 1e-3,
|
||||
"mF": 1e-3,
|
||||
"microfarad": 1e-6,
|
||||
"μF": 1e-6,
|
||||
"nanofarad": 1e-9,
|
||||
"nF": 1e-9,
|
||||
}
|
||||
|
||||
self.INDUCTANCE_CHART: dict[str, float] = {
|
||||
"henry": 1,
|
||||
"H": 1,
|
||||
"millihenry": 1e-3,
|
||||
"mH": 1e-3,
|
||||
"microhenry": 1e-6,
|
||||
"μH": 1e-6,
|
||||
"nanohenry": 1e-9,
|
||||
"nH": 1e-9,
|
||||
}
|
||||
|
||||
self.FREQUENCY_CHART: dict[str, float] = {
|
||||
"hertz": 1,
|
||||
"Hz": 1,
|
||||
"kilohertz": 1e3,
|
||||
"kHz": 1e3,
|
||||
"megahertz": 1e6,
|
||||
"MHz": 1e6,
|
||||
"gigahertz": 1e9,
|
||||
"GHz": 1e9,
|
||||
}
|
||||
|
||||
self.LUMINANCE_CHART: dict[str, float] = {
|
||||
"candela": 1,
|
||||
"cd": 1,
|
||||
"lumen": 1,
|
||||
"lm": 1,
|
||||
"lux": 1,
|
||||
"lx": 1,
|
||||
}
|
||||
|
||||
self.AREA_CHART: dict[str, float] = {
|
||||
"square-meter": 1,
|
||||
"m2": 1,
|
||||
"square-kilometer": 1e6,
|
||||
"km2": 1e6,
|
||||
"hectare": 1e4,
|
||||
"ha": 1e4,
|
||||
"are": 1e2,
|
||||
"a": 1e2,
|
||||
"square-centimeter": 1e-4,
|
||||
"cm2": 1e-4,
|
||||
"square-millimeter": 1e-6,
|
||||
"mm2": 1e-6,
|
||||
}
|
||||
|
||||
# Ya no usamos currency_converter aquí.
|
||||
|
||||
|
||||
class Conversion():
|
||||
def __init__(self):
|
||||
self.units = Units()
|
||||
|
||||
def convert(self, value: float, from_type: str, to_type: str):
|
||||
"""
|
||||
Generalized conversion function que funciona con todas las categorías,
|
||||
incluyendo moneda via floatrates.com.
|
||||
"""
|
||||
# Colección de todos los charts no-monedas
|
||||
charts = {
|
||||
"WEIGHT_CHART": self.units.WEIGHT_CHART,
|
||||
"LENGTH_CHART": self.units.LENGTH_CHART,
|
||||
"TEMPERATURE_CHART": self.units.TEMPERATURE_CHART,
|
||||
"TIME_CHART": self.units.TIME_CHART,
|
||||
"LIQUID_VOLUME_CHART": self.units.LIQUID_VOLUME_CHART,
|
||||
"STORAGE_TYPE_CHART": self.units.STORAGE_TYPE_CHART,
|
||||
"ANGLE_CHART": self.units.ANGLE_CHART,
|
||||
"ENERGY_CHART": self.units.ENERGY_CHART,
|
||||
"SPEED_CHART": self.units.SPEED_CHART,
|
||||
"PRESSURE_CHART": self.units.PRESSURE_CHART,
|
||||
"FORCE_CHART": self.units.FORCE_CHART,
|
||||
"POWER_CHART": self.units.POWER_CHART,
|
||||
"VOLTAGE_CHART": self.units.VOLTAGE_CHART,
|
||||
"CURRENT_CHART": self.units.CURRENT_CHART,
|
||||
"RESISTANCE_CHART": self.units.RESISTANCE_CHART,
|
||||
"CAPACITANCE_CHART": self.units.CAPACITANCE_CHART,
|
||||
"INDUCTANCE_CHART": self.units.INDUCTANCE_CHART,
|
||||
"FREQUENCY_CHART": self.units.FREQUENCY_CHART,
|
||||
"LUMINANCE_CHART": self.units.LUMINANCE_CHART,
|
||||
"AREA_CHART": self.units.AREA_CHART,
|
||||
}
|
||||
|
||||
# 1) Revisar si está en alguno de los charts (no monedas)
|
||||
for chart_name, chart in charts.items():
|
||||
if from_type in chart and to_type in chart:
|
||||
# Temperaturas usan lambdas
|
||||
if chart_name == "TEMPERATURE_CHART":
|
||||
if from_type == to_type:
|
||||
return value
|
||||
to_kelvin = chart[from_type][0]
|
||||
from_kelvin = chart[to_type][1]
|
||||
return from_kelvin(to_kelvin(value))
|
||||
|
||||
# Handle WEIGHT_CHART separately (tuple values)
|
||||
if chart_name == "WEIGHT_CHART":
|
||||
if from_type == to_type:
|
||||
return value
|
||||
to_kg = chart[from_type][0]
|
||||
from_kg = chart[to_type][1]
|
||||
return value * to_kg * from_kg
|
||||
|
||||
# Cualquier otro chart numérico
|
||||
if from_type == to_type:
|
||||
return value
|
||||
return value * (chart[from_type] / chart[to_type])
|
||||
|
||||
# 2) Si ambos son códigos de moneda (p. ej. “USD”, “ARS”)
|
||||
# asumimos que están en mayúsculas y tienen 3 letras.
|
||||
if len(from_type) == 3 and len(to_type) == 3 and from_type.isalpha() and to_type.isalpha():
|
||||
return self._convert_currency_via_floatrates(value, from_type, to_type)
|
||||
|
||||
# 3) Si no cae en ningún caso, error.
|
||||
raise ValueError(f"Unsupported conversion: {from_type} to {to_type}")
|
||||
|
||||
def _convert_currency_via_floatrates(self, value: float, from_code: str, to_code: str) -> float:
|
||||
"""
|
||||
Convierte usando el JSON de floatrates.com:
|
||||
- Hace GET a https://www.floatrates.com/daily/{from_lower}.json
|
||||
- Toma el rate de la clave to_lower y multiplica.
|
||||
"""
|
||||
from_lower = from_code.lower()
|
||||
to_lower = to_code.lower()
|
||||
|
||||
if from_lower == to_lower:
|
||||
return value
|
||||
|
||||
url = f"https://www.floatrates.com/daily/{from_lower}.json"
|
||||
resp = requests.get(url, timeout=5)
|
||||
if resp.status_code != 200:
|
||||
raise ValueError(f"Error al obtener datos de floatrates para {from_code}")
|
||||
|
||||
data = resp.json()
|
||||
if to_lower not in data:
|
||||
raise ValueError(f"Moneda destino '{to_code}' no encontrada en la respuesta de floatrates para '{from_code}'")
|
||||
|
||||
rate = data[to_lower]["rate"]
|
||||
return value * rate
|
||||
|
||||
def parse_input_and_convert(self, input: str):
|
||||
parts = input.split()
|
||||
addition = "s" if parts[-1].endswith("s") else ""
|
||||
|
||||
if "and" in parts: # valor unidad1 and valor2 unidad2 _ a unidad_destino
|
||||
parts.remove("and")
|
||||
if len(parts) != 6:
|
||||
raise ValueError("Formato inválido. Esperado: 'value from_type and value2 from_type2 _ to_type'")
|
||||
|
||||
value1, from_type1, value2, from_type2, _, to_type = parts
|
||||
value1, value2 = float(value1), float(value2)
|
||||
from_type1 = self.clean_type(from_type1)
|
||||
from_type2 = self.clean_type(from_type2)
|
||||
to_type = self.clean_type(to_type)
|
||||
|
||||
if from_type1 == from_type2:
|
||||
return self.convert(value1 + value2, from_type1, to_type), to_type + addition
|
||||
else:
|
||||
res = 0
|
||||
res += self.convert(value1, from_type1, to_type)
|
||||
res += self.convert(value2, from_type2, to_type)
|
||||
return res, to_type + addition
|
||||
else:
|
||||
if len(parts) != 4:
|
||||
raise ValueError("Formato inválido. Esperado: 'value from_type _ to_type'")
|
||||
value, from_type, _, to_type = parts
|
||||
value = float(value)
|
||||
from_type = self.clean_type(from_type)
|
||||
to_type = self.clean_type(to_type)
|
||||
return self.convert(value, from_type, to_type), to_type + addition
|
||||
|
||||
def clean_type(self, type: str) -> str:
|
||||
"""
|
||||
Si es moneda (3 letras), lo pasa a mayúsculas.
|
||||
Si termina en 's' (y no es 'celsius'), le quita la 's' para
|
||||
las otras unidades. """
|
||||
if len(type) == 3 and type.isalpha():
|
||||
return type.upper()
|
||||
if type.endswith("s") and type.lower() != "celsius":
|
||||
# Para las tablas que tienen singular/plural
|
||||
singular = type[:-1].lower()
|
||||
# Si existe en STORAGE_TYPE_CHART, lo usamos;
|
||||
# si no, devolvemos singular en minúsculas para otros charts.
|
||||
if singular in self.units.STORAGE_TYPE_CHART:
|
||||
return singular
|
||||
return singular.lower()
|
||||
return type
|
||||
|
||||
|
||||
# Ejemplo rápido de uso:
|
||||
if __name__ == "__main__":
|
||||
conv = Conversion()
|
||||
# Convierte 10 USD a ARS:
|
||||
result, suffix = conv.parse_input_and_convert("10 USD _ ARS")
|
||||
print(f"{result:.2f} {suffix}") # Ej: "10 USD _ ARS" -> "38754.23 ARS"
|
||||
@@ -0,0 +1,235 @@
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Dict, List, Literal
|
||||
|
||||
import gi
|
||||
import psutil
|
||||
from fabric.utils import exec_shell_command, exec_shell_command_async, get_relative_path
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
from loguru import logger
|
||||
|
||||
from .colors import Colors
|
||||
from .icons import distro_text_icons
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
|
||||
class ExecutableNotFoundError(ImportError):
|
||||
"""Raised when an executable is not found."""
|
||||
|
||||
def __init__(self, executable_name: str):
|
||||
super().__init__(
|
||||
f"{Colors.ERROR}Executable {Colors.UNDERLINE}{executable_name}{Colors.RESET} not found. Please install it using your package manager." # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
# Function to escape the markup
|
||||
def parse_markup(text):
|
||||
return text
|
||||
|
||||
|
||||
# support for multiple monitors
|
||||
def for_monitors(widget):
|
||||
n = Gdk.Display.get_default().get_n_monitors() if Gdk.Display.get_default() else 1
|
||||
return [widget(i) for i in range(n)]
|
||||
|
||||
|
||||
# Function to get the system icon theme
|
||||
def copy_theme(theme: str):
|
||||
destination_file = get_relative_path("../styles/theme.scss")
|
||||
source_file = get_relative_path(f"../styles/themes/{theme}.scss")
|
||||
|
||||
if not os.path.exists(source_file):
|
||||
logger.warning(
|
||||
f"{Colors.WARNING}Warning: The theme file '{theme}.scss' was not found. Using default theme." # noqa: E501
|
||||
)
|
||||
source_file = get_relative_path("../styles/themes/catpuccin-mocha.scss")
|
||||
|
||||
try:
|
||||
with open(source_file, "r") as source_file:
|
||||
content = source_file.read()
|
||||
|
||||
# Open the destination file in write mode
|
||||
with open(destination_file, "w") as destination_file:
|
||||
destination_file.write(content)
|
||||
logger.info(f"{Colors.INFO}[THEME] '{theme}' applied successfully.")
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(
|
||||
f"{Colors.ERROR}Error: The theme file '{source_file}' was not found."
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
# Merge the parsed data with the default configuration
|
||||
def merge_defaults(data: dict, defaults: dict):
|
||||
return {**defaults, **data}
|
||||
|
||||
|
||||
# Validate the widgets
|
||||
def validate_widgets(parsed_data, default_config):
|
||||
layout = parsed_data["layout"]
|
||||
for section in layout:
|
||||
for widget in layout[section]:
|
||||
if widget not in default_config:
|
||||
raise ValueError(
|
||||
f"Invalid widget {widget} found in section {section}. Please check the widget name." # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
# Function to exclude keys from a dictionary )
|
||||
def exclude_keys(d: Dict, keys_to_exclude: List[str]) -> Dict:
|
||||
return {k: v for k, v in d.items() if k not in keys_to_exclude}
|
||||
|
||||
|
||||
# Function to format time in hours and minutes
|
||||
def format_time(secs: int):
|
||||
mm, _ = divmod(secs, 60)
|
||||
hh, mm = divmod(mm, 60)
|
||||
return "%d h %02d min" % (hh, mm)
|
||||
|
||||
|
||||
# Function to convert bytes to kilobytes, megabytes, or gigabytes
|
||||
def convert_bytes(bytes: int, to: Literal["kb", "mb", "gb"], format_spec=".1f"):
|
||||
multiplier = 1
|
||||
|
||||
if to == "mb":
|
||||
multiplier = 2
|
||||
elif to == "gb":
|
||||
multiplier = 3
|
||||
|
||||
return f"{format(bytes / (1024**multiplier), format_spec)}{to.upper()}"
|
||||
|
||||
|
||||
# Function to get the system uptime
|
||||
def uptime():
|
||||
boot_time = psutil.boot_time()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
diff = now.timestamp() - boot_time
|
||||
|
||||
# Convert the difference in seconds to hours and minutes
|
||||
hours, remainder = divmod(diff, 3600)
|
||||
minutes, _ = divmod(remainder, 60)
|
||||
|
||||
return f"{int(hours):02}:{int(minutes):02}"
|
||||
|
||||
|
||||
# Function to convert seconds to milliseconds
|
||||
def convert_seconds_to_milliseconds(seconds: int):
|
||||
return seconds * 1000
|
||||
|
||||
|
||||
# Function to check if an icon exists, otherwise use a fallback icon
|
||||
def check_icon_exists(icon_name: str, fallback_icon: str) -> str:
|
||||
if Gtk.IconTheme.get_default().has_icon(icon_name):
|
||||
return icon_name
|
||||
return fallback_icon
|
||||
|
||||
|
||||
# Function to execute a shell command asynchronously
|
||||
def play_sound(file: str):
|
||||
exec_shell_command_async(f"play {file}", None)
|
||||
|
||||
|
||||
# Function to get the distro icon
|
||||
def get_distro_icon():
|
||||
distro_id = GLib.get_os_info("ID")
|
||||
|
||||
# Search for the icon in the list
|
||||
return distro_text_icons.get(distro_id, "")
|
||||
|
||||
|
||||
# Function to check if an executable exists
|
||||
def executable_exists(executable_name):
|
||||
executable_path = shutil.which(executable_name)
|
||||
return bool(executable_path)
|
||||
|
||||
|
||||
def send_notification(
|
||||
title: str,
|
||||
body: str,
|
||||
urgency: Literal["low", "normal", "critical"],
|
||||
icon=None,
|
||||
app_name="Application",
|
||||
timeout=None,
|
||||
):
|
||||
"""
|
||||
Sends a notification using the notify-send command.
|
||||
:param title: The title of the notification
|
||||
:param body: The message body of the notification
|
||||
:param urgency: The urgency of the notification ('low', 'normal', 'critical')
|
||||
:param icon: Optional icon for the notification
|
||||
:param app_name: The application name that is sending the notification
|
||||
:param timeout: Optional timeout in milliseconds (e.g., 5000 for 5 seconds)
|
||||
"""
|
||||
# Base command
|
||||
command = [
|
||||
"notify-send",
|
||||
"--urgency",
|
||||
urgency,
|
||||
"--app-name",
|
||||
app_name,
|
||||
title,
|
||||
body,
|
||||
]
|
||||
|
||||
# Add icon if provided
|
||||
if icon:
|
||||
command.extend(["--icon", icon])
|
||||
|
||||
if timeout is not None:
|
||||
command.extend(["-t", str(timeout)])
|
||||
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to send notification: {e}")
|
||||
|
||||
|
||||
# Function to get the relative time
|
||||
def get_relative_time(mins: int) -> str:
|
||||
# Seconds
|
||||
if mins == 0:
|
||||
return "now"
|
||||
|
||||
# Minutes
|
||||
if mins < 60:
|
||||
return f"{mins} minute{'s' if mins > 1 else ''} ago"
|
||||
|
||||
# Hours
|
||||
if mins < 1440:
|
||||
hours = mins // 60
|
||||
return f"{hours} hour{'s' if hours > 1 else ''} ago"
|
||||
|
||||
# Days
|
||||
days = mins // 1440
|
||||
return f"{days} day{'s' if days > 1 else ''} ago"
|
||||
|
||||
|
||||
# Function to get the percentage of a value
|
||||
def convert_to_percent(
|
||||
current: int | float, max: int | float, is_int=True
|
||||
) -> int | float:
|
||||
if is_int:
|
||||
return int((current / max) * 100)
|
||||
else:
|
||||
return (current / max) * 100
|
||||
|
||||
|
||||
# Function to ensure the directory exists
|
||||
def ensure_dir_exists(path: str):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
|
||||
# Function to unique list
|
||||
def unique_list(lst) -> List:
|
||||
return list(set(lst))
|
||||
|
||||
|
||||
# Function to check if an app is running
|
||||
def is_app_running(app_name: str) -> bool:
|
||||
return len(exec_shell_command(f"pidof {app_name}")) != 0
|
||||
@@ -0,0 +1,254 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class GlobalKeybindHandler:
|
||||
"""
|
||||
Handler for global keybinds that redirects commands to the focused monitor.
|
||||
|
||||
This class provides methods to open notch modules, access widgets, and
|
||||
perform other actions on the currently focused monitor.
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if hasattr(self, '_initialized'):
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
self._monitor_manager = None
|
||||
|
||||
def set_monitor_manager(self, monitor_manager):
|
||||
"""Set the monitor manager reference."""
|
||||
self._monitor_manager = monitor_manager
|
||||
|
||||
def open_notch_module(self, module_name: str) -> bool:
|
||||
"""
|
||||
Open a notch module on the currently focused monitor.
|
||||
|
||||
Args:
|
||||
module_name: Name of the module to open
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return False
|
||||
|
||||
focused_monitor_id = self._monitor_manager.get_focused_monitor_id()
|
||||
|
||||
# Close any open notches on other monitors
|
||||
self._monitor_manager.close_all_notches_except(focused_monitor_id)
|
||||
|
||||
# Get notch instance for focused monitor
|
||||
notch = self._monitor_manager.get_focused_instance('notch')
|
||||
if notch and hasattr(notch, 'open_module'):
|
||||
try:
|
||||
notch.open_module(module_name)
|
||||
self._monitor_manager.set_notch_state(focused_monitor_id, True, module_name)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"GlobalKeybindHandler: Error opening module '{module_name}': {e}")
|
||||
|
||||
return False
|
||||
|
||||
def toggle_notch(self) -> bool:
|
||||
"""
|
||||
Toggle notch on the currently focused monitor.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return False
|
||||
|
||||
focused_monitor_id = self._monitor_manager.get_focused_monitor_id()
|
||||
is_open = self._monitor_manager.is_notch_open(focused_monitor_id)
|
||||
|
||||
notch = self._monitor_manager.get_focused_instance('notch')
|
||||
if notch:
|
||||
try:
|
||||
if is_open:
|
||||
if hasattr(notch, 'close'):
|
||||
notch.close()
|
||||
self._monitor_manager.set_notch_state(focused_monitor_id, False)
|
||||
else:
|
||||
if hasattr(notch, 'open'):
|
||||
notch.open()
|
||||
self._monitor_manager.set_notch_state(focused_monitor_id, True)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"GlobalKeybindHandler: Error toggling notch: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def get_dashboard_wallpapers_widget(self):
|
||||
"""
|
||||
Get the dashboard wallpapers widget from the focused monitor.
|
||||
|
||||
Returns:
|
||||
Wallpapers widget instance or None
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return None
|
||||
|
||||
notch = self._monitor_manager.get_focused_instance('notch')
|
||||
if notch and hasattr(notch, 'dashboard'):
|
||||
dashboard = notch.dashboard
|
||||
if hasattr(dashboard, 'widgets') and hasattr(dashboard.widgets, 'wallpapers'):
|
||||
return dashboard.widgets.wallpapers
|
||||
|
||||
return None
|
||||
|
||||
def get_dashboard_widget(self, widget_name: str):
|
||||
"""
|
||||
Get a specific dashboard widget from the focused monitor.
|
||||
|
||||
Args:
|
||||
widget_name: Name of the widget to get
|
||||
|
||||
Returns:
|
||||
Widget instance or None
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return None
|
||||
|
||||
notch = self._monitor_manager.get_focused_instance('notch')
|
||||
if notch and hasattr(notch, 'dashboard'):
|
||||
dashboard = notch.dashboard
|
||||
if hasattr(dashboard, 'widgets'):
|
||||
return getattr(dashboard.widgets, widget_name, None)
|
||||
|
||||
return None
|
||||
|
||||
def open_launcher(self) -> bool:
|
||||
"""Open launcher on focused monitor."""
|
||||
return self.open_notch_module('launcher')
|
||||
|
||||
def open_overview(self) -> bool:
|
||||
"""Open overview on focused monitor."""
|
||||
return self.open_notch_module('overview')
|
||||
|
||||
def open_dashboard(self) -> bool:
|
||||
"""Open dashboard on focused monitor."""
|
||||
return self.open_notch_module('dashboard')
|
||||
|
||||
def open_power_menu(self) -> bool:
|
||||
"""Open power menu on focused monitor."""
|
||||
return self.open_notch_module('power')
|
||||
|
||||
def open_toolbox(self) -> bool:
|
||||
"""Open toolbox on focused monitor."""
|
||||
return self.open_notch_module('tools')
|
||||
|
||||
def open_emoji_picker(self) -> bool:
|
||||
"""Open emoji picker on focused monitor."""
|
||||
return self.open_notch_module('emoji')
|
||||
|
||||
def open_clipboard_history(self) -> bool:
|
||||
"""Open clipboard history on focused monitor."""
|
||||
return self.open_notch_module('cliphist')
|
||||
|
||||
def get_focused_monitor_info(self) -> Optional[dict]:
|
||||
"""
|
||||
Get information about the currently focused monitor.
|
||||
|
||||
Returns:
|
||||
Monitor info dict or None
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return None
|
||||
|
||||
return self._monitor_manager.get_focused_monitor()
|
||||
|
||||
def get_all_monitors_info(self) -> list:
|
||||
"""
|
||||
Get information about all monitors.
|
||||
|
||||
Returns:
|
||||
List of monitor info dicts
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return []
|
||||
|
||||
return self._monitor_manager.get_monitors()
|
||||
|
||||
def toggle_bar(self) -> bool:
|
||||
"""
|
||||
Toggle bar visibility and force notch/dock to occlusion mode.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self._monitor_manager:
|
||||
return False
|
||||
|
||||
monitors = self._monitor_manager.get_monitors()
|
||||
|
||||
for monitor in monitors:
|
||||
bar = self._monitor_manager.get_instance(monitor['id'], 'bar')
|
||||
notch = self._monitor_manager.get_instance(monitor['id'], 'notch')
|
||||
|
||||
if bar and notch:
|
||||
try:
|
||||
current_visibility = bar.get_visible()
|
||||
bar.set_visible(not current_visibility)
|
||||
|
||||
if not current_visibility:
|
||||
# Bar is being shown - restore from occlusion
|
||||
notch.restore_from_occlusion()
|
||||
# Also restore docks on all monitors
|
||||
try:
|
||||
from modules.dock import Dock
|
||||
for dock_instance in Dock._instances:
|
||||
if hasattr(dock_instance, 'restore_from_occlusion'):
|
||||
dock_instance.restore_from_occlusion()
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
# Bar is being hidden - force occlusion
|
||||
notch.force_occlusion()
|
||||
# Also force occlusion on docks on all monitors
|
||||
try:
|
||||
from modules.dock import Dock
|
||||
for dock_instance in Dock._instances:
|
||||
if hasattr(dock_instance, 'force_occlusion'):
|
||||
dock_instance.force_occlusion()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"GlobalKeybindHandler: Error toggling bar: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Singleton accessor
|
||||
_global_keybind_handler_instance = None
|
||||
|
||||
def get_global_keybind_handler() -> GlobalKeybindHandler:
|
||||
"""Get the global GlobalKeybindHandler instance."""
|
||||
global _global_keybind_handler_instance
|
||||
if _global_keybind_handler_instance is None:
|
||||
_global_keybind_handler_instance = GlobalKeybindHandler()
|
||||
return _global_keybind_handler_instance
|
||||
|
||||
def init_global_keybind_objects():
|
||||
"""Initialize global keybind handler with monitor manager."""
|
||||
try:
|
||||
from utils.monitor_manager import get_monitor_manager
|
||||
|
||||
handler = get_global_keybind_handler()
|
||||
manager = get_monitor_manager()
|
||||
handler.set_monitor_manager(manager)
|
||||
|
||||
return handler
|
||||
except ImportError as e:
|
||||
print(f"Error initializing global keybind objects: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,56 @@
|
||||
import json
|
||||
from typing import Dict
|
||||
|
||||
import gi
|
||||
|
||||
import warnings
|
||||
|
||||
from fabric.hyprland import Hyprland
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk
|
||||
|
||||
|
||||
# IDC, Gdk.Screen.get_monitor_plug_name is deprecated
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
# Another idea is to use Gdk.Monitor.get_model() however,
|
||||
# there is no garuntee that this will be unique
|
||||
# Example: both monitors have the same model number
|
||||
# (quite common in multi monitor setups)
|
||||
|
||||
|
||||
# Also, using Gdk.Display.get_monitor_at_point(x,y)
|
||||
# does not work correctly on all wayland setups
|
||||
|
||||
|
||||
# Annoyingly, Gdk 4.0 has a solution to this with
|
||||
# Gdk.Monitor.get_description() or Gdk.Monitor.get_connector()
|
||||
# which both can be used to uniquely identify a monitor
|
||||
|
||||
|
||||
class HyprlandWithMonitors(Hyprland):
|
||||
def __init__(self, commands_only: bool = False, **kwargs):
|
||||
self.display: Gdk.Display = Gdk.Display.get_default()
|
||||
super().__init__(commands_only, **kwargs)
|
||||
|
||||
# Add new arguments
|
||||
def get_all_monitors(self) -> Dict:
|
||||
monitors = json.loads(self.send_command("j/monitors").reply)
|
||||
return {monitor["id"]: monitor["name"] for monitor in monitors}
|
||||
|
||||
def get_gdk_monitor_id_from_name(self, plug_name: str) -> int | None:
|
||||
for i in range(self.display.get_n_monitors()):
|
||||
if self.display.get_default_screen().get_monitor_plug_name(i) == plug_name:
|
||||
return i
|
||||
return None
|
||||
|
||||
def get_gdk_monitor_id(self, hyprland_id: int) -> int | None:
|
||||
monitors = self.get_all_monitors()
|
||||
if hyprland_id in monitors:
|
||||
return self.get_gdk_monitor_id_from_name(monitors[hyprland_id])
|
||||
return None
|
||||
|
||||
def get_current_gdk_monitor_id(self) -> int | None:
|
||||
active_workspace = json.loads(self.send_command("j/activeworkspace").reply)
|
||||
return self.get_gdk_monitor_id_from_name(active_workspace["monitor"])
|
||||
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import GLib, Gtk
|
||||
from loguru import logger
|
||||
|
||||
import config.data as data
|
||||
|
||||
ICON_CACHE_FILE = data.CACHE_DIR + "/icons.json"
|
||||
if not os.path.exists(data.CACHE_DIR):
|
||||
os.makedirs(data.CACHE_DIR)
|
||||
|
||||
|
||||
class IconResolver:
|
||||
def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"):
|
||||
if os.path.exists(ICON_CACHE_FILE):
|
||||
with open(ICON_CACHE_FILE) as f:
|
||||
try:
|
||||
self._icon_dict = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
logger.info("[ICONS] Cache file does not exist or is corrupted")
|
||||
self._icon_dict = {}
|
||||
else:
|
||||
self._icon_dict = {}
|
||||
|
||||
self.default_applicaiton_icon = default_applicaiton_icon
|
||||
|
||||
def get_icon_name(self, app_id: str):
|
||||
if app_id in self._icon_dict:
|
||||
return self._icon_dict[app_id]
|
||||
new_icon = self._compositor_find_icon(app_id)
|
||||
logger.info(
|
||||
f"[ICONS] found new icon: '{new_icon}' for app id: '{app_id}', storing..."
|
||||
)
|
||||
self._store_new_icon(app_id, new_icon)
|
||||
return new_icon
|
||||
|
||||
def get_icon_pixbuf(self, app_id: str, size: int = 16):
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
icon_name = self.get_icon_name(app_id)
|
||||
try:
|
||||
# Try to load the resolved icon.
|
||||
return icon_theme.load_icon(icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE)
|
||||
except GLib.Error as primary_error:
|
||||
logger.warning(
|
||||
f"Warning: Icon '{icon_name}' not found in theme. Error: {primary_error}"
|
||||
)
|
||||
try:
|
||||
# Fallback to the default application icon.
|
||||
return icon_theme.load_icon(
|
||||
self.default_applicaiton_icon, size, Gtk.IconLookupFlags.FORCE_SIZE
|
||||
)
|
||||
except GLib.Error as fallback_error:
|
||||
logger.error(
|
||||
f"Error: Fallback icon '{self.default_applicaiton_icon}' also not found. Error: {fallback_error}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _store_new_icon(self, app_id: str, icon: str):
|
||||
self._icon_dict[app_id] = icon
|
||||
with open(ICON_CACHE_FILE, "w") as f:
|
||||
json.dump(self._icon_dict, f)
|
||||
|
||||
def _get_icon_from_desktop_file(self, desktop_file_path: str):
|
||||
# Retrieve the icon specified in the [Desktop Entry] section.
|
||||
with open(desktop_file_path) as f:
|
||||
for line in f.readlines():
|
||||
if "Icon=" in line:
|
||||
return "".join(line[5:].split())
|
||||
return self.default_applicaiton_icon
|
||||
|
||||
def _get_desktop_file(self, app_id: str) -> str | None:
|
||||
data_dirs = GLib.get_system_data_dirs()
|
||||
for data_dir in data_dirs:
|
||||
data_dir = os.path.join(data_dir, "applications")
|
||||
if os.path.exists(data_dir):
|
||||
files = os.listdir(data_dir)
|
||||
matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()]
|
||||
if matching:
|
||||
return os.path.join(data_dir, matching[0])
|
||||
for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))):
|
||||
matching = [s for s in files if word.lower() in s.lower()]
|
||||
if matching:
|
||||
return os.path.join(data_dir, matching[0])
|
||||
return None
|
||||
|
||||
def _compositor_find_icon(self, app_id: str):
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
if icon_theme.has_icon(app_id):
|
||||
return app_id
|
||||
if icon_theme.has_icon(app_id + "-desktop"):
|
||||
return app_id + "-desktop"
|
||||
desktop_file = self._get_desktop_file(app_id)
|
||||
return (
|
||||
self._get_icon_from_desktop_file(desktop_file)
|
||||
if desktop_file
|
||||
else self.default_applicaiton_icon
|
||||
)
|
||||
@@ -0,0 +1,550 @@
|
||||
common_text_icons = {
|
||||
"playing": "",
|
||||
"paused": "",
|
||||
"power": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"updates": "",
|
||||
"thermometer": "",
|
||||
}
|
||||
|
||||
distro_text_icons = {
|
||||
"deepin": "",
|
||||
"fedora": "",
|
||||
"arch": "",
|
||||
"nixos": "",
|
||||
"debian": "",
|
||||
"opensuse-tumbleweed": "",
|
||||
"ubuntu": "",
|
||||
"endeavouros": "",
|
||||
"manjaro": "",
|
||||
"popos": "",
|
||||
"garuda": "",
|
||||
"zorin": "",
|
||||
"mxlinux": "",
|
||||
"arcolinux": "",
|
||||
"gentoo": "",
|
||||
"artix": "",
|
||||
"centos": "",
|
||||
"hyperbola": "",
|
||||
"kubuntu": "",
|
||||
"mandriva": "",
|
||||
"xerolinux": "",
|
||||
"parabola": "",
|
||||
"void": "",
|
||||
"linuxmint": "",
|
||||
"archlabs": "",
|
||||
"devuan": "",
|
||||
"freebsd": "",
|
||||
"openbsd": "",
|
||||
"slackware": "",
|
||||
}
|
||||
|
||||
# sourced from wttr.in
|
||||
weather_text_icons = {
|
||||
"113": {"description": "Sunny", "icon": ""},
|
||||
"116": {"description": "PartlyCloudy", "icon": ""},
|
||||
"119": {"description": "Cloudy", "icon": ""},
|
||||
"122": {"description": "VeryCloudy", "icon": ""},
|
||||
"143": {"description": "Fog", "icon": ""},
|
||||
"176": {"description": "LightShowers", "icon": ""},
|
||||
"179": {"description": "LightSleetShowers", "icon": ""},
|
||||
"182": {"description": "LightSleet", "icon": ""},
|
||||
"185": {"description": "LightSleet", "icon": ""},
|
||||
"200": {"description": "ThunderyShowers", "icon": ""},
|
||||
"227": {"description": "LightSnow", "icon": ""},
|
||||
"230": {"description": "HeavySnow", "icon": ""},
|
||||
"248": {"description": "Fog", "icon": ""},
|
||||
"260": {"description": "Fog", "icon": ""},
|
||||
"263": {"description": "LightShowers", "icon": ""},
|
||||
"266": {"description": "LightRain", "icon": ""},
|
||||
"281": {"description": "LightSleet", "icon": ""},
|
||||
"284": {"description": "LightSleet", "icon": ""},
|
||||
"293": {"description": "LightRain", "icon": ""},
|
||||
"296": {"description": "LightRain", "icon": ""},
|
||||
"299": {"description": "HeavyShowers", "icon": ""},
|
||||
"302": {"description": "HeavyRain", "icon": ""},
|
||||
"305": {"description": "HeavyShowers", "icon": ""},
|
||||
"308": {"description": "HeavyRain", "icon": ""},
|
||||
"311": {"description": "LightSleet", "icon": ""},
|
||||
"314": {"description": "LightSleet", "icon": ""},
|
||||
"317": {"description": "LightSleet", "icon": ""},
|
||||
"320": {"description": "LightSnow", "icon": ""},
|
||||
"323": {"description": "LightSnowShowers", "icon": ""},
|
||||
"326": {"description": "LightSnowShowers", "icon": ""},
|
||||
"329": {"description": "HeavySnow", "icon": ""},
|
||||
"332": {"description": "HeavySnow", "icon": ""},
|
||||
"335": {"description": "HeavySnowShowers", "icon": ""},
|
||||
"338": {"description": "HeavySnow", "icon": ""},
|
||||
"350": {"description": "LightSleet", "icon": ""},
|
||||
"353": {"description": "LightShowers", "icon": ""},
|
||||
"356": {"description": "HeavyShowers", "icon": ""},
|
||||
"359": {"description": "HeavyRain", "icon": ""},
|
||||
"362": {"description": "LightSleetShowers", "icon": ""},
|
||||
"365": {"description": "LightSleetShowers", "icon": ""},
|
||||
"368": {"description": "LightSnowShowers", "icon": ""},
|
||||
"371": {"description": "HeavySnowShowers", "icon": ""},
|
||||
"374": {"description": "LightSleetShowers", "icon": ""},
|
||||
"377": {"description": "LightSleet", "icon": ""},
|
||||
"386": {"description": "ThunderyShowers", "icon": ""},
|
||||
"389": {"description": "ThunderyHeavyRain", "icon": ""},
|
||||
"392": {"description": "ThunderySnowShowers", "icon": ""},
|
||||
"395": {"description": "HeavySnowShowers", "icon": ""},
|
||||
}
|
||||
|
||||
weather_text_icons_v2 = {
|
||||
"113": {
|
||||
"description": "Sunny",
|
||||
"icon": "",
|
||||
"image": "clear-day",
|
||||
"icon-night": "",
|
||||
"image-night": "clear-night",
|
||||
},
|
||||
"116": {
|
||||
"description": "PartlyCloudy",
|
||||
"icon": "",
|
||||
"image": "cloudy",
|
||||
"icon-night": "",
|
||||
"image-night": "cloudy",
|
||||
},
|
||||
"119": {
|
||||
"description": "Cloudy",
|
||||
"icon": "",
|
||||
"image": "cloudy",
|
||||
"icon-night": "",
|
||||
"image-night": "cloudy",
|
||||
},
|
||||
"122": {
|
||||
"description": "VeryCloudy",
|
||||
"icon": "",
|
||||
"image": "cloudy",
|
||||
"icon-night": "",
|
||||
"image-night": "cloudy",
|
||||
},
|
||||
"143": {
|
||||
"description": "Fog",
|
||||
"icon": "",
|
||||
"image": "fog",
|
||||
"icon-night": "",
|
||||
"image-night": "fog",
|
||||
},
|
||||
"176": {
|
||||
"description": "LightShowers",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"179": {
|
||||
"description": "LightSleetShowers",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"182": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"185": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"200": {
|
||||
"description": "ThunderyShowers",
|
||||
"icon": "",
|
||||
"image": "thunderstorms",
|
||||
"icon-night": "",
|
||||
"image-night": "thunderstorms",
|
||||
},
|
||||
"227": {
|
||||
"description": "LightSnow",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"230": {
|
||||
"description": "HeavySnow",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"248": {
|
||||
"description": "Fog",
|
||||
"icon": "",
|
||||
"image": "fog",
|
||||
"icon-night": "",
|
||||
"image-night": "fog",
|
||||
},
|
||||
"260": {
|
||||
"description": "Fog",
|
||||
"icon": "",
|
||||
"image": "fog",
|
||||
"icon-night": "",
|
||||
"image-night": "fog",
|
||||
},
|
||||
"263": {
|
||||
"description": "LightShowers",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"266": {
|
||||
"description": "LightRain",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"281": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"284": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"293": {
|
||||
"description": "LightRain",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"296": {
|
||||
"description": "LightRain",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"299": {
|
||||
"description": "HeavyShowers",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"302": {
|
||||
"description": "HeavyRain",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"305": {
|
||||
"description": "HeavyShowers",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"308": {
|
||||
"description": "HeavyRain",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"311": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"314": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"317": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"320": {
|
||||
"description": "LightSnow",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"323": {
|
||||
"description": "LightSnowShowers",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"326": {
|
||||
"description": "LightSnowShowers",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"329": {
|
||||
"description": "HeavySnow",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"332": {
|
||||
"description": "HeavySnow",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"335": {
|
||||
"description": "HeavySnowShowers",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"338": {
|
||||
"description": "HeavySnow",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"350": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"353": {
|
||||
"description": "LightShowers",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"356": {
|
||||
"description": "HeavyShowers",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"359": {
|
||||
"description": "HeavyRain",
|
||||
"icon": "",
|
||||
"image": "rain",
|
||||
"icon-night": "",
|
||||
"image-night": "rain",
|
||||
},
|
||||
"362": {
|
||||
"description": "LightSleetShowers",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"365": {
|
||||
"description": "HeavySleetShowers",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"368": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"371": {
|
||||
"description": "HeavySleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
"374": {
|
||||
"description": "HeavySnowShowers",
|
||||
"icon": "",
|
||||
"image": "snow",
|
||||
"icon-night": "",
|
||||
"image-night": "snow",
|
||||
},
|
||||
"377": {
|
||||
"description": "LightSleet",
|
||||
"icon": "",
|
||||
"image": "sleet",
|
||||
"icon-night": "",
|
||||
"image-night": "sleet",
|
||||
},
|
||||
}
|
||||
|
||||
volume_text_icons = {
|
||||
"overamplified": "",
|
||||
"high": "",
|
||||
"medium": "",
|
||||
"low": "",
|
||||
"muted": "",
|
||||
}
|
||||
|
||||
volume_text_icons = {
|
||||
"overamplified": "",
|
||||
"high": "",
|
||||
"medium": "",
|
||||
"low": "",
|
||||
"muted": "",
|
||||
}
|
||||
|
||||
brightness_text_icons = {
|
||||
"off": "", # lowest brightness
|
||||
"low": "",
|
||||
"medium": "",
|
||||
"high": "", # highest brightness
|
||||
}
|
||||
|
||||
icons = {
|
||||
"missing": "image-missing-symbolic",
|
||||
"nix": {
|
||||
"nix": "nix-snowflake-symbolic",
|
||||
},
|
||||
"app": {
|
||||
"terminal": "terminal-symbolic",
|
||||
},
|
||||
"fallback": {
|
||||
"executable": "application-x-executable",
|
||||
"notification": "dialog-information-symbolic",
|
||||
"video": "video-x-generic-symbolic",
|
||||
"audio": "audio-x-generic-symbolic",
|
||||
},
|
||||
"ui": {
|
||||
"close": "window-close-symbolic",
|
||||
"colorpicker": "color-select-symbolic",
|
||||
"info": "info-symbolic",
|
||||
"link": "external-link-symbolic",
|
||||
"lock": "system-lock-screen-symbolic",
|
||||
"menu": "open-menu-symbolic",
|
||||
"refresh": "view-refresh-symbolic",
|
||||
"search": "system-search-symbolic",
|
||||
"settings": "emblem-system-symbolic",
|
||||
"themes": "preferences-desktop-theme-symbolic",
|
||||
"tick": "object-select-symbolic",
|
||||
"time": "hourglass-symbolic",
|
||||
"toolbars": "toolbars-symbolic",
|
||||
"warning": "dialog-warning-symbolic",
|
||||
"avatar": "avatar-default-symbolic",
|
||||
"arrow": {
|
||||
"right": "pan-end-symbolic",
|
||||
"left": "pan-start-symbolic",
|
||||
"down": "pan-down-symbolic",
|
||||
"up": "pan-up-symbolic",
|
||||
},
|
||||
},
|
||||
"audio": {
|
||||
"mic": {
|
||||
"muted": "microphone-disabled-symbolic",
|
||||
"low": "microphone-sensitivity-low-symbolic",
|
||||
"medium": "microphone-sensitivity-medium-symbolic",
|
||||
"high": "microphone-sensitivity-high-symbolic",
|
||||
},
|
||||
"volume": {
|
||||
"muted": "audio-volume-muted-symbolic",
|
||||
"low": "audio-volume-low-symbolic",
|
||||
"medium": "audio-volume-medium-symbolic",
|
||||
"high": "audio-volume-high-symbolic",
|
||||
"overamplified": "audio-volume-overamplified-symbolic",
|
||||
},
|
||||
"type": {
|
||||
"headset": "audio-headphones-symbolic",
|
||||
"speaker": "audio-speakers-symbolic",
|
||||
"card": "audio-card-symbolic",
|
||||
},
|
||||
"mixer": "mixer-symbolic",
|
||||
},
|
||||
"powerprofile": {
|
||||
"balanced": "power-profile-balanced-symbolic",
|
||||
"power-saver": "power-profile-power-saver-symbolic",
|
||||
"performance": "power-profile-performance-symbolic",
|
||||
},
|
||||
"battery": {
|
||||
"charging": "battery-flash-symbolic",
|
||||
"warning": "battery-empty-symbolic",
|
||||
},
|
||||
"bluetooth": {
|
||||
"enabled": "bluetooth-active-symbolic",
|
||||
"disabled": "bluetooth-disabled-symbolic",
|
||||
},
|
||||
"brightness": {
|
||||
"indicator": "display-brightness-symbolic",
|
||||
"keyboard": "keyboard-brightness-symbolic",
|
||||
"screen": "display-brightness-symbolic",
|
||||
},
|
||||
"powermenu": {
|
||||
"sleep": "weather-clear-night-symbolic",
|
||||
"reboot": "system-reboot-symbolic",
|
||||
"logout": "system-log-out-symbolic",
|
||||
"shutdown": "system-shutdown-symbolic",
|
||||
},
|
||||
"recorder": {
|
||||
"recording": "media-record-symbolic",
|
||||
"stopped": "media-record-symbolic",
|
||||
},
|
||||
"notifications": {
|
||||
"noisy": "org.gnome.Settings-notifications-symbolic",
|
||||
"silent": "notifications-disabled-symbolic",
|
||||
"message": "chat-bubbles-symbolic",
|
||||
},
|
||||
"trash": {
|
||||
"full": "user-trash-full-symbolic",
|
||||
"empty": "user-trash-symbolic",
|
||||
},
|
||||
"mpris": {
|
||||
"shuffle": {
|
||||
"enabled": "media-playlist-shuffle-symbolic",
|
||||
"disabled": "media-playlist-consecutive-symbolic",
|
||||
},
|
||||
"loop": {
|
||||
"none": "media-playlist-repeat-symbolic",
|
||||
"track": "media-playlist-repeat-song-symbolic",
|
||||
"playlist": "media-playlist-repeat-symbolic",
|
||||
},
|
||||
"playing": "media-playback-pause-symbolic",
|
||||
"paused": "media-playback-start-symbolic",
|
||||
"stopped": "media-playback-start-symbolic",
|
||||
"prev": "media-skip-backward-symbolic",
|
||||
"next": "media-skip-forward-symbolic",
|
||||
},
|
||||
"system": {
|
||||
"cpu": "org.gnome.SystemMonitor-symbolic",
|
||||
"ram": "drive-harddisk-solidstate-symbolic",
|
||||
"temp": "temperature-symbolic",
|
||||
},
|
||||
"color": {
|
||||
"dark": "dark-mode-symbolic",
|
||||
"light": "light-mode-symbolic",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk
|
||||
|
||||
|
||||
class Signal:
|
||||
"""Simple signal implementation for monitor manager."""
|
||||
|
||||
def __init__(self):
|
||||
self._callbacks = []
|
||||
|
||||
def connect(self, callback):
|
||||
"""Connect a callback to this signal."""
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def emit(self, *args, **kwargs):
|
||||
"""Emit the signal to all connected callbacks."""
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"Error in signal callback: {e}")
|
||||
|
||||
|
||||
class MonitorManager:
|
||||
"""
|
||||
Centralized monitor management for Ax-Shell multi-monitor support.
|
||||
|
||||
Manages monitor detection, workspace paging, notch states, and component instances.
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if hasattr(self, '_initialized'):
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
self._monitors: List[Dict] = []
|
||||
self._focused_monitor_id: int = 0
|
||||
self._notch_states: Dict[int, bool] = {}
|
||||
self._current_notch_module: Dict[int, Optional[str]] = {}
|
||||
self._monitor_instances: Dict[int, Dict] = {}
|
||||
self._monitor_focus_service = None
|
||||
|
||||
# Signals
|
||||
self.monitor_changed = Signal()
|
||||
self.notch_focus_changed = Signal()
|
||||
|
||||
self.refresh_monitors()
|
||||
|
||||
def set_monitor_focus_service(self, service):
|
||||
"""Set the monitor focus service reference."""
|
||||
self._monitor_focus_service = service
|
||||
if service:
|
||||
service.monitor_focused.connect(self._on_monitor_focused)
|
||||
|
||||
def _get_gtk_monitor_info(self) -> List[Dict]:
|
||||
"""Get monitor information using GTK/GDK including scale factors."""
|
||||
gtk_monitors = []
|
||||
try:
|
||||
display = Gdk.Display.get_default()
|
||||
if display and hasattr(display, 'get_n_monitors'):
|
||||
n_monitors = display.get_n_monitors()
|
||||
for i in range(n_monitors):
|
||||
monitor = display.get_monitor(i)
|
||||
if monitor:
|
||||
geometry = monitor.get_geometry()
|
||||
scale_factor = monitor.get_scale_factor()
|
||||
model = monitor.get_model() or f'monitor-{i}'
|
||||
|
||||
gtk_monitors.append({
|
||||
'id': i,
|
||||
'name': model,
|
||||
'width': geometry.width,
|
||||
'height': geometry.height,
|
||||
'x': geometry.x,
|
||||
'y': geometry.y,
|
||||
'scale': scale_factor
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error getting GTK monitor info: {e}")
|
||||
|
||||
return gtk_monitors
|
||||
|
||||
def refresh_monitors(self) -> List[Dict]:
|
||||
"""
|
||||
Detect monitors using Hyprland API for accurate info, with GTK for scale detection.
|
||||
|
||||
Returns:
|
||||
List of monitor dictionaries with id, name, width, height, x, y, scale
|
||||
"""
|
||||
self._monitors = []
|
||||
|
||||
try:
|
||||
# Try Hyprland first for primary info (more accurate)
|
||||
result = subprocess.run(
|
||||
["hyprctl", "monitors", "-j"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
hypr_monitors = json.loads(result.stdout)
|
||||
|
||||
for i, monitor in enumerate(hypr_monitors):
|
||||
monitor_name = monitor.get('name', f'monitor-{i}')
|
||||
|
||||
# Get scale directly from Hyprland (more reliable)
|
||||
hypr_scale = monitor.get('scale', 1.0)
|
||||
|
||||
self._monitors.append({
|
||||
'id': i,
|
||||
'name': monitor_name,
|
||||
'width': monitor.get('width', 1920),
|
||||
'height': monitor.get('height', 1080),
|
||||
'x': monitor.get('x', 0),
|
||||
'y': monitor.get('y', 0),
|
||||
'focused': monitor.get('focused', False),
|
||||
'scale': hypr_scale
|
||||
})
|
||||
|
||||
# Initialize states for new monitors
|
||||
if i not in self._notch_states:
|
||||
self._notch_states[i] = False
|
||||
self._current_notch_module[i] = None
|
||||
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
|
||||
# Fallback to GTK only if Hyprland fails
|
||||
self._fallback_to_gtk()
|
||||
|
||||
# Ensure we have at least one monitor
|
||||
if not self._monitors:
|
||||
self._monitors = [{
|
||||
'id': 0,
|
||||
'name': 'default',
|
||||
'width': 1920,
|
||||
'height': 1080,
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'focused': True,
|
||||
'scale': 1.0
|
||||
}]
|
||||
self._notch_states[0] = False
|
||||
self._current_notch_module[0] = None
|
||||
|
||||
# Update focused monitor
|
||||
for monitor in self._monitors:
|
||||
if monitor.get('focused', False):
|
||||
self._focused_monitor_id = monitor['id']
|
||||
break
|
||||
|
||||
self.monitor_changed.emit(self._monitors)
|
||||
return self._monitors
|
||||
|
||||
def _fallback_to_gtk(self):
|
||||
"""Fallback monitor detection using GTK with scale information."""
|
||||
try:
|
||||
display = Gdk.Display.get_default()
|
||||
if display and hasattr(display, 'get_n_monitors'):
|
||||
n_monitors = display.get_n_monitors()
|
||||
for i in range(n_monitors):
|
||||
monitor = display.get_monitor(i)
|
||||
geometry = monitor.get_geometry()
|
||||
scale_factor = monitor.get_scale_factor()
|
||||
|
||||
self._monitors.append({
|
||||
'id': i,
|
||||
'name': monitor.get_model() or f'monitor-{i}',
|
||||
'width': geometry.width,
|
||||
'height': geometry.height,
|
||||
'x': geometry.x,
|
||||
'y': geometry.y,
|
||||
'focused': i == 0, # Assume first monitor is focused
|
||||
'scale': scale_factor
|
||||
})
|
||||
|
||||
if i not in self._notch_states:
|
||||
self._notch_states[i] = False
|
||||
self._current_notch_module[i] = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_monitors(self) -> List[Dict]:
|
||||
"""Get list of all monitors."""
|
||||
return self._monitors.copy()
|
||||
|
||||
def get_monitor_by_id(self, monitor_id: int) -> Optional[Dict]:
|
||||
"""Get monitor by ID."""
|
||||
for monitor in self._monitors:
|
||||
if monitor['id'] == monitor_id:
|
||||
return monitor.copy()
|
||||
return None
|
||||
|
||||
def get_focused_monitor_id(self) -> int:
|
||||
"""Get currently focused monitor ID."""
|
||||
return self._focused_monitor_id
|
||||
|
||||
def get_focused_monitor(self) -> Optional[Dict]:
|
||||
"""Get currently focused monitor."""
|
||||
return self.get_monitor_by_id(self._focused_monitor_id)
|
||||
|
||||
def get_workspace_range_for_monitor(self, monitor_id: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Get workspace range for a monitor (10 workspaces per monitor).
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor ID
|
||||
|
||||
Returns:
|
||||
Tuple of (start_workspace, end_workspace)
|
||||
"""
|
||||
start = (monitor_id * 10) + 1
|
||||
end = start + 9
|
||||
return (start, end)
|
||||
|
||||
def get_monitor_for_workspace(self, workspace_id: int) -> int:
|
||||
"""
|
||||
Get monitor ID for a workspace.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace number
|
||||
|
||||
Returns:
|
||||
Monitor ID
|
||||
"""
|
||||
if workspace_id <= 0:
|
||||
return 0
|
||||
return (workspace_id - 1) // 10
|
||||
|
||||
def get_monitor_scale(self, monitor_id: int) -> float:
|
||||
"""
|
||||
Get scale factor for a monitor.
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor ID
|
||||
|
||||
Returns:
|
||||
Scale factor (default 1.0 if not found)
|
||||
"""
|
||||
monitor = self.get_monitor_by_id(monitor_id)
|
||||
return monitor.get('scale', 1.0) if monitor else 1.0
|
||||
|
||||
def is_notch_open(self, monitor_id: int) -> bool:
|
||||
"""Check if notch is open on a monitor."""
|
||||
return self._notch_states.get(monitor_id, False)
|
||||
|
||||
def set_notch_state(self, monitor_id: int, is_open: bool, module: Optional[str] = None):
|
||||
"""Set notch state for a monitor."""
|
||||
self._notch_states[monitor_id] = is_open
|
||||
self._current_notch_module[monitor_id] = module if is_open else None
|
||||
|
||||
def get_current_notch_module(self, monitor_id: int) -> Optional[str]:
|
||||
"""Get current notch module for a monitor."""
|
||||
return self._current_notch_module.get(monitor_id)
|
||||
|
||||
def close_all_notches_except(self, except_monitor_id: int):
|
||||
"""Close all notches except on specified monitor."""
|
||||
for monitor_id in self._notch_states:
|
||||
if monitor_id != except_monitor_id and self._notch_states[monitor_id]:
|
||||
self.set_notch_state(monitor_id, False)
|
||||
# Get notch instance and close it
|
||||
instances = self._monitor_instances.get(monitor_id, {})
|
||||
notch = instances.get('notch')
|
||||
if notch and hasattr(notch, 'close_notch'):
|
||||
notch.close_notch()
|
||||
|
||||
def register_monitor_instances(self, monitor_id: int, instances: Dict):
|
||||
"""
|
||||
Register component instances for a monitor.
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor ID
|
||||
instances: Dict with 'bar', 'notch', 'dock', 'corners' keys
|
||||
"""
|
||||
self._monitor_instances[monitor_id] = instances
|
||||
|
||||
def get_monitor_instances(self, monitor_id: int) -> Dict:
|
||||
"""Get component instances for a monitor."""
|
||||
return self._monitor_instances.get(monitor_id, {})
|
||||
|
||||
def get_instance(self, monitor_id: int, component: str):
|
||||
"""Get specific component instance for a monitor."""
|
||||
instances = self._monitor_instances.get(monitor_id, {})
|
||||
return instances.get(component)
|
||||
|
||||
def get_focused_instance(self, component: str):
|
||||
"""Get component instance from focused monitor."""
|
||||
return self.get_instance(self._focused_monitor_id, component)
|
||||
|
||||
def _on_monitor_focused(self, monitor_name: str, monitor_id: int, workspace_id: int):
|
||||
"""Handle monitor focus change."""
|
||||
old_focused = self._focused_monitor_id
|
||||
self._focused_monitor_id = monitor_id
|
||||
|
||||
# Handle notch focus switching
|
||||
if old_focused != monitor_id:
|
||||
self._handle_notch_focus_switch(old_focused, monitor_id)
|
||||
|
||||
def _handle_notch_focus_switch(self, old_monitor: int, new_monitor: int):
|
||||
"""Handle notch switching between monitors."""
|
||||
# Close notch on old monitor if open
|
||||
if self.is_notch_open(old_monitor):
|
||||
old_module = self.get_current_notch_module(old_monitor)
|
||||
self.close_all_notches_except(-1) # Close all
|
||||
|
||||
# Open notch on new monitor with same module
|
||||
if old_module:
|
||||
new_instances = self.get_monitor_instances(new_monitor)
|
||||
notch = new_instances.get('notch')
|
||||
if notch and hasattr(notch, 'open_module'):
|
||||
notch.open_module(old_module)
|
||||
|
||||
self.notch_focus_changed.emit(old_monitor, new_monitor)
|
||||
|
||||
|
||||
# Singleton accessor
|
||||
_monitor_manager_instance = None
|
||||
|
||||
def get_monitor_manager() -> MonitorManager:
|
||||
"""Get the global MonitorManager instance."""
|
||||
global _monitor_manager_instance
|
||||
if _monitor_manager_instance is None:
|
||||
_monitor_manager_instance = MonitorManager()
|
||||
return _monitor_manager_instance
|
||||
@@ -0,0 +1,146 @@
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
import config.data as data
|
||||
|
||||
def get_current_workspace():
|
||||
"""
|
||||
Get the current workspace ID using hyprctl.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hyprctl", "activeworkspace"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
# Assume the output similar to: "ID <number>"
|
||||
# Extracting the number from the output
|
||||
parts = result.stdout.split()
|
||||
for i, part in enumerate(parts):
|
||||
if part == "ID" and i + 1 < len(parts):
|
||||
return int(parts[i+1])
|
||||
except Exception as e:
|
||||
print(f"Error getting current workspace: {e}")
|
||||
return -1
|
||||
|
||||
def get_screen_dimensions():
|
||||
"""
|
||||
Get screen dimensions from hyprctl.
|
||||
|
||||
Returns:
|
||||
tuple: (width, height) of the monitor containing the current workspace
|
||||
"""
|
||||
try:
|
||||
# Get current workspace
|
||||
workspace_id = get_current_workspace()
|
||||
|
||||
# Get monitor information
|
||||
result = subprocess.run(
|
||||
["hyprctl", "-j", "monitors"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
monitors = json.loads(result.stdout)
|
||||
|
||||
# Find the monitor containing our workspace
|
||||
for monitor in monitors:
|
||||
if monitor.get("activeWorkspace", {}).get("id") == workspace_id:
|
||||
return monitor.get("width", data.CURRENT_WIDTH), monitor.get("height", data.CURRENT_HEIGHT)
|
||||
|
||||
# Fallback to first monitor
|
||||
if monitors:
|
||||
return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get("height", data.CURRENT_HEIGHT)
|
||||
except Exception as e:
|
||||
print(f"Error getting screen dimensions: {e}")
|
||||
|
||||
# Default fallback values
|
||||
return data.CURRENT_WIDTH, data.CURRENT_HEIGHT
|
||||
|
||||
def check_occlusion(occlusion_region, workspace=None):
|
||||
"""
|
||||
Check if a region is occupied by any window on a given workspace.
|
||||
|
||||
Parameters:
|
||||
occlusion_region: Can be one of:
|
||||
- tuple (side, size): where side is "top", "bottom", "left", or "right"
|
||||
and size is the pixel width of the region
|
||||
- tuple (x, y, width, height): The full region coordinates (legacy format)
|
||||
workspace (int, optional): The workspace ID to check. If None, the current workspace is used.
|
||||
|
||||
Returns:
|
||||
bool: True if any window overlaps with the occlusion region, False otherwise.
|
||||
"""
|
||||
if workspace is None:
|
||||
workspace = get_current_workspace()
|
||||
|
||||
# Handle simplified side-based format
|
||||
if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2:
|
||||
side, size = occlusion_region
|
||||
if isinstance(side, str):
|
||||
# Convert side-based format to coordinates
|
||||
screen_width, screen_height = get_screen_dimensions()
|
||||
|
||||
if side.lower() == "bottom":
|
||||
occlusion_region = (0, screen_height - size, screen_width, size)
|
||||
elif side.lower() == "top":
|
||||
occlusion_region = (0, 0, screen_width, size)
|
||||
elif side.lower() == "left":
|
||||
occlusion_region = (0, 0, size, screen_height)
|
||||
elif side.lower() == "right":
|
||||
occlusion_region = (screen_width - size, 0, size, screen_height)
|
||||
|
||||
# Ensure occlusion_region is in the correct format (x, y, width, height)
|
||||
if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4:
|
||||
print(f"Invalid occlusion region format: {occlusion_region}")
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["hyprctl", "-j", "clients"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
clients = json.loads(result.stdout)
|
||||
except Exception as e:
|
||||
print(f"Error retrieving client windows: {e}")
|
||||
return False
|
||||
|
||||
occ_x, occ_y, occ_width, occ_height = occlusion_region
|
||||
occ_x2 = occ_x + occ_width
|
||||
occ_y2 = occ_y + occ_height
|
||||
|
||||
# Get screen dimensions for fullscreen check
|
||||
screen_width, screen_height = get_screen_dimensions()
|
||||
|
||||
for client in clients:
|
||||
# Check if client is mapped
|
||||
if not client.get("mapped", False):
|
||||
continue
|
||||
|
||||
# Ensure client has proper workspace information and matches the workspace
|
||||
client_workspace = client.get("workspace", {})
|
||||
if client_workspace.get("id") != workspace:
|
||||
continue
|
||||
|
||||
# Ensure client has position and size info
|
||||
position = client.get("at")
|
||||
size = client.get("size")
|
||||
if not position or not size:
|
||||
continue
|
||||
|
||||
x, y = position
|
||||
width, height = size
|
||||
win_x1, win_y1 = x, y
|
||||
win_x2, win_y2 = x + width, y + height
|
||||
|
||||
# Check for fullscreen windows (size matches screen and positioned at 0,0)
|
||||
if (width, height) == (screen_width, screen_height) and (x, y) == (0, 0):
|
||||
# For fullscreen windows, check if occlusion region is the top area
|
||||
if occ_y == 0 and occ_height > 0: # Top region
|
||||
return True # Consider fullscreen as occluding the top
|
||||
|
||||
# Check for intersection between the window and occlusion region
|
||||
if not (win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2):
|
||||
return True # Occlusion region is occupied
|
||||
|
||||
return False # No window overlaps the occlusion region
|
||||
Reference in New Issue
Block a user