huge updates but idk what i update

This commit is contained in:
sankalp committed 2025-02-28 16:44:38 +05:45
1 parent 5d88e059d9
commit 4350f7459e
19 files changed
+563 -617

No files matched your search

+134 -50
View File
@@ -1,3 +1,4 @@
from typing import Literal
from fabric.hyprland.widgets import Language
from fabric.system_tray.widgets import SystemTray
from fabric.utils import (
@@ -5,24 +6,76 @@ from fabric.utils import (
bulk_replace,
exec_shell_command_async,
get_relative_path,
bulk_connect,
)
from fabric.widgets.shapes import Corner
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.datetime import DateTime
from fabric.widgets.label import Label
from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
from gi.repository import GLib
from modules.bar.components import (
BatteryLabel,
SystemInfo,
TaskBar,
Battery,
Metrics,
workspace,
Indicators,
SystemIndicators,
UpdatesWidget,
)
from services import sc
import snippets.iconss as icons
import utils.icons as icons
class StatusBarCorner(Box):
def __init__(self, corner: Literal["top-right", "top-left"]):
super().__init__(
style="margin-bottom: 15px;",
name="bar-corner",
children=Corner(
orientation=corner,
size=15,
),
)
class Tray(Box):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.tray = Box(
name="left-tray",
children=[SystemTray(name="tray", icon_size=16, spacing=4)],
)
self.profile = Button(
child=Label(name="tray-revealer", markup=icons.chevron_left)
)
self.revealer = Revealer(transition_type="slide-left", transition_duration=1000)
self.revealer.add(self.tray)
bulk_connect(
self.profile,
{
"enter-notify-event": self._on_enter,
"leave-notify-event": self._on_leave,
"button-press-event": self.toggle_revealer,
},
)
self.add(self.profile)
self.add(self.revealer)
def _on_enter(self, *args):
self.profile.set_cursor("pointer")
def _on_leave(self, *args):
self.profile.set_cursor("default")
def toggle_revealer(self, *args):
new_state = not self.revealer.get_reveal_child()
self.revealer.set_reveal_child(new_state)
new_icon = icons.chevron_right if new_state else icons.chevron_left
self.profile.get_child().set_markup(new_icon)
class Bar(Window):
@@ -39,7 +92,7 @@ class Bar(Window):
),
),
)
self.bar_content = CenterBox(name="center-bar")
self.recording_indicator = Button(
name="recording-indicator",
child=Label(name="recorder", markup=icons.record),
@@ -47,16 +100,10 @@ class Bar(Window):
on_clicked=lambda *_: sc.screencast_stop(),
)
sc.connect(
"recording", lambda _, status: self.on_recording_status_change(status)
)
sc.connect("recording", self.on_recording_status_change)
self.date_time = DateTime(formatters=["%-I:%M 󰧞 %a %d %b"], name="datetime")
self.battery = BatteryLabel()
self.taskbar = TaskBar()
self.stats = SystemInfo()
self.tray = SystemTray(name="tray", icon_size=16, spacing=4)
self.battery = Battery()
self.launcher = Button(
name="logo",
child=Label(name="logo-name", label="󰣇 󰫿󰫰󰫵"),
@@ -64,8 +111,6 @@ class Bar(Window):
"fabric-cli exec modus 'launcher.open(\"launcher\")'"
),
)
self.updates = UpdatesWidget()
self.indicators = Indicators()
self.button_config = Button(
name="button-bar",
on_clicked=lambda *_: exec_shell_command_async(
@@ -73,35 +118,22 @@ class Bar(Window):
),
child=Label(name="button-bar-label", markup=icons.config),
)
self.stats = Metrics()
self.updates = UpdatesWidget()
self.tray = Tray()
self.indicators = SystemIndicators()
self.applets = Box(
name="applets",
name="system-indicators",
spacing=4,
orientation="h",
children=[
self.language,
self.indicators,
],
children=[self.language, self.indicators],
)
self.bar = CenterBox(
name="bar",
start_children=Box(
name="start-container",
spacing=8,
orientation="h",
children=[self.launcher, self.workspaces, self.stats, self.updates],
),
center_children=Box(
name="center-container",
spacing=8,
orientation="h",
children=self.taskbar,
),
end_children=Box(
name="end-container",
spacing=8,
orientation="h",
self.bar_content.end_children = [
StatusBarCorner("top-right"),
Box(
name="bar",
spacing=4,
children=[
self.recording_indicator,
self.tray,
@@ -110,27 +142,79 @@ class Bar(Window):
self.date_time,
self.button_config,
],
style_classes="end-container",
),
)
]
self.bar_content.start_children = [
Box(
name="bar",
spacing=4,
children=[self.launcher, self.workspaces, self.stats, self.updates],
style_classes="start-container",
),
StatusBarCorner("top-left"),
]
super().__init__(
layer="top",
anchor="left bottom right",
anchor="left top right",
exclusivity="auto",
visible=True,
child=self.bar,
child=self.bar_content,
)
self.hidden = False
def on_recording_status_change(self, status):
print(f"Recording status changed: {status}")
def on_recording_status_change(self, _, status):
self.recording_indicator.set_visible(status)
def toggle_hidden(self):
self.hidden = not self.hidden
if self.hidden:
self.bar.remove_style_class("visible")
self.bar.add_style_class("hidden")
self.bar_content.add_style_class("hidden")
self.bar_content.remove_style_class("visible")
self.set_property("visible", False)
else:
self.bar.remove_style_class("hidden")
self.bar.add_style_class("visible")
self.bar_content.add_style_class("visible")
self.bar_content.remove_style_class("hidden")
self.set_property("visible", True)
class ScreenCorners(Window):
def __init__(self):
super().__init__(
layer="top",
anchor="top left bottom right",
pass_through=True,
child=Box(
orientation="vertical",
children=[
Box(
children=[
self.make_corner("top-left"),
Box(h_expand=True),
self.make_corner("top-right"),
]
),
Box(v_expand=True),
Box(
children=[
self.make_corner("bottom-left"),
Box(h_expand=True),
self.make_corner("bottom-right"),
]
),
],
),
)
def make_corner(self, orientation) -> Box:
return Box(
h_expand=False,
v_expand=False,
name="bar-corner",
children=Corner(
orientation=orientation,
size=15,
),
)
+2 -3
View File
@@ -1,6 +1,5 @@
from .battery import *
from .stats import *
from .taskbar import *
from .metric import *
from .workspace import *
from .indicators import *
from .system_indicators import *
from .updates import *
+4 -4
View File
@@ -10,10 +10,10 @@ from fabric.widgets.revealer import Revealer
from fabric.core.fabricator import Fabricator
from fabric.utils.helpers import exec_shell_command_async
from gi.repository import GLib
import snippets.iconss as icons
import utils.icons as icons
class BatteryLabel(Box):
class Battery(Box):
def __init__(self, **kwargs):
super().__init__(
name="battery",
@@ -59,8 +59,8 @@ class BatteryLabel(Box):
value=0,
size=28,
line_width=2,
start_angle=180,
end_angle=360,
start_angle=150,
end_angle=390,
)
self.bat_overlay = Overlay(
@@ -5,11 +5,10 @@ from fabric.widgets.label import Label
from fabric.widgets.box import Box
from fabric.widgets.overlay import Overlay
from fabric.widgets.circularprogressbar import CircularProgressBar
import snippets.iconss as icons
import utils.icons as icons
class SystemInfo(Box):
class Metrics(Box):
ICONS = {
"CPU": icons.cpu,
"RAM": icons.memory,
@@ -20,7 +19,7 @@ class SystemInfo(Box):
def __init__(self, **kwargs):
super().__init__(
name="system-info",
name="metrics",
spacing=8,
h_align="center",
v_align="fill",
@@ -30,14 +29,12 @@ class SystemInfo(Box):
self.progress_bars = {
system: CircularProgressBar(
name="stats-progress",
name="metric-circle",
line_style="round",
line_width=3,
line_width=2,
size=28,
start_at=0,
end_at=0.5,
start_angle=0,
end_angle=180,
start_angle=150,
end_angle=390,
)
for system in self.ICONS
}
@@ -45,7 +42,7 @@ class SystemInfo(Box):
for system, icon_name in self.ICONS.items():
overlay = Overlay(
child=self.progress_bars[system],
overlays=[Label(name="stats-icons", markup=icon_name)],
overlays=[Label(name="metric-icon", markup=icon_name)],
)
self.add(overlay)
@@ -55,14 +52,17 @@ class SystemInfo(Box):
ram_usage = psutil.virtual_memory().percent
swap_usage = psutil.swap_memory().percent
disk_usage = psutil.disk_usage("/").percent
cpu = psutil.cpu_percent(interval=0)
cpu_usage = psutil.cpu_percent(interval=0)
temp = self._get_device_temperature()
temp_usage = temp if temp is not None else 0
usages = {
"CPU": cpu,
"CPU": cpu_usage,
"RAM": ram_usage,
"Swap": swap_usage,
"Disk": disk_usage,
"Temp": self._get_device_temperature(),
"Temp": temp_usage,
}
for system, usage in usages.items():
@@ -76,7 +76,7 @@ class SystemInfo(Box):
try:
temps = psutil.sensors_temperatures()
for key in ("coretemp", "cpu_thermal"):
if key in temps:
if key in temps and temps[key]:
return round(temps[key][0].current, 1)
except Exception:
pass
@@ -4,19 +4,19 @@ from fabric.widgets.label import Label
from fabric.widgets.box import Box
from fabric.bluetooth import BluetoothClient
from services import NetworkClient, audio
import snippets.iconss as icons
import utils.icons as icons
class Indicators(Box):
class SystemIndicators(Box):
def __init__(self, **kwargs):
super().__init__(orientation="h", spacing=2, **kwargs)
self.bluetooth_icon = Label(name="indicator-icon")
self.wifi_icon = Label(name="indicator-icon")
self.volume_icon_button = Label(name="indicator-icon")
self.microphone_icon = Label(name="indicator-icon")
self.idle_label = Label(name="indicator-icon")
self.night_label = Label(name="indicator-icon")
self.bluetooth_icon = Label(name="system-indicator-icon")
self.wifi_icon = Label(name="system-indicator-icon")
self.volume_icon_button = Label(name="system-indicator-icon")
self.microphone_icon = Label(name="system-indicator-icon")
self.idle_label = Label(name="system-indicator-icon")
self.night_label = Label(name="system-indicator-icon")
for widget in [
self.bluetooth_icon,
@@ -86,8 +86,8 @@ class Indicators(Box):
if stream.muted
else (
icons.vol_mute
if volume_level == 0
else (icons.vol_medium if volume_level <= 50 else icons.vol_high)
if volume_level < 1
else (icons.vol_medium if volume_level >= 1 else icons.vol_high)
)
)
-154
View File
@@ -1,154 +0,0 @@
import json
import os
from typing import TypedDict
from fabric.hyprland.widgets import get_hyprland_connection
from fabric.utils import exec_shell_command_async
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from gi.repository import GdkPixbuf, GLib, Gtk
from loguru import logger
class PagerClient(TypedDict):
title: str
initialClass: str
mapped: bool
hidden: bool
address: str
class TaskBar(Box):
def __init__(self, icon_size: int = 22, **kwargs):
super().__init__(
orientation="h",
spacing=7,
name="taskbar",
**kwargs,
)
self.connection = get_hyprland_connection()
self.icon_size = icon_size
self.icon_theme = Gtk.IconTheme.get_default()
self.set_visible(False)
if self.connection.ready:
self.render_with_delay()
else:
self.connection.connect("event::ready", lambda *_: self.render_with_delay())
for event in (
"activewindow",
"openwindow",
"closewindow",
"changefloatingmode",
):
self.connection.connect("event::" + event, lambda *_: self.render(None))
def render_with_delay(self, *_):
GLib.timeout_add(101, self.render)
def render(self, *_):
self.children = []
clients = self.fetch_clients()
active_window_address = self.get_active_window_address()
visible_clients = [
client for client in clients if client["mapped"] and not client["hidden"]
]
if visible_clients:
for client in visible_clients:
window_class = client["initialClass"].lower()
icon = self.bake_window_icon(window_class)
button = Button(image=icon, tooltip_text=client["title"])
if client["address"] == active_window_address:
button.add_style_class("activated")
button.connect(
"button-press-event",
self.on_icon_click,
client["address"],
)
self.add(button)
self.set_visible(True)
self.show_all()
else:
self.set_visible(False)
self.set_visible(len(self.children) > 0)
if self.children:
self.show_all()
def get_active_window_address(self) -> str:
active_window_info = json.loads(
self.connection.send_command("j/activewindow").reply.decode()
)
return active_window_info.get("address", "")
def on_icon_click(self, widget, event, address):
exec_shell_command_async(
f"hyprctl dispatch focuswindow address:{address}", lambda *_: None
)
def fetch_clients(self) -> list[PagerClient]:
return json.loads(self.connection.send_command("j/clients").reply.decode())
def bake_window_icon(
self,
window_class: str,
fallback_icon: str = "image-missing",
) -> Image:
icon_name = self.get_icon_from_desktop_entry(window_class)
if icon_name:
pixbuf = self.load_icon(icon_name)
else:
pixbuf = self.load_icon(window_class, fallback_icon)
return Image(pixbuf=pixbuf, size=self.icon_size)
def get_icon_from_desktop_entry(self, window_class: str) -> str:
for data_dir in GLib.get_system_data_dirs():
applications_dir = os.path.join(data_dir, "applications")
if os.path.isdir(applications_dir):
for desktop_file in os.listdir(applications_dir):
if desktop_file.endswith(".desktop"):
file_path = os.path.join(applications_dir, desktop_file)
try:
with open(file_path) as f:
app_name = None
icon_name = None
for line in f:
if line.startswith("Name="):
app_name = line.split("=", 2)[1].strip().lower()
elif line.startswith("Icon=") and app_name:
icon_name = line.split("=", 2)[1].strip()
if window_class in app_name:
return icon_name
except Exception as e:
logger.error(f"Error reading {file_path}: {e}")
return None
def load_icon(
self,
icon_name: str,
fallback_icon: str = "image-missing",
) -> GdkPixbuf.Pixbuf:
try:
pixbuf = self.icon_theme.load_icon(
icon_name,
self.icon_size,
Gtk.IconLookupFlags.FORCE_SIZE,
)
except Exception:
pixbuf = self.icon_theme.load_icon(
fallback_icon,
self.icon_size,
Gtk.IconLookupFlags.FORCE_SIZE,
)
return pixbuf
+2 -2
View File
@@ -3,11 +3,11 @@ from fabric.utils import exec_shell_command_async, get_relative_path, invoke_rep
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.widgets.stack import Stack
import snippets.iconss as icons
from loguru import logger
from gi.repository import Gtk
from fabric.widgets.button import Button
import utils.icons as icons
class UpdatesWidget(Button):
+121 -199
View File
@@ -1,62 +1,49 @@
from collections.abc import Iterator
import operator
from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GLib, Gdk
from snippets import read_config
from fabric.widgets.image import Image
from gi.repository import Gdk, GLib
import json
import os
import re
import math
import re
import subprocess
import webbrowser
from functools import partial
from utils import read_config
class AppLauncher(Box):
def __init__(self, **kwargs):
super().__init__(
name="app-launcher",
visible=False,
all_visible=False,
**kwargs,
name="app-launcher", visible=False, all_visible=False, **kwargs
)
self.scrolled_window = None
self.launcher = kwargs["launcher"]
self.selected_index = -1
self._arranger_handler: int = 0
self._arranger_handler = 0
self._all_apps = get_desktop_applications()
self.config = read_config()
# Calculator history initialization
self.calc_history_path = os.path.expanduser("~/.cache/modus/calc.json")
if os.path.exists(self.calc_history_path):
with open(self.calc_history_path, "r") as f:
self.calc_history = json.load(f)
else:
self.calc_history = []
self.calc_history = self._load_calc_history()
self.viewport = Box(name="viewport", spacing=4, orientation="v")
self.search_entry = Entry(
name="search-entry",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
on_activate=lambda entry, *_: self.on_search_entry_activate(
entry.get_text()
),
notify_text=self._on_notify_text,
on_activate=self._on_activate,
on_key_press_event=self.on_search_entry_key_press,
)
self.scrolled_window = None
self.header_box = Box(
name="header-box",
orientation="h",
children=[self.search_entry],
name="header-box", orientation="h", children=[self.search_entry]
)
self.launcher_box = Box(
@@ -69,28 +56,43 @@ class AppLauncher(Box):
self.add(self.launcher_box)
self.show_all()
def _load_calc_history(self):
if os.path.exists(self.calc_history_path):
with open(self.calc_history_path, "r") as f:
return json.load(f)
return []
def _on_notify_text(self, entry, *args):
self.arrange_viewport(entry.get_text())
def _on_activate(self, entry, *args):
self.on_search_entry_activate(entry.get_text())
def close_launcher(self, *_):
self.viewport.children = []
self.selected_index = -1
self.launcher.close()
self.hide_scrolled_window()
def open_launcher(self):
self._all_apps = get_desktop_applications()
self.arrange_viewport("")
def arrange_viewport(self, query: str = ""):
if query.startswith(":"): # Don't show ScrolledWindow for web search
self.destroy_scrolled_window()
if query.startswith(":"):
self.hide_scrolled_window()
self.launcher.dashboard.hide()
return
if query.startswith("="):
self.show_scrolled_window()
# In calculator mode, update history view once (not per keystroke)
self.update_calculator_viewport()
self.launcher.dashboard.hide()
return
remove_handler(self._arranger_handler) if self._arranger_handler else None
if self._arranger_handler:
remove_handler(self._arranger_handler)
self._arranger_handler = 0
self.viewport.children = []
self.selected_index = -1
@@ -99,171 +101,140 @@ class AppLauncher(Box):
self.show_scrolled_window()
else:
self.launcher.dashboard.show()
self.destroy_scrolled_window()
self.hide_scrolled_window()
return False
filtered_apps = [
app
for app in self._all_apps
if query.casefold()
in (
(app.display_name or "")
+ " "
+ app.name
+ " "
+ (app.generic_name or "")
).casefold()
]
filtered_apps_iter = iter(
sorted(
[
app
for app in self._all_apps
if query.casefold()
in (
(app.display_name or "")
+ (" " + app.name + " ")
+ (app.generic_name or "")
).casefold()
],
key=lambda app: (app.display_name or "").casefold(),
)
sorted(filtered_apps, key=lambda app: (app.display_name or "").casefold())
)
should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps)
self._arranger_handler = idle_add(
lambda apps_iter: self.add_next_application(apps_iter)
or self.handle_arrange_complete(should_resize, query),
filtered_apps_iter,
pin=True,
partial(self._arranger_callback, query=query), filtered_apps_iter, pin=True
)
def _arranger_callback(self, apps_iter: Iterator[DesktopApp], query: str):
if self.add_next_application(apps_iter):
return True
return self.handle_arrange_complete(query)
def show_scrolled_window(self):
if self.scrolled_window is None:
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
min_content_size=(-1, -1),
style_classes="applauncher-scrolled-window",
h_scrollbar_policy="never",
child=self.viewport,
v_expand=True,
)
self.launcher_box.add(self.scrolled_window)
self.scrolled_window.show_all()
self.get_style_context().add_class("applauncher")
def destroy_scrolled_window(self):
if hasattr(self, "scrolled_window") and self.scrolled_window:
# self.scrolled_window.destroy()
self.launcher_box.remove(self.scrolled_window)
self.scrolled_window = None
self.scrolled_window.show_all()
GLib.idle_add(self.update_scrolled_window_size)
self.get_style_context().remove_class("applauncher")
def hide_scrolled_window(self):
if self.scrolled_window:
self.scrolled_window.hide()
def handle_arrange_complete(self, should_resize, query):
if should_resize:
self.resize_viewport()
# Only auto-select first item if query exists
if query.strip() != "" and self.viewport.get_children():
def handle_arrange_complete(self, query):
self.update_scrolled_window_size()
if query.strip() and self.viewport.get_children():
self.update_selection(0)
return False
def add_next_application(self, apps_iter: Iterator[DesktopApp]):
if not (app := next(apps_iter, None)):
app = next(apps_iter, None)
if app is None:
return False
self.viewport.add(self.bake_application_slot(app))
return True
def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button:
button = Button(
def bake_application_slot(self, app: DesktopApp) -> Button:
return Button(
name="launcher-app",
child=Box(
orientation="h",
children=[
Image(
pixbuf=app.get_icon_pixbuf(size=32),
h_align="start",
name="launcher-app-icon",
),
Image(pixbuf=app.get_icon_pixbuf(size=32), h_align="start"),
Label(
label=app.display_name or "Unknown",
ellipsization="end",
v_align="center",
h_align="center",
),
],
),
tooltip_text=app.description,
on_clicked=lambda *_: (app.launch(), self.close_launcher()),
**kwargs,
on_clicked=partial(self._on_app_clicked, app),
)
return button
def _on_app_clicked(self, app, *args):
app.launch()
self.close_launcher()
def update_selection(self, new_index: int):
if self.selected_index != -1 and self.selected_index < len(
self.viewport.get_children()
):
current_button = self.viewport.get_children()[self.selected_index]
current_button.get_style_context().remove_class("selected")
children = self.viewport.get_children()
if self.selected_index >= 0 < len(children):
children[self.selected_index].get_style_context().remove_class("selected")
if new_index != -1 and new_index < len(self.viewport.get_children()):
new_button = self.viewport.get_children()[new_index]
new_button.get_style_context().add_class("selected")
if 0 <= new_index < len(children):
children[new_index].get_style_context().add_class("selected")
self.selected_index = new_index
self.scroll_to_selected(new_button)
else:
self.selected_index = -1
def scroll_to_selected(self, button):
def scroll():
adj = self.scrolled_window.get_vadjustment()
alloc = button.get_allocation()
if alloc.height == 0:
return False
def update_scrolled_window_size(self):
if self.scrolled_window is None:
return
y = alloc.y
height = alloc.height
page_size = adj.get_page_size()
current_value = adj.get_value()
children = self.viewport.get_children()
item_count = len(children)
visible_top = current_value
visible_bottom = current_value + page_size
if item_count == 0:
self.scrolled_window.set_size_request(-1, 0)
return
if y < visible_top:
adj.set_value(y)
elif y + height > visible_bottom:
new_value = y + height - page_size
adj.set_value(new_value)
return False
first_child = children[0]
first_child_height = first_child.get_allocation().height
GLib.idle_add(scroll)
if first_child_height == 0:
GLib.idle_add(self.update_scrolled_window_size)
return
spacing = self.viewport.props.spacing
total_height = (item_count * first_child_height) + ((item_count - 1) * spacing)
max_height = 400
self.scrolled_window.set_size_request(-1, min(total_height, max_height))
def on_search_entry_activate(self, text):
commands = self.config.get("commands", {})
if text.startswith(":i "): # Detecting ':i' search
search_query = text[3:].strip() # Extract query after ':i '
if search_query:
url = f"https://www.google.com/search?q={search_query}"
webbrowser.open(url)
self.close_launcher()
if text.startswith(":i "):
webbrowser.open(f"https://www.google.com/search?q={text[3:].strip()}")
self.close_launcher()
return
if text.startswith("="):
# If in calculator mode and no history item is selected, evaluate new expression.
if self.selected_index == -1:
self.evaluate_calculator_expression(text)
self.evaluate_calculator_expression(text)
return
if text in commands:
command = commands[text]
exec(command, {"launcher": self.launcher})
else:
children = self.viewport.get_children()
if children:
if text.strip() == "" and self.selected_index == -1:
return
selected_index = self.selected_index if self.selected_index != -1 else 0
if 0 <= selected_index < len(children):
children[selected_index].clicked()
children = self.viewport.get_children()
if children:
(children[self.selected_index or 0]).clicked()
def on_search_entry_key_press(self, widget, event):
keyval = event.keyval
if keyval == Gdk.KEY_Down:
self.move_selection(1)
if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Up):
self.move_selection(1 if event.keyval == Gdk.KEY_Down else -1)
return True
elif keyval == Gdk.KEY_Up:
self.move_selection(-1)
return True
elif keyval == Gdk.KEY_Escape:
if event.keyval == Gdk.KEY_Escape:
self.close_launcher()
return True
return False
@@ -272,81 +243,32 @@ class AppLauncher(Box):
children = self.viewport.get_children()
if not children:
return
if self.selected_index == -1 and delta == 1:
new_index = 0
else:
new_index = self.selected_index + delta
new_index = max(0, min(new_index, len(children) - 1))
new_index = max(0, min(self.selected_index + delta, len(children) - 1))
self.update_selection(new_index)
def save_calc_history(self):
with open(self.calc_history_path, "w") as f:
json.dump(self.calc_history, f)
def evaluate_calculator_expression(self, text: str):
# Remove the '=' prefix and extra spaces
expr = text.lstrip("=").strip()
if not expr:
return
# Replace operators: '^' -> '**', and '×' -> '*'
expr = expr.replace("^", "**").replace("×", "*")
# Replace factorial: e.g. 5! -> math.factorial(5)
expr = text.lstrip("=").replace("^", "**").replace("×", "*")
expr = re.sub(r"(\d+)!", r"math.factorial(\1)", expr)
# Replace brackets: allow [] and {} as ()
for old, new in [("[", "("), ("]", ")"), ("{", "("), ("}", ")")]:
expr = expr.replace(old, new)
try:
result = eval(expr, {"__builtins__": None, "math": math})
except Exception as e:
result = f"Error: {e}"
# Prepend to history (newest first)
result = eval(expr, {"__builtins__": None, "math": math})
self.calc_history.insert(0, f"{text} => {result}")
self.save_calc_history()
self.update_calculator_viewport()
def update_calculator_viewport(self):
self.viewport.children = []
for item in self.calc_history:
btn = self.create_calc_history_button(item)
self.viewport.add(btn)
# Remove resetting selected_index unconditionally so that a highlighted result isn't lost.
# Optionally, only reset if the input is not more than "=".
# if self.search_entry.get_text().strip() != "=":
# self.selected_index = -1
self.viewport.children = [
Button(
child=Label(label=item),
on_clicked=partial(self.copy_text_to_clipboard, item),
)
for item in self.calc_history
]
self.update_scrolled_window_size()
def create_calc_history_button(self, text: str) -> Button:
btn = Button(
name="app-slot-button", # reuse existing CSS styling
child=Box(
name="calc-slot-box",
orientation="h",
spacing=10,
children=[
Label(
name="calc-label",
label=text,
ellipsization="end",
v_align="center",
h_align="center",
),
],
),
tooltip_text=text,
on_clicked=lambda *_: self.copy_text_to_clipboard(text),
def save_calc_history(self):
with open(self.calc_history_path, "w") as f:
json.dump(self.calc_history, f)
def copy_text_to_clipboard(self, text: str, *args):
subprocess.run(
["wl-copy"], input=text.split("=>", 1)[-1].strip().encode(), check=True
)
return btn
def copy_text_to_clipboard(self, text: str):
# Split the text on "=>" and copy only the result part if available
parts = text.split("=>", 1)
copy_text = parts[1].strip() if len(parts) > 1 else text
try:
subprocess.run(["wl-copy"], input=copy_text.encode(), check=True)
except subprocess.CalledProcessError as e:
print(f"Clipboard copy failed: {e}")
def delete_selected_calc_history(self):
if self.selected_index != -1 and self.selected_index < len(self.calc_history):
del self.calc_history[self.selected_index]
self.save_calc_history()
self.update_calculator_viewport()
+1 -1
View File
@@ -5,7 +5,7 @@ from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.bluetooth import BluetoothClient, BluetoothDevice
import snippets.iconss as icons
import utils.icons as icons
class BluetoothDeviceSlot(CenterBox):
+3 -3
View File
@@ -5,7 +5,7 @@ from fabric.widgets.label import Label
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
import snippets.iconss as icons
import utils.icons as icons
class Calendar(Gtk.Box):
@@ -27,7 +27,7 @@ class Calendar(Gtk.Box):
self.prev_month_button = Gtk.Button(
name="prev-month-button",
child=Label(name="month-button-label", markup=icons.chevron_left)
child=Label(name="month-button-label", markup=icons.chevron_left),
)
self.prev_month_button.connect("clicked", self.on_prev_month_clicked)
self.header.pack_start(self.prev_month_button, False, False, 0)
@@ -37,7 +37,7 @@ class Calendar(Gtk.Box):
self.next_month_button = Gtk.Button(
name="next-month-button",
child=Label(name="month-button-label", markup=icons.chevron_right)
child=Label(name="month-button-label", markup=icons.chevron_right),
)
self.next_month_button.connect("clicked", self.on_next_month_clicked)
self.header.pack_start(self.next_month_button, False, False, 0)
+17 -13
View File
@@ -1,6 +1,7 @@
import os
import subprocess
from typing import List
from functools import partial
from fabric import Fabricator
from fabric.widgets.box import Box
from fabric.widgets.button import Button
@@ -8,7 +9,7 @@ from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GLib
import snippets.iconss as icons
import utils.icons as icons
class Cliphist(Box):
@@ -19,15 +20,15 @@ class Cliphist(Box):
visible=False,
**kwargs,
)
self._arranger_handler = 0
self.cliphist_manager = CliphistManager(self)
self.viewport = None
self.launcher = kwargs["launcher"]
self.search_entry = Entry(
name="search-entry",
h_expand=True,
on_activate=lambda entry, *_: self.handle_search(entry.get_text()),
on_activate=self._on_search_activate,
)
self.header_box = Box(
@@ -55,6 +56,9 @@ class Cliphist(Box):
self.add(self.launcher_box)
def _on_search_activate(self, entry, *args):
self.handle_search(entry.get_text())
def open_launcher(self):
if not self.viewport:
self.viewport = Box(name="viewport", spacing=4, orientation="v")
@@ -63,7 +67,6 @@ class Cliphist(Box):
spacing=10,
min_content_size=(-1, -1),
h_scrollbar_policy="never",
# v_scrollbar_policy="never",
child=self.viewport,
)
self.launcher_box.add(self.scrolled_window)
@@ -89,9 +92,10 @@ class CliphistManager:
self.wl_paste_watcher = Fabricator(
poll_from="wl-paste --watch echo", stream=True, interval=-1
)
self.wl_paste_watcher.connect(
"changed", lambda *_: self.update_cliphist_history()
)
self.wl_paste_watcher.connect("changed", self._on_wl_paste_changed)
def _on_wl_paste_changed(self, *args):
self.update_cliphist_history()
def get_clip_history(self) -> List[dict]:
try:
@@ -118,14 +122,13 @@ class CliphistManager:
["cliphist", "decode", clip_id], capture_output=True, check=True
)
subprocess.run(["wl-copy"], input=decode_result.stdout, check=True)
self.launcher.open_launcher()
self.launcher.close()
except subprocess.CalledProcessError as e:
print(f"Error copying clip {clip_id}: {e}")
def save_image_file(self, clip_id: str) -> str:
output_file = f"/tmp/cliphist-{clip_id}.png"
os.makedirs("/tmp", exist_ok=True)
try:
decode_result = subprocess.run(
["cliphist", "decode", clip_id], capture_output=True, check=True
@@ -150,6 +153,9 @@ class CliphistManager:
if query.lower() in clip["content"].lower()
]
def _on_clip_clicked(self, clip_id: str, *args):
self.copy_clip(clip_id)
def bake_clip_slot(self, item: dict) -> Button:
if "[[ binary data" in item["content"].lower():
return self._create_image_button(item)
@@ -161,7 +167,7 @@ class CliphistManager:
button = Button(
h_align="start",
name="clip-img-item",
on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
on_clicked=partial(self._on_clip_clicked, item["id"]),
)
button.set_style(
f"background-image: url('{output_file}'); background-size: cover; background-position: center;"
@@ -178,17 +184,15 @@ class CliphistManager:
v_align="center",
h_align="start",
),
on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
on_clicked=partial(self._on_clip_clicked, item["id"]),
name="clip-item",
)
def arrange_viewport(self, query: str = ""):
if not self.launcher.viewport:
return
self.launcher.viewport.children = []
filtered_clips = self.query_clips(query)
for clip in filtered_clips:
button = self.bake_clip_slot(clip)
if button:
+1 -1
View File
@@ -9,7 +9,7 @@ from gi.repository import Gtk, Gdk, GLib # Added Gdk import
from fabric.utils import get_relative_path
gi.require_version("Gtk", "3.0")
import snippets.iconss as icons
import utils.icons as icons
def add_hover_cursor(widget):
+11 -15
View File
@@ -1,7 +1,7 @@
import subprocess
from typing import Generator, List
import ijson
from functools import partial
from fabric.utils import remove_handler, get_relative_path
from fabric.widgets.box import Box
from fabric.widgets.button import Button
@@ -18,26 +18,23 @@ class Emoji(Box):
visible=False,
**kwargs,
)
self.launcher = kwargs["launcher"]
self._arranger_handler: int = 0
self._arranger_handler = 0
self.emoji_manager = EmojiManager(self)
self.viewport = None
self.search_entry = Entry(
name="search-entry",
h_expand=True,
notify_text=lambda entry, *_: self.handle_search_input(entry.get_text()),
on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()),
notify_text=self._on_search_input,
on_activate=self._on_search_input,
)
self.header_box = Box(
name="header-box",
spacing=10,
orientation="h",
children=[
self.search_entry,
],
children=[self.search_entry],
)
self.launcher_box = Box(
@@ -49,6 +46,9 @@ class Emoji(Box):
self.add(self.launcher_box)
def _on_search_input(self, entry, *args):
self.handle_search_input(entry.get_text())
def open_launcher(self):
if not self.viewport:
self.viewport = Box(name="viewport", spacing=4, orientation="v")
@@ -63,12 +63,11 @@ class Emoji(Box):
self.viewport.children = []
self.emoji_manager.arrange_viewport()
self.viewport.show()
self.search_entry.grab_focus()
def close_launcher(self):
self.launcher.close()
self.launcher.close_launcher()
def handle_search_input(self, text: str):
self.emoji_manager.arrange_viewport(text)
@@ -105,7 +104,7 @@ class EmojiManager:
name="emoji-item",
child=Label(label=emoji[0], h_align="center"),
tooltip_text=emoji[1],
on_clicked=lambda *_: self.copy_emoji(emoji),
on_clicked=partial(self.copy_emoji, emoji),
**kwargs,
)
@@ -113,11 +112,8 @@ class EmojiManager:
if not self.launcher.viewport:
return
(
if self.launcher._arranger_handler:
remove_handler(self.launcher._arranger_handler)
if self.launcher._arranger_handler
else None
)
self.launcher.viewport.children = []
filtered_emojis = self.query_emojis(query)
+1 -2
View File
@@ -2,7 +2,7 @@ from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.utils.helpers import exec_shell_command_async
import snippets.iconss as icons
import utils.icons as icons
class PowerMenu(Box):
@@ -86,4 +86,3 @@ class PowerMenu(Box):
def poweroff(self, *args):
exec_shell_command_async("systemctl poweroff")
self.close_menu()
+1 -1
View File
@@ -9,7 +9,7 @@ from fabric.widgets.scrolledwindow import ScrolledWindow
CACHE_DIR = os.getenv(
"XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "fabric")
"XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "modus")
)
BINS = os.path.join(CACHE_DIR, "binaries")
+2 -2
View File
@@ -6,7 +6,7 @@ from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
import snippets.iconss as icons
import utils.icons as icons
class TodoManager(Box):
@@ -83,7 +83,7 @@ class TodoListManager:
def __init__(self, launcher):
self.launcher = launcher
self.todos = []
self.todo_file = os.path.expanduser("~/.local/share/modus/todos.json")
self.todo_file = os.path.expanduser("~/.local/state/modus/todos.json")
self.load_todos()
def load_todos(self):
+231 -137
View File
@@ -1,21 +1,21 @@
import hashlib
import json
import os
import hashlib, json, os, colorsys
from PIL import Image
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk
from fabric.utils import get_relative_path
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
from snippets import WALLPAPERS_DIR
from fabric.utils import get_relative_path, exec_shell_command_async
from concurrent.futures import ThreadPoolExecutor, wait
from utils import WALLPAPERS_DIR
import utils.icons as icons
class WallpaperSelector(Box):
CACHE_DIR = os.path.expanduser("~/.cache/modus/wallpapers")
SETTINGS_FILE = get_relative_path("../../../config/assets/settings.json")
CURRENT_WALLPAPER_FILE = os.path.expanduser("~/.cache/current_wallpaper")
SCHEMES = {
"TonalSpot": "tonalSpot",
"Expressive": "expressive",
@@ -27,6 +27,7 @@ class WallpaperSelector(Box):
"Fidelity": "fidelity",
"Content": "content",
}
ITEM_WIDTH = 108
def __init__(self, **kwargs):
super().__init__(
@@ -38,24 +39,32 @@ class WallpaperSelector(Box):
**kwargs,
)
self.launcher = kwargs["launcher"]
os.makedirs(self.CACHE_DIR, exist_ok=True)
self.files = [f for f in os.listdir(WALLPAPERS_DIR) if self._is_image(f)]
self.thumbnails = []
self.thumbnail_queue = []
self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor
self.thumbnails, self.thumbnail_queue = [], []
self.executor = ThreadPoolExecutor(max_workers=4)
self.selected_index, self.current_wallpaper = -1, None
self._init_widgets()
self._start_thumbnail_thread()
self.setup_file_monitor()
if os.path.exists(self.CURRENT_WALLPAPER_FILE):
gfile = Gio.File.new_for_path(self.CURRENT_WALLPAPER_FILE)
self.current_wp_monitor = gfile.monitor_file(
Gio.FileMonitorFlags.NONE, None
)
self.current_wp_monitor.connect(
"changed", self.on_current_wallpaper_changed
)
self.show_all()
self.search_entry.grab_focus()
self.selected_index = -1
# IconView for displaying thumbnails.
def _init_widgets(self):
self.viewport = Gtk.IconView(name="wallpaper-icons")
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.set_item_width(self.ITEM_WIDTH)
self.viewport.connect("item-activated", self.on_wallpaper_selected)
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
@@ -63,94 +72,117 @@ class WallpaperSelector(Box):
v_expand=True,
child=self.viewport,
)
self.search_entry = Entry(
name="search-entry-walls",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
on_key_press_event=self.on_search_entry_key_press,
)
self.search_entry.connect("focus-out-event", self.on_search_entry_focus_out)
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)
self.dropdown_box = Box(
name="dropdown-box",
children=[
self.scheme_dropdown,
],
self.materialyoucolor_switcher = Gtk.Switch(name="materialyoucolor-switcher")
self.materialyoucolor_switcher.set_vexpand(False)
self.materialyoucolor_switcher.set_hexpand(False)
self.materialyoucolor_switcher.set_valign(Gtk.Align.CENTER)
self.materialyoucolor_switcher.set_halign(Gtk.Align.CENTER)
self.materialyoucolor_switcher.set_active(True)
self.mat_icon = Label(name="mat-label", markup=icons.palette)
self.dropdown_box = Box(name="dropdown-box", children=[self.scheme_dropdown])
self.custom_color_entry = Entry(
name="entry-custom-color",
h_expand=True,
notify_text=lambda entry, *_: self.on_custom_color_submitted(entry),
on_key_press_event=self.on_custom_color_key_press,
)
self.header_box = CenterBox(
name="header-box",
spacing=10,
orientation="h",
start_children=[self.search_entry],
start_children=[self.materialyoucolor_switcher, self.mat_icon],
center_children=[
self.search_entry,
Label(label="Custom Color:"),
self.custom_color_entry,
],
end_children=[self.dropdown_box],
)
self.add(self.header_box)
self.add(self.scrolled_window)
self._start_thumbnail_thread()
self.setup_file_monitor()
self.show_all()
self.search_entry.grab_focus()
def setup_file_monitor(self):
gfile = Gio.File.new_for_path(WALLPAPERS_DIR)
self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
self.file_monitor.connect("changed", self.on_directory_changed)
def on_current_wallpaper_changed(self, monitor, file, other_file, event_type):
if event_type in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED):
try:
with open(self.CURRENT_WALLPAPER_FILE, "r") as f:
new_wp = f.read().strip()
if new_wp and new_wp != self.current_wallpaper:
self.current_wallpaper = new_wp
if self.custom_color_entry.get_text().strip().lower() in [
"",
"none",
]:
scheme = self.scheme_dropdown.get_active_id()
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
command = f'python -O {color_generator} --scheme "{scheme}" --image "{self.current_wallpaper}"'
self._run_command(command)
except Exception as e:
print(f"Error updating current wallpaper: {e}")
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._delete_cache(file_name)
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:
return
if event_type in (
Gio.FileMonitorEvent.CREATED,
Gio.FileMonitorEvent.CHANGED,
) and self._is_image(file_name):
if (
event_type == Gio.FileMonitorEvent.CREATED
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)
elif event_type == Gio.FileMonitorEvent.CHANGED and file_name in self.files:
self._delete_cache(file_name)
self.executor.submit(self._process_file, file_name)
def _delete_cache(self, file_name: str):
cache_path = self._get_cache_path(file_name)
if os.path.exists(cache_path):
try:
os.remove(cache_path)
except Exception:
pass
def arrange_viewport(self, query: str = ""):
model = self.viewport.get_model()
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:
for pixbuf, file_name in sorted(
[
(thumb, name)
for thumb, name in self.thumbnails
if query.casefold() in name.casefold()
],
key=lambda x: x[1].lower(),
):
model.append([pixbuf, file_name])
if query.strip() == "":
if not query.strip():
self.viewport.unselect_all()
self.selected_index = -1
elif len(model) > 0:
@@ -161,59 +193,37 @@ class WallpaperSelector(Box):
file_name = model[path][1]
full_path = os.path.join(WALLPAPERS_DIR, file_name)
selected_scheme = self.scheme_dropdown.get_active_id()
wallpaper = get_relative_path("../../../config/scripts/wallpaper.py")
command = f"python -O {wallpaper} -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)
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
try:
command = f'python -O {color_generator} -R --scheme "{scheme_id}"'
wallpaper_script = get_relative_path("../../../config/scripts/wallpaper.py")
if self.materialyoucolor_switcher.get_active():
command = f"python -O {wallpaper_script} -I {full_path}"
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}")
self.update_scheme(selected_scheme)
else:
cmd = f"swww img {full_path} -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest"
exec_shell_command_async(cmd)
def on_search_entry_key_press(self, widget, event):
if event.state & Gdk.ModifierType.SHIFT_MASK:
if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down):
schemes_list = list(self.SCHEMES.keys())
current_id = self.scheme_dropdown.get_active_id()
current_index = (
schemes_list.index(current_id) if current_id in schemes_list else 0
scheme_list = [
scheme_id for _, scheme_id in sorted(self.SCHEMES.items())
]
try:
current_index = scheme_list.index(
self.scheme_dropdown.get_active_id()
)
except ValueError:
current_index = 0
new_index = (
(current_index - 1) % len(scheme_list)
if event.keyval == Gdk.KEY_Up
else (current_index + 1) % len(scheme_list)
)
if event.keyval == Gdk.KEY_Up:
new_index = (current_index - 1) % len(schemes_list)
else:
new_index = (current_index + 1) % len(schemes_list)
self.scheme_dropdown.set_active(new_index)
return True
elif event.keyval == Gdk.KEY_Right:
self.scheme_dropdown.popup()
return True
if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
self.move_selection_2d(event.keyval)
return True
@@ -222,7 +232,6 @@ class WallpaperSelector(Box):
path = Gtk.TreePath.new_from_indices([self.selected_index])
self.on_wallpaper_selected(self.viewport, path)
return True
return False
def move_selection_2d(self, keyval):
@@ -230,33 +239,24 @@ class WallpaperSelector(Box):
total_items = len(model)
if total_items == 0:
return
if self.selected_index == -1:
new_index = (
0 if keyval in (Gdk.KEY_Down, Gdk.KEY_Right) else total_items - 1
)
else:
current_index = self.selected_index
# Calculate columns based on the allocation width.
allocation = self.viewport.get_allocation()
item_width = 108
columns = max(1, allocation.width // item_width)
if keyval == Gdk.KEY_Right:
new_index = current_index + 1
elif keyval == Gdk.KEY_Left:
new_index = current_index - 1
elif keyval == Gdk.KEY_Down:
new_index = current_index + columns
elif keyval == Gdk.KEY_Up:
new_index = current_index - columns
# Ensure new_index remains within limits.
if new_index < 0:
new_index = 0
if new_index >= total_items:
new_index = total_items - 1
columns = max(1, allocation.width // self.ITEM_WIDTH)
new_index = current_index + (
1
if keyval == Gdk.KEY_Right
else -1
if keyval == Gdk.KEY_Left
else columns
if keyval == Gdk.KEY_Down
else -columns
)
new_index = max(0, min(new_index, total_items - 1))
self.update_selection(new_index)
def update_selection(self, new_index: int):
@@ -267,14 +267,14 @@ class WallpaperSelector(Box):
self.selected_index = new_index
def _start_thumbnail_thread(self):
thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None)
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)
wait(futures)
GLib.idle_add(self._process_batch)
def _process_file(self, file_name):
@@ -287,13 +287,10 @@ class WallpaperSelector(Box):
side = min(width, height)
left = (width - side) // 2
top = (height - side) // 2
right = left + side
bottom = top + side
img_cropped = img.crop((left, top, right, bottom))
img_cropped = img.crop((left, top, left + side, top + side))
img_cropped.thumbnail((96, 96), Image.Resampling.LANCZOS)
img_cropped.save(cache_path, "PNG")
except Exception as e:
print(f"Error processing {file_name}: {e}")
except Exception:
return
self.thumbnail_queue.append((cache_path, file_name))
GLib.idle_add(self._process_batch)
@@ -316,13 +313,110 @@ class WallpaperSelector(Box):
file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest()
return os.path.join(self.CACHE_DIR, f"{file_hash}.png")
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
)
self.update_scheme(scheme_id)
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
command = f'python -O {color_generator} -R --scheme "{scheme_id}"'
self._run_command(
command,
success_message=f"Applied color scheme: {display_name}",
failure_message="Failed to apply color scheme:",
)
def update_scheme(self, scheme: str):
self._update_settings_field("generation-scheme", scheme)
def on_custom_color_submitted(self, entry: Entry):
color = entry.get_text().strip()
if not color:
self.update_custom_color("")
return
if self.is_valid_hex_color(color):
color_value = color
elif self.is_valid_hue(color):
color_value = self.hue_to_hex(float(color))
else:
return
self.update_custom_color(color_value)
def on_custom_color_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Return:
self.on_custom_color_submitted(widget)
return True
return False
def _fetch_current_wallpaper(self):
if not self.current_wallpaper or not os.path.exists(self.current_wallpaper):
if os.path.exists(self.CURRENT_WALLPAPER_FILE):
try:
with open(self.CURRENT_WALLPAPER_FILE, "r") as f:
self.current_wallpaper = f.read().strip()
except Exception as e:
print(f"Error reading current wallpaper: {e}")
return self.current_wallpaper
def update_custom_color(self, color: str):
color = color if color and color.lower() != "none" else "none"
self._update_settings_field("custom-color", color)
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
if color == "none":
scheme = self.scheme_dropdown.get_active_id()
current_wp = self._fetch_current_wallpaper()
command = f'python -O {color_generator} --scheme "{scheme}" --image "{current_wp}"'
else:
command = f'python -O {color_generator} --color "{color}"'
self._run_command(command)
@staticmethod
def _is_image(file_name: str) -> bool:
return file_name.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
)
def on_search_entry_focus_out(self, widget, event):
if self.get_mapped():
widget.grab_focus()
return False
@staticmethod
def is_valid_hex_color(value: str) -> bool:
return (
value.startswith("#")
and len(value) == 7
and all(c in "0123456789ABCDEFabcdef" for c in value[1:])
)
@staticmethod
def is_valid_hue(value: str) -> bool:
try:
return 0 <= float(value) <= 360
except ValueError:
return False
def hue_to_hex(self, hue: float) -> str:
r, g, b = colorsys.hls_to_rgb(hue / 360.0, 0.5, 1.0)
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
def _update_settings_field(self, field: str, value):
try:
with open(self.SETTINGS_FILE, "r") as f:
settings = json.load(f)
settings[field] = value
with open(self.SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
except Exception:
pass
def _run_command(
self, command: str, success_message: str = None, failure_message: str = None
):
try:
GLib.spawn_command_line_async(command)
if success_message:
print(success_message)
except Exception:
if failure_message:
print(failure_message)
+1 -1
View File
@@ -5,7 +5,7 @@ from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.scrolledwindow import ScrolledWindow
from services import NetworkClient
import snippets.iconss as icons
import utils.icons as icons
class WifiNetworkSlot(CenterBox):
+6 -4
View File
@@ -38,13 +38,12 @@ class Launcher(Window):
self.wifi = WifiManager()
self.calendar = Calendar()
# Wrap the dashboard in a Box container
self.dashboard = Box(
name="dashboard",
orientation="h",
spacing=10,
children=[
self.dashboard,
],
children=[self.dashboard],
)
self.launcher = AppLauncher(launcher=self)
@@ -78,7 +77,10 @@ class Launcher(Window):
self.add(self.launcher_box)
self.show_all()
self.hide()
self.add_keybinding("Escape", lambda *_: self.close())
self.add_keybinding("Escape", self._on_escape)
def _on_escape(self, *args):
return self.close()
def close(self):
self.set_keyboard_mode("none")