Compare commits

9 Commits

Author SHA1 Message Date
6366f57d6e custom apps 2025-05-19 10:32:06 +02:00
5c5fce2581 init: app launcher 2025-05-19 10:07:37 +02:00
5d08a48b6c init: window finder 2025-05-15 23:29:20 +02:00
82b0cf7aaa feat: nixos dbus availability 2025-05-15 18:38:00 +02:00
e4744bab81 right module order 2025-05-13 23:22:03 +02:00
872dbfc792 fix vinyl enable 2025-05-13 23:18:06 +02:00
64781af68f add default 2025-05-13 23:13:45 +02:00
0ebfbdb3a9 add yaml example 2025-05-13 23:10:23 +02:00
bf3920ad35 config 2025-05-13 23:09:05 +02:00
13 changed files with 479 additions and 50 deletions

52
bar/config.py Normal file
View File

@@ -0,0 +1,52 @@
import yaml
import os
from platformdirs import user_config_dir
import argparse
APP_NAME = "makku_bar"
XDG_CONFIG_HOME = user_config_dir(appname=APP_NAME)
XDG_CONFIG_FILE = os.path.join(XDG_CONFIG_HOME, "config.yaml")
def load_config(config_path=XDG_CONFIG_FILE):
"""Loads configuration from a YAML file."""
if config_path is None:
print("No configuration file path provided or found.")
return None
try:
with open(config_path, "r") as f:
config = yaml.safe_load(f)
return config
except FileNotFoundError:
print(f"Error: Configuration file not found at {config_path}")
return None
except yaml.YAMLError as e:
print(f"Error parsing YAML file '{config_path}': {e}")
return None
except Exception as e:
print(f"An unexpected error occurred loading config file '{config_path}': {e}")
return None
def load_args():
parser = argparse.ArgumentParser(description="makku_bar")
parser.add_argument(
"-c",
"--config",
help="Path to a custom configuration file.",
type=str,
)
args = parser.parse_args()
return args.config
app_config = load_config() if not load_args() else load_config(load_args())
if app_config is None:
raise Exception("Config file missing")
VINYL = app_config.get("vinyl", {"enable": False})

View File

@@ -1,5 +1,3 @@
# fabric bar.py example
# https://github.com/Fabric-Development/fabric/blob/rewrite/examples/bar/bar.py
from loguru import logger
from fabric import Application
@@ -12,39 +10,46 @@ from fabric.utils import (
get_relative_path,
)
from .modules.bar import StatusBar
from .modules.window_fuzzy import FuzzyWindowFinder
from .modules.app_launcher import AppLauncher
tray = SystemTray(name="system-tray", spacing=4)
river = get_river_connection()
dummy = Window(visible=False)
finder = FuzzyWindowFinder()
launcher = AppLauncher()
bar_windows = []
app = Application("bar", dummy, finder, launcher)
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
def spawn_bars():
logger.info("[Bar] Spawning bars after river ready")
outputs = river.outputs
if not outputs:
logger.warning("[Bar] No outputs found — skipping bar spawn")
return
output_ids = sorted(outputs.keys())
for i, output_id in enumerate(output_ids):
bar = StatusBar(display=output_id, tray=tray if i == 0 else None, monitor=i)
bar_windows.append(bar)
return False
def main():
tray = SystemTray(name="system-tray", spacing=4)
river = get_river_connection()
dummy = Window(visible=False)
bar_windows = []
def spawn_bars():
logger.info("[Bar] Spawning bars after river ready")
outputs = river.outputs
if not outputs:
logger.warning("[Bar] No outputs found — skipping bar spawn")
return
output_ids = sorted(outputs.keys())
for i, output_id in enumerate(output_ids):
bar = StatusBar(display=output_id, tray=tray if i == 0 else None, monitor=i)
bar_windows.append(bar)
return False
if river.ready:
spawn_bars()
else:
river.connect("notify::ready", lambda sender, pspec: spawn_bars())
app = Application("bar", dummy)
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
app.run()

192
bar/modules/app_launcher.py Normal file
View File

