feat: update ags

This commit is contained in:
2023-12-30 20:38:47 +01:00
parent d2f9104fe4
commit 32d78e57a3
213 changed files with 8155 additions and 9843 deletions

View File

@@ -0,0 +1,77 @@
env:
es2021: true
extends: eslint:recommended
overrides: []
parserOptions:
ecmaVersion: latest
sourceType: "module"
rules:
arrow-parens:
- error
- as-needed
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
- single
- avoidEscape: true
semi:
- error
- always
nonblock-statement-body-position:
- error
- below
no-trailing-spaces:
- error
array-bracket-spacing:
- error
- never
key-spacing:
- error
- beforeColon: false
afterColon: true
object-curly-spacing:
- error
- always
no-useless-escape:
- off
globals:
pkg: readonly
ags: readonly
ARGV: readonly
imports: readonly
print: readonly
console: readonly
logError: readonly

5
home/desktops/hyprland/ags/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
types
package-lock.json
weather_key
setup.sh

View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/scripts/templates" />
</list>
</option>
</component>
</module>

View File

@@ -1,8 +0,0 @@
<project version="4">
<component name="ComposerSettings">
<execution />
</component>
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ags.iml" filepath="$PROJECT_DIR$/.idea/ags.iml" />
</modules>
</component>
</project>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -1,58 +1,21 @@
// Import
import { App, Utils } from "./imports.js";
// Windows
import Bar from "./windows/bar.js";
import Cheatsheet from "./windows/cheatsheet.js";
import {
CornerTopleft,
CornerTopright,
CornerBottomleft,
CornerBottomright,
} from "./windows/corners.js";
import Indicator from "./windows/osd.js";
import Osk from "./windows/osk.js";
import Overview from "./windows/overview.js";
import Session from "./windows/session.js";
import SideLeft from "./windows/sideleft.js";
import SideRight from "./windows/sideright.js";
import { readFile } from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import { timeout } from "resource:///com/github/Aylur/ags/utils.js";
const pkgjson = JSON.parse(readFile(App.configDir + "/package.json"));
const CLOSE_ANIM_TIME = 150;
timeout(1000, () => JSON.stringify(App));
// Init
Utils.exec(`bash -c 'mkdir -p ~/.cache/ags/user'`);
// SCSS compilation
Utils.exec(`sassc ${App.configDir}/scss/main.scss ${App.configDir}/style.css`);
App.resetCss();
App.applyCss(`${App.configDir}/style.css`);
// Config object
export default {
style: `${App.configDir}/style.css`,
stackTraceOnError: true,
closeWindowDelay: {
// For animations
sideright: CLOSE_ANIM_TIME,
sideleft: CLOSE_ANIM_TIME,
osk: CLOSE_ANIM_TIME,
// No anims, but allow menu service update
session: 1,
overview: 1,
cheatsheet: 1,
},
windows: [
//Bar(),
...Array.from({ length: 3 }, (_, i) => [
CornerTopleft(i),
CornerTopright(i),
CornerBottomleft(i),
CornerBottomright(i),
]),
//Overview(),
Indicator(),
//Cheatsheet(),
SideRight(),
SideLeft(),
//Osk(), // On-screen keyboard
//Session(),
],
const v = {
ags: `v${pkg.version}`,
expected: `v${pkgjson.version}`,
};
function mismatch() {
print(`my config expects ${v.expected}, but your ags is ${v.ags}`);
App.connect("config-parsed", (app) => app.Quit());
return {};
}
export default v.ags === v.expected
? (await import("./js/main.js")).default
: mismatch();

View File

@@ -1,132 +0,0 @@
export const keybindList = [[
{
"icon": "pin_drop",
"name": "Workspaces: navigation",
"binds": [
{ "keys": ["", "+", "#"], "action": "Go to workspace #" },
{ "keys": ["", "+", "S"], "action": "Toggle special workspace" },
{ "keys": ["", "+", "(Scroll ↑↓)"], "action": "Go to workspace -1/+1" },
{ "keys": ["Ctrl", "", "+", "←"], "action": "Go to workspace on the left" },
{ "keys": ["Ctrl", "", "+", "→"], "action": "Go to workspace on the right" },
{ "keys": ["", "+", "PageUp"], "action": "Go to workspace on the left" },
{ "keys": ["", "+", "PageDown"], "action": "Go to workspace on the right" }
],
"appeartick": 1
},
{
"icon": "overview_key",
"name": "Workspaces: management",
"binds": [
{ "keys": ["", "Alt", "+", "#"], "action": "Move window to workspace #" },
{ "keys": ["", "Alt", "+", "S"], "action": "Move window to special workspace" },
{ "keys": ["", "Alt", "+", "PageUp"], "action": "Move window to workspace on the left" },
{ "keys": ["", "Alt", "+", "PageDown"], "action": "Move window to workspace on the right" }
],
"appeartick": 1
},
{
"icon": "move_group",
"name": "Windows",
"binds": [
{ "keys": ["", "+", "←↑→↓"], "action": "Focus window in direction" },
{ "keys": ["", "Shift", "+", "←↑→↓"], "action": "Swap window in direction" },
{ "keys": ["", "+", ";"], "action": "Split ratio -" },
{ "keys": ["", "+", "'"], "action": "Split ratio +" },
{ "keys": ["", "+", "Lmb"], "action": "Move window" },
{ "keys": ["", "+", "Mmb"], "action": "Move window" },
{ "keys": ["", "+", "Rmb"], "action": "Resize window" },
{ "keys": ["", "+", "F"], "action": "Fullscreen" },
{ "keys": ["", "Alt", "+", "F"], "action": "Fake fullscreen" }
],
"appeartick": 1
}
],
[
{
"icon": "widgets",
"name": "Widgets (AGS)",
"binds": [
{ "keys": ["", "OR", "", "+", "Tab"], "action": "Toggle overview/launcher" },
{ "keys": ["Ctrl", "", "+", "R"], "action": "Restart AGS" },
{ "keys": ["", "+", "/"], "action": "Toggle this cheatsheet" },
{ "keys": ["", "+", "N"], "action": "Toggle sidebar" },
{ "keys": ["", "+", "K"], "action": "Toggle virtual keyboard" },
{ "keys": ["Ctrl", "Alt", "+", "Del"], "action": "Power/Session menu" },
{ "keys": ["Esc"], "action": "Exit a window" },
{ "keys": ["rightCtrl"], "action": "Dismiss/close sidebar" },
// { "keys": ["", "+", "B"], "action": "Toggle left sidebar" },
// { "keys": ["", "+", "N"], "action": "Toggle right sidebar" },
// { "keys": ["", "+", "G"], "action": "Toggle volume mixer" },
// { "keys": ["", "+", "M"], "action": "Toggle useless audio visualizer" },
// { "keys": ["(right)Ctrl"], "action": "Dismiss notification & close menus" }
],
"appeartick": 2
},
{
"icon": "construction",
"name": "Utilities",
"binds": [
{ "keys": ["PrtSc"], "action": "Screenshot >> clipboard" },
{ "keys": ["", "Shift", "+", "S"], "action": "Screen snip >> clipboard" },
{ "keys": ["", "Shift", "+", "T"], "action": "Image to text >> clipboard" },
{ "keys": ["", "Shift", "+", "C"], "action": "Color picker" },
{ "keys": ["", "Alt", "+", "R"], "action": "Record region" },
{ "keys": ["Ctrl", "Alt", "+", "R"], "action": "Record region with sound" },
{ "keys": ["", "Shift", "Alt", "+", "R"], "action": "Record screen with sound" }
],
"appeartick": 2
},
// {
// "icon": "edit",
// "name": "Edit mode",
// "binds": [
// { "keys": ["Esc"], "action": "Exit Edit mode" },
// { "keys": ["#"], "action": "Go to to workspace #" },
// { "keys": ["Alt", "+", "#"], "action": "Dump windows to workspace #" },
// { "keys": ["Shift", "+", "#"], "action": "Swap windows with workspace #" },
// { "keys": ["Lmb"], "action": "Move window" },
// { "keys": ["Mmb"], "action": "Move window" },
// { "keys": ["Rmb"], "action": "Resize window" }
// ],
// "appeartick": 2
// }
],
[
{
"icon": "apps",
"name": "Apps",
"binds": [
{ "keys": ["", "+", "T"], "action": "Launch terminal: foot" },
{ "keys": ["", "+", "↵"], "action": "Launch terminal: WezTerm" },
{ "keys": ["", "+", "W"], "action": "Launch browser: Firefox" },
{ "keys": ["", "+", "C"], "action": "Launch editor: vscode" },
{ "keys": ["", "+", "X"], "action": "Launch editor: GNOME Text Editor" },
{ "keys": ["", "+", "I"], "action": "Launch settings: GNOME Control center" }
],
"appeartick": 3
},
{
"icon": "keyboard",
"name": "Typing",
"binds": [
{ "keys": ["", "+", "V"], "action": "Clipboard history >> clipboard" },
{ "keys": ["", "+", "."], "action": "Emoji picker >> clipboard" },
{ "keys": ["", "+", " 󱁐 "], "action": "Switch language" }
],
"appeartick": 3
},
{
"icon": "terminal",
"name": "Launcher commands",
"binds": [
{ "keys": [">raw"], "action": "Toggle mouse acceleration" },
{ "keys": [">img"], "action": "Select wallpaper and generate colorscheme" },
{ "keys": [">light"], "action": "Use light theme for next color generations" },
{ "keys": [">dark"], "action": "Use dark theme for next color generations" },
{ "keys": [">todo"], "action": "Type something after that to add a To-do item" },
],
"appeartick": 3
}
]];

View File

@@ -1,114 +0,0 @@
// We're going to use ydotool
// See /usr/include/linux/input-event-codes.h for keycodes
export const defaultOskLayout = "qwerty_full"
export const oskLayouts = {
qwerty_full: {
name: "QWERTY - Full",
name_short: "US",
comment: "Like physical keyboard",
// A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
// keys: [
// [{ k: "Esc", t: "fn" }, { k: "F1", t: "fn" }, { k: "F2", t: "fn" }, { k: "F3", t: "fn" }, { k: "F4", t: "fn" }, { k: "F5", t: "fn" }, { k: "F6", t: "fn" }, { k: "F7", t: "fn" }, { k: "F8", t: "fn" }, { k: "F9", t: "fn" }, { k: "F10", t: "fn" }, { k: "F11", t: "fn" }, { k: "F12", t: "fn" }, { k: "PrtSc", t: "fn" }, { k: "Del", t: "fn" }],
// [{ k: "`", ks: "~", t: "normal" }, { k: "1", ks: "!", t: "normal" }, { k: "2", ks: "@", t: "normal" }, { k: "3", ks: "#", t: "normal" }, { k: "4", ks: "$", t: "normal" }, { k: "5", ks: "%", t: "normal" }, { k: "6", ks: "^", t: "normal" }, { k: "7", ks: "&", t: "normal" }, { k: "8", ks: "*", t: "normal" }, { k: "9", ks: "(", t: "normal" }, { k: "0", ks: ")", t: "normal" }, { k: "-", ks: "_", t: "normal" }, { k: "=", ks: "+", t: "normal" }, { k: "Backspace", t: "shift" }],
// [{ k: "Tab", t: "tab" }, { k: "q", ks: "Q", t: "normal" }, { k: "w", ks: "W", t: "normal" }, { k: "e", ks: "E", t: "normal" }, { k: "r", ks: "R", t: "normal" }, { k: "t", ks: "T", t: "normal" }, { k: "y", ks: "Y", t: "normal" }, { k: "u", ks: "U", t: "normal" }, { k: "i", ks: "I", t: "normal" }, { k: "o", ks: "O", t: "normal" }, { k: "p", ks: "P", t: "normal" }, { k: "[", ks: "{", t: "normal" }, { k: "]", ks: "}", t: "normal" }, { k: "\\", ks: "|", t: "expand" }],
// [{ k: "Caps", t: "caps" }, { k: "a", ks: "A", t: "normal" }, { k: "s", ks: "S", t: "normal" }, { k: "d", ks: "D", t: "normal" }, { k: "f", ks: "F", t: "normal" }, { k: "g", ks: "G", t: "normal" }, { k: "h", ks: "H", t: "normal" }, { k: "j", ks: "J", t: "normal" }, { k: "k", ks: "K", t: "normal" }, { k: "l", ks: "L", t: "normal" }, { k: ";", ks: ":", t: "normal" }, { k: "'", ks: '"', t: "normal" }, { k: "Enter", t: "expand" }],
// [{ k: "Shift", t: "shift" }, { k: "z", ks: "Z", t: "normal" }, { k: "x", ks: "X", t: "normal" }, { k: "c", ks: "C", t: "normal" }, { k: "v", ks: "V", t: "normal" }, { k: "b", ks: "B", t: "normal" }, { k: "n", ks: "N", t: "normal" }, { k: "m", ks: "M", t: "normal" }, { k: ",", ks: "<", t: "normal" }, { k: ".", ks: ">", t: "normal" }, { k: "/", ks: "?", t: "normal" }, { k: "Shift", t: "expand" }],
// [{ k: "Ctrl", t: "control" }, { k: "Fn", t: "normal" }, { k: "Win", t: "normal" }, { k: "Alt", t: "normal" }, { k: "Space", t: "space" }, { k: "Alt", t: "normal" }, { k: "Menu", t: "normal" }, { k: "Ctrl", t: "control" }]
// ]
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
// A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "`", labelShift: "~", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "@", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "#", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: "$", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: "^", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "&", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "q", labelShift: "Q", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "w", labelShift: "W", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "e", labelShift: "E", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "r", labelShift: "R", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "t", labelShift: "T", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "y", labelShift: "Y", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "u", labelShift: "U", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "i", labelShift: "I", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "o", labelShift: "O", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "p", labelShift: "P", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "[", labelShift: "{", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "]", labelShift: "}", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
],
[
{ keytype: "normal", label: "Caps", shape: "caps", keycode: 58 },
{ keytype: "normal", label: "a", labelShift: "A", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "s", labelShift: "S", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "d", labelShift: "D", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "f", labelShift: "F", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "g", labelShift: "G", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "h", labelShift: "H", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "j", labelShift: "J", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "k", labelShift: "K", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "l", labelShift: "L", shape: "normal", keycode: 38 },
{ keytype: "normal", label: ";", labelShift: ":", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "'", labelShift: '"', shape: "normal", keycode: 40 },
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "z", labelShift: "Z", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "x", labelShift: "X", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "c", labelShift: "C", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "v", labelShift: "V", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "b", labelShift: "B", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "n", labelShift: "N", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "m", labelShift: "M", shape: "normal", keycode: 50 },
{ keytype: "normal", label: ",", labelShift: "<", shape: "normal", keycode: 51 },
{ keytype: "normal", label: ".", labelShift: ">", shape: "normal", keycode: 52 },
{ keytype: "normal", label: "/", labelShift: "?", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", shape: "expand", keycode: 54 }
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
// { label: "Super", shape: "normal", keycode: 125 }, // dangerous
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
}
}

View File

@@ -1,14 +0,0 @@
export const quotes = [
{
quote: 'Nvidia, fuck you',
author: 'Linus Torvalds',
},
{
quote: 'reproducible system? cock and vagina?',
author: 'vaxry',
},
{
quote: "haha pointers hee hee i love pointe-\\\nProcess Vaxry exited with signal SIGSEGV",
author: 'vaxry',
}
];

View File

@@ -1,40 +0,0 @@
const resource = file => `resource:///com/github/Aylur/ags/${file}.js`;
const require = async file => (await import(resource(file))).default;
const service = async file => (await require(`service/${file}`));
export const App = await require('app');
export const Widget = await require('widget');
export const Service = await require('service');
export const Variable = await require('variable');
export const Utils = await import(resource('utils'));
export const Applications = await service('applications');
export const Audio = await service('audio');
export const Battery = await service('battery');
export const Bluetooth = await service('bluetooth');
export const Hyprland = await service('hyprland');
export const Mpris = await service('mpris');
export const Network = await service('network');
export const Notifications = await service('notifications');
export const SystemTray = await service('systemtray');
globalThis['App'] = App; //////////////////////////////
// globalThis['Widget'] = Widget;
// globalThis['Service'] = Service;
// globalThis['Variable'] = Variable;
globalThis['Utils'] = Utils; ///////////////////////////
// globalThis['Applications'] = Applications;
// globalThis['Audio'] = Audio;
// globalThis['Battery'] = Battery;
// globalThis['Bluetooth'] = Bluetooth;
// globalThis['Hyprland'] = Hyprland;
// globalThis['Mpris'] = Mpris;
// globalThis['Network'] = Network;
globalThis['Notifications'] = Notifications;
// globalThis['SystemTray'] = SystemTray;
const { exec } = Utils;
const SCREEN_WIDTH = Number(exec(`bash -c "xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f1 | head -1"`));
const SCREEN_HEIGHT = Number(exec(`bash -c "xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f2 | head -1"`));
globalThis['SCREEN_WIDTH'] = SCREEN_WIDTH;
globalThis['SCREEN_HEIGHT'] = SCREEN_HEIGHT;

View File

@@ -0,0 +1,99 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PopupWindow from "../misc/PopupWindow.js";
import icons from "../icons.js";
const pkg = JSON.parse(Utils.readFile(App.configDir + "/package.json"));
const show = JSON.parse(
Utils.readFile(Utils.CACHE_DIR + "/show_about") || "true",
);
const dontShow = () =>
Utils.writeFile("false", Utils.CACHE_DIR + "/show_about");
const avatar = App.configDir + "/assets/aylur.jpg";
/**
* @param {Object} o
* @param {string} o.label
* @param {string} o.link
*/
const LinkButton = ({ label, link }) =>
Widget.Button({
on_clicked: () => Utils.execAsync(["xdg-open", link]),
child: Widget.Box({
children: [
Widget.Label({ label, hexpand: true, xalign: 0 }),
Widget.Icon(icons.ui.link),
],
}),
});
export default () =>
PopupWindow({
name: "about",
transition: "slide_down",
child: Widget.Box({
vertical: true,
class_name: "window-content",
children: [
Widget.Box({
class_name: "avatar",
hpack: "center",
css: `background-image: url('${avatar}');`,
}),
Widget.Box({
vertical: true,
class_name: "labels vertical",
children: [
Widget.Label({
class_name: "title",
label: pkg.description,
}),
Widget.Label({
class_name: "author",
label: pkg.author,
}),
Widget.Label({
class_name: "version",
hpack: "center",
label: pkg.version,
}),
],
}),
Widget.Box({
class_name: "buttons",
vertical: true,
vexpand: true,
vpack: "end",
children: [
LinkButton({
label: "Support me on Ko-fi",
link: pkg.kofi,
}),
LinkButton({
label: "Report an Issue",
link: pkg.bugs.url,
}),
],
}),
Widget.Button({
class_name: "dont-show",
on_clicked: () => {
dontShow();
App.toggleWindow("about");
},
child: Widget.Box({
children: [
Widget.Label("Don't show again"),
Widget.Box({ hexpand: true }),
Widget.Icon(icons.ui.close),
],
}),
}),
],
}),
});
export function showAbout(force = false) {
if (show || force) App.toggleWindow("about");
}

View File

@@ -0,0 +1,47 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import options from "../options.js";
import { lookUpIcon } from "resource:///com/github/Aylur/ags/utils.js";
/** @param {import('resource:///com/github/Aylur/ags/service/applications.js').Application} app */
export default (app) => {
const title = Widget.Label({
class_name: "title",
label: app.name,
xalign: 0,
vpack: "center",
truncate: "end",
});
const description = Widget.Label({
class_name: "description",
label: app.description || "",
wrap: true,
xalign: 0,
justification: "left",
vpack: "center",
});
const icon = Widget.Icon({
icon: lookUpIcon(app.icon_name || "") ? app.icon_name || "" : "",
size: options.applauncher.icon_size.bind("value"),
});
const textBox = Widget.Box({
vertical: true,
vpack: "center",
children: app.description ? [title, description] : [title],
});
return Widget.Button({
class_name: "app-item",
attribute: app,
child: Widget.Box({
children: [icon, textBox],
}),
on_clicked: () => {
App.closeWindow("applauncher");
app.launch();
},
});
};

View File

@@ -0,0 +1,89 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
import PopupWindow from "../misc/PopupWindow.js";
import AppItem from "./AppItem.js";
import icons from "../icons.js";
import { launchApp } from "../utils.js";
const WINDOW_NAME = "applauncher";
const Applauncher = () => {
const mkItems = () => [
Widget.Separator({ hexpand: true }),
...Applications.query("").flatMap((app) =>
Widget.Revealer({
setup: (w) => (w.attribute = { app, revealer: w }),
child: Widget.Box({
vertical: true,
children: [
Widget.Separator({ hexpand: true }),
AppItem(app),
Widget.Separator({ hexpand: true }),
],
}),
}),
),
Widget.Separator({ hexpand: true }),
];
let items = mkItems();
const list = Widget.Box({
vertical: true,
children: items,
});
const entry = Widget.Entry({
hexpand: true,
primary_icon_name: icons.apps.search,
// set some text so on-change works the first time
text: "-",
on_accept: ({ text }) => {
const list = Applications.query(text || "");
if (list[0]) {
App.toggleWindow(WINDOW_NAME);
launchApp(list[0]);
}
},
on_change: ({ text }) =>
items.map((item) => {
if (item.attribute) {
const { app, revealer } = item.attribute;
revealer.reveal_child = app.match(text);
}
}),
});
return Widget.Box({
vertical: true,
children: [
entry,
Widget.Scrollable({
hscroll: "never",
child: list,
}),
],
setup: (self) =>
self.hook(App, (_, win, visible) => {
if (win !== WINDOW_NAME) return;
entry.text = "-";
entry.text = "";
if (visible) {
entry.grab_focus();
} else {
items = mkItems();
list.children = items;
}
}),
});
};
export default () =>
PopupWindow({
name: WINDOW_NAME,
transition: "slide_down",
child: Applauncher(),
});

View File

@@ -0,0 +1,45 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
/**
* @typedef {Object} PanelButtonProps
* @property {any} content
* @property {string=} window
*/
/**
* @param {import('types/widgets/button').ButtonProps & PanelButtonProps} o
*/
export default ({
class_name,
content,
window = "",
connections = [],
...rest
}) => {
let open = false;
const connection = [
App,
(self, win, visible) => {
if (win !== window) return;
if (open && !visible) {
open = false;
self.toggleClassName("active", false);
}
if (visible) {
open = true;
self.toggleClassName("active");
}
},
];
return Widget.Button({
class_name: `panel-button ${class_name}`,
child: Widget.Box({ children: [content] }),
connections: connections.concat([connection]),
...rest,
});
};

View File

@@ -0,0 +1,117 @@
import SystemTray from "resource:///com/github/Aylur/ags/service/systemtray.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import OverviewButton from "./buttons/OverviewButton.js";
import Workspaces from "./buttons/Workspaces.js";
import FocusedClient from "./buttons/FocusedClient.js";
import MediaIndicator from "./buttons/MediaIndicator.js";
import DateButton from "./buttons/DateButton.js";
import NotificationIndicator from "./buttons/NotificationIndicator.js";
import SysTray from "./buttons/SysTray.js";
import ColorPicker from "./buttons/ColorPicker.js";
import SystemIndicators from "./buttons/SystemIndicators.js";
import PowerMenu from "./buttons/PowerMenu.js";
import ScreenRecord from "./buttons/ScreenRecord.js";
import BatteryBar from "./buttons/BatteryBar.js";
import SubMenu from "./buttons/SubMenu.js";
import Recorder from "../services/screenrecord.js";
// import * as System from './buttons/System.js';
// import Taskbar from './buttons/Taskbar.js';
import options from "../options.js";
const submenuItems = Variable(1);
SystemTray.connect("changed", () => {
submenuItems.setValue(SystemTray.items.length + 1);
});
/**
* @template T
* @param {T=} service
* @param {(self: T) => boolean=} condition
*/
const SeparatorDot = (service, condition) => {
const visibility = (self) => {
if (!options.bar.separators.value) return (self.visible = false);
self.visible =
condition && service ? condition(service) : options.bar.separators.value;
};
const conn = service ? [[service, visibility]] : [];
return Widget.Separator({
connections: [["draw", visibility], ...conn],
binds: [["visible", options.bar.separators]],
vpack: "center",
});
};
const Start = () =>
Widget.Box({
class_name: "start",
children: [
OverviewButton(),
SeparatorDot(),
Workspaces(),
SeparatorDot(),
FocusedClient(),
Widget.Box({ hexpand: true }),
NotificationIndicator(),
SeparatorDot(Notifications, (n) => n.notifications.length > 0 || n.dnd),
],
});
const Center = () =>
Widget.Box({
class_name: "center",
children: [DateButton()],
});
const End = () =>
Widget.Box({
class_name: "end",
children: [
SeparatorDot(Mpris, (m) => m.players.length > 0),
MediaIndicator(),
Widget.Box({ hexpand: true }),
SubMenu({
items: submenuItems,
children: [SysTray(), ColorPicker()],
}),
SeparatorDot(),
ScreenRecord(),
SeparatorDot(Recorder, (r) => r.recording),
SystemIndicators(),
SeparatorDot(Battery, (b) => b.available),
BatteryBar(),
SeparatorDot(),
PowerMenu(),
],
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
name: `bar${monitor}`,
class_name: "transparent",
exclusivity: "exclusive",
monitor,
binds: [
[
"anchor",
options.bar.position,
"value",
(pos) => [pos, "left", "right"],
],
],
child: Widget.CenterBox({
class_name: "panel",
start_widget: Start(),
center_widget: Center(),
end_widget: End(),
}),
});

View File

@@ -0,0 +1,108 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import icons from "../../icons.js";
import FontIcon from "../../misc/FontIcon.js";
import options from "../../options.js";
import PanelButton from "../PanelButton.js";
const Indicator = () =>
Widget.Stack({
items: [
["false", Widget.Icon({ binds: [["icon", Battery, "icon-name"]] })],
["true", FontIcon(icons.battery.charging)],
],
binds: [["visible", options.battery.bar.show_icon]],
connections: [
[
Battery,
(stack) => {
stack.shown = `${Battery.charging || Battery.charged}`;
},
],
],
});
const PercentLabel = () =>
Widget.Revealer({
transition: "slide_right",
binds: [["reveal-child", options.battery.show_percentage]],
child: Widget.Label({
binds: [["label", Battery, "percent", (p) => `${p}%`]],
}),
});
const LevelBar = () =>
Widget.LevelBar({
connections: [
[
options.battery.bar.full,
(self) => {
const full = options.battery.bar.full.value;
self.vpack = full ? "fill" : "center";
self.hpack = full ? "fill" : "center";
},
],
],
binds: [["value", Battery, "percent", (p) => p / 100]],
});
const WholeButton = () =>
Widget.Overlay({
class_name: "whole-button",
child: LevelBar(),
pass_through: true,
overlays: [
Widget.Box({
hpack: "center",
children: [
FontIcon({
icon: icons.battery.charging,
binds: [["visible", Battery, "charging"]],
}),
Widget.Box({
hpack: "center",
vpack: "center",
child: PercentLabel(),
}),
],
}),
],
});
export default () =>
PanelButton({
class_name: "battery-bar",
on_clicked: () => {
const v = options.battery.show_percentage.value;
options.battery.show_percentage.value = !v;
},
content: Widget.Box({
connections: [
[
Battery,
(w) => {
w.toggleClassName("charging", Battery.charging || Battery.charged);
w.toggleClassName(
"medium",
Battery.percent < options.battery.medium.value,
);
w.toggleClassName(
"low",
Battery.percent < options.battery.low.value,
);
w.toggleClassName("half", Battery.percent < 48);
},
],
],
binds: [
["visible", Battery, "available"],
[
"children",
options.battery.bar.full,
"value",
(full) =>
full ? [WholeButton()] : [Indicator(), PercentLabel(), LevelBar()],
],
],
}),
});

View File

@@ -0,0 +1,27 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Colors from "../../services/colorpicker.js";
import PanelButton from "../PanelButton.js";
import Gdk from "gi://Gdk";
export default () =>
PanelButton({
class_name: "color-picker",
content: Widget.Icon("color-select-symbolic"),
binds: [["tooltip-text", Colors, "colors", (v) => `${v.length} colors`]],
on_clicked: () => Colors.pick(),
on_secondary_click: (btn) => {
if (Colors.colors.length === 0) return;
Widget.Menu({
class_name: "colorpicker",
children: Colors.colors.map((color) =>
Widget.MenuItem({
child: Widget.Label(color),
css: `background-color: ${color}`,
on_activate: () => Colors.wlCopy(color),
}),
),
}).popup_at_widget(btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
},
});

View File

@@ -0,0 +1,11 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Clock from "../../misc/Clock.js";
import PanelButton from "../PanelButton.js";
export default ({ format = "%H:%M - %A %e." } = {}) =>
PanelButton({
class_name: "dashboard panel-button",
on_clicked: () => App.toggleWindow("dashboard"),
window: "dashboard",
content: Clock({ format }),
});

View File

@@ -0,0 +1,55 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import PanelButton from "../PanelButton.js";
import options from "../../options.js";
import { substitute } from "../../utils.js";
export const ClientLabel = () =>
Widget.Label({
binds: [
[
"label",
Hyprland.active.client,
"class",
(c) => {
const { titles } = options.substitutions;
return substitute(titles, c);
},
],
],
});
export const ClientIcon = () =>
Widget.Icon({
connections: [
[
Hyprland.active.client,
(self) => {
const { icons } = options.substitutions;
const { client } = Hyprland.active;
const classIcon = substitute(icons, client.class) + "-symbolic";
const titleIcon = substitute(icons, client.class) + "-symbolic";
const hasTitleIcon = Utils.lookUpIcon(titleIcon);
const hasClassIcon = Utils.lookUpIcon(classIcon);
if (hasClassIcon) self.icon = classIcon;
if (hasTitleIcon) self.icon = titleIcon;
self.visible = !!(hasTitleIcon || hasClassIcon);
},
],
],
});
export default () =>
PanelButton({
class_name: "focused-client",
content: Widget.Box({
children: [ClientIcon(), ClientLabel()],
binds: [["tooltip-text", Hyprland.active, "client", (c) => c.title]],
}),
});

View File

@@ -0,0 +1,84 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import HoverRevealer from "../../misc/HoverRevealer.js";
import * as mpris from "../../misc/mpris.js";
import options from "../../options.js";
export const getPlayer = (name = options.mpris.preferred.value) =>
Mpris.getPlayer(name) || Mpris.players[0] || null;
/**
* @param {Object} o
* @param {import('types/service/mpris').MprisPlayer} o.player
* @param {import('../../misc/HoverRevealer').HoverRevealProps['direction']=} o.direction
*/
const Indicator = ({ player, direction = "right" }) =>
HoverRevealer({
class_name: `media panel-button ${player.name}`,
direction,
on_primary_click: () => player.playPause(),
on_scroll_up: () => player.next(),
on_scroll_down: () => player.previous(),
on_secondary_click: () => player.playPause(),
indicator: mpris.PlayerIcon(player),
child: Widget.Label({
vexpand: true,
truncate: "end",
max_width_chars: 40,
connections: [
[
player,
(label) => {
label.label = `${player.track_artists.join(", ")} - ${
player.track_title
}`;
},
],
],
}),
connections: [
[
player,
(revealer) => {
if (revealer._current === player.track_title) return;
revealer._current = player.track_title;
revealer.reveal_child = true;
Utils.timeout(3000, () => {
revealer.reveal_child = false;
});
},
],
],
});
/**
* @param {Object} o
* @param {import('../../misc/HoverRevealer').HoverRevealProps['direction']=} o.direction
*/
export default ({ direction = "right" } = {}) => {
let current = null;
const update = (box) => {
const player = getPlayer();
box.visible = !!player;
if (!player) {
current = null;
return;
}
if (current === player) return;
current = player;
box.children = [Indicator({ player, direction })];
};
return Widget.Box({
connections: [
[options.mpris.preferred, update],
[Mpris, update, "notify::players"],
],
});
};

View File

@@ -0,0 +1,63 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../../icons.js";
import HoverRevealer from "../../misc/HoverRevealer.js";
/**
* @param {Object} o
* @param {import('../../misc/HoverRevealer').HoverRevealProps['direction']=} o.direction
*/
export default ({ direction = "left" } = {}) =>
HoverRevealer({
class_name: "notifications panel-button",
eventboxConnections: [
["button-press-event", () => App.openWindow("dashboard")],
[
Notifications,
(box) =>
(box.visible =
Notifications.notifications.length > 0 || Notifications.dnd),
],
],
connections: [
[
Notifications,
(revealer) => {
const title = Notifications.notifications[0]?.summary;
if (revealer._title === title) return;
revealer._title = title;
revealer.reveal_child = true;
Utils.timeout(3000, () => {
revealer.reveal_child = false;
});
},
],
],
direction,
indicator: Widget.Icon({
binds: [
[
"icon",
Notifications,
"dnd",
(dnd) =>
dnd ? icons.notifications.silent : icons.notifications.noisy,
],
],
}),
child: Widget.Label({
truncate: "end",
max_width_chars: 40,
binds: [
[
"label",
Notifications,
"notifications",
(n) => n.reverse()[0]?.summary || "",
],
],
}),
});

View File

@@ -0,0 +1,24 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import PanelButton from "../PanelButton.js";
import FontIcon from "../../misc/FontIcon.js";
import { distroIcon } from "../../variables.js";
import options from "../../options.js";
export default () =>
PanelButton({
class_name: "overview",
window: "overview",
on_clicked: () => App.toggleWindow("overview"),
content: FontIcon({
binds: [
[
"icon",
options.bar.icon,
"value",
(v) => {
return v === "distro-icon" ? distroIcon : v;
},
],
],
}),
});

View File

@@ -0,0 +1,11 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import icons from "../../icons.js";
import PanelButton from "../PanelButton.js";
export default () =>
PanelButton({
class_name: "powermenu",
content: Widget.Icon(icons.powermenu.shutdown),
on_clicked: () => App.openWindow("powermenu"),
});

View File

@@ -0,0 +1,30 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PanelButton from "../PanelButton.js";
import Recorder from "../../services/screenrecord.js";
import icons from "../../icons.js";
export default () =>
PanelButton({
class_name: "recorder",
on_clicked: () => Recorder.stop(),
binds: [["visible", Recorder, "recording"]],
content: Widget.Box({
children: [
Widget.Icon(icons.recorder.recording),
Widget.Label({
binds: [
[
"label",
Recorder,
"timer",
(time) => {
const sec = time % 60;
const min = Math.floor(time / 60);
return `${min}:${sec < 10 ? "0" + sec : sec}`;
},
],
],
}),
],
}),
});

View File

@@ -0,0 +1,72 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import icons from "../../icons.js";
import options from "../../options.js";
/**
* @param {import('types/widgets/revealer').default} revealer
* @param {'left' | 'right' | 'up' | 'down'} direction
* @param {import('types/variable').Variable} items
*/
const Arrow = (revealer, direction, items) => {
let deg = 0;
const icon = Widget.Icon({
icon: icons.ui.arrow[direction],
});
const animate = () => {
const t = options.transition.value / 20;
const step = revealer.reveal_child ? 10 : -10;
for (let i = 0; i < 18; ++i) {
Utils.timeout(t * i, () => {
deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
});
}
};
return Widget.Button({
class_name: "panel-button sub-menu",
connections: [
[
items,
(btn) => {
btn.tooltip_text = `${items.value} Items`;
},
],
],
on_clicked: () => {
animate();
revealer.reveal_child = !revealer.reveal_child;
},
child: icon,
});
};
/**
* @param {Object} o
* @param {import('types/widgets/box').default['children']} o.children
* @param {'left' | 'right' | 'up' | 'down'=} o.direction
* @param {import('types/variable').Variable} o.items
*/
export default ({ children, direction = "left", items = Variable(0) }) => {
const posStart = direction === "up" || direction === "left";
const posEnd = direction === "down" || direction === "right";
const revealer = Widget.Revealer({
transition: `slide_${direction}`,
child: Widget.Box({
children,
}),
});
return Widget.Box({
vertical: direction === "up" || direction === "down",
children: [
posStart && revealer,
Arrow(revealer, direction, items),
posEnd && revealer,
],
});
};

View File

@@ -0,0 +1,44 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import SystemTray from "resource:///com/github/Aylur/ags/service/systemtray.js";
import PanelButton from "../PanelButton.js";
import Gdk from "gi://Gdk";
/** @param {import('types/service/systemtray').TrayItem} item */
const SysTrayItem = (item) =>
PanelButton({
class_name: "tray-item",
content: Widget.Icon({ icon: item.bind("icon") }),
tooltip_markup: item.bind("tooltip_markup"),
setup: (self) => {
const id = item.menu?.connect("popped-up", (menu) => {
self.toggleClassName("active");
menu.connect("notify::visible", (menu) => {
self.toggleClassName("active", menu.visible);
});
menu.disconnect(id);
});
if (id) self.connect("destroy", () => item.menu?.disconnect(id));
},
// @ts-expect-error popup_at_widget missing from types?
on_primary_click: (btn) =>
item.menu?.popup_at_widget(
btn,
Gdk.Gravity.SOUTH,
Gdk.Gravity.NORTH,
null,
),
// @ts-expect-error popup_at_widget missing from types?
on_secondary_click: (btn) =>
item.menu?.popup_at_widget(
btn,
Gdk.Gravity.SOUTH,
Gdk.Gravity.NORTH,
null,
),
});
export default () =>
Widget.Box().bind("children", SystemTray, "items", (i) => i.map(SysTrayItem));

View File

@@ -0,0 +1,57 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PanelButton from "../PanelButton.js";
import * as variables from "../../variables.js";
import icons from "../../icons.js";
/** @param {'cpu' | 'ram'} type */
const System = (type) => {
const icon = Widget.Icon({
class_name: "icon",
icon: icons.system[type],
});
const progress = Widget.Box({
class_name: "progress",
child: Widget.CircularProgress({
binds: [["value", variables[type]]],
}),
});
const revealer = Widget.Revealer({
transition: "slide_right",
child: Widget.Label({
binds: [
[
"label",
variables[type],
"value",
(v) => {
return ` ${type}: ${Math.round(v * 100)}%`;
},
],
],
}),
});
return PanelButton({
class_name: `system ${type}`,
on_clicked: () => (revealer.reveal_child = !revealer.reveal_child),
content: Widget.EventBox({
on_hover: () => (revealer.reveal_child = true),
on_hover_lost: () => (revealer.reveal_child = false),
child: Widget.Box({
children: [
icon,
Widget.Box({
class_name: "revealer",
child: revealer,
}),
progress,
],
}),
}),
});
};
export const CPU = () => System("cpu");
export const RAM = () => System("ram");

View File

@@ -0,0 +1,135 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import Bluetooth from "resource:///com/github/Aylur/ags/service/bluetooth.js";
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import Network from "resource:///com/github/Aylur/ags/service/network.js";
import HoverRevealer from "../../misc/HoverRevealer.js";
import PanelButton from "../PanelButton.js";
import Asusctl from "../../services/asusctl.js";
import Indicator from "../../services/onScreenIndicator.js";
import icons from "../../icons.js";
import FontIcon from "../../misc/FontIcon.js";
const ProfileIndicator = () =>
Widget.Icon()
.bind("visible", Asusctl, "profile", (p) => p !== "Balanced")
.bind("icon", Asusctl, "profile", (i) => icons.asusctl.profile[i]);
const ModeIndicator = () =>
FontIcon()
.bind("visible", Asusctl, "mode", (m) => m !== "Hybrid")
.bind("icon", Asusctl, "mode", (i) => icons.asusctl.mode[i]);
const MicrophoneIndicator = () =>
Widget.Icon().hook(
Audio,
(icon) => {
if (!Audio.microphone) return;
const { muted, low, medium, high } = icons.audio.mic;
if (Audio.microphone.is_muted) return (icon.icon = muted);
/** @type {Array<[number, string]>} */
const cons = [
[67, high],
[34, medium],
[1, low],
[0, muted],
];
icon.icon =
cons.find(([n]) => n <= Audio.microphone.volume * 100)?.[1] || "";
icon.visible = Audio.recorders.length > 0 || Audio.microphone.is_muted;
},
"speaker-changed",
);
const DNDIndicator = () =>
Widget.Icon({
visible: Notifications.bind("dnd"),
icon: icons.notifications.silent,
});
const BluetoothDevicesIndicator = () =>
Widget.Box().hook(
Bluetooth,
(box) => {
box.children = Bluetooth.connectedDevices.map(({ iconName, name }) =>
HoverRevealer({
indicator: Widget.Icon(iconName + "-symbolic"),
child: Widget.Label(name),
}),
);
box.visible = Bluetooth.connectedDevices.length > 0;
},
"notify::connected-devices",
);
const BluetoothIndicator = () =>
Widget.Icon({
class_name: "bluetooth",
icon: icons.bluetooth.enabled,
visible: Bluetooth.bind("enabled"),
});
const NetworkIndicator = () =>
Widget.Icon().hook(Network, (self) => {
const icon = Network[Network.primary || "wifi"]?.iconName;
self.icon = icon || "";
self.visible = !!icon;
});
const AudioIndicator = () =>
Widget.Icon().hook(
Audio,
(self) => {
if (!Audio.speaker) return;
const { muted, low, medium, high, overamplified } = icons.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] || "";
},
"speaker-changed",
);
export default () =>
PanelButton({
class_name: "quicksettings panel-button",
on_clicked: () => App.toggleWindow("quicksettings"),
setup: (self) =>
self.hook(App, (_, win, visible) => {
self.toggleClassName("active", win === "quicksettings" && visible);
}),
on_scroll_up: () => {
Audio.speaker.volume += 0.02;
Indicator.speaker();
},
on_scroll_down: () => {
Audio.speaker.volume -= 0.02;
Indicator.speaker();
},
content: Widget.Box({
children: [
Asusctl?.available && ProfileIndicator(),
Asusctl?.available && ModeIndicator(),
DNDIndicator(),
BluetoothDevicesIndicator(),
BluetoothIndicator(),
NetworkIndicator(),
AudioIndicator(),
MicrophoneIndicator(),
],
}),
});

View File

@@ -0,0 +1,34 @@
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PanelButton from "../PanelButton.js";
import { launchApp } from "../../utils.js";
import icons from "../../icons.js";
const focus = ({ address }) =>
Hyprland.sendMessage(`dispatch focuswindow address:${address}`);
/** @param {import('types/widgets/box').default} box */
const setChildren = (box) =>
(box.children = Hyprland.clients.map((client) => {
if (Hyprland.active.workspace.id !== client.workspace.id) return;
for (const app of Applications.list) {
if (client.class && app.match(client.class)) {
return PanelButton({
content: Widget.Icon(app.icon_name || icons.fallback.executable),
tooltip_text: app.name,
on_primary_click: () => focus(client),
on_middle_click: () => launchApp(app),
});
}
}
}));
export default () =>
Widget.Box({
connections: [
[Hyprland, setChildren, "notify::clients"],
[Hyprland, setChildren, "notify::active"],
],
});

View File

@@ -0,0 +1,64 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import options from "../../options.js";
import { range } from "../../utils.js";
/** @param {any} arg */
const dispatch = (arg) => Utils.execAsync(`hyprctl dispatch workspace ${arg}`);
const Workspaces = () => {
const ws = options.workspaces.value;
return Widget.Box({
children: range(ws || 20).map((i) =>
Widget.Button({
setup: (btn) => (btn.id = i),
on_clicked: () => dispatch(i),
child: Widget.Label({
label: `${i}`,
class_name: "indicator",
vpack: "center",
}),
connections: [
[
Hyprland,
(btn) => {
btn.toggleClassName("active", Hyprland.active.workspace.id === i);
btn.toggleClassName(
"occupied",
Hyprland.getWorkspace(i)?.windows > 0,
);
},
],
],
}),
),
connections: ws
? []
: [
[
Hyprland.active.workspace,
(box) =>
box.children.map((btn) => {
btn.visible = Hyprland.workspaces.some(
(ws) => ws.id === btn.id,
);
}),
],
],
});
};
export default () =>
Widget.EventBox({
class_name: "workspaces panel-button",
child: Widget.Box({
// its nested like this to keep it consistent with other PanelButton widgets
child: Widget.EventBox({
on_scroll_up: () => dispatch("m+1"),
on_scroll_down: () => dispatch("m-1"),
class_name: "eventbox",
binds: [["child", options.workspaces, "value", Workspaces]],
}),
}),
});

View File

@@ -0,0 +1,30 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import DateColumn from "./DateColumn.js";
import NotificationColumn from "./NotificationColumn.js";
import PopupWindow from "../misc/PopupWindow.js";
import options from "../options.js";
export default () =>
PopupWindow({
name: "dashboard",
connections: [
[
options.bar.position,
(self) => {
self.anchor = [options.bar.position.value];
if (options.bar.position.value === "top")
self.transition = "slide_down";
if (options.bar.position.value === "bottom")
self.transition = "slide_up";
},
],
],
child: Widget.Box({
children: [
NotificationColumn(),
Widget.Separator({ orientation: 1 }),
DateColumn(),
],
}),
});

View File

@@ -0,0 +1,70 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../icons.js";
import Clock from "../misc/Clock.js";
import * as vars from "../variables.js";
import options from "../options.js";
/**
* @param {'cpu' | 'ram' | 'temp'} type
* @param {string} title
* @param {string} unit
*/
const SysProgress = (type, title, unit) =>
Widget.Box({
class_name: `circular-progress-box ${type}`,
hexpand: true,
binds: [
[
"tooltipText",
vars[type],
"value",
(v) => `${title}: ${Math.floor(v * 100)}${unit}`,
],
],
child: Widget.CircularProgress({
hexpand: true,
class_name: `circular-progress ${type}`,
child: Widget.Icon(icons.system[type]),
start_at: 0.75,
binds: [
["value", vars[type]],
["rounded", options.radii, "value", (v) => v > 0],
],
}),
});
export default () =>
Widget.Box({
vertical: true,
class_name: "datemenu vertical",
children: [
Widget.Box({
class_name: "clock-box",
vertical: true,
children: [
Clock({ format: "%H:%M" }),
Widget.Label({
class_name: "uptime",
binds: [["label", vars.uptime, "value", (t) => `uptime: ${t}`]],
}),
],
}),
Widget.Box({
class_name: "calendar",
children: [
Widget.Calendar({
hexpand: true,
hpack: "center",
}),
],
}),
Widget.Box({
class_name: "system-info horizontal",
children: [
SysProgress("cpu", "Cpu", "%"),
SysProgress("ram", "Ram", "%"),
SysProgress("temp", "Temperature", "°"),
],
}),
],
});

View File

@@ -0,0 +1,90 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import icons from "../icons.js";
import Notification from "../misc/Notification.js";
import { timeout } from "resource:///com/github/Aylur/ags/utils.js";
const ClearButton = () =>
Widget.Button({
on_clicked: () => {
const list = Array.from(Notifications.notifications);
for (let i = 0; i < list.length; i++)
timeout(50 * i, () => list[i]?.close());
},
binds: [["sensitive", Notifications, "notifications", (n) => n.length > 0]],
child: Widget.Box({
children: [
Widget.Label("Clear "),
Widget.Icon({
binds: [
[
"icon",
Notifications,
"notifications",
(n) => (n.length > 0 ? icons.trash.full : icons.trash.empty),
],
],
}),
],
}),
});
const Header = () =>
Widget.Box({
class_name: "header",
children: [
Widget.Label({ label: "Notifications", hexpand: true, xalign: 0 }),
ClearButton(),
],
});
const NotificationList = () =>
Widget.Box({
vertical: true,
vexpand: true,
connections: [
[
Notifications,
(box) => {
box.children = Notifications.notifications
.reverse()
.map(Notification);
box.visible = Notifications.notifications.length > 0;
},
],
],
});
const Placeholder = () =>
Widget.Box({
class_name: "placeholder",
vertical: true,
vpack: "center",
hpack: "center",
vexpand: true,
hexpand: true,
children: [
Widget.Icon(icons.notifications.silent),
Widget.Label("Your inbox is empty"),
],
binds: [["visible", Notifications, "notifications", (n) => n.length === 0]],
});
export default () =>
Widget.Box({
class_name: "notifications",
vertical: true,
children: [
Header(),
Widget.Scrollable({
vexpand: true,
class_name: "notification-scrollable",
child: Widget.Box({
class_name: "notification-list",
vertical: true,
children: [NotificationList(), Placeholder()],
}),
}),
],
});

View File

@@ -0,0 +1,74 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Clock from "../misc/Clock.js";
import DesktopMenu from "./DesktopMenu.js";
import options from "../options.js";
const DesktopClock = () =>
Widget.Box({
class_name: "clock-box-shadow",
child: Widget.CenterBox({
class_name: "clock-box",
children: [
Clock({
class_name: "clock",
hpack: "center",
format: "%H",
}),
Widget.Box({
class_name: "separator-box",
vertical: true,
hexpand: true,
hpack: "center",
children: [
Widget.Separator({ vpack: "center", vexpand: true }),
Widget.Separator({ vpack: "center", vexpand: true }),
],
}),
Clock({
class_name: "clock",
hpack: "center",
format: "%M",
}),
],
}),
});
const Desktop = () =>
Widget.EventBox({
on_secondary_click: (_, event) => DesktopMenu().popup_at_pointer(event),
child: Widget.Box({
vertical: true,
vexpand: true,
hexpand: true,
binds: [["visible", options.desktop.clock.enable]],
connections: [
[
options.desktop.clock.position,
(box) => {
const [hpack = "center", vpack = "center", offset = 64] =
options.desktop.clock.position.value.split(" ") || [];
// @ts-expect-error
box.hpack = hpack;
box.vpack = vpack;
box.setCss(`margin: ${Number(offset)}px;`);
},
],
],
children: [
DesktopClock(),
Clock({ format: "%B %e. %A", class_name: "date" }),
],
}),
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
monitor,
name: `desktop${monitor}`,
layer: "background",
class_name: "desktop",
anchor: ["top", "bottom", "left", "right"],
child: Desktop(),
});

View File

@@ -0,0 +1,66 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import PowerMenu from "../services/powermenu.js";
import icons from "../icons.js";
import Gtk from "gi://Gtk";
import { openSettings } from "../settings/theme.js";
/**
* @param {string} label
* @param {string} icon
* @param {import('types/widgets/menu').MenuItemProps['on_activate']} on_activate
*/
const Item = (label, icon, on_activate) =>
Widget.MenuItem({
on_activate,
child: Widget.Box({
children: [
Widget.Icon(icon),
Widget.Label({
label,
hexpand: true,
xalign: 0,
}),
],
}),
});
export default () =>
Widget.Menu({
class_name: "desktop-menu",
children: [
Widget.MenuItem({
child: Widget.Box({
children: [
Widget.Icon(icons.powermenu.shutdown),
Widget.Label({
label: "System",
hexpand: true,
xalign: 0,
}),
],
}),
submenu: Widget.Menu({
children: [
Item("Shutdown", icons.powermenu.shutdown, () =>
PowerMenu.action("shutdown"),
),
Item("Log Out", icons.powermenu.logout, () =>
PowerMenu.action("logout"),
),
Item("Reboot", icons.powermenu.reboot, () =>
PowerMenu.action("reboot"),
),
Item("Sleep", icons.powermenu.sleep, () =>
PowerMenu.action("reboot"),
),
],
}),
}),
Item("Applications", icons.apps.apps, () =>
App.openWindow("applauncher"),
),
new Gtk.SeparatorMenuItem(),
Item("Settings", icons.ui.settings, openSettings),
],
});

View File

@@ -0,0 +1,155 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../icons.js";
import options from "../options.js";
import { launchApp, range } from "../utils.js";
const focus = ({ address }) =>
Hyprland.sendMessage(`dispatch focuswindow address:${address}`);
/** @param {import('types/widgets/button').ButtonProps & { icon: string, pinned?: boolean }} o */
const AppButton = ({ icon, pinned = false, ...rest }) => {
const indicators = Widget.Box({
vpack: "end",
hpack: "center",
children: range(5, 0).map(() =>
Widget.Box({
class_name: "indicator",
visible: false,
}),
),
});
const button = Widget.Button({
...rest,
child: Widget.Box({
class_name: "box",
child: Widget.Overlay({
child: Widget.Icon({
icon,
binds: [["size", options.desktop.dock.icon_size]],
}),
pass_through: true,
overlays: pinned ? [indicators] : [],
}),
}),
});
return Object.assign(button, { indicators });
};
const Taskbar = () =>
Widget.Box({
binds: [
[
"children",
Hyprland,
"clients",
(c) =>
c.map((client) => {
for (const appName of options.desktop.dock.pinned_apps.value) {
if (client.class.toLowerCase().includes(appName.toLowerCase()))
return null;
}
for (const app of Applications.list) {
if (
(client.title && app.match(client.title)) ||
(client.class && app.match(client.class))
) {
return AppButton({
icon: app.icon_name || "",
tooltip_text: app.name,
on_primary_click: () => focus(client),
on_middle_click: () => launchApp(app),
});
}
}
}),
],
],
});
const PinnedApps = () =>
Widget.Box({
class_name: "pins",
homogeneous: true,
binds: [
[
"children",
options.desktop.dock.pinned_apps,
"value",
(v) =>
v
.map((term) => ({ app: Applications.query(term)?.[0], term }))
.filter(({ app }) => app)
.map(({ app, term = true }) =>
AppButton({
pinned: true,
icon: app.icon_name || "",
on_primary_click: () => {
for (const client of Hyprland.clients) {
if (client.class.toLowerCase().includes(term))
return focus(client);
}
launchApp(app);
},
on_middle_click: () => launchApp(app),
tooltip_text: app.name,
connections: [
[
Hyprland,
(button) => {
const running = Hyprland.clients.filter((client) =>
client.class.toLowerCase().includes(term),
);
const focused = running.find(
(client) =>
client.address === Hyprland.active.client.address,
);
const index = running.findIndex((c) => c === focused);
for (let i = 0; i < 5; ++i) {
const indicator = button.indicators.children[i];
indicator.visible = i < running.length;
indicator.toggleClassName("focused", i === index);
}
button.set_tooltip_text(
running.length === 1 ? running[0].title : app.name,
);
},
],
],
}),
),
],
],
});
export default () => {
const pinnedapps = PinnedApps();
const taskbar = Taskbar();
const applauncher = AppButton({
class_name: "launcher nonrunning",
icon: icons.apps.apps,
tooltip_text: "Applications",
on_clicked: () => App.toggleWindow("applauncher"),
});
const separator = Widget.Separator({
vpack: "center",
hpack: "center",
orientation: 1,
connections: [
[Hyprland, (box) => (box.visible = taskbar.children.length > 0)],
],
});
return Widget.Box({
class_name: "dock",
children: [applauncher, pinnedapps, separator, taskbar],
});
};

View File

@@ -0,0 +1,54 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Dock from "./Dock.js";
import options from "../options.js";
/** @param {number} monitor */
export default (monitor) => {
const revealer = Widget.Revealer({
transition: "slide_up",
child: Dock(),
setup: (self) => {
const update = () => {
const ws = Hyprland.getWorkspace(Hyprland.active.workspace.id);
if (Hyprland.getMonitor(monitor)?.name === ws?.monitor)
self.reveal_child = ws?.windows === 0;
};
self
.hook(Hyprland, update, "client-added")
.hook(Hyprland, update, "client-removed")
.hook(Hyprland.active.workspace, update);
},
});
return Widget.Window({
monitor,
name: `dock${monitor}`,
class_name: "floating-dock",
anchor: ["bottom"],
child: Widget.Box({
children: [
revealer,
Widget.Box({
class_name: "padding",
css: "padding: 2px;",
}),
],
}),
connections: [
[
"enter-notify-event",
() => {
revealer.reveal_child = true;
},
],
[
"leave-notify-event",
() => {
revealer.reveal_child = false;
},
],
],
binds: [["visible", options.bar.position, "value", (v) => v !== "bottom"]],
});
};

View File

@@ -0,0 +1,121 @@
export default {
lock: "system-lock-screen-symbolic",
fallback: {
executable: "application-x-executable-symbolic",
},
audio: {
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",
},
mixer: "",
},
asusctl: {
profile: {
Balanced: "power-profile-balanced-symbolic",
Quiet: "power-profile-power-saver-symbolic",
Performance: "power-profile-performance-symbolic",
},
mode: {
Integrated: "",
Hybrid: "󰢮",
},
},
apps: {
apps: "view-app-grid-symbolic",
search: "folder-saved-search-symbolic",
},
battery: {
charging: "󱐋",
warning: "battery-empty-symbolic",
},
bluetooth: {
enabled: "bluetooth-active-symbolic",
disabled: "bluetooth-disabled-symbolic",
},
brightness: {
indicator: "display-brightness-symbolic",
keyboard: "keyboard-brightness-symbolic",
screen: "display-brightness-symbolic",
},
powermenu: {
sleep: "weather-clear-night-symbolic",
reboot: "system-reboot-symbolic",
logout: "system-log-out-symbolic",
shutdown: "system-shutdown-symbolic",
},
recorder: {
recording: "media-record-symbolic",
},
notifications: {
noisy: "preferences-system-notifications-symbolic",
silent: "notifications-disabled-symbolic",
},
trash: {
full: "user-trash-full-symbolic",
empty: "user-trash-symbolic",
},
mpris: {
fallback: "audio-x-generic-symbolic",
shuffle: {
enabled: "󰒟",
disabled: "󰒟",
},
loop: {
none: "󰓦",
track: "󰓦",
playlist: "󰑐",
},
playing: "󰏦",
paused: "󰐍",
stopped: "󰐍",
prev: "󰒮",
next: "󰒭",
},
ui: {
close: "window-close-symbolic",
info: "info-symbolic",
menu: "open-menu-symbolic",
link: "external-link-symbolic",
settings: "emblem-system-symbolic",
tick: "object-select-symbolic",
arrow: {
right: "pan-end-symbolic",
left: "pan-start-symbolic",
down: "pan-down-symbolic",
up: "pan-up-symbolic",
},
},
system: {
cpu: "org.gnome.SystemMonitor-symbolic",
ram: "drive-harddisk-solidstate-symbolic",
temp: "temperature-symbolic",
},
dialog: {
Search: "",
Applauncher: "󰵆",
Bar: "",
Border: "󰃇",
Color: "󰏘",
Desktop: "",
Font: "",
General: "󰒓",
Miscellaneous: "󰠱",
Theme: "󰃟",
Notifications: "󰂚 ",
},
};

