huge updates but idk what i update
This commit is contained in:
1 parent
5d88e059d9
commit
4350f7459e
19 files changed
+563
-617
No files matched your search
+134
-50
@@ -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,
|
||||
),
|
||||
)
|
||||
@@ -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 *
|
||||
@@ -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
|
||||
+10
-10
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user