This commit is contained in:
sankalp committed 2025-02-17 21:27:34 +05:45
1 parent f10e060b30
commit 32291d0625
6 files changed
+160 -741

No files matched your search

+1
View File
@@ -7,3 +7,4 @@ from .power import *
from .sh import *
from .todo import *
from .wallpapers import *
from .wifi import *
+2 -3
View File
@@ -79,7 +79,8 @@ class BluetoothConnections(Box):
child=self.toggle_icon,
on_clicked=lambda *_: self.client.toggle_power(),
)
self.client.notify("scanning")
self.client.notify("enabled")
self.client.connect(
"notify::enabled",
lambda *_: self.toggle_icon.set_label(
@@ -119,8 +120,6 @@ class BluetoothConnections(Box):
# to run notify closures thus display the status
# without having to wait until an actual change
self.client.notify("scanning")
self.client.notify("enabled")
def on_device_added(self, client: BluetoothClient, address: str):
if not (device := client.get_device(address)):
+2 -2
View File
@@ -48,7 +48,7 @@ class Cliphist(Box):
)
self.launcher_box = Box(
name="cliphist-launcher-box",
name="cliphist-history",
spacing=10,
orientation="v",
h_expand=True,
@@ -65,7 +65,7 @@ class Cliphist(Box):
if not self.viewport:
self.viewport = Box(name="viewport", spacing=4, orientation="v")
self.scrolled_window = ScrolledWindow(
name="cliphist-history",
name="scrolled-window",
spacing=10,
h_scrollbar_policy="never",
child=self.viewport,
@@ -1,357 +0,0 @@
import hashlib
import json
import os
from PIL import Image
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GdkPixbuf, GLib, Gtk, Gio
from snippets import MaterialIcon
from fabric.utils import exec_shell_command, get_relative_path
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
from fabric.widgets.label import Label
def is_valid_hex_color(value: str) -> bool:
"""Validate if the string is a Hex color."""
return (
value.startswith("#")
and len(value) == 7
and all(c in "0123456789ABCDEFabcdef" for c in value[1:])
)
def is_valid_hue(value: str) -> bool:
"""Validate if the string is a numeric Hue value (0–360)."""
return value.isnumeric() and 0 <= float(value) <= 360
class WallpaperSelector(Box):
CACHE_DIR = os.path.expanduser("~/.cache/modus/wallpapers")
SETTINGS_FILE = os.path.expanduser("~/dotfiles/hypr/scripts/settings.json")
SCHEMES = {
"TonalSpot": "tonalSpot",
"Expressive": "expressive",
"FruitSalad": "fruitSalad",
"Monochrome": "monochrome",
"Rainbow": "rainbow",
"Vibrant": "vibrant",
"Neutral": "neutral",
"Fidelity": "fidelity",
"Content": "content",
}
def __init__(self, **kwargs):
super().__init__(name="wallpapers", spacing=4, orientation="v", **kwargs)
self.launcher = kwargs["launcher"]
self.wallpapers_dir = os.path.expanduser("~/Pictures/wallpapers")
os.makedirs(self.CACHE_DIR, exist_ok=True)
self.files = [f for f in os.listdir(self.wallpapers_dir) if self._is_image(f)]
self.thumbnails = []
self.thumbnail_queue = []
self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor
self.viewport = Gtk.IconView()
self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str))
self.viewport.set_pixbuf_column(0)
self.viewport.set_text_column(1)
self.viewport.set_item_width(0)
self.viewport.connect("item-activated", self.on_wallpaper_selected)
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
h_expand=True,
v_expand=True,
child=self.viewport,
)
self.search_entry = Entry(
name="search-entry",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
)
self.scheme_dropdown = Gtk.ComboBoxText()
self.scheme_dropdown.set_name("scheme-dropdown")
self.scheme_dropdown.set_tooltip_text("Select color scheme")
for display_name, scheme_id in sorted(self.SCHEMES.items()):
self.scheme_dropdown.append(scheme_id, display_name)
self.scheme_dropdown.set_active_id("tonalSpot")
self.scheme_dropdown.connect("changed", self.on_scheme_changed)
initial_icon = "light_mode" if not self.check_dark_mode_state() else "dark_mode"
self.toggle_button = Button(
child=MaterialIcon(initial_icon),
name="toggle-launcher-button",
on_clicked=self.toggle_dark_mode,
)
self.custom_color_entry = Entry(
name="search-entry",
h_expand=True,
)
self.custom_color_entry.connect("activate", self.on_custom_color_submitted)
self.custom_color_box = Box(
name="custom-color-box",
spacing=10,
orientation="h",
children=[
Label(label="Custom Color:"),
self.custom_color_entry,
],
)
self.dropdown_box = Box(
name="dropdown-box",
orientation="h",
children=[
self.scheme_dropdown,
MaterialIcon("keyboard_arrow_down"),
],
)
self.header_box = Box(
name="header-box",
spacing=10,
orientation="h",
children=[
self.search_entry,
self.dropdown_box,
self.custom_color_box,
self.toggle_button,
],
)
self.add(self.header_box)
self.add(self.scrolled_window)
self._start_thumbnail_thread()
self.setup_file_monitor()
self.show_all()
def setup_file_monitor(self):
gfile = Gio.File.new_for_path(self.wallpapers_dir)
self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
self.file_monitor.connect("changed", self.on_directory_changed)
def on_directory_changed(self, monitor, file, other_file, event_type):
file_name = file.get_basename()
if event_type == Gio.FileMonitorEvent.DELETED:
if file_name in self.files:
self.files.remove(file_name)
cache_path = self._get_cache_path(file_name)
if os.path.exists(cache_path):
try:
os.remove(cache_path)
except Exception as e:
print(f"Error deleting cache {cache_path}: {e}")
self.thumbnails = [(p, n) for p, n in self.thumbnails if n != file_name]
GLib.idle_add(self.arrange_viewport, self.search_entry.get_text())
elif event_type == Gio.FileMonitorEvent.CREATED:
if self._is_image(file_name) and file_name not in self.files:
self.files.append(file_name)
self.files.sort()
self.executor.submit(self._process_file, file_name)
elif event_type == Gio.FileMonitorEvent.CHANGED:
if self._is_image(file_name) and file_name in self.files:
cache_path = self._get_cache_path(file_name)
if os.path.exists(cache_path):
try:
os.remove(cache_path)
except Exception as e:
print(f"Error deleting cache for changed file {file_name}: {e}")
self.executor.submit(self._process_file, file_name)
def close_selector(self):
self.launcher.close()
def arrange_viewport(self, query: str = ""):
self.viewport.get_model().clear()
filtered_thumbnails = [
(thumb, name)
for thumb, name in self.thumbnails
if query.casefold() in name.casefold()
]
filtered_thumbnails.sort(key=lambda x: x[1].lower())
for pixbuf, file_name in filtered_thumbnails:
self.viewport.get_model().append([pixbuf, file_name])
def toggle_dark_mode(self, *_):
GLib.spawn_command_line_async(
f"bash {get_relative_path('../../../assets/scripts/dark-theme.sh --toggle')}"
)
icon_name = "dark_mode" if not self.check_dark_mode_state() else "light_mode"
if self.toggle_button.get_child():
self.toggle_button.remove(self.toggle_button.get_child())
self.toggle_button.add(MaterialIcon(icon_name))
self.toggle_button.show_all()
def check_dark_mode_state(self):
result = exec_shell_command(
"gsettings get org.gnome.desktop.interface color-scheme"
)
return result.strip().replace("'", "") == "prefer-dark"
def on_wallpaper_selected(self, iconview, path):
model = iconview.get_model()
file_name = model[path][1]
full_path = os.path.join(self.wallpapers_dir, file_name)
selected_scheme = self.scheme_dropdown.get_active_id()
home_dir = GLib.get_home_dir()
command = (
f"python -O {home_dir}/dotfiles/hypr/scripts/wallpaper.py -I {full_path}"
)
GLib.spawn_command_line_async(command)
self.update_scheme(selected_scheme)
def on_scheme_changed(self, combo):
scheme_id = combo.get_active_id()
display_name = next(
name for name, id in self.SCHEMES.items() if id == scheme_id
)
print(f"Color scheme selected: {display_name} ({scheme_id})")
self.update_scheme(scheme_id)
home_dir = GLib.get_home_dir()
color_generator = os.path.join(home_dir, "dotfiles/material-colors/generate.py")
try:
command = f'python -O {color_generator} -R --scheme "{scheme_id}"'
GLib.spawn_command_line_async(command)
print(f"Applied color scheme: {display_name}")
except Exception as e:
print(f"Failed to apply color scheme: {e}")
def update_scheme(self, scheme: str):
try:
with open(self.SETTINGS_FILE, "r") as f:
settings = json.loads(f.read())
settings["generation-scheme"] = scheme
with open(self.SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
except Exception as error:
print(f"Failed to update generation-scheme in settings.json: {error}")
def _start_thumbnail_thread(self):
thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None)
def _preload_thumbnails(self, _data):
futures = [
self.executor.submit(self._process_file, file_name)
for file_name in self.files
]
concurrent.futures.wait(futures)
GLib.idle_add(self._process_batch)
def _process_file(self, file_name):
full_path = os.path.join(self.wallpapers_dir, file_name)
cache_path = self._get_cache_path(file_name)
if not os.path.exists(cache_path):
try:
with Image.open(full_path) as img:
img.thumbnail((96, 96), Image.Resampling.BILINEAR)
img.save(cache_path, "PNG")
except Exception as e:
print(f"Error processing {file_name}: {e}")
return
self.thumbnail_queue.append((cache_path, file_name))
GLib.idle_add(self._process_batch)
def _process_batch(self):
batch = self.thumbnail_queue[:10]
del self.thumbnail_queue[:10]
for cache_path, file_name in batch:
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
self.thumbnails.append((pixbuf, file_name))
self.viewport.get_model().append([pixbuf, file_name])
except Exception as e:
print(f"Error loading thumbnail {cache_path}: {e}")
if self.thumbnail_queue:
GLib.idle_add(self._process_batch)
return False
def _get_cache_path(self, file_name: str) -> str:
file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest()
return os.path.join(self.CACHE_DIR, f"{file_hash}.png")
@staticmethod
def _is_image(file_name: str) -> bool:
return file_name.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
)
def on_custom_color_submitted(self, entry: Entry):
"""Handle color submission."""
color = entry.get_text().strip()
if is_valid_hex_color(color) or is_valid_hue(color):
color_value = (
color if is_valid_hex_color(color) else self.hue_to_hex(float(color))
)
print(f"Applying custom color: {color_value}")
self.update_custom_color(color_value)
else:
print("Invalid color input. Please use a valid Hex or Hue.")
def update_custom_color(self, color: str):
"""Update the custom color in settings and regenerate theme."""
try:
with open(self.SETTINGS_FILE, "r") as f:
settings = json.loads(f.read())
settings["custom-color"] = color
with open(self.SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
home_dir = GLib.get_home_dir()
color_generator = os.path.join(
home_dir, "dotfiles/material-colors/generate.py"
)
command = f'python -O {color_generator} --color "{color}"'
GLib.spawn_command_line_async(command)
print(f"Custom color applied: {color}")
except Exception as e:
print(f"Failed to update custom color: {e}")
@staticmethod
def hue_to_hex(hue: float) -> str:
"""Convert Hue to Hex color."""
hue = hue / 360.0
rgb = WallpaperSelector.hls_to_rgb(hue, 0.5, 1.0)
return f"#{int(rgb[0] * 255):02X}{int(rgb[1] * 255):02X}{int(rgb[2] * 255):02X}"
@staticmethod
def hls_to_rgb(h: float, l: float, s: float) -> tuple:
"""Convert HLS to RGB color."""
if s == 0:
return l, l, l
def hue2rgb(p, q, t):
if t < 0:
t += 1
if t > 1:
t -= 1
if t < 1 / 6:
return p + (q - p) * 6 * t
if t < 1 / 2:
return q
if t < 2 / 3:
return p + (q - p) * (2 / 3 - t) * 6
return p
q = l * (1 + s) if l < 0.5 else l + s - l * s
p = 2 * l - q
r = hue2rgb(p, q, h + 1 / 3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1 / 3)
return r, g, b
@@ -1,379 +0,0 @@
import hashlib
import json
import os
from PIL import Image
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk
from snippets import MaterialIcon
from fabric.utils import exec_shell_command, get_relative_path
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
from fabric.widgets.label import Label
def is_valid_hex_color(value: str) -> bool:
"""Validate if the string is a Hex color."""
return (
value.startswith("#")
and len(value) == 7
and all(c in "0123456789ABCDEFabcdef" for c in value[1:])
)
def is_valid_hue(value: str) -> bool:
"""Validate if the string is a numeric Hue value (0–360)."""
return value.isnumeric() and 0 <= float(value) <= 360
class WallpaperSelector(Box):
CACHE_DIR = os.path.expanduser("~/.cache/fabric/wallpapers")
SETTINGS_FILE = os.path.expanduser("~/dotfiles/hypr/scripts/.settings.json")
SCHEMES = {
"TonalSpot": "tonalSpot",
"Expressive": "expressive",
"FruitSalad": "fruitSalad",
"Monochrome": "monochrome",
"Rainbow": "rainbow",
"Vibrant": "vibrant",
"Neutral": "neutral",
"Fidelity": "fidelity",
"Content": "content",
}
def __init__(self, **kwargs):
super().__init__(name="wallpapers", spacing=5, orientation="v", **kwargs)
self.launcher = kwargs["launcher"]
self.wallpapers_dir = os.path.expanduser("~/Pictures/wallpapers")
os.makedirs(self.CACHE_DIR, exist_ok=True)
self.files = [f for f in os.listdir(self.wallpapers_dir) if self._is_image(f)]
self.thumbnails = []
self.thumbnail_queue = []
self.executor = ThreadPoolExecutor(max_workers=5) # Shared executor
self.viewport = Gtk.IconView()
self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str))
self.viewport.set_pixbuf_column(1)
self.viewport.set_text_column(2)
self.viewport.set_item_width(1)
self.viewport.connect("item-activated", self.on_wallpaper_selected)
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=11,
h_expand=True,
v_expand=True,
child=self.viewport,
)
self.search_entry = Entry(
name="search-entry",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
)
self.scheme_dropdown = Gtk.ComboBoxText()
self.scheme_dropdown.set_name("scheme-dropdown")
self.scheme_dropdown.set_tooltip_text("Select color scheme")
for display_name, scheme_id in sorted(self.SCHEMES.items()):
self.scheme_dropdown.append(scheme_id, display_name)
self.scheme_dropdown.set_active_id("tonalSpot")
self.scheme_dropdown.connect("changed", self.on_scheme_changed)
initial_icon = "light_mode" if not self.check_dark_mode_state() else "dark_mode"
self.toggle_button = Button(
child=MaterialIcon(initial_icon),
name="toggle-launcher-button",
on_clicked=self.toggle_dark_mode,
)
self.dropdown_box = Box(
name="dropdown-box",
orientation="h",
children=[
self.scheme_dropdown,
MaterialIcon("keyboard_arrow_down"),
],
)
self.custom_color_entry = Entry(
name="custom-color-entry",
h_expand=True,
# notify_text=self.on_custom_color_changed,
)
self.custom_color_entry.connect("activate", self.on_custom_color_submitted)
self.color_preview = Box(
name="color-preview",
)
self.custom_color_box = Box(
name="custom-color-box",
spacing=10,
orientation="h",
children=[
Label(label="Custom Color:", xalign=0),
self.custom_color_entry,
self.color_preview,
],
)
self.header_box = Box(
name="header-box",
spacing=11,
orientation="h",
children=[
self.search_entry,
self.dropdown_box,
self.custom_color_box,
self.toggle_button,
],
)
self.add(self.header_box)
self.add(self.scrolled_window)
self._start_thumbnail_thread()
self.setup_file_monitor()
self.show_all()
def setup_file_monitor(self):
gfile = Gio.File.new_for_path(self.wallpapers_dir)
self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
self.file_monitor.connect("changed", self.on_directory_changed)
def on_directory_changed(self, monitor, file, other_file, event_type):
file_name = file.get_basename()
if event_type == Gio.FileMonitorEvent.DELETED:
if file_name in self.files:
self.files.remove(file_name)
cache_path = self._get_cache_path(file_name)
if os.path.exists(cache_path):
try:
os.remove(cache_path)
except Exception as e:
print(f"Error deleting cache {cache_path}: {e}")
self.thumbnails = [(p, n) for p, n in self.thumbnails if n != file_name]
GLib.idle_add(self.arrange_viewport, self.search_entry.get_text())
elif event_type == Gio.FileMonitorEvent.CREATED:
if self._is_image(file_name) and file_name not in self.files:
self.files.append(file_name)
self.files.sort()
self.executor.submit(self._process_file, file_name)
elif event_type == Gio.FileMonitorEvent.CHANGED:
if self._is_image(file_name) and file_name in self.files:
cache_path = self._get_cache_path(file_name)
if os.path.exists(cache_path):
try:
os.remove(cache_path)
except Exception as e:
print(f"Error deleting cache for changed file {file_name}: {e}")
self.executor.submit(self._process_file, file_name)
def close_selector(self):
self.launcher.close()
def arrange_viewport(self, query: str = ""):
self.viewport.get_model().clear()
filtered_thumbnails = [
(thumb, name)
for thumb, name in self.thumbnails
if query.casefold() in name.casefold()
]
filtered_thumbnails.sort(key=lambda x: x[2].lower())
for pixbuf, file_name in filtered_thumbnails:
self.viewport.get_model().append([pixbuf, file_name])
def toggle_dark_mode(self, *_):
GLib.spawn_command_line_async(
f"bash {get_relative_path('../../../assets/scripts/dark-theme.sh --toggle')}"
)
icon_name = "dark_mode" if not self.check_dark_mode_state() else "light_mode"
if self.toggle_button.get_child():
self.toggle_button.remove(self.toggle_button.get_child())
self.toggle_button.add(MaterialIcon(icon_name))
self.toggle_button.show_all()
def check_dark_mode_state(self):
result = exec_shell_command(
"gsettings get org.gnome.desktop.interface color-scheme"
)
return result.strip().replace("'", "") == "prefer-dark"
def on_wallpaper_selected(self, iconview, path):
model = iconview.get_model()
file_name = model[path][2]
full_path = os.path.join(self.wallpapers_dir, file_name)
selected_scheme = self.scheme_dropdown.get_active_id()
home_dir = GLib.get_home_dir()
command = (
f"python -O {home_dir}/dotfiles/hypr/scripts/wallpaper.py -I {full_path}"
)
GLib.spawn_command_line_async(command)
self.update_scheme(selected_scheme)
def on_scheme_changed(self, combo):
scheme_id = combo.get_active_id()
display_name = next(
name for name, id in self.SCHEMES.items() if id == scheme_id
)
print(f"Color scheme selected: {display_name} ({scheme_id})")
self.update_scheme(scheme_id)
home_dir = GLib.get_home_dir()
color_generator = os.path.join(home_dir, "dotfiles/material-colors/generate.py")
try:
command = f'python -O {color_generator} -R --scheme "{scheme_id}"'
GLib.spawn_command_line_async(command)
print(f"Applied color scheme: {display_name}")
except Exception as e:
print(f"Failed to apply color scheme: {e}")
def update_scheme(self, scheme: str):
try:
with open(self.SETTINGS_FILE, "r") as f:
settings = json.loads(f.read())
settings["generation-scheme"] = scheme
with open(self.SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=3)
except Exception as error:
print(f"Failed to update generation-scheme in settings.json: {error}")
def _start_thumbnail_thread(self):
thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None)
def _preload_thumbnails(self, _data):
futures = [
self.executor.submit(self._process_file, file_name)
for file_name in self.files
]
concurrent.futures.wait(futures)
GLib.idle_add(self._process_batch)
def _process_file(self, file_name):
full_path = os.path.join(self.wallpapers_dir, file_name)
cache_path = self._get_cache_path(file_name)
if not os.path.exists(cache_path):
try:
with Image.open(full_path) as img:
img.thumbnail((96, 96), Image.Resampling.BILINEAR)
img.save(cache_path, "PNG")
except Exception as e:
print(f"Error processing {file_name}: {e}")
return
self.thumbnail_queue.append((cache_path, file_name))
GLib.idle_add(self._process_batch)
def _process_batch(self):
batch = self.thumbnail_queue[:10]
del self.thumbnail_queue[:10]
for cache_path, file_name in batch:
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
self.thumbnails.append((pixbuf, file_name))
self.viewport.get_model().append([pixbuf, file_name])
except Exception as e:
print(f"Error loading thumbnail {cache_path}: {e}")
if self.thumbnail_queue:
GLib.idle_add(self._process_batch)
return False
def _get_cache_path(self, file_name: str) -> str:
file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest()
return os.path.join(self.CACHE_DIR, f"{file_hash}.png")
# Custom Color Functions
# def on_custom_color_changed(self, entry: Entry, *_):
# """Live preview for color entry changes."""
# color = entry.get_text().strip()
# if is_valid_hex_color(color):
# self.color_preview.override_background_color(
# Gtk.StateFlags.NORMAL, Gdk.RGBA.from_string(color)
# )
# elif is_valid_hue(color):
# hex_color = self.hue_to_hex(float(color))
# self.color_preview.override_background_color(
# Gtk.StateFlags.NORMAL, Gdk.RGBA.from_string(hex_color)
# )
# else:
# self.color_preview.override_background_color(Gtk.StateFlags.NORMAL, None)
def on_custom_color_submitted(self, entry: Entry):
"""Handle color submission."""
color = entry.get_text().strip()
if is_valid_hex_color(color) or is_valid_hue(color):
color_value = (
color if is_valid_hex_color(color) else self.hue_to_hex(float(color))
)
print(f"Applying custom color: {color_value}")
self.update_custom_color(color_value)
else:
print("Invalid color input. Please use a valid Hex or Hue.")
def update_custom_color(self, color: str):
"""Update the custom color in settings and regenerate theme."""
try:
with open(self.SETTINGS_FILE, "r") as f:
settings = json.loads(f.read())
settings["custom-color"] = color
with open(self.SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
home_dir = GLib.get_home_dir()
color_generator = os.path.join(
home_dir, "dotfiles/material-colors/generate.py"
)
command = f'python -O {color_generator} --color "{color}"'
GLib.spawn_command_line_async(command)
print(f"Custom color applied: {color}")
except Exception as e:
print(f"Failed to update custom color: {e}")
@staticmethod
def _is_image(file_name: str) -> bool:
return file_name.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
)
@staticmethod
def hue_to_hex(hue: float) -> str:
"""Convert Hue to Hex color."""
hue = hue / 360.0
rgb = WallpaperSelector.hls_to_rgb(hue, 0.5, 1.0)
return f"#{int(rgb[0] * 255):02X}{int(rgb[1] * 255):02X}{int(rgb[2] * 255):02X}"
@staticmethod
def hls_to_rgb(h: float, l: float, s: float) -> tuple:
"""Convert HLS to RGB color."""
if s == 0:
return l, l, l
def hue2rgb(p, q, t):
if t < 0:
t += 1
if t > 1:
t -= 1
if t < 1 / 6:
return p + (q - p) * 6 * t
if t < 1 / 2:
return q
if t < 2 / 3:
return p + (q - p) * (2 / 3 - t) * 6
return p
q = l * (1 + s) if l < 0.5 else l + s - l * s
p = 2 * l - q
r = hue2rgb(p, q, h + 1 / 3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1 / 3)
return r, g, b
+155
View File
@@ -0,0 +1,155 @@
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.image import Image
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.scrolledwindow import ScrolledWindow
from snippets import MaterialIcon
from services import NetworkClient
class WifiNetworkSlot(CenterBox):
def __init__(self, network, client: NetworkClient, **kwargs):
super().__init__(name="wifi-device", **kwargs)
self.network = network
self.client = client
self.connecting = False # Tracks if we are connecting/disconnecting
self.connection_icon = MaterialIcon("signal_wifi_off", 12)
self.connect_button = Button(
name="wifi-connect",
label="Connect",
on_clicked=self.on_connect_clicked,
)
self.start_children = [
Box(
spacing=8,
children=[
Image(icon_name=network.get("icon-name"), size=32),
Label(label=network.get("ssid")),
self.connection_icon,
],
)
]
self.end_children = self.connect_button
self.visible = True
self.show_all()
# Listen to WiFi state changes
self.client.wifi_device.connect("changed", lambda *_: self.update_status())
self.update_status()
def on_connect_clicked(self, *_):
connected = self.network.get("ssid") == self.client.wifi_device.ssid
if connected:
self.connect_button.set_label("Disconnecting...")
self.client.wifi_device.enabled = False # Disables WiFi to disconnect
else:
self.connect_button.set_label("Connecting...")
self.client.connect_wifi_bssid(self.network.get("bssid"))
self.connecting = True
self.connect_button.sensitive = False
def update_status(self):
state = self.client.wifi_device.state
connected = self.network.get("ssid") == self.client.wifi_device.ssid
if self.connecting:
if state in ["activating"]:
self.connect_button.set_label("Connecting...")
elif state in ["deactivating"]:
self.connect_button.set_label("Disconnecting...")
else:
self.connecting = False
self.connect_button.sensitive = True
if not self.connecting:
self.connection_icon.set_label(
"signal_wifi_4_bar" if connected else "signal_wifi_off"
)
self.connect_button.set_label("Disconnect" if connected else "Connect")
class WifiManager(Box):
def __init__(self, **kwargs):
super().__init__(
name="wifi",
spacing=8,
orientation="vertical",
**kwargs,
)
self.client = NetworkClient()
self.scan_icon = MaterialIcon("wifi_find")
self.toggle_icon = MaterialIcon("signal_wifi_off")
self.scan_button = Button(
name="wifi-scan",
child=self.scan_icon,
tooltip_text="Scan",
on_clicked=lambda *_: self.client.wifi_device
and self.client.wifi_device.scan(),
)
self.toggle_button = Button(
name="wifi-toggle",
tooltip_text="Toggle WiFi",
child=self.toggle_icon,
on_clicked=lambda *_: self.client.wifi_device
and self.client.wifi_device.toggle_wifi(),
)
self.available_box = Box(spacing=2, orientation="vertical")
self.children = [
CenterBox(
name="wifi-header",
start_children=self.scan_button,
center_children=Label(name="wifi-text", label="WiFi Networks"),
end_children=self.toggle_button,
),
ScrolledWindow(
name="scrolled-window",
v_expand=True,
child=self.available_box,
),
]
self.client.connect("device-ready", lambda *_: self.on_device_ready())
def on_device_ready(self):
if not self.client.wifi_device:
return
self.update_toggle_icon()
self.client.wifi_device.connect(
"notify::enabled",
lambda *_: self.update_toggle_icon(),
)
self.build_wifi_options()
def update_toggle_icon(self):
self.toggle_icon.set_label(
"signal_wifi_4_bar"
if self.client.wifi_device.enabled
else "signal_wifi_off"
)
def build_wifi_options(self):
self.available_box.children = []
if not self.client.wifi_device:
return
aps = self.client.wifi_device.access_points
seen_ssids = set()
for ap in aps:
ssid = ap.get("ssid")
if ap.get("ssid") != "Unknown" and ssid not in seen_ssids:
seen_ssids.add(ssid)
self.available_box.add(WifiNetworkSlot(ap, self.client))