View File

@@ -0,0 +1,59 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Avatar from "../misc/Avatar.js";
import Lockscreen from "../services/lockscreen.js";
import Layer from "gi://GtkLayerShell";
const PasswordEntry = () =>
Widget.Box({
children: [
Widget.Entry({
connections: [[Lockscreen, (entry) => (entry.text = ""), "lock"]],
visibility: false,
placeholder_text: "Password",
on_accept: ({ text }) => Lockscreen.auth(text || ""),
hpack: "center",
hexpand: true,
}),
Widget.Spinner({
active: true,
vpack: "center",
connections: [
[Lockscreen, (w, auth) => (w.visible = auth), "authenticating"],
],
}),
],
});
/** @param {number} monitor */
export default (monitor) => {
const win = Widget.Window({
name: `lockscreen${monitor}`,
class_name: "lockscreen",
monitor,
layer: "overlay",
visible: false,
connections: [[Lockscreen, (w, lock) => (w.visible = lock), "lock"]],
child: Widget.Box({
css: "min-width: 3000px; min-height: 2000px;",
class_name: "shader",
child: Widget.Box({
class_name: "content",
vertical: true,
hexpand: true,
vexpand: true,
hpack: "center",
vpack: "center",
children: [
Avatar({
hpack: "center",
vpack: "center",
}),
PasswordEntry(),
],
}),
}),
});
Layer.set_keyboard_mode(win, Layer.KeyboardMode.EXCLUSIVE);
return win;
};

