added stuff

This commit is contained in:
vali 2024-04-09 23:11:33 +02:00
commit 236b8c2a6b
907 changed files with 70990 additions and 0 deletions

View file

@ -0,0 +1 @@
use nix

View file

@ -0,0 +1,108 @@
root: true
env:
es2021: true
extends:
- "eslint:recommended"
- "plugin:@typescript-eslint/recommended"
parser: "@typescript-eslint/parser"
parserOptions:
ecmaVersion: 2022
sourceType: "module"
project: "./tsconfig.json"
warnOnUnsupportedTypeScriptVersion: false
ignorePatterns:
- types/
- node_modules/
- bin/
- result/
- style/
plugins:
- "@typescript-eslint"
rules:
"@typescript-eslint/ban-ts-comment":
- "off"
"@typescript-eslint/no-non-null-assertion":
- "off"
"@typescript-eslint/no-explicit-any":
- "off"
"@typescript-eslint/no-unused-vars":
- error
- varsIgnorePattern: (^unused|_$)
argsIgnorePattern: ^(unused|_)
"@typescript-eslint/no-empty-interface":
- "off"
comma-dangle:
- error
- always-multiline
comma-spacing:
- error
- before: false
after: true
comma-style:
- error
- last
curly:
- error
- multi-or-nest
- consistent
dot-location:
- error
- property
eol-last:
- error
indent:
- error
- 4
- SwitchCase: 1
keyword-spacing:
- error
- before: true
lines-between-class-members:
- error
- always
- exceptAfterSingleLine: true
padded-blocks:
- error
- never
- allowSingleLineBlocks: false
prefer-const:
- error
quotes:
- error
- double
- avoidEscape: true
semi:
- error
- always
nonblock-statement-body-position:
- error
- below
no-trailing-spaces:
- error
no-useless-escape:
- off
max-len:
- error
- code: 100
func-call-spacing:
- error
array-bracket-spacing:
- error
space-before-blocks:
- error
key-spacing:
- error
object-curly-spacing:
- error
- always
globals:
globalThis: readonly
imports: readonly
Intl: readonly
log: readonly
logError: readonly
print: readonly
printerr: readonly
console: readonly

View file

@ -0,0 +1 @@
node_modules/

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,14 @@
extends: stylelint-config-standard-scss
ignoreFiles:
- "**/*.js"
- "**/*.ts"
rules:
selector-type-no-unknown: null
declaration-empty-line-before: null
no-descending-specificity: null
selector-pseudo-class-no-unknown: null
color-function-notation: legacy
alpha-value-notation: number
scss/operator-no-unspaced: null
scss/no-global-function-names: null
scss/dollar-variable-empty-line-before: null

View file