@@ -0,0 +1,192 @@
"""
example configuration shows how to make a simple
desktop applications launcher, this example doesn't involve
any styling (except a couple of basic style properties)
the purpose of this configuration is to show to to use
the given utils and mainly how using lazy executors might
make the configuration way more faster than it's supposed to be
"""
import operator
from collections.abc import Iterator
from dataclasses import dataclass
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from fabric.widgets.entry import Entry
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler
import subprocess
from time import sleep
import threading
@dataclass()
class CustomApp:
name: str
generic_name: str | None
display_name: str | None
description: str | None
executable: str | None
command_line: str | None
hidden: bool
def __init__(
self,
name,
display_name=None,
executable=None,
generic_name=None,
description=None,
command_line=None,
hidden=False,
):
self.name = name
self.generic_name = generic_name
self.display_name = display_name
self.description = description
self.executable = executable
self.command_line = command_line
self.hidden = hidden
def launch(self):
def background():
subprocess.run([self.command_line])
threading.Thread(target=background, daemon=True).start()
def get_icon_pixbuf(
self,
size: int = 48,
default_icon: str | None = "image-missing",
) -> None:
return None
class AppLauncher(Window):
def __init__(self, **kwargs):
super().__init__(
layer="top",
anchor="center",
exclusivity="none",
keyboard_mode="on-demand",
visible=False,
all_visible=False,
**kwargs,
)
self._arranger_handler: int = 0
self._all_apps = get_desktop_applications()
self._custom_apps = [
CustomApp("Screenshot Clipboard", command_line="grim2clip")
]
self.viewport = Box(spacing=2, orientation="v")
self.search_entry = Entry(
placeholder="Search Applications...",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
)
self.scrolled_window = ScrolledWindow(
min_content_size=(280, 320),
max_content_size=(280 * 2, 320),
child=self.viewport,
)
self.add(
Box(
spacing=2,
orientation="v",
style="margin: 2px",
children=[
# the header with the search entry
Box(
spacing=2,
orientation="h",
children=[
self.search_entry,
Button(
image=Image(icon_name="window-close"),
tooltip_text="Exit",
on_clicked=lambda *_: self.application.quit(),
),
],
),
# the actual slots holder
self.scrolled_window,
],
)
)
self.show_all()
def arrange_viewport(self, query: str = ""):
# reset everything so we can filter current viewport's slots...
# remove the old handler so we can avoid race conditions
remove_handler(self._arranger_handler) if self._arranger_handler else None
# remove all children from the viewport
self.viewport.children = []
combined_apps = self._all_apps + self._custom_apps
# make a new iterator containing the filtered apps
filtered_apps_iter = iter(
[
app
for app in combined_apps
if query.casefold()
in (
(app.display_name or "")
+ (" " + app.name + " ")
+ (app.generic_name or "")
).casefold()
]
)
should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps)
# all aboard...
# start the process of adding slots with a lazy executor
# using this method makes the process of adding slots way more less
# resource expensive without blocking the main thread and resulting in a lock
self._arranger_handler = idle_add(
lambda *args: self.add_next_application(*args)
or (self.resize_viewport() if should_resize else False),
filtered_apps_iter,
pin=True,
)
return False
def add_next_application(self, apps_iter: Iterator[DesktopApp]):
if not (app := next(apps_iter, None)):
return False
self.viewport.add(self.bake_application_slot(app))
return True
def resize_viewport(self):
self.scrolled_window.set_min_content_width(
self.viewport.get_allocation().width # type: ignore
)
return False
def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button:
return Button(
child=Box(
orientation="h",
spacing=12,
children=[
Image(pixbuf=app.get_icon_pixbuf(), h_align="start", size=32),
Label(
label=app.display_name or "Unknown",
v_align="center",
h_align="center",
),
],
),
tooltip_text=app.description,
on_clicked=lambda *_: (self.hide(), app.launch()),
**kwargs,
)

View File

@@ -19,6 +19,8 @@ from fabric.utils import (
)
from fabric.widgets.circularprogressbar import CircularProgressBar
from bar.config import VINYL
class StatusBar(Window):
def __init__(
@@ -74,7 +76,9 @@ class StatusBar(Window):
overlays=[self.cpu_progress_bar, self.progress_label],
)
self.player = Player()
self.vinyl = VinylButton()
self.vinyl = None
if VINYL["enable"]:
self.vinyl = VinylButton()
self.status_container = Box(
name="widgets-container",
@@ -83,11 +87,12 @@ class StatusBar(Window):
children=self.progress_bars_overlay,
)
end_container_children = [
self.vinyl,
self.status_container,
]
end_container_children = []
if self.vinyl:
end_container_children.append(self.vinyl)
end_container_children.append(self.status_container)
if self.system_tray:
end_container_children.append(self.system_tray)

View File

@@ -0,0 +1,75 @@
import operator
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.entry import Entry
from fabric.utils import idle_add
from gi.repository import Gdk
class FuzzyWindowFinder(Window):
def __init__(
self,
monitor: int = 0,
):
super().__init__(
name="finder",
anchor="center",
monitor=monitor,
keyboard_mode="on-demand",
type="popup",
visible=False,
)
self._all_windows = ["Test", "Uwu", "Tidal"]
self.viewport = Box(name="viewport", spacing=4, orientation="v")
self.search_entry = Entry(
name="search-entry",
placeholder="Search Windows...",
h_expand=True,
editable=True,
notify_text=self.notify_text,
on_activate=lambda entry, *_: self.on_search_entry_activate(
entry.get_text()
),
on_key_press_event=self.on_search_entry_key_press,
)
self.picker_box = Box(
name="picker-box",
spacing=4,
orientation="v",
children=[self.search_entry, self.viewport],
)
self.add(self.picker_box)
self.arrange_viewport("")
def notify_text(self, entry, *_):
text = entry.get_text()
self.arrange_viewport(text) # Update list on typing
print(text)
def on_search_entry_key_press(self, widget, event):
# if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
# self.move_selection_2d(event.keyval)
# return True
print(event.keyval)
if event.keyval in [Gdk.KEY_Escape, 103]:
self.hide()
return True
return False
def on_search_entry_activate(self, text):
print(f"activate {text}")
def arrange_viewport(self, query: str = ""):
self.viewport.children = [] # Clear previous entries
filtered = [w for w in self._all_windows if query.lower() in w.lower()]
for window in filtered:
self.viewport.add(
Box(name="slot-box", orientation="h", children=[Label(label=window)])
)