View File

@@ -0,0 +1,7 @@
#! /usr/bin/env python
import pam
import sys
import getpass
print(pam.authenticate(getpass.getuser(), sys.argv[1]));

View File

@@ -0,0 +1,48 @@
import Applauncher from "./applauncher/Applauncher.js";
import Dashboard from "./dashboard/Dashboard.js";
import Desktop from "./desktop/Desktop.js";
import FloatingDock from "./dock/FloatingDock.js";
import Lockscreen from "./lockscreen/Lockscreen.js";
import Notifications from "./notifications/Notifications.js";
import OSD from "./osd/OSD.js";
import Overview from "./overview/Overview.js";
import PowerMenu from "./powermenu/PowerMenu.js";
import QuickSettings from "./quicksettings/QuickSettings.js";
import ScreenCorners from "./screencorner/ScreenCorners.js";
import TopBar from "./bar/TopBar.js";
import Verification from "./powermenu/Verification.js";
import About from "./about/about.js";
import { init } from "./settings/setup.js";
import { forMonitors } from "./utils.js";
import { initWallpaper } from "./settings/wallpaper.js";
import options from "./options.js";
initWallpaper();
const windows = () => [
forMonitors(Desktop),
forMonitors(FloatingDock),
forMonitors(Lockscreen),
forMonitors(Notifications),
forMonitors(OSD),
forMonitors(ScreenCorners),
forMonitors(TopBar),
Applauncher(),
Dashboard(),
Overview(),
PowerMenu(),
QuickSettings(),
Verification(),
About(),
];
export default {
onConfigParsed: init,
windows: windows().flat(1),
maxStreamVolume: 1.05,
cacheNotificationActions: false,
closeWindowDelay: {
quicksettings: options.transition.value,
dashboard: options.transition.value,
},
};