@ -0,0 +1,29 @@
# Ags Configuration
A complete-ish shell replacement with a strong dependency on Hyprland.
Currently features a drop-in replacement for my old Waybar configuration
paired with a few other features that I found interesting, such as a program
launcher and desktop right click capture.
## Developing Locally
This configuration is primarily tied to a systemd user service - the
dependencies will be made available to ags inside a wrapper, so you do not
need to add anything to your `home.packages`. If developing locally, those
dependencies will need to be available inside your devshell. Take a look at the
`dependencies` list in `default.nix` and enter a shell with the required packages
to be able to run `ags -c ./config.js`. Currently `sassc` and `python3` are
necessary to be able to start the bar. If you skip this step, ags will not actually
display the bar.
## Credits
I have taken inspiration or/and code snippets from the cool people below. If you like
this configuration, consider giving them a star on their respective repositories.
- [Exoess](https://github.com/exoess/.files) - initially based on their configuration
- [SoraTenshi](https://github.com/SoraTenshi/ags-env) - the connection widget and weather module inspiration
- [Fufexan](https://github.com/fufexan/dotfiles/tree/main/home/programs/ags) - cool dude overall, inspiration
for a few widgets and his willingness to help with my skill issues
And of course [Aylur](https://github.com/Aylur) for his awesome work on AGS.

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# vim: syntax=bash
notifySend="notify-send"
getSwallowStatus() {
output=$(hyprctl getoption misc:enable_swallow)
if [[ $output == *"int: 1"* ]]; then
status=false
else
status=true
fi
echo "{\"status\": $status}"
}
switchSwallowStatus() {
enable=$1
if [ "$enable" = true ]; then
statusMsg="Turned on swallowing"
keyword="true"
else
statusMsg="Turned off swallowing"
keyword="false"
fi
hyprctl keyword misc:enable_swallow $keyword
$notifySend "Hyprland" "$statusMsg"
}
if [ $# -gt 0 ] && [ "${1}" = "query" ]; then
getSwallowStatus
exit 0
fi
output=$(hyprctl getoption misc:enable_swallow)
if [[ $output == *"int: 1"* ]]; then
switchSwallowStatus false
else
switchSwallowStatus true
fi

View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# vim: syntax=sh
move_window() {
local position="$1"
local size="$2"
if [[ -z "$position" || -z "$size" ]]; then
echo "Error: Both position and size are required." 1>&2
exit 1
fi
hyprctl --batch "dispatch moveactive exact ${position//,/ }; dispatch resizeactive exact ${size//x/ }"
}
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <position> <size>"
exit 1
fi
move_window "$1" "$2"

View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# vim: syntax=bash
open_window() {
local position="$1"
local size="$2"
local command="$3"
# Validate input
if [[ -z "$position" || -z "$size" || -z "$command" ]]; then
echo "Error: Position, size, and command are required." 1>&2
exit 1
fi
hyprctl dispatch exec "[float; move ${position//,/ }; size ${size//x/ }] $command"
}
if [[ $# -ne 3 ]]; then
echo "Usage: $0 <position> <size> <command>"
exit 1
fi
open_window "$1" "$2" "$3"

View file

@ -0,0 +1,178 @@
#!/usr/bin/env python
# vim: syntax=python
import os
import json
import requests
import logging
from datetime import datetime, timedelta
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)
CACHE_EXPIRATION = 60
XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
CACHE_DIR = os.path.join(XDG_CACHE_HOME, "zephyr")
FALLBACK_CACHE_DIR = "/tmp"
CACHE_FILE = os.path.join(CACHE_DIR, "zephyr_cache.json")
SUNNY = "\udb81\udda8"
CLOUDY = "\ue312"
RAIN = "\ue318"
SNOW = "\ue31a"
THUNDERSTORM = "\ue31d"
PARTLY_CLOUDY = "\ue302"
CLEAR = "\ue30d"
HOURS_AGO_THRESHOLD = 2
TEMP_THRESHOLD_COLD = 10
TEMP_THRESHOLD_HOT = 0
def ensure_cache_directory():
try:
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR, exist_ok=True)
except Exception as e:
logger.error(f"Error creating cache directory: {e}")
def get_weather_data():
ensure_cache_directory()
try:
response = requests.get("https://wttr.in/?format=j1")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Error fetching weather data: {e}")
return None
def get_cached_weather_data():
try:
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, "r") as cache_file:
cached_data = json.load(cache_file)
cache_time = datetime.strptime(
cached_data["timestamp"], "%Y-%m-%d %H:%M:%S"
)
if datetime.now() - cache_time < timedelta(minutes=CACHE_EXPIRATION):
return cached_data["data"]
except Exception as e:
logger.error(f"Error loading cached data: {e}")
return None
def cache_weather_data(data):
try:
with open(CACHE_FILE, "w") as cache_file:
cached_data = {
"data": data,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
json.dump(cached_data, cache_file)
except Exception as e:
logger.error(f"Error caching data: {e}")
def format_time(time):
return time.replace("00", "").zfill(2)
def format_temp(temp):
return f" {temp}°".ljust(4)
def get_emoji_for_condition(condition):
emoji_map = {
"Sunny": SUNNY,
"Partly cloudy": PARTLY_CLOUDY,
"Overcast": CLOUDY,
"Patchy rain nearby": RAIN,
"Clear": CLEAR,
"Fog": "\ue313",
"Frost": "\udb83\udf29",
"Thunder": THUNDERSTORM,
"Snow": SNOW,
"Windy": "\u27A7",
"Mist": "\u2601",
"Drizzle": "\u2601",
"Heavy rain": "\u2614",
"Sleet": "\u2744",
"Wintry mix": "\u2744",
"Clear/Sunny": CLEAR,
"Clear/Mostly clear": CLEAR,
"Clear/Mostly clear (night)": CLEAR,
"Drizzle (night)": "\u2601",
}
return emoji_map.get(condition, "")
def format_conditions(hour):
condition_probabilities = {
"chanceoffog": "Fog",
"chanceoffrost": "Frost",
"chanceofovercast": "Overcast",
"chanceofrain": "Rain",
"chanceofsnow": "Snow",
"chanceofsunshine": "Sunshine",
"chanceofthunder": "Thunder",
"chanceofwindy": "Wind",
}
if "chanceofpartlycloudy" in hour:
condition_probabilities["chanceofpartlycloudy"] = "Partly Cloudy"
conditions = []
for event, description in condition_probabilities.items():
if event in hour:
probability = int(hour[event])
if probability > 0:
emoji = get_emoji_for_condition(description)
conditions.append(f"{emoji} {description} {probability}%")
return ", ".join(conditions)
def format_weather_data(weather_data):
current_condition = weather_data["current_condition"][0]
temp = int(current_condition["FeelsLikeC"])
temp_sign = "+" if TEMP_THRESHOLD_HOT > temp > TEMP_THRESHOLD_COLD else ""
formatted_data = {
"text": f"{SUNNY} \n{temp_sign}{temp}°",
"tooltip": f"{current_condition['weatherDesc'][0]['value']} {current_condition['temp_C']}°\n"
f"Feels like: {current_condition['FeelsLikeC']}°\n"
f"Wind: {current_condition['windspeedKmph']}Km/h\n"
f"Humidity: {current_condition['humidity']}%\n",
}
for i, day in enumerate(weather_data["weather"]):
formatted_data["tooltip"] += f"\n"
if i == 0:
formatted_data["tooltip"] += "Today, "
if i == 1:
formatted_data["tooltip"] += "Tomorrow, "
formatted_data["tooltip"] += f"{day['date']}\n"
formatted_data["tooltip"] += f"⬆️ {day['maxtempC']}° ⬇️ {day['mintempC']}° "
formatted_data[
"tooltip"
] += f"🌅 {day['astronomy'][0]['sunrise']} 🌇 {day['astronomy'][0]['sunset']}\n"
now = datetime.now()
for hour in day["hourly"]:
hour_time = format_time(hour["time"])
if i == 0 and int(hour_time) < now.hour - HOURS_AGO_THRESHOLD:
continue
formatted_data[
"tooltip"
] += f"{hour_time} {get_emoji_for_condition(hour['weatherDesc'][0]['value'])} {format_temp(hour['FeelsLikeC'])} {hour['weatherDesc'][0]['value']}, {format_conditions(hour)}\n"
return formatted_data
def main():
weather_data = get_weather_data()
if weather_data is None:
weather_data = get_cached_weather_data()
if weather_data:
formatted_data = format_weather_data(weather_data)
cache_weather_data(formatted_data)
print(json.dumps(formatted_data))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,34 @@
import { App, Notifications } from "./js/imports.js";
const css = App.configDir + "/style.css";
// Windows
import { AppLauncher } from "./js/windows/launcher/index.js";
import { Bar } from "./js/windows/bar/index.js";
import { Desktop } from "./js/windows/desktop/index.js";
import { Popups } from "./js/windows/popups/index.js";
import { Notifs } from "./js/windows/notifications/index.js";
import { Media } from "./js/windows/music/index.js";
App.connect("config-parsed", () => print("config parsed"));
Notifications.popupTimeout = 5000;
Notifications.forceTimeout = false;
Notifications.cacheActions = true;
// Main config
export default {
style: css,
closeWindowDelay: {
launcher: 300,
music: 300,
},
};
/**
* @param {any[]} windows
*/
function addWindows(windows) {
windows.forEach((win) => App.addWindow(win));
}
addWindows([AppLauncher(), Bar(), Media(), Desktop(), Popups(), Notifs()]);

View file

@ -0,0 +1,122 @@
{
inputs,
osConfig,
config,
pkgs,
lib,
...
}: let
inherit (lib.fileset) fileFilter unions difference toSource;
inherit (lib.modules) mkIf;
inherit (osConfig.modules) device;
# dependencies required for the ags runtime to function properly
# some of those dependencies are used internally for setting variables
# or basic functionality where built-in services do not suffice
coreDeps = with pkgs; [
inputs.hyprpicker.packages.${pkgs.system}.default
inputs.hyprland.packages.${pkgs.system}.default
config.programs.foot.package
# basic functionality
inotify-tools
gtk3
# script and service helpers
bash
brightnessctl
coreutils
gawk
gvfs
imagemagick
libnotify
procps
ripgrep
slurp
sysstat
# for weather widget
(python3.withPackages (ps: [ps.requests]))
];
# applications that are not necessarily required to compile ags
# but are used by the widgets to launch certain applications
widgetDeps = with pkgs; [
pavucontrol
networkmanagerapplet
blueman
];
dependencies = coreDeps ++ widgetDeps;
filterNixFiles = fileFilter (file: lib.hasSuffix ".nix" file.name) ./.;
baseSrc = unions [
# runtime executables
./bin
# ags widgets and utilities
./js
./config.js
# compiled stylesheet
# should be generated using the below command
# `sassc -t compressed style/main.scss style.css`
./style.css
];
filter = difference baseSrc filterNixFiles;
cfg = config.programs.ags;
acceptedTypes = ["desktop" "laptop" "lite" "hybrid"];
in {
imports = [inputs.ags.homeManagerModules.default];
config = mkIf (builtins.elem device.type acceptedTypes) {
programs.ags = {
enable = true;
configDir = toSource {
root = ./.;
fileset = filter;
};
};
systemd.user.services.ags = {
Install.WantedBy = ["graphical-session.target"];
Unit = {
Description = "Aylur's Gtk Shell (Ags)";
After = ["graphical-session-pre.target"];
PartOf = [
"tray.target"
"graphical-session.target"
];
};
Service = {
Type = "simple";
Environment = "PATH=/run/wrappers/bin:${lib.makeBinPath dependencies}";
ExecStart = "${cfg.package}/bin/ags";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR2 $MAINPID"; # hot-reloading
# runtime
RuntimeDirectory = "ags";
ProtectSystem = "strict";
ProtectHome = "read-only";
CacheDirectory = ["ags"];
ReadWritePaths = [
# socket access
"%t" # /run/user/1000 for the socket
"/tmp/hypr" # hyprland socket
# for thumbnail caching
"~/notashelf/.local/share/firefox-mpris/"
"~/.cache/ags/media"
];
# restart on failure
Restart = "on-failure";
KillMode = "mixed";
};
};
};
}

View file

@ -0,0 +1,119 @@
export const Icon = {
settings: "org.gnome.Settings-symbolic",
tick: "object-select-symbolic",
audio: {
mic: {
muted: "microphone-disabled-symbolic",
unmuted: "microphone-sensitivity-high-symbolic",
},
volume: {
muted: "audio-volume-muted-symbolic",
low: "audio-volume-low-symbolic",
medium: "audio-volume-medium-symbolic",
high: "audio-volume-high-symbolic",
overamplified: "audio-volume-overamplified-symbolic",
},
type: {
headset: "audio-headphones-symbolic",
speaker: "audio-speakers-symbolic",
card: "audio-card-symbolic",
},
mixer: "tool-symbolic",
},
apps: {
apps: "view-app-grid-symbolic",
search: "folder-saved-search-symbolic",
},
bluetooth: {
enabled: "bluetooth-active-symbolic",
disabled: "bluetooth-disabled-symbolic",
},
brightness: {
indicator: "display-brightness-symbolic",
keyboard: "keyboard-brightness-symbolic",
screen: ["󰛩", "󱩎", "󱩏", "󱩐", "󱩑", "󱩒", "󱩓", "󱩔", "󱩕", "󱩖", "󰛨"],
},
powermenu: {
sleep: "weather-clear-night-symbolic",
reboot: "system-reboot-symbolic",
logout: "system-log-out-symbolic",
shutdown: "system-shutdown-symbolic",
lock: "system-lock-screen-symbolic",
close: "window-close-symbolic",
},
recorder: {
recording: "media-record-symbolic",
},
notifications: {
noisy: "preferences-system-notifications-symbolic",
silent: "notifications-disabled-symbolic",
critical: "messagebox_critical-symbolic",
chat: "notification-symbolic",
close: "window-close-symbolic",
},
header: {
refresh: "view-refresh-symbolic",
settings: "settings-symbolic",
power: "system-shutdown-symbolic",
},
trash: {
full: "user-trash-full-symbolic",
empty: "user-trash-symbolic",
},
mpris: {
fallback: "audio-x-generic-symbolic",
shuffle: {
enabled: "media-playlist-shuffle-symbolic",
disabled: "media-playlist-no-shuffle-symbolic",
},
loop: {
none: "media-playlist-no-repeat-symbolic",
track: "media-playlist-repeat-song-symbolic",
playlist: "media-playlist-repeat-symbolic",
},
playing: "media-playback-pause-symbolic",
paused: "media-playback-start-symbolic",
stopped: "media-playback-stop-symbolic",
prev: "media-skip-backward-symbolic",
next: "media-skip-forward-symbolic",
},
ui: {
send: "mail-send-symbolic",
arrow: {
right: "pan-end-symbolic",
left: "pan-start-symbolic",
down: "pan-down-symbolic",
up: "pan-up-symbolic",
},
},
speaker: {
overamplified: "\uf14b",
high: "\ue050",
medium: "\ue04d",
low: "\ue04e",
muted: "\ue04f",
},
microphone: {
overamplified: "\ue029",
high: "\ue029",
medium: "\ue029",
low: "\ue029",
muted: "\ue02b",
},
wired: {
power: "󰈀",
poweroff: "󱘖",
},
wifi: {
none: "󰤭",
bad: "󰤠",
low: "󰤟",
normal: "󰤢",
good: "󰤨",
},
system: {
cpu: "org.gnome.SystemMonitor-symbolic",
ram: "drive-harddisk-solidstate-symbolic",
temp: "temperature-symbolic",
},
};

View file

@ -0,0 +1,27 @@
export const require = async (file) => (await import(resource(file))).default;
export const resource = (file) => `resource:///com/github/Aylur/ags/${file}.js`;
export const fromService = async (file) => await require(`service/${file}`);
export const requireCustom = async (/** @type {string} */ path) =>
(await import(path)).default;
export const App = await require("app");
export const GLib = await requireCustom("gi://GLib");
export const Gtk = await requireCustom("gi://Gtk?version=3.0");
export const Service = await require("service");
export const Utils = await import(resource("utils"));
export const Variable = await require("variable");
export const Widget = await require("widget");
// Services
export const Battery = await fromService("battery");
export const Bluetooth = await fromService("bluetooth");
export const Hyprland = await fromService("hyprland");
export const Mpris = await fromService("mpris");
export const Network = await fromService("network");
export const Applications = await fromService("applications");
export const Audio = await fromService("audio");
export const Notifications = await fromService("notifications");
export const SystemTray = await fromService("systemtray");
// Extras
export const Icons = await requireCustom("./utils/icons.js");

View file

@ -0,0 +1,50 @@
import { Service, Utils } from "../imports.js";
const { exec } = Utils;
class Brightness extends Service {
static {
Service.register(
this,
{},
{
screen: ["float", "rw"],
},
);
}
_screen = 0;
get screen() {
return this._screen;
}
set screen(percent) {
if (percent < 0) percent = 0;
if (percent > 1) percent = 1;
Utils.execAsync(`brightnessctl s ${percent * 100}% -q`)
.then(() => {
this._screen = percent;
this.changed("screen");
})
.catch(console.error);
}
constructor() {
super();
try {
this._screen =
Number(exec("brightnessctl g")) /
Number(exec("brightnessctl m"));
} catch (error) {
console.error("missing dependancy: brightnessctl");
}
}
}
const service = new Brightness();
globalThis.brightness = service;
export default service;

View file

@ -0,0 +1,49 @@
import { Service } from "../imports.js";
const { Gio } = imports.gi;
class DirectoryMonitorService extends Service {
static {
Service.register(this, {}, {});
}
_monitors = [];
constructor() {
super();
}
recursiveDirectoryMonitor(directoryPath) {
const directory = Gio.File.new_for_path(directoryPath);
const monitor = directory.monitor_directory(
Gio.FileMonitorFlags.NONE,
null,
);
this._monitors.push(monitor);
monitor.connect(
"changed",
(fileMonitor, file, otherFile, eventType) => {
if (eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT) {
this.emit("changed");
}
},
);
const enumerator = directory.enumerate_children(
"standard::*",
Gio.FileQueryInfoFlags.NONE,
null,
);
let fileInfo;
while ((fileInfo = enumerator.next_file(null)) !== null) {
const childPath = directoryPath + "/" + fileInfo.get_name();
if (fileInfo.get_file_type() === Gio.FileType.DIRECTORY) {
this.recursiveDirectoryMonitor(childPath);
}
}
}
}
const service = new DirectoryMonitorService();
export default service;

View file

@ -0,0 +1,56 @@
import { Utils, Service } from "../imports.js";
const { subprocess } = Utils;
class InputMonitorService extends Service {
static {
Service.register(
this,
{
keypress: ["jsobject"],
keyrelease: ["jsobject"],
keyrepeat: ["jsobject"],
event: ["jsobject"],
},
{},
);
}
constructor() {
super();
this._evtest = subprocess("evtest /dev/input/event3", (str) =>
this._handleEvent(str),
);
}
_handleEvent(event) {
//ignore initial output
if (!event.startsWith("Event")) return;
//ignore SYN_REPORTS
if (event.includes("SYN")) return;
const eventData = event.substring(7).split(", ");
const eventInfo = {};
//evetnInfo structure:
//{
// time: unix timstamp
// type: event type
// code: keycode (this is the harware keycode)
// value: depends on type, for EV_KEY 0->release, 1->press, 2->repeat(when holding)
//}
eventData.forEach((data) => {
const [key, value, value2] = data.split(" ");
eventInfo[key] = isNaN(value) ? value : Number(value);
if (key === "code") eventInfo["name"] = value2.slice(1, -1);
});
//only emit on EV_KEY
if (eventInfo.type === 1) {
if (eventInfo.value === 0) this.emit("keyrelease", eventInfo);
if (eventInfo.value === 1) this.emit("keypress", eventInfo);
if (eventInfo.value === 2) this.emit("keyrepeat", eventInfo);
}
//emit on every event, just in case, you need it
this.emit("event", eventInfo);
}
}
export default new InputMonitorService();

View file

@ -0,0 +1,21 @@
import { Widget } from "../imports.js";
import { queryExact } from "./global.js";
const { Button, Icon } = Widget;
export default ({
appName,
onClicked = () => queryExact(appName).launch(),
icon = queryExact(appName).iconName,
size = 36,
...props
}) => {
const appIcon = Button({
onClicked,
child: Icon({
icon,
size,
...props,
}),
});
return appIcon;
};

View file

@ -0,0 +1,62 @@
import { Audio, Widget } from "../imports.js";
const { Slider, Label } = Widget;
const { speaker } = Audio;
const audio = {
mixer: "",
mic: {
muted: "microphone-disabled-symbolic",
low: "microphone-sensitivity-low-symbolic",
medium: "microphone-sensitivity-medium-symbolic",
high: "microphone-sensitivity-high-symbolic",
},
volume: {
muted: "audio-volume-muted-symbolic",
low: "audio-volume-low-symbolic",
medium: "audio-volume-medium-symbolic",
high: "audio-volume-high-symbolic",
overamplified: "audio-volume-overamplified-symbolic",
},
type: {
headset: "audio-headphones-symbolic",
speaker: "audio-speakers-symbolic",
card: "audio-card-symbolic",
},
};
export const getAudioIcon = (self) => {
if (!Audio.speaker) return;
const { muted, low, medium, high, overamplified } = audio.volume;
if (Audio.speaker.is_muted) return (self.icon = muted);
/** @type {Array<[number, string]>} */
const cons = [
[101, overamplified],
[67, high],
[34, medium],
[1, low],
[0, muted],
];
self.icon = cons.find(([n]) => n <= Audio.speaker.volume * 100)?.[1] || "";
};
export const getSliderIcon = () =>
Label({
className: "volPopupIcon",
label: speaker.bind("volume").as((/** @type {number} */ v) => {
return ["󰝟", "󰕿", "", "󰕾"][
speaker.stream?.isMuted ? 0 : Math.floor((v * 100) / 26)
];
}),
});
export const volumePercentBar = () =>
Slider({
className: "volPopupBar",
drawValue: false,
value: speaker.bind("volume"),
onChange: ({ value }) => (speaker.volume = value),
});

View file

@ -0,0 +1,57 @@
import { Battery } from "../imports.js";
/**
* toTime converts a given value to a human-readable
* format where the number of hours and minutes are
* inferred from time, which is assumed to be in seconds.
*
* @param {number} time - time in seconds
*/
export const toTime = (time) => {
const MINUTE = 60;
const HOUR = MINUTE * 60;
if (time > 24 * HOUR) return "";
const hours = Math.round(time / HOUR);
const minutes = Math.round((time - hours * HOUR) / MINUTE);
const hoursDisplay = hours > 0 ? `${hours}h ` : "";
const minutesDisplay = minutes > 0 ? `${minutes}m ` : "";
return `${hoursDisplay}${minutesDisplay}`;
};
export const getBatteryTime = () => {
const timeRemaining = Battery.timeRemaining;
return timeRemaining > 0 && toTime(timeRemaining) != ""
? `${toTime(timeRemaining)}remaining`
: "";
};
export const getBatteryPercentage = () => {
const percent = Battery.percent;
return percent > 0 && percent < 100 ? `${percent}%` : "";
};
export const getBatteryTooltip = () => {
const time = getBatteryTime();
const percent = Battery.percent;
return `${percent}% | ${time}`;
};
export const getBatteryIcon = () => {
// if Battery.percent is not between 0 and 100, handle the error
if (Battery.percent < 0 || Battery.percent > 100)
return "Battery percentage is not a valid value!";
const icons = [
["󰂎", "󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"],
["󰢟", "󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"],
];
const chargingIndex = Battery.charging ? 1 : 0;
const percentIndex = Math.floor(Battery.percent / 10);
return icons[chargingIndex][percentIndex].toString();
};

View file

@ -0,0 +1,53 @@
import { Bluetooth, Icons } from "../imports.js";
export const getBluetoothDevice = (addr) =>
Bluetooth.getDevice(addr).alias ?? Bluetooth.getDevice(addr).name;
export const getBluetoothIcon = (connected) => {
if (!Bluetooth.enabled) return Icons.bluetooth.disabled;
if (connected.length > 0) return Icons.bluetooth.active;
return Icons.bluetooth.disconnected;
};
export const getBluetoothTooltip = (connected) => {
if (!Bluetooth.enabled) return "Bluetooth off";
if (connected.length > 0) {
const dev = Bluetooth.getDevice(connected[0].address);
let battery_str = "";
if (dev.battery_percentage > 0) {
battery_str += ` ${dev.battery_percentage}%`;
}
return dev.name + battery_str;
}
return "Bluetooth on";
};
export const getBluetoothClass = (connected) => {
if (!Bluetooth.enabled) return "bluetooth-disabled";
if (connected.length > 0) {
const dev = Bluetooth.getDevice(connected.at(0).address);
if (dev.battery_percentage <= 25) return "bluetooth-active-low-battery";
if (dev.battery_percentage > 25) return "bluetooth-paired";
}
return "bluetooth-active";
};
export const getBluetoothLabel = (connected) => {
if (!Bluetooth.enabled) return "󰂲";
if (connected.length > 0) {
const dev = Bluetooth.getDevice(connected.at(0).address);
if (dev.battery_percentage <= 25) return "󰥇";
}
return "󰂰";
};

View file

@ -0,0 +1,36 @@
import { Widget } from "../imports.js";
import { queryExact } from "./global.js";
const { Box, Icon, Label, Button } = Widget;
/**
* Builds a desktop item with a specific name and label.
* It uses the `queryExact` function to find the exact application based on its name.
* Then, it creates a button widget with the application's icon and label.
* When the button is clicked, it launches the application.
*
* @function buildDesktopItem
* @param {string} name - The name of the application.
* @param {string} label - The label of the desktop item.
* @returns {Object} The desktop item widget.
*/
export const buildDesktopItem = (name, label) => {
const app = queryExact(name);
return Button({
className: "desktopIcon",
cursor: "pointer",
onClicked: () => app.launch(),
child: Box({
vertical: true,
children: [
Icon({
icon: app.iconName,
size: 48,
}),
Label({
className: "desktopIconLabel",
label,
}),
],
}),
});
};

View file

@ -0,0 +1,38 @@
import { Applications, Utils } from "../imports.js";
const { execAsync } = Utils;
const { list, query } = Applications;
/**
* Queries the exact application based on its name.
* First tries to find the application in the list of applications.
* If it doesn't find it, then it queries the application by its name.
*
* @function queryExact
* @param {string} appName - The name of the application to query.
* @returns {Object} The queried application object. Returns null if the application is not found.
*/
export function queryExact(appName) {
return (
list.filter(
(app) => app.name.toLowerCase() === appName.toLowerCase(),
)[0] ?? query(appName)[0]
);
}
/**
* Tries to launch an application based on its name.
* First it tries to kill the application if it's already running.
* Regardless of whether the killing has been successful or not, it
* tries to launch the application.
*
* @function launchApp
* @param {string} appName - The name of the application to launch.
* @returns {void}
*/
export function launchApp(appName) {
if (queryExact(appName)) {
execAsync(["sh", "-c", `killall ${appName}`]);
}
execAsync(["sh", "-c", `${appName}`]);
}

View file

@ -0,0 +1,12 @@
import { Hyprland } from "../imports.js";
export const getFocusedWorkspace = (self) =>
self.children.forEach((btn) => {
btn.className =
btn.attribute.index === Hyprland.active.workspace.id
? "focused"
: "";
btn.visible = Hyprland.workspaces.some(
(ws) => ws.id === btn.attribute.index,
);
});

View file

@ -0,0 +1,56 @@
export default {
bluetooth: {
active: "bluetooth-active-symbolic",
disabled: "bluetooth-disabled-symbolic",
disconnected: "bluetooth-disconnected-symbolic",
},
brightness: "display-brightness-symbolic",
media: {
play: "media-playback-start-symbolic",
pause: "media-playback-pause-symbolic",
next: "media-skip-forward-symbolic",
previous: "media-skip-backward-symbolic",
player: "multimedia-player-symbolic",
},
volume: {
muted: "audio-volume-muted-symbolic",
low: "audio-volume-low-symbolic",
medium: "audio-volume-medium-symbolic",
high: "audio-volume-high-symbolic",
overamplified: "audio-volume-overamplified-symbolic",
},
speaker: {
overamplified: "\uf14b",
high: "\ue050",
medium: "\ue04d",
low: "\ue04e",
muted: "\ue04f",
},
microphone: {
overamplified: "\ue029",
high: "\ue029",
medium: "\ue029",
low: "\ue029",
muted: "\ue02b",
},
wired: {
power: "󰈀",
poweroff: "󱘖",
},
wifi: {
none: "󰤭",
bad: "󰤠",
low: "󰤟",
normal: "󰤢",
good: "󰤨",
},
powerButton: "system-shutdown-symbolic",
};

View file

@ -0,0 +1,3 @@
export const getLauncherIcon = (self, windowName, visible) => {
windowName === "launcher" && (self.child.label = visible ? "󱢡" : "󱢦");
};

View file

@ -0,0 +1,46 @@
import { Icons, Utils } from "../imports.js";
import GLib from "gi://GLib";
export const findPlayer = (players) => {
// try to get the first active player
const activePlayer = players.find((p) => p.playBackStatus == "Playing");
if (activePlayer != null) return activePlayer;
// otherwise get the first "working" player
for (const p of players) {
if (p.title != "undefined") return p;
}
};
export const mprisStateIcon = (status) => {
const state = status == "Playing" ? "pause" : "play";
return Icons.media[state];
};
export const MEDIA_CACHE_PATH = Utils.CACHE_DIR + "/media";
export const blurredPath = MEDIA_CACHE_PATH + "/blurred";
export const generateBackground = (cover_path) => {
const url = cover_path;
if (!url) return "";
const makeBg = (bg) => `background: center/cover url('${bg}')`;
const blurred = blurredPath + url.substring(MEDIA_CACHE_PATH.length);
if (GLib.file_test(blurred, GLib.FileTest.EXISTS)) {
return makeBg(blurred);
}
Utils.ensureDirectory(blurredPath);
Utils.exec(`convert ${url} -blur 0x22 ${blurred}`);
return makeBg(blurred);
};
export function lengthStr(length) {
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? "0" : "";
return `${min}:${sec0}${sec}`;
}

View file

@ -0,0 +1,40 @@
import { Network } from "../imports.js";
import { Icon } from "../icons.js";
const { wifi, wired } = Icon;
export const getWifiIcon = (strength) => {
if (strength < 0.1) return wifi.none;
if (strength < 0.26) return wifi.bad;
if (strength < 0.51) return wifi.low;
if (strength < 0.76) return wifi.normal;
if (strength > 0.76) return wifi.good;
else return wifi.none;
};
export const getWifiTooltip = (strength, ssid) => {
const wifi = Network.wifi;
const wifiStrength = `Strength: ${strength * 100}`;
switch (wifi.internet) {
case "connected":
return `Connected to ${ssid} | Strength: ${wifiStrength}`;
case "connecting":
return `Connecting to ${ssid} | Strength: ${wifiStrength}`;
case "disconnected":
return `Disconnected from ${ssid} | Strength: ${wifiStrength}`;
default:
return `No connection | Strength: ${wifiStrength}`;
}
};
export const getWiredIcon = (internet) => {
if (internet === "connected") return wired.power;
if (internet === "connecting") return wired.poweroff;
if (internet === "disconnected") return wired.poweroff;
return wired.poweroff;
};
export const getWiredTooltip = (internet) => {
return `Status: ${internet}`;
};

View file

@ -0,0 +1,53 @@
import { App, Widget, Utils } from "../imports.js";
const { Box, Revealer, Window } = Widget;
export default ({
onOpen = () => {},
onClose = () => {},
name,
child,
transition = "slide_up",
transitionDuration = 250,
...props
}) => {
const window = Window({
name,
visible: false,
...props,
child: Box({
css: `
min-height: 2px;
min-width: 2px;
`,
child: Revealer({
transition,
transitionDuration,
child: child || Box(),
setup: (self) => {
self.hook(App, (rev, currentName, isOpen) => {
if (currentName === name) {
rev.revealChild = isOpen;
if (isOpen) {
onOpen(window);
} else {
Utils.timeout(transitionDuration, () => {
onClose(window);
});
}
}
});
},
}),
}),
});
window.getChild = () => window.child.children[0].child;
window.setChild = (newChild) => {
window.child.children[0].child = newChild;
window.child.children[0].show_all();
};
return window;
};

View file

@ -0,0 +1,25 @@
import { App, Utils } from "../imports.js";
const { exec, execAsync } = Utils;
function genCommand(arg) {
return ["sh", "-c", `${App.configDir}/bin/hyprctl_swallow ${arg}`];
}
const swallowQuery = genCommand("query");
const swallowToggle = genCommand("toggle");
export const getSwallowStatus = () => {
execAsync(swallowQuery);
let result = exec("hyprctl -j getoption misc:enable_swallow");
return JSON.parse(result).set;
};
export const status = Variable(getSwallowStatus());
export const toggleSwallowStatus = () => {
execAsync(swallowToggle);
// toggle swallow status
status.value = !status.value;
};

View file

@ -0,0 +1,17 @@
import { Widget, SystemTray } from "../imports.js";
const { Button, Icon } = Widget;
export const getTrayItems = (self) => {
self.children = SystemTray.items.map((item) =>
Button({
className: "trayIcon",
child: Icon({
setup: (self) => self.bind("icon", item, "icon"),
}),
setup: (self) =>
self.bind("tooltip-markup", item, "tooltip-markup"),
onPrimaryClick: (_, event) => item.activate(event),
onSecondaryClick: (_, event) => item.openMenu(event),
}),
);
};

View file

@ -0,0 +1,15 @@
import { Variable, App } from "../imports.js";
export const WeatherValue = Variable(
{},
{
poll: [
36000,
["sh", "-c", `python ${App.configDir}/bin/weather`],
(out) => JSON.parse(out),
],
},
);
export const getWeatherIcon = (value) => value.text || "...";
export const getWeatherTooltip = (value) => value.tooltip || "...";

View file

@ -0,0 +1,71 @@
import { Widget } from "../../imports.js";
const { Window, Box, CenterBox } = Widget;
// Widgets
import { LauncherIcon } from "./modules/launcher.js";
import { Workspaces } from "./modules/workspaces.js";
import { Tray } from "./modules/tray.js";
import { BatteryWidget } from "./modules/battery.js";
import { Clock } from "./modules/clock.js";
import { PowerMenu } from "./modules/power.js";
import { Swallow } from "./modules/swallow.js";
import { BluetoothWidget } from "./modules/bluetooth.js";
import { AudioWidget } from "./modules/audio.js";
import { NetworkWidget } from "./modules/network.js";
import { SystemUsage } from "./modules/system.js";
import { Weather } from "./modules/weather.js";
const Top = () =>
Box({
className: "barTop",
vertical: true,
vpack: "start",
children: [LauncherIcon(), SystemUsage(), Weather()],
});
const Center = () =>
Box({
className: "barCenter",
vertical: true,
children: [Workspaces()],
});
const Bottom = () =>
Box({
className: "barBottom",
vertical: true,
vpack: "end",
children: [
Tray(),
Box({
className: "utilsBox",
vertical: true,
children: [
BluetoothWidget(),
AudioWidget(),
Swallow(),
BatteryWidget(),
NetworkWidget(),
],
}),
Clock(),
PowerMenu(),
],
});
export const Bar = ({ monitor } = {}) =>
Window({
name: "bar",
anchor: ["top", "bottom", "left"],
exclusivity: "exclusive",
layer: "top",
margins: [8, 0, 8, 8],
monitor,
child: CenterBox({
className: "bar",
vertical: true,
startWidget: Top(),
centerWidget: Center(),
endWidget: Bottom(),
}),
});

View file

@ -0,0 +1,22 @@
import { Audio, Widget } from "../../../imports.js";
import { getAudioIcon } from "../../../utils/audio.js";
import { launchApp } from "../../../utils/global.js";
const { Button, Icon } = Widget;
const AudioIcon = () =>
Icon({
setup: (self) => {
self.hook(Audio, getAudioIcon, "speaker-changed");
},
});
export const AudioWidget = () => {
return Button({
className: "audio",
cursor: "pointer",
visible: true,
child: AudioIcon(),
onClicked: () => launchApp("pavucontrol"),
});
};

View file

@ -0,0 +1,35 @@
import { Widget, Battery } from "../../../imports.js";
import {
getBatteryPercentage,
getBatteryTooltip,
getBatteryIcon,
} from "../../../utils/battery.js";
const { Button, Box, Label, Revealer } = Widget;
const BatIcon = () =>
Label({ className: "batIcon" })
// NOTE: label needs to be used instead of icon here
.bind("label", Battery, "percent", getBatteryIcon)
.bind("tooltip-text", Battery, "percent", getBatteryTooltip);
const BatStatus = () =>
Revealer({
transition: "slide_down",
transition_duration: 200,
child: Label().bind("label", Battery, "percent", getBatteryPercentage),
});
export const BatteryWidget = () =>
Button({
onPrimaryClick: (self) => {
self.child.children[1].revealChild =
!self.child.children[1].revealChild;
},
child: Box({
className: "battery",
cursor: "pointer",
vertical: true,
children: [BatIcon(), BatStatus()],
visible: Battery.bind("available"),
}),
});

View file

@ -0,0 +1,29 @@
import { Bluetooth, Widget, Utils } from "../../../imports.js";
import {
getBluetoothIcon,
getBluetoothLabel,
getBluetoothClass,
getBluetoothTooltip,
} from "../../../utils/bluetooth.js";
const { Button, Label } = Widget;
const BluetoothModule = () =>
Label({ className: "bluetoothIcon" })
.bind("label", Bluetooth, "connected-devices", getBluetoothIcon)
.bind("class", Bluetooth, "connected-devices", getBluetoothClass)
.bind("label", Bluetooth, "connected-devices", getBluetoothLabel)
.bind(
"tooltip-text",
Bluetooth,
"connected-devices",
getBluetoothTooltip,
);
export const BluetoothWidget = () =>
Button({
className: "bluetooth",
cursor: "pointer",
child: BluetoothModule(),
visible: Bluetooth.connectedDevices.length > 0,
onClicked: () => Utils.exec("blueman-applet"),
});

View file

@ -0,0 +1,26 @@
import { Widget, Utils } from "../../../imports.js";
const { exec, execAsync } = Utils;
const { Label, Box } = Widget;
const Time = () =>
Label({
className: "timeLabel",
setup: (self) => {
// the current quote syntax is the only one that works
// eslint-disable-next-line quotes
self.poll(1000, (self) => (self.label = exec('date "+%H%n%M"')));
self.poll(1000, (self) =>
execAsync(["date", "+%H%n%M"])
.then((time) => (self.label = time))
// eslint-disable-next-line no-undef
.catch(print.error),
);
},
});
export const Clock = () =>
Box({
className: "clock",
vertical: true,
children: [Time()],
});

View file

@ -0,0 +1,15 @@
import { Widget, App } from "../../../imports.js";
import { getLauncherIcon } from "../../../utils/launcher.js";
const { Button, Label } = Widget;
export const LauncherIcon = () =>
Button({
vexpand: false,
className: "launcherIcon",
cursor: "pointer",
child: Label("󱢦"),
onClicked: () => App.toggleWindow("launcher"),
setup: (self) => {
self.hook(App, getLauncherIcon, "window-toggled");
},
});

View file

@ -0,0 +1,10 @@
import { Widget, Utils } from "../../../imports.js";
const { Button, Label } = Widget;
export const Lock = () =>
Button({
className: "lock",
cursor: "pointer",
child: Label(""),
onClicked: () => Utils.exec("swaylock"),
});

View file

@ -0,0 +1,34 @@
import { Network, Widget, Utils } from "../../../imports.js";
import {
getWifiIcon,
getWifiTooltip,
getWiredIcon,
getWiredTooltip,
} from "../../../utils/network.js";
const { Stack, Button, Label } = Widget;
const WifiIndicator = () =>
Label({ has_tooltip: true })
.bind("label", Network.wifi, "strength", getWifiIcon)
.bind("tooltip-text", Network.wifi, "strength", getWifiTooltip);
const WiredIndicator = () =>
Label({ cursor: "pointer" })
.bind("label", Network.wired, "internet", getWiredIcon)
.bind("tooltip-text", Network.wired, "internet", getWiredTooltip);
export const NetworkWidget = () =>
Button({
className: "network",
cursor: "pointer",
onClicked: () => Utils.exec("nm-connection-editor"),
child: Stack({
shown: Network.bind("primary").as(
(/** @type {any} */ p) => p || "wifi",
),
children: {
wifi: WifiIndicator(),
wired: WiredIndicator(),
},
}),
});

View file

@ -0,0 +1,10 @@
import { Widget } from "../../../imports.js";
const { Button, Label } = Widget;
export const PowerMenu = () =>
Button({
vexpand: false,
className: "power",
cursor: "pointer",
child: Label(""),
});

View file

@ -0,0 +1,15 @@
import { Widget } from "../../../imports.js";
const { Label, Button } = Widget;
import { toggleSwallowStatus, status } from "../../../utils/swallow.js";
export const Swallow = () =>
Button({
className: "swallow",
cursor: "pointer",
tooltipText: `Swallow: ${status.value}`,
onPrimaryClick: toggleSwallowStatus,
child: Label({
label: "󰊰",
}),
}).hook(status, (self) => (self.tooltipText = `${status.value}`));

View file

@ -0,0 +1,134 @@
import { Variable, Widget } from "../../../imports.js";
const { Button, Revealer, Box, Label, CircularProgress } = Widget;
const getMemClass = (v) => {
const val = v * 100;
const className = [
[100, "memCritical"],
[75, "memHigh"],
[35, "memMod"],
[5, "memLow"],
[0, "memIdle"],
[-1, "memRevealer"],
].find(([threshold]) => threshold <= val)[1];
return className;
};
const getCpuClass = (v) => {
const val = v * 100;
const className = [
[100, "cpuCritical"],
[75, "cpuHigh"],
[35, "cpuMod"],
[5, "cpuLow"],
[0, "cpuIdle"],
[-1, "cpuRevealer"],
].find(([threshold]) => threshold <= val)[1];
return className;
};
const divide = ([total, free]) => free / total;
const cpu = Variable(0, {
poll: [
2000,
"top -b -n 1",
(out) =>
divide([
100,
out
.split("\n")
.find((line) => line.includes("Cpu(s)"))
.split(/\s+/)[1]
.replace(",", "."),
]),
],
});
const mem = Variable(0, {
poll: [
2000,
"free",
(out) =>
divide(
out
.split("\n")
.find((line) => line.includes("Mem:"))
.split(/\s+/)
.splice(1, 2),
),
],
});
/**
* @param {string} name
* @param {typeof cpu | typeof ram} process
* @param {Array<any>} extraChildren
* @param {() => void} onPrimary
*/
const systemWidget = (name, process, extraChildren = [], onPrimary) =>
Button({
className: name + "Button",
onPrimaryClick: onPrimary,
child: Box({
className: name,
vertical: true,
children: [
CircularProgress({
className: name + "Progress",
// binds: [["value", process]],
rounded: true,
inverted: false,
startAt: 0.27,
}).bind("value", process),
...extraChildren,
],
}),
});
const CPU = systemWidget(
"cpu",
cpu,
[
Revealer({
transition: "slide_down",
child: Label()
.bind("label", cpu, "value", (v) => `${Math.floor(v * 100)}%`)
.bind("className", cpu, "value", getCpuClass),
transition_duration: 250,
}),
],
(self) => {
self.child.children[1].revealChild =
!self.child.children[1].revealChild;
},
);
const MEM = systemWidget(
"mem",
mem,
[
Revealer({
transition: "slide_down",
child: Label()
.bind("label", mem, "value", (v) => `${Math.floor(v * 100)}%`)
.bind("className", cpu, "value", getMemClass),
transition_duration: 250,
}),
],
(self) => {
self.child.children[1].revealChild =
!self.child.children[1].revealChild;
},
);
export const SystemUsage = () =>
Box({
className: "systemUsage",
vertical: true,
cursor: "pointer",
children: [CPU, MEM],
});

View file

@ -0,0 +1,40 @@
import { Widget, SystemTray } from "../../../imports.js";
import { getTrayItems } from "../../../utils/tray.js";
const { Box, EventBox, Label, Revealer } = Widget;
const RevIcon = () =>
Label({
className: "trayChevron",
label: "",
});
const TrayItems = () =>
Box({
className: "trayIcons",
vertical: true,
setup: (self) => {
self.hook(SystemTray, getTrayItems);
},
});
export const Tray = () =>
EventBox({
onPrimaryClick: (self) => {
self.child.children[0].label = self.child.children[1].revealChild
? ""
: "";
self.child.children[1].revealChild =
!self.child.children[1].revealChild;
},
child: Box({
className: "tray",
vertical: true,
children: [
RevIcon(),
Revealer({
transition: "slide_up",
child: TrayItems(),
}),
],
}),
});

View file

@ -0,0 +1,24 @@
import { Widget } from "../../../imports.js";
import {
WeatherValue,
getWeatherIcon,
getWeatherTooltip,
} from "../../../utils/weather.js";
const { Label } = Widget;
const weatherWidget = () =>
Label({
hexpand: false,
vexpand: false,
class_name: "weather",
setup: (self) => {
self.bind("label", WeatherValue, "value", getWeatherIcon);
self.bind("tooltip-text", WeatherValue, "value", getWeatherTooltip);
},
});
export const Weather = () =>
Widget.CenterBox({
vertical: true,
centerWidget: weatherWidget(),
});

View file

@ -0,0 +1,25 @@
import { Widget, Hyprland } from "../../../imports.js";
import { getFocusedWorkspace } from "../../../utils/hyprland.js";
const { Box, Button } = Widget;
const { messageAsync } = Hyprland;
export const Workspaces = () =>
Box({
className: "workspaces",
child: Box({
vertical: true,
children: Array.from({ length: 10 }, (_, i) => i + 1).map((i) =>
Button({
cursor: "pointer",
attribute: { index: i },
onClicked: () => messageAsync(`dispatch workspace ${i}`),
onSecondaryClick: () =>
messageAsync(`dispatch movetoworkspacesilent ${i}`),
}),
),
setup: (self) => {
self.hook(Hyprland, getFocusedWorkspace);
},
}),
});

View file

@ -0,0 +1,11 @@
import { Widget } from "../../imports.js";
const { Box } = Widget;
export const DesktopIcons = () =>
Box({
className: "desktopIcons",
vertical: true,
hpack: "start",
vpack: "start",
children: [],
});

View file

@ -0,0 +1,98 @@
import { Widget, Utils } from "../../imports.js";
const { Box, EventBox, Label, MenuItem, Menu } = Widget;
const { exec, execAsync } = Utils;
/**
* Creates a menu item with an icon.
* @param {string} icon - The icon to display for the menu item.
* @param {string} itemLabel - The label for the menu item.
* @param {Function} onClick - The function to be executed when the menu item is activated.
* @returns {Object} A menu item object with the specified icon, label, and click action.
*/
function ItemWithIcon(icon, itemLabel, onClick) {
return MenuItem({
className: "desktopMenuItem",
child: Box({
children: [
Label({
className: "desktopMenuItemIcon",
label: icon,
}),
Label(itemLabel),
],
}),
onActivate: onClick,
});
}
const Separator = () =>
MenuItem({
child: Box({
className: "separator",
css: `
min-height: 1px;
margin: 3px 6px;
`,
}),
});
const rioMenu = () => {
return [
ItemWithIcon("󰆍", "Terminal", () =>
exec(
'sh -c "$HOME/.config/ags/bin/open_window `slurp -d -c 999999 -w 2` foot"',
),
),
ItemWithIcon("󰘖", "Resize", () =>
exec(
'sh -c "$HOME/.config/ags/bin/move_window `slurp -d -c 999999 -w 2`"',
),
),
ItemWithIcon("󰁁", "Move", () => exec("hyprctl dispatch submap move")),
ItemWithIcon("󰅖", "Delete", () => exec("hyprctl kill")),
Separator(),
];
};
const Powermenu = () => {
return MenuItem({
className: "desktopMenuItem",
child: Box({
children: [
Label({
className: "desktopMenuItemIcon",
label: "󰐥",
}),
Label("Powermenu"),
],
}),
submenu: Menu({
className: "desktopMenu",
children: [
ItemWithIcon("󰍁", "Lock", () => Utils.exec("gtklock")),
ItemWithIcon("󰍃", "Log Out", () =>
exec("hyprctl dispatch exit"),
),
ItemWithIcon("󰖔", "Suspend", () => exec("systemctl suspend")),
ItemWithIcon("󰜉", "Reboot", () => exec("systemctl reboot")),
ItemWithIcon("󰐥", "Shutdown", () => exec("systemctl poweroff")),
],
}),
});
};
export const DesktopMenu = () =>
EventBox({
onSecondaryClick: (_, event) =>
Menu({
className: "desktopMenu",
children: [
...rioMenu(),
ItemWithIcon("󰈊", "Colorpicker", () =>
execAsync(["hyprpicker", "-a", "wl-copy"]),
),
Separator(),
Powermenu(),
],
}).popup_at_pointer(event),
});

View file

@ -0,0 +1,17 @@
import { Widget } from "../../imports.js";
const { Window } = Widget;
import { DesktopMenu } from "./desktopMenu.js";
import { DesktopIcons } from "./desktopIcons.js";
export const Desktop = ({ monitor } = {}) =>
Window({
name: "desktop",
anchor: ["top", "bottom", "left", "right"],
layer: "bottom",
monitor,
child: Widget.Overlay({
child: DesktopMenu(),
overlays: [DesktopIcons()],
}),
});

View file

@ -0,0 +1,113 @@
import { Widget, App, Applications, Utils, Hyprland } from "../../imports.js";
import PopupWindow from "../../utils/popupWindow.js";
const { Box, Button, Icon, Label, Scrollable, Entry } = Widget;
const WINDOW_NAME = "launcher";
const truncateString = (str, maxLength) =>
str.length > maxLength ? `${str.slice(0, maxLength)}...` : str;
const AppItem = (app) =>
Button({
className: "launcherApp",
onClicked: () => {
App.closeWindow(WINDOW_NAME);
Hyprland.messageAsync(`dispatch exec gtk-launch ${app.desktop}`);
++app.frequency;
},
setup: (self) => (self.app = app),
child: Box({
children: [
Icon({
className: "launcherItemIcon",
icon: app.iconName || "",
size: 24,
}),
Box({
className: "launcherItem",
vertical: true,
vpack: "center",
children: [
Label({
className: "launcherItemTitle",
label: app.name,
xalign: 0,
vpack: "center",
truncate: "end",
}),
!!app.description &&
Widget.Label({
className: "launcherItemDescription",
label:
truncateString(app.description, 75) || "",
wrap: true,
xalign: 0,
justification: "left",
vpack: "center",
}),
],
}),
],
}),
});
const Launcher = () => {
const list = Box({ vertical: true });
const entry = Entry({
className: "launcherEntry",
hexpand: true,
text: "-",
onAccept: ({ text }) => {
const isCommand = text.startsWith(">");
const appList = Applications.query(text || "");
if (isCommand === true) {
App.toggleWindow(WINDOW_NAME);
Utils.execAsync(text.slice(1));
} else if (appList[0]) {
App.toggleWindow(WINDOW_NAME);
appList[0].launch();
}
},
onChange: ({ text }) =>
list.children.map((item) => {
item.visible = item.app.match(text);
}),
});
return Widget.Box({
className: "launcher",
vertical: true,
setup: (self) => {
self.hook(App, (_, name, visible) => {
if (name !== WINDOW_NAME) return;
list.children = Applications.list.map(AppItem);
entry.text = "";
if (visible) entry.grab_focus();
});
},
children: [
entry,
Scrollable({
hscroll: "never",
css: "min-width: 250px; min-height: 360px;",
child: list,
}),
],
});
};
export const AppLauncher = () =>
PopupWindow({
name: WINDOW_NAME,
anchor: ["top", "bottom", "right"],
margins: [13, 13, 0, 13],
layer: "overlay",
transition: "slide_down",
transitionDuration: 150,
popup: true,
keymode: "on-demand",
child: Launcher(),
});

View file

@ -0,0 +1,29 @@
import { Icons, Widget } from "../../imports.js";
import { mprisStateIcon } from "../../utils/mpris.js";
export default (player) =>
Widget.CenterBox({
className: "controls",
hpack: "center",
startWidget: Widget.Button({
onClicked: () => player.previous(),
child: Widget.Icon(Icons.media.previous),
}),
centerWidget: Widget.Button({
onClicked: () => player.playPause(),
child: Widget.Icon().bind(
"icon",
player,
"play-back-status",
mprisStateIcon,
),
}),
endWidget: Widget.Button({
onClicked: () => player.next(),
child: Widget.Icon(Icons.media.next),
}),
});

View file

@ -0,0 +1,9 @@
import { Widget } from "../../imports.js";
export default (player) =>
Widget.Box({ className: "cover" }).bind(
"css",
player,
"cover-path",
(cover) => `background-image: url('${cover ?? ""}')`,
);

View file

@ -0,0 +1,45 @@
import { Mpris, Widget } from "../../imports.js";
import { findPlayer, generateBackground } from "../../utils/mpris.js";
import PopupWindow from "./popup_window.js";
import Cover from "./cover.js";
import { Artists, Title } from "./title_artists.js";
import TimeInfo from "./time_info.js";
import Controls from "./controls.js";
import PlayerInfo from "./player_info.js";
const Info = (player) =>
Widget.Box({
className: "info",
vertical: true,
vexpand: false,
hexpand: false,
homogeneous: true,
children: [
PlayerInfo(player),
Title(player),
Artists(player),
Controls(player),
TimeInfo(player),
],
});
const MusicBox = (player) =>
Widget.Box({
className: "music window",
children: [Cover(player), Info(player)],
}).bind("css", player, "cover-path", generateBackground);
export const Media = () =>
PopupWindow({
monitor: 0,
anchor: ["top"],
layer: "top",
margins: [8, 0, 0, 0],
name: "music",
child: Widget.Box(),
}).bind("child", Mpris, "players", (players) => {
if (players.length == 0) return Widget.Box();
return MusicBox(findPlayer(players));
});

View file

@ -0,0 +1,23 @@
import { Icons, Utils, Widget } from "../../imports.js";
export default (player) =>
Widget.Box({
className: "player-info",
vexpand: true,
vpack: "start",
children: [
Widget.Icon({
hexpand: true,
hpack: "end",
className: "player-icon",
tooltipText: player.identity ?? "",
}).bind("icon", player, "entry", (entry) => {
// the Spotify icon is called spotify-client
if (entry == "spotify") entry = "spotify-client";
return Utils.lookUpIcon(entry ?? "")
? entry
: Icons.media.player;
}),
],
});

View file

@ -0,0 +1,46 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import { Widget } from "../../imports.js";
const { Box, Revealer, Window } = Widget;
export default ({
name,
child,
revealerSetup = null,
transition = "crossfade",
transitionDuration = 200,
...props
}) => {
const window = Window({
name,
popup: false,
focusable: false,
visible: false,
...props,
setup: (self) => (self.getChild = () => child),
child: Box({
css: `
min-height: 1px;
min-width: 1px;
padding: 1px;
`,
child: Revealer({
transition,
transitionDuration,
child: child,
setup:
revealerSetup ??
((self) =>
self.hook(App, (self, currentName, visible) => {
if (currentName === name) {
self.reveal_child = visible;
}
})),
}),
}),
});
return window;
};

View file

@ -0,0 +1,67 @@
import { Widget } from "../../imports.js";
import { lengthStr } from "../../utils/mpris.js";
export const PositionLabel = (player) =>
Widget.Label({
className: "position",
hexpand: true,
xalign: 0,
setup: (self) => {
const update = (_, time) => {
player.length > 0
? (self.label = lengthStr(time || player.position))
: (self.visible = !!player);
};
self.hook(player, update, "position").poll(1000, update);
},
});
export const LengthLabel = (player) =>
Widget.Label({
className: "length",
hexpand: true,
xalign: 1,
})
.bind("visible", player, "length", (length) => length > 0)
.bind("label", player, "length", (length) => lengthStr(length));
export const Position = (player) =>
Widget.Slider({
className: "position",
draw_value: false,
onChange: ({ value }) => (player.position = player.length * value),
setup: (self) => {
const update = () => {
if (self.dragging) return;
self.visible = player.length > 0;
if (player.length > 0) {
self.value = player.position / player.length;
}
};
self.hook(player, update)
.hook(player, update, "position")
.poll(1000, update);
},
});
export default (player) =>
Widget.Box({
vertical: true,
vexpand: true,
vpack: "end",
children: [
Widget.Box({
hexpand: true,
children: [PositionLabel(player), LengthLabel(player)],
}),
Position(player),
],
});

View file

@ -0,0 +1,32 @@
import { Widget } from "../../imports.js";
export const Title = (player) =>
Widget.Scrollable({
className: "title",
vscroll: "never",
hscroll: "automatic",
child: Widget.Label({
className: "title",
label: "Nothing playing",
}).bind(
"label",
player,
"track-title",
(title) => title ?? "Nothing playing",
),
});
export const Artists = (player) =>
Widget.Scrollable({
className: "artists",
vscroll: "never",
hscroll: "automatic",
child: Widget.Label({ className: "artists" }).bind(
"label",
player,
"track-artists",
(artists) => artists.join(", ") ?? "",
),
});

View file

@ -0,0 +1,134 @@
import { Hyprland, Notifications, Utils, Widget } from "../../imports.js";
const { Box, Icon, Label, Button, EventBox, Window } = Widget;
const { lookUpIcon } = Utils;
const closeAll = () => {
Notifications.popups.map((n) => n.dismiss());
};
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Box({
css: `
background-image: url("${image}");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
`,
});
}
if (lookUpIcon(app_icon)) {
return Icon(app_icon);
}
if (app_entry && lookUpIcon(app_entry)) {
return Icon(app_entry);
}
return null;
};
const Notification = (notif) => {
const icon = Box({
vpack: "start",
class_name: "icon",
// @ts-ignore
setup: (/** @type {{ child: any; }} */ self) => {
const icon = NotificationIcon(notif);
if (icon !== null) self.child = icon;
},
});
const title = Label({
class_name: "title",
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: notif.summary,
use_markup: true,
});
const body = Label({
class_name: "body",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
max_width_chars: 100,
wrap: true,
label: notif.body,
});
const actions = Box({
class_name: "actions",
children: notif.actions
.filter(({ id }) => id != "default")
.map(({ id, label }) =>
Button({
class_name: "action-button",
on_clicked: () => notif.invoke(id),
hexpand: true,
child: Label(label),
}),
),
});
return EventBox({
on_primary_click: () => {
if (notif.actions.length > 0) notif.invoke(notif.actions[0].id);
},
on_middle_click: closeAll,
on_secondary_click: () => notif.dismiss(),
child: Box({
class_name: `notification ${notif.urgency}`,
vertical: true,
children: [
Box({
class_name: "info",
children: [
icon,
Box({
vertical: true,
class_name: "text",
vpack: "center",
setup: (self) => {
if (notif.body.length > 0)
self.children = [title, body];
else self.children = [title];
},
}),
],
}),
actions,
],
}),
});
};
let lastMonitor;
export const Notifs = () =>
Window({
name: "notifications",
anchor: ["top", "right"],
margins: [8, 8, 8, 0],
child: Box({
css: "padding: 1px;",
class_name: "notifications",
vertical: true,
// @ts-ignore
children: Notifications.bind("popups").transform((popups) => {
return popups.map(Notification);
}),
}),
}).hook(Hyprland.active, (self) => {
// prevent useless resets
if (lastMonitor === Hyprland.active.monitor) return;
self.monitor = Hyprland.active.monitor.id;
});

View file

@ -0,0 +1,58 @@
import { Widget, Utils } from "../../imports.js";
import Brightness from "../../services/brightness.js";
const { Box, Slider, Label, Revealer } = Widget;
const BrightnessIcon = () =>
Label({
className: "brtPopupIcon",
setup: (self) => {
self.hook(Brightness, (self) => {
const icons = ["", "", "", "", "", "", "", "", ""];
let index = Math.floor((Brightness.screen * 100) / 11);
index = Math.max(0, Math.min(index, icons.length - 1));
if (index >= 0 && index < icons.length) {
self.label = icons[index].toString();
} else {
log("Index out of bounds:", index);
}
});
},
});
const PercentBar = () =>
Slider({
className: "brtPopupBar",
drawValue: false,
onChange: ({ value }) => (Brightness.screen = value),
setup: (self) => {
self.hook(Brightness, (self) => (self.value = Brightness.screen));
},
});
export const BrightnessPopup = () =>
Box({
css: `min-height: 1px;
min-width: 1px;`,
child: Revealer({
transition: "slide_up",
child: Box({
className: "brightnessPopup",
children: [BrightnessIcon(), PercentBar()],
}),
attribute: { count: 0 },
setup: (self) => {
self.hook(Brightness, (self) => {
self.revealChild = true;
self.attribute.count++;
Utils.timeout(1500, () => {
self.attribute.count--;
if (self.attribute.count === 0)
self.revealChild = false;
});
});
},
}),
});

View file

@ -0,0 +1,18 @@
import { Widget } from "../../imports.js";
// Widgets
import { BrightnessPopup } from "./brightnessPopup.js";
import { VolumePopup } from "./volumePopup.js";
export const Popups = () =>
Widget.Window({
name: "popups",
className: "popups",
anchor: ["bottom", "right"],
layer: "overlay",
margins: [0, 12, 8, 0],
child: Widget.Box({
vertical: true,
children: [BrightnessPopup(), VolumePopup()],
}),
});

View file

@ -0,0 +1,36 @@
import { Widget, Utils, Audio } from "../../imports.js";
import { getSliderIcon, volumePercentBar } from "../../utils/audio.js";
const { Box, Revealer } = Widget;
const { speaker } = Audio;
const { timeout } = Utils;
export const VolumePopup = () =>
Box({
css: `
min-height: 2px;
min-width: 2px;
`,
child: Revealer({
transition: "slide_up",
child: Box({
className: "volumePopup",
children: [getSliderIcon(), volumePercentBar()],
}),
attribute: { count: 0 },
setup: (self) =>
self.hook(
speaker,
() => {
self.reveal_child = true;
self.attribute.count++;
timeout(1500, () => {
self.attribute.count--;
if (self.attribute.count === 0)
self.reveal_child = false;
});
},
"notify::volume",
),
}),
});

View file

@ -0,0 +1,18 @@
{
"name": "nyx-ags",
"version": "1.5.5",
"author": "NotAShelf",
"description": "The ags configuretion segment of my NixOS configurations.",
"main": "config.js",
"scripts": {
"lint": "eslint . --fix",
"stylelint": "stylelint ./scss --fix",
"all": "nix-shell -p nodejs --run \"npm install\" && npm run lint && npm run stylelint && rm -rf node_modules"
},
"devDependencies": {
"stylelint-config-standard-scss": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"eslint": "^8.44.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
{pkgs ? import <nixpkgs> {}}: let
# trivial builders
inherit (pkgs) mkShell writeShellScriptBin;
in
mkShell {
buildInputs = with pkgs; [
nodejs-slim
# python3 w/ requests is necessary for weather data fetch
# ags actually doesn't start without it since it's stored
# as a variable
(python3.withPackages (ps: [ps.requests]))
# while developing locally, you need types and other eslint deps
# so that our eslint config works properly
# pnpm is used to fetch the deps from package.json
nodePackages.pnpm
# dart-sass is for compiling the stylesheets
dart-sass
(writeShellScriptBin "compile-stylesheet" ''
# compile scss files
${dart-sass}/bin/sass --verbose \
--style compressed \
--no-source-map --fatal-deprecation --future-deprecation \
./style/main.scss > ./style.css
'')
];
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,34 @@
// Components
// top
@import "modules/launcher";
@import "modules/system";
@import "modules/weather";
// center
@import "modules/workspaces";
// bottom
@import "modules/tray";
@import "modules/battery";
@import "modules/bluetooth";
@import "modules/lock";
@import "modules/swallow";
@import "modules/audio";
@import "modules/net";
@import "modules/clock";
@import "modules/power";
// general config
.bar {
@include barWindow;
}
// top section
.utilsBox {
@include barSection;
}
// bottom section
.systemInfo {
@include barSection;
}

View file

@ -0,0 +1,3 @@
.audio {
@include barModule;
}

View file

@ -0,0 +1,3 @@
.battery {
@include barModule;
}

View file

@ -0,0 +1,28 @@
// default state of the bluetooth icon
.bluetooth {
@include barModule;
}
// if bluetooth is paired
// but not active
.bluetooth-paired {
color: $onSurface;
}
// if bluetooth is paired
// and active
.bluetooth-active {
color: $onSurface;
}
// if bluetooth is disabled
.bluetooth-disabled {
color: lighten($surfaceVariant, 30%);
transition:
all 0.3s $materialStandard,
border 0.35s $materialStandard;
&:hover {
color: $red;
}
}

View file

@ -0,0 +1,9 @@
.clock {
background: $surfaceVariant;
color: $onPrimary;
font-family: $monoFont;
font-weight: 800;
border-radius: 12px;
margin: 6px 4px;
padding: 6px;
}

View file

@ -0,0 +1,13 @@
.launcherIcon {
background: $surfaceVariant;
font-family: $iconFont;
border-radius: 12px;
margin: 6px 4px;
padding: 6px;
min-height: 1.5rem;
transition: all 0.2s $materialAccel;
&:hover {
background: lighten($surfaceVariant, 5%);
}
}

View file

@ -0,0 +1,7 @@
.lock {
background: $surface;
font-size: 24px;
border-radius: 12px;
margin: 2px 4px;
padding: 2px;
}

View file

@ -0,0 +1,3 @@
.network {
@include barModule;
}

View file

@ -0,0 +1,15 @@
.power {
color: $red;
background: $surface;
font-size: 24px;
border-radius: 12px;
margin: 6px 4px;
padding: 6px;
min-height: 1.5rem;
transition: all 0.2s $materialAccel;
&:hover {
background: lighten($surfaceVariant, 5%);
color: lighten($red, 5%);
}
}

View file

@ -0,0 +1,3 @@
.swallow {
@include barModule;
}

View file

@ -0,0 +1,41 @@
$size: 1.2rem;
.systemUsage {
color: $onSurface;
background: $surfaceVariant;
font-family: $iconFont;
border-radius: 12px;
margin: 4px;
}
// cpu indicator
.cpuButton {
padding: 6px 2px 3px 2px;
margin: 1px;
}
.cpuProgress {
color: $lavender;
padding: 4px 4px;
margin: 0.1rem;
font-size: 4px;
background: $primary;
min-height: $size;
min-width: $size;
}
// memory indicator
.memButton {
padding: 3px 2px 6px 2px;
margin: 1px;
}
.memProgress {
color: $blue;
padding: 4px 4px;
margin: 0.1rem;
font-size: 4px;
background: $primary;
min-height: $size;
min-width: $size;
}

View file

@ -0,0 +1,15 @@
.tray {
margin: 0;
}
.trayChevron {
font-family: $iconFont;
}
.trayIcons {
margin: 3px 0 0;
}
.trayIcon {
margin: 0 0 3px;
}

View file

@ -0,0 +1,14 @@
.weather {
background: $surfaceVariant;
font-family: "Material Symbols Sharp", Roboto;
border-radius: 12px;
margin: 6px 4px;
padding: 4px;
min-height: 1.5rem;
min-width: 1rem;
transition: all 0.2s $materialAccel;
&:hover {
background: lighten($surfaceVariant, 5%);
}
}

View file

@ -0,0 +1,29 @@
.workspaces {
background: transparent;
padding: 14px;
button {
@include barModule;
// override some styles provided by barModule
// to better suit the position of the workspaces
// module
margin: 5px 3px;
min-width: 0.6rem;
min-height: 0.6rem;
color: transparent;
background: $onSurface;
border-radius: 99px;
&:hover {
background: $lavender;
}
&.focused {
border-radius: 18px;
background: $blue;
padding: 8px 0;
}
}
}

View file

@ -0,0 +1,3 @@
$materialStandard: cubic-bezier(0.2, 0, 0, 1);
$materialDecel: cubic-bezier(0, 0, 0, 1);
$materialAccel: cubic-bezier(0.3, 0, 1, 1);

View file

@ -0,0 +1,16 @@
$primary: #1e1e2e;
$onPrimary: #cdd6f4;
$secondary: #181825;
$onSecondary: #cdd6f4;
$surface: #313244;
$onSurface: #cdd6f4;
$surfaceVariant: #313244;
$onSurfaceVariant: #cdd6f4;
$shadow: #000;
// other colors
$red: #f38ba8;
$yellow: #f9e2af;
$green: #a6e3a1;
$blue: #89b4fa;
$lavender: #b4befe;

View file

@ -0,0 +1,3 @@
// Components
@import "desktopMenu";
@import "desktopIcons";

View file

@ -0,0 +1,18 @@
.desktopIcons {
margin: 24px 0 0 24px;
}
.desktopIcon {
border-radius: 6px;
padding: 6px;
transition: all 200ms cubic-bezier(0, 0, 1, 1);
&:hover {
background: transparentize($lavender, 0.3);
}
}
.desktopIconLabel {
color: $surface;
font-family: $font;
}

View file

@ -0,0 +1,29 @@
.desktopMenu {
background: $primary;
color: $onSurface;
font-family: $font;
border-radius: 14px;
padding: 6px 3px;
}
.desktopMenuItem {
border-radius: 14px;
margin: 0 3px;
padding: 6px 12px;
transition: all 0.2s $materialAccel;
&:hover {
background: lighten($surfaceVariant, 5%);
color: lighten($onSurface, 5%);
}
}
.desktopMenuItemIcon {
font-family: $iconFont;
padding: 2px 8px 2px 2px;
}
.separator {
background: $surface;
padding: 1px 3px;
}

View file

@ -0,0 +1,3 @@
$font: roboto;
$monoFont: robotomono;
$iconFont: symbolsnerdfontmono;

View file

@ -0,0 +1,42 @@
.launcher {
color: $onSurface;
background: $primary;
font-family: $font;
border-radius: 12px;
margin: 0 0 16px;
padding: 6px;
}
.launcherApp {
border-radius: 12px;
margin: 3px;
padding: 3px;
&:focus {
background: $surfaceVariant;
}
}
.launcherEntry {
caret-color: $onSurface;
background: $surfaceVariant;
border-radius: 10px;
margin: 6px;
padding: 3px 12px;
}
.launcherItem {
}
.launcherItemIcon {
margin: 3px 6px;
}
.launcherItemTitle {
font-size: 16px;
font-weight: bold;
}
.launcherItemDescription {
font-size: 12px;
}

View file

@ -0,0 +1,14 @@
// common components
@import "colors";
@import "beziers";
@import "fonts";
@import "mixins";
@import "prelude";
// modules and widgets
@import "bar/bar";
@import "launcher/launcher";
@import "desktop/desktop";
@import "popups/popups";
@import "music/music";
@import "notifications/notifications";

View file

@ -0,0 +1,93 @@
$rounding: 12px; // intter rounding
$padding: 2px; // component padding
// general mixins
@mixin animated {
transition:
all 0.3s $materialStandard,
border 0.35s $materialStandard;
}
@mixin roundingOuter {
// Outer rounding = inner rounding + padding
border-radius: calc(#{$rounding} + #{$padding});
}
@mixin roundingInner {
border-radius: $rounding;
}
// window mixin represents a top-level window
// e.g. the bar or a music widget
@mixin window {
@include roundingOuter;
}
// bar window
@mixin barWindow {
@include window;
color: $onSurface;
background: $primary;
font-family: $font;
border-radius: 14px;
padding: 2px;
}
@mixin barSection {
@include roundingInner;
color: $onSurface;
background: $surfaceVariant;
font-family: $iconFont;
margin: 4px;
padding: 6px;
}
@mixin barModule {
// include animations
@include animated;
// standard widget dimensions
margin: 6px 3px;
// and styling
color: $onSurface;
background: $surfaceVariant;
&:hover {
color: $lavender;
}
}
// popup windows
@mixin popup {
background: $primary;
border-radius: 12px;
margin: 6px;
padding: 12px;
min-width: 200px;
}
@mixin popupIcon {
font-family: $iconFont;
margin: 0 6px 0 0;
}
@mixin popupBar {
min-width: 200px;
& scale {
min-height: 12px;
}
& trough {
background: transparentize($surfaceVariant, 0.5);
border-radius: 24px;
min-height: 12px;
}
& highlight {
background: $onSurface;
border-radius: 24px;
min-width: 12px;
}
}

View file

@ -0,0 +1,56 @@
.music.window {
background: rgba(0, 0, 0, 0.5);
margin: 5px 10px 15px;
padding: $padding;
.cover {
background-position: center;
background-size: cover;
border-radius: $rounding;
box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.5);
margin: 0.4rem;
min-height: 13rem;
min-width: 13rem;
}
}
.music.window .info {
margin: 0.5rem;
label,
scale {
margin: 0.3rem 0;
}
label.position,
label.length {
font-size: 0.8rem;
margin-bottom: 0;
}
scale {
margin-top: 0;
margin-bottom: 0;
}
.title {
font-size: 1.5rem;
font-weight: bold;
min-width: 14rem;
}
}
.music.window .controls {
button {
margin: 0 0.2rem;
font-size: 1.5rem;
}
}
.music.window .player-info {
margin-bottom: 0;
.player-icon {
font-size: 1.2rem;
}
}

View file

@ -0,0 +1,69 @@
.notification {
@include window;
margin: 5px 5px 5px 12px;
min-width: 25rem;
background-color: $primary;
color: $onPrimary;
// low priority
&.low {
border: 1.5px solid $lavender;
}
// medium priority
&.normal {
border: 1.5px solid $blue;
}
// critical priority
&.critical {
border: 1.5px solid $red;
}
}
.notifications widget:last-child .notification {
margin-bottom: 15px;
}
.notification .icon {
image {
font-size: 5rem;
margin: 0.5rem;
min-height: 5rem;
min-width: 5rem;
}
> box {
border-radius: $rounding;
margin: 0.5rem;
min-height: 5rem;
min-width: 5rem;
}
}
.notification .actions .action-button {
@include window;
@include animated;
padding: 4px 0;
&:hover {
background: $onSurfaceVariant;
}
}
.notification .text {
margin: 6px 4px;
.title {
margin: 4px 6px;
color: $onPrimary;
font-weight: 900;
}
.body {
margin: 4px 6px;
color: $onPrimary;
font-weight: 600;
}
}

View file

@ -0,0 +1,11 @@
.brightnessPopup {
@include popup;
}
.brtPopupIcon {
@include popupIcon;
}
.brtPopupBar {
@include popupBar;
}

View file

@ -0,0 +1,3 @@
// Components
@import "brightnessPopup";
@import "volumePopup";

View file

@ -0,0 +1,11 @@
.volumePopup {
@include popup;
}
.volPopupIcon {
@include popupIcon;
}
.volPopupBar {
@include popupBar;
}

View file

@ -0,0 +1,12 @@
* {
all: unset;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
font-family: "Material Design Icons", "Iosevka Nerd Font Mono", Inter,
Roboto, sans-serif;
}
// restore tooltip styling that got unset in the above class
tooltip {
@include roundingOuter;
background: $primary;
}

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"allowJs": true,
"checkJs": true,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"typeRoots": ["./types"],
"skipLibCheck": true,
},
}