updates
This commit is contained in:
6 files changed
+160
-741
No files matched your search
@@ -7,3 +7,4 @@ from .power import *
|
||||
from .sh import *
|
||||
from .todo import *
|
||||
from .wallpapers import *
|
||||
from .wifi import *
|
||||
@@ -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)):
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
Reference in new issue
Block a user