View File

@@ -0,0 +1,16 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import options from "../options.js";
/** @param {import('types/widgets/box').BoxProps=} props */
export default (props) =>
Widget.Box({ ...props, class_name: "avatar" })
.hook(options.desktop.avatar, (box) =>
box.setCss(`
background-image: url('${options.desktop.avatar.value}');
background-size: cover;
`),
)
.on("size-allocate", (box) => {
const h = box.get_allocated_height();
box.set_size_request(Math.ceil(h * 1.1), -1);
});

View File

@@ -0,0 +1,18 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
export default () =>
Widget.Icon({
class_name: "battery",
binds: [["icon", Battery, "icon-name"]],
connections: [
[
Battery,
(icon) => {
icon.toggleClassName("charging", Battery.charging);
icon.toggleClassName("charged", Battery.charged);
icon.toggleClassName("low", Battery.percent < 30);
},
],
],
});

View File

@@ -0,0 +1,26 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import GLib from "gi://GLib";
/**
* @param {import('types/widgets/label').Props & {
* format?: string,
* interval?: number,
* }} o
*/
export default ({
format = "%H:%M:%S %B %e. %A",
interval = 1000,
...rest
} = {}) =>
Widget.Label({
class_name: "clock",
...rest,
connections: [
[
interval,
(label) =>
(label.label =
GLib.DateTime.new_now_local().format(format) || "wrong format"),
],
],
});

View File

@@ -0,0 +1,49 @@
import Gtk from "gi://Gtk";
import { createCtor } from "resource:///com/github/Aylur/ags/widget.js";
import AgsLabel from "resource:///com/github/Aylur/ags/widgets/label.js";
import GObject from "gi://GObject";
export default createCtor(
class FontIcon extends AgsLabel {
static {
GObject.registerClass(this);
}
/** @param {string | import('types/widgets/label').Props & { icon?: string }} params */
constructor(params = "") {
// @ts-expect-error
const { icon = "", ...rest } = params;
super(typeof params === "string" ? {} : rest);
this.toggleClassName("font-icon");
if (typeof params === "object") this.icon = icon;
if (typeof params === "string") this.icon = params;
}
get icon() {
return this.label;
}
set icon(icon) {
this.label = icon;
}
get size() {
return this.get_style_context().get_property(
"font-size",
Gtk.StateFlags.NORMAL,
);
}
/** @returns {[number, number]} */
vfunc_get_preferred_height() {
return [this.size, this.size];
}
/** @returns {[number, number]} */
vfunc_get_preferred_width() {
return [this.size, this.size];
}
},
);

View File

@@ -0,0 +1,64 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
/**
* @typedef {import('types/widgets/eventbox').EventBoxProps & {
* indicator?: import('types/widgets/box').BoxProps['child']
* direction?: 'left' | 'right' | 'down' | 'up'
* duration?: number
* eventboxConnections?: import('types/widgets/box').BoxProps['connections']
* connections?: import('types/widgets/revealer').RevealerProps['connections']
* }} HoverRevealProps
*/
/**
* @param {HoverRevealProps} props
*/
export default ({
indicator,
child,
direction = "left",
duration = 300,
connections = [],
eventboxConnections = [],
binds = [],
...rest
}) => {
let open = false;
const vertical = direction === "down" || direction === "up";
const posStart = direction === "down" || direction === "right";
const posEnd = direction === "up" || direction === "left";
const revealer = Widget.Revealer({
transition: `slide_${direction}`,
connections,
binds,
transition_duration: duration,
child,
});
const eventbox = Widget.EventBox({
...rest,
connections: eventboxConnections,
on_hover: () => {
if (open) return;
revealer.reveal_child = true;
Utils.timeout(duration, () => (open = true));
},
on_hover_lost: () => {
if (!open) return;
revealer.reveal_child = false;
open = false;
},
child: Widget.Box({
vertical,
children: [posStart && indicator, revealer, posEnd && indicator],
}),
});
return Widget.Box({
children: [eventbox],
});
};

View File

@@ -0,0 +1,63 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import RegularWindow from "./RegularWindow.js";
import Gtk from "gi://Gtk";
export default () => {
const selected = Widget.Label({
css: "font-size: 1.2em;",
});
const flowbox = Widget.FlowBox({
min_children_per_line: 10,
setup: (self) => {
self.connect("child-activated", (_, child) => {
selected.label = child.get_child().iconName;
});
Gtk.IconTheme.get_default()
.list_icons(null)
.sort()
.map((icon) => {
!icon.endsWith(".symbolic") &&
self.insert(
Widget.Icon({
icon,
size: 38,
}),
-1,
);
});
self.show_all();
},
});
const entry = Widget.Entry({
on_change: ({ text }) =>
flowbox.get_children().forEach((child) => {
child.visible = child.get_child().iconName.includes(text);
}),
});
return RegularWindow({
name: "icons",
visible: true,
child: Widget.Box({
css: "padding: 30px;",
spacing: 20,
vertical: true,
children: [
entry,
Widget.Scrollable({
hscroll: "never",
vscroll: "always",
hexpand: true,
vexpand: true,
css: "min-width: 500px;" + "min-height: 500px;",
child: flowbox,
}),
selected,
],
}),
});
};

View File

@@ -0,0 +1,132 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import GLib from "gi://GLib";
/** @param {import('types/service/notifications').Notification} n */
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon img",
css: `
background-image: url("${image}");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
min-width: 78px;
min-height: 78px;
`,
});
}
let icon = "dialog-information-symbolic";
if (Utils.lookUpIcon(app_icon)) icon = app_icon;
if (Utils.lookUpIcon(app_entry || "")) icon = app_entry || "";
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon",
css: `
min-width: 78px;
min-height: 78px;
`,
child: Widget.Icon({
icon,
size: 58,
hpack: "center",
hexpand: true,
vpack: "center",
vexpand: true,
}),
});
};
/** @param {import('types/service/notifications').Notification} notification */
export default (notification) => {
const content = Widget.Box({
class_name: "content",
children: [
NotificationIcon(notification),
Widget.Box({
hexpand: true,
vertical: true,
children: [
Widget.Box({
children: [
Widget.Label({
class_name: "title",
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: notification.summary,
use_markup: true,
}),
Widget.Label({
class_name: "time",
vpack: "start",
label: GLib.DateTime.new_from_unix_local(
notification.time,
).format("%H:%M"),
}),
Widget.Button({
class_name: "close-button",
vpack: "start",
child: Widget.Icon("window-close-symbolic"),
on_clicked: () => notification.close(),
}),
],
}),
Widget.Label({
class_name: "description",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
label: notification.body,
wrap: true,
}),
],
}),
],
});
const actionsbox = Widget.Revealer({
transition: "slide_down",
child: Widget.EventBox({
child: Widget.Box({
class_name: "actions horizontal",
children: notification.actions.map((action) =>
Widget.Button({
class_name: "action-button",
on_clicked: () => notification.invoke(action.id),
hexpand: true,
child: Widget.Label(action.label),
}),
),
}),
}),
});
return Widget.EventBox({
class_name: `notification ${notification.urgency}`,
vexpand: false,
on_primary_click: () => notification.dismiss(),
on_hover() {
actionsbox.reveal_child = true;
},
on_hover_lost() {
actionsbox.reveal_child = true;
notification.dismiss();
},
child: Widget.Box({
vertical: true,
children: [content, notification.actions.length > 0 && actionsbox],
}),
});
};

View File

@@ -0,0 +1,65 @@
import AgsWindow from "resource:///com/github/Aylur/ags/widgets/window.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import options from "../options.js";
import GObject from "gi://GObject";
class PopupWindow extends AgsWindow {
static {
GObject.registerClass(this);
}
/** @param {import('types/widgets/window').WindowProps & {
* name: string
* child: import('types/widgets/box').default
* transition?: import('types/widgets/revealer').RevealerProps['transition']
* }} o
*/
constructor({ name, child, transition = "none", visible = false, ...rest }) {
super({
...rest,
name,
popup: true,
focusable: true,
class_names: ["popup-window", name],
});
child.toggleClassName("window-content");
this.revealer = Widget.Revealer({
transition,
child,
transitionDuration: options.transition.value,
connections: [
[
App,
(_, wname, visible) => {
if (wname === name) this.revealer.reveal_child = visible;
},
],
],
});
this.child = Widget.Box({
css: "padding: 1px;",
child: this.revealer,
});
this.show_all();
this.visible = visible;
}
set transition(dir) {
this.revealer.transition = dir;
}
get transition() {
return this.revealer.transition;
}
}
/** @param {import('types/widgets/window').WindowProps & {
* name: string
* child: import('types/widgets/box').default
* transition?: import('types/widgets/revealer').RevealerProps['transition']
* }} config
*/
export default (config) => new PopupWindow(config);

View File

@@ -0,0 +1,57 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
export default ({
height = 18,
width = 180,
vertical = false,
child,
...props
}) => {
const fill = Widget.Box({
class_name: "fill",
hexpand: vertical,
vexpand: !vertical,
hpack: vertical ? "fill" : "start",
vpack: vertical ? "end" : "fill",
children: [child],
});
let fill_size = 0;
return Widget.Box({
...props,
class_name: "progress",
css: `
min-width: ${width}px;
min-height: ${height}px;
`,
children: [fill],
setup: (progress) =>
(progress.setValue = (value) => {
if (value < 0) return;
const axis = vertical ? "height" : "width";
const axisv = vertical ? height : width;
const min = vertical ? width : height;
const preferred = (axisv - min) * value + min;
if (!fill_size) {
fill_size = preferred;
fill.setCss(`min-${axis}: ${preferred}px;`);
return;
}
const frames = 10;
const goal = preferred - fill_size;
const step = goal / frames;
for (let i = 0; i < frames; ++i) {
Utils.timeout(5 * i, () => {
fill_size += step;
fill.setCss(`min-${axis}: ${fill_size}px`);
});
}
}),
});
};

View File

@@ -0,0 +1,19 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Gtk from "gi://Gtk";
import AgsWidget from "resource:///com/github/Aylur/ags/widgets/widget.js";
class RegularWindow extends AgsWidget(Gtk.Window, "RegularWindow") {
static {
AgsWidget.register(this);
}
/**
* @param {import('types/widgets/widget').BaseProps<
* RegularWindow, Gtk.Window.ConstructorProperties
* >} params */
constructor(params) {
// @ts-expect-error
super(params);
}
}
export default Widget.createCtor(RegularWindow);

View File