29
bar/styles/finder.css Normal file
View File

@@ -0,0 +1,29 @@
#picker-box {
padding: 12px;
background-color: rgba(40, 40, 40, 0.95); /* darker for contrast */
border-radius: 8px;
font-family: sans-serif;
font-size: 14px;
color: white;
}
#viewport {
padding: 8px;
background-color: rgba(30, 30, 30, 0.9); /* dark background for contrast */
border-radius: 6px;
font-family: sans-serif;
font-size: 14px;
color: white; /* ensure contrast */
}
#viewport > * {
padding: 6px 10px;
margin-bottom: 4px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.05);
}
#viewport:hover {
background-color: rgba(255, 255, 255, 0.15); /* hover feedback */
}

View File

@@ -3,6 +3,7 @@
@import url("./menu.css");
@import url("./vinyl.css");
@import url("./bar.css");
@import url("./finder.css");
/* unset so we can style everything from the ground up. */

2
example.yaml Normal file
View File

@@ -0,0 +1,2 @@
vinyl:
enabled: true

View File

@@ -32,7 +32,12 @@
{
formatter = pkgs.nixfmt-rfc-style;
devShells.default = pkgs.callPackage ./nix/shell.nix { inherit pkgs; };
packages.default = pkgs.callPackage ./nix/derivation.nix { inherit (pkgs) lib python3Packages; };
packages = {
default = pkgs.callPackage ./nix/derivation.nix { inherit (pkgs) lib python3Packages; };
makku = pkgs.writeShellScriptBin "makku" ''
dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"finder.show()" > /dev/null 2>&1
'';
};
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/bar";
@@ -47,6 +52,11 @@
pkgs,
...
}:
let
cfg = config.services.makku-bar;
settingsFormat = pkgs.formats.yaml { };
in
{
options.services.makku-bar = {
enable = lib.mkEnableOption "makku-bar status bar";
@@ -56,24 +66,44 @@
default = self.packages.${pkgs.system}.default;
description = "The makku-bar package to use.";
};
settings = lib.mkOption {
type = lib.types.submodule {
options = {
vinyl = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
};
};
};
default = {
vinyl.enable = false;
};
};
};
config = lib.mkIf config.services.makku-bar.enable {
systemd.user.services.makku-bar = {
Unit = {
Description = "Makku Status Bar";
After = [ "graphical-session.target" ];
};
systemd.user.services.makku-bar =
let
configFile = settingsFormat.generate "config.yaml" cfg.settings;
in
{
Unit = {
Description = "Makku Status Bar";
After = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${config.services.makku-bar.package}/bin/bar";
Restart = "on-failure";
};
Service = {
ExecStart = "${config.services.makku-bar.package}/bin/bar --config ${configFile}";
Restart = "on-failure";
};
Install = {
WantedBy = [ "default.target" ];
Install = {
WantedBy = [ "default.target" ];
};
};
};
};
};
};

View File

@@ -43,10 +43,26 @@ python3Packages.buildPythonApplication {
dependencies = with python3Packages; [
python-fabric
pywayland
pyyaml
platformdirs
];
doCheck = false;
dontWrapGApps = true;
installPhase = ''
runHook preInstall
mkdir -p $out/${python3Packages.python.sitePackages}
cp -r bar $out/${python3Packages.python.sitePackages}/
# If you have any scripts to install
mkdir -p $out/bin
cp scripts/launcher.py $out/bin/bar
chmod +x $out/bin/bar
runHook postInstall
'';
preFixup = ''
makeWrapperArgs+=("''${gappsWrapperArgs[@]}")
'';

View File

@@ -27,6 +27,8 @@ pkgs.mkShell {
wayland-scanner
wayland
wayland-protocols
playerctl
(python3.withPackages (
ps: with ps; [
setuptools
@@ -39,6 +41,8 @@ pkgs.mkShell {
pylsp-mypy
pyls-isort
python-lsp-ruff
pyyaml
platformdirs
]
))
];

View File

@@ -14,14 +14,11 @@ description = "Fabric using Nix example."
readme = "README.md"
license = {file = "LICENSE"}
[project.scripts]
bar = "bar.main:main"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
where = ["."]
[tool.setuptools.packages]
find = { namespaces = true }
[tool.setuptools.package-data]
"*" = ["*.css", "styles"]

21
scripts/launcher.py Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import sys
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
site_packages_dir = os.path.join(
script_dir,
os.pardir,
"lib",
f"python{sys.version_info.major}.{sys.version_info.minor}",
"site-packages",
)
if site_packages_dir not in sys.path:
sys.path.insert(0, site_packages_dir)
from bar.main import *
sys.argv[0] = os.path.join(script_dir, os.path.basename(__file__))
sys.exit(main())