refactor(ags): move wim and binto configs together under modules

This commit is contained in:
matt1432 2024-01-17 19:18:41 -05:00
parent 39f98b657b
commit a0014161ce
108 changed files with 123 additions and 4164 deletions

View file

@ -0,0 +1,146 @@
{
"env": {
"es2021": true
},
"extends": "eslint:recommended",
"parser": "@typescript-eslint/parser",
"overrides": [],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@stylistic", "@typescript-eslint"],
"rules": {
"array-callback-return": ["error", {
"allowImplicit": true,
"checkForEach": true
}],
"no-constructor-return": ["error"],
"no-unreachable-loop": ["error", { "ignore": [
"ForInStatement", "ForOfStatement"
]}],
"no-use-before-define": ["error", {
"functions": false
}],
"block-scoped-var": ["error"],
"capitalized-comments": ["warn", "always", {
"ignoreConsecutiveComments": true
}],
"class-methods-use-this": ["error"],
"curly": ["warn"],
"default-case-last": ["warn"],
"default-param-last": ["error"],
"eqeqeq": ["error", "smart"],
"func-names": ["warn", "never"],
"func-style": ["warn", "expression"],
"logical-assignment-operators": ["warn", "always"],
"no-array-constructor": ["error"],
"no-empty-function": ["warn"],
"no-empty-static-block": ["warn"],
"no-extend-native": ["error"],
"no-extra-bind": ["warn"],
"no-implicit-coercion": ["warn"],
"no-iterator": ["error"],
"no-labels": ["error"],
"no-lone-blocks": ["error"],
"no-lonely-if": ["error"],
"no-loop-func": ["error"],
"no-magic-numbers": ["error", {
"ignore": [-1, 0.1, 0, 1, 2, 3, 10, 33, 66, 100, 255, 360, 450, 1000],
"ignoreDefaultValues": true
}],
"no-multi-assign": ["error"],
"no-new": ["error"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],
"no-object-constructor": ["error"],
"no-proto": ["error"],
"no-return-assign": ["error"],
"no-sequences": ["error"],
"no-shadow": ["error", { "builtinGlobals": true }],
"no-undef-init": ["warn"],
"no-undefined": ["error"],
"no-useless-constructor": ["warn"],
"no-useless-escape": ["off"],
"no-useless-return": ["error"],
"no-var": ["error"],
"no-void": ["error"],
"no-with": ["error"],
"object-shorthand": ["error", "always"],
"one-var": ["error", "never"],
"operator-assignment": ["warn", "always"],
"prefer-arrow-callback": ["error"],
"prefer-const": ["error"],
"prefer-object-has-own": ["error"],
"prefer-regex-literals": ["error"],
"prefer-template": ["warn"],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@stylistic/array-bracket-newline": ["warn", "consistent"],
"@stylistic/array-bracket-spacing": ["warn", "never"],
"@stylistic/arrow-parens": ["warn", "always"],
"@stylistic/brace-style": ["warn", "stroustrup"],
"@stylistic/comma-dangle": ["warn", "always-multiline"],
"@stylistic/comma-spacing": ["warn", { "before": false, "after": true }],
"@stylistic/comma-style": ["error", "last"],
"@stylistic/dot-location": ["error", "property"],
"@stylistic/function-call-argument-newline": ["warn", "consistent"],
"@stylistic/function-paren-newline": ["warn", "consistent"],
"@stylistic/indent": ["warn", 4, {
"SwitchCase": 1,
"ignoreComments": true
}],
"@stylistic/key-spacing": ["warn", { "beforeColon": false, "afterColon": true }],
"@stylistic/keyword-spacing": ["warn", { "before": true }],
"@stylistic/linebreak-style": ["error", "unix"],
"@stylistic/lines-between-class-members": ["warn", "always", { "exceptAfterSingleLine": true }],
"@stylistic/max-len": ["warn", {
"code": 80,
"ignoreComments": true,
"ignoreTrailingComments": true,
"ignoreUrls": true
}],
"@stylistic/multiline-ternary": ["warn", "always-multiline"],
"@stylistic/new-parens": ["error"],
"@stylistic/no-mixed-operators": ["warn"],
"@stylistic/no-mixed-spaces-and-tabs": ["error"],
"@stylistic/no-multi-spaces": ["error"],
"@stylistic/no-tabs": ["error"],
"@stylistic/no-trailing-spaces": ["error"],
"@stylistic/no-whitespace-before-property": ["warn"],
"@stylistic/nonblock-statement-body-position": ["error", "below"],
"@stylistic/object-curly-newline": ["warn", { "consistent": true }],
"@stylistic/object-curly-spacing": ["warn", "always"],
"@stylistic/operator-linebreak": ["warn", "after"],
"@stylistic/padded-blocks": ["error", "never"],
"@stylistic/padding-line-between-statements": ["warn",
{ "blankLine": "always", "prev": "*", "next": "return" },
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
{ "blankLine": "always", "prev": ["case", "default"], "next": "*" }
],
"@stylistic/quote-props": ["error", "consistent-as-needed"],
"@stylistic/quotes": ["error", "single", { "avoidEscape": true }],
"@stylistic/semi": ["error", "always"],
"@stylistic/semi-spacing": ["warn"],
"@stylistic/space-before-blocks": ["warn"],
"@stylistic/space-before-function-paren": ["warn", "never"],
"@stylistic/space-infix-ops": ["warn"],
"@stylistic/spaced-comment": ["warn", "always"],
"@stylistic/switch-colon-spacing": ["warn"],
"@stylistic/wrap-regex": ["warn"]
},
"globals": {
"ARGV": "readonly",
"imports": "readonly",
"print": "readonly",
"console": "readonly",
"logError": "readonly",
"setTimeout": "readonly",
"setInterval": "readonly"
}
}

View file

@ -0,0 +1,17 @@
extends: stylelint-config-standard-scss
ignoreFiles:
- "**/*.js"
- "**/*.ts"
rules:
selector-type-no-unknown: null
selector-class-pattern: 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
no-invalid-position-at-import-rule: null
font-family-no-missing-generic-family-keyword: null

View file

@ -0,0 +1,33 @@
import Pointers from './services/pointers.ts';
import AppLauncher from './ts/applauncher/main.ts';
import Bar from './ts/bar/binto.ts';
import { NotifPopups, NotifCenter } from './ts/notifications/binto.ts';
import Powermenu from './ts/powermenu.ts';
const closeWinDelay = 800;
// TODO: add OSD, workspace indicator / overview and current window indicator
export default {
notificationPopupTimeout: 5000,
cacheNotificationActions: true,
onConfigParsed: () => {
globalThis.Pointers = Pointers;
},
closeWindowDelay: {
'applauncher': closeWinDelay,
'notification-center': closeWinDelay,
'powermenu': closeWinDelay,
},
windows: [
AppLauncher(),
NotifCenter(),
Powermenu(),
Bar(),
NotifPopups(),
],
};

View file

@ -0,0 +1,9 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -3 14 14" id="meteor-icon-kit__regular-chevron-down-s" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.70711 0.29289C1.31658 -0.09763 0.68342 -0.09763 0.29289 0.29289C-0.09763 0.68342 -0.09763 1.3166 0.29289 1.7071L6.2929 7.7071C6.6834 8.0976 7.3166 8.0976 7.7071 7.7071L13.7071 1.7071C14.0976 1.3166 14.0976 0.68342 13.7071 0.29289C13.3166 -0.09763 12.6834 -0.09763 12.2929 0.29289L7 5.5858L1.70711 0.29289z" fill="#ffffff"/>
</g>
</svg>

After

(image error) Size: 879 B

View file

@ -0,0 +1,41 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import { execAsync, monitorFile } from 'resource:///com/github/Aylur/ags/utils.js';
/** @param {string} host */
const watchAndCompileSass = (host) => {
const reloadCss = () => {
const scss = `${App.configDir}/scss/${host}.scss`;
const css = '/tmp/ags/style.css';
execAsync(`sassc ${scss} ${css}`).then(() => {
App.resetCss();
App.applyCss(css);
}).catch(print);
};
monitorFile(
`${App.configDir}/scss`,
reloadCss,
'directory',
);
reloadCss();
};
/** @param {string} host */
export const transpileTypeScript = async(host) => {
await execAsync([
'bun', 'build', `${App.configDir}/${host}.ts`,
'--outdir', '/tmp/ags',
'--external', 'resource:///*',
'--external', 'gi://*',
'--external', 'cairo',
'--external', '*/fzf.es.js',
]).catch(print);
watchAndCompileSass(host);
// The file is going to be there after transpilation
// @ts-ignore
return await import('file:///tmp/ags/main.js');
};

3667
modules/ags/config/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{
"main": "config.js",
"dependencies": {
"fzf": "^0.5.2"
},
"devDependencies": {
"eslint": "^8.52.0",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"stylelint-config-standard-scss": "^11.0.0",
"@stylistic/eslint-plugin": "^1.4.0",
"@girs/dbusmenugtk3-0.4": "^0.4.0-3.2.0",
"@girs/gobject-2.0": "^2.76.1-3.2.3",
"@girs/gtk-3.0": "^3.24.39-3.2.2",
"@girs/gvc-1.0": "^1.0.0-3.1.0",
"@girs/nm-1.0": "^1.43.1-3.1.0"
}
}

View file