@@ -0,0 +1,321 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../icons.js";
import { blurImg } from "../utils.js";
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/box').BoxProps=} props
*/
export const CoverArt = (player, props) =>
Widget.Box({
...props,
class_name: "cover",
binds: [
[
"css",
player,
"cover-path",
(path) => `background-image: url("${path}")`,
],
],
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/box').BoxProps=} props
*/
export const BlurredCoverArt = (player, props) =>
Widget.Box({
...props,
class_name: "blurred-cover",
connections: [
[
player,
(box) =>
blurImg(player.cover_path).then((img) => {
img && box.setCss(`background-image: url("${img}")`);
}),
"notify::cover-path",
],
],
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/label').Props=} props
*/
export const TitleLabel = (player, props) =>
Widget.Label({
...props,
class_name: "title",
binds: [["label", player, "track-title"]],
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/label').Props=} props
*/
export const ArtistLabel = (player, props) =>
Widget.Label({
...props,
class_name: "artist",
binds: [["label", player, "track-artists", (a) => a.join(", ") || ""]],
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/icon').Props & { symbolic?: boolean }=} props
*/
export const PlayerIcon = (player, { symbolic = true, ...props } = {}) =>
Widget.Icon({
...props,
class_name: "player-icon",
tooltip_text: player.identity || "",
connections: [
[
player,
(icon) => {
const name = `${player.entry}${symbolic ? "-symbolic" : ""}`;
Utils.lookUpIcon(name)
? (icon.icon = name)
: (icon.icon = icons.mpris.fallback);
},
],
],
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/slider').SliderProps=} props
*/
export const PositionSlider = (player, props) =>
Widget.Slider({
...props,
class_name: "position-slider",
draw_value: false,
on_change: ({ value }) => {
player.position = player.length * value;
},
properties: [
[
"update",
(slider) => {
if (slider.dragging) return;
slider.visible = player.length > 0;
if (player.length > 0) slider.value = player.position / player.length;
},
],
],
connections: [
[player, (s) => s._update(s)],
[player, (s) => s._update(s), "position"],
[1000, (s) => s._update(s)],
],
});
/** @param {number} length */
function lengthStr(length) {
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? "0" : "";
return `${min}:${sec0}${sec}`;
}
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PositionLabel = (player) =>
Widget.Label({
properties: [
[
"update",
(label, time) => {
player.length > 0
? (label.label = lengthStr(time || player.position))
: (label.visible = !!player);
},
],
],
connections: [
[player, (l, time) => l._update(l, time), "position"],
[1000, (l) => l._update(l)],
],
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const LengthLabel = (player) =>
Widget.Label({
connections: [
[
player,
(label) => {
player.length > 0
? (label.label = lengthStr(player.length))
: (label.visible = !!player);
},
],
],
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const Slash = (player) =>
Widget.Label({
label: "/",
connections: [
[
player,
(label) => {
label.visible = player.length > 0;
},
],
],
});
/**
* @param {Object} o
* @param {import('types/service/mpris').MprisPlayer} o.player
* @param {import('types/widgets/stack').StackProps['items']} o.items
* @param {'shuffle' | 'loop' | 'playPause' | 'previous' | 'next'} o.onClick
* @param {string} o.prop
* @param {string} o.canProp
* @param {any} o.cantValue
*/
const PlayerButton = ({ player, items, onClick, prop, canProp, cantValue }) =>
Widget.Button({
child: Widget.Stack({
items,
binds: [["shown", player, prop, (p) => `${p}`]],
}),
on_clicked: player[onClick].bind(player),
binds: [["visible", player, canProp, (c) => c !== cantValue]],
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const ShuffleButton = (player) =>
PlayerButton({
player,
items: [
[
"true",
Widget.Label({
class_name: "shuffle enabled",
label: icons.mpris.shuffle.enabled,
}),
],
[
"false",
Widget.Label({
class_name: "shuffle disabled",
label: icons.mpris.shuffle.disabled,
}),
],
],
onClick: "shuffle",
prop: "shuffle-status",
canProp: "shuffle-status",
cantValue: null,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const LoopButton = (player) =>
PlayerButton({
player,
items: [
[
"None",
Widget.Label({
class_name: "loop none",
label: icons.mpris.loop.none,
}),
],
[
"Track",
Widget.Label({
class_name: "loop track",
label: icons.mpris.loop.track,
}),
],
[
"Playlist",
Widget.Label({
class_name: "loop playlist",
label: icons.mpris.loop.playlist,
}),
],
],
onClick: "loop",
prop: "loop-status",
canProp: "loop-status",
cantValue: null,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PlayPauseButton = (player) =>
PlayerButton({
player,
items: [
[
"Playing",
Widget.Label({
class_name: "playing",
label: icons.mpris.playing,
}),
],
[
"Paused",
Widget.Label({
class_name: "paused",
label: icons.mpris.paused,
}),
],
[
"Stopped",
Widget.Label({
class_name: "stopped",
label: icons.mpris.stopped,
}),
],
],
onClick: "playPause",
prop: "play-back-status",
canProp: "can-play",
cantValue: false,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PreviousButton = (player) =>
PlayerButton({
player,
items: [
[
"true",
Widget.Label({
class_name: "previous",
label: icons.mpris.prev,
}),
],
],
onClick: "previous",
prop: "can-go-prev",
canProp: "can-go-prev",
cantValue: false,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const NextButton = (player) =>
PlayerButton({
player,
items: [
[
"true",
Widget.Label({
class_name: "next",
label: icons.mpris.next,
}),
],
],
onClick: "next",
prop: "can-go-next",
canProp: "can-go-next",
cantValue: false,
});

View File

@@ -0,0 +1,72 @@
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Notification from "../misc/Notification.js";
import options from "../options.js";
/** @param {import('types/widgets/revealer').default} parent */
const Popups = (parent) => {
const map = new Map();
const onDismissed = (_, id, force = false) => {
if (!id || !map.has(id)) return;
if (map.get(id).isHovered() && !force) return;
if (map.size - 1 === 0) parent.reveal_child = false;
Utils.timeout(200, () => {
map.get(id)?.destroy();
map.delete(id);
});
};
/** @param {import('types/widgets/box').default} box */
const onNotified = (box, id) => {
if (!id || Notifications.dnd) return;
const n = Notifications.getNotification(id);
if (!n) return;
if (options.notifications.black_list.value.includes(n.app_name || ""))
return;
map.delete(id);
map.set(id, Notification(n));
box.children = Array.from(map.values()).reverse();
Utils.timeout(10, () => {
parent.reveal_child = true;
});
};
return Widget.Box({
vertical: true,
connections: [
[Notifications, onNotified, "notified"],
[Notifications, onDismissed, "dismissed"],
[Notifications, (box, id) => onDismissed(box, id, true), "closed"],
],
});
};
/** @param {import('types/widgets/revealer').RevealerProps['transition']} transition */
const PopupList = (transition = "slide_down") =>
Widget.Box({
css: "padding: 1px",
children: [
Widget.Revealer({
transition,
setup: (self) => (self.child = Popups(self)),
}),
],
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
monitor,
name: `notifications${monitor}`,
class_name: "notifications",
binds: [["anchor", options.notifications.position]],
child: PopupList(),
});

View File

@@ -0,0 +1,303 @@
/**
* An object holding Options that are Variables with cached values.
*
* to update an option at runtime simply run
* ags -r "options.path.to.option.setValue('value')"
*
* resetting:
* ags -r "options.reset()"
*/
import {
Option,
resetOptions,
getValues,
apply,
getOptions,
} from "./settings/option.js";
import { USER } from "resource:///com/github/Aylur/ags/utils.js";
import themes from "./themes.js";
export default {
reset: resetOptions,
values: getValues,
apply: apply,
list: getOptions,
spacing: Option(9),
padding: Option(8),
radii: Option(9),
popover_padding_multiplier: Option(1.4, {
category: "General",
note: "popover-padding: padding × this",
type: "float",
unit: "",
}),
color: {
red: Option("#e55f86", { scss: "red" }),
green: Option("#00D787", { scss: "green" }),
yellow: Option("#EBFF71", { scss: "yellow" }),
blue: Option("#51a4e7", { scss: "blue" }),
magenta: Option("#9077e7", { scss: "magenta" }),
teal: Option("#51e6e6", { scss: "teal" }),
orange: Option("#E79E64", { scss: "orange" }),
},
theme: {
name: Option(themes[0].name, {
category: "exclude",
note: "Name to show as active in quicktoggles",
}),
icon: Option(themes[0].icon, {
category: "exclude",
note: "Icon to show as active in quicktoggles",
}),
scheme: Option("dark", {
enums: ["dark", "light"],
type: "enum",
note: "Color scheme to set on Gtk apps: 'ligth' or 'dark'",
title: "Color Scheme",
scss: "color-scheme",
}),
bg: Option("#171717", {
title: "Background Color",
scss: "bg-color",
}),
fg: Option("#eeeeee", {
title: "Foreground Color",
scss: "fg-color",
}),
accent: {
accent: Option("$blue", {
category: "Theme",
title: "Accent Color",
scss: "accent",
}),
fg: Option("#141414", {
category: "Theme",
title: "Accent Foreground Color",
scss: "accent-fg",
}),
gradient: Option("to right, $accent, lighten($accent, 6%)", {
category: "Theme",
title: "Accent Linear Gradient",
scss: "accent-gradient",
}),
},
widget: {
bg: Option("$fg-color", {
category: "Theme",
title: "Widget Background Color",
scss: "_widget-bg",
}),
opacity: Option(94, {
category: "Theme",
title: "Widget Background Opacity",
unit: "",
scss: "widget-opacity",
}),
},
},
border: {
color: Option("$fg-color", {
category: "Border",
title: "Border Color",
scss: "_border-color",
}),
opacity: Option(97, {
category: "Border",
title: "Border Opacity",
unit: "",
}),
width: Option(1, {
category: "Border",
title: "Border Width",
}),
},
hypr: {
inactive_border: Option("rgba(333333ff)", {
category: "Border",
title: "Border on Inactive Windows",
scss: "exclude",
}),
wm_gaps_multiplier: Option(2.4, {
category: "General",
scss: "wm-gaps-multiplier",
note: "wm-gaps: padding × this",
type: "float",
unit: "",
}),
},
// TODO: use this on revealers
transition: Option(200, {
category: "exclude",
note: "Transition time on aminations in ms, e.g on hover",
unit: "ms",
}),
font: {
font: Option("Ubuntu Nerd Font", {
type: "font",
title: "Font",
scss: "font",
}),
mono: Option("Mononoki Nerd Font", {
title: "Monospaced Font",
scss: "mono-font",
}),
size: Option(13, {
scss: "font-size",
unit: "pt",
}),
},
applauncher: {
width: Option(500),
height: Option(500),
icon_size: Option(52),
},
bar: {
position: Option("top", {
enums: ["top", "bottom"],
type: "enum",
}),
style: Option("normal", {
enums: ["floating", "normal", "separated"],
type: "enum",
}),
flat_buttons: Option(true, { scss: "bar-flat-buttons" }),
separators: Option(true),
icon: Option("distro-icon", {
note: '"distro-icon" or a single font',
}),
},
battery: {
show_percentage: Option(true, {
persist: true,
noReload: false,
category: "exclude",
}),
bar: {
show_icon: Option(true, { category: "Bar" }),
width: Option(70, { category: "Bar" }),
height: Option(14, { category: "Bar" }),
full: Option(false, { category: "Bar" }),
},
low: Option(30, { category: "Bar" }),
medium: Option(50, { category: "Bar" }),
},
desktop: {
wallpaper: {
fg: Option("#fff", { scss: "wallpaper-fg" }),
img: Option(themes[0].options["desktop.wallpaper.img"], {
scssFormat: (v) => `"${v}"`,
type: "img",
}),
},
avatar: Option(`/var/lib/AccountsService/icons/${USER}`, {
scssFormat: (v) => `"${v}"`,
type: "img",
note: "displayed in quicksettings and locksreen",
}),
screen_corners: Option(true, { scss: "screen-corners" }),
clock: {
enable: Option(true),
position: Option("center center", {
note: "halign valign",
}),
},
drop_shadow: Option(true, { scss: "drop-shadow" }),
shadow: Option("rgba(0, 0, 0, .6)", { scss: "shadow" }),
dock: {
icon_size: Option(56),
pinned_apps: Option(
[
"firefox",
"org.wezfurlong.wezterm",
"org.gnome.Nautilus",
"org.gnome.Calendar",
"obsidian",
"transmission-gtk",
"caprine",
"teams-for-linux",
"discord",
"spotify",
"com.usebottles.bottles",
"org.gnome.Software",
],
{ scss: "exclude" },
),
},
},
notifications: {
black_list: Option(["Spotify"], { note: "app-name | entry" }),
position: Option(["top"], { note: "anchor" }),
width: Option(450),
},
dashboard: {
sys_info_size: Option(70, {
category: "Desktop",
scss: "sys-info-size",
}),
},
mpris: {
black_list: Option(["Caprine"], {
category: "Bar",
title: "List of blacklisted mpris players",
note: "filters for bus-name, name, identity, entry",
}),
preferred: Option("spotify", {
category: "Bar",
title: "Preferred player",
}),
},
workspaces: Option(7, {
category: "Bar",
title: "No. workspaces on bar and overview",
note: "Set it to 0 to make it dynamic",
}),
temperature: "/sys/class/thermal/thermal_zone0/temp",
systemFetchInterval: 5000,
brightnessctlKBD: "asus::kbd_backlight",
substitutions: {
icons: [
["transmission-gtk", "transmission"],
["blueberry.py", "bluetooth"],
["Caprine", "facebook-messenger"],
["", "preferences-desktop-display"],
],
titles: [
["com.github.Aylur.ags", "AGS"],
["transmission-gtk", "Transmission"],
["com.obsproject.Studio", "OBS"],
["com.usebottles.bottles", "Bottles"],
["com.github.wwmm.easyeffects", "Easy Effects"],
["org.gnome.TextEditor", "Text Editor"],
["org.gnome.design.IconLibrary", "Icon Library"],
["blueberry.py", "Blueberry"],
["org.wezfurlong.wezterm", "Wezterm"],
["com.raggesilver.BlackBox", "BlackBox"],
["firefox", "Firefox"],
["org.gnome.Nautilus", "Files"],
["libreoffice-writer", "Writer"],
["", "Desktop"],
],
},
};

View File

@@ -0,0 +1,77 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import FontIcon from "../misc/FontIcon.js";
import Progress from "../misc/Progress.js";
import Indicator from "../services/onScreenIndicator.js";
export const OnScreenIndicator = ({ height = 300, width = 48 } = {}) =>
Widget.Box({
class_name: "indicator",
css: "padding: 1px;",
child: Widget.Revealer({
transition: "slide_left",
connections: [
[
Indicator,
(revealer, value) => {
revealer.reveal_child = value > -1;
},
],
],
child: Progress({
width,
height,
vertical: true,
connections: [
[Indicator, (progress, value) => progress.setValue(value)],
],
child: Widget.Stack({
vpack: "start",
hpack: "center",
hexpand: false,
items: [
[
"true",
Widget.Icon({
hpack: "center",
size: width,
connections: [
[Indicator, (icon, _v, name) => (icon.icon = name || "")],
],
}),
],
[
"false",
FontIcon({
hpack: "center",
hexpand: true,
css: `font-size: ${width}px;`,
connections: [
[Indicator, (icon, _v, name) => (icon.icon = name || "")],
],
}),
],
],
connections: [
[
Indicator,
(stack, _v, name) => {
stack.shown = `${!!Utils.lookUpIcon(name)}`;
},
],
],
}),
}),
}),
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
name: `indicator${monitor}`,
monitor,
class_name: "indicator",
layer: "overlay",
anchor: ["right"],
child: OnScreenIndicator(),
});

View File

@@ -0,0 +1,50 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import { createSurfaceFromWidget, substitute } from "../utils.js";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import options from "../options.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
const SCALE = 0.08;
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)];
/** @param {string} args */
const dispatch = (args) => Hyprland.sendMessage(`dispatch ${args}`);
/** @param {string} str */
const icon = (str) => substitute(options.substitutions.icons, str);
export default ({ address, size: [w, h], class: c, title }) =>
Widget.Button({
class_name: "client",
tooltip_text: `${title}`,
child: Widget.Icon({
css: `
min-width: ${w * SCALE}px;
min-height: ${h * SCALE}px;
`,
icon: icon(c),
}),
on_secondary_click: () => dispatch(`closewindow address:${address}`),
on_clicked: () =>
dispatch(`focuswindow address:${address}`).then(() =>
App.closeWindow("overview"),
),
setup: (btn) => {
btn.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
TARGET,
Gdk.DragAction.COPY,
);
btn.connect("drag-data-get", (_w, _c, data) =>
data.set_text(address, address.length),
);
btn.connect("drag-begin", (_, context) => {
Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(btn));
btn.toggleClassName("hidden", true);
});
btn.connect("drag-end", () => btn.toggleClassName("hidden", false));
},
});

View File

@@ -0,0 +1,53 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import PopupWindow from "../misc/PopupWindow.js";
import Workspace from "./Workspace.js";
import options from "../options.js";
import { range } from "../utils.js";
const ws = options.workspaces;
/** @param {import('types/widgets/box').default} box */
const update = (box) => {
if (App.windows.has("overview") && !App.getWindow("overview")?.visible)
return;
Hyprland.sendMessage("j/clients")
.then((clients) => {
box.children.forEach((ws) => {
// @ts-expect-error
ws.attribute(JSON.parse(clients));
});
})
.catch(console.error);
};
/** @param {import('types/widgets/box').default} box */
const children = (box) => {
if (ws.value === 0) {
box.children = Hyprland.workspaces
.sort((ws1, ws2) => ws1.id - ws2.id)
.map(({ id }) => Workspace(id));
}
};
export default () =>
PopupWindow({
name: "overview",
child: Widget.Box({
setup: update,
connections: [
[
ws,
(box) => {
box.children = range(ws.value).map(Workspace);
update(box);
children(box);
},
],
[Hyprland, update],
[Hyprland, children, "notify::workspaces"],
],
}),
});

View File

@@ -0,0 +1,64 @@
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import Client from "./Client.js";
const SCALE = 0.08;
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)];
/** @param {string} args */
const dispatch = (args) => Utils.execAsync(`hyprctl dispatch ${args}`);
/** @param {number} index */
export default (index) => {
const fixed = Gtk.Fixed.new();
return Widget.Box({
class_name: "workspace",
vpack: "center",
css: `
min-width: ${1920 * SCALE}px;
min-height: ${1080 * SCALE}px;
`,
connections: [
[
Hyprland,
(box) => {
box.toggleClassName("active", Hyprland.active.workspace.id === index);
},
],
],
child: Widget.EventBox({
hexpand: true,
vexpand: true,
on_primary_click: () => dispatch(`workspace ${index}`),
setup: (eventbox) => {
eventbox.drag_dest_set(
Gtk.DestDefaults.ALL,
TARGET,
Gdk.DragAction.COPY,
);
eventbox.connect("drag-data-received", (_w, _c, _x, _y, data) => {
dispatch(`movetoworkspacesilent ${index},address:${data.get_text()}`);
});
},
child: fixed,
}),
/** @param {Array<import('types/service/hyprland').Client>} clients */
attribute: (clients) => {
fixed.get_children().forEach((ch) => ch.destroy());
clients
.filter(({ workspace: { id } }) => id === index)
.forEach((c) => {
c.at[0] -= Hyprland.getMonitor(c.monitor)?.x || 0;
c.at[1] -= Hyprland.getMonitor(c.monitor)?.y || 0;
c.mapped && fixed.put(Client(c), c.at[0] * SCALE, c.at[1] * SCALE);
});
fixed.show_all();
},
});
};

View File

@@ -0,0 +1,31 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../icons.js";
import PowerMenu from "../services/powermenu.js";
import ShadedPopup from "./ShadedPopup.js";
/**
* @param {'sleep' | 'reboot' | 'logout' | 'shutdown'} action
* @param {string} label
*/
const SysButton = (action, label) =>
Widget.Button({
on_clicked: () => PowerMenu.action(action),
child: Widget.Box({
vertical: true,
children: [Widget.Icon(icons.powermenu[action]), Widget.Label(label)],
}),
});
export default () =>
ShadedPopup({
name: "powermenu",
expand: true,
child: Widget.Box({
children: [
SysButton("sleep", "Sleep"),
SysButton("reboot", "Reboot"),
SysButton("logout", "Log Out"),
SysButton("shutdown", "Shutdown"),
],
}),
});

View File

@@ -0,0 +1,42 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
/** @param {string} windowName */
const Padding = (windowName) =>
Widget.EventBox({
class_name: "padding",
hexpand: true,
vexpand: true,
connections: [["button-press-event", () => App.toggleWindow(windowName)]],
});
/**
* @param {import('types/widgets/window').WindowProps & {
* name: string
* child: import('types/widgets/box').default
* }} o
*/
export default ({ name, child, ...rest }) =>
Widget.Window({
...rest,
class_names: ["popup-window", name],
name,
visible: false,
popup: true,
focusable: true,
setup() {
child.toggleClassName("window-content");
},
child: Widget.CenterBox({
class_name: "shader",
css: "min-width: 5000px; min-height: 3000px;",
children: [
Padding(name),
Widget.CenterBox({
vertical: true,
children: [Padding(name), child, Padding(name)],
}),
Padding(name),
],
}),
});

View File

@@ -0,0 +1,46 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import PowerMenu from "../services/powermenu.js";
import ShadedPopup from "./ShadedPopup.js";
export default () =>
ShadedPopup({
name: "verification",
expand: true,
child: Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: "text-box",
vertical: true,
children: [
Widget.Label({
class_name: "title",
binds: [["label", PowerMenu, "title"]],
}),
Widget.Label({
class_name: "desc",
label: "Are you sure?",
}),
],
}),
Widget.Box({
class_name: "buttons horizontal",
vexpand: true,
vpack: "end",
homogeneous: true,
children: [
Widget.Button({
child: Widget.Label("No"),
on_clicked: () => App.toggleWindow("verification"),
}),
Widget.Button({
child: Widget.Label("Yes"),
on_clicked: () => Utils.exec(PowerMenu.cmd),
}),
],
}),
],
}),
});

View File

@@ -0,0 +1,73 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Header from "./widgets/Header.js";
import PopupWindow from "../misc/PopupWindow.js";
import { Volume, Microhone, SinkSelector, AppMixer } from "./widgets/Volume.js";
import { NetworkToggle, WifiSelection } from "./widgets/Network.js";
import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth.js";
import { ThemeToggle, ThemeSelector } from "./widgets/Theme.js";
import { ProfileToggle, ProfileSelector } from "./widgets/AsusProfile.js";
import Media from "./widgets/Media.js";
import Brightness from "./widgets/Brightness.js";
import DND from "./widgets/DND.js";
import MicMute from "./widgets/MicMute.js";
import options from "../options.js";
const Row = (toggles = [], menus = []) =>
Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: "row horizontal",
children: toggles,
}),
...menus,
],
});
const Homogeneous = (toggles) =>
Widget.Box({
homogeneous: true,
children: toggles,
});
export default () =>
PopupWindow({
name: "quicksettings",
connections: [
[
options.bar.position,
(self) => {
self.anchor = ["right", options.bar.position.value];
if (options.bar.position.value === "top")
self.transition = "slide_down";
if (options.bar.position.value === "bottom")
self.transition = "slide_up";
},
],
],
child: Widget.Box({
vertical: true,
children: [
Header(),
Widget.Box({
class_name: "sliders-box vertical",
vertical: true,
children: [
Row([Volume()], [SinkSelector(), AppMixer()]),
Microhone(),
Brightness(),
],
}),
Row(
[Homogeneous([NetworkToggle(), BluetoothToggle()]), DND()],
[WifiSelection(), BluetoothDevices()],
),
Row(
[Homogeneous([ProfileToggle(), ThemeToggle()]), MicMute()],
[ProfileSelector(), ThemeSelector()],
),
Media(),
],
}),
});

View File

@@ -0,0 +1,149 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../icons.js";
/** name of the currently opened menu */
export const opened = Variable("");
App.connect("window-toggled", (_, name, visible) => {
if (name === "quicksettings" && !visible)
Utils.timeout(500, () => (opened.value = ""));
});
/**
* @param {string} name - menu name
* @param {(() => void) | false=} activate
*/
export const Arrow = (name, activate) => {
let deg = 0;
let iconOpened = false;
return Widget.Button({
child: Widget.Icon({
icon: icons.ui.arrow.right,
connections: [
[
opened,
(icon) => {
if (
(opened.value === name && !iconOpened) ||
(opened.value !== name && iconOpened)
) {
const step = opened.value === name ? 10 : -10;
iconOpened = !iconOpened;
for (let i = 0; i < 9; ++i) {
Utils.timeout(15 * i, () => {
deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
});
}
}
},
],
],
}),
on_clicked: () => {
opened.value = opened.value === name ? "" : name;
if (typeof activate === "function") activate();
},
});
};
/**
* @param {Object} o
* @param {string} o.name - menu name
* @param {import('gi://Gtk').Gtk.Widget} o.icon
* @param {import('gi://Gtk').Gtk.Widget} o.label
* @param {() => void} o.activate
* @param {() => void} o.deactivate
* @param {boolean=} o.activateOnArrow
* @param {[import('gi://GObject').GObject.Object, () => boolean]} o.connection
*/
export const ArrowToggleButton = ({
name,
icon,
label,
activate,
deactivate,
activateOnArrow = true,
connection: [service, condition],
}) =>
Widget.Box({
class_name: "toggle-button",
connections: [
[
service,
(box) => {
box.toggleClassName("active", condition());
},
],
],
children: [
Widget.Button({
child: Widget.Box({
hexpand: true,
class_name: "label-box horizontal",
children: [icon, label],
}),
on_clicked: () => {
if (condition()) {
deactivate();
if (opened.value === name) opened.value = "";
} else {
activate();
}
},
}),
Arrow(name, activateOnArrow && activate),
],
});
/**
* @param {Object} o
* @param {string} o.name - menu name
* @param {import('gi://Gtk').Gtk.Widget} o.icon
* @param {import('gi://Gtk').Gtk.Widget} o.title
* @param {import('gi://Gtk').Gtk.Widget[]} o.content
*/
export const Menu = ({ name, icon, title, content }) =>
Widget.Revealer({
transition: "slide_down",
binds: [["reveal-child", opened, "value", (v) => v === name]],
child: Widget.Box({
class_names: ["menu", name],
vertical: true,
children: [
Widget.Box({
class_name: "title horizontal",
children: [icon, title],
}),
Widget.Separator(),
...content,
],
}),
});
/**
* @param {Object} o
* @param {import('gi://Gtk').Gtk.Widget} o.icon
* @param {() => void} o.toggle
* @param {[import('gi://GObject').GObject.Object, () => boolean]} o.connection
*/
export const SimpleToggleButton = ({
icon,
toggle,
connection: [service, condition],
}) =>
Widget.Button({
class_name: "simple-toggle",
connections: [
[
service,
(box) => {
box.toggleClassName("active", condition());
},
],
],
child: icon,
on_clicked: toggle,
});

View File

@@ -0,0 +1,61 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import Asusctl from "../../services/asusctl.js";
import { ArrowToggleButton, Menu } from "../ToggleButton.js";
export const ProfileToggle = () =>
ArrowToggleButton({
name: "asusctl-profile",
icon: Widget.Icon({
binds: [["icon", Asusctl, "profile", (p) => icons.asusctl.profile[p]]],
}),
label: Widget.Label({
binds: [["label", Asusctl, "profile"]],
}),
connection: [Asusctl, () => Asusctl.profile !== "Balanced"],
activate: () => Asusctl.setProfile("Quiet"),
deactivate: () => Asusctl.setProfile("Balanced"),
activateOnArrow: false,
});
export const ProfileSelector = () =>
Menu({
name: "asusctl-profile",
icon: Widget.Icon({
binds: [["icon", Asusctl, "profile", (p) => icons.asusctl.profile[p]]],
}),
title: Widget.Label("Profile Selector"),
content: [
Widget.Box({
vertical: true,
hexpand: true,
children: [
Widget.Box({
vertical: true,
children: Asusctl.profiles.map((prof) =>
Widget.Button({
on_clicked: () => Asusctl.setProfile(prof),
child: Widget.Box({
children: [
Widget.Icon(icons.asusctl.profile[prof]),
Widget.Label(prof),
],
}),
}),
),
}),
],
}),
Widget.Separator(),
Widget.Button({
on_clicked: () => Utils.execAsync("rog-control-center"),
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Rog Control Center"),
],
}),
}),
],
});

View File

@@ -0,0 +1,95 @@
import Bluetooth from "resource:///com/github/Aylur/ags/service/bluetooth.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import { Menu, ArrowToggleButton } from "../ToggleButton.js";
export const BluetoothToggle = () =>
ArrowToggleButton({
name: "bluetooth",
icon: Widget.Icon({
connections: [
[
Bluetooth,
(icon) => {
icon.icon = Bluetooth.enabled
? icons.bluetooth.enabled
: icons.bluetooth.disabled;
},
],
],
}),
label: Widget.Label({
truncate: "end",
connections: [
[
Bluetooth,
(label) => {
if (!Bluetooth.enabled) return (label.label = "Disabled");
if (Bluetooth.connectedDevices.length === 0)
return (label.label = "Not Connected");
if (Bluetooth.connectedDevices.length === 1)
return (label.label = Bluetooth.connectedDevices[0].alias);
label.label = `${Bluetooth.connectedDevices.length} Connected`;
},
],
],
}),
connection: [Bluetooth, () => Bluetooth.enabled],
deactivate: () => (Bluetooth.enabled = false),
activate: () => (Bluetooth.enabled = true),
});
const DeviceItem = (device) =>
Widget.Box({
children: [
Widget.Icon(device.icon_name + "-symbolic"),
Widget.Label(device.name),
Widget.Label({
label: `${device.battery_percentage}%`,
binds: [["visible", device, "battery-percentage", (p) => p > 0]],
}),
Widget.Box({ hexpand: true }),
Widget.Spinner({
binds: [
["active", device, "connecting"],
["visible", device, "connecting"],
],
}),
Widget.Switch({
active: device.connected,
binds: [["visible", device, "connecting", (c) => !c]],
connections: [
[
"notify::active",
({ active }) => {
device.setConnection(active);
},
],
],
}),
],
});
export const BluetoothDevices = () =>
Menu({
name: "bluetooth",
icon: Widget.Icon(icons.bluetooth.disabled),
title: Widget.Label("Bluetooth"),
content: [
Widget.Box({
hexpand: true,
vertical: true,
binds: [
[
"children",
Bluetooth,
"devices",
(ds) => ds.filter((d) => d.name).map(DeviceItem),
],
],
}),
],
});

View File

@@ -0,0 +1,29 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import Brightness from "../../services/brightness.js";
const BrightnessSlider = () =>
Widget.Slider({
draw_value: false,
hexpand: true,
binds: [["value", Brightness, "screen"]],
on_change: ({ value }) => (Brightness.screen = value),
});
export default () =>
Widget.Box({
children: [
Widget.Button({
child: Widget.Icon(icons.brightness.indicator),
binds: [
[
"tooltip-text",
Brightness,
"screen",
(v) => `Screen Brightness: ${Math.floor(v * 100)}%`,
],
],
}),
BrightnessSlider(),
],
});

View File

@@ -0,0 +1,23 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import icons from "../../icons.js";
import { SimpleToggleButton } from "../ToggleButton.js";
export default () =>
SimpleToggleButton({
icon: Widget.Icon({
connections: [
[
Notifications,
(icon) => {
icon.icon = Notifications.dnd
? icons.notifications.silent
: icons.notifications.noisy;
},
"notify::dnd",
],
],
}),
toggle: () => (Notifications.dnd = !Notifications.dnd),
connection: [Notifications, () => Notifications.dnd],
});

View File

@@ -0,0 +1,48 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import PowerMenu from "../../services/powermenu.js";
import Lockscreen from "../../services/lockscreen.js";
import Avatar from "../../misc/Avatar.js";
import icons from "../../icons.js";
import { openSettings } from "../../settings/theme.js";
import { uptime } from "../../variables.js";
export default () =>
Widget.Box({
class_name: "header horizontal",
children: [
Avatar(),
Widget.Box({
hpack: "end",
vpack: "center",
hexpand: true,
children: [
Widget.Box({
class_name: "battery horizontal",
children: [
Widget.Icon({ binds: [["icon", Battery, "icon-name"]] }),
Widget.Label({
binds: [["label", Battery, "percent", (p) => `${p}%`]],
}),
],
}),
Widget.Label({
class_name: "uptime",
binds: [["label", uptime, "value", (v) => `up: ${v}`]],
}),
Widget.Button({
on_clicked: openSettings,
child: Widget.Icon(icons.ui.settings),
}),
Widget.Button({
on_clicked: () => Lockscreen.lockscreen(),
child: Widget.Icon(icons.lock),
}),
Widget.Button({
on_clicked: () => PowerMenu.action("shutdown"),
child: Widget.Icon(icons.powermenu.shutdown),
}),
],
}),
],
});

View File

@@ -0,0 +1,106 @@
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as mpris from "../../misc/mpris.js";
import options from "../../options.js";
/** @param {import('types/service/mpris').MprisPlayer} player */
const Footer = (player) =>
Widget.CenterBox({
class_name: "footer-box",
children: [
Widget.Box({
class_name: "position",
children: [
mpris.PositionLabel(player),
mpris.Slash(player),
mpris.LengthLabel(player),
],
}),
Widget.Box({
class_name: "controls",
children: [
mpris.ShuffleButton(player),
mpris.PreviousButton(player),
mpris.PlayPauseButton(player),
mpris.NextButton(player),
mpris.LoopButton(player),
],
}),
mpris.PlayerIcon(player, {
symbolic: false,
hexpand: true,
hpack: "end",
}),
],
});
/** @param {import('types/service/mpris').MprisPlayer} player */
const TextBox = (player) =>
Widget.Box({
children: [
mpris.CoverArt(player, {
hpack: "end",
hexpand: false,
}),
Widget.Box({
hexpand: true,
vertical: true,
class_name: "labels",
children: [
mpris.TitleLabel(player, {
xalign: 0,
justification: "left",
wrap: true,
}),
mpris.ArtistLabel(player, {
xalign: 0,
justification: "left",
wrap: true,
}),
],
}),
],
});
/** @param {import('types/service/mpris').MprisPlayer} player */
const PlayerBox = (player) =>
Widget.Box({
class_name: `player ${player.name}`,
child: mpris.BlurredCoverArt(player, {
hexpand: true,
child: Widget.Box({
hexpand: true,
vertical: true,
children: [
TextBox(player),
mpris.PositionSlider(player),
Footer(player),
],
}),
}),
});
export default () =>
Widget.Box({
vertical: true,
class_name: "media vertical",
connections: [
[
"draw",
(self) => {
self.visible = Mpris.players.length > 0;
},
],
],
binds: [
[
"children",
Mpris,
"players",
(ps) =>
ps
.filter((p) => !options.mpris.black_list.value.includes(p.identity))
.map(PlayerBox),
],
],
});

View File

@@ -0,0 +1,23 @@
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import { SimpleToggleButton } from "../ToggleButton.js";
export default () =>
SimpleToggleButton({
icon: Widget.Icon({
connections: [
[
Audio,
(icon) => {
icon.icon = Audio.microphone?.is_muted
? icons.audio.mic.muted
: icons.audio.mic.high;
},
"microphone-changed",
],
],
}),
toggle: () => (Audio.microphone.is_muted = !Audio.microphone.is_muted),
connection: [Audio, () => Audio.microphone?.is_muted],
});

View File

@@ -0,0 +1,91 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Network from "resource:///com/github/Aylur/ags/service/network.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../../icons.js";
import { Menu, ArrowToggleButton } from "../ToggleButton.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
export const NetworkToggle = () =>
ArrowToggleButton({
name: "network",
icon: Widget.Icon({
connections: [
[
Network,
(icon) => {
icon.icon = Network.wifi.icon_name || "";
},
],
],
}),
label: Widget.Label({
truncate: "end",
connections: [
[
Network,
(label) => {
label.label = Network.wifi.ssid || "Not Connected";
},
],
],
}),
connection: [Network, () => Network.wifi.enabled],
deactivate: () => (Network.wifi.enabled = false),
activate: () => {
Network.wifi.enabled = true;
Network.wifi.scan();
},
});
export const WifiSelection = () =>
Menu({
name: "network",
icon: Widget.Icon({
connections: [
[
Network,
(icon) => {
icon.icon = Network.wifi.icon_name;
},
],
],
}),
title: Widget.Label("Wifi Selection"),
content: [
Widget.Box({
vertical: true,
connections: [
[
Network,
(box) =>
(box.children = Network.wifi?.access_points.map((ap) =>
Widget.Button({
on_clicked: () =>
Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`),
child: Widget.Box({
children: [
Widget.Icon(ap.iconName),
Widget.Label(ap.ssid || ""),
ap.active &&
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
}),
],
}),
}),
)),
],
],
}),
Widget.Separator(),
Widget.Button({
on_clicked: () =>
Applications.query("gnome-control-center")?.[0].launch(),
child: Widget.Box({
children: [Widget.Icon(icons.ui.settings), Widget.Label("Network")],
}),
}),
],
});

View File

@@ -0,0 +1,57 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import { ArrowToggleButton, Menu, opened } from "../ToggleButton.js";
import themes from "../../themes.js";
import icons from "../../icons.js";
import options from "../../options.js";
import { setTheme, openSettings } from "../../settings/theme.js";
export const ThemeToggle = () =>
ArrowToggleButton({
name: "theme",
icon: Widget.Label({ binds: [["label", options.theme.icon]] }),
label: Widget.Label({ binds: [["label", options.theme.name]] }),
connection: [opened, () => opened.value === "theme"],
activate: () => opened.setValue("theme"),
activateOnArrow: false,
deactivate: () => {},
});
export const ThemeSelector = () =>
Menu({
name: "theme",
icon: Widget.Label({
binds: [["label", options.theme.icon]],
}),
title: Widget.Label("Theme Selector"),
content: [
...themes.map(({ name, icon }) =>
Widget.Button({
on_clicked: () => setTheme(name),
child: Widget.Box({
children: [
Widget.Label(icon),
Widget.Label(name),
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
binds: [
["visible", options.theme.name, "value", (v) => v === name],
],
}),
],
}),
}),
),
Widget.Separator(),
Widget.Button({
on_clicked: openSettings,
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Theme Settings"),
],
}),
}),
],
});

View File

@@ -0,0 +1,194 @@
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../../icons.js";
import FontIcon from "../../misc/FontIcon.js";
import { getAudioTypeIcon } from "../../utils.js";
import { Arrow } from "../ToggleButton.js";
import { Menu } from "../ToggleButton.js";
/** @param {'speaker' | 'microphone'=} type */
const VolumeIndicator = (type = "speaker") =>
Widget.Button({
on_clicked: () => (Audio[type].is_muted = !Audio[type].is_muted),
child: Widget.Icon({
connections: [
[
Audio,
(icon) => {
if (!Audio[type]) return;
icon.icon =
type === "speaker"
? getAudioTypeIcon(Audio[type].icon_name || "")
: icons.audio.mic.high;
icon.tooltip_text = `Volume ${Math.floor(
Audio[type].volume * 100,
)}%`;
},
`${type}-changed`,
],
],
}),
});
/** @param {'speaker' | 'microphone'=} type */
const VolumeSlider = (type = "speaker") =>
Widget.Slider({
hexpand: true,
draw_value: false,
on_change: ({ value }) => (Audio[type].volume = value),
connections: [
[
Audio,
(slider) => {
slider.value = Audio[type]?.volume;
},
`${type}-changed`,
],
],
});
export const Volume = () =>
Widget.Box({
children: [
VolumeIndicator("speaker"),
VolumeSlider("speaker"),
Widget.Box({
vpack: "center",
child: Arrow("sink-selector"),
}),
Widget.Box({
vpack: "center",
child: Arrow("app-mixer"),
connections: [
[
Audio,
(box) => {
box.visible = Audio.apps.length > 0;
},
],
],
}),
],
});
export const Microhone = () =>
Widget.Box({
class_name: "slider horizontal",
binds: [["visible", Audio, "recorders", (r) => r.length > 0]],
children: [VolumeIndicator("microphone"), VolumeSlider("microphone")],
});
/** @param {import('types/service/audio').Stream} stream */
const MixerItem = (stream) =>
Widget.Box({
hexpand: true,
class_name: "mixer-item horizontal",
children: [
Widget.Icon({
binds: [["tooltipText", stream, "name"]],
connections: [
[
stream,
(icon) => {
icon.icon = Utils.lookUpIcon(stream.name || "")
? stream.name || ""
: icons.mpris.fallback;
},
],
],
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
xalign: 0,
truncate: "end",
binds: [["label", stream, "description"]],
}),
Widget.Slider({
hexpand: true,
draw_value: false,
binds: [["value", stream, "volume"]],
on_change: ({ value }) => (stream.volume = value),
}),
],
}),
Widget.Label({
xalign: 1,
connections: [
[
stream,
(l) => {
l.label = `${Math.floor(stream.volume * 100)}%`;
},
],
],
}),
],
});
/** @param {import('types/service/audio').Stream} stream */
const SinkItem = (stream) =>
Widget.Button({
hexpand: true,
on_clicked: () => (Audio.speaker = stream),
child: Widget.Box({
children: [
Widget.Icon({
icon: getAudioTypeIcon(stream.icon_name || ""),
tooltip_text: stream.icon_name,
}),
Widget.Label(
(stream.description || "").split(" ").slice(0, 4).join(" "),
),
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
binds: [["visible", Audio, "speaker", (s) => s === stream]],
}),
],
}),
});
const SettingsButton = () =>
Widget.Button({
on_clicked: () => Utils.execAsync("pavucontrol"),
hexpand: true,
child: Widget.Box({
children: [Widget.Icon(icons.ui.settings), Widget.Label("Settings")],
}),
});
export const AppMixer = () =>
Menu({
name: "app-mixer",
icon: FontIcon(icons.audio.mixer),
title: Widget.Label("App Mixer"),
content: [
Widget.Box({
vertical: true,
binds: [["children", Audio, "apps", (a) => a.map(MixerItem)]],
}),
Widget.Separator(),
SettingsButton(),
],
});
export const SinkSelector = () =>
Menu({
name: "sink-selector",
icon: Widget.Icon(icons.audio.type.headset),
title: Widget.Label("Sink Selector"),
content: [
Widget.Box({
vertical: true,
binds: [["children", Audio, "speakers", (s) => s.map(SinkItem)]],
}),
Widget.Separator(),
SettingsButton(),
],
});

View File

@@ -0,0 +1,76 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Gtk from "gi://Gtk";
import options from "../options.js";
/** @param {'topleft' | 'topright' | 'bottomleft' | 'bottomright'} place */
const Corner = (place) =>
Widget.DrawingArea({
class_name: "corner",
hexpand: true,
vexpand: true,
hpack: place.includes("left") ? "start" : "end",
vpack: place.includes("top") ? "start" : "end",
connections: [
[
options.radii,
(self) => {
const r = options.radii.value * 2;
self.set_size_request(r, r);
},
],
],
setup: (self) =>
self.connect("draw", (self, cr) => {
const context = self.get_style_context();
const c = context.get_property(
"background-color",
Gtk.StateFlags.NORMAL,
);
const r = context.get_property("border-radius", Gtk.StateFlags.NORMAL);
switch (place) {
case "topleft":
cr.arc(r, r, r, Math.PI, (3 * Math.PI) / 2);
cr.lineTo(0, 0);
break;
case "topright":
cr.arc(0, r, r, (3 * Math.PI) / 2, 2 * Math.PI);
cr.lineTo(r, 0);
break;
case "bottomleft":
cr.arc(r, 0, r, Math.PI / 2, Math.PI);
cr.lineTo(0, r);
break;
case "bottomright":
cr.arc(0, 0, r, 0, Math.PI / 2);
cr.lineTo(r, r);
break;
}
cr.closePath();
cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
cr.fill();
}),
});
/** @type {Array<'topleft' | 'topright' | 'bottomleft' | 'bottomright'>} */
const places = ["topleft", "topright", "bottomleft", "bottomright"];
/** @param {number} monitor */
export default (monitor) =>
places.map((place) =>
Widget.Window({
name: `corner${monitor}${place}`,
monitor,
class_name: "corner",
anchor: [
place.includes("top") ? "top" : "bottom",
place.includes("right") ? "right" : "left",
],
binds: [["visible", options.desktop.screen_corners]],
child: Corner(place),
}),
);

View File

@@ -0,0 +1,70 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
class Asusctl extends Service {
static {
Service.register(
this,
{},
{
profile: ["string", "r"],
mode: ["string", "r"],
},
);
}
profiles = Object.freeze(["Performance", "Balanced", "Quiet"]);
#profile = "Balanced";
#mode = "Hyprid";
nextProfile() {
Utils.execAsync("asusctl profile -n")
.then(() => {
this.#profile = Utils.exec("asusctl profile -p").split(" ")[3];
this.changed("profile");
})
.catch(console.error);
}
/** @param {'Performance' | 'Balanced' | 'Quiet'} prof */
setProfile(prof) {
Utils.execAsync(`asusctl profile --profile-set ${prof}`)
.then(() => {
this.#profile = prof;
this.changed("profile");
})
.catch(console.error);
}
nextMode() {
Utils.execAsync(
`supergfxctl -m ${this.#mode === "Hybrid" ? "Integrated" : "Hybrid"}`,
)
.then(() => {
this.#mode = Utils.exec("supergfxctl -g");
this.changed("profile");
})
.catch(console.error);
}
constructor() {
super();
if (Utils.exec("which asusctl")) {
this.available = true;
this.#profile = Utils.exec("asusctl profile -p").split(" ")[3];
Utils.execAsync("supergfxctl -g").then((mode) => (this.#mode = mode));
} else {
this.available = false;
}
}
get profile() {
return this.#profile;
}
get mode() {
return this.#mode;
}
}
export default new Asusctl();

View File

@@ -0,0 +1,72 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import options from "../options.js";
import { dependencies } from "../utils.js";
const KBD = options.brightnessctlKBD;
class Brightness extends Service {
static {
Service.register(
this,
{},
{
screen: ["float", "rw"],
kbd: ["int", "rw"],
},
);
}
#kbd = 0;
#kbdMax = 3;
#screen = 0;
get kbd() {
return this.#kbd;
}
get screen() {
return this.#screen;
}
set kbd(value) {
if (!dependencies(["brightnessctl"])) return;
if (value < 0 || value > this.#kbdMax) return;
Utils.execAsync(`brightnessctl -d ${KBD} s ${value} -q`)
.then(() => {
this.#kbd = value;
this.changed("kbd");
})
.catch(console.error);
}
set screen(percent) {
if (!dependencies(["brightnessctl"])) return;
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();
if (dependencies(["brightnessctl"])) {
this.#kbd = Number(Utils.exec(`brightnessctl -d ${KBD} g`));
this.#kbdMax = Number(Utils.exec(`brightnessctl -d ${KBD} m`));
this.#screen =
Number(Utils.exec("brightnessctl g")) /
Number(Utils.exec("brightnessctl m"));
}
}
}
export default new Brightness();

View File

@@ -0,0 +1,73 @@
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import { Variable } from "resource:///com/github/Aylur/ags/variable.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import { dependencies } from "../utils.js";
const COLORS_CACHE = Utils.CACHE_DIR + "/colorpicker.json";
class Colors extends Service {
static {
Service.register(
this,
{},
{
colors: ["jsobject"],
},
);
}
/** @type {Variable<string[]>} */
#colors = new Variable([]);
get colors() {
return this.#colors.value;
}
#notifID = 0;
constructor() {
super();
this.#colors.connect("changed", () => this.changed("colors"));
Utils.readFileAsync(COLORS_CACHE)
.then((out) => this.#colors.setValue(JSON.parse(out || "[]")))
.catch(() => print("no colorpicker cache found"));
}
/** @param {string} color */
wlCopy(color) {
Utils.execAsync(["wl-copy", color]).catch((err) => console.error(err));
}
async pick() {
if (!dependencies(["hyprpicker"])) return;
const color = await Utils.execAsync("hyprpicker");
if (!color) return;
this.wlCopy(color);
const list = this.#colors.value;
if (!list.includes(color)) {
list.push(color);
if (list.length > 10) list.shift();
this.#colors.value = list;
Utils.writeFile(JSON.stringify(list, null, 2), COLORS_CACHE).catch(
(err) => console.error(err),
);
}
this.#notifID = Notifications.Notify(
"Color Picker",
this.#notifID,
"color-select-symbolic",
color,
"",
[],
{},
);
}
}
export default new Colors();

View File

@@ -0,0 +1,30 @@
import Service from "resource:///com/github/Aylur/ags/service.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
const authpy = App.configDir + "/js/lockscreen/auth.py";
class Lockscreen extends Service {
static {
Service.register(this, {
lock: ["boolean"],
authenticating: ["boolean"],
});
}
lockscreen() {
this.emit("lock", true);
}
/** @param {string} password */
auth(password) {
this.emit("authenticating", true);
Utils.execAsync([authpy, password])
.then((out) => {
this.emit("lock", out !== "True");
this.emit("authenticating", false);
})
.catch((err) => console.error(err));
}
}
export default new Lockscreen();

View File

@@ -0,0 +1,58 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import icons from "../icons.js";
import { getAudioTypeIcon } from "../utils.js";
import Brightness from "./brightness.js";
class Indicator extends Service {
static {
Service.register(this, {
popup: ["double", "string"],
});
}
#delay = 1500;
#count = 0;
/**
* @param {number} value - 0 < v < 1
* @param {string} icon
*/
popup(value, icon) {
this.emit("popup", value, icon);
this.#count++;
Utils.timeout(this.#delay, () => {
this.#count--;
if (this.#count === 0) this.emit("popup", -1, icon);
});
}
speaker() {
this.popup(
Audio.speaker?.volume || 0,
getAudioTypeIcon(Audio.speaker?.icon_name || ""),
);
}
display() {
// brightness is async, so lets wait a bit
Utils.timeout(10, () =>
this.popup(Brightness.screen, icons.brightness.screen),
);
}
kbd() {
// brightness is async, so lets wait a bit
Utils.timeout(10, () =>
this.popup((Brightness.kbd * 33 + 1) / 100, icons.brightness.keyboard),
);
}
connect(event = "popup", callback) {
return super.connect(event, callback);
}
}
export default new Indicator();

View File

@@ -0,0 +1,43 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
class PowerMenu extends Service {
static {
Service.register(
this,
{},
{
title: ["string"],
cmd: ["string"],
},
);
}
#title = "";
#cmd = "";
get title() {
return this.#title;
}
get cmd() {
return this.#cmd;
}
/** @param {'sleep' | 'reboot' | 'logout' | 'shutdown'} action */
action(action) {
[this.#cmd, this.#title] = {
sleep: ["systemctl suspend", "Sleep"],
reboot: ["systemctl reboot", "Reboot"],
logout: ["pkill Hyprland", "Log Out"],
shutdown: ["shutdown now", "Shutdown"],
}[action];
this.notify("cmd");
this.notify("title");
this.emit("changed");
App.closeWindow("powermenu");
App.openWindow("verification");
}
}
export default new PowerMenu();

View File

@@ -0,0 +1,112 @@
import Service from "resource:///com/github/Aylur/ags/service.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import GLib from "gi://GLib";
import { dependencies } from "../utils.js";
const now = () => GLib.DateTime.new_now_local().format("%Y-%m-%d_%H-%M-%S");
class Recorder extends Service {
static {
Service.register(
this,
{},
{
timer: ["int"],
recording: ["boolean"],
},
);
}
#path = GLib.get_home_dir() + "/Videos/Screencasting";
#file = "";
#interval = 0;
recording = false;
timer = 0;
async start() {
if (!dependencies(["slurp", "wf-recorder"])) return;
if (this.recording) return;
const area = await Utils.execAsync("slurp");
Utils.ensureDirectory(this.#path);
this.#file = `${this.#path}/${now()}.mp4`;
Utils.execAsync(["wf-recorder", "-g", area, "-f", this.#file]);
this.recording = true;
this.changed("recording");
this.timer = 0;
this.#interval = Utils.interval(1000, () => {
this.changed("timer");
this.timer++;
});
}
async stop() {
if (!dependencies(["notify-send"])) return;
if (!this.recording) return;
Utils.execAsync("killall -INT wf-recorder");
this.recording = false;
this.changed("recording");
GLib.source_remove(this.#interval);
const res = await Utils.execAsync([
"notify-send",
"-A",
"files=Show in Files",
"-A",
"view=View",
"-i",
"video-x-generic-symbolic",
"Screenrecord",
this.#file,
]);
if (res === "files") Utils.execAsync("xdg-open " + this.#path);
if (res === "view") Utils.execAsync("xdg-open " + this.#file);
}
async screenshot(full = false) {
if (!dependencies(["slurp", "wayshot"])) return;
const path = GLib.get_home_dir() + "/Pictures/Screenshots";
const file = `${path}/${now()}.png`;
Utils.ensureDirectory(path);
await Utils.execAsync(
["wayshot", "-f", file].concat(
full ? [] : ["-s", await Utils.execAsync("slurp")],
),
);
Utils.execAsync(["bash", "-c", `wl-copy < ${file}`]);
const res = await Utils.execAsync([
"notify-send",
"-A",
"files=Show in Files",
"-A",
"view=View",
"-A",
"edit=Edit",
"-i",
file,
"Screenshot",
file,
]);
if (res === "files") Utils.execAsync("xdg-open " + path);
if (res === "view") Utils.execAsync("xdg-open " + file);
if (res === "edit") Utils.execAsync(["swappy", "-f", file]);
App.closeWindow("dashboard");
}
}
export default new Recorder();

View File

@@ -0,0 +1,326 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import RegularWindow from "../misc/RegularWindow.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import icons from "../icons.js";
import { getOptions, getValues } from "./option.js";
import options from "../options.js";
const optionsList = getOptions();
const categories = Array.from(
new Set(optionsList.map((opt) => opt.category)),
).filter((category) => category !== "exclude");
const currentPage = Variable(categories[0]);
const search = Variable("");
const showSearch = Variable(false);
showSearch.connect("changed", ({ value }) => {
if (!value) search.value = "";
});
/** @param {import('./option.js').Opt<string>} opt */
const EnumSetter = (opt) => {
const lbl = Widget.Label({ binds: [["label", opt]] });
const step = (dir = 1) => {
const i = opt.enums.findIndex((i) => i === lbl.label);
opt.setValue(
dir > 0
? i + dir > opt.enums.length - 1
? opt.enums[0]
: opt.enums[i + dir]
: i + dir < 0
? opt.enums[opt.enums.length - 1]
: opt.enums[i + dir],
true,
);
};
const next = Widget.Button({
child: Widget.Icon(icons.ui.arrow.right),
on_clicked: () => step(+1),
});
const prev = Widget.Button({
child: Widget.Icon(icons.ui.arrow.left),
on_clicked: () => step(-1),
});
return Widget.Box({
class_name: "enum-setter",
children: [prev, lbl, next],
});
};
/** @param {import('./option.js').Opt} opt */
const Setter = (opt) => {
switch (opt.type) {
case "number":
return Widget.SpinButton({
setup(self) {
self.set_range(0, 1000);
self.set_increments(1, 5);
},
connections: [
["value-changed", (self) => opt.setValue(self.value, true)],
[opt, (self) => (self.value = opt.value)],
],
});
case "float":
case "object":
return Widget.Entry({
on_accept: (self) => opt.setValue(JSON.parse(self.text || ""), true),
connections: [[opt, (self) => (self.text = JSON.stringify(opt.value))]],
});
case "string":
return Widget.Entry({
on_accept: (self) => opt.setValue(self.text, true),
connections: [[opt, (self) => (self.text = opt.value)]],
});
case "enum":
return EnumSetter(opt);
case "boolean":
return Widget.Switch({
connections: [
["notify::active", (self) => opt.setValue(self.active, true)],
[opt, (self) => (self.active = opt.value)],
],
});
case "img":
return Widget.FileChooserButton({
connections: [
[
"selection-changed",
(self) => {
opt.setValue(self.get_uri()?.replace("file://", ""), true);
},
],
],
});
case "font":
return Widget.FontButton({
show_size: false,
use_size: false,
connections: [
["notify::font", ({ font }) => opt.setValue(font, true)],
[opt, (self) => (self.font = opt.value)],
],
});
default:
return Widget.Label({
label: "no setter with type " + opt.type,
});
}
};
/** @param {import('./option.js').Opt} opt */
const Row = (opt) =>
Widget.Box({
class_name: "row",
setup: (self) => (self.opt = opt),
children: [
Widget.Box({
vertical: true,
vpack: "center",
children: [
opt.title &&
Widget.Label({
xalign: 0,
class_name: "summary",
label: opt.title,
}),
Widget.Label({
xalign: 0,
class_name: "id",
label: `id: "${opt.id}"`,
}),
],
}),
Widget.Box({ hexpand: true }),
Widget.Box({
vpack: "center",
vertical: true,
children: [
Widget.Box({
hpack: "end",
child: Setter(opt),
}),
opt.note &&
Widget.Label({
xalign: 1,
class_name: "note",
label: opt.note,
}),
],
}),
],
});
/** @param {string} category */
const Page = (category) =>
Widget.Scrollable({
vexpand: true,
class_name: "page",
child: Widget.Box({
class_name: "page-content vertical",
vertical: true,
connections: [
[
search,
(self) => {
for (const child of self.children) {
child.visible =
child.opt.id.includes(search.value) ||
child.opt.title.includes(search.value) ||
child.opt.note.includes(search.value);
}
},
],
],
children: optionsList
.filter((opt) => opt.category.includes(category))
.map(Row),
}),
});
const sidebar = Widget.Revealer({
binds: [["reveal-child", search, "value", (v) => !v]],
transition: "slide_right",
child: Widget.Box({
hexpand: false,
vertical: true,
children: [
Widget.Box({
class_name: "sidebar-header",
children: [
Widget.Button({
hexpand: true,
label: icons.dialog.Search + " Search",
on_clicked: () => (showSearch.value = !showSearch.value),
}),
Widget.Button({
hpack: "end",
child: Widget.Icon(icons.ui.info),
on_clicked: () => App.toggleWindow("about"),
}),
],
}),
Widget.Scrollable({
vexpand: true,
hscroll: "never",
child: Widget.Box({
class_name: "sidebar-box vertical",
vertical: true,
children: [
...categories.map((name) =>
Widget.Button({
label: (icons.dialog[name] || "") + " " + name,
xalign: 0,
binds: [
[
"class-name",
currentPage,
"value",
(v) => (v === name ? "active" : ""),
],
],
on_clicked: () => currentPage.setValue(name),
}),
),
],
}),
}),
Widget.Box({
class_name: "sidebar-footer",
child: Widget.Button({
class_name: "copy",
child: Widget.Label({
label: " Save",
xalign: 0,
}),
hexpand: true,
on_clicked: () => {
Utils.execAsync(["wl-copy", getValues()]);
Utils.execAsync([
"notify-send",
"-i",
"preferences-desktop-theme-symbolic",
"Theme copied to clipboard",
'To save it permanently, make a new theme in <span weight="bold">themes.js</span>',
]);
},
}),
}),
],
}),
});
const searchEntry = Widget.Revealer({
transition: "slide_down",
binds: [
["reveal-child", showSearch],
["transition-duration", options.transition],
],
child: Widget.Entry({
connections: [
[
showSearch,
(self) => {
if (!showSearch.value) self.text = "";
if (showSearch.value) self.grab_focus();
},
],
],
hexpand: true,
class_name: "search",
placeholder_text: "Search Options",
secondary_icon_name: icons.apps.search,
on_change: ({ text }) => (search.value = text || ""),
}),
});
const categoriesStack = Widget.Stack({
transition: "slide_left_right",
items: categories.map((name) => [name, Page(name)]),
binds: [
["shown", currentPage],
["visible", search, "value", (v) => !v],
],
});
const searchPage = Widget.Box({
binds: [["visible", search, "value", (v) => !!v]],
child: Page(""),
});
export default RegularWindow({
name: "settings-dialog",
title: "Settings",
setup: (win) => win.set_default_size(800, 500),
connections: [
[
"delete-event",
(win) => {
win.hide();
return true;
},
],
[
"key-press-event",
(self, event) => {
if (event.get_keyval()[1] === imports.gi.Gdk.KEY_Escape) {
self.text = "";
showSearch.setValue(false);
search.setValue("");
}
},
],
],
child: Widget.Box({
children: [
sidebar,
Widget.Box({
vertical: true,
children: [searchEntry, categoriesStack, searchPage],
}),
],
}),
});

View File

@@ -0,0 +1,37 @@
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
export async function globals() {
try {
globalThis.options = (await import("../options.js")).default;
globalThis.iconBrowser = (await import("../misc/IconBrowser.js")).default;
globalThis.app = (
await import("resource:///com/github/Aylur/ags/app.js")
).default;
globalThis.audio = (
await import("resource:///com/github/Aylur/ags/service/audio.js")
).default;
globalThis.recorder = (await import("../services/screenrecord.js")).default;
globalThis.brightness = (await import("../services/brightness.js")).default;
globalThis.indicator = (
await import("../services/onScreenIndicator.js")
).default;
Mpris.players.forEach((player) => {
player.connect("changed", (player) => {
globalThis.mpris = player || Mpris.players[0];
});
});
Mpris.connect("player-added", (mpris, bus) => {
mpris.getPlayer(bus)?.connect("changed", (player) => {
globalThis.mpris = player || Mpris.players[0];
});
});
Mpris.connect("player-closed", () => {
globalThis.mpris = Mpris.players[0];
});
} catch (error) {
logError(error);
}
}

View File

@@ -0,0 +1,76 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import options from "../options.js";
import { readFile, writeFile } from "resource:///com/github/Aylur/ags/utils.js";
const noIgnorealpha = ["verification", "powermenu", "lockscreen"];
/** @param {Array<string>} batch */
function sendBatch(batch) {
const cmd = batch
.filter((x) => !!x)
.map((x) => `keyword ${x}`)
.join("; ");
Hyprland.sendMessage(`[[BATCH]]/${cmd}`);
}
/** @param {string} scss */
function getColor(scss) {
if (scss.includes("#")) return scss.replace("#", "");
if (scss.includes("$")) {
const opt = options
.list()
.find((opt) => opt.scss === scss.replace("$", ""));
return opt?.value.replace("#", "") || "ff0000";
}
}
export function hyprlandInit() {
if (readFile("/tmp/ags/hyprland-init")) return;
sendBatch(
Array.from(App.windows).flatMap(([name]) => [
`layerrule blur, ${name}`,
noIgnorealpha.some((skip) => name.includes(skip))
? ""
: `layerrule ignorealpha 0.6, ${name}`,
]),
);
writeFile("init", "/tmp/ags/hyprland-init");
}
export async function setupHyprland() {
const wm_gaps = Math.floor(
options.hypr.wm_gaps_multiplier.value * options.spacing.value,
);
const border_width = options.border.width.value;
const radii = options.radii.value;
const drop_shadow = options.desktop.drop_shadow.value;
const bar_style = options.bar.style.value;
const bar_pos = options.bar.position.value;
const inactive_border = options.hypr.inactive_border.value;
const accent = getColor(options.theme.accent.accent.value);
const batch = [];
JSON.parse(await Hyprland.sendMessage("j/monitors")).forEach(({ name }) => {
const v = bar_pos === "top" ? `-${wm_gaps},0,0,0` : `0,-${wm_gaps},0,0`;
if (bar_style !== "normal") batch.push(`monitor ${name},addreserved,${v}`);
else batch.push(`monitor ${name},addreserved,0,0,0,0`);
});
batch.push(
`general:border_size ${border_width}`,
`general:gaps_out ${wm_gaps}`,
`general:gaps_in ${Math.floor(wm_gaps / 2)}`,
`general:col.active_border rgba(${accent}ff)`,
`general:col.inactive_border ${inactive_border}`,
`decoration:rounding ${radii}`,
`decoration:drop_shadow ${drop_shadow ? "yes" : "no"}`,
);
sendBatch(batch);
}

View File

@@ -0,0 +1,198 @@
import {
CACHE_DIR,
readFile,
writeFile,
} from "resource:///com/github/Aylur/ags/utils.js";
import { exec } from "resource:///com/github/Aylur/ags/utils.js";
import options from "../options.js";
import Service, { Binding } from "resource:///com/github/Aylur/ags/service.js";
import { reloadScss } from "./scss.js";
import { setupHyprland } from "./hyprland.js";
const CACHE_FILE = CACHE_DIR + "/options.json";
/** object that holds the overriedden values */
let cacheObj = JSON.parse(readFile(CACHE_FILE) || "{}");
/**
* @template T
* @typedef {Object} OptionConfig
* @property {string=} scss - name of scss variable set to "exclude" to not include it in the generated scss file
* @property {string=} unit - scss unit on numbers, default is "px"
* @property {string=} title
* @property {string=} note
* @property {string=} category
* @property {boolean=} noReload - don't reload css & hyprland on change
* @property {boolean=} persist - ignore reset call
* @property {'object' | 'string' | 'img' | 'number' | 'float' | 'font' | 'enum' =} type
* @property {Array<string> =} enums
* @property {(value: T) => any=} format
* @property {(value: T) => any=} scssFormat
*/
/** @template T */
export class Opt extends Service {
static {
Service.register(
this,
{},
{
value: ["jsobject"],
},
);
}
#value;
#scss = "";
unit = "px";
noReload = false;
persist = false;
id = "";
title = "";
note = "";
type = "";
category = "";
/** @type {Array<string>} */
enums = [];
/** @type {(v: T) => any} */
format = (v) => v;
/** @type {(v: T) => any} */
scssFormat = (v) => v;
/**
* @param {T} value
* @param {OptionConfig<T> =} config
*/
constructor(value, config) {
super();
this.#value = value;
this.defaultValue = value;
this.type = typeof value;
if (config) Object.keys(config).forEach((c) => (this[c] = config[c]));
import("../options.js").then(this.#init.bind(this));
}
set scss(scss) {
this.#scss = scss;
}
get scss() {
return this.#scss || this.id.split(".").join("-").split("_").join("-");
}
#init() {
getOptions(); // sets the ids as a side effect
if (cacheObj[this.id] !== undefined) this.setValue(cacheObj[this.id]);
const words = this.id
.split(".")
.flatMap((w) => w.split("_"))
.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
this.title ||= words.join(" ");
this.category ||= words.length === 1 ? "General" : words.at(0) || "General";
this.connect("changed", () => {
cacheObj[this.id] = this.value;
writeFile(JSON.stringify(cacheObj, null, 2), CACHE_FILE);
});
}
get value() {
return this.#value;
}
set value(value) {
this.setValue(value);
}
/** @param {T} value */
setValue(value, reload = false) {
if (typeof value !== typeof this.defaultValue) {
console.error(
Error(
`WrongType: Option "${this.id}" can't be set to ${value}, ` +
`expected "${typeof this.defaultValue}", but got "${typeof value}"`,
),
);
return;
}
if (this.value !== value) {
this.#value = this.format(value);
this.changed("value");
if (reload && !this.noReload) {
reloadScss();
setupHyprland();
}
}
}
reset(reload = false) {
if (!this.persist) this.setValue(this.defaultValue, reload);
}
}
/**
* @template T
* @param {T} value
* @param {OptionConfig<T> =} config
* @returns {Opt<T>}
*/
export function Option(value, config) {
return new Opt(value, config);
}
/** @returns {Array<Opt<any>>} */
export function getOptions(object = options, path = "") {
return Object.keys(object).flatMap((key) => {
/** @type Option<any> */
const obj = object[key];
const id = path ? path + "." + key : key;
if (obj instanceof Opt) {
obj.id = id;
return obj;
}
if (typeof obj === "object") return getOptions(obj, id);
return [];
});
}
export function resetOptions() {
exec(`rm -rf ${CACHE_FILE}`);
cacheObj = {};
getOptions().forEach((opt) => opt.reset());
}
export function getValues() {
const obj = {};
for (const opt of getOptions()) {
if (opt.category !== "exclude") obj[opt.id] = opt.value;
}
return JSON.stringify(obj, null, 2);
}
/** @param {string | object} config */
export function apply(config) {
const options = getOptions();
const settings = typeof config === "string" ? JSON.parse(config) : config;
for (const id of Object.keys(settings)) {
const opt = options.find((opt) => opt.id === id);
if (!opt) {
print(`No option with id: "${id}"`);
continue;
}
opt.setValue(settings[id]);
}
}

View File

@@ -0,0 +1,65 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import { getOptions } from "./option.js";
import { dependencies } from "../utils.js";
export function scssWatcher() {
return Utils.subprocess(
[
"inotifywait",
"--recursive",
"--event",
"create,modify",
"-m",
App.configDir + "/scss",
],
reloadScss,
() => print("missing dependancy for css hotreload: inotify-tools"),
);
}
/**
* generate an scss file that makes every option available as a variable
* based on the passed scss parameter or the path in the object
*
* e.g
* options.bar.style.value => $bar-style
*/
export async function reloadScss() {
if (!dependencies(["sassc"])) return;
const opts = getOptions();
const vars = opts.map((opt) => {
if (opt.scss === "exclude") return "";
const unit = typeof opt.value === "number" ? opt.unit : "";
const value = opt.scssFormat ? opt.scssFormat(opt.value) : opt.value;
return `$${opt.scss}: ${value}${unit};`;
});
const bar_style = opts.find((opt) => opt.id === "bar.style")?.value || "";
const additional =
bar_style === "normal"
? "//"
: `
window#quicksettings .window-content {
margin-right: $wm-gaps;
}
`;
try {
const tmp = "/tmp/ags/scss";
Utils.ensureDirectory(tmp);
await Utils.writeFile(vars.join("\n"), `${tmp}/options.scss`);
await Utils.writeFile(additional, `${tmp}/additional.scss`);
await Utils.execAsync(
`sassc ${App.configDir}/scss/main.scss ${tmp}/style.css`,
);
App.resetCss();
App.applyCss(`${tmp}/style.css`);
} catch (error) {
if (error instanceof Error) console.error(error.message);
if (typeof error === "string") console.error(error);
}
}

View File

@@ -0,0 +1,114 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import options from "../options.js";
import icons from "../icons.js";
import { reloadScss, scssWatcher } from "./scss.js";
import { wallpaper } from "./wallpaper.js";
import { hyprlandInit, setupHyprland } from "./hyprland.js";
import { globals } from "./globals.js";
import { showAbout } from "../about/about.js";
import Gtk from "gi://Gtk";
export function init() {
notificationBlacklist();
warnOnLowBattery();
globals();
tmux();
gsettigsColorScheme();
gtkFontSettings();
scssWatcher();
dependandOptions();
reloadScss();
hyprlandInit();
setupHyprland();
wallpaper();
showAbout();
}
function dependandOptions() {
options.bar.style.connect("changed", ({ value }) => {
if (value !== "normal")
options.desktop.screen_corners.setValue(false, true);
});
}
function tmux() {
if (!Utils.exec("which tmux")) return;
/** @param {string} scss */
function getColor(scss) {
if (scss.includes("#")) return scss;
if (scss.includes("$")) {
const opt = options
.list()
.find((opt) => opt.scss === scss.replace("$", ""));
return opt?.value;
}
}
options.theme.accent.accent.connect("changed", ({ value }) =>
Utils.execAsync(`tmux set @main_accent ${getColor(value)}`).catch((err) =>
console.error(err.message),
),
);
}
function gsettigsColorScheme() {
if (!Utils.exec("which gsettings")) return;
options.theme.scheme.connect("changed", ({ value }) => {
const gsettings = "gsettings set org.gnome.desktop.interface color-scheme";
Utils.execAsync(`${gsettings} "prefer-${value}"`).catch((err) =>
console.error(err.message),
);
});
}
function gtkFontSettings() {
const settings = Gtk.Settings.get_default();
if (!settings) {
console.error(Error("Gtk.Settings unavailable"));
return;
}
const callback = () => {
const { size, font } = options.font;
settings.gtk_font_name = `${font.value} ${size.value}`;
};
options.font.font.connect("notify::value", callback);
options.font.size.connect("notify::value", callback);
}
function notificationBlacklist() {
Notifications.connect("notified", (_, id) => {
const n = Notifications.getNotification(id);
options.notifications.black_list.value.forEach((item) => {
if (n?.app_name.includes(item) || n?.app_entry?.includes(item)) n.close();
});
});
}
function warnOnLowBattery() {
Battery.connect("notify::percent", () => {
const low = options.battery.low.value;
if (
Battery.percent !== low ||
Battery.percent !== low / 2 ||
!Battery.charging
)
return;
Utils.execAsync([
"notify-send",
`${Battery.percent}% Battery Percentage`,
"-i",
icons.battery.warning,
"-u",
"critical",
]);
});
}

View File

@@ -0,0 +1,55 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import options from "../options.js";
import themes from "../themes.js";
import { reloadScss } from "./scss.js";
import { setupHyprland } from "./hyprland.js";
import { wallpaper } from "./wallpaper.js";
/** @param {string} name */
export function setTheme(name) {
options.reset();
const theme = themes.find((t) => t.name === name);
if (!theme) return print("No theme named " + name);
options.apply(theme.options);
reloadScss();
setupHyprland();
wallpaper();
}
export const WP = App.configDir + "/assets/";
export const lightColors = {
"theme.scheme": "light",
"color.red": "#e55f86",
"color.green": "#00D787",
"color.yellow": "#EBFF71",
"color.blue": "#51a4e7",
"color.magenta": "#9077e7",
"color.teal": "#51e6e6",
"color.orange": "#E79E64",
"theme.bg": "#fffffa",
"theme.fg": "#141414",
};
export const Theme = ({ name, icon = " ", ...options }) => ({
name,
icon,
options: {
"theme.name": name,
"theme.icon": icon,
...options,
},
});
let settingsDialog;
export async function openSettings() {
if (settingsDialog) return settingsDialog.present();
try {
settingsDialog = (await import("./SettingsDialog.js")).default;
settingsDialog.present();
} catch (error) {
if (error instanceof Error) console.error(error.message);
}
}

View File

@@ -0,0 +1,25 @@
import options from "../options.js";
import { exec, execAsync } from "resource:///com/github/Aylur/ags/utils.js";
import { dependencies } from "../utils.js";
export function initWallpaper() {
if (dependencies(["swww"])) {
exec("swww init");
options.desktop.wallpaper.img.connect("changed", wallpaper);
}
}
export function wallpaper() {
if (!dependencies(["swww"])) return;
execAsync([
"swww",
"img",
"--transition-type",
"grow",
"--transition-pos",
exec("hyprctl cursorpos").replace(" ", ""),
options.desktop.wallpaper.img.value,
]).catch((err) => console.error(err));
}

View File

@@ -0,0 +1,66 @@
/**
* A Theme is a set of options that will be applied
* ontop of the default values. see options.js for possible options
*/
import { Theme, WP, lightColors } from "./settings/theme.js";
export default [
Theme({
name: "Kitty Dark",
icon: "󰄛",
"desktop.wallpaper.img": WP + "kittybl.jpeg",
}),
Theme({
name: "Kitty Light",
icon: "󰄛",
"desktop.wallpaper.img": WP + "kitty.jpeg",
...lightColors,
"theme.widget.bg": "$accent",
"theme.widget.opacity": 64,
}),
Theme({
name: "Leaves",
icon: "󰌪",
"desktop.wallpaper.img": WP + "leaves.jpg",
"theme.accent.accent": "$green",
"theme.accent.gradient": "to right, $accent, darken($accent, 14%)",
"theme.widget.opacity": 92,
"border.opacity": 86,
"theme.bg": "transparentize(#171717, 0.3)",
"bar.style": "floating",
radii: 0,
}),
Theme({
name: "Ivory",
icon: "󰟆",
...lightColors,
"desktop.wallpaper.img": WP + "ivory.png",
"desktop.wallpaper.fg": "$bg_color",
"desktop.screen_corners": false,
"bar.style": "separated",
"theme.widget.bg": "$accent",
"theme.widget.opacity": 64,
"desktop.drop_shadow": false,
"border.width": 2,
"border.opacity": 0,
"theme.accent.gradient": "to right, $accent, darken($accent, 6%)",
"hypr.inactive_border": "rgba(111111FF)",
"bar.separators": false,
}),
Theme({
name: "Space",
icon: "",
"desktop.wallpaper.img": WP + "space.jpg",
spacing: 11,
padding: 10,
radii: 12,
"theme.accent.accent": "$magenta",
"desktop.screen_corners": false,
"desktop.clock.enable": false,
"bar.separators": false,
"bar.icon": "",
"theme.bg": "transparentize(#171717, 0.3)",
"theme.widget.opacity": 95,
"bar.flat_buttons": false,
}),
];

View File

@@ -0,0 +1,99 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import cairo from "cairo";
import icons from "./icons.js";
import Gdk from "gi://Gdk";
import GLib from "gi://GLib";
/**
* @param {number} length
* @param {number=} start
* @returns {Array<number>}
*/
export function range(length, start = 1) {
return Array.from({ length }, (_, i) => i + start);
}
/**
* @param {Array<[string, string] | string[]>} collection
* @param {string} item
* @returns {string}
*/
export function substitute(collection, item) {
return collection.find(([from]) => from === item)?.[1] || item;
}
/**
* @param {(monitor: number) => any} widget
* @returns {Array<import('types/widgets/window').default>}
*/
export function forMonitors(widget) {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
return range(n, 0).map(widget).flat(1);
}
/**
* @param {import('gi://Gtk').Gtk.Widget} widget
* @returns {any} - missing cairo type
*/
export function createSurfaceFromWidget(widget) {
const alloc = widget.get_allocation();
const surface = new cairo.ImageSurface(
cairo.Format.ARGB32,
alloc.width,
alloc.height,
);
const cr = new cairo.Context(surface);
cr.setSourceRGBA(255, 255, 255, 0);
cr.rectangle(0, 0, alloc.width, alloc.height);
cr.fill();
widget.draw(cr);
return surface;
}
/** @param {string} icon */
export function getAudioTypeIcon(icon) {
const substitues = [
["audio-headset-bluetooth", icons.audio.type.headset],
["audio-card-analog-usb", icons.audio.type.speaker],
["audio-card-analog-pci", icons.audio.type.card],
];
return substitute(substitues, icon);
}
/** @param {import('types/service/applications').Application} app */
export function launchApp(app) {
Utils.execAsync(["hyprctl", "dispatch", "exec", `sh -c ${app.executable}`]);
app.frequency += 1;
}
/** @param {Array<string>} bins */
export function dependencies(bins) {
const deps = bins.map((bin) => {
const has = Utils.exec(`which ${bin}`);
if (!has) print(`missing dependency: ${bin}`);
return !!has;
});
return deps.every((has) => has);
}
/** @param {string} img - path to an img file */
export function blurImg(img) {
const cache = Utils.CACHE_DIR + "/media";
return new Promise((resolve) => {
if (!img) resolve("");
const dir = cache + "/blurred";
const blurred = dir + img.substring(cache.length);
if (GLib.file_test(blurred, GLib.FileTest.EXISTS)) return resolve(blurred);
Utils.ensureDirectory(dir);
Utils.execAsync(["convert", img, "-blur", "0x22", blurred])
.then(() => resolve(blurred))
.catch(() => resolve(""));
});
}

View File

@@ -0,0 +1,88 @@
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import GLib from "gi://GLib";
import options from "./options.js";
const intval = options.systemFetchInterval;
export const uptime = Variable("", {
poll: [
60_000,
"cat /proc/uptime",
(line) => {
const uptime = Number.parseInt(line.split(".")[0]) / 60;
if (uptime > 18 * 60) return "Go Sleep";
const h = Math.floor(uptime / 60);
const s = Math.floor(uptime % 60);
return `${h}:${s < 10 ? "0" + s : s}`;
},
],
});
export const distro = GLib.get_os_info("ID");
export const distroIcon = (() => {
switch (distro) {
case "fedora":
return "";
case "arch":
return "";
case "nixos":
return "";
case "debian":
return "";
case "opensuse-tumbleweed":
return "";
case "ubuntu":
return "";
case "endeavouros":
return "";
default:
return "";
}
})();
/** @type {function([string, string] | string[]): number} */
const divide = ([total, free]) =>
Number.parseInt(free) / Number.parseInt(total);
export const cpu = Variable(0, {
poll: [
intval,
"top -b -n 1",
(out) =>
divide([
"100",
out
.split("\n")
.find((line) => line.includes("Cpu(s)"))
?.split(/\s+/)[1]
.replace(",", ".") || "0",
]),
],
});
export const ram = Variable(0, {
poll: [
intval,
"free",
(out) =>
divide(
out
.split("\n")
.find((line) => line.includes("Mem:"))
?.split(/\s+/)
.splice(1, 2) || ["1", "1"],
),
],
});
export const temp = Variable(0, {
poll: [
intval,
"cat " + options.temperature,
(n) => {
return Number.parseInt(n) / 100_000;
},
],
});

Some files were not shown because too many files have changed in this diff Show More