@ -0,0 +1,118 @@
.applauncher {
all: unset;
border: 2px solid $contrast-bg;
background-color: $bg;
color: #f8f8f2;
padding: 2px;
* {
font-size: 16px;
}
list, row {
all: unset;
}
.header {
margin: 16.2px;
margin-bottom: 0;
image, entry {
all: unset;
border-radius: 9px;
color: #f8f8f2;
background-color: rgba(#44475a, 0.6);
border: 1px solid #44475a;
padding: 4.5px;
}
image {
margin-right: 9px;
-gtk-icon-transform: scale(0.8);
font-size: 25.6px;
}
}
scrolledwindow {
padding: 10px;
padding-bottom: 0;
min-width: 900px;
min-height: 650px;
scrollbar, scrollbar * {
all: unset;
}
scrollbar.vertical {
transition: 200ms;
background-color: rgba(23, 23, 23, 0.3);
margin: 20px 0;
&:hover {
background-color: rgba(23, 23, 23, 0.7);
slider {
background-color: rgba(238, 238, 238, 0.7);
min-width: .6em;
}
}
slider {
background-color: rgba(238, 238, 238, 0.5);
border-radius: 9px;
min-width: .4em;
min-height: 2em;
transition: 200ms;
}
}
}
.placeholder {
margin-top: 9px;
color: #f8f8f2;
font-size: 1.2em;
}
.app {
all: unset;
transition: 200ms;
border-radius: 9px;
label {
transition: 200ms;
&.title {
margin-top: 20px;
color: #f8f8f2;
}
&.description {
color: rgba(238, 238, 238, 0.7);
}
}
image {
transition: 200ms;
margin: 0 8px;
}
&:active {
background-color: rgba($contrast-bg, 0.5);
box-shadow: inset 0 0 0 3px rgba(238, 238, 238, 0.03);
}
}
*:selected, .app:hover, .app:focus {
* {
font-weight: unset;
}
label.title {
color: $contrast-bg;
}
image {
-gtk-icon-shadow: 2px 2px $contrast-bg;
}
}
}

View file

@ -0,0 +1,23 @@
.bar {
.clock, .notif-panel {
padding: 4.5px 7px;
background-color: $bgfull;
}
.sys-tray {
menubar {
background-color: $bgfull;
padding: 2.5px;
}
menuitem {
image { color: #CBA6F7; }
padding: 0 2px;
* {
font-size: 25px;
}
}
}
}

View file

@ -0,0 +1,108 @@
.notification-center {
min-height: 700px;
min-width: 524px;
background: $bg;
padding: 0;
* {
font-size: 16px;
}
.header {
padding: 10px;
margin-top: 22px;
margin-bottom: 9px;
label {
font-size: 22px;
}
.clear {
box {
all: unset;
transition: 200ms;
color: #eee;
background-color: #664C90;
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
padding: 4.5px 9px;
}
&.hover box {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: rgba(238, 238, 238, 0.154);
color: #f1f1f1;
}
&.disabled box {
box-shadow: none;
background-color: rgba(#664C90, 0.3);
color: rgba(238, 238, 238, 0.3);
}
label {
font-size: 1.2em;
}
}
}
.notification-list-box {
background: $bgfull;
padding: 0 12px;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
scrollbar {
all: unset;
border-radius: 8px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
* {
all: unset;
}
&:hover {
border-radius: 8px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
scrollbar.vertical {
transition: 200ms;
background-color: rgba(23, 23, 23, 0.3);
&:hover {
background-color: rgba(23, 23, 23, 0.7);
slider {
background-color: rgba(238, 238, 238, 0.7);
min-width: .6em;
}
}
slider {
background-color: rgba(238, 238, 238, 0.5);
border-radius: 9px;
min-width: .4em;
min-height: 2em;
transition: 200ms;
}
}
}
.placeholder {
color: white;
image {
font-size: 7em;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
-gtk-icon-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
}
label {
font-size: 1.2em;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
-gtk-icon-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
}
}
}

View file

@ -0,0 +1,206 @@
$background-color-1: rgba(238, 238, 238, 0.154);
$background-color-2: rgba(230, 112, 144, 0.5);
$background-color-3: rgba(238, 238, 238, 0.06);
$background-color-4: #51a4e7;
$background-color-5: transparent;
$background-color-6: #171717;
$background-color-7: rgba(23, 23, 23, 0.3);
$background-color-8: rgba(23, 23, 23, 0.7);
$background-color-9: rgba(238, 238, 238, 0.7);
$background-color-10: rgba(238, 238, 238, 0.5);
.notification.critical {
>box {
box-shadow: inset 0 0 0.5em 0 #e67090;
}
}
.notification {
>box {
all: unset;
box-shadow: 0 0 4.5px 0 rgba(0, 0, 0, 0.4);
margin: 9px 9px 0;
background-color: $bg;
padding: 16.2px;
* {
font-size: 16px;
}
}
&:hover {
.close-button {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-1;
background-color: $background-color-2;
}
}
.title {
margin-right: 9px;
font-size: 1.1em;
}
.description {
font-size: .9em;
min-width: 350px;
}
.icon {
margin-right: 9px;
}
.icon.img {
border: 1px solid rgba(238, 238, 238, 0.03);
}
.actions {
button {
all: unset;
transition: all 500ms;
background-color: $background-color-3;
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
font-size: 1.2em;
padding: 4.5px 9px;
margin: 9px 4.5px 0;
* {
font-size: 16px;
}
&:focus {
box-shadow: inset 0 0 0 1px #51a4e7;
background-color: $background-color-1;
}
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-1;
}
&:active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:checked {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:disabled {
box-shadow: none;
background-color: $background-color-5;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
button.on {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
button.active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
}
button.close-button {
all: unset;
transition: all 500ms;
background-color: $background-color-5;
background-image: none;
box-shadow: none;
margin-left: 9px;
min-width: 1.2em;
min-height: 1.2em;
* {
font-size: 16px;
}
&:focus {
box-shadow: inset 0 0 0 1px #51a4e7;
background-color: $background-color-1;
}
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-1;
background-color: $background-color-2;
}
&:active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-4;
background-image: linear-gradient(#e67090, #e67090);
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:checked {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:disabled {
box-shadow: none;
background-color: $background-color-5;
}
}
button.close-button.on {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
button.close-button.active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
}

View file

@ -0,0 +1,34 @@
.powermenu {
background-color: $bg;
color: $fg;
padding: 10px;
font-family: "MesloLGS NF";
/* font-family: Iosevka Nerd Font; */
font-size: 70px;
border: 2px solid $contrast-bg;
label {
min-width: 140px;
min-height: 130px;
}
.button {
margin: 5px 10px;
min-width: 80px;
transition: all ease .2s;
&:hover { background-color: $bg-secondary; }
&:active { background-color: $bg-secondary; }
.content {
padding: 0 15px;
}
}
.shutdown { color: $red; }
.reboot { color: $magenta; }
.logout { color: $yellow; }
}
.powermenu-clickhandler {
background-color: black;
}

View file

@ -0,0 +1,16 @@
window,
button,
eventbox,
box,
progressbar,
trough,
undershoot {
all: unset;
}
@import "./common";
@import './binto-widgets/applauncher';
@import './binto-widgets/bar';
@import './binto-widgets/notification';
@import './binto-widgets/notification-center';
@import './binto-widgets/powermenu';

View file

@ -0,0 +1,33 @@
$darkbg: #0b0d16;
$bg: rgba(40, 42, 54, 0.8); // rgba(69, 71, 90, 0.3); #0d0f18;
$bgfull: rgb(40, 42, 54);
$contrast-bg: rgba(189, 147, 249, 0.8);
$contrast-bg-full: rgba(189, 147, 249, 1);
$bg-secondary: rgba(#382c4a, 0.8);
$bg-secondary-alt: #a5b6cf;
$fg: #a5b6cf;
$fg-dim: #a5b6cf;
$watermelon: #dd6777;
// Aliases
$background: $bg;
$background-secondary: $bg-secondary;
$background-secondary-alt: $bg-secondary-alt;
$foreground: $fg;
$foreground-dim: $fg-dim;
$black: #151720;
$dimblack: #1a1c25;
$lightblack: #262831;
$red: #dd6777;
$blue: #86aaec;
$cyan: #93cee9;
$blue-desaturated: #93cee9;
$magenta: #c296eb;
$purple: #c296eb;
$green: #90ceaa;
$aquamarine: #90ceaa;
$yellow: #ecd3a0;
$accent: $blue;
$javacafe-magenta: #c296eb;
$javacafe-blue: #86aaec;

View file

@ -0,0 +1,118 @@
.applauncher {
all: unset;
border: 2px solid $contrast-bg;
border-radius: 25px;
background-color: $bg;
color: #f8f8f2;
padding: 2px;
* {
font-size: 16px;
}
list, row {
all: unset;
}
.header {
margin: 16.2px;
margin-bottom: 0;
image, entry {
all: unset;
border-radius: 9px;
color: #f8f8f2;
background-color: rgba(#44475a, 0.6);
border: 1px solid #44475a;
padding: 4.5px;
}
image {
margin-right: 9px;
-gtk-icon-transform: scale(0.8);
font-size: 25.6px;
}
}
scrolledwindow {
padding: 10px;
padding-bottom: 0;
min-width: 700px;
min-height: 450px;
scrollbar, scrollbar * {
all: unset;
}
scrollbar.vertical {
transition: 200ms;
background-color: rgba(23, 23, 23, 0.3);
margin: 20px 0;
&:hover {
background-color: rgba(23, 23, 23, 0.7);
slider {
background-color: rgba(238, 238, 238, 0.7);
min-width: .6em;
}
}
slider {
background-color: rgba(238, 238, 238, 0.5);
border-radius: 9px;
min-width: .4em;
min-height: 2em;
transition: 200ms;
}
}
}
.placeholder {
margin-top: 9px;
color: #f8f8f2;
font-size: 1.2em;
}
.app {
all: unset;
transition: 200ms;
border-radius: 9px;
label {
transition: 200ms;
&.title {
color: #f8f8f2;
}
&.description {
color: rgba(238, 238, 238, 0.7);
}
}
image {
transition: 200ms;
margin: 0 8px;
}
&:active {
background-color: rgba($contrast-bg, 0.5);
box-shadow: inset 0 0 0 3px rgba(238, 238, 238, 0.03);
}
}
*:selected, .app:hover, .app:focus {
* {
font-weight: unset;
}
label.title {
color: $contrast-bg;
}
image {
-gtk-icon-shadow: 2px 2px $contrast-bg;
}
}
}

View file

@ -0,0 +1,75 @@
.date {
background-color: $bg;
color: $fg;
border-radius: 30px;
border: 2px solid $contrast-bg;
}
.timebox {
margin: 30px 0;
.time-container {
.content {
font-weight: bolder;
font-size: 60px;
}
.divider {
margin: 8px 15px;
padding: 0 1px;
background: linear-gradient($red, $magenta, $blue, $cyan);
}
}
.date-container {
margin-top: 2px;
}
}
.cal-box {
border-radius: 30px;
padding: 0 1rem .2rem;
color: $fg;
background-color: $bgfull;
border-bottom: 2px solid $contrast-bg;
border-top: 2px solid $contrast-bg;
margin: 0 12px 18px;
.cal {
font-size: 20px;
background-color: inherit;
padding: .5rem .10rem 0;
margin-left: 10px;
border-radius: 30px;
& > * {
border: solid 0 transparent;
}
&.highlight {
padding: 10rem;
}
}
}
calendar:selected {
color: $cyan;
}
calendar.header {
color: $cyan;
font-weight: bold;
}
calendar.button {
color: $cyan;
}
calendar.highlight {
color: $green;
font-weight: bold;
}
calendar:indeterminate {
color: $lightblack;
}

View file

@ -0,0 +1,114 @@
.notification-center {
min-height: 700px;
min-width: 524px;
background: $bg;
border-radius: 30px;
border-top-right-radius: 0;
border: 2px solid $contrast-bg;
padding: 0;
* {
font-size: 16px;
}
.header {
padding: 10px;
margin-top: 22px;
margin-bottom: 9px;
label {
font-size: 22px;
}
.clear {
box {
all: unset;
transition: 200ms;
border-radius: 9px;
color: #eee;
background-color: #664C90;
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
padding: 4.5px 9px;
}
&.hover box {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: rgba(238, 238, 238, 0.154);
color: #f1f1f1;
}
&.disabled box {
box-shadow: none;
background-color: rgba(#664C90, 0.3);
color: rgba(238, 238, 238, 0.3);
}
label {
font-size: 1.2em;
}
}
}
.notification-list-box {
background: $bgfull;
padding: 0 12px;
border-radius: 30px;
border-top: 2px solid $contrast-bg;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
scrollbar {
all: unset;
border-radius: 8px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
* {
all: unset;
}
&:hover {
border-radius: 8px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
scrollbar.vertical {
transition: 200ms;
background-color: rgba(23, 23, 23, 0.3);
&:hover {
background-color: rgba(23, 23, 23, 0.7);
slider {
background-color: rgba(238, 238, 238, 0.7);
min-width: .6em;
}
}
slider {
background-color: rgba(238, 238, 238, 0.5);
border-radius: 9px;
min-width: .4em;
min-height: 2em;
transition: 200ms;
}
}
}
.placeholder {
color: white;
image {
font-size: 7em;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
-gtk-icon-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
}
label {
font-size: 1.2em;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
-gtk-icon-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6);
}
}
}

View file

@ -0,0 +1,210 @@
$background-color-1: rgba(238, 238, 238, 0.154);
$background-color-2: rgba(230, 112, 144, 0.5);
$background-color-3: rgba(238, 238, 238, 0.06);
$background-color-4: #51a4e7;
$background-color-5: transparent;
$background-color-6: #171717;
$background-color-7: rgba(23, 23, 23, 0.3);
$background-color-8: rgba(23, 23, 23, 0.7);
$background-color-9: rgba(238, 238, 238, 0.7);
$background-color-10: rgba(238, 238, 238, 0.5);
.notification.critical {
>box {
box-shadow: inset 0 0 0.5em 0 #e67090;
}
}
.notification {
>box {
all: unset;
margin: 9px 9px 0;
border: 2px solid $contrast-bg;
border-radius: 15px;
background-color: $bg;
padding: 16.2px;
* {
font-size: 16px;
}
}
&:hover {
.close-button {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-1;
background-color: $background-color-2;
}
}
.title {
margin-right: 9px;
font-size: 1.1em;
}
.description {
font-size: .9em;
min-width: 350px;
}
.icon {
border-radius: 7.2px;
margin-right: 9px;
}
.icon.img {
border: 1px solid rgba(238, 238, 238, 0.03);
}
.actions {
button {
all: unset;
transition: all 500ms;
background-color: $background-color-3;
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
border-radius: 7.2px;
font-size: 1.2em;
padding: 4.5px 9px;
margin: 9px 4.5px 0;
* {
font-size: 16px;
}
&:focus {
box-shadow: inset 0 0 0 1px #51a4e7;
background-color: $background-color-1;
}
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-1;
}
&:active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:checked {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:disabled {
box-shadow: none;
background-color: $background-color-5;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
button.on {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
button.active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
}
button.close-button {
all: unset;
transition: all 500ms;
background-color: $background-color-5;
background-image: none;
box-shadow: none;
margin-left: 9px;
border-radius: 7.2px;
min-width: 1.2em;
min-height: 1.2em;
* {
font-size: 16px;
}
&:focus {
box-shadow: inset 0 0 0 1px #51a4e7;
background-color: $background-color-1;
}
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-1;
background-color: $background-color-2;
}
&:active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-color: $background-color-4;
background-image: linear-gradient(#e67090, #e67090);
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:checked {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
&:disabled {
box-shadow: none;
background-color: $background-color-5;
}
}
button.close-button.on {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
button.close-button.active {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03);
background-image: linear-gradient(to right, #51a4e7, #6cb2eb);
background-color: $background-color-4;
&:hover {
box-shadow: inset 0 0 0 1px rgba(238, 238, 238, 0.03), inset 0 0 0 99px rgba(238, 238, 238, 0.154);
}
}
}

View file

@ -0,0 +1,43 @@
.osd {
padding: 12px 20px;
border-radius: 999px;
background: rgba(40, 42, 54, 0.8);
border: 2px solid $contrast-bg;
label {
min-width: 170px;
}
progressbar:disabled {
opacity: 0.5;
}
progressbar {
min-height: 6px;
min-width: 170px;
border-radius: 999px;
background: transparent;
border: none;
trough {
background: #363847;
min-height: inherit;
border-radius: inherit;
border: none;
}
progress {
background: #79659f;
min-height: inherit;
border-radius: inherit;
border: none;
}
}
image {
font-size: 2rem;
color: white;
margin-left: -0.4rem;
margin-right: 0.8rem;
}
}

View file

@ -0,0 +1,95 @@
.thingy {
border-radius: 2rem 2rem 0 0;
min-height: 2.7rem;
min-width: 20rem;
.settings {
padding: 0.5rem;
.button {
background-color: $bgfull;
border: 0.1rem solid $darkbg;
border-radius: 0.7rem;
padding: 0.3rem;
&.toggled {
background-color: $contrast-bg;
}
}
}
}
.osk {
padding-top: 4px;
border-radius: 10px 10px 0;
.side {
.key {
&:active label {
background-color: $contrast-bg;
}
label {
background-color: $bg;
border: 0.08rem solid $darkbg;
border-radius: 0.7rem;
min-height: 3rem;
transition: background-color 0.2s ease-in-out,
border-color 0.2s ease-in-out;
&.normal, &.Super {
min-width: 3rem;
}
&.Tab, &.Backspace {
min-width: 7rem;
}
&.Enter, &.Caps {
min-width: 8rem;
}
&.Shift {
min-width: 9rem;
}
&.Space {
min-width: 20rem;
}
&.PrtSc, &.AltGr {
min-width: 3.2rem;
}
&.active {
background-color: $darkbg;
}
&.altgr {
border: 0.08rem solid blue;
}
}
}
&.right-side {
.key .mod {
&.Ctrl {
min-width: 2.4rem;
}
}
}
&.left-side {
.key .mod {
&.Alt {
min-width: 3rem;
}
&.Ctrl {
min-width: 4rem;
}
}
}
}
}

View file

@ -0,0 +1,53 @@
.overview {
background-color: rgba($bgfull, 0.4);
border: 2px solid $contrast-bg;
border-radius: 10px;
.workspace {
padding: 4px 15px 4px 0;
border: 2px solid transparent;
border-radius: 10px;
&.active {
background-color: rgba(lighten($color: black, $amount: 15), 0.8);
border: 2px solid black;
}
}
.workspace .window {
background-color: $bgfull;
border-radius: 10px;
margin: 0 10px;
transition: min-width 0.2s ease-in-out,
min-height 0.2s ease-in-out,
border-color 0.2s ease-in-out,
font-size 0.2s ease-in-out;
}
.normal {
margin-bottom: 5px;
.workspace {
.window {
border: 2px solid #411C6C;
&.active {
border: 2px solid purple;
}
}
}
}
.special {
.workspace {
.window {
border: 2px solid lighten($color: black, $amount: 20);
&.active {
border: 2px solid purple;
}
}
}
}
}

View file

@ -0,0 +1,117 @@
.arrow {
transition: -gtk-icon-transform 0.3s ease-in-out;
margin-bottom: 12px;
}
.media {
margin-top: 9px;
}
.player {
all: unset;
padding: 10px;
min-width: 400px;
min-height: 200px;
border-radius: 30px;
border-top: 2px solid $contrast-bg;
border-bottom: 2px solid $contrast-bg;
transition: background 250ms;
.top {
font-size: 23px;
}
.metadata {
.title{
font-weight: 500;
transition: text 250ms;
}
.artist{
font-weight: 400;
font-size: 15px;
transition: text 250ms;
}
}
.bottom {
font-size: 30px;
}
.pausebutton {
transition: background-color ease .2s,
color ease .2s;
font-size: 15px;
padding: 4px 4px 4px 7px;
}
.playing {
transition: background-color ease .2s,
color ease .2s;
border-radius: 15px;
}
.stopped,
.paused {
transition: background-color ease .2s,
color ease .2s;
border-radius: 26px;
padding: 4px 4px 4px 10px;
}
button label {
min-width: 35px;
}
}
.position-indicator {
min-width: 18px;
margin: 7px;
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px 0 rgba(255, 255, 255, 0.3);
border-radius: 100%;
}
.previous,
.next,
.shuffle,
.loop {
border-radius: 100%;
transition: color 200ms;
&:hover {
border-radius: 100%;
background-color: rgba(127, 132, 156, 0.4);
transition: color 200ms;
}
}
.loop {
label {
padding-right: 8px;
}
}
.position-slider {
highlight {
margin: 0;
border-radius: 2em;
}
trough {
margin: 0 8px;
border-radius: 2em;
}
slider {
margin: -8px;
min-height: 20px;
border-radius: 10px;
transition: background-color 0.5s ease-in-out;
}
slider:hover {
transition: background-color 0.5s ease-in-out;
}
}

View file

@ -0,0 +1,37 @@
.powermenu {
background-color: $bg;
color: $fg;
padding: 10px;
font-family: "MesloLGS NF";
/* font-family: Iosevka Nerd Font; */
font-size: 70px;
border-radius: 30px;
border: 2px solid $contrast-bg;
label {
min-width: 140px;
min-height: 130px;
}
.button {
margin: 5px 10px;
border-radius: 12px;
min-width: 80px;
transition: all ease .2s;
&:hover { background-color: $bg-secondary; }
&:active { background-color: $bg-secondary; }
.content {
border-radius: 4px;
padding: 0 15px;
}
}
.shutdown { color: $red; }
.reboot { color: $magenta; }
.logout { color: $yellow; }
}
.powermenu-clickhandler {
background-color: black;
}

View file

@ -0,0 +1,182 @@
.quick-settings {
font-size: 30px;
min-width: 500px;
padding: 0;
background-color: $bg;
border-radius: 30px 0 30px 30px;
border: 2px solid $contrast-bg;
}
.title {
font-size: 22px;
margin-top: 30px;
}
.grid-label {
font-size: 30px;
margin-left: 15px;
margin-right: 10px;
min-width: 50px;
transition: color 0.3s ease-in-out;
}
.scrolled-indicator {
margin: 5px 0;
}
.menu {
margin: 10px;
padding: 0;
border: 1.5px solid $contrast-bg;
border-radius: 10px;
font-size: 12px;
scrolledwindow {
padding: 3px;
}
row {
padding: 0;
margin: 0;
}
.menu-item {
margin: 5px;
label {
font-size: 16px;
margin-left: 5px;
}
image {
font-size: 20px;
}
}
}
.sub-label {
font-size: 14px;
padding: 3px;
border: 2px solid $contrast-bg;
border-radius: 10px 20px 20px 10px;
min-width: 106px;
background: #1b1b1b;
margin-top: 5px;
}
.grid-chev {
margin-left: 10px;
margin-right: 12px;
font-size: 25px;
transition: -gtk-icon-transform 0.3s ease-in-out;
}
.button-grid {
font-size: 10px;
min-width: 440px;
background-color: $bgfull;
border-top: 2px solid $contrast-bg;
border-bottom: 2px solid $contrast-bg;
border-radius: 15px;
padding: 10px 15px;
}
.grid-button {
min-height: 65px;
min-width: 70px;
}
.left-part {
background: #1b1b1b;
border-top-left-radius: 15px;
border-bottom-left-radius: 15px;
border-left: 2px solid $contrast-bg;
border-top: 2px solid $contrast-bg;
border-bottom: 2px solid $contrast-bg;
transition: all 0.5s ease-in-out;
}
.right-part {
background: #1b1b1b;
border-top-right-radius: 30px;
border-bottom-right-radius: 30px;
border-right: 2px solid $contrast-bg;
border-top: 2px solid $contrast-bg;
border-bottom: 2px solid $contrast-bg;
transition: all 0.5s ease-in-out;
}
.right-part:hover, .right-part:active {
color: $contrast-bg;
border: 2px solid $contrast-bg;
border-top-left-radius: 7px;
border-bottom-left-radius: 7px;
transition: all 0.5s ease-in-out;
}
.left-part:hover, .left-part:active {
color: $contrast-bg;
border: 2px solid $contrast-bg;
border-top-right-radius: 7px;
border-bottom-right-radius: 7px;
transition: all 0.5s ease-in-out;
}
.player {
margin-top: 6px;
min-height: 220px;
opacity: 0;
}
.slider-box {
min-height: 100px;
min-width: 470px;
background-color: $bgfull;
border-top: 2px solid $contrast-bg;
border-bottom: 2px solid $contrast-bg;
border-radius: 15px;
margin-top: 30px;
margin-bottom: 20px;
.slider-label {
font-size: 30px;
min-width: 40px;
margin-right: -20px;
}
.slider {
min-height: 55px;
scale {
min-width: 400px;
margin-left: 18px;
margin-right: 20px;
highlight {
margin: 0;
background-color: #79659f;
border-radius: 2em;
}
trough {
background-color: #363847;
border-radius: 2em;
}
slider {
margin: -4px;
min-width: 20px;
min-height: 20px;
background: #3e4153;
border-radius: 100%;
transition: background-color 0.5s ease-in-out;
}
slider:hover {
background-color: #303240;
transition: background-color 0.5s ease-in-out;
}
}
}
}

View file

@ -0,0 +1,30 @@
.sys-tray {
padding: 5px;
background-color: $bg;
border-radius: 80px;
border: 2px solid $bg-secondary;
transition: background-color 0.5s ease-in-out,
border 0.5s ease-in-out;
menuitem {
image { color: #CBA6F7; }
background-color: transparent;
padding: 0 2px;
border-radius: 100%;
transition: all 0.5s ease-in-out;
&:hover {
border-radius: 100%;
transition: all 0.5s ease-in-out;
}
* {
font-size: 25px;
border-radius: 10px;
}
}
menubar {
background-color: transparent;
}
}

View file

@ -0,0 +1,117 @@
.bar {
margin: 5px;
}
.osk-toggle,
.tablet-toggle,
.heart-toggle {
font-size: 28px;
min-height: 40px;
min-width: 53px;
}
.heart-toggle {
font-size: 28px;
min-height: 40px;
color: #CBA6F7;
}
.notif-panel {
font-size: 20px;
min-height: 37px;
min-width: 105px;
}
.quick-settings-toggle {
font-size: 24px;
min-height: 40px;
min-width: 40px;
padding-right: 4px;
margin-left: -3px;
}
.toggle-off {
background-color: $bg;
color: #CBA6F7;
border-radius: 80px;
border: 2px solid $bg-secondary;
transition: background-color 0.5s ease-in-out,
border 0.5s ease-in-out;
}
.toggle-on {
background-color: $bg;
color: #CBA6F7;
border-radius: 80px;
border: 2px solid $contrast-bg;
transition: background-color 0.5s ease-in-out,
border 0.5s ease-in-out;
}
.toggle-on.hover, .toggle-off.hover {
background-color: rgba(127, 132, 156, 0.4);
transition: background-color 0.5s ease-in-out,
border 0.5s ease-in-out;
}
.clock {
font-size: 20px;
padding: 0 15px;
}
.audio,
.bluetooth,
.brightness,
.keyboard {
padding: 0 10px;
font-size: 20px;
margin-right: -10px;
}
.network {
padding: 0 10px;
font-size: 20px;
}
.bg-text {
color: $bg;
font-weight: bold;
}
.battery {
padding: 0 10px;
font-size: 20px;
.battery-indicator {
&.charging {
color: green;
}
&.charged {
// TODO: charged battery style
}
&.low {
color: red;
}
}
icon {
.charging {
// TODO: charging battery style
}
.discharging {
// TODO: discharging battery style
}
}
label {
font-size: 20px;
}
}
tooltip {
background: rgba(0,0,0, 0.6);
border-radius: 5px;
}

View file

@ -0,0 +1,31 @@
.workspaces {
background-color: $bg;
border-radius: 80px;
border: 2px solid $bg-secondary;
padding: 3px 12px;
}
.button {
margin: 0 2.5px;
min-height: 22px;
min-width: 22px;
border-radius: 100%;
border: 2px solid transparent;
}
.occupied {
border: 2px solid $bg;
background: $contrast-bg;
transition: background-color 0.6s ease-in-out;
}
.urgent {
border: 2px solid $bg;
background: red;
transition: background-color 0.6s ease-in-out;
}
.active {
border: 2px solid #50fa7b;
transition: margin-left 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}

View file

@ -0,0 +1,24 @@
window,
button,
eventbox,
box,
progressbar,
trough,
undershoot {
all: unset;
}
@import "./common";
@import "./wim-widgets/powermenu";
@import "./wim-widgets/traybuttons";
@import "./wim-widgets/workspaces";
@import "./wim-widgets/systray";
@import "./wim-widgets/notification-center";
@import "./wim-widgets/notification";
@import "./wim-widgets/date";
@import "./wim-widgets/quick-settings";
@import "./wim-widgets/player";
@import "./wim-widgets/overview";
@import "./wim-widgets/applauncher";
@import "./wim-widgets/osd";
@import "./wim-widgets/osk";

View file

@ -0,0 +1,145 @@
import Service from 'resource:///com/github/Aylur/ags/service.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { exec, execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
const KBD = 'tpacpi::kbd_backlight';
const CAPS = 'input0::capslock';
const INTERVAL = 500;
const SCREEN_ICONS = {
90: 'display-brightness-high-symbolic',
70: 'display-brightness-medium-symbolic',
20: 'display-brightness-low-symbolic',
5: 'display-brightness-off-symbolic',
};
class Brightness extends Service {
static {
Service.register(this, {
screen: ['float'],
kbd: ['float'],
caps: ['int'],
}, {
'screen-icon': ['string', 'rw'],
'caps-icon': ['string', 'rw'],
});
}
#kbd = 0;
#kbdMax = 0;
#screen = 0;
#screenIcon = 'display-brightness-symbolic';
#caps = 0;
#capsIcon = 'caps-lock-symbolic';
get kbd() {
return this.#kbd;
}
get screen() {
return this.#screen;
}
get screenIcon() {
return this.#screenIcon;
}
get caps() {
return this.#caps;
}
get capsIcon() {
return this.#capsIcon;
}
set kbd(value) {
if (value < 0 || value > this.#kbdMax) {
return;
}
execAsync(`brightnessctl -d ${KBD} s ${value} -q`)
.then(() => {
this.#kbd = value;
this.emit('kbd', this.#kbd);
})
.catch(console.error);
}
set screen(percent) {
if (percent < 0) {
percent = 0;
}
if (percent > 1) {
percent = 1;
}
execAsync(`brightnessctl s ${percent * 100}% -q`)
.then(() => {
this.#screen = percent;
this.#getScreenIcon();
this.emit('screen', this.#screen);
})
.catch(console.error);
}
constructor() {
super();
try {
this.#monitorKbdState();
this.#kbdMax = Number(exec(`brightnessctl -d ${KBD} m`));
this.#caps = Number(exec(`brightnessctl -d ${CAPS} g`));
this.#screen = Number(exec('brightnessctl g')) /
Number(exec('brightnessctl m'));
}
catch (error) {
console.error('missing dependancy: brightnessctl');
}
}
#getScreenIcon() {
const brightness = this.#screen * 100;
// eslint-disable-next-line
for (const threshold of [4, 19, 69, 89]) {
if (brightness > threshold + 1) {
this.#screenIcon = SCREEN_ICONS[threshold + 1];
this.notify('screen-icon');
}
}
}
fetchCapsState() {
execAsync(`brightnessctl -d ${CAPS} g`)
.then((out) => {
this.#caps = Number(out);
this.#capsIcon = this.#caps ?
'caps-lock-symbolic' :
'capslock-disabled-symbolic';
this.notify('caps-icon');
this.emit('caps', this.#caps);
})
.catch(logError);
}
#monitorKbdState() {
Variable(0, {
poll: [
INTERVAL,
`brightnessctl -d ${KBD} g`,
(out) => {
if (parseInt(out) !== this.#kbd) {
this.#kbd = parseInt(out);
this.emit('kbd', this.#kbd);
return this.#kbd;
}
},
],
});
}
}
const brightnessService = new Brightness();
export default brightnessService;

View file

@ -0,0 +1,218 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Service from 'resource:///com/github/Aylur/ags/service.js';
import { subprocess } from 'resource:///com/github/Aylur/ags/utils.js';
const ON_RELEASE_TRIGGERS = [
'released',
'TOUCH_UP',
'HOLD_END',
];
const ON_CLICK_TRIGGERS = [
'pressed',
'TOUCH_DOWN',
];
// Types
import AgsWindow from 'types/widgets/window';
type Subprocess = typeof imports.gi.Gio.Subprocess;
type Layer = {
address: string;
x: number;
y: number;
w: number;
h: number;
namespace: string;
};
type Levels = {
0?: Array<Layer> | null;
1?: Array<Layer> | null;
2?: Array<Layer> | null;
3?: Array<Layer> | null;
};
type Layers = {
levels: Levels;
};
type CursorPos = {
x: number;
y: number;
};
class Pointers extends Service {
static {
Service.register(this, {
'proc-started': ['boolean'],
'proc-destroyed': ['boolean'],
'device-fetched': ['boolean'],
'new-line': ['string'],
'released': ['string'],
'clicked': ['string'],
});
}
#process: Subprocess;
#lastLine = '';
#pointers = [] as Array<String>;
get process() {
return this.#process;
}
get lastLine() {
return this.#lastLine;
}
get pointers() {
return this.#pointers;
}
constructor() {
super();
this.#initAppConnection();
}
startProc() {
if (this.#process) {
return;
}
this.#process = subprocess(
['libinput', 'debug-events'],
(output) => {
if (output.includes('cancelled')) {
return;
}
if (ON_RELEASE_TRIGGERS.some((p) => output.includes(p))) {
this.#lastLine = output;
Pointers.detectClickedOutside('released');
this.emit('released', output);
this.emit('new-line', output);
}
if (ON_CLICK_TRIGGERS.some((p) => output.includes(p))) {
this.#lastLine = output;
Pointers.detectClickedOutside('clicked');
this.emit('clicked', output);
this.emit('new-line', output);
}
},
);
this.emit('proc-started', true);
}
killProc() {
if (this.#process) {
this.#process.force_exit();
this.#process = null;
this.emit('proc-destroyed', true);
}
}
#initAppConnection() {
App.connect('window-toggled', () => {
const anyVisibleAndClosable =
(Array.from(App.windows) as Array<[string, AgsWindow]>)
.some((w) => {
const closable = w[1].attribute?.close_on_unfocus &&
!(w[1].attribute.close_on_unfocus === 'none' ||
w[1].attribute.close_on_unfocus === 'stay');
return w[1].visible && closable;
});
if (anyVisibleAndClosable) {
this.startProc();
}
else {
this.killProc();
}
});
}
static detectClickedOutside(clickStage: string) {
const toClose = (Array.from(App.windows) as Array<[string, AgsWindow]>)
.some((w) => {
const closable = (w[1].attribute?.close_on_unfocus &&
w[1].attribute.close_on_unfocus === clickStage);
return w[1].visible && closable;
});
if (!toClose) {
return;
}
Hyprland.sendMessage('j/layers').then((response) => {
const layers = JSON.parse(response) as { Layers: Layers };
Hyprland.sendMessage('j/cursorpos').then((res) => {
const pos = JSON.parse(res) as CursorPos;
Object.values(layers).forEach((key) => {
const overlayLayer = key.levels['3'];
if (overlayLayer) {
const noCloseWidgetsNames = ['bar', 'osk'];
const getNoCloseWidgets = (names: Array<string>) => {
const arr = [] as Array<Layer>;
names.forEach((name) => {
arr.push(
overlayLayer.find(
(n) => n.namespace === name,
) ||
// Return an empty Layer if widget doesn't exist
{
address: '',
x: 0, y: 0, w: 0, h: 0,
namespace: '',
},
);
});
return arr;
};
const clickIsOnWidget = (w: Layer) => {
return pos.x > w.x && pos.x < w.x + w.w &&
pos.y > w.y && pos.y < w.y + w.h;
};
const noCloseWidgets =
getNoCloseWidgets(noCloseWidgetsNames);
const widgets = overlayLayer.filter((n) => {
const window =
(App.getWindow(n.namespace) as AgsWindow);
return window &&
window.attribute?.close_on_unfocus &&
window.attribute?.close_on_unfocus ===
clickStage;
});
if (noCloseWidgets.some(clickIsOnWidget)) {
// Don't handle clicks when on certain widgets
}
else {
widgets?.forEach(
(w) => {
if (!(pos.x > w.x && pos.x < w.x + w.w &&
pos.y > w.y && pos.y < w.y + w.h)) {
App.closeWindow(w.namespace);
}
},
);
}
}
});
}).catch(print);
}).catch(print);
}
}
export default new Pointers();

View file

@ -0,0 +1,205 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Service from 'resource:///com/github/Aylur/ags/service.js';
import TouchGestures from './touch-gestures.ts';
import { execAsync, subprocess } from 'resource:///com/github/Aylur/ags/utils.js';
const ROTATION_MAP = {
'normal': 0,
'right-up': 3,
'bottom-up': 2,
'left-up': 1,
};
const SCREEN = 'desc:BOE 0x0964';
const DEVICES = [
'wacom-hid-52eb-finger',
'wacom-hid-52eb-pen',
];
// Types
type Subprocess = typeof imports.gi.Gio.Subprocess;
class Tablet extends Service {
static {
Service.register(this, {
'device-fetched': ['boolean'],
'autorotate-started': ['boolean'],
'autorotate-destroyed': ['boolean'],
'inputs-blocked': ['boolean'],
'inputs-unblocked': ['boolean'],
'laptop-mode': ['boolean'],
'tablet-mode': ['boolean'],
'mode-toggled': ['boolean'],
'osk-toggled': ['boolean'],
});
}
#tabletMode = false;
#oskState = false;
#autorotate: Subprocess;
#blockedInputs: Subprocess;
get tabletMode() {
return this.#tabletMode;
}
get oskState() {
return this.#oskState;
}
constructor() {
super();
this.#listenOskState();
}
#blockInputs() {
if (this.#blockedInputs) {
return;
}
this.#blockedInputs = subprocess(['libinput', 'debug-events', '--grab',
'--device', '/dev/input/by-path/platform-i8042-serio-0-event-kbd',
'--device', '/dev/input/by-path/platform-i8042-serio-1-event-mouse',
'--device', '/dev/input/by-path/platform-AMDI0010:02-event-mouse',
'--device', '/dev/input/by-path/platform-thinkpad_acpi-event',
'--device', '/dev/video-bus'],
() => { /**/ });
this.emit('inputs-blocked', true);
}
#unblockInputs() {
if (this.#blockedInputs) {
this.#blockedInputs.force_exit();
this.#blockedInputs = null;
this.emit('inputs-unblocked', true);
}
}
setTabletMode() {
execAsync(['gsettings', 'set', 'org.gnome.desktop.a11y.applications',
'screen-keyboard-enabled', 'true']).catch(print);
execAsync(['brightnessctl', '-d', 'tpacpi::kbd_backlight', 's', '0'])
.catch(print);
this.startAutorotate();
this.#blockInputs();
this.#tabletMode = true;
this.emit('tablet-mode', true);
this.emit('mode-toggled', true);
}
setLaptopMode() {
execAsync(['gsettings', 'set', 'org.gnome.desktop.a11y.applications',
'screen-keyboard-enabled', 'false']).catch(print);
execAsync(['brightnessctl', '-d', 'tpacpi::kbd_backlight', 's', '2'])
.catch(print);
this.killAutorotate();
this.#unblockInputs();
this.#tabletMode = false;
this.emit('laptop-mode', true);
this.emit('mode-toggled', true);
}
toggleMode() {
if (this.#tabletMode) {
this.setLaptopMode();
}
else {
this.setTabletMode();
}
this.emit('mode-toggled', true);
}
startAutorotate() {
if (this.#autorotate) {
return;
}
this.#autorotate = subprocess(
['monitor-sensor'],
(output) => {
if (output.includes('orientation changed')) {
const index = output.split(' ').at(-1);
if (!index) {
return;
}
const orientation = ROTATION_MAP[index];
Hyprland.sendMessage(
`keyword monitor ${SCREEN},transform,${orientation}`,
).catch(print);
const batchRotate = DEVICES.map((dev) =>
`keyword device:${dev}:transform ${orientation}; `);
Hyprland.sendMessage(`[[BATCH]] ${batchRotate.flat()}`);
if (TouchGestures.gestureDaemon) {
TouchGestures.killDaemon();
TouchGestures.startDaemon();
}
}
},
);
this.emit('autorotate-started', true);
}
killAutorotate() {
if (this.#autorotate) {
this.#autorotate.force_exit();
this.#autorotate = null;
this.emit('autorotate-destroyed', true);
}
}
#listenOskState() {
subprocess(
['bash', '-c', 'busctl monitor --user sm.puri.OSK0'],
(output) => {
if (output.includes('BOOLEAN')) {
const match = output.match('true|false');
if (match) {
this.#oskState = match[0] === 'true';
this.emit('osk-toggled', this.#oskState);
}
}
},
);
}
static openOsk() {
execAsync(['busctl', 'call', '--user',
'sm.puri.OSK0', '/sm/puri/OSK0', 'sm.puri.OSK0',
'SetVisible', 'b', 'true'])
.catch(print);
}
static closeOsk() {
execAsync(['busctl', 'call', '--user',
'sm.puri.OSK0', '/sm/puri/OSK0', 'sm.puri.OSK0',
'SetVisible', 'b', 'false'])
.catch(print);
}
toggleOsk() {
if (this.#oskState) {
Tablet.closeOsk();
}
else {
Tablet.openOsk();
}
}
}
const tabletService = new Tablet();
export default tabletService;

View file

@ -0,0 +1,144 @@
import Service from 'resource:///com/github/Aylur/ags/service.js';
import { subprocess } from 'resource:///com/github/Aylur/ags/utils.js';
const SCREEN = '/dev/input/by-path/platform-AMDI0010\:00-event';
const GESTURE_VERIF = [
'LR', // Left to Right
'RL', // Right to Left
'DU', // Down to Up
'UD', // Up to Down
'DLUR', // Down to Left to Up to Right (clockwise motion from Down)
'DRUL', // Down to Right to Up to Left (counter-clockwise from Down)
'URDL', // Up to Right to Down to Left (clockwise motion from Up)
'ULDR', // Up to Left to Down to Right (counter-clockwise from Up)
];
const EDGE_VERIF = [
'*', // Any
'N', // None
'L', // Left
'R', // Right
'T', // Top
'B', // Bottom
'TL', // Top left
'TR', // Top right
'BL', // Bottom left
'BR', // Bottom right
];
const DISTANCE_VERIF = [
'*', // Any
'S', // Short
'M', // Medium
'L', // Large
];
// Types
type Subprocess = typeof imports.gi.Gio.Subprocess;
// TODO: add actmode param
// TODO: support multiple daemons for different thresholds
class TouchGestures extends Service {
static {
Service.register(this, {
'daemon-started': ['boolean'],
'daemon-destroyed': ['boolean'],
});
}
#gestures = new Map();
#gestureDaemon: Subprocess;
get gestures() {
return this.#gestures;
}
get gestureDaemon() {
return this.#gestureDaemon;
}
addGesture({
name,
nFingers = '1',
gesture,
edge = '*',
distance = '*',
command,
}) {
gesture = String(gesture).toUpperCase();
if (!GESTURE_VERIF.includes(gesture)) {
logError('Wrong gesture id');
return;
}
edge = String(edge).toUpperCase();
if (!EDGE_VERIF.includes(edge)) {
logError('Wrong edge id');
return;
}
distance = String(distance).toUpperCase();
if (!DISTANCE_VERIF.includes(distance)) {
logError('Wrong distance id');
return;
}
if (typeof command !== 'string') {
globalThis[name] = command;
command = `ags -r "${name}()"`;
}
this.#gestures.set(name, [
'-g',
`${nFingers},${gesture},${edge},${distance},${command}`,
]);
if (this.#gestureDaemon) {
this.killDaemon();
}
this.startDaemon();
}
startDaemon() {
if (this.#gestureDaemon) {
return;
}
let command = [
'lisgd', '-d', SCREEN,
// Orientation
'-o', '0',
// Threshold of gesture recognized
'-t', '125',
// Leniency of gesture angle
'-r', '25',
// Timeout time
'-m', '3200',
];
this.#gestures.forEach((gesture) => {
command = command.concat(gesture);
});
this.#gestureDaemon = subprocess(
command,
() => { /**/ },
);
this.emit('daemon-started', true);
}
killDaemon() {
if (this.#gestureDaemon) {
this.#gestureDaemon.force_exit();
this.#gestureDaemon = null;
this.emit('daemon-destroyed', true);
}
}
}
const touchGesturesService = new TouchGestures();
export default touchGesturesService;

View file

@ -0,0 +1,70 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import { Box, Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
import CursorBox from '../misc/cursorbox.ts';
// Types
import { Application } from 'types/service/applications.ts';
export default (app: Application) => {
const icon = Icon({ size: 42 });
const iconString = app.app.get_string('Icon');
if (app.icon_name) {
if (lookUpIcon(app.icon_name)) {
icon.icon = app.icon_name;
}
else if (iconString && iconString !== 'nix-snowflake') {
icon.icon = iconString;
}
else {
icon.icon = '';
}
}
const textBox = Box({
vertical: true,
vpack: 'start',
children: [
Label({
class_name: 'title',
label: app.name,
xalign: 0,
truncate: 'end',
}),
Label({
class_name: 'description',
label: app.description || '',
wrap: true,
xalign: 0,
justification: 'left',
}),
Label(),
],
});
return CursorBox({
hexpand: true,
class_name: 'app',
attribute: { app },
on_primary_click_release: () => {
App.closeWindow('applauncher');
app.launch();
},
child: Box({
children: [
icon,
textBox,
],
}),
});
};

View file

@ -0,0 +1,162 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
// FIXME: find cleaner way to import this
// @ts-expect-error
import { Fzf } from 'file:///home/matt/.nix/modules/ags/config/node_modules/fzf/dist/fzf.es.js';
import { Box, Entry, Icon, Label, ListBox, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
import PopupWindow from '../misc/popup.ts';
import AppItem from './app-item.ts';
// Types
import { Application } from 'types/service/applications.ts';
type ListBoxRow = typeof imports.gi.Gtk.ListBoxRow;
const Applauncher = (window_name = 'applauncher') => {
let fzfResults: Array<any>;
const list = ListBox({});
const setSort = (text: string) => {
const fzf = new Fzf(Applications.list, {
selector: (app: Application) => {
return app.name + app.executable;
},
tiebreakers: [
(a: Application, b: Application) => b.frequency - a.frequency,
],
});
fzfResults = fzf.find(text);
list.set_sort_func(
(a: ListBoxRow, b: ListBoxRow) => {
const row1 = a.get_children()[0]?.attribute.app.name;
const row2 = b.get_children()[0]?.attribute.app.name;
if (!row1 || !row2) {
return 0;
}
return fzfResults.indexOf(row1) -
fzfResults.indexOf(row1) || 0;
},
);
};
const makeNewChildren = () => {
const rows = list.get_children() as Array<ListBoxRow>;
rows.forEach((ch) => {
ch.destroy();
});
const children = Applications.query('')
.flatMap((app) => AppItem(app));
children.forEach((ch) => {
list.add(ch);
});
list.show_all();
};
makeNewChildren();
const placeholder = Revealer({
child: Label({
label: " Couldn't find a match",
class_name: 'placeholder',
}),
});
const entry = Entry({
// Set some text so on-change works the first time
text: '-',
hexpand: true,
on_accept: ({ text }) => {
const appList = Applications.query(text || '');
if (appList[0]) {
App.closeWindow(window_name);
appList[0].launch();
}
},
on_change: ({ text }) => {
if (text === null) {
return;
}
setSort(text);
let visibleApps = 0;
const rows = list.get_children() as Array<ListBoxRow>;
rows.forEach((row) => {
row.changed();
const item = row.get_children()[0];
if (item?.attribute.app) {
const isMatching = fzfResults.find((r) => {
return r.item.name === item.attribute.app.name;
});
row.visible = isMatching;
if (isMatching) {
++visibleApps;
}
}
});
placeholder.reveal_child = visibleApps <= 0;
},
});
return Box({
class_name: 'applauncher',
vertical: true,
setup: (self) => {
self.hook(App, (_, name, visible) => {
if (name !== window_name) {
return;
}
entry.text = '';
if (visible) {
entry.grab_focus();
}
else {
makeNewChildren();
}
});
},
children: [
Box({
class_name: 'header',
children: [
Icon('preferences-system-search-symbolic'),
entry,
],
}),
Scrollable({
hscroll: 'never',
vscroll: 'automatic',
child: Box({
vertical: true,
children: [list, placeholder],
}),
}),
],
});
};
export default () => PopupWindow({
name: 'applauncher',
focusable: true,
child: Applauncher(),
});

View file

@ -0,0 +1,50 @@
import { Box, CenterBox, Window } from 'resource:///com/github/Aylur/ags/widget.js';
import SysTray from './items/systray.ts';
import Separator from '../misc/separator.ts';
import NotifButton from './items/notif-button.ts';
import Clock from './items/clock.ts';
const PADDING = 20;
export default () => Window({
name: 'bar',
layer: 'overlay',
exclusivity: 'exclusive',
anchor: ['bottom', 'left', 'right'],
monitor: 1,
child: Box({
vertical: true,
children: [
CenterBox({
class_name: 'bar',
start_widget: Box({
hpack: 'start',
children: [
Separator(PADDING),
SysTray(),
],
}),
center_widget: Box({
children: [],
}),
end_widget: Box({
hpack: 'end',
children: [
NotifButton(),
Separator(PADDING / 2),
Clock(),
Separator(PADDING),
],
}),
}),
Separator(PADDING, { vertical: true }),
],
}),
});

View file

@ -0,0 +1,87 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, EventBox, Revealer, Window } from 'resource:///com/github/Aylur/ags/widget.js';
// Types
import { Variable as Var } from 'types/variable';
import AgsBox from 'types/widgets/box';
import { RevealerProps } from 'types/widgets/revealer';
const BarCloser = (variable: Var<boolean>) => Window({
name: 'bar-closer',
visible: false,
anchor: ['top', 'bottom', 'left', 'right'],
layer: 'overlay',
child: EventBox({
on_hover: (self) => {
variable.value = false;
const parent = self.get_parent();
if (parent) {
parent.visible = false;
}
},
child: Box({
css: 'padding: 1px',
}),
}),
});
export default (props: RevealerProps) => {
const Revealed = Variable(true);
const barCloser = BarCloser(Revealed);
return Box({
css: 'min-height: 1px',
hexpand: true,
vertical: true,
setup: (self) => {
const checkCurrentWsFsState = () => {
const workspace = Hyprland.getWorkspace(
Hyprland.active.workspace.id,
);
if (workspace) {
Revealed.value = !workspace['hasfullscreen'];
}
};
const checkGlobalFsState = (_: AgsBox, fullscreen: boolean) => {
Revealed.value = !fullscreen;
};
self
.hook(Hyprland.active, checkCurrentWsFsState)
.hook(Hyprland, checkGlobalFsState, 'fullscreen');
},
children: [
Revealer({
...props,
transition: 'slide_down',
reveal_child: true,
}).bind('reveal_child', Revealed),
Revealer({
reveal_child: Revealed.bind()
.transform((v) => !v),
child: EventBox({
on_hover: () => {
barCloser.visible = true;
Revealed.value = true;
},
child: Box({
css: 'min-height: 5px;',
}),
}),
}),
],
});
};

View file

@ -0,0 +1,22 @@
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import { Label, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
import { SpeakerIcon } from '../../misc/audio-icons.ts';
import HoverRevealer from './hover-revealer.ts';
export default () => HoverRevealer({
class_name: 'audio',
icon: Icon({
icon: SpeakerIcon.bind(),
}),
label: Label().hook(Audio, (self) => {
if (Audio.speaker?.volume) {
self.label =
`${Math.round(Audio.speaker?.volume * 100)}%`;
}
}, 'speaker-changed'),
});

View file

@ -0,0 +1,27 @@
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import { Label, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
import HoverRevealer from './hover-revealer.ts';
export default () => HoverRevealer({
class_name: 'bluetooth',
icon: Icon().hook(Bluetooth, (self) => {
if (Bluetooth.enabled) {
self.icon = Bluetooth.connected_devices[0] ?
Bluetooth.connected_devices[0].icon_name :
'bluetooth-active-symbolic';
}
else {
self.icon = 'bluetooth-disabled-symbolic';
}
}),
label: Label().hook(Bluetooth, (self) => {
self.label = Bluetooth.connected_devices[0] ?
`${Bluetooth.connected_devices[0]}` :
'Disconnected';
}, 'notify::connected-devices'),
});

View file

@ -0,0 +1,17 @@
import { Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Brightness from '../../../services/brightness.ts';
import HoverRevealer from './hover-revealer.ts';
export default () => HoverRevealer({
class_name: 'brightness',
icon: Icon({
icon: Brightness.bind('screenIcon'),
}),
label: Label().hook(Brightness, (self) => {
self.label = `${Math.round(Brightness.screen * 100)}%`;
}, 'screen'),
});

View file

@ -0,0 +1,42 @@
import { Box, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.ts';
import CursorBox from '../../misc/cursorbox.ts';
export default ({
class_name,
icon,
label,
spacing = 5,
}) => {
const hoverRevLabel = Revealer({
transition: 'slide_right',
child: Box({
children: [
Separator(spacing),
label,
],
}),
});
const widget = CursorBox({
on_hover: () => {
hoverRevLabel.reveal_child = true;
},
child: Box({
class_name,
children: [
icon,
hoverRevLabel,
],
}),
});
return widget;
};

View file

@ -0,0 +1,66 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import HoverRevealer from './hover-revealer.ts';
const DEFAULT_KB = 'at-translated-set-2-keyboard';
import AgsLabel from 'types/widgets/label.ts';
type Keyboard = {
address: string;
name: string;
rules: string;
model: string;
layout: string;
variant: string;
options: string;
active_keymap: string;
main: boolean;
};
const getKbdLayout = (self: AgsLabel, _: string, layout: string) => {
if (layout) {
if (layout === 'error') {
return;
}
const shortName = layout.match(/\(([A-Za-z]+)\)/);
self.label = shortName ? shortName[1] : layout;
}
else {
// At launch, kb layout is undefined
Hyprland.sendMessage('j/devices').then((obj) => {
const keyboards = Array.from(JSON.parse(obj)
.keyboards) as Array<Keyboard>;
const kb = keyboards.find((v) => v.name === DEFAULT_KB);
if (kb) {
layout = kb.active_keymap;
const shortName = layout
.match(/\(([A-Za-z]+)\)/);
self.label = shortName ? shortName[1] : layout;
}
else {
self.label = 'None';
}
}).catch(print);
}
};
export default () => HoverRevealer({
class_name: 'keyboard',
spacing: 4,
icon: Icon({
icon: 'input-keyboard-symbolic',
size: 20,
}),
label: Label({ css: 'font-size: 20px;' })
.hook(Hyprland, getKbdLayout, 'keyboard-layout'),
});

View file

@ -0,0 +1,38 @@
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import { Label, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
import HoverRevealer from './hover-revealer.ts';
export default () => HoverRevealer({
class_name: 'network',
icon: Icon().hook(Network, (self) => {
if (Network.wifi.internet === 'connected' ||
Network.wifi.internet === 'connecting') {
self.icon = Network.wifi.icon_name;
}
else if (Network.wired.internet === 'connected' ||
Network.wired.internet === 'connecting') {
self.icon = Network.wired.icon_name;
}
else {
self.icon = Network.wifi.icon_name;
}
}),
label: Label().hook(Network, (self) => {
if (Network.wifi.internet === 'connected' ||
Network.wifi.internet === 'connecting') {
self.label = Network.wifi.ssid || 'Unknown';
}
else if (Network.wired.internet === 'connected' ||
Network.wired.internet === 'connecting') {
self.label = 'Connected';
}
else {
self.label = 'Disconnected';
}
}),
});

View file

@ -0,0 +1,31 @@
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
import { Label, Icon, Box } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.ts';
const LOW_BATT = 20;
const SPACING = 5;
export default () => Box({
class_name: 'toggle-off battery',
children: [
Icon({
class_name: 'battery-indicator',
icon: Battery.bind('icon_name'),
}).hook(Battery, (self) => {
self.toggleClassName('charging', Battery.charging);
self.toggleClassName('charged', Battery.charged);
self.toggleClassName('low', Battery.percent < LOW_BATT);
}),
Separator(SPACING),
Label({
label: Battery.bind('percent')
.transform((v) => `${v}%`),
}),
],
});

View file

@ -0,0 +1,21 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import CursorBox from '../../misc/cursorbox.ts';
import Clock from './clock';
export default () => CursorBox({
class_name: 'toggle-off',
on_primary_click_release: () => App.toggleWindow('calendar'),
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName === 'calendar') {
self.toggleClassName('toggle-on', visible);
}
});
},
child: Clock(),
});

View file

@ -0,0 +1,12 @@
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
export default () => Label({ class_name: 'clock' })
.poll(1000, (self) => {
const time = imports.gi.GLib
.DateTime.new_now_local();
self.label = time.format('%a. ') +
time.get_day_of_month() +
time.format(' %b. %H:%M');
});

View file

@ -0,0 +1,33 @@
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Box, Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.ts';
const SPACING = 8;
export default () => Box({
children: [
Separator(SPACING / 2),
Icon({ size: 30 })
.hook(Hyprland.active.client, (self) => {
const app = Applications
.query(Hyprland.active.client.class)[0];
if (app) {
self.icon = app.icon_name || '';
self.visible = Hyprland.active.client.title !== '';
}
}),
Separator(SPACING),
Label({
css: 'color: #CBA6F7; font-size: 18px',
truncate: 'end',
label: Hyprland.active.client.bind('title'),
}),
],
});

View file

@ -0,0 +1,28 @@
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import CursorBox from '../../misc/cursorbox.ts';
import Persist from '../../misc/persist.ts';
const HeartState = Variable('');
Persist({
name: 'heart',
gobject: HeartState,
prop: 'value',
condition: '',
whenFalse: '󰣐',
});
export default () => CursorBox({
on_primary_click_release: () => {
HeartState.value = HeartState.value === '' ? '󰣐' : '';
},
child: Label({
class_name: 'heart-toggle',
label: HeartState.bind(),
}),
});

View file

@ -0,0 +1,62 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { Box, CenterBox, Icon, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import CursorBox from '../../misc/cursorbox.ts';
import Separator from '../../misc/separator.ts';
const SPACING = 4;
// Types
import AgsWindow from 'types/widgets/window.ts';
export default () => CursorBox({
class_name: 'toggle-off',
on_primary_click_release: (self) => {
(App.getWindow('notification-center') as AgsWindow)
?.attribute.set_x_pos(
self.get_allocation(),
'right',
);
App.toggleWindow('notification-center');
},
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName === 'notification-center') {
self.toggleClassName('toggle-on', visible);
}
});
},
child: CenterBox({
class_name: 'notif-panel',
center_widget: Box({
children: [
Icon().hook(Notifications, (self) => {
if (Notifications.dnd) {
self.icon = 'notification-disabled-symbolic';
}
else if (Notifications.notifications.length > 0) {
self.icon = 'notification-new-symbolic';
}
else {
self.icon = 'notification-symbolic';
}
}),
Separator(SPACING),
Label({
label: Notifications.bind('notifications')
.transform((n) => String(n.length)),
}),
],
}),
}),
});

View file

@ -0,0 +1,23 @@
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Tablet from '../../../services/tablet.ts';
import CursorBox from '../../misc/cursorbox.ts';
export default () => CursorBox({
class_name: 'toggle-off',
on_primary_click_release: () => Tablet.toggleOsk(),
setup: (self) => {
self.hook(Tablet, () => {
self.toggleClassName('toggle-on', Tablet.oskState);
}, 'osk-toggled');
},
child: Label({
class_name: 'osk-toggle',
xalign: 0.6,
label: '󰌌 ',
}),
});

View file

@ -0,0 +1,85 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import { Box, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Audio from '../hovers/audio.ts';
import Bluetooth from '../hovers/bluetooth.ts';
import Brightness from '../hovers/brightness.ts';
import KeyboardLayout from '../hovers/keyboard-layout.ts';
import Network from '../hovers/network.ts';
import CursorBox from '../../misc/cursorbox.ts';
import Separator from '../../misc/separator.ts';
const SPACING = 4;
// Types
import AgsRevealer from 'types/widgets/revealer.ts';
import AgsBox from 'types/widgets/box.ts';
import AgsWindow from 'types/widgets/window.ts';
export default () => {
const hoverRevealers = [
KeyboardLayout(),
Brightness(),
Audio(),
Bluetooth(),
Network(),
];
return CursorBox({
class_name: 'toggle-off',
on_primary_click_release: (self) => {
(App.getWindow('quick-settings') as AgsWindow)
?.attribute.set_x_pos(
self.get_allocation(),
'right',
);
App.toggleWindow('quick-settings');
},
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName === 'quick-settings') {
self.toggleClassName('toggle-on', visible);
}
});
},
attribute: {
hoverRevealers: hoverRevealers.map((rev) => {
const box = rev.child as AgsBox;
return box.children[1];
}),
},
on_hover_lost: (self) => {
self.attribute.hoverRevealers.forEach(
(rev: AgsRevealer) => {
rev.reveal_child = false;
},
);
},
child: Box({
class_name: 'quick-settings-toggle',
vertical: false,
children: [
Separator(SPACING),
...hoverRevealers,
Label(' '),
Separator(SPACING),
],
}),
});
};

View file

@ -0,0 +1,94 @@
import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { Box, Icon, MenuItem, MenuBar, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../../misc/separator.ts';
const REVEAL_DURATION = 500;
const SPACING = 12;
// Types
import { TrayItem } from 'types/service/systemtray.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
type Menu = typeof imports.gi.Gtk.Menu;
const SysTrayItem = (item: TrayItem) => {
if (item.id === 'spotify-client') {
return;
}
return MenuItem({
submenu: <Menu> item.menu,
tooltip_markup: item.bind('tooltip_markup'),
child: Revealer({
transition: 'slide_right',
transition_duration: REVEAL_DURATION,
child: Icon({ size: 24 }).bind('icon', item, 'icon'),
}),
});
};
const SysTray = () => MenuBar({
attribute: { items: new Map() },
setup: (self) => {
self
.hook(SystemTray, (_, id) => {
const item = SystemTray.getItem(id);
if (self.attribute.items.has(id) || !item) {
return;
}
const w = SysTrayItem(item);
// Early return if item is in blocklist
if (!w) {
return;
}
self.attribute.items.set(id, w);
self.child = w;
self.show_all();
(<AgsRevealer> w.child).reveal_child = true;
}, 'added')
.hook(SystemTray, (_, id) => {
if (!self.attribute.items.has(id)) {
return;
}
self.attribute.items.get(id).child.reveal_child = false;
timeout(REVEAL_DURATION, () => {
self.attribute.items.get(id).destroy();
self.attribute.items.delete(id);
});
}, 'removed');
},
});
export default () => {
const systray = SysTray();
return Revealer({
transition: 'slide_right',
child: Box({
children: [
Box({
class_name: 'sys-tray',
children: [systray],
}),
Separator(SPACING),
],
}),
}).hook(SystemTray, (self) => {
self.reveal_child = systray.get_children().length > 0;
});
};

View file

@ -0,0 +1,24 @@
import { Box, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import Tablet from '../../../services/tablet.ts';
import CursorBox from '../../misc/cursorbox.ts';
export default () => CursorBox({
class_name: 'toggle-off',
on_primary_click_release: () => Tablet.toggleMode(),
setup: (self) => {
self.hook(Tablet, () => {
self.toggleClassName('toggle-on', Tablet.tabletMode);
}, 'mode-toggled');
},
child: Box({
class_name: 'tablet-toggle',
vertical: false,
children: [Label(' 󰦧 ')],
}),
});

View file

@ -0,0 +1,172 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { Box, Overlay, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import CursorBox from '../../misc/cursorbox.ts';
const URGENT_DURATION = 1000;
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
import AgsOverlay from 'types/widgets/overlay.ts';
import AgsEventBox from 'types/widgets/eventbox.ts';
const Workspace = ({ id }: { id: number }) => {
return Revealer({
transition: 'slide_right',
attribute: { id },
child: CursorBox({
tooltip_text: `${id}`,
on_primary_click_release: () => {
Hyprland.sendMessage(`dispatch workspace ${id}`);
},
child: Box({
vpack: 'center',
class_name: 'button',
setup: (self) => {
const update = (_: AgsBox, addr: string | undefined) => {
const workspace = Hyprland.getWorkspace(id);
const occupied = workspace && workspace.windows > 0;
self.toggleClassName('occupied', occupied);
if (!addr) {
return;
}
// Deal with urgent windows
const client = Hyprland.getClient(addr);
const isThisUrgent = client &&
client.workspace.id === id;
if (isThisUrgent) {
self.toggleClassName('urgent', true);
// Only show for a sec when urgent is current workspace
if (Hyprland.active.workspace.id === id) {
timeout(URGENT_DURATION, () => {
self.toggleClassName('urgent', false);
});
}
}
};
self
.hook(Hyprland, update)
// Deal with urgent windows
.hook(Hyprland, update, 'urgent-window')
.hook(Hyprland.active.workspace, () => {
if (Hyprland.active.workspace.id === id) {
self.toggleClassName('urgent', false);
}
});
},
}),
}),
});
};
export default () => {
const L_PADDING = 16;
const WS_WIDTH = 30;
const updateHighlight = (self: AgsBox) => {
const currentId = Hyprland.active.workspace.id;
const indicators = (((self.get_parent() as AgsOverlay)
.child as AgsEventBox)
.child as AgsBox)
.children as Array<AgsRevealer>;
const currentIndex = indicators
.findIndex((w) => w.attribute.id === currentId);
if (currentIndex < 0) {
return;
}
self.setCss(`margin-left: ${L_PADDING + (currentIndex * WS_WIDTH)}px`);
};
const highlight = Box({
vpack: 'center',
hpack: 'start',
class_name: 'button active',
}).hook(Hyprland.active.workspace, updateHighlight);
const widget = Overlay({
pass_through: true,
overlays: [highlight],
child: CursorBox({
child: Box({
class_name: 'workspaces',
attribute: { workspaces: [] },
setup: (self) => {
const workspaces = (): Array<AgsRevealer> =>
self.attribute.workspaces;
const refresh = () => {
(self.children as Array<AgsRevealer>).forEach((rev) => {
rev.reveal_child = false;
});
workspaces().forEach((ws) => {
ws.reveal_child = true;
});
};
const updateWorkspaces = () => {
Hyprland.workspaces.forEach((ws) => {
const currentWs = (self.children as Array<AgsBox>)
.find((ch) => ch.attribute.id === ws.id);
if (!currentWs && ws.id > 0) {
self.add(Workspace({ id: ws.id }));
}
});
self.show_all();
// Make sure the order is correct
workspaces().forEach((workspace, i) => {
(<AgsBox> workspace.get_parent()).reorder_child(
workspace,
i,
);
});
};
self.hook(Hyprland, () => {
self.attribute.workspaces =
(self.children as Array<AgsBox>).filter((ch) => {
return Hyprland.workspaces.find((ws) => {
return ws.id === ch.attribute.id;
});
}).sort((a, b) => a.attribute.id - b.attribute.id);
updateWorkspaces();
refresh();
// Make sure the highlight doesn't go too far
const TEMP_TIMEOUT = 10;
timeout(TEMP_TIMEOUT, () => updateHighlight(highlight));
});
},
}),
}),
});
return widget;
};

View file

@ -0,0 +1,85 @@
import { Window, CenterBox, Box } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../misc/separator.ts';
import Battery from './items/battery.ts';
import Clock from './items/cal-opener.ts';
import CurrentWindow from './items/current-window.ts';
import Heart from './items/heart.ts';
import NotifButton from './items/notif-button.ts';
import OskToggle from './items/osk-toggle.ts';
import QsToggle from './items/quick-settings.ts';
import SysTray from './items/systray.ts';
import TabletToggle from './items/tablet-toggle.ts';
import Workspaces from './items/workspaces.ts';
import BarReveal from './fullscreen.ts';
const SPACING = 12;
export default () => Window({
name: 'bar',
layer: 'overlay',
anchor: ['top', 'left', 'right'],
margins: [-1, 0, 0, 0],
exclusivity: 'exclusive',
child: BarReveal({
child: CenterBox({
css: 'margin: 6px 5px 5px 5px',
class_name: 'bar',
start_widget: Box({
hpack: 'start',
children: [
OskToggle(),
Separator(SPACING),
TabletToggle(),
Separator(SPACING),
SysTray(),
Workspaces(),
Separator(SPACING),
CurrentWindow(),
],
}),
center_widget: Box({
children: [
Separator(SPACING),
Clock(),
Separator(SPACING),
],
}),
end_widget: Box({
hpack: 'end',
children: [
Heart(),
Separator(SPACING),
Battery(),
Separator(SPACING),
NotifButton(),
Separator(SPACING),
QsToggle(),
],
}),
}),
}),
});

View file

@ -0,0 +1,52 @@
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
import RoundedCorner from './screen-corners.ts';
const TopLeft = () => Window({
name: 'cornertl',
layer: 'overlay',
exclusivity: 'ignore',
anchor: ['top', 'left'],
visible: true,
click_through: true,
child: RoundedCorner('topleft'),
});
const TopRight = () => Window({
name: 'cornertr',
layer: 'overlay',
exclusivity: 'ignore',
anchor: ['top', 'right'],
visible: true,
click_through: true,
child: RoundedCorner('topright'),
});
const BottomLeft = () => Window({
name: 'cornerbl',
layer: 'overlay',
exclusivity: 'ignore',
anchor: ['bottom', 'left'],
visible: true,
click_through: true,
child: RoundedCorner('bottomleft'),
});
const BottomRight = () => Window({
name: 'cornerbr',
layer: 'overlay',
exclusivity: 'ignore',
anchor: ['bottom', 'right'],
visible: true,
click_through: true,
child: RoundedCorner('bottomright'),
});
export default () => [
TopLeft(),
TopRight(),
BottomLeft(),
BottomRight(),
];

View file

@ -0,0 +1,79 @@
import { Box, DrawingArea } from 'resource:///com/github/Aylur/ags/widget.js';
import Gtk from 'gi://Gtk';
export default (
place = 'top left',
css = 'background-color: black;',
) => Box({
hpack: place.includes('left') ? 'start' : 'end',
vpack: place.includes('top') ? 'start' : 'end',
css: `
padding: 1px; margin:
${place.includes('top') ? '-1px' : '0'}
${place.includes('right') ? '-1px' : '0'}
${place.includes('bottom') ? '-1px' : '0'}
${place.includes('left') ? '-1px' : '0'};
`,
child: DrawingArea({
css: `
border-radius: 18px;
border-width: 0.068rem;
${css}
`,
setup: (widget) => {
let r = widget.get_style_context()
.get_property('border-radius', Gtk.StateFlags.NORMAL);
widget.set_size_request(r, r);
widget.connect('draw', (_, cr) => {
const c = widget.get_style_context()
.get_property('background-color', Gtk.StateFlags.NORMAL);
r = widget.get_style_context()
.get_property('border-radius', Gtk.StateFlags.NORMAL);
const borderColor = widget.get_style_context()
.get_property('color', Gtk.StateFlags.NORMAL);
// You're going to write border-width: something anyway
const borderWidth = widget.get_style_context()
.get_border(Gtk.StateFlags.NORMAL).left;
widget.set_size_request(r, r);
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();
cr.setLineWidth(borderWidth);
cr.setSourceRGBA(borderColor.red,
borderColor.green,
borderColor.blue,
borderColor.alpha);
cr.stroke();
});
},
}),
});

View file

@ -0,0 +1,98 @@
import { Box, Calendar, Label } from 'resource:///com/github/Aylur/ags/widget.js';
const { DateTime } = imports.gi.GLib;
import PopupWindow from './misc/popup.ts';
const Divider = () => Box({
class_name: 'divider',
vertical: true,
});
const Time = () => Box({
class_name: 'timebox',
vertical: true,
children: [
Box({
class_name: 'time-container',
hpack: 'center',
vpack: 'center',
children: [
Label({
class_name: 'content',
label: 'hour',
setup: (self) => {
self.poll(1000, () => {
self.label = DateTime.new_now_local().format('%H');
});
},
}),
Divider(),
Label({
class_name: 'content',
label: 'minute',
setup: (self) => {
self.poll(1000, () => {
self.label = DateTime.new_now_local().format('%M');
});
},
}),
],
}),
Box({
class_name: 'date-container',
hpack: 'center',
child: Label({
css: 'font-size: 20px',
label: 'complete date',
setup: (self) => {
self.poll(1000, () => {
const time = DateTime.new_now_local();
self.label = time.format('%A, %B ') +
time.get_day_of_month() +
time.format(', %Y');
});
},
}),
}),
],
});
const CalendarWidget = () => Box({
class_name: 'cal-box',
child: Calendar({
show_day_names: true,
show_heading: true,
class_name: 'cal',
}),
});
const TOP_MARGIN = 6;
export default () => PopupWindow({
name: 'calendar',
anchor: ['top'],
margins: [TOP_MARGIN, 0, 0, 0],
child: Box({
class_name: 'date',
vertical: true,
children: [
Time(),
CalendarWidget(),
],
}),
});

View file

@ -0,0 +1,171 @@
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { Box, EventBox, Overlay } from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
const MAX_OFFSET = 200;
const OFFSCREEN = 500;
const ANIM_DURATION = 500;
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
opacity ${ANIM_DURATION}ms ease;`;
// Types
import AgsOverlay from 'types/widgets/overlay';
import OverlayProps from 'types/widgets/overlay';
import AgsBox from 'types/widgets/box';
import AgsCenterBox from 'types/widgets/centerbox';
import { Connectable } from 'types/widgets/widget';
type Gesture = {
attribute?: Object
setup?(self: Connectable<AgsOverlay> & AgsOverlay): void
props?: OverlayProps
};
export default ({
attribute = {},
setup = () => {/**/},
...props
}: Gesture) => {
const widget = EventBox();
const gesture = Gtk.GestureDrag.new(widget);
// Have empty PlayerBox to define the size of the widget
const emptyPlayer = Box({
class_name: 'player',
attribute: { empty: true },
});
const content = Overlay({
...props,
attribute: {
...attribute,
dragging: false,
includesWidget: (playerW: AgsOverlay) => {
return content.overlays.find((w) => w === playerW);
},
showTopOnly: () => content.overlays.forEach((over) => {
over.visible = over === content.overlays.at(-1);
}),
moveToTop: (player: AgsCenterBox) => {
player.visible = true;
content.reorder_overlay(player, -1);
timeout(ANIM_DURATION, () => {
content.attribute.showTopOnly();
});
},
},
child: emptyPlayer,
setup: (self) => {
setup(self);
self
.hook(gesture, (_, realGesture) => {
if (realGesture) {
self.overlays.forEach((over) => {
over.visible = true;
});
}
else {
self.attribute.showTopOnly();
}
// Don't allow gesture when only one player
if (self.overlays.length <= 1) {
return;
}
self.attribute.dragging = true;
let offset = gesture.get_offset()[1];
const playerBox = self.overlays.at(-1) as AgsBox;
if (!offset) {
return;
}
// Slide right
if (offset >= 0) {
playerBox.setCss(`
margin-left: ${offset}px;
margin-right: -${offset}px;
${playerBox.attribute.bgStyle}
`);
}
// Slide left
else {
offset = Math.abs(offset);
playerBox.setCss(`
margin-left: -${offset}px;
margin-right: ${offset}px;
${playerBox.attribute.bgStyle}
`);
}
}, 'drag-update')
.hook(gesture, () => {
// Don't allow gesture when only one player
if (self.overlays.length <= 1) {
return;
}
self.attribute.dragging = false;
const offset = gesture.get_offset()[1];
const playerBox = self.overlays.at(-1) as AgsBox;
// If crosses threshold after letting go, slide away
if (offset && Math.abs(offset) > MAX_OFFSET) {
// Disable inputs during animation
widget.sensitive = false;
// Slide away right
if (offset >= 0) {
playerBox.setCss(`
${TRANSITION}
margin-left: ${OFFSCREEN}px;
margin-right: -${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
// Slide away left
else {
playerBox.setCss(`
${TRANSITION}
margin-left: -${OFFSCREEN}px;
margin-right: ${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
timeout(ANIM_DURATION, () => {
// Put the player in the back after anim
self.reorder_overlay(playerBox, 0);
// Recenter player
playerBox.setCss(playerBox.attribute.bgStyle);
widget.sensitive = true;
self.attribute.showTopOnly();
});
}
else {
// Recenter with transition for animation
playerBox.setCss(`${TRANSITION}
${playerBox.attribute.bgStyle}`);
}
}, 'drag-end');
},
});
widget.add(content);
return widget;
};

View file

@ -0,0 +1,462 @@
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
import { Button, Icon, Label, Stack, Slider, CenterBox, Box } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync, lookUpIcon, readFileAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import Separator from '../misc/separator.ts';
import CursorBox from '../misc/cursorbox.ts';
const ICON_SIZE = 32;
const icons = {
mpris: {
fallback: 'audio-x-generic-symbolic',
shuffle: {
enabled: '󰒝',
disabled: '󰒞',
},
loop: {
none: '󰑗',
track: '󰑘',
playlist: '󰑖',
},
playing: ' ',
paused: ' ',
stopped: ' ',
prev: '󰒮',
next: '󰒭',
},
};
// Types
import { MprisPlayer } from 'types/service/mpris.ts';
import { Variable as Var } from 'types/variable';
import AgsOverlay from 'types/widgets/overlay.ts';
import AgsCenterBox, { CenterBoxProps } from 'types/widgets/centerbox.ts';
import AgsLabel from 'types/widgets/label.ts';
import AgsIcon from 'types/widgets/icon.ts';
import AgsStack from 'types/widgets/stack.ts';
export const CoverArt = (
player: MprisPlayer,
colors: Var<any>,
props: CenterBoxProps,
) => CenterBox({
...props,
vertical: true,
attribute: {
bgStyle: '',
player,
},
setup: (self) => {
// Give temp cover art
readFileAsync(player.cover_path).catch(() => {
if (!colors.value && !player.track_cover_url) {
colors.value = {
imageAccent: '#6b4fa2',
buttonAccent: '#ecdcff',
buttonText: '#25005a',
hoverAccent: '#d4baff',
};
self.attribute.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${colors.value.imageAccent}),
rgb(0, 0, 0);
background-size: cover;
background-position: center;
`;
self.setCss(self.attribute.bgStyle);
}
});
self.hook(player, () => {
execAsync(['bash', '-c', `[[ -f "${player.cover_path}" ]] &&
coloryou "${player.cover_path}" | grep -v Warning`])
.then((out) => {
if (!Mpris.players.find((p) => player === p)) {
return;
}
colors.value = JSON.parse(out);
self.attribute.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${colors.value.imageAccent}),
url("${player.cover_path}");
background-size: cover;
background-position: center;
`;
if (!(self.get_parent() as AgsCenterBox)
.attribute.dragging) {
self.setCss(self.attribute.bgStyle);
}
}).catch((err) => {
if (err !== '') {
print(err);
}
});
});
},
});
export const TitleLabel = (player: MprisPlayer) => Label({
xalign: 0,
max_width_chars: 40,
truncate: 'end',
justification: 'left',
class_name: 'title',
label: player.bind('track_title'),
});
export const ArtistLabel = (player: MprisPlayer) => Label({
xalign: 0,
max_width_chars: 40,
truncate: 'end',
justification: 'left',
class_name: 'artist',
label: player.bind('track_artists')
.transform((a) => a.join(', ') || ''),
});
export const PlayerIcon = (player: MprisPlayer, overlay: AgsOverlay) => {
const playerIcon = (
p: MprisPlayer,
widget?: AgsOverlay,
over?: AgsOverlay,
) => CursorBox({
tooltip_text: p.identity || '',
on_primary_click_release: () => {
widget?.attribute.moveToTop(over);
},
child: Icon({
class_name: widget ? 'position-indicator' : 'player-icon',
size: widget ? 0 : ICON_SIZE,
setup: (self) => {
self.hook(p, () => {
self.icon = lookUpIcon(p.entry) ?
p.entry :
icons.mpris.fallback;
});
},
}),
});
return Box().hook(Mpris, (self) => {
const grandPa = self.get_parent()?.get_parent();
if (!grandPa) {
return;
}
const thisIndex = overlay.overlays
.indexOf(grandPa);
self.children = (overlay.overlays as Array<AgsOverlay>)
.map((over, i) => {
self.children.push(Separator(2));
return i === thisIndex ?
playerIcon(player) :
playerIcon(over.attribute.player, overlay, over);
})
.reverse();
});
};
const { Gdk } = imports.gi;
const display = Gdk.Display.get_default();
export const PositionSlider = (
player: MprisPlayer,
colors: Var<any>,
) => Slider({
class_name: 'position-slider',
vpack: 'center',
hexpand: true,
draw_value: false,
on_change: ({ value }) => {
player.position = player.length * value;
},
setup: (self) => {
const update = () => {
if (!self.dragging) {
self.visible = player.length > 0;
if (player.length > 0) {
self.value = player.position / player.length;
}
}
};
self
.poll(1000, () => update())
.hook(player, () => update(), 'position')
.hook(colors, () => {
if (colors.value) {
const c = colors.value;
self.setCss(`
highlight { background-color: ${c.buttonAccent}; }
slider { background-color: ${c.buttonAccent}; }
slider:hover { background-color: ${c.hoverAccent}; }
trough { background-color: ${c.buttonText}; }
`);
}
})
// OnClick
.on('button-press-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
})
// OnRelease
.on('button-release-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
});
type PlayerButtonType = {
player: MprisPlayer
colors: Var<any>
items: Array<[name: string, widget: AgsLabel | AgsIcon]>
onClick: string
prop: string
};
const PlayerButton = ({
player,
colors,
items,
onClick,
prop,
}: PlayerButtonType) => CursorBox({
child: Button({
attribute: { hovered: false },
child: Stack({ items }),
on_primary_click_release: () => player[onClick](),
on_hover: (self) => {
self.attribute.hovered = true;
if (prop === 'playBackStatus' && colors.value) {
const c = colors.value;
items.forEach((item) => {
item[1].setCss(`
background-color: ${c.hoverAccent};
color: ${c.buttonText};
min-height: 40px;
min-width: 36px;
margin-bottom: 1px;
margin-right: 1px;
`);
});
}
},
on_hover_lost: (self) => {
self.attribute.hovered = false;
if (prop === 'playBackStatus' && colors.value) {
const c = colors.value;
items.forEach((item) => {
item[1].setCss(`
background-color: ${c.buttonAccent};
color: ${c.buttonText};
min-height: 42px;
min-width: 38px;
`);
});
}
},
setup: (self) => {
self
.hook(player, () => {
(self.child as AgsStack).shown = `${player[prop]}`;
})
.hook(colors, () => {
if (!Mpris.players.find((p) => player === p)) {
return;
}
if (colors.value) {
const c = colors.value;
if (prop === 'playBackStatus') {
if (self.attribute.hovered) {
Array.from(items).forEach((item) => {
item[1].setCss(`
background-color: ${c.hoverAccent};
color: ${c.buttonText};
min-height: 40px;
min-width: 36px;
margin-bottom: 1px;
margin-right: 1px;
`);
});
}
else {
items.forEach((item) => {
item[1].setCss(`
background-color: ${c.buttonAccent};
color: ${c.buttonText};
min-height: 42px;
min-width: 38px;
`);
});
}
}
else {
self.setCss(`
* { color: ${c.buttonAccent}; }
*:hover { color: ${c.hoverAccent}; }
`);
}
}
});
},
}),
});
export const ShuffleButton = (
player: MprisPlayer,
colors: Var<any>,
) => PlayerButton({
player,
colors,
items: [
['true', Label({
class_name: 'shuffle enabled',
label: icons.mpris.shuffle.enabled,
})],
['false', Label({
class_name: 'shuffle disabled',
label: icons.mpris.shuffle.disabled,
})],
],
onClick: 'shuffle',
prop: 'shuffleStatus',
});
export const LoopButton = (
player: MprisPlayer,
colors: Var<any>,
) => PlayerButton({
player,
colors,
items: [
['None', Label({
class_name: 'loop none',
label: icons.mpris.loop.none,
})],
['Track', Label({
class_name: 'loop track',
label: icons.mpris.loop.track,
})],
['Playlist', Label({
class_name: 'loop playlist',
label: icons.mpris.loop.playlist,
})],
],
onClick: 'loop',
prop: 'loopStatus',
});
export const PlayPauseButton = (
player: MprisPlayer,
colors: Var<any>,
) => PlayerButton({
player,
colors,
items: [
['Playing', Label({
class_name: 'pausebutton playing',
label: icons.mpris.playing,
})],
['Paused', Label({
class_name: 'pausebutton paused',
label: icons.mpris.paused,
})],
['Stopped', Label({
class_name: 'pausebutton stopped paused',
label: icons.mpris.stopped,
})],
],
onClick: 'playPause',
prop: 'playBackStatus',
});
export const PreviousButton = (
player: MprisPlayer,
colors: Var<any>,
) => PlayerButton({
player,
colors,
items: [
['true', Label({
class_name: 'previous',
label: icons.mpris.prev,
})],
['false', Label({
class_name: 'previous',
label: icons.mpris.prev,
})],
],
onClick: 'previous',
prop: 'canGoPrev',
});
export const NextButton = (
player: MprisPlayer,
colors: Var<any>,
) => PlayerButton({
player,
colors,
items: [
['true', Label({
class_name: 'next',
label: icons.mpris.next,
})],
['false', Label({
class_name: 'next',
label: icons.mpris.next,
})],
],
onClick: 'next',
prop: 'canGoNext',
});

View file

@ -0,0 +1,207 @@
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, CenterBox } from 'resource:///com/github/Aylur/ags/widget.js';
import * as mpris from './mpris.ts';
import PlayerGesture from './gesture.ts';
import Separator from '../misc/separator.ts';
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
const SPACING = 8;
// Types
import { MprisPlayer } from 'types/service/mpris.ts';
import AgsOverlay from 'types/widgets/overlay.ts';
import { Variable as Var } from 'types/variable';
import AgsBox from 'types/widgets/box.ts';
const Top = (
player: MprisPlayer,
overlay: AgsOverlay,
) => Box({
class_name: 'top',
hpack: 'start',
vpack: 'start',
children: [
mpris.PlayerIcon(player, overlay),
],
});
const Center = (
player: MprisPlayer,
colors: Var<any>,
) => Box({
class_name: 'center',
children: [
CenterBox({
vertical: true,
start_widget: Box({
class_name: 'metadata',
vertical: true,
hpack: 'start',
vpack: 'center',
hexpand: true,
children: [
mpris.TitleLabel(player),
mpris.ArtistLabel(player),
],
}),
}),
CenterBox({
vertical: true,
center_widget: mpris.PlayPauseButton(player, colors),
}),
],
});
const Bottom = (
player: MprisPlayer,
colors: Var<any>,
) => Box({
class_name: 'bottom',
children: [
mpris.PreviousButton(player, colors),
Separator(SPACING),
mpris.PositionSlider(player, colors),
Separator(SPACING),
mpris.NextButton(player, colors),
Separator(SPACING),
mpris.ShuffleButton(player, colors),
Separator(SPACING),
mpris.LoopButton(player, colors),
],
});
const PlayerBox = (
player: MprisPlayer,
colors: Var<any>,
overlay: AgsOverlay,
) => {
const widget = mpris.CoverArt(player, colors, {
class_name: `player ${player.name}`,
hexpand: true,
start_widget: Top(player, overlay),
center_widget: Center(player, colors),
end_widget: Bottom(player, colors),
});
widget.visible = false;
return widget;
};
export default () => {
const content = PlayerGesture({
attribute: {
players: new Map(),
setup: false,
},
setup: (self) => {
self
.hook(Mpris, (_: AgsOverlay, bus_name: string) => {
const players = self.attribute.players;
if (players.has(bus_name)) {
return;
}
// Sometimes the signal doesn't give the bus_name
if (!bus_name) {
const player = Mpris.players.find((p) => {
return !players.has(p.bus_name);
});
if (player) {
bus_name = player.bus_name;
}
else {
return;
}
}
// Get the one on top so we can move it up later
const previousFirst = self.overlays.at(-1);
// Make the new player
const player = Mpris.getPlayer(bus_name);
const Colors = Variable(null);
if (!player) {
return;
}
players.set(
bus_name,
PlayerBox(
player,
Colors,
content.get_children()[0] as AgsOverlay,
),
);
self.overlays = Array.from(players.values())
.map((widget) => widget) as Array<AgsBox>;
const includes = self.attribute
.includesWidget(previousFirst);
// Select favorite player at startup
const attrs = self.attribute;
if (!attrs.setup && players.has(FAVE_PLAYER)) {
attrs.moveToTop(players.get(FAVE_PLAYER));
attrs.setup = true;
}
// Move previousFirst on top again
else if (includes) {
attrs.moveToTop(previousFirst);
}
}, 'player-added')
.hook(Mpris, (_: AgsOverlay, bus_name: string) => {
const players = self.attribute.players;
if (!bus_name || !players.has(bus_name)) {
return;
}
// Get the one on top so we can move it up later
const previousFirst = self.overlays.at(-1);
// Remake overlays without deleted one
players.delete(bus_name);
self.overlays = Array.from(players.values())
.map((widget) => widget) as Array<AgsBox>;
// Move previousFirst on top again
const includes = self.attribute
.includesWidget(previousFirst);
if (includes) {
self.attribute.moveToTop(previousFirst);
}
}, 'player-closed');
},
});
return Box({
class_name: 'media',
child: content,
});
};

View file

@ -0,0 +1,58 @@
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
const speakerIcons = {
101: 'audio-volume-overamplified-symbolic',
67: 'audio-volume-high-symbolic',
34: 'audio-volume-medium-symbolic',
1: 'audio-volume-low-symbolic',
0: 'audio-volume-muted-symbolic',
};
const micIcons = {
67: 'audio-input-microphone-high-symbolic',
34: 'audio-input-microphone-medium-symbolic',
1: 'audio-input-microphone-low-symbolic',
0: 'audio-input-microphone-muted-symbolic',
};
export const SpeakerIcon = Variable('');
Audio.connect('speaker-changed', () => {
if (!Audio.speaker) {
return;
}
if (Audio.speaker.stream.is_muted) {
SpeakerIcon.value = speakerIcons[0];
}
else {
const vol = Audio.speaker.volume * 100;
for (const threshold of [-1, 0, 33, 66, 100]) {
if (vol > threshold + 1) {
SpeakerIcon.value = speakerIcons[threshold + 1];
}
}
}
});
export const MicIcon = Variable('');
Audio.connect('microphone-changed', () => {
if (!Audio.microphone) {
return;
}
if (Audio.microphone.stream.is_muted) {
MicIcon.value = micIcons[0];
}
else {
const vol = Audio.microphone.volume * 100;
for (const threshold of [-1, 0, 33, 66]) {
if (vol > threshold + 1) {
MicIcon.value = micIcons[threshold + 1];
}
}
}
});

View file

@ -0,0 +1,15 @@
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
export default () => Window({
name: 'bg-gradient',
layer: 'background',
exclusivity: 'ignore',
anchor: ['top', 'bottom', 'left', 'right'],
css: `
background-image: -gtk-gradient (linear,
left top, left bottom,
from(rgba(0, 0, 0, 0.5)),
to(rgba(0, 0, 0, 0)));
`,
});

View file

@ -0,0 +1,14 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
// Types
import AgsWindow from 'types/widgets/window';
export default () => {
(Array.from(App.windows) as Array<[string, AgsWindow]>)
.filter((w) => w[1].attribute?.close_on_unfocus &&
w[1].attribute?.close_on_unfocus !== 'stay')
.forEach((w) => {
App.closeWindow(w[0]);
});
};

View file

@ -0,0 +1,90 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk, Gdk } = imports.gi;
const display = Gdk.Display.get_default();
import * as EventBoxTypes from 'types/widgets/eventbox';
type CursorBox = EventBoxTypes.EventBoxProps & {
on_primary_click_release?(self: EventBoxTypes.default): void;
on_hover?(self: EventBoxTypes.default): void;
on_hover_lost?(self: EventBoxTypes.default): void;
};
export default ({
on_primary_click_release = () => {/**/},
on_hover = () => {/**/},
on_hover_lost = () => {/**/},
attribute,
...props
}: CursorBox) => {
// Make this variable to know if the function should
// be executed depending on where the click is released
const CanRun = Variable(true);
const Disabled = Variable(false);
const cursorBox = EventBox({
...props,
attribute: {
...attribute,
disabled: Disabled,
},
on_primary_click_release: (self) => {
// Every click, do a one shot connect to
// CanRun to wait for location of click
const id = CanRun.connect('changed', () => {
if (CanRun.value && !Disabled.value) {
on_primary_click_release(self);
}
CanRun.disconnect(id);
});
},
// OnHover
}).on('enter-notify-event', (self) => {
on_hover(self);
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
Disabled.value ?
'not-allowed' :
'pointer',
));
self.toggleClassName('hover', true);
// OnHoverLost
}).on('leave-notify-event', (self) => {
on_hover_lost(self);
self.window.set_cursor(null);
self.toggleClassName('hover', false);
// Disabled class
}).hook(Disabled, (self) => {
self.toggleClassName('disabled', Disabled.value);
});
const gesture = Gtk.GestureLongPress.new(cursorBox);
cursorBox.hook(gesture, () => {
const pointer = gesture.get_point(null);
const x = pointer[1];
const y = pointer[2];
if ((!x || !y) || (x === 0 && y === 0)) {
return;
}
CanRun.value = !(
x > cursorBox.get_allocated_width() ||
y > cursorBox.get_allocated_height()
);
}, 'end');
return cursorBox;
};

View file

@ -0,0 +1,52 @@
import { execAsync, readFileAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
const { get_home_dir } = imports.gi.GLib;
type Persist = {
name: string
gobject: typeof imports.gi.GObject
prop: string
condition?: boolean | string // If string, compare following props to this
whenTrue?: boolean | string
whenFalse?: boolean | string
signal?: string
};
export default ({
name,
gobject,
prop,
condition = true,
whenTrue = condition,
whenFalse = false,
signal = 'changed',
}: Persist) => {
const cacheFile = `${get_home_dir()}/.cache/ags/.${name}`;
const stateCmd = () => ['bash', '-c',
`echo ${gobject[prop] === condition} > ${cacheFile}`];
const monitorState = () => {
gobject.connect(signal, () => {
execAsync(stateCmd()).catch(print);
});
};
readFileAsync(cacheFile)
.then((content) => {
// JSON.parse was the only way I found to reliably
// convert a string of 'true' or 'false' into a bool
gobject[prop] = JSON.parse(content) ? whenTrue : whenFalse;
timeout(1000, () => {
monitorState();
});
})
.catch(() => {
execAsync(stateCmd())
.then(() => {
monitorState();
})
.catch(print);
});
};

View file

@ -0,0 +1,379 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, Overlay, Window } from 'resource:///com/github/Aylur/ags/widget.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
// Types
type Allocation = typeof imports.gi.Gtk.Allocation;
type Widget = typeof imports.gi.Gtk.Widget;
import { RevealerProps } from 'types/widgets/revealer';
import { WindowProps } from 'types/widgets/window';
import AgsWindow from 'types/widgets/window';
import AgsBox from 'types/widgets/box';
import AgsOverlay from 'types/widgets/overlay';
import { Binding } from 'types/service';
type PopupWindow = WindowProps & {
transition?: RevealerProps['transition']
transition_duration?: number
bezier?: string
on_open?(self: AgsWindow): void
on_close?(self: AgsWindow): void
blur?: boolean
close_on_unfocus?: 'none' | 'stay' | 'released' | 'clicked'
anchor?: Array<string>
name: string
};
// FIXME: deal with overlay children?
// TODO: make this a new class to be able to edit props
export default ({
transition = 'slide_down',
transition_duration = 800,
bezier = 'cubic-bezier(0.68, -0.4, 0.32, 1.4)',
on_open = () => {/**/},
on_close = () => {/**/},
// Window props
name,
child = Box(),
visible = false,
anchor = [],
layer = 'overlay',
blur = false,
close_on_unfocus = 'released',
...props
}: PopupWindow) => {
const Child = Variable(child);
const AntiClip = Variable(false);
const needsAnticlipping = bezier.match(/-[0-9]/) !== null &&
transition !== 'crossfade';
const attribute = {
set_x_pos: (
alloc: Allocation,
side = 'right' as 'left' | 'right',
) => {
const window = App.getWindow(name) as AgsWindow;
if (!window) {
return;
}
const width = window.get_display()
.get_monitor_at_point(alloc.x, alloc.y)
.get_geometry().width;
window.margins = [
window.margins[0],
side === 'right' ?
(width - alloc.x - alloc.width) :
window.margins[1],
window.margins[2],
side === 'right' ?
window.margins[3] :
(alloc.x - alloc.width),
];
},
get_child: () => Child.value,
set_child: (new_child: Widget) => {
Child.value = new_child;
App.getWindow(name)?.child.show_all();
},
// This is for my custom pointers.ts
close_on_unfocus,
};
if (transition === 'none') {
return Window({
name,
layer,
anchor,
visible: false,
...props,
attribute,
child: Child.bind(),
});
}
const window = Window({
name,
layer,
anchor,
visible: false,
...props,
attribute,
setup: () => {
// Add way to make window open on startup
const id = App.connect('config-parsed', () => {
if (visible) {
App.openWindow(`${name}`);
}
App.disconnect(id);
});
if (blur) {
Hyprland.sendMessage('[[BATCH]] ' +
`keyword layerrule ignorealpha[0.97],${name}; ` +
`keyword layerrule blur,${name}`);
}
},
child: Overlay({
overlays: [Box({
setup: (self) => {
// Make sure child doesn't
// get bigger than it should
const MAX_ANCHORS = 4;
self.hpack = 'center';
self.vpack = 'center';
if (anchor.includes('top') &&
anchor.includes('bottom')) {
self.vpack = 'center';
}
else if (anchor.includes('top')) {
self.vpack = 'start';
}
else if (anchor.includes('bottom')) {
self.vpack = 'end';
}
if (anchor.includes('left') &&
anchor.includes('right')) {
self.hpack = 'center';
}
else if (anchor.includes('left')) {
self.hpack = 'start';
}
else if (anchor.includes('right')) {
self.hpack = 'end';
}
if (anchor.length === MAX_ANCHORS) {
self.hpack = 'center';
self.vpack = 'center';
}
if (needsAnticlipping) {
const reorder_child = (position: number) => {
// If unanchored, we have another anticlip widget
// so we can't change the order
if (anchor.length !== 0) {
for (const ch of self.children) {
if (ch !== Child.value) {
self.reorder_child(ch, position);
return;
}
}
}
};
self.hook(AntiClip, () => {
if (transition === 'slide_down') {
self.vertical = true;
reorder_child(-1);
}
else if (transition === 'slide_up') {
self.vertical = true;
reorder_child(0);
}
else if (transition === 'slide_right') {
self.vertical = false;
reorder_child(-1);
}
else if (transition === 'slide_left') {
self.vertical = false;
reorder_child(0);
}
});
}
},
children: Child.bind().transform((v) => {
if (needsAnticlipping) {
return [
// Add an anticlip widget when unanchored
// to not have a weird animation
anchor.length === 0 && Box({
css: `
min-height: 100px;
min-width: 100px;
padding: 2px;
`,
visible: AntiClip.bind(),
}),
v,
Box({
css: `
min-height: 100px;
min-width: 100px;
padding: 2px;
`,
visible: AntiClip.bind(),
}),
];
}
else {
return [v];
}
}) as Binding<any, any, Widget[]>,
})],
setup: (self) => {
self.on('get-child-position', (_, ch) => {
const overlay = (Child.value as Widget)
.get_parent() as AgsOverlay;
if (ch === overlay) {
const alloc = overlay.get_allocation();
const setAlloc = (v: number) => v - 2 < 0 ? 1 : v;
(self.child as AgsBox).css = `
min-height: ${setAlloc(alloc.height - 2)}px;
min-width: ${setAlloc(alloc.width - 2)}px;
`;
}
});
},
child: Box({
css: `
min-height: 1px;
min-width: 1px;
padding: 1px;
`,
setup: (self) => {
let currentTimeout: number;
self.hook(App, (_, currentName, isOpen) => {
if (currentName === name) {
const overlay = (Child.value as Widget)
.get_parent() as AgsOverlay;
const alloc = overlay.get_allocation();
const height = needsAnticlipping ?
alloc.height + 100 + 10 :
alloc.height + 10;
if (needsAnticlipping) {
AntiClip.value = true;
const thisTimeout = timeout(
transition_duration,
() => {
// Only run the timeout if there isn't a newer timeout
if (thisTimeout === currentTimeout) {
AntiClip.value = false;
}
},
);
currentTimeout = thisTimeout;
}
let css = '';
/* Margin: top | right | bottom | left */
switch (transition) {
case 'slide_down':
css = `margin:
-${height}px
0
${height}px
0
;`;
break;
case 'slide_up':
css = `margin:
${height}px
0
-${height}px
0
;`;
break;
case 'slide_left':
css = `margin:
0
-${height}px
0
${height}px
;`;
break;
case 'slide_right':
css = `margin:
0
${height}px
0
-${height}px
;`;
break;
case 'crossfade':
css = `
opacity: 0;
min-height: 1px;
min-width: 1px;
`;
break;
default:
break;
}
if (isOpen) {
on_open(window);
// To get the animation, we need to set the css
// to hide the widget and then timeout to have
// the animation
overlay.css = css;
timeout(10, () => {
overlay.css = `
transition: margin
${transition_duration}ms ${bezier},
opacity
${transition_duration}ms ${bezier};
`;
});
}
else {
timeout(transition_duration, () => {
on_close(window);
});
overlay.css = `${css}
transition: margin
${transition_duration}ms ${bezier},
opacity
${transition_duration}ms ${bezier};
`;
}
}
});
},
}),
}),
});
return window;
};

View file

@ -0,0 +1,13 @@
import { Box } from 'resource:///com/github/Aylur/ags/widget.js';
export default (size: number, {
vertical = false,
css = '',
...props
} = {}) => {
return Box({
css: `${vertical ? 'min-height' : 'min-width'}: ${size}px; ${css}`,
...props,
});
};

View file

@ -0,0 +1,269 @@
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, Icon, Label, Button } from 'resource:///com/github/Aylur/ags/widget.js';
import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
const { GLib } = imports.gi;
import Gesture from './gesture.ts';
import CursorBox from '../misc/cursorbox.ts';
// Types
import { Notification as NotifObj } from 'types/service/notifications.ts';
import AgsEventBox from 'types/widgets/eventbox.ts';
import { Client } from 'types/service/hyprland.ts';
type NotificationWidget = {
notif: NotifObj
slideIn?: 'Left' | 'Right'
command?(): void
};
const setTime = (time: number) => {
return GLib.DateTime
.new_from_unix_local(time)
.format('%H:%M');
};
const getDragState = (box: AgsEventBox) => (box.get_parent()?.get_parent()
?.get_parent()?.get_parent()?.get_parent() as AgsEventBox)
?.attribute.dragging;
const NotificationIcon = (notif: NotifObj) => {
let iconCmd = (box: AgsEventBox):void => {
console.log(box);
};
if (notif.app_entry && Applications.query(notif.app_entry).length > 0) {
const app = Applications.query(notif.app_entry)[0];
let wmClass = app.app.get_string('StartupWMClass');
if (app.app?.get_filename()?.includes('discord')) {
wmClass = 'discord';
}
if (wmClass != null) {
iconCmd = (box) => {
if (!getDragState(box)) {
if (wmClass === 'thunderbird') {
Hyprland.sendMessage('dispatch ' +
'togglespecialworkspace thunder');
}
else if (wmClass === 'Spotify') {
Hyprland.sendMessage('dispatch ' +
'togglespecialworkspace spot');
}
else {
Hyprland.sendMessage('j/clients').then((msg) => {
const clients = JSON.parse(msg) as Array<Client>;
const classes = [] as Array<string>;
for (const key of clients) {
if (key.class) {
classes.push(key.class);
}
}
if (wmClass && classes.includes(wmClass)) {
Hyprland.sendMessage('dispatch ' +
`focuswindow ^(${wmClass})`);
}
else {
Hyprland.sendMessage('dispatch workspace empty')
.then(() => {
app.launch();
});
}
});
}
globalThis.closeAll();
}
};
}
}
if (notif.image) {
return CursorBox({
on_primary_click_release: iconCmd,
child: Box({
vpack: 'start',
hexpand: false,
class_name: 'icon img',
css: `
background-image: url("${notif.image}");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
min-width: 78px;
min-height: 78px;
`,
}),
});
}
let icon = 'dialog-information-symbolic';
if (lookUpIcon(notif.app_icon)) {
icon = notif.app_icon;
}
if (notif.app_entry && lookUpIcon(notif.app_entry)) {
icon = notif.app_entry;
}
return CursorBox({
on_primary_click_release: iconCmd,
child: Box({
vpack: 'start',
hexpand: false,
class_name: 'icon',
css: `
min-width: 78px;
min-height: 78px;
`,
children: [Icon({
icon, size: 58,
hpack: 'center',
hexpand: true,
vpack: 'center',
vexpand: true,
})],
}),
});
};
// Make a variable to connect to for Widgets
// to know when there are notifs or not
export const HasNotifs = Variable(false);
export const Notification = ({
notif,
slideIn = 'Left',
command = () => {/**/},
}: NotificationWidget) => {
if (!notif) {
return;
}
const BlockedApps = [
'Spotify',
];
if (BlockedApps.find((app) => app === notif.app_name)) {
notif.close();
return;
}
HasNotifs.value = Notifications.notifications.length > 0;
// Init notif
const notifWidget = Gesture({
command,
slideIn,
id: notif.id,
});
// Add body to notif
(notifWidget.child as AgsEventBox).add(Box({
class_name: `notification ${notif.urgency}`,
vexpand: false,
// Notification
child: Box({
vertical: true,
children: [
// Content
Box({
children: [
NotificationIcon(notif),
Box({
hexpand: true,
vertical: true,
children: [
// Top of Content
Box({
children: [
// Title
Label({
class_name: 'title',
xalign: 0,
justification: 'left',
hexpand: true,
max_width_chars: 24,
truncate: 'end',
wrap: true,
label: notif.summary,
use_markup: notif.summary
.startsWith('<'),
}),
// Time
Label({
class_name: 'time',
vpack: 'start',
label: setTime(notif.time),
}),
// Close button
CursorBox({
child: Button({
class_name: 'close-button',
vpack: 'start',
on_primary_click_release: () =>
notif.close(),
child: Icon('window-close' +
'-symbolic'),
}),
}),
],
}),
// Description
Label({
class_name: 'description',
hexpand: true,
use_markup: true,
xalign: 0,
justification: 'left',
label: notif.body,
wrap: true,
}),
],
}),
],
}),
// Actions
Box({
class_name: 'actions',
children: notif.actions.map((action) => Button({
class_name: 'action-button',
hexpand: true,
on_primary_click_release: () => notif.invoke(action.id),
child: Label(action.label),
})),
}),
],
}),
}));
return notifWidget;
};

View file

@ -0,0 +1,25 @@
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
import NotifCenterWidget from './center.ts';
import PopUpsWidget from './popup.ts';
import PopupWindow from '../misc/popup.ts';
export const NotifPopups = () => Window({
name: 'notifications',
anchor: ['bottom', 'left'],
monitor: 1,
child: PopUpsWidget(),
});
export const NotifCenter = () => PopupWindow({
name: 'notification-center',
anchor: ['bottom', 'right'],
transition: 'slide_up',
monitor: 1,
child: NotifCenterWidget(),
});

View file

@ -0,0 +1,159 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { Label, Box, Icon, Scrollable, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { Notification, HasNotifs } from './base.ts';
import CursorBox from '../misc/cursorbox.ts';
// Types
import AgsBox from 'types/widgets/box.ts';
import { Notification as NotifObj } from 'resource:///com/github/Aylur/ags/service/notifications.js';
const addNotif = (box: AgsBox, notif: NotifObj) => {
if (notif) {
const NewNotif = Notification({
notif,
slideIn: 'Right',
command: () => notif.close(),
});
if (NewNotif) {
box.pack_end(NewNotif, false, false, 0);
box.show_all();
}
}
};
const NotificationList = () => Box({
vertical: true,
vexpand: true,
vpack: 'start',
visible: HasNotifs.bind(),
setup: (self) => {
self
.hook(Notifications, (box, id) => {
// Handle cached notifs
if (box.children.length === 0) {
Notifications.notifications.forEach((n) => {
addNotif(box, n);
});
}
else if (id) {
const notifObj = Notifications.getNotification(id);
if (notifObj) {
addNotif(box, notifObj);
}
}
}, 'notified')
.hook(Notifications, (box, id) => {
const notif = (box.children as Array<AgsBox>)
.find((ch) => ch.attribute.id === id);
if (notif?.sensitive) {
notif.attribute.slideAway('Right');
}
}, 'closed');
},
});
const ClearButton = () => CursorBox({
class_name: 'clear',
on_primary_click_release: () => {
Notifications.clear();
timeout(1000, () => App.closeWindow('notification-center'));
},
setup: (self) => {
self.hook(HasNotifs, () => {
self.attribute.disabled?.setValue(!HasNotifs.value);
});
},
child: Box({
children: [
Label('Clear '),
Icon({
setup: (self) => {
self.hook(Notifications, () => {
self.icon = Notifications.notifications.length > 0 ?
'user-trash-full-symbolic' :
'user-trash-symbolic';
});
},
}),
],
}),
});
const Header = () => Box({
class_name: 'header',
children: [
Label({
label: 'Notifications',
hexpand: true,
xalign: 0,
}),
ClearButton(),
],
});
const Placeholder = () => Revealer({
transition: 'crossfade',
reveal_child: HasNotifs.bind()
.transform((v) => !v),
child: Box({
class_name: 'placeholder',
vertical: true,
vpack: 'center',
hpack: 'center',
vexpand: true,
hexpand: true,
children: [
Icon('notification-disabled-symbolic'),
Label('Your inbox is empty'),
],
}),
});
export default () => Box({
class_name: 'notification-center',
vertical: true,
children: [
Header(),
Box({
class_name: 'notification-wallpaper-box',
children: [
Scrollable({
class_name: 'notification-list-box',
hscroll: 'never',
vscroll: 'automatic',
child: Box({
class_name: 'notification-list',
vertical: true,
children: [
NotificationList(),
Placeholder(),
],
}),
}),
],
}),
],
});

View file

@ -0,0 +1,211 @@
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { Box, EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { HasNotifs } from './base.ts';
const { Gdk, Gtk } = imports.gi;
const display = Gdk.Display.get_default();
// Types
import AgsBox from 'types/widgets/box.ts';
const MAX_OFFSET = 200;
const OFFSCREEN = 300;
const ANIM_DURATION = 500;
const SLIDE_MIN_THRESHOLD = 10;
const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
const SQUEEZED = 'margin-bottom: -70px; margin-top: -70px;';
const MAX_LEFT = `
margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const MAX_RIGHT = `
margin-left: ${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const slideLeft = `${TRANSITION} ${MAX_LEFT}
margin-top: 0px;
margin-bottom: 0px;
opacity: 0;`;
const squeezeLeft = `${TRANSITION} ${MAX_LEFT} ${SQUEEZED} opacity: 0;`;
const slideRight = `${TRANSITION} ${MAX_RIGHT}
margin-top: 0px;
margin-bottom: 0px;
opacity: 0;`;
const squeezeRight = `${TRANSITION} ${MAX_RIGHT} ${SQUEEZED} opacity: 0;`;
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
export default ({
id,
slideIn = 'Left',
command = () => {/**/},
...props
}) => {
const widget = EventBox({
...props,
setup: (self) => {
self
// OnClick
.on('button-press-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
})
// OnRelease
.on('button-release-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grab',
));
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grab',
));
self.toggleClassName('hover', true);
if (!self.attribute.hovered) {
self.attribute.hovered = true;
}
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
if (self.attribute.hovered) {
self.attribute.hovered = false;
}
});
},
attribute: {
dragging: false,
hovered: false,
ready: false,
id,
slideAway: (side: 'Left' | 'Right') => {
// Slide away
(widget.child as AgsBox)
.setCss(side === 'Left' ? slideLeft : slideRight);
// Make it uninteractable
widget.sensitive = false;
timeout(ANIM_DURATION - 100, () => {
// Reduce height after sliding away
(widget.child as AgsBox)?.setCss(side === 'Left' ?
squeezeLeft :
squeezeRight);
timeout(ANIM_DURATION, () => {
// Kill notif and update HasNotifs after anim is done
command();
HasNotifs.value = Notifications
.notifications.length > 0;
(widget.get_parent() as AgsBox)?.remove(widget);
});
});
},
},
});
const gesture = Gtk.GestureDrag.new(widget);
widget.add(Box({
css: squeezeLeft,
setup: (self) => {
self
// When dragging
.hook(gesture, () => {
let offset = gesture.get_offset()[1];
if (offset === 0) {
return;
}
// Slide right
if (offset > 0) {
self.setCss(`
margin-top: 0px; margin-bottom: 0px;
opacity: 1; transition: none;
margin-left: ${offset}px;
margin-right: -${offset}px;
`);
}
// Slide left
else {
offset = Math.abs(offset);
self.setCss(`
margin-top: 0px; margin-bottom: 0px;
opacity: 1; transition: none;
margin-right: ${offset}px;
margin-left: -${offset}px;
`);
}
// Put a threshold on if a click is actually dragging
widget.attribute.dragging =
Math.abs(offset) > SLIDE_MIN_THRESHOLD;
widget.cursor = 'grabbing';
}, 'drag-update')
// On drag end
.hook(gesture, () => {
// Make it slide in on init
if (!widget.attribute.ready) {
// Reverse of slideAway, so it started at squeeze, then we go to slide
self.setCss(slideIn === 'Left' ?
slideLeft :
slideRight);
timeout(ANIM_DURATION, () => {
// Then we go to center
self.setCss(defaultStyle);
timeout(ANIM_DURATION, () => {
widget.attribute.ready = true;
});
});
return;
}
const offset = gesture.get_offset()[1];
// If crosses threshold after letting go, slide away
if (Math.abs(offset) > MAX_OFFSET) {
if (offset > 0) {
widget.attribute.slideAway('Right');
}
else {
widget.attribute.slideAway('Left');
}
}
else {
self.setCss(defaultStyle);
widget.cursor = 'grab';
widget.attribute.dragging = false;
}
}, 'drag-end');
},
}));
return widget;
};

View file

@ -0,0 +1,77 @@
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { Box } from 'resource:///com/github/Aylur/ags/widget.js';
import { interval } from 'resource:///com/github/Aylur/ags/utils.js';
import GLib from 'gi://GLib';
import { Notification } from './base.ts';
const DELAY = 2000;
// Types
import AgsBox from 'types/widgets/box.ts';
export default () => Box({
vertical: true,
// Needed so it occupies space at the start
css: 'padding: 1px;',
setup: (self) => {
const addPopup = (id: number) => {
if (!id) {
return;
}
const notif = Notifications.getNotification(id);
if (notif) {
const NewNotif = Notification({
notif,
command: () => {
if (notif.popup) {
notif.dismiss();
}
},
});
if (NewNotif) {
// Use this instead of add to put it at the top
self.pack_end(NewNotif, false, false, 0);
self.show_all();
}
}
};
const handleDismiss = (id: number, force = false) => {
const notif = (self.children as Array<AgsBox>)
.find((ch) => ch.attribute.id === id);
if (!notif) {
return;
}
// If notif isn't hovered or was closed, slide away
if (!notif.attribute.hovered || force) {
notif.attribute.slideAway('Left');
}
// If notif is hovered, delay close
else if (notif.attribute.hovered) {
const intervalId = interval(DELAY, () => {
if (!notif.attribute.hovered && intervalId) {
notif.attribute.slideAway('Left');
GLib.source_remove(intervalId);
}
});
}
};
self
.hook(Notifications, (_, id) => addPopup(id), 'notified')
.hook(Notifications, (_, id) => handleDismiss(id), 'dismissed')
.hook(Notifications, (_, id) => handleDismiss(id, true), 'closed');
},
});

View file

@ -0,0 +1,22 @@
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
import NotifCenterWidget from './center.ts';
import PopUpsWidget from './popup.ts';
import PopupWindow from '../misc/popup.ts';
export const NotifPopups = () => Window({
name: 'notifications',
anchor: ['top', 'left'],
child: PopUpsWidget(),
});
const TOP_MARGIN = 6;
export const NotifCenter = () => PopupWindow({
name: 'notification-center',
anchor: ['top', 'right'],
margins: [TOP_MARGIN, 0, 0, 0],
child: NotifCenterWidget(),
});

View file

@ -0,0 +1,155 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { execAsync, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
const { Gtk } = imports.gi;
import Tablet from '../../services/tablet.ts';
const KEY_N = 249;
const HIDDEN_MARGIN = 340;
const ANIM_DURATION = 700;
// Types
import AgsWindow from 'types/widgets/window.ts';
import AgsBox from 'types/widgets/box.ts';
const releaseAllKeys = () => {
const keycodes = Array.from(Array(KEY_N).keys());
execAsync([
'ydotool', 'key',
...keycodes.map((keycode) => `${keycode}:0`),
]).catch(print);
};
export default (window: AgsWindow) => {
const gesture = Gtk.GestureDrag.new(window);
const child = window.child as AgsBox;
child.setCss(`margin-bottom: -${HIDDEN_MARGIN}px;`);
let signals = [] as Array<number>;
window.attribute = {
setVisible: (state: boolean) => {
if (state) {
window.visible = true;
window.attribute.setSlideDown();
child.setCss(`
transition: margin-bottom 0.7s
cubic-bezier(0.36, 0, 0.66, -0.56);
margin-bottom: 0px;
`);
}
else {
timeout(ANIM_DURATION + 10, () => {
if (!Tablet.tabletMode) {
window.visible = false;
}
});
releaseAllKeys();
window.attribute.setSlideUp();
child.setCss(`
transition: margin-bottom 0.7s
cubic-bezier(0.36, 0, 0.66, -0.56);
margin-bottom: -${HIDDEN_MARGIN}px;
`);
}
},
killGestureSigs: () => {
signals.forEach((id) => {
gesture.disconnect(id);
});
signals = [];
},
setSlideUp: () => {
window.attribute.killGestureSigs();
// Begin drag
signals.push(
gesture.connect('drag-begin', () => {
Hyprland.sendMessage('j/cursorpos').then((out) => {
gesture.startY = JSON.parse(out).y;
});
}),
);
// Update drag
signals.push(
gesture.connect('drag-update', () => {
Hyprland.sendMessage('j/cursorpos').then((out) => {
const currentY = JSON.parse(out).y;
const offset = gesture.startY - currentY;
if (offset < 0) {
return;
}
(window.child as AgsBox).setCss(`
margin-bottom: ${offset - HIDDEN_MARGIN}px;
`);
});
}),
);
// End drag
signals.push(
gesture.connect('drag-end', () => {
(window.child as AgsBox).setCss(`
transition: margin-bottom 0.5s ease-in-out;
margin-bottom: -${HIDDEN_MARGIN}px;
`);
}),
);
},
setSlideDown: () => {
window.attribute.killGestureSigs();
// Begin drag
signals.push(
gesture.connect('drag-begin', () => {
Hyprland.sendMessage('j/cursorpos').then((out) => {
gesture.startY = JSON.parse(out).y;
});
}),
);
// Update drag
signals.push(
gesture.connect('drag-update', () => {
Hyprland.sendMessage('j/cursorpos').then((out) => {
const currentY = JSON.parse(out).y;
const offset = gesture.startY - currentY;
if (offset > 0) {
return;
}
(window.child as AgsBox).setCss(`
margin-bottom: ${offset}px;
`);
});
}),
);
// End drag
signals.push(
gesture.connect('drag-end', () => {
(window.child as AgsBox).setCss(`
transition: margin-bottom 0.5s ease-in-out;
margin-bottom: 0px;
`);
}),
);
},
};
return window;
};

View file

@ -0,0 +1,104 @@
// TODO: right Ctrl https://handwiki.org/wiki/images/4/41/KB_Canadian_Multilingual_Standard.svg
export const defaultOskLayout = 'qwerty_custom';
export const oskLayouts = {
qwerty_custom: {
name: 'QWERTY - Custom',
name_short: 'CSA',
comment: 'Like physical keyboard',
// 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: 'Home', shape: 'fn', keycode: 110 },
{ keytype: 'normal', label: 'End', shape: 'fn', keycode: 115 },
{ keytype: 'normal', label: 'Del', shape: 'fn', keycode: 111 },
],
[
{ keytype: 'normal', label: '/', labelShift: '\\', labelAltGr: '|', 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: '#', labelAltGr: '¤', 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: '&', labelAltGr: '{', shape: 'normal', keycode: 8 },
{ keytype: 'normal', label: '8', labelShift: '*', labelAltGr: '}', shape: 'normal', keycode: 9 },
{ keytype: 'normal', label: '9', labelShift: '(', labelAltGr: '[', shape: 'normal', keycode: 10 },
{ keytype: 'normal', label: '0', labelShift: ')', labelAltGr: ']', shape: 'normal', keycode: 11 },
{ keytype: 'normal', label: '-', labelShift: '_', shape: 'normal', keycode: 12 },
{ keytype: 'normal', label: '=', labelShift: '+', labelAltGr: '¬', 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', labelAltGr: '€', 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: '"', labelAltGr: '`', shape: 'normal', keycode: 26 },
{ keytype: 'normal', label: 'ç', labelShift: 'Ç', labelAltGr: '~', 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: ':', labelAltGr: '°', 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', labelAltGr: '«', shape: 'normal', keycode: 44 },
{ keytype: 'normal', label: 'x', labelShift: 'X', labelAltGr: '»', 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: "'", labelAltGr: '<', shape: 'normal', keycode: 51 },
{ keytype: 'normal', label: '.', labelShift: '"', labelAltGr: '>', 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 },
{ keytype: 'modkey', label: 'Super', shape: 'normal', keycode: 125 },
{ keytype: 'modkey', label: 'Alt', shape: 'normal', keycode: 56 },
{ keytype: 'normal', label: 'Space', shape: 'space', keycode: 57 },
{ keytype: 'normal', label: 'Space', shape: 'space', keycode: 57 },
{ keytype: 'modkey', label: 'AltGr', shape: 'normal', keycode: 100 },
{ keytype: 'normal', label: 'PrtSc', shape: 'fn', keycode: 99 },
{ keytype: 'modkey', label: 'Ctrl', shape: 'control', keycode: 97 },
],
],
},
};

View file

@ -0,0 +1,167 @@
import { Box, CenterBox, Label, ToggleButton } from 'resource:///com/github/Aylur/ags/widget.js';
const { Gdk } = imports.gi;
const display = Gdk.Display.get_default();
import Separator from '../misc/separator.ts';
import RoundedCorner from '../corners/screen-corners.ts';
import Key from './keys.ts';
import { defaultOskLayout, oskLayouts } from './keyboard-layouts.ts';
const keyboardLayout = defaultOskLayout;
const keyboardJson = oskLayouts[keyboardLayout];
const L_KEY_PER_ROW = [8, 7, 6, 6, 6, 4]; // eslint-disable-line
const COLOR = 'rgba(0, 0, 0, 0.3)';
const SPACING = 4;
// Types
import AgsWindow from 'types/widgets/window.ts';
import AgsBox from 'types/widgets/box.ts';
export default (window: AgsWindow) => Box({
vertical: true,
children: [
CenterBox({
hpack: 'center',
start_widget: RoundedCorner('bottomright', `
background-color: ${COLOR};
`),
center_widget: CenterBox({
class_name: 'thingy',
css: `background: ${COLOR};`,
center_widget: Box({
hpack: 'center',
class_name: 'settings',
children: [
ToggleButton({
class_name: 'button',
active: true,
vpack: 'center',
setup: (self) => {
self
.on('toggled', () => {
self.toggleClassName(
'toggled',
self.get_active(),
);
window.exclusivity = self.get_active() ?
'exclusive' :
'normal';
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(
Gdk.Cursor.new_from_name(
display,
'pointer',
),
);
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
child: Label('Exclusive'),
}),
],
}),
}),
end_widget: RoundedCorner('bottomleft', `
background-color: ${COLOR};
`),
}),
CenterBox({
css: `background: ${COLOR};`,
class_name: 'osk',
hexpand: true,
start_widget: Box({
class_name: 'left-side side',
hpack: 'start',
vertical: true,
children: keyboardJson.keys.map((row, rowIndex) => {
const keys = [] as Array<AgsBox>;
row.forEach((key, keyIndex) => {
if (keyIndex < L_KEY_PER_ROW[rowIndex]) {
keys.push(Key(key));
}
});
return Box({
vertical: true,
children: [
Box({
class_name: 'row',
children: [
Separator(SPACING),
...keys,
],
}),
Separator(SPACING, { vertical: true }),
],
});
}),
}),
center_widget: Box({
hpack: 'center',
vpack: 'center',
children: [
],
}),
end_widget: Box({
class_name: 'right-side side',
hpack: 'end',
vertical: true,
children: keyboardJson.keys.map((row, rowIndex) => {
const keys = [] as Array<AgsBox>;
row.forEach((key, keyIndex) => {
if (keyIndex >= L_KEY_PER_ROW[rowIndex]) {
keys.push(Key(key));
}
});
return Box({
vertical: true,
children: [
Box({
hpack: 'end',
class_name: 'row',
children: keys,
}),
Separator(SPACING, { vertical: true }),
],
});
}),
}),
}),
],
});

View file

@ -0,0 +1,249 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Brightness from '../../services/brightness.ts';
import { Box, EventBox, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
const { Gdk, Gtk } = imports.gi;
const display = Gdk.Display.get_default();
import Separator from '../misc/separator.ts';
// Keep track of when a non modifier key
// is clicked to release all modifiers
const NormalClick = Variable(false);
// Keep track of modifier statuses
const Super = Variable(false);
const LAlt = Variable(false);
const LCtrl = Variable(false);
const AltGr = Variable(false);
const RCtrl = Variable(false);
const Caps = Variable(false);
Brightness.connect('caps', (_, state) => {
Caps.value = state;
});
// Assume both shifts are the same for key.labelShift
const LShift = Variable(false);
const RShift = Variable(false);
const Shift = Variable(false);
LShift.connect('changed', () => {
Shift.value = LShift.value || RShift.value;
});
RShift.connect('changed', () => {
Shift.value = LShift.value || RShift.value;
});
const SPACING = 4;
const LSHIFT_CODE = 42;
const LALT_CODE = 56;
const LCTRL_CODE = 29;
// Types
import { Variable as Var } from 'types/variable.ts';
type Key = {
keytype: string,
label: string,
labelShift?: string,
labelAltGr?: string,
shape: string,
keycode: number
};
const ModKey = (key: Key) => {
let Mod: Var<any>;
if (key.label === 'Super') {
Mod = Super;
}
// Differentiate left and right mods
else if (key.label === 'Shift' && key.keycode === LSHIFT_CODE) {
Mod = LShift;
}
else if (key.label === 'Alt' && key.keycode === LALT_CODE) {
Mod = LAlt;
}
else if (key.label === 'Ctrl' && key.keycode === LCTRL_CODE) {
Mod = LCtrl;
}
else if (key.label === 'Shift') {
Mod = RShift;
}
else if (key.label === 'AltGr') {
Mod = AltGr;
}
else if (key.label === 'Ctrl') {
Mod = RCtrl;
}
const label = Label({
class_name: `mod ${key.label}`,
label: key.label,
});
const button = EventBox({
class_name: 'key',
on_primary_click_release: () => {
console.log('mod toggled');
execAsync(`ydotool key ${key.keycode}:${Mod.value ? 0 : 1}`);
label.toggleClassName('active', !Mod.value);
Mod.value = !Mod.value;
},
setup: (self) => {
self
.hook(NormalClick, () => {
Mod.value = false;
label.toggleClassName('active', false);
execAsync(`ydotool key ${key.keycode}:0`);
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
child: label,
});
return Box({
children: [
button,
Separator(SPACING),
],
});
};
const RegularKey = (key: Key) => {
const widget = EventBox({
class_name: 'key',
child: Label({
class_name: `normal ${key.label}`,
label: key.label,
setup: (self) => {
self
.hook(Shift, () => {
if (!key.labelShift) {
return;
}
self.label = Shift.value ? key.labelShift : key.label;
})
.hook(Caps, () => {
if (key.label === 'Caps') {
self.toggleClassName('active', Caps.value);
return;
}
if (!key.labelShift) {
return;
}
if (key.label.match(/[A-Za-z]/)) {
self.label = Caps.value ?
key.labelShift :
key.label;
}
})
.hook(AltGr, () => {
if (!key.labelAltGr) {
return;
}
self.toggleClassName('altgr', AltGr.value);
self.label = AltGr.value ? key.labelAltGr : key.label;
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
}),
});
const gesture = Gtk.GestureLongPress.new(widget);
gesture.delay_factor = 1.0;
// Long press
widget.hook(gesture, () => {
const pointer = gesture.get_point(null);
const x = pointer[1];
const y = pointer[2];
if ((!x || !y) || (x === 0 && y === 0)) {
return;
}
console.log('Not implemented yet');
// TODO: popup menu for accents
}, 'pressed');
// OnPrimaryClickRelease
widget.hook(gesture, () => {
const pointer = gesture.get_point(null);
const x = pointer[1];
const y = pointer[2];
if ((!x || !y) || (x === 0 && y === 0)) {
return;
}
console.log('key clicked');
execAsync(`ydotool key ${key.keycode}:1`);
execAsync(`ydotool key ${key.keycode}:0`);
NormalClick.value = true;
}, 'cancelled');
return Box({
children: [
widget,
Separator(SPACING),
],
});
};
export default (key: Key) => key.keytype === 'normal' ?
RegularKey(key) :
ModKey(key);

View file

@ -0,0 +1,37 @@
import { Window } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import Tablet from '../../services/tablet.ts';
import Gesture from './gesture.ts';
import Keyboard from './keyboard.ts';
// Start ydotool daemon
execAsync('ydotoold').catch(print);
// Window
export default () => {
const window = Window({
name: 'osk',
visible: false,
layer: 'overlay',
anchor: ['left', 'bottom', 'right'],
setup: (self) => {
self
.hook(Tablet, (_, state) => {
self.attribute.setVisible(state);
}, 'osk-toggled')
.hook(Tablet, () => {
if (!Tablet.tabletMode && !Tablet.oskState) {
window.visible = false;
}
}, 'mode-toggled');
},
});
window.child = Keyboard(window);
return Gesture(window);
};

View file

@ -0,0 +1,65 @@
import { Box, Icon, ProgressBar } from 'resource:///com/github/Aylur/ags/widget.js';
const Y_POS = 80;
// Types
import AgsBox from 'types/widgets/box';
import { IconProps } from 'types/widgets/icon';
import { GObject } from 'gi://GObject';
import AgsStack from 'types/widgets/stack';
type Widget = typeof imports.gi.Gtk.Widget;
import { Connectable } from 'types/widgets/widget';
import AgsProgressBar from 'types/widgets/progressbar';
type ConnectFunc = (self?: AgsProgressBar) => void;
type OSD = {
stack: AgsStack
icon: string | IconProps
info: {
mod: GObject.Object
signal?: string
logic?(self: AgsProgressBar): void
widget?: Widget
}
};
export default ({ stack, icon, info }: OSD) => {
let connectFunc: ConnectFunc;
const osd = Box({
css: `margin-bottom: ${Y_POS}px;`,
children: [Box({
class_name: 'osd',
children: [
Icon({
hpack: 'start',
// Can take a string or an object of props
...(typeof icon === 'string' ? { icon } : icon),
}),
// Can take a static widget instead of a progressbar
info.logic ?
ProgressBar({ vpack: 'center' }) :
info.widget,
],
})],
});
// Handle requests to show the OSD
// Different wether it's a bar or static
if (info.logic) {
connectFunc = (self) => new Promise<void>((r) => {
if (info.logic && self) {
info.logic(self);
}
r();
}).then(() => stack.attribute.popup(osd));
}
else {
connectFunc = () => stack.attribute.popup(osd);
}
((osd.children[0] as AgsBox).children[1] as Connectable<AgsProgressBar>)
.hook(info.mod, connectFunc, info.signal);
return osd;
};

View file

@ -0,0 +1,65 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { Stack } from 'resource:///com/github/Aylur/ags/widget.js';
import PopupWindow from '../misc/popup.ts';
import AgsBox from 'types/widgets/box.ts';
import AgsStack from 'types/widgets/stack.ts';
// Import all the OSDs as an array
const OSDList = [] as Array<(stack: AgsStack) => AgsBox>;
import * as Modules from './osds.ts';
for (const osd in Modules) {
OSDList.push(Modules[osd]);
} // Array
const HIDE_DELAY = 2000;
const transition_duration = 300;
const OSDs = () => {
const stack = Stack({
transition: 'over_up_down',
transition_duration,
attribute: { popup: () => {/**/} },
});
// Send reference of stack to all items
stack.items = OSDList.map((osd, i) => [`${i}`, osd(stack)]);
// Delay popup method so it
// doesn't show any OSDs at launch
timeout(1000, () => {
let count = 0;
stack.attribute.popup = (osd: AgsBox) => {
++count;
stack.set_visible_child(osd);
App.openWindow('osd');
timeout(HIDE_DELAY, () => {
--count;
if (count === 0) {
App.closeWindow('osd');
}
});
};
});
return stack;
};
export default () => PopupWindow({
name: 'osd',
anchor: ['bottom'],
exclusivity: 'ignore',
close_on_unfocus: 'stay',
transition: 'slide_up',
transition_duration,
bezier: 'ease',
child: OSDs(),
});

View file

@ -0,0 +1,104 @@
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Label } from 'resource:///com/github/Aylur/ags/widget.js';
import OSD from './ctor.ts';
import Brightness from '../../services/brightness.ts';
import { SpeakerIcon } from '../misc/audio-icons.ts';
import { MicIcon } from '../misc/audio-icons.ts';
const AUDIO_MAX = 1.5;
const ShowSpeaker = Variable(true);
globalThis.showSpeaker = () => {
ShowSpeaker.value = !ShowSpeaker.value;
};
// Types
import AgsStack from 'types/widgets/stack.ts';
export const SpeakerOSD = (stack: AgsStack) => OSD({
stack,
icon: { icon: SpeakerIcon.bind() },
info: {
mod: ShowSpeaker,
logic: (self) => {
if (!Audio.speaker) {
return;
}
self.value = Audio.speaker ?
Audio.speaker.volume / AUDIO_MAX :
0;
self.sensitive = !Audio.speaker?.stream.is_muted;
},
},
});
export const ScreenBrightnessOSD = (stack: AgsStack) => OSD({
stack,
icon: { icon: Brightness.bind('screenIcon') },
info: {
mod: Brightness,
signal: 'screen',
logic: (self) => {
self.value = Brightness.screen;
},
},
});
export const KbdBrightnessOSD = (stack: AgsStack) => OSD({
stack,
icon: 'keyboard-brightness-symbolic',
info: {
mod: Brightness,
signal: 'kbd',
logic: (self) => {
if (!self.value) {
self.value = Brightness.kbd / 2;
return;
}
self.value = Brightness.kbd / 2;
self.sensitive = Brightness.kbd !== 0;
},
},
});
export const MicOSD = (stack: AgsStack) => OSD({
stack,
icon: { icon: MicIcon.bind() },
info: {
mod: Audio,
signal: 'microphone-changed',
logic: (self) => {
if (!Audio.microphone) {
return;
}
self.value = Audio.microphone ? Audio.microphone.volume : 0;
self.sensitive = !Audio.microphone?.stream.is_muted;
},
},
});
export const CapsLockOSD = (stack: AgsStack) => OSD({
stack,
icon: { icon: Brightness.bind('capsIcon') },
info: {
mod: Brightness,
signal: 'caps',
widget: Label({
vpack: 'center',
label: 'Caps Lock',
}),
},
});

View file

@ -0,0 +1,198 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Icon, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { WindowButton } from './dragndrop.ts';
import * as VARS from './variables.ts';
// Types
import { Client as HyprClient } from 'types/service/hyprland.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
import AgsBox from 'types/widgets/box.ts';
import AgsButton from 'types/widgets/button.ts';
import AgsIcon from 'types/widgets/icon.ts';
const scale = (size: number) => (size * VARS.SCALE) - VARS.MARGIN;
const getFontSize = (client: HyprClient) => {
const valX = scale(client.size[0]) * VARS.ICON_SCALE;
const valY = scale(client.size[1]) * VARS.ICON_SCALE;
const size = Math.min(valX, valY);
return size <= 0 ? 0.1 : size;
};
const IconStyle = (client: HyprClient) => `
min-width: ${scale(client.size[0])}px;
min-height: ${scale(client.size[1])}px;
font-size: ${getFontSize(client)}px;
`;
const Client = (
client: HyprClient,
active: Boolean,
clients: Array<HyprClient>,
box: AgsBox,
) => {
const wsName = String(client.workspace.name).replace('special:', '');
const wsId = client.workspace.id;
const addr = `address:${client.address}`;
return Revealer({
transition: 'crossfade',
reveal_child: true,
attribute: {
address: client.address,
to_destroy: false,
},
child: WindowButton({
mainBox: box,
address: client.address,
on_secondary_click_release: () => {
Hyprland.sendMessage(`dispatch closewindow ${addr}`);
},
on_primary_click_release: () => {
if (wsId < 0) {
if (client.workspace.name === 'special') {
Hyprland.sendMessage('dispatch ' +
`movetoworkspacesilent special:${wsId},${addr}`)
.then(() => {
Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${wsId}`)
.then(() => {
App.closeWindow('overview');
}).catch(print);
}).catch(print);
}
else {
Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${wsName}`)
.then(() => {
App.closeWindow('overview');
}).catch(print);
}
}
else {
// Close special workspace if one is opened
const activeAddress = Hyprland.active.client.address;
const currentActive = clients.find((c) => {
return c.address === activeAddress;
});
if (currentActive && currentActive.workspace.id < 0) {
const currentSpecial = `${currentActive.workspace.name}`
.replace('special:', '');
Hyprland.sendMessage('dispatch ' +
`togglespecialworkspace ${currentSpecial}`)
.catch(print);
}
Hyprland.sendMessage(`dispatch focuswindow ${addr}`)
.then(() => {
App.closeWindow('overview');
}).catch(print);
}
},
child: Icon({
class_name: `window ${active ? 'active' : ''}`,
css: `${IconStyle(client)} font-size: 10px;`,
icon: client.class,
}),
}),
});
};
export const updateClients = (box: AgsBox) => {
Hyprland.sendMessage('j/clients').then((out) => {
let clients = JSON.parse(out) as Array<HyprClient>;
clients = clients.filter((client) => client.class);
box.attribute.workspaces.forEach(
(workspace: AgsRevealer) => {
const fixed = workspace.attribute.get_fixed();
const toRemove = fixed.get_children() as Array<AgsRevealer>;
clients.filter((client) =>
client.workspace.id === workspace.attribute.id)
.forEach((client) => {
const active =
client.address === Hyprland.active.client.address;
// TODO: see if this works on multi monitor setup
const alloc = box.get_allocation();
let monitor = box.get_display()
.get_monitor_at_point(alloc.x, alloc.y);
monitor = Hyprland.monitors.find((mon) => {
return mon.make === monitor.manufacturer &&
mon.model === monitor.model;
});
client.at[0] -= monitor.x;
client.at[1] -= monitor.y;
// Special workspaces that haven't been opened yet
// return a size of 0. We need to set them to default
// values to show the workspace properly
if (client.size[0] === 0) {
client.size[0] = VARS.DEFAULT_SPECIAL.SIZE_X;
client.size[1] = VARS.DEFAULT_SPECIAL.SIZE_Y;
client.at[0] = VARS.DEFAULT_SPECIAL.POS_X;
client.at[1] = VARS.DEFAULT_SPECIAL.POS_Y;
}
const newClient = [
(fixed.get_children() as Array<AgsRevealer>)
.find((ch) =>
ch.attribute.address === client.address),
client.at[0] * VARS.SCALE,
client.at[1] * VARS.SCALE,
] as [AgsRevealer, number, number];
// If it exists already
if (newClient[0]) {
toRemove.splice(toRemove.indexOf(newClient[0]), 1);
fixed.move(...newClient);
}
else {
newClient[0] = Client(client, active, clients, box);
fixed.put(...newClient);
}
// Set a timeout here to have an animation when the icon first appears
timeout(1, () => {
((newClient[0].child as AgsButton)
.child as AgsIcon)
.class_name = `window ${active}`;
((newClient[0].child as AgsButton)
.child as AgsIcon).setCss(IconStyle(client));
});
});
fixed.show_all();
toRemove.forEach((ch) => {
if (ch.attribute.to_destroy) {
ch.destroy();
}
else {
ch.reveal_child = false;
ch.attribute.to_destroy = true;
}
});
},
);
}).catch(print);
};

View file

@ -0,0 +1,50 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Box } from 'resource:///com/github/Aylur/ags/widget.js';
import * as VARS from './variables.ts';
const PADDING = 34;
const MARGIN = 9;
const DEFAULT_STYLE = `
min-width: ${VARS.SCREEN.X * VARS.SCALE}px;
min-height: ${(VARS.SCREEN.Y * VARS.SCALE) - (VARS.MARGIN / 2)}px;
border-radius: 10px;
`;
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
import AgsCenterBox from 'types/widgets/centerbox.ts';
import AgsEventBox from 'types/widgets/eventbox.ts';
export const Highlighter = () => Box({
vpack: 'start',
hpack: 'start',
class_name: 'workspace active',
css: DEFAULT_STYLE,
});
export const updateCurrentWorkspace = (main: AgsBox, highlighter: AgsBox) => {
const currentId = Hyprland.active.workspace.id;
const row = Math.floor((currentId - 1) / VARS.WORKSPACE_PER_ROW);
const rowObject = (main.children[0] as AgsBox).children[row] as AgsRevealer;
const workspaces = ((((rowObject.child as AgsCenterBox)
.center_widget as AgsEventBox)
.child as AgsBox)
.get_children() as Array<AgsRevealer>)
.filter((w) => w.reveal_child);
const currentIndex = workspaces.findIndex(
(w) => w.attribute.id === currentId,
);
const left = currentIndex * ((VARS.SCREEN.X * VARS.SCALE) + 2 + PADDING);
const height = row * ((VARS.SCREEN.Y * VARS.SCALE) + (PADDING / 2));
highlighter.setCss(`
${DEFAULT_STYLE}
margin-left: ${MARGIN + left}px;
margin-top: ${MARGIN + height}px;
`);
};

View file

@ -0,0 +1,119 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Button, EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
import Cairo from 'cairo';
const { Gtk, Gdk } = imports.gi;
import { updateClients } from './clients.ts';
const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
const display = Gdk.Display.get_default();
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsButton from 'types/widgets/button.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
import { ButtonProps } from 'types/widgets/button.ts';
import { EventBoxProps } from 'types/widgets/eventbox.ts';
type WindowButtonType = ButtonProps & {
address: string
mainBox: AgsBox
};
const createSurfaceFromWidget = (widget: AgsButton) => {
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;
};
let hidden = 0;
export const WorkspaceDrop = ({ ...props }: EventBoxProps) => EventBox({
...props,
setup: (self) => {
self.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY);
self.on('drag-data-received', (_, _c, _x, _y, data) => {
let id = (self.get_parent() as AgsRevealer)?.attribute.id;
if (id < -1) {
id = (self.get_parent() as AgsRevealer)?.attribute.name;
}
else if (id === -1) {
id = `special:${++hidden}`;
}
else if (id === 1000) {
id = 'empty';
}
Hyprland.sendMessage('dispatch ' +
`movetoworkspacesilent ${id},address:${data.get_text()}`)
.catch(print);
});
},
});
export const WindowButton = ({
address,
mainBox,
...props
}: WindowButtonType) => Button({
...props,
setup: (self) => {
self.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
TARGET,
Gdk.DragAction.COPY,
);
self
.on('drag-data-get', (_w, _c, data) => {
data.set_text(address, address.length);
})
.on('drag-begin', (_, context) => {
Gtk.drag_set_icon_surface(
context,
createSurfaceFromWidget(self),
);
(self.get_parent() as AgsRevealer)?.set_reveal_child(false);
})
.on('drag-end', () => {
self.get_parent()?.destroy();
updateClients(mainBox);
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
});

View file

@ -0,0 +1,152 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Box, Overlay, Window } from 'resource:///com/github/Aylur/ags/widget.js';
import { WorkspaceRow, getWorkspaces, updateWorkspaces } from './workspaces.ts';
import { Highlighter, updateCurrentWorkspace } from './current-workspace.ts';
import { updateClients } from './clients.ts';
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsOverlay from 'types/widgets/overlay.ts';
// TODO: have a 'page' for each monitor, arrows on both sides to loop through
export const Overview = () => {
const highlighter = Highlighter();
const mainBox = Box({
// Do this for scss hierarchy
class_name: 'overview',
css: 'all: unset',
vertical: true,
vpack: 'center',
hpack: 'center',
attribute: {
workspaces: [],
update: () => {
getWorkspaces(mainBox);
updateWorkspaces(mainBox);
updateClients(mainBox);
updateCurrentWorkspace(mainBox, highlighter);
},
},
children: [
Box({
vertical: true,
children: [
WorkspaceRow('normal', 0),
],
}),
Box({
vertical: true,
children: [
WorkspaceRow('special', 0),
],
}),
],
setup: (self) => {
self.hook(Hyprland, () => {
if (!App.getWindow('overview')?.visible) {
return;
}
self.attribute.update();
});
},
});
const widget = Overlay({
overlays: [highlighter, mainBox],
attribute: {
get_child: () => mainBox,
closing: false,
},
// Make size of overlay big enough for content
child: Box({
class_name: 'overview',
css: `
min-height: ${mainBox.get_allocated_height()}px;
min-width: ${mainBox.get_allocated_width()}px;
`,
}),
// TODO: throttle this?
setup: (self) => {
self.on('get-child-position', (_, ch) => {
if (ch === mainBox && !self.attribute.closing) {
(self.child as AgsBox).setCss(`
transition: min-height 0.2s ease, min-width 0.2s ease;
min-height: ${mainBox.get_allocated_height()}px;
min-width: ${mainBox.get_allocated_width()}px;
`);
}
});
},
});
return widget;
};
// FIXME: can't use PopupWindow because this is an overlay already
export default () => {
const transition_duration = 800;
const win = Window({
name: 'overview',
visible: false,
// Needs this to have space
// allocated at the start
child: Box({
css: `
min-height: 1px;
min-width: 1px;
padding: 1px;
`,
}),
attribute: { close_on_unfocus: 'none' },
setup: (self) => {
const name = 'overview';
Hyprland.sendMessage('[[BATCH]] ' +
`keyword layerrule ignorealpha[0.97],${name}; ` +
`keyword layerrule blur,${name}`);
self.hook(App, (_, currentName, isOpen) => {
if (currentName === self.name) {
if (isOpen) {
self.child = Overview();
self.show_all();
(self.child as AgsOverlay)
.attribute.get_child().attribute.update();
}
else {
(self.child as AgsOverlay).attribute.closing = true;
((self.child as AgsOverlay)
.child as AgsBox).css = `
min-height: 1px;
min-width: 1px;
transition: all
${transition_duration - 10}ms ease;
`;
}
}
});
},
});
return win;
};

View file

@ -0,0 +1,14 @@
export const SCALE = 0.11;
export const ICON_SCALE = 0.8;
export const MARGIN = 8;
export const DEFAULT_SPECIAL = {
SIZE_X: 1524,
SIZE_Y: 908,
POS_X: 197,
POS_Y: 170,
};
export const WORKSPACE_PER_ROW = 6;
export const SCREEN = {
X: 1920,
Y: 1200,
};

View file

@ -0,0 +1,201 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { Revealer, CenterBox, Box, EventBox, Fixed, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import { WorkspaceDrop } from './dragndrop.ts';
import * as VARS from './variables.ts';
const EMPTY_OFFSET = 16;
const DEFAULT_STYLE = `
min-width: ${(VARS.SCREEN.X * VARS.SCALE) + EMPTY_OFFSET}px;
min-height: ${VARS.SCREEN.Y * VARS.SCALE}px;
`;
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
import AgsCenterBox from 'types/widgets/centerbox.ts';
import AgsEventBox from 'types/widgets/eventbox.ts';
export const getWorkspaces = (box: AgsBox) => {
const children = [] as Array<AgsRevealer>;
(box.children as Array<AgsBox>).forEach((type) => {
(type.children as Array<AgsRevealer>).forEach(
(row) => {
((((row.child as AgsCenterBox)
?.center_widget as AgsEventBox)
?.child as AgsBox)
.children as Array<AgsRevealer>)
.forEach((workspace) => {
children.push(workspace);
});
},
);
});
box.attribute.workspaces = children.sort((a, b) => {
return a.attribute.id - b.attribute.id;
});
};
const Workspace = (id: number, name: string, normal = true) => {
const fixed = Fixed({});
const workspace = Revealer({
transition: 'slide_right',
transition_duration: 500,
attribute: {
id,
name,
get_fixed: () => fixed,
},
setup: (self) => {
if (normal) {
self.hook(Hyprland, () => {
const activeId = Hyprland.active.workspace.id;
const active = activeId === self.attribute.id;
const ws = Hyprland.getWorkspace(self.attribute.id);
self.reveal_child =
(ws?.windows && ws.windows > 0) || active;
});
}
},
child: WorkspaceDrop({
child: Box({
class_name: 'workspace',
css: normal ?
DEFAULT_STYLE :
`
min-width: ${(VARS.SCREEN.X * VARS.SCALE / 2) +
EMPTY_OFFSET}px;
min-height: ${VARS.SCREEN.Y * VARS.SCALE}px;
`,
children: normal ?
[fixed] :
[
fixed,
Label({
label: ' +',
css: 'font-size: 40px;',
}),
],
}),
}),
});
return workspace;
};
export const WorkspaceRow = (class_name: string, i: number) => {
const addWorkspace = Workspace(
class_name === 'special' ? -1 : 1000,
class_name === 'special' ? 'special' : '',
false,
);
return Revealer({
transition: 'slide_down',
hpack: class_name === 'special' ? 'fill' : 'start',
setup: (self) => {
self.hook(Hyprland, (rev) => {
const minId = i * VARS.WORKSPACE_PER_ROW;
const activeId = Hyprland.active.workspace.id;
const rowExists = Hyprland.workspaces.some((ws) => {
const isInRow = ws.id > minId;
const hasClients = ws.windows > 0;
const isActive = ws.id === activeId;
return isInRow && (hasClients || isActive);
});
rev.reveal_child = rowExists;
});
},
child: CenterBox({
center_widget: EventBox({
setup: (self) => {
self.hook(Hyprland, () => {
const maxId = (i + 1) * VARS.WORKSPACE_PER_ROW;
const activeId = Hyprland.active.workspace.id;
const isSpecial = class_name === 'special';
const nextRowExists = Hyprland.workspaces.some((ws) => {
const isInNextRow = ws.id > maxId;
const hasClients = ws.windows > 0;
const isActive = ws.id === activeId;
return isInNextRow && (hasClients || isActive);
});
addWorkspace.reveal_child = isSpecial || !nextRowExists;
});
},
child: Box({
class_name,
children: [addWorkspace],
}),
}),
}),
});
};
export const updateWorkspaces = (box: AgsBox) => {
Hyprland.workspaces.forEach((ws) => {
const currentWs = (box.attribute.workspaces as Array<AgsRevealer>).find(
(ch) => ch.attribute.id === ws.id,
);
if (!currentWs) {
let type = 0;
let rowNo = 0;
if (ws.id < 0) {
// This means it's a special workspace
type = 1;
}
else {
rowNo = Math.floor((ws.id - 1) / VARS.WORKSPACE_PER_ROW);
const wsRow = box.children[type] as AgsBox;
const wsQty = wsRow.children.length;
if (rowNo >= wsQty) {
for (let i = wsQty; i <= rowNo; ++i) {
wsRow.add(WorkspaceRow(
type ? 'special' : 'normal', i,
));
}
}
}
const row = ((((box.children[type] as AgsBox)
.children[rowNo] as AgsRevealer)
.child as AgsCenterBox)
.center_widget as AgsEventBox)
.child as AgsBox;
row.add(Workspace(ws.id, type ? ws.name : ''));
}
});
// Make sure the order is correct
box.attribute.workspaces.forEach(
(workspace: AgsRevealer, i: number) => {
(workspace?.get_parent() as AgsBox)
?.reorder_child(workspace, i);
},
);
box.show_all();
};

View file

@ -0,0 +1,48 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { CenterBox, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import PopupWindow from './misc/popup.ts';
import CursorBox from './misc/cursorbox.ts';
const PowermenuWidget = () => CenterBox({
class_name: 'powermenu',
vertical: false,
start_widget: CursorBox({
class_name: 'shutdown button',
on_primary_click_release: () => execAsync(['systemctl', 'poweroff'])
.catch(print),
child: Label({
label: '襤',
}),
}),
center_widget: CursorBox({
class_name: 'reboot button',
on_primary_click_release: () => execAsync(['systemctl', 'reboot'])
.catch(print),
child: Label({
label: '勒',
}),
}),
end_widget: CursorBox({
class_name: 'logout button',
on_primary_click_release: () => Hyprland.sendMessage('dispatch exit')
.catch(print),
child: Label({
label: '',
}),
}),
});
export default () => PopupWindow({
name: 'powermenu',
child: PowermenuWidget(),
});

View file

@ -0,0 +1,215 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import { Box, Icon, Label, ListBox, Overlay, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
import CursorBox from '../misc/cursorbox.ts';
const SCROLL_THRESH_H = 200;
const SCROLL_THRESH_N = 7;
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsScrollable from 'types/widgets/scrollable.ts';
type ListBoxRow = typeof imports.gi.Gtk.ListBoxRow;
import { BluetoothDevice as BTDev } from 'types/service/bluetooth.ts';
const BluetoothDevice = (dev: BTDev) => Box({
class_name: 'menu-item',
attribute: { dev },
children: [Revealer({
reveal_child: true,
transition: 'slide_down',
child: CursorBox({
on_primary_click_release: () => dev.setConnection(true),
child: Box({
hexpand: true,
children: [
Icon({
icon: dev.bind('icon_name'),
}),
Label({
label: dev.bind('name'),
}),
Icon({
icon: 'object-select-symbolic',
hexpand: true,
hpack: 'end',
}).hook(dev, (self) => {
self.setCss(`opacity: ${dev.paired ?
'1' :
'0'};
`);
}),
],
}),
}),
})],
});
export const BluetoothMenu = () => {
const DevList = new Map();
const topArrow = Revealer({
transition: 'slide_down',
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
class_name: 'scrolled-indicator',
size: 16,
css: '-gtk-icon-transform: rotate(180deg);',
}),
});
const bottomArrow = Revealer({
transition: 'slide_up',
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
class_name: 'scrolled-indicator',
size: 16,
}),
});
return Overlay({
pass_through: true,
overlays: [
Box({
vpack: 'start',
hpack: 'center',
css: 'margin-top: 12px',
children: [topArrow],
}),
Box({
vpack: 'end',
hpack: 'center',
css: 'margin-bottom: 12px',
children: [bottomArrow],
}),
],
child: Box({
class_name: 'menu',
child: Scrollable({
hscroll: 'never',
vscroll: 'never',
setup: (self) => {
self.on('edge-reached', (_, pos) => {
// Manage scroll indicators
if (pos === 2) {
topArrow.reveal_child = false;
bottomArrow.reveal_child = true;
}
else if (pos === 3) {
topArrow.reveal_child = true;
bottomArrow.reveal_child = false;
}
});
},
child: ListBox({
setup: (self) => {
self.set_sort_func((a, b) => {
const bState = (b.get_children()[0] as AgsBox)
.attribute.dev.paired;
const aState = (a.get_children()[0] as AgsBox)
.attribute.dev.paired;
return bState - aState;
});
self.hook(Bluetooth, () => {
// Get all devices
const Devices = Bluetooth.devices.concat(
Bluetooth.connected_devices,
);
// Add missing devices
Devices.forEach((dev) => {
if (!DevList.has(dev) && dev.name) {
DevList.set(dev, BluetoothDevice(dev));
self.add(DevList.get(dev));
self.show_all();
}
});
// Delete ones that don't exist anymore
const difference = Array.from(DevList.keys())
.filter((dev) => !Devices
.find((d) => dev === d) &&
dev.name);
difference.forEach((dev) => {
const devWidget = DevList.get(dev);
if (devWidget) {
if (devWidget.toDestroy) {
devWidget.get_parent().destroy();
DevList.delete(dev);
}
else {
devWidget.child.reveal_child = false;
devWidget.toDestroy = true;
}
}
});
// Start scrolling after a specified height
// is reached by the children
const height = Math.max(
self.get_parent()?.get_allocated_height() || 0,
SCROLL_THRESH_H,
);
const scroll = (self.get_parent() as ListBoxRow)
?.get_parent() as AgsScrollable;
if (scroll) {
const n_child = self.get_children().length;
if (n_child > SCROLL_THRESH_N) {
scroll.vscroll = 'always';
scroll.setCss(`min-height: ${height}px;`);
// Make bottom scroll indicator appear only
// when first getting overflowing children
if (!(bottomArrow.reveal_child === true ||
topArrow.reveal_child === true)) {
bottomArrow.reveal_child = true;
}
}
else {
scroll.vscroll = 'never';
scroll.setCss('');
topArrow.reveal_child = false;
bottomArrow.reveal_child = false;
}
}
// Trigger sort_func
(self.get_children() as Array<ListBoxRow>)
.forEach((ch) => {
ch.changed();
});
});
},
}),
}),
}),
});
};

View file

@ -0,0 +1,343 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, Icon, Label, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import { SpeakerIcon, MicIcon } from '../misc/audio-icons.ts';
import CursorBox from '../misc/cursorbox.ts';
import Separator from '../misc/separator.ts';
import { NetworkMenu } from './network.ts';
import { BluetoothMenu } from './bluetooth.ts';
// Types
import { GObject } from 'gi://GObject';
import AgsBox from 'types/widgets/box.ts';
import AgsIcon from 'types/widgets/icon.ts';
import AgsLabel from 'types/widgets/label.ts';
import AgsRevealer from 'types/widgets/revealer.ts';
import { Variable as Var } from 'types/variable.ts';
type IconTuple = [
GObject.Object,
(self: AgsIcon) => void,
signal?: string,
];
type IndicatorTuple = [
GObject.Object,
(self: AgsLabel) => void,
signal?: string,
];
type GridButtonType = {
command?(): void
secondary_command?(): void
on_open?(menu: AgsRevealer): void
icon: string | IconTuple
indicator?: IndicatorTuple
menu?: any
};
const SPACING = 28;
const ButtonStates = [] as Array<Var<any>>;
const GridButton = ({
command = () => {/**/},
secondary_command = () => {/**/},
on_open = () => {/**/},
icon,
indicator,
menu,
}: GridButtonType) => {
const Activated = Variable(false);
ButtonStates.push(Activated);
let iconWidget = Icon();
let indicatorWidget = Label();
// Allow setting icon dynamically or statically
if (typeof icon === 'string') {
iconWidget = Icon({
class_name: 'grid-label',
icon,
setup: (self) => {
self.hook(Activated, () => {
self.setCss(`color: ${Activated.value ?
'rgba(189, 147, 249, 0.8)' :
'unset'};`);
});
},
});
}
else if (Array.isArray(icon)) {
iconWidget = Icon({
class_name: 'grid-label',
setup: (self) => {
self
.hook(...icon)
.hook(Activated, () => {
self.setCss(`color: ${Activated.value ?
'rgba(189, 147, 249, 0.8)' :
'unset'};`);
});
},
});
}
if (indicator) {
indicatorWidget = Label({
class_name: 'sub-label',
justification: 'left',
truncate: 'end',
max_width_chars: 12,
setup: (self) => {
self.hook(...indicator);
},
});
}
if (menu) {
menu = Revealer({
transition: 'slide_down',
child: menu,
reveal_child: Activated.bind(),
});
}
const widget = Box({
vertical: true,
children: [
Box({
class_name: 'grid-button',
children: [
CursorBox({
class_name: 'left-part',
on_primary_click_release: () => {
if (Activated.value) {
secondary_command();
}
else {
command();
}
},
child: iconWidget,
}),
CursorBox({
class_name: 'right-part',
on_primary_click_release: () => {
ButtonStates.forEach((state) => {
if (state !== Activated) {
state.value = false;
}
});
Activated.value = !Activated.value;
},
on_hover: (self) => {
if (menu) {
const rowMenu =
((((self.get_parent() as AgsBox)
?.get_parent() as AgsBox)
?.get_parent() as AgsBox)
?.get_parent() as AgsBox)
?.children[1] as AgsBox;
const isSetup = (rowMenu
.get_children() as Array<AgsBox>)
.find((ch) => ch === menu);
if (!isSetup) {
rowMenu.add(menu);
rowMenu.show_all();
}
}
},
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
class_name: 'grid-chev',
setup: (self) => {
self.hook(Activated, () => {
let deg = 270;
if (Activated.value) {
deg = menu ? 360 : 450;
on_open(menu);
}
self.setCss(`
-gtk-icon-transform: rotate(${deg}deg);
`);
});
},
}),
}),
],
}),
indicatorWidget,
],
});
return widget;
};
const Row = ({ buttons }) => {
const child = Box({
class_name: 'button-row',
hpack: 'center',
});
const widget = Box({
vertical: true,
children: [
child,
Box({ vertical: true }),
],
});
for (let i = 0; i < buttons.length; ++i) {
if (i === buttons.length - 1) {
child.add(buttons[i]);
}
else {
child.add(buttons[i]);
child.add(Separator(SPACING));
}
}
return widget;
};
const FirstRow = () => Row({
buttons: [
GridButton({
command: () => Network.toggleWifi(),
secondary_command: () => {
// TODO: connection editor
},
icon: [Network, (self) => {
self.icon = Network.wifi?.icon_name;
}],
indicator: [Network, (self) => {
self.label = Network.wifi?.ssid || Network.wired?.internet;
}],
menu: NetworkMenu(),
on_open: () => Network.wifi.scan(),
}),
// TODO: do vpn
GridButton({
command: () => {
//
},
secondary_command: () => {
//
},
icon: 'airplane-mode-disabled-symbolic',
}),
GridButton({
command: () => Bluetooth.toggle(),
secondary_command: () => {
// TODO: bluetooth connection editor
},
icon: [Bluetooth, (self) => {
if (Bluetooth.enabled) {
self.icon = Bluetooth.connected_devices[0] ?
Bluetooth.connected_devices[0].icon_name :
'bluetooth-active-symbolic';
}
else {
self.icon = 'bluetooth-disabled-symbolic';
}
}],
indicator: [Bluetooth, (self) => {
self.label = Bluetooth.connected_devices[0] ?
`${Bluetooth.connected_devices[0]}` :
'Disconnected';
}, 'notify::connected-devices'],
menu: BluetoothMenu(),
on_open: (menu) => {
execAsync(`bluetoothctl scan ${menu.reveal_child ?
'on' :
'off'}`).catch(print);
},
}),
],
});
const SecondRow = () => Row({
buttons: [
GridButton({
command: () => {
execAsync(['pactl', 'set-sink-mute',
'@DEFAULT_SINK@', 'toggle']).catch(print);
},
secondary_command: () => {
execAsync(['bash', '-c', 'pavucontrol'])
.catch(print);
},
icon: [SpeakerIcon, (self) => {
self.icon = SpeakerIcon.value;
}],
}),
GridButton({
command: () => {
execAsync(['pactl', 'set-source-mute',
'@DEFAULT_SOURCE@', 'toggle']).catch(print);
},
secondary_command: () => {
execAsync(['bash', '-c', 'pavucontrol'])
.catch(print);
},
icon: [MicIcon, (self) => {
self.icon = MicIcon.value;
}],
}),
GridButton({
command: () => {
execAsync(['lock']).catch(print);
},
secondary_command: () => App.openWindow('powermenu'),
icon: 'system-lock-screen-symbolic',
}),
],
});
export default () => Box({
class_name: 'button-grid',
vertical: true,
hpack: 'center',
children: [
FirstRow(),
Separator(10, { vertical: true }),
SecondRow(),
],
});

View file

@ -0,0 +1,58 @@
import { Box, Label, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
import ButtonGrid from './button-grid.ts';
import SliderBox from './slider-box.ts';
import Player from '../media-player/player.ts';
import PopupWindow from '../misc/popup.ts';
import ToggleButton from './toggle-button.ts';
const QuickSettingsWidget = () => {
const rev = Revealer({
transition: 'slide_down',
child: Player(),
});
return Box({
class_name: 'qs-container',
vertical: true,
children: [
Box({
class_name: 'quick-settings',
vertical: true,
children: [
Label({
label: 'Control Center',
class_name: 'title',
hpack: 'start',
css: `
margin-left: 20px;
margin-bottom: 30px;
`,
}),
ButtonGrid(),
SliderBox(),
ToggleButton(rev),
],
}),
rev,
],
});
};
const TOP_MARGIN = 6;
export default () => PopupWindow({
name: 'quick-settings',
anchor: ['top', 'right'],
margins: [TOP_MARGIN, 0, 0, 0],
child: QuickSettingsWidget(),
});

View file

@ -0,0 +1,251 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, Icon, Label, ListBox, Overlay, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import CursorBox from '../misc/cursorbox.ts';
const SCROLL_THRESH_H = 200;
const SCROLL_THRESH_N = 7;
// Types
import AgsBox from 'types/widgets/box.ts';
import AgsScrollable from 'types/widgets/scrollable.ts';
type ListBoxRow = typeof imports.gi.Gtk.ListBoxRow;
type APType = {
bssid: string
address: string
lastSeen: number
ssid: string
active: boolean
strength: number
iconName: string
};
const AccessPoint = (ap: APType) => {
const widget = Box({
class_name: 'menu-item',
attribute: {
ap: Variable(ap),
},
});
const child = Box({
hexpand: true,
children: [
Icon().hook(widget.attribute.ap, (self) => {
self.icon = widget.attribute.ap.value.iconName;
}),
Label().hook(widget.attribute.ap, (self) => {
self.label = widget.attribute.ap.value.ssid || '';
}),
Icon({
icon: 'object-select-symbolic',
hexpand: true,
hpack: 'end',
setup: (self) => {
self.hook(Network, () => {
self.setCss(
`opacity: ${
widget.attribute.ap.value.ssid ===
Network.wifi.ssid ?
'1' :
'0'
};
`,
);
});
},
}),
],
});
widget.add(Revealer({
reveal_child: true,
transition: 'slide_down',
child: CursorBox({
on_primary_click_release: () => {
execAsync(`nmcli device wifi connect
${widget.attribute.ap.value.bssid}`).catch(print);
},
child,
}),
}));
return widget;
};
export const NetworkMenu = () => {
const APList = new Map();
const topArrow = Revealer({
transition: 'slide_down',
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
class_name: 'scrolled-indicator',
size: 16,
css: '-gtk-icon-transform: rotate(180deg);',
}),
});
const bottomArrow = Revealer({
transition: 'slide_up',
child: Icon({
icon: `${App.configDir }/icons/down-large.svg`,
class_name: 'scrolled-indicator',
size: 16,
}),
});
return Overlay({
pass_through: true,
overlays: [
Box({
vpack: 'start',
hpack: 'center',
css: 'margin-top: 12px',
children: [topArrow],
}),
Box({
vpack: 'end',
hpack: 'center',
css: 'margin-bottom: 12px',
children: [bottomArrow],
}),
],
child: Box({
class_name: 'menu',
child: Scrollable({
hscroll: 'never',
vscroll: 'never',
setup: (self) => {
self.on('edge-reached', (_, pos) => {
// Manage scroll indicators
if (pos === 2) {
topArrow.reveal_child = false;
bottomArrow.reveal_child = true;
}
else if (pos === 3) {
topArrow.reveal_child = true;
bottomArrow.reveal_child = false;
}
});
},
child: ListBox({
setup: (self) => {
self.set_sort_func((a, b) => {
const bState = (b.get_children()[0] as AgsBox)
.attribute.ap.value.strength;
const aState = (a.get_children()[0] as AgsBox)
.attribute.ap.value.strength;
return bState - aState;
});
self.hook(Network, () => {
// Add missing APs
const currentAPs = Network.wifi
?.access_points as Array<APType>;
currentAPs.forEach((ap) => {
if (ap.ssid !== 'Unknown') {
if (APList.has(ap.ssid)) {
const accesPoint = APList.get(ap.ssid)
.attribute.ap.value;
if (accesPoint.strength < ap.strength) {
APList.get(ap.ssid).attribute
.ap.value = ap;
}
}
else {
APList.set(ap.ssid, AccessPoint(ap));
self.add(APList.get(ap.ssid));
self.show_all();
}
}
});
// Delete ones that don't exist anymore
const difference = Array.from(APList.keys())
.filter((ssid) => !Network.wifi.access_points
.find((ap) => ap.ssid === ssid) &&
ssid !== 'Unknown');
difference.forEach((ssid) => {
const apWidget = APList.get(ssid);
if (apWidget) {
if (apWidget.toDestroy) {
apWidget.get_parent().destroy();
APList.delete(ssid);
}
else {
apWidget.child.reveal_child = false;
apWidget.toDestroy = true;
}
}
});
// Start scrolling after a specified height
// is reached by the children
const height = Math.max(
self.get_parent()?.get_allocated_height() || 0,
SCROLL_THRESH_H,
);
const scroll = (self.get_parent() as ListBoxRow)
?.get_parent() as AgsScrollable;
if (scroll) {
const n_child = self.get_children().length;
if (n_child > SCROLL_THRESH_N) {
scroll.vscroll = 'always';
scroll.setCss(`min-height: ${height}px;`);
// Make bottom scroll indicator appear only
// when first getting overflowing children
if (!(bottomArrow.reveal_child === true ||
topArrow.reveal_child === true)) {
bottomArrow.reveal_child = true;
}
}
else {
scroll.vscroll = 'never';
scroll.setCss('');
topArrow.reveal_child = false;
bottomArrow.reveal_child = false;
}
}
// Trigger sort_func
(self.get_children() as Array<ListBoxRow>)
.forEach((ch) => {
ch.changed();
});
});
},
}),
}),
}),
});
};

View file

@ -0,0 +1,143 @@
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import { Box, Slider, Icon } from 'resource:///com/github/Aylur/ags/widget.js';
const { Gdk } = imports.gi;
const display = Gdk.Display.get_default();
import Brightness from '../../services/brightness.ts';
import { SpeakerIcon } from '../misc/audio-icons.ts';
export default () => Box({
class_name: 'slider-box',
vertical: true,
hpack: 'center',
children: [
Box({
class_name: 'slider',
vpack: 'start',
hpack: 'center',
children: [
Icon({
size: 26,
class_name: 'slider-label',
icon: SpeakerIcon.bind(),
}),
Slider({
vpack: 'center',
max: 0.999,
draw_value: false,
on_change: ({ value }) => {
if (Audio.speaker) {
Audio.speaker.volume = value;
}
},
setup: (self) => {
self
.hook(Audio, () => {
self.value = Audio.speaker?.volume || 0;
}, 'speaker-changed')
// OnClick
.on('button-press-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
})
// OnRelease
.on('button-release-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
}),
],
}),
Box({
class_name: 'slider',
vpack: 'start',
hpack: 'center',
children: [
Icon({
class_name: 'slider-label',
icon: Brightness.bind('screenIcon'),
}),
Slider({
vpack: 'center',
draw_value: false,
on_change: ({ value }) => {
Brightness.screen = value;
},
setup: (self) => {
self
.hook(Brightness, () => {
self.value = Brightness.screen;
}, 'screen')
// OnClick
.on('button-press-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
})
// OnRelease
.on('button-release-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
}),
],
}),
],
});

View file

@ -0,0 +1,64 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
import { CenterBox, Icon, ToggleButton } from 'resource:///com/github/Aylur/ags/widget.js';
// Types
import AgsRevealer from 'types/widgets/revealer';
const { Gdk } = imports.gi;
const display = Gdk.Display.get_default();
export default (rev: AgsRevealer) => {
const child = Icon({
icon: `${App.configDir}/icons/down-large.svg`,
class_name: 'arrow',
css: '-gtk-icon-transform: rotate(180deg);',
});
const button = CenterBox({
center_widget: ToggleButton({
setup: (self) => {
// Open at startup if there are players
const id = Mpris.connect('changed', () => {
self.set_active(Mpris.players.length > 0);
Mpris.disconnect(id);
});
self
.on('toggled', () => {
if (self.get_active()) {
child
.setCss('-gtk-icon-transform: rotate(0deg);');
rev.reveal_child = true;
}
else {
child
.setCss('-gtk-icon-transform: rotate(180deg);');
rev.reveal_child = false;
}
})
// OnHover
.on('enter-notify-event', () => {
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
child,
}),
});
return button;
};

View file

@ -0,0 +1,67 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Brightness from '../services/brightness.ts';
import Pointers from '../services/pointers.ts';
import Tablet from '../services/tablet.ts';
import TouchGestures from '../services/touch-gestures.ts';
import closeAll from './misc/closer.ts';
import Persist from './misc/persist.ts';
export default () => {
globalThis.Brightness = Brightness;
globalThis.Pointers = Pointers;
globalThis.Tablet = Tablet;
globalThis.closeAll = closeAll;
Persist({
name: 'bluetooth',
gobject: Bluetooth,
prop: 'enabled',
signal: 'notify::enabled',
});
TouchGestures.addGesture({
name: 'openAppLauncher',
gesture: 'UD',
edge: 'T',
command: () => App.openWindow('applauncher'),
});
TouchGestures.addGesture({
name: 'oskOn',
gesture: 'DU',
edge: 'B',
command: 'busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 ' +
'SetVisible b true',
});
TouchGestures.addGesture({
name: 'oskOff',
gesture: 'UD',
edge: 'B',
command: 'busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 ' +
'SetVisible b false',
});
TouchGestures.addGesture({
name: 'swipeSpotify1',
gesture: 'LR',
edge: 'L',
command: () => Hyprland.sendMessage(
'dispatch togglespecialworkspace spot',
),
});
TouchGestures.addGesture({
name: 'swipeSpotify2',
gesture: 'RL',
edge: 'L',
command: () => Hyprland.sendMessage(
'dispatch togglespecialworkspace spot',
),
});
};

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022"
],
"allowImportingTsExtensions": true,
"allowJs": true,
"checkJs": true,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"typeRoots": [
"./types/ags.d.ts",
"./node_modules/@girs"
],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

48
modules/ags/config/wim.ts Normal file
View file

@ -0,0 +1,48 @@
import Setup from './ts/setup.ts';
import AppLauncher from './ts/applauncher/main.ts';
import Bar from './ts/bar/wim.ts';
import BgFade from './ts/misc/background-fade.ts';
import Calendar from './ts/date.ts';
import Corners from './ts/corners/main.ts';
import { NotifPopups, NotifCenter } from './ts/notifications/wim.ts';
import OSD from './ts/osd/main.ts';
import OSK from './ts/on-screen-keyboard/main.ts';
import Overview from './ts/overview/main.ts';
import Powermenu from './ts/powermenu.ts';
import QSettings from './ts/quick-settings/main.ts';
Setup();
const closeWinDelay = 800;
export default {
notificationPopupTimeout: 5000,
cacheNotificationActions: true,
closeWindowDelay: {
'applauncher': closeWinDelay,
'calendar': closeWinDelay,
'notification-center': closeWinDelay,
'osd': 300,
'osk': closeWinDelay,
'overview': closeWinDelay,
'powermenu': closeWinDelay,
'quick-settings': closeWinDelay,
},
windows: [
...Corners(),
AppLauncher(),
Calendar(),
NotifCenter(),
OSD(),
OSK(),
Overview(),
Powermenu(),
QSettings(),
Bar(),
BgFade(),
NotifPopups(),
],
};

View file

@ -4,7 +4,7 @@
pkgs,
...
}: let
inherit (config.vars) configDir mainUser;
inherit (config.vars) mainUser hostName;
isTouchscreen = config.hardware.sensor.iio.enable;
in {
services.upower.enable = true;
@ -22,45 +22,56 @@ in {
in {
programs.ags = {
enable = true;
configDir = symlink "${configDir}/ags";
configDir = symlink /home/${mainUser}/.nix/modules/ags/config;
package = ags.packages.${pkgs.system}.default;
extraPackages = with pkgs; [
libgudev
];
};
home.packages =
[config.customPkgs.coloryou]
++ (with pkgs; [
# ags
sassc
bun
playerctl
home = {
file = {
".config/ags/config.js".text =
/*
javascript
*/
''
import { transpileTypeScript } from './js/utils.js';
## gui
pavucontrol # TODO: replace with ags widget
(writeShellApplication {
name = "updateTypes";
runtimeInputs = [nodejs_18 typescript git];
text = ''
if [[ -d /tmp/ags-types ]]; then
rm -r /tmp/ags-types
fi
rm -r ~/.config/ags/types
git clone https://github.com/Aylur/ags.git /tmp/ags-types
/tmp/ags-types/example/starter-config/setup.sh
rm -r /tmp/ags-types
export default (await transpileTypeScript("${hostName}")).default;
'';
})
])
++ (optionals isTouchscreen (with pkgs; [
lisgd
squeekboard
ydotool
]));
};
packages =
[config.customPkgs.coloryou]
++ (with pkgs; [
# ags
sassc
bun
playerctl
## gui
pavucontrol # TODO: replace with ags widget
(writeShellApplication {
name = "updateTypes";
runtimeInputs = [nodejs_18 typescript git];
text = ''
if [[ -d /tmp/ags-types ]]; then
rm -r /tmp/ags-types
fi
rm -r ~/.config/ags/types
git clone https://github.com/Aylur/ags.git /tmp/ags-types
/tmp/ags-types/example/starter-config/setup.sh
rm -r /tmp/ags-types
'';
})
])
++ (optionals isTouchscreen (with pkgs; [
lisgd
squeekboard
ydotool
]));
};
})
];
}