feat(agsV2): separate from agsV1

This commit is contained in:
matt1432 2024-10-30 23:43:37 -04:00
parent 1fc5a48bf9
commit f522c984c4
64 changed files with 182 additions and 50 deletions

View file

@ -19,10 +19,7 @@ in {
gtkSessionLock = gtk-session-lock.packages.${pkgs.system}.default;
in
mkIf cfgDesktop.ags.enable {
# Enable pam for ags
security.pam.services.ags = {};
security.pam.services.astal-auth = {};
services.upower.enable = true;
home-manager.users.${cfgDesktop.user}.imports = [
@ -59,8 +56,6 @@ in {
}
];
imports = [(import ./v2 self)];
programs.ags = {
enable = true;
extraPackages = [

View file

@ -1 +0,0 @@
use flake "$FLAKE#node"

View file

@ -1,3 +0,0 @@
@girs
node_modules
tsconfig.json

View file

@ -1,82 +0,0 @@
import { App } from 'astal/gtk3';
import GLib from 'gi://GLib';
import style from './style.scss';
import AppLauncher from './widgets/applauncher/main';
import Bar from './widgets/bar/wim';
import BgFade from './widgets/bg-fade/main';
import Calendar from './widgets/date/main';
import Clipboard from './widgets/clipboard/main';
import Corners from './widgets/corners/main';
import IconBrowser from './widgets/icon-browser/main';
import { NotifPopups, NotifCenter } from './widgets/notifs/main';
import OSD from './widgets/osd/main';
import PowerMenu from './widgets/powermenu/main';
import Screenshot from './widgets/screenshot/main';
import { closeAll as closeAllFunc } from './lib';
import BrightnessService from './services/brightness';
import MonitorClicks from './services/monitor-clicks';
import Lockscreen from './widgets/lockscreen/main';
declare global {
function closeAll(): void;
// eslint-disable-next-line
var Brightness: typeof BrightnessService;
}
globalThis.closeAll = closeAllFunc;
globalThis.Brightness = BrightnessService;
const CONF = GLib.getenv('CONF');
switch (CONF) {
case 'lock': {
App.start({
css: style,
instanceName: CONF,
main: () => {
Lockscreen();
},
});
break;
}
case 'wim': {
App.start({
css: style,
requestHandler(js, res) {
App.eval(js).then(res).catch(res);
},
main: () => {
AppLauncher();
Bar();
BgFade();
Calendar();
Clipboard();
Corners();
IconBrowser();
NotifPopups();
NotifCenter();
OSD();
PowerMenu();
Screenshot();
Brightness.initService({
kbd: 'tpacpi::kbd_backlight',
caps: 'input1::capslock',
});
new MonitorClicks();
},
});
break;
}
}

View file

@ -1,95 +0,0 @@
// Vars from my Gradient theme
$accent_color: #bd93f9;
$accent_bg_color: #bd93f9;
$accent_fg_color: #f8f8f2;
$destructive_color: #ff5555;
$destructive_bg_color: #ff5555;
$destructive_fg_color: #f8f8f2;
$success_color: #50fa7b;
$success_bg_color: #50fa7b;
$success_fg_color: #f8f8f2;
$warning_color: #f1fa8c;
$warning_bg_color: #f1fa8c;
$warning_fg_color: rgba(0, 0, 0, 0.8);
$error_color: #ff5555;
$error_bg_color: #ff5555;
$error_fg_color: #f8f8f2;
$window_bg_color: #282a36;
$window_fg_color: #f8f8f2;
$view_bg_color: #282a36;
$view_fg_color: #f8f8f2;
$headerbar_bg_color: #282a36;
$headerbar_fg_color: #f8f8f2;
$headerbar_border_color: #ffffff;
$headerbar_backdrop_color: $window_bg_color;
$headerbar_shade_color: rgba(0, 0, 0, 0.36);
$card_bg_color: rgba(255, 255, 255, 0.08);
$card_fg_color: #f8f8f2;
$card_shade_color: rgba(0, 0, 0, 0.36);
$dialog_bg_color: #282a36;
$dialog_fg_color: #f8f8f2;
$popover_bg_color: #282a36;
$popover_fg_color: #f8f8f2;
$shade_color: #383838;
$scrollbar_outline_color: rgba(0, 0, 0, 0.5);
$green_1: #8ff0a4;
$green_2: #57e389;
$green_3: #33d17a;
$green_4: #2ec27e;
$green_5: #26a269;
$yellow_1: #f9f06b;
$yellow_2: #f8e45c;
$yellow_3: #f6d32d;
$yellow_4: #f5c211;
$yellow_5: #e5a50a;
$orange_1: #ffbe6f;
$orange_2: #ffa348;
$orange_3: #ff7800;
$orange_4: #e66100;
$orange_5: #c64600;
$red_1: #f66151;
$red_2: #ed333b;
$red_3: #e01b24;
$red_4: #c01c28;
$red_5: #a51d2d;
$purple_1: #dc8add;
$purple_2: #c061cb;
$purple_3: #9141ac;
$purple_4: #813d9c;
$purple_5: #613583;
$brown_1: #cdab8f;
$brown_2: #b5835a;
$brown_3: #986a44;
$brown_4: #865e3c;
$brown_5: #63452c;
$light_1: #ffffff;
$light_2: #f6f5f4;
$light_3: #deddda;
$light_4: #c0bfbc;
$light_5: #9a9996;
$dark_1: #77767b;
$dark_2: #5e5c64;
$dark_3: #3d3846;
$dark_4: #241f31;
$dark_5: #000000;
$blue_1: #99c1f1;
$blue_2: #62a0ea;
$blue_3: #3584e4;
$blue_4: #1c71d8;
$blue_5: #1a5fb4;
$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

@ -1,91 +0,0 @@
self: {
lib,
osConfig,
pkgs,
...
}: {
config = let
inherit (lib) attrValues boolToString removeAttrs;
inherit (osConfig.vars) hostName;
cfgDesktop = osConfig.roles.desktop;
inherit (self.inputs) agsV2 gtk-session-lock;
gtkSessionLock = gtk-session-lock.packages.${pkgs.system}.default;
agsV2Packages = agsV2.packages.${pkgs.system};
astalLibs = attrValues (removeAttrs agsV2.inputs.astal.packages.${pkgs.system} ["docs" "gjs"]) ++ [gtkSessionLock];
agsFull = agsV2Packages.ags.override {extraPackages = astalLibs;};
configDir = "/home/matt/.nix/nixosModules/ags/v2";
in {
home = {
packages = [
(pkgs.writeShellApplication {
name = "agsV2";
text = ''
export CONF="wim"
exec ${agsFull}/bin/ags --config ${configDir} "$@"
'';
})
(pkgs.writeShellApplication {
name = "agsConf";
text = ''
export CONF="$1"
exec ${agsFull}/bin/ags --config ${configDir}
'';
})
];
file = let
inherit
(import "${self}/lib" {inherit pkgs self;})
buildNodeModules
buildNodeTypes
;
in (
(buildNodeTypes {
pname = "agsV2";
configPath = "${configDir}/@girs";
packages = astalLibs;
})
// {
"${configDir}/node_modules".source =
buildNodeModules ./. "sha256-pK9S6qUjTIL0JDegYJlHSY5XEpLFKfA98MfZ59Q3IL4=";
"${configDir}/tsconfig.json".source = pkgs.writers.writeJSON "tsconfig.json" {
"$schema" = "https://json.schemastore.org/tsconfig";
"compilerOptions" = {
"experimentalDecorators" = true;
"strict" = true;
"target" = "ES2023";
"moduleResolution" = "Bundler";
"jsx" = "react-jsx";
"jsxImportSource" = "${agsV2Packages.gjs}/share/astal/gjs/gtk3";
"paths" = {
"astal" = ["${agsV2Packages.gjs}/share/astal/gjs"];
"astal/*" = ["${agsV2Packages.gjs}/share/astal/gjs/*"];
};
"skipLibCheck" = true;
"module" = "ES2022";
"lib" = ["ES2023"];
};
};
"${configDir}/widgets/lockscreen/vars.ts".text =
# javascript
''
export default {
mainMonitor: '${cfgDesktop.mainMonitor}',
dupeLockscreen: ${boolToString cfgDesktop.displayManager.duplicateScreen},
hasFprintd: ${boolToString (hostName == "wim")},
};
'';
}
);
};
};
# For accurate stack trace
_file = ./default.nix;
}

View file

@ -1,26 +0,0 @@
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
const SRC: string;
declare module 'inline:*' {
const content: string;
export default content;
}
declare module '*.sass' {
const content: string;
export default content;
}
declare module '*.scss' {
const content: string;
export default content;
}
declare module '*.css' {
const content: string;
export default content;
}

View file

@ -1,449 +0,0 @@
import eslint from '@eslint/js';
import jsdoc from 'eslint-plugin-jsdoc';
import stylistic from '@stylistic/eslint-plugin';
import tseslint from 'typescript-eslint';
export default tseslint.config({
files: ['**/*.{js,ts,tsx}'],
ignores: ['node_modules/**', 'types/**'],
extends: [
eslint.configs.recommended,
jsdoc.configs['flat/recommended-typescript'],
stylistic.configs['recommended-flat'],
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
],
rules: {
// JSDoc settings
'jsdoc/tag-lines': ['warn', 'any', { startLines: 1 }],
'jsdoc/check-line-alignment': ['warn', 'always', {
tags: ['param', 'arg', 'argument', 'property', 'prop'],
}],
'jsdoc/no-types': 'off',
// Newer settings
'@typescript-eslint/no-extraneous-class': ['off'],
'@typescript-eslint/no-implied-eval': ['off'],
'class-methods-use-this': 'off',
'@stylistic/no-multiple-empty-lines': 'off',
'@stylistic/jsx-indent-props': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'error',
'@stylistic/indent-binary-ops': 'off',
// Pre-flat config
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'array-callback-return': [
'error',
{
allowImplicit: true,
checkForEach: true,
},
],
'no-constructor-return': [
'error',
],
'no-unreachable-loop': [
'error',
{
ignore: [
'ForInStatement',
'ForOfStatement',
],
},
],
'block-scoped-var': [
'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,
4,
5,
10,
12,
33,
66,
100,
255,
360,
450,
500,
1000,
],
ignoreDefaultValues: true,
ignoreClassFieldInitialValues: true,
},
],
'no-multi-assign': [
'error',
],
'no-new-wrappers': [
'error',
],
'no-object-constructor': [
'error',
],
'no-proto': [
'error',
],
'no-return-assign': [
'error',
],
'no-sequences': [
'error',
],
'no-shadow': [
'error',
{
builtinGlobals: true,
allow: [
'Window',
],
},
],
'no-undef-init': [
'warn',
],
'no-undefined': [
'error',
],
'no-useless-constructor': [
'warn',
],
'no-useless-escape': [
'off',
],
'no-useless-return': [
'error',
],
'no-var': [
'error',
],
'no-void': [
'off',
],
'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-prototype-builtins': 'off',
'@typescript-eslint/no-var-requires': [
'off',
],
'@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,
ignoredNodes: ['TemplateLiteral > *'],
},
],
'@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: 105,
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',
],
},
});

View file

@ -1,123 +0,0 @@
import { App, Gdk } from 'astal/gtk3';
import AstalHyprland from 'gi://AstalHyprland';
const Hyprland = AstalHyprland.get_default();
/* Types */
import PopupWindow from './widgets/misc/popup-window';
export interface Layer {
address: string
x: number
y: number
w: number
h: number
namespace: string
}
export interface Levels {
0?: Layer[] | null
1?: Layer[] | null
2?: Layer[] | null
3?: Layer[] | null
}
export interface Layers {
levels: Levels
}
export type LayerResult = Record<string, Layers>;
export interface CursorPos {
x: number
y: number
}
export const get_hyprland_monitor = (monitor: Gdk.Monitor): AstalHyprland.Monitor | undefined => {
const manufacturer = monitor.manufacturer?.replace(',', '');
const model = monitor.model?.replace(',', '');
const start = `${manufacturer} ${model}`;
return Hyprland.get_monitors().find((m) => m.description?.startsWith(start));
};
export const get_hyprland_monitor_desc = (monitor: Gdk.Monitor): string => {
const manufacturer = monitor.manufacturer?.replace(',', '');
const model = monitor.model?.replace(',', '');
const start = `${manufacturer} ${model}`;
return `desc:${Hyprland.get_monitors().find((m) => m.description?.startsWith(start))?.description}`;
};
export const get_gdkmonitor_from_desc = (desc: string): Gdk.Monitor => {
const display = Gdk.Display.get_default();
for (let m = 0; m < (display?.get_n_monitors() ?? 0); m++) {
const monitor = display?.get_monitor(m);
if (monitor && desc === get_hyprland_monitor_desc(monitor)) {
return monitor;
}
}
throw Error(`Monitor ${desc} not found`);
};
export const get_monitor_desc = (mon: AstalHyprland.Monitor): string => {
return `desc:${mon.description}`;
};
export const hyprMessage = (message: string) => new Promise<string>((
resolution = () => { /**/ },
rejection = () => { /**/ },
) => {
try {
Hyprland.message_async(message, (_, asyncResult) => {
const result = Hyprland.message_finish(asyncResult);
resolution(result);
});
}
catch (e) {
rejection(e);
}
});
export const centerCursor = async(): Promise<void> => {
let x: number;
let y: number;
const monitor = (JSON.parse(await hyprMessage('j/monitors')) as AstalHyprland.Monitor[])
.find((m) => m.focused) as AstalHyprland.Monitor;
// @ts-expect-error this should be good
switch (monitor.transform) {
case 1:
x = monitor.x - (monitor.height / 2);
y = monitor.y - (monitor.width / 2);
break;
case 2:
x = monitor.x - (monitor.width / 2);
y = monitor.y - (monitor.height / 2);
break;
case 3:
x = monitor.x + (monitor.height / 2);
y = monitor.y + (monitor.width / 2);
break;
default:
x = monitor.x + (monitor.width / 2);
y = monitor.y + (monitor.height / 2);
break;
}
await hyprMessage(`dispatch movecursor ${x} ${y}`);
};
export const closeAll = () => {
(App.get_windows() as PopupWindow[])
.filter((w) => w &&
w.close_on_unfocus &&
w.close_on_unfocus !== 'stay')
.forEach((w) => {
App.get_window(w.name)?.set_visible(false);
});
};

File diff suppressed because it is too large Load diff

View file

@ -1,17 +0,0 @@
{
"name": "ags-v2",
"version": "0.0.0",
"main": "app.ts",
"dependencies": {
"@eslint/js": "9.13.0",
"@stylistic/eslint-plugin": "2.9.0",
"@types/eslint__js": "8.42.3",
"@types/node": "22.8.1",
"eslint": "9.13.0",
"eslint-plugin-jsdoc": "50.4.3",
"fzf": "0.5.2",
"jiti": "2.3.3",
"typescript": "5.6.3",
"typescript-eslint": "8.11.0"
}
}

View file

@ -1,156 +0,0 @@
import { execAsync, interval } from 'astal';
import GObject, { register, property } from 'astal/gobject';
const SCREEN_ICONS: Record<number, string> = {
90: 'display-brightness-high-symbolic',
70: 'display-brightness-medium-symbolic',
20: 'display-brightness-low-symbolic',
5: 'display-brightness-off-symbolic',
};
const INTERVAL = 500;
@register()
class Brightness extends GObject.Object {
declare private _kbd: string;
declare private _caps: string;
declare private _screen: number;
@property(Number)
get screen() {
return this._screen;
};
set screen(percent) {
if (percent < 0) {
percent = 0;
}
if (percent > 1) {
percent = 1;
}
percent = parseFloat(percent.toFixed(2));
execAsync(`brightnessctl s ${percent * 100}% -q`)
.then(() => {
this._screen = percent;
this.notify('screen');
this._getScreenIcon();
})
.catch(console.error);
}
private _screenIcon = 'display-brightness-high-symbolic';
@property(String)
get screenIcon() {
return this._screenIcon;
}
declare private _kbdMax: number;
declare private _kbdLevel: number;
@property(Number)
get kbdLevel() {
return this._kbdLevel;
}
set kbdLevel(value) {
if (value < 0 || value > this._kbdMax) {
return;
}
execAsync(`brightnessctl -d ${this._kbd} s ${value} -q`)
.then(() => {
this._kbdLevel = value;
this.notify('kbd-level');
})
.catch(console.error);
}
declare private _capsLevel: number;
@property(Number)
get capsLevel() {
return this._capsLevel;
}
private _capsIcon = 'caps-lock-symbolic';
@property(String)
get capsIcon() {
return this._capsIcon;
}
/**
* This is to basically have the constructor run when I want and
* still export this to wherever I need to.
*
* @param o params
* @param o.kbd name of kbd in brightnessctl
* @param o.caps name of caps_lock in brightnessctl
*/
public async initService({ kbd = '', caps = '' }) {
this._kbd = kbd;
this._caps = caps;
try {
this._monitorKbdState();
this._kbdMax = Number(await execAsync(`brightnessctl -d ${this._kbd} m`));
this._screen = Number(await execAsync('brightnessctl g')) /
Number(await execAsync('brightnessctl m'));
}
catch (_e) {
console.error('missing dependancy: brightnessctl');
}
}
private _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');
}
}
}
private _monitorKbdState() {
const timer = interval(INTERVAL, () => {
execAsync(`brightnessctl -d ${this._kbd} g`).then(
(out) => {
if (parseInt(out) !== this._kbdLevel) {
this._kbdLevel = parseInt(out);
this.notify('kbd-level');
}
},
).catch(() => {
timer?.cancel();
});
});
}
public fetchCapsState() {
execAsync(`brightnessctl -d ${this._caps} g`)
.then((out) => {
this._capsLevel = Number(out);
this._capsIcon = this._capsLevel ?
'caps-lock-symbolic' :
'capslock-disabled-symbolic';
this.notify('caps-icon');
this.notify('caps-level');
})
.catch(logError);
}
}
const brightnessService = new Brightness();
export default brightnessService;

View file

@ -1,193 +0,0 @@
import { subprocess } from 'astal';
import { App } from 'astal/gtk3';
import GObject, { register, signal } from 'astal/gobject';
import AstalIO from 'gi://AstalIO';
import { hyprMessage } from '../lib';
const ON_RELEASE_TRIGGERS = [
'released',
'TOUCH_UP',
'HOLD_END',
];
const ON_CLICK_TRIGGERS = [
'pressed',
'TOUCH_DOWN',
];
/* Types */
import PopupWindow from '../widgets/misc/popup-window';
import { CursorPos, Layer, LayerResult } from '../lib';
@register()
export default class MonitorClicks extends GObject.Object {
@signal(Boolean)
declare procStarted: (state: boolean) => void;
@signal(Boolean)
declare procDestroyed: (state: boolean) => void;
@signal(String)
declare released: (procLine: string) => void;
@signal(String)
declare clicked: (procLine: string) => void;
private process = null as AstalIO.Process | null;
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))) {
MonitorClicks.detectClickedOutside('released');
this.emit('released', output);
}
if (ON_CLICK_TRIGGERS.some((p) => output.includes(p))) {
MonitorClicks.detectClickedOutside('clicked');
this.emit('clicked', output);
}
},
);
this.emit('proc-started', true);
}
killProc() {
if (this.process) {
this.process.kill();
this.process = null;
this.emit('proc-destroyed', true);
}
}
#initAppConnection() {
App.connect('window-toggled', () => {
const anyVisibleAndClosable =
(App.get_windows() as PopupWindow[]).some((w) => {
const closable = w.close_on_unfocus &&
!(
w.close_on_unfocus === 'none' ||
w.close_on_unfocus === 'stay'
);
return w.visible && closable;
});
if (anyVisibleAndClosable) {
this.startProc();
}
else {
this.killProc();
}
});
}
static async detectClickedOutside(clickStage: string) {
const toClose = ((App.get_windows() as PopupWindow[])).some((w) => {
const closable = (
w.close_on_unfocus &&
w.close_on_unfocus === clickStage
);
return w.visible && closable;
});
if (!toClose) {
return;
}
try {
const layers = JSON.parse(await hyprMessage('j/layers')) as LayerResult;
const pos = JSON.parse(await hyprMessage('j/cursorpos')) as CursorPos;
Object.values(layers).forEach((key) => {
const overlayLayer = key.levels['3'];
if (overlayLayer) {
const noCloseWidgetsNames = [
'bar-',
'osk',
];
const getNoCloseWidgets = (names: string[]) => {
const arr = [] as Layer[];
names.forEach((name) => {
arr.push(
overlayLayer.find(
(n) => n.namespace.startsWith(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) => {
let window = null as null | PopupWindow;
if (App.get_windows().some((win) => win.name === n.namespace)) {
window = (App.get_window(n.namespace) as PopupWindow);
}
return window &&
window.close_on_unfocus &&
window.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.get_window(w.namespace)?.set_visible(false);
}
});
}
}
});
}
catch (e) {
console.log(e);
}
}
}

View file

@ -1,51 +0,0 @@
@import 'colors.scss';
window,
viewport,
stack {
all: unset;
}
progressbar {
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;
}
&:disabled {
opacity: 0.5;
}
}
.widget {
margin: 10px;
padding: 5px;
border-radius: 7px;
background-color: rgba($window_bg_color, 1);
box-shadow: 8px 8px rgba($accent_color, 1);
}
@import 'widgets/applauncher/style.scss';
@import 'widgets/bar/style.scss';
@import 'widgets/clipboard/style.scss';
@import 'widgets/date/style.scss';
@import 'widgets/icon-browser/style.scss';
@import 'widgets/lockscreen/style.scss';
@import 'widgets/misc/style.scss';
@import 'widgets/notifs/style.scss';
@import 'widgets/osd/style.scss';
@import 'widgets/powermenu/style.scss';
@import 'widgets/screenshot/style.scss';

View file

@ -1,65 +0,0 @@
import { Gtk, Widget } from 'astal/gtk3';
import { register } from 'astal/gobject';
/* Types */
import AstalApps from 'gi://AstalApps';
type AppItemProps = Widget.BoxProps & {
app: AstalApps.Application
};
@register()
export class AppItem extends Widget.Box {
readonly app: AstalApps.Application;
constructor({
app,
hexpand = true,
className = '',
...rest
}: AppItemProps) {
super({
...rest,
className: `app ${className}`,
hexpand,
});
this.app = app;
const icon = (
<icon
icon={this.app.iconName}
css="font-size: 42px; margin-right: 25px;"
/>
);
const textBox = (
<box
vertical
>
<label
className="title"
label={app.name}
xalign={0}
truncate
valign={Gtk.Align.CENTER}
/>
{app.description !== '' && (
<label
className="description"
label={app.description}
wrap
xalign={0}
justify={Gtk.Justification.LEFT}
valign={Gtk.Align.CENTER}
/>
)}
</box>
);
this.add(icon);
this.add(textBox);
}
}
export default AppItem;

View file

@ -1,27 +0,0 @@
import { execAsync } from 'astal';
import AstalApps from 'gi://AstalApps';
const bash = async(strings: TemplateStringsArray | string, ...values: unknown[]) => {
const cmd = typeof strings === 'string' ?
strings :
strings.flatMap((str, i) => `${str}${values[i] ?? ''}`)
.join('');
return execAsync(['bash', '-c', cmd]).catch((err) => {
console.error(cmd, err);
return '';
});
};
export const launchApp = (app: AstalApps.Application) => {
const exe = app.executable
.split(/\s+/)
.filter((str) => !str.startsWith('%') && !str.startsWith('@'))
.join(' ');
bash(`${exe} &`);
app.frequency += 1;
};

View file

@ -1,55 +0,0 @@
import { App } from 'astal/gtk3';
import AstalApps from 'gi://AstalApps';
import SortedList from '../misc/sorted-list';
import { launchApp } from './launch';
import AppItem from './app-item';
export default () => SortedList({
name: 'applauncher',
create_list: () => AstalApps.Apps.new().get_list(),
create_row: (app) => <AppItem app={app} />,
fzf_options: {
selector: (app) => app.name + app.executable,
tiebreakers: [
(a, b) => b.item.frequency - a.item.frequency,
],
},
compare_props: ['name', 'frequency'],
on_row_activated: (row) => {
const app = (row.get_children()[0] as AppItem).app;
launchApp(app);
App.get_window('win-applauncher')?.set_visible(false);
},
sort_func: (a, b, entry, fzfResults) => {
const row1 = (a.get_children()[0] as AppItem).app;
const row2 = (b.get_children()[0] as AppItem).app;
if (entry.text === '' || entry.text === '-') {
a.set_visible(true);
b.set_visible(true);
return row2.frequency - row1.frequency;
}
else {
const s1 = fzfResults.find((r) => r.item.name === row1.name)?.score ?? 0;
const s2 = fzfResults.find((r) => r.item.name === row2.name)?.score ?? 0;
a.set_visible(s1 !== 0);
b.set_visible(s2 !== 0);
return s2 - s1;
}
},
});

View file

@ -1,6 +0,0 @@
.applauncher {
.app {
margin: 20px;
font-size: 16px;
}
}

View file

@ -1,187 +0,0 @@
import { App, Astal, Gdk, Gtk, Widget } from 'astal/gtk3';
import { bind, Variable } from 'astal';
import AstalHyprland from 'gi://AstalHyprland';
const Hyprland = AstalHyprland.get_default();
import { get_hyprland_monitor_desc, get_monitor_desc, hyprMessage } from '../../lib';
const FullscreenState = Variable({
monitors: [] as string[],
clientAddrs: new Map() as Map<string, string>,
});
Hyprland.connect('event', async() => {
const arrayEquals = (a1: unknown[], a2: unknown[]) =>
a1.sort().toString() === a2.sort().toString();
const mapEquals = (m1: Map<string, string>, m2: Map<string, string>) =>
m1.size === m2.size &&
Array.from(m1.keys()).every((key) => m1.get(key) === m2.get(key));
try {
const newMonitors = JSON.parse(await hyprMessage('j/monitors')) as AstalHyprland.Monitor[];
const fs = FullscreenState.get();
const fsClients = Hyprland.get_clients().filter((c) => {
const mon = newMonitors.find((monitor) => monitor.id === c.get_monitor()?.id);
return c.fullscreenClient !== 0 &&
c.workspace.id === mon?.activeWorkspace.id;
});
const monitors = fsClients.map((c) =>
get_monitor_desc(c.monitor));
const clientAddrs = new Map(fsClients.map((c) => [
get_monitor_desc(c.monitor),
c.address ?? '',
]));
const hasChanged =
!arrayEquals(monitors, fs.monitors) ||
!mapEquals(clientAddrs, fs.clientAddrs);
if (hasChanged) {
FullscreenState.set({
monitors,
clientAddrs,
});
}
}
catch (e) {
console.log(e);
}
});
export default ({
anchor,
gdkmonitor = Gdk.Display.get_default()?.get_monitor(0) as Gdk.Monitor,
child,
...rest
}: {
anchor: Astal.WindowAnchor
gdkmonitor?: Gdk.Monitor
} & Widget.WindowProps) => {
const monitor = get_hyprland_monitor_desc(gdkmonitor);
const BarVisible = Variable(true);
FullscreenState.subscribe((v) => {
BarVisible.set(!v.monitors.includes(monitor));
});
const barCloser = (
<window
name={`bar-${monitor}-closer`}
css="all: unset;"
visible={false}
gdkmonitor={gdkmonitor}
layer={Astal.Layer.OVERLAY}
anchor={
Astal.WindowAnchor.TOP |
Astal.WindowAnchor.BOTTOM |
Astal.WindowAnchor.LEFT |
Astal.WindowAnchor.RIGHT
}
>
<eventbox
on_hover={() => {
barCloser.visible = false;
BarVisible.set(false);
}}
>
<box css="padding: 1px;" />
</eventbox>
</window>
);
// Hide bar instantly when out of focus
Hyprland.connect('notify::focused-workspace', () => {
const addr = FullscreenState.get().clientAddrs.get(monitor);
if (addr) {
const client = Hyprland.get_client(addr);
if (client?.workspace.id !== Hyprland.get_focused_workspace().get_id()) {
BarVisible.set(true);
barCloser.visible = false;
}
else {
BarVisible.set(false);
barCloser.visible = true;
}
}
});
const buffer = (
<box
css="min-height: 10px;"
visible={bind(BarVisible).as((v) => !v)}
/>
);
const vertical = anchor >= (Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT);
const isBottomOrLeft = (
anchor === (Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM)
) || (
anchor === (Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM)
);
let transition: Gtk.RevealerTransitionType;
if (vertical) {
transition = isBottomOrLeft ?
Gtk.RevealerTransitionType.SLIDE_UP :
Gtk.RevealerTransitionType.SLIDE_DOWN;
}
else {
transition = isBottomOrLeft ?
Gtk.RevealerTransitionType.SLIDE_RIGHT :
Gtk.RevealerTransitionType.SLIDE_LEFT;
}
const barWrap = (
<revealer
reveal_child={bind(BarVisible)}
transitionType={transition}
>
{child}
</revealer>
);
const win = (
<window
name={`bar-${monitor}`}
namespace={`bar-${monitor}`}
layer={Astal.Layer.OVERLAY}
gdkmonitor={gdkmonitor}
anchor={anchor}
{...rest}
>
<eventbox
onHover={() => {
if (!BarVisible.get()) {
barCloser.visible = true;
BarVisible.set(true);
}
}}
>
<box
css="min-height: 1px; padding: 1px;"
hexpand
halign={Gtk.Align.FILL}
vertical={vertical}
>
{isBottomOrLeft ?
[buffer, barWrap] :
[barWrap, buffer]}
</box>
</eventbox>
</window>
) as Widget.Window;
App.add_window(win);
return win;
};

View file

@ -1,45 +0,0 @@
import { bind } from 'astal';
import AstalBattery from 'gi://AstalBattery';
const Battery = AstalBattery.get_default();
import Separator from '../../misc/separator';
const LOW_BATT = 20;
export default () => (
<box className="bar-item battery">
<icon
setup={(self) => {
const update = () => {
const percent = Math.round(Battery.get_percentage() * 100);
const level = Math.floor(percent / 10) * 10;
const isCharging = Battery.get_charging();
const charged = percent === 100 && isCharging;
const iconName = charged ?
'battery-level-100-charged-symbolic' :
`battery-level-${level}${isCharging ?
'-charging' :
''}-symbolic`;
self.set_icon(iconName);
self.toggleClassName('charging', isCharging);
self.toggleClassName('charged', charged);
self.toggleClassName('low', percent < LOW_BATT);
};
update();
Battery.connect('notify::percentage', () => update());
Battery.connect('notify::icon-name', () => update());
Battery.connect('notify::battery-icon-name', () => update());
}}
/>
<Separator size={8} />
<label label={bind(Battery, 'percentage').as((v) => `${Math.round(v * 100)}%`)} />
</box>
);

View file

@ -1,55 +0,0 @@
import { bind, Variable } from 'astal';
import { App } from 'astal/gtk3';
import GLib from 'gi://GLib';
import PopupWindow from '../../misc/popup-window';
export default () => {
const timeVar = Variable<string>('').poll(1000, (prev) => {
const time = GLib.DateTime.new_now_local();
const dayName = time.format('%a. ');
const dayNum = time.get_day_of_month();
const date = time.format(' %b. ');
const hour = (new Date().toLocaleString([], {
hour: 'numeric',
minute: 'numeric',
hour12: true,
}) ?? '')
.replace('a.m.', 'AM')
.replace('p.m.', 'PM');
return (dayNum && dayName && date) ?
(dayName + dayNum + date + hour) :
prev;
});
return (
<button
className="bar-item"
cursor="pointer"
onButtonReleaseEvent={(self) => {
const win = App.get_window('win-calendar') as PopupWindow;
win.set_x_pos(
self.get_allocation(),
'right',
);
win.visible = !win.visible;
}}
setup={(self) => {
App.connect('window-toggled', (_, win) => {
if (win.name === 'win-notif-center') {
self.toggleClassName('toggle-on', win.visible);
}
});
}}
>
<label label={bind(timeVar)} />
</button>
);
};

View file

@ -1,78 +0,0 @@
import { bind, Variable } from 'astal';
import AstalApps from 'gi://AstalApps';
const Applications = AstalApps.Apps.new();
import AstalHyprland from 'gi://AstalHyprland';
const Hyprland = AstalHyprland.get_default();
import Separator from '../../misc/separator';
import { hyprMessage } from '../../../lib';
export default () => {
const visibleIcon = Variable<boolean>(false);
const focusedIcon = Variable<string>('');
const focusedTitle = Variable<string>('');
let lastFocused: string | undefined;
const updateVars = (
client: AstalHyprland.Client | null = Hyprland.get_focused_client(),
) => {
lastFocused = client?.get_address();
const app = Applications.fuzzy_query(
client?.get_class() ?? '',
)[0];
const icon = app?.iconName;
if (icon) {
visibleIcon.set(true);
focusedIcon.set(icon);
}
else {
visibleIcon.set(false);
}
focusedTitle.set(client?.get_title() ?? '');
const id = client?.connect('notify::title', (c) => {
if (c.get_address() !== lastFocused) {
c.disconnect(id);
}
focusedTitle.set(c.get_title());
});
};
updateVars();
Hyprland.connect('notify::focused-client', () => updateVars());
Hyprland.connect('client-removed', () => updateVars());
Hyprland.connect('client-added', async() => {
try {
updateVars(Hyprland.get_client(JSON.parse(await hyprMessage('j/activewindow')).address));
}
catch (e) {
console.log(e);
}
});
return (
<box
className="bar-item current-window"
visible={bind(focusedTitle).as((title) => title !== '')}
>
<icon
css="font-size: 32px;"
icon={bind(focusedIcon)}
visible={bind(visibleIcon)}
/>
<Separator size={8} />
<label
label={bind(focusedTitle)}
truncate
/>
</box>
);
};

View file

@ -1,59 +0,0 @@
import { bind } from 'astal';
import { App } from 'astal/gtk3';
import AstalNotifd from 'gi://AstalNotifd';
const Notifications = AstalNotifd.get_default();
import Separator from '../../misc/separator';
const SPACING = 4;
// Types
import PopupWindow from '../../misc/popup-window';
export default () => (
<button
className="bar-item"
cursor="pointer"
onButtonReleaseEvent={(self) => {
const win = App.get_window('win-notif-center') as PopupWindow;
win.set_x_pos(
self.get_allocation(),
'right',
);
win.visible = !win.visible;
}}
setup={(self) => {
App.connect('window-toggled', (_, win) => {
if (win.name === 'win-notif-center') {
self.toggleClassName('toggle-on', win.visible);
}
});
}}
>
<box>
<icon
icon={bind(Notifications, 'notifications').as((notifs) => {
if (Notifications.dontDisturb) {
return 'notification-disabled-symbolic';
}
else if (notifs.length > 0) {
return 'notification-new-symbolic';
}
else {
return 'notification-symbolic';
}
})}
/>
<Separator size={SPACING} />
<label label={bind(Notifications, 'notifications').as((n) => String(n.length))} />
</box>
</button>
);

View file

@ -1,78 +0,0 @@
import { App, Gdk, Gtk, Widget } from 'astal/gtk3';
import { bind, idle } from 'astal';
import AstalTray from 'gi://AstalTray';
const Tray = AstalTray.get_default();
const SKIP_ITEMS = ['.spotify-wrapped'];
const TrayItem = (item: AstalTray.TrayItem) => {
if (item.iconThemePath) {
App.add_icons(item.iconThemePath);
}
const menu = item.create_menu();
return (
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT}
revealChild={false}
>
<button
className="tray-item"
cursor="pointer"
tooltipMarkup={bind(item, 'tooltipMarkup')}
onDestroy={() => menu?.destroy()}
onClickRelease={(self) => {
menu?.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
}}
>
<icon gIcon={bind(item, 'gicon')} />
</button>
</revealer>
);
};
export default () => {
const itemMap = new Map<string, Widget.Revealer>();
return (
<box
className="bar-item system-tray"
visible={bind(Tray, 'items').as((items) => items.length !== 0)}
setup={(self) => {
self
.hook(Tray, 'item-added', (_, item: string) => {
if (itemMap.has(item) || SKIP_ITEMS.includes(Tray.get_item(item).title)) {
return;
}
const widget = TrayItem(Tray.get_item(item)) as Widget.Revealer;
itemMap.set(item, widget);
self.add(widget);
idle(() => {
widget.set_reveal_child(true);
});
})
.hook(Tray, 'item-removed', (_, item: string) => {
if (!itemMap.has(item)) {
return;
}
const widget = itemMap.get(item);
widget?.set_reveal_child(false);
setTimeout(() => {
widget?.destroy();
}, 1000);
});
}}
/>
);
};

View file

@ -1,172 +0,0 @@
import { Gtk, Widget } from 'astal/gtk3';
import { timeout } from 'astal';
import AstalHyprland from 'gi://AstalHyprland';
const Hyprland = AstalHyprland.get_default();
import { hyprMessage } from '../../../lib';
const URGENT_DURATION = 1000;
const Workspace = ({ id = 0 }) => (
<revealer
name={id.toString()}
transitionType={Gtk.RevealerTransitionType.SLIDE_RIGHT}
>
<eventbox
cursor="pointer"
tooltip_text={id.toString()}
onClickRelease={() => {
hyprMessage(`dispatch workspace ${id}`).catch(console.log);
}}
>
<box
valign={Gtk.Align.CENTER}
className="button"
setup={(self) => {
const update = (
_: Widget.Box,
client?: AstalHyprland.Client,
) => {
const workspace = Hyprland.get_workspace(id);
const occupied = workspace && workspace.get_clients().length > 0;
self.toggleClassName('occupied', occupied);
if (!client) {
return;
}
const isUrgent = client &&
client.get_workspace().get_id() === id;
if (isUrgent) {
self.toggleClassName('urgent', true);
// Only show for a sec when urgent is current workspace
if (Hyprland.get_focused_workspace().get_id() === id) {
timeout(URGENT_DURATION, () => {
self.toggleClassName('urgent', false);
});
}
}
};
update(self);
self
.hook(Hyprland, 'event', () => update(self))
// Deal with urgent windows
.hook(Hyprland, 'urgent', update)
.hook(Hyprland, 'notify::focused-workspace', () => {
if (Hyprland.get_focused_workspace().get_id() === id) {
self.toggleClassName('urgent', false);
}
});
}}
/>
</eventbox>
</revealer>
);
export default () => {
const L_PADDING = 2;
const WS_WIDTH = 30;
const updateHighlight = (self: Widget.Box) => {
const currentId = Hyprland.get_focused_workspace().get_id().toString();
const indicators = ((self.get_parent() as Widget.Overlay)
.child as Widget.Box)
.children as Widget.Revealer[];
const currentIndex = indicators.findIndex((w) => w.name === currentId);
if (currentIndex >= 0) {
self.css = `margin-left: ${L_PADDING + (currentIndex * WS_WIDTH)}px`;
}
};
const highlight = (
<box
className="button active"
valign={Gtk.Align.CENTER}
halign={Gtk.Align.START}
setup={(self) => {
self.hook(Hyprland, 'notify::focused-workspace', updateHighlight);
}}
/>
) as Widget.Box;
let workspaces: Widget.Revealer[] = [];
return (
<box
className="bar-item"
>
<overlay
className="workspaces"
passThrough
overlay={highlight}
>
<box
setup={(self) => {
const refresh = () => {
(self.children as Widget.Revealer[]).forEach((rev) => {
rev.reveal_child = false;
});
workspaces.forEach((ws) => {
ws.reveal_child = true;
});
};
const updateWorkspaces = () => {
Hyprland.get_workspaces().forEach((ws) => {
const currentWs = (self.children as Widget.Revealer[])
.find((ch) => ch.name === ws.id.toString());
if (!currentWs && ws.id > 0) {
self.add(Workspace({ id: ws.id }));
}
});
// Make sure the order is correct
workspaces.forEach((workspace, i) => {
(workspace.get_parent() as Widget.Box)
.reorder_child(workspace, i);
});
};
const updateAll = () => {
workspaces = (self.children as Widget.Revealer[])
.filter((ch) => {
return Hyprland.get_workspaces().find((ws) => {
return ws.id.toString() === ch.name;
});
})
.sort((a, b) => parseInt(a.name ?? '0') - parseInt(b.name ?? '0'));
updateWorkspaces();
refresh();
// Make sure the highlight doesn't go too far
const TEMP_TIMEOUT = 100;
timeout(TEMP_TIMEOUT, () => updateHighlight(highlight));
};
updateAll();
self.hook(Hyprland, 'event', updateAll);
}}
/>
</overlay>
</box>
);
};

View file

@ -1,70 +0,0 @@
.bar {
margin-left: 5px;
margin-right: 15px;
margin-bottom: 13px;
.bar-item {
padding: 5px 10px 5px 10px;
border-radius: 7px;
background-color: darken($window_bg_color, 3%);
font-size: 20px;
min-height: 35px;
transition: background-color 300ms;
&:hover {
background-color: lighten($window_bg_color, 3%);
}
&.battery icon {
&.charging {
color: green;
}
&.low {
color: red;
}
}
.workspaces {
.button {
margin: 0 2.5px;
min-height: 22px;
min-width: 22px;
border-radius: 100%;
border: 2px solid transparent;
}
.occupied {
border: 2px solid $window_bg_color;
background: $accent_color;
transition: background-color 0.3s ease-in-out;
}
.urgent {
border: 2px solid $window_bg_color;
background: red;
transition: background-color 0.3s ease-in-out;
}
.active {
border: 2px solid #50fa7b;
transition: margin-left 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
}
&.system-tray {
.tray-item {
all: unset;
font-size: 30px;
min-width: 36px;
border-radius: 100%;
transition: background-color 300ms;
&:hover {
background: $window_bg_color;
}
}
}
}
}

View file

@ -1,54 +0,0 @@
import { Astal, Gtk } from 'astal/gtk3';
import Battery from './items/battery';
import Clock from './items/clock';
import CurrentClient from './items/current-client';
import NotifButton from './items/notif-button';
import SysTray from './items/tray';
import Workspaces from './items/workspaces';
import BarRevealer from './fullscreen';
import Separator from '../misc/separator';
export default () => (
<BarRevealer
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={
Astal.WindowAnchor.TOP |
Astal.WindowAnchor.LEFT |
Astal.WindowAnchor.RIGHT
}
>
<centerbox className="bar widget">
<box hexpand halign={Gtk.Align.START}>
<Workspaces />
<Separator size={8} />
<SysTray />
<Separator size={8} />
<CurrentClient />
<Separator size={8} />
</box>
<box>
<Clock />
</box>
<box hexpand halign={Gtk.Align.END}>
<NotifButton />
<Separator size={8} />
<Battery />
<Separator size={2} />
</box>
</centerbox>
</BarRevealer>
);

View file

@ -1,24 +0,0 @@
import { Astal } from 'astal/gtk3';
export default () => {
return (
<window
name="bg-fade"
layer={Astal.Layer.BACKGROUND}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={
Astal.WindowAnchor.TOP |
Astal.WindowAnchor.BOTTOM |
Astal.WindowAnchor.LEFT |
Astal.WindowAnchor.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

@ -1,98 +0,0 @@
import { execAsync } from 'astal';
import { register } from 'astal/gobject';
import { Gtk, Widget } from 'astal/gtk3';
export interface EntryObject {
id: number
content: string
entry: string
}
const SCALE = 150;
const BINARY_DATA = /\[\[ binary data (\d+) (KiB|MiB) (\w+) (\d+)x(\d+) \]\]/;
export const CLIP_SCRIPT = `${SRC}/widgets/clipboard/cliphist.sh`;
@register()
export class ClipItem extends Widget.Box {
declare id: number;
declare content: string;
public show_image(file: string, width: string | number, height: string | number) {
this.children[2].destroy();
const initCss = () => {
const _widthPx = Number(width);
const heightPx = Number(height);
const maxWidth = 400;
const widthPx = (_widthPx / heightPx) * SCALE;
let css = `background-image: url("${file}");`;
if (widthPx > maxWidth) {
const newHeightPx = (SCALE / widthPx) * maxWidth;
css += `min-height: ${newHeightPx}px; min-width: ${maxWidth}px;`;
}
else {
css += `min-height: 150px; min-width: ${widthPx}px;`;
}
return css;
};
const icon = (
<box
valign={Gtk.Align.CENTER}
css={initCss()}
/>
);
this.children = [...this.children, icon];
};
constructor({ item }: { item: EntryObject }) {
super({
children: [
<label
label={item.id.toString()}
xalign={0}
valign={Gtk.Align.CENTER}
/>,
<label
label="・"
xalign={0}
valign={Gtk.Align.CENTER}
/>,
<label
label={item.content}
xalign={0}
valign={Gtk.Align.CENTER}
truncate
/>,
],
});
this.id = item.id;
this.content = item.content;
const matches = this.content.match(BINARY_DATA);
if (matches) {
// const size = matches[1];
const format = matches[3];
const width = matches[4];
const height = matches[5];
if (format === 'png') {
execAsync(`${CLIP_SCRIPT} --save-by-id ${this.id}`)
.then((file) => {
this.show_image(file, width, height);
})
.catch(print);
}
}
}
}
export default ClipItem;

View file

@ -1,44 +0,0 @@
#!/usr/bin/env bash
# https://github.com/koeqaife/hyprland-material-you/blob/d23cf9d524522c8c215664c2c3334c2b51609cae/ags/scripts/cliphist.sh
get() {
cliphist list | iconv -f "$(locale charmap)" -t UTF-8 -c
}
copy_by_id() {
id=$1
cliphist decode "$id" | wl-copy
}
clear() {
cliphist wipe
}
save_cache_file() {
id=$1
output_file="/tmp/ags/cliphist/$id.png"
if [[ ! -f "$output_file" ]]; then
mkdir -p "/tmp/ags/cliphist/"
cliphist decode "$id" >"$output_file"
fi
echo "$output_file"
}
clear_tmp() {
rm "/tmp/ags/cliphist/*"
}
if [[ "$1" == "--get" ]]; then
get
elif [[ "$1" == "--copy-by-id" ]]; then
{ copy_by_id "$2"; }
elif [[ "$1" == "--save-by-id" ]]; then
{ save_cache_file "$2"; }
elif [[ "$1" == "--clear-cache" ]]; then
clear_tmp
elif [[ "$1" == "--clear" ]]; then
clear
fi

View file

@ -1,66 +0,0 @@
import { execAsync } from 'astal';
import { App } from 'astal/gtk3';
import SortedList from '../misc/sorted-list';
import { CLIP_SCRIPT, ClipItem, EntryObject } from './clip-item';
export default () => SortedList<EntryObject>({
name: 'clipboard',
create_list: async() => {
const output = await execAsync(`${CLIP_SCRIPT} --get`)
.then((str) => str)
.catch((err) => {
print(err);
return '';
});
return output
.split('\n')
.filter((line) => line.trim() !== '')
.map((entry) => {
const [id, ...content] = entry.split('\t');
return { id: parseInt(id.trim()), content: content.join(' ').trim(), entry };
});
},
create_row: (item) => <ClipItem item={item} />,
fzf_options: {
selector: (item) => item.content,
},
compare_props: ['id'],
on_row_activated: (row) => {
const clip = row.get_children()[0] as ClipItem;
execAsync(`${CLIP_SCRIPT} --copy-by-id ${clip.id}`);
App.get_window('win-clipboard')?.set_visible(false);
},
sort_func: (a, b, entry, fzfResults) => {
const row1 = a.get_children()[0] as ClipItem;
const row2 = b.get_children()[0] as ClipItem;
if (entry.text === '' || entry.text === '-') {
a.set_visible(true);
b.set_visible(true);
return row2.id - row1.id;
}
else {
const s1 = fzfResults.find((r) => r.item.id === row1.id)?.score ?? 0;
const s2 = fzfResults.find((r) => r.item.id === row2.id)?.score ?? 0;
a.set_visible(s1 !== 0);
b.set_visible(s2 !== 0);
return s2 - s1;
}
},
});

View file

@ -1,4 +0,0 @@
.clipboard .list row box {
margin: 20px;
font-size: 16px;
}

View file

@ -1,68 +0,0 @@
import { Astal } from 'astal/gtk3';
import RoundedCorner from './screen-corners';
const TopLeft = () => (
<window
name="cornertl"
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={
Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT
}
clickThrough={true}
>
{RoundedCorner('topleft')}
</window>
);
const TopRight = () => (
<window
name="cornertr"
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={
Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT
}
clickThrough={true}
>
{RoundedCorner('topright')}
</window>
);
const BottomLeft = () => (
<window
name="cornerbl"
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={
Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT
}
clickThrough={true}
>
{RoundedCorner('bottomleft')}
</window>
);
const BottomRight = () => (
<window
name="cornerbr"
layer={Astal.Layer.OVERLAY}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={
Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT
}
clickThrough={true}
>
{RoundedCorner('bottomright')}
</window>
);
export default () => [
TopLeft(),
TopRight(),
BottomLeft(),
BottomRight(),
];

View file

@ -1,81 +0,0 @@
import { Gtk } from 'astal/gtk3';
import Cairo from 'cairo';
export default (
place = 'top left',
css = 'background-color: black;',
) => (
<box
halign={place.includes('left') ? Gtk.Align.START : Gtk.Align.END}
valign={place.includes('top') ? Gtk.Align.START : Gtk.Align.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'};
`}
>
<drawingarea
css={`
border-radius: 18px;
border-width: 0.068rem;
${css}
`}
setup={(widget) => {
const styleContext = widget.get_style_context();
let radius = styleContext.get_property('border-radius', Gtk.StateFlags.NORMAL) as number;
widget.set_size_request(radius, radius);
widget.connect('draw', (_, cairoContext: Cairo.Context) => {
const bgColor = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
const borderColor = styleContext.get_color(Gtk.StateFlags.NORMAL);
const borderWidth = styleContext.get_border(Gtk.StateFlags.NORMAL).left;
radius = styleContext.get_property('border-radius', Gtk.StateFlags.NORMAL) as number;
widget.set_size_request(radius, radius);
switch (place) {
case 'topleft':
cairoContext.arc(radius, radius, radius, Math.PI, 3 * Math.PI / 2);
cairoContext.lineTo(0, 0);
break;
case 'topright':
cairoContext.arc(0, radius, radius, 3 * Math.PI / 2, 2 * Math.PI);
cairoContext.lineTo(radius, 0);
break;
case 'bottomleft':
cairoContext.arc(radius, 0, radius, Math.PI / 2, Math.PI);
cairoContext.lineTo(0, radius);
break;
case 'bottomright':
cairoContext.arc(0, 0, radius, 0, Math.PI / 2);
cairoContext.lineTo(radius, radius);
break;
}
cairoContext.closePath();
cairoContext.setSourceRGBA(bgColor.red, bgColor.green, bgColor.blue, bgColor.alpha);
cairoContext.fill();
cairoContext.setLineWidth(borderWidth);
cairoContext.setSourceRGBA(
borderColor.red,
borderColor.green,
borderColor.blue,
borderColor.alpha,
);
cairoContext.stroke();
});
}}
/>
</box>
);

View file

@ -1,97 +0,0 @@
import { bind, Variable } from 'astal';
import { Astal, Gtk } from 'astal/gtk3';
import GLib from 'gi://GLib';
import PopupWindow from '../misc/popup-window';
const Divider = () => (
<box
className="divider"
vertical
/>
);
const Time = () => {
const hour = Variable<string>('').poll(1000, () => GLib.DateTime.new_now_local().format('%H') || '');
const min = Variable<string>('').poll(1000, () => GLib.DateTime.new_now_local().format('%M') || '');
const fullDate = Variable<string>('').poll(1000, () => {
const time = GLib.DateTime.new_now_local();
const dayNameMonth = time.format('%A, %B ');
const dayNum = time.get_day_of_month();
const date = time.format(', %Y');
return dayNum && dayNameMonth && date ?
dayNameMonth + dayNum + date :
'';
});
return (
<box
className="timebox"
vertical
>
<box
className="time-container"
halign={Gtk.Align.CENTER}
valign={Gtk.Align.CENTER}
>
<label
className="content"
label={bind(hour)}
/>
<Divider />
<label
className="content"
label={bind(min)}
/>
</box>
<box
className="date-container"
halign={Gtk.Align.CENTER}
>
<label
css="font-size: 20px;"
label={bind(fullDate)}
/>
</box>
</box>
);
};
const DateWidget = () => {
const cal = new Gtk.Calendar({
show_day_names: true,
show_heading: true,
});
cal.show_all();
return (
<box
className="date widget"
vertical
>
<Time />
<box className="cal-box">
{cal}
</box>
</box>
);
};
export default () => (
<PopupWindow
name="calendar"
anchor={Astal.WindowAnchor.TOP}
>
<DateWidget />
</PopupWindow>
);

View file

@ -1,66 +0,0 @@
.date {
margin-top: 0;
}
.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 {
padding: 0 1rem .2rem;
margin: 0 12px 18px;
calendar {
font-size: 20px;
background-color: inherit;
padding: .5rem .10rem 0;
margin-left: 10px;
&>* {
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

@ -1,46 +0,0 @@
import { Gtk, Widget } from 'astal/gtk3';
import SortedList from '../misc/sorted-list';
export default () => SortedList({
name: 'icon-browser',
create_list: () => Gtk.IconTheme.get_default().list_icons(null)
.filter((icon) => icon.endsWith('symbolic'))
.sort(),
create_row: (icon) => (
<box>
<icon css="font-size: 60px; margin-right: 25px;" icon={icon} />
<label label={icon} />
</box>
),
on_row_activated: (row) => {
const icon = ((row.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
console.log(icon);
},
sort_func: (a, b, entry, fzfResults) => {
const row1 = ((a.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
const row2 = ((b.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
if (entry.text === '' || entry.text === '-') {
a.set_visible(true);
b.set_visible(true);
return row1.charCodeAt(0) - row2.charCodeAt(0);
}
else {
const s1 = fzfResults.find((r) => r.item === row1)?.score ?? 0;
const s2 = fzfResults.find((r) => r.item === row2)?.score ?? 0;
a.set_visible(s1 !== 0);
b.set_visible(s2 !== 0);
return s2 - s1;
}
},
});

View file

@ -1,4 +0,0 @@
.icon-browser .list row box {
margin: 20px;
font-size: 16px;
}

View file

@ -1,240 +0,0 @@
import { bind, idle, timeout, Variable } from 'astal';
import { App, Astal, Gdk, Gtk, Widget } from 'astal/gtk3';
import { register } from 'astal/gobject';
import AstalAuth from 'gi://AstalAuth';
import Lock from 'gi://GtkSessionLock';
import Separator from '../misc/separator';
import { get_hyprland_monitor_desc } from '../../lib';
declare global {
function authFinger(): void;
}
// This file is generated by Nix
import Vars from './vars';
export default () => {
const lock = Lock.prepare_lock();
const windows = new Map<Gdk.Monitor, Gtk.Window>();
@register()
class BlurredBox extends Widget.Box {
geometry = {} as { w: number, h: number };
}
const blurBGs: BlurredBox[] = [];
const transition_duration = 1000;
const WINDOW_MARGINS = -2;
const ENTRY_SPACING = 20;
const CLOCK_SPACING = 60;
const bgCSS = ({ w = 1, h = 1 } = {}) => `
border: 2px solid rgba(189, 147, 249, 0.8);
background: rgba(0, 0, 0, 0.2);
min-height: ${h}px;
min-width: ${w}px;
transition: min-height ${transition_duration / 2}ms,
min-width ${transition_duration / 2}ms;
`;
const unlock = () => {
blurBGs.forEach((b) => {
b.css = bgCSS({
w: b.geometry.w,
h: 1,
});
timeout(transition_duration / 2, () => {
b.css = bgCSS({
w: 1,
h: 1,
});
});
});
timeout(transition_duration, () => {
lock.unlock_and_destroy();
Gdk.Display.get_default()?.sync();
App.quit();
});
};
const Clock = () => {
const time = Variable<string>('').poll(1000, () => {
return (new Date().toLocaleString([], {
hour: 'numeric',
minute: 'numeric',
hour12: true,
}) ?? '')
.replace('a.m.', 'AM')
.replace('p.m.', 'PM');
});
return (
<label
className="lock-clock"
label={bind(time)}
/>
);
};
const PasswordPrompt = (monitor: Gdk.Monitor, visible: boolean) => {
const rev = new BlurredBox({ css: bgCSS() });
idle(() => {
rev.geometry = {
w: monitor.geometry.width,
h: monitor.geometry.height,
};
rev.css = bgCSS({
w: rev.geometry.w,
h: 1,
});
timeout(transition_duration / 2, () => {
rev.css = bgCSS({
w: rev.geometry.w,
h: rev.geometry.h,
});
});
});
blurBGs.push(rev);
<window
name={`blur-bg-${monitor.get_model()}`}
namespace={`blur-bg-${monitor.get_model()}`}
gdkmonitor={monitor}
layer={Astal.Layer.OVERLAY}
anchor={
Astal.WindowAnchor.TOP |
Astal.WindowAnchor.LEFT |
Astal.WindowAnchor.RIGHT |
Astal.WindowAnchor.BOTTOM
}
margin={WINDOW_MARGINS}
exclusivity={Astal.Exclusivity.IGNORE}
>
<box
halign={Gtk.Align.CENTER}
valign={Gtk.Align.CENTER}
>
{rev}
</box>
</window>;
const label = <label label="Enter password:" /> as Widget.Label;
return new Gtk.Window({
child: visible ?
(
<box
vertical
halign={Gtk.Align.CENTER}
valign={Gtk.Align.CENTER}
spacing={16}
>
<Clock />
<Separator size={CLOCK_SPACING} vertical />
<box
halign={Gtk.Align.CENTER}
className="avatar"
/>
<box
className="entry-box"
vertical
>
{label}
<Separator size={ENTRY_SPACING} vertical />
<entry
halign={Gtk.Align.CENTER}
xalign={0.5}
visibility={false}
placeholder_text="password"
onRealize={(self) => self.grab_focus()}
onActivate={(self) => {
self.sensitive = false;
AstalAuth.Pam.authenticate(self.text ?? '', (_, task) => {
try {
AstalAuth.Pam.authenticate_finish(task);
unlock();
}
catch (e) {
self.text = '';
label.label = (e as Error).message;
self.sensitive = true;
}
});
}}
/>
</box>
</box>
) :
<box />,
});
};
const createWindow = (monitor: Gdk.Monitor) => {
const hyprDesc = get_hyprland_monitor_desc(monitor);
const entryVisible = Vars.mainMonitor === hyprDesc || Vars.dupeLockscreen;
const win = PasswordPrompt(monitor, entryVisible);
windows.set(monitor, win);
};
const lock_screen = () => {
const display = Gdk.Display.get_default();
for (let m = 0; m < (display?.get_n_monitors() ?? 0); m++) {
const monitor = display?.get_monitor(m);
if (monitor) {
createWindow(monitor);
}
}
display?.connect('monitor-added', (_, monitor) => {
createWindow(monitor);
});
lock.lock_lock();
windows.forEach((win, monitor) => {
lock.new_surface(win, monitor);
win.show();
});
};
const on_finished = () => {
lock.destroy();
Gdk.Display.get_default()?.sync();
App.quit();
};
lock.connect('finished', on_finished);
if (Vars.hasFprintd) {
globalThis.authFinger = () => AstalAuth.Pam.authenticate('', (_, task) => {
try {
AstalAuth.Pam.authenticate_finish(task);
unlock();
}
catch (e) {
console.error((e as Error).message);
}
});
globalThis.authFinger();
}
lock_screen();
};

View file

@ -1,4 +0,0 @@
.lock-clock {
font-size: 80pt;
font-family: 'Ubuntu Mono';
}

View file

@ -1,112 +0,0 @@
import { App, Astal, Gtk, Widget } from 'astal/gtk3';
import { property, register } from 'astal/gobject';
import { Binding, idle } from 'astal';
import { get_hyprland_monitor, hyprMessage } from '../../lib';
/* Types */
type CloseType = 'none' | 'stay' | 'released' | 'clicked';
type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' |
'slide right' | 'popin' | 'fade';
type PopupCallback = (self?: Widget.Window) => void;
export type PopupWindowProps = Widget.WindowProps & {
transition?: HyprTransition | Binding<HyprTransition>
close_on_unfocus?: CloseType | Binding<CloseType>
on_open?: PopupCallback
on_close?: PopupCallback
};
@register()
export class PopupWindow extends Widget.Window {
@property(String)
declare transition: HyprTransition | Binding<HyprTransition>;
@property(String)
declare close_on_unfocus: CloseType | Binding<CloseType>;
on_open: PopupCallback;
on_close: PopupCallback;
constructor({
transition = 'slide top',
close_on_unfocus = 'released',
on_open = () => { /**/ },
on_close = () => { /**/ },
name,
visible = false,
layer = Astal.Layer.OVERLAY,
...rest
}: PopupWindowProps) {
super({
...rest,
name: `win-${name}`,
namespace: `win-${name}`,
visible: false,
layer,
setup: () => idle(() => {
// Add way to make window open on startup
if (visible) {
this.visible = true;
}
}),
});
App.add_window(this);
const setTransition = (_: PopupWindow, t: HyprTransition | Binding<HyprTransition>) => {
hyprMessage(`keyword layerrule animation ${t}, ${this.name}`).catch(console.log);
};
this.connect('notify::transition', setTransition);
this.close_on_unfocus = close_on_unfocus;
this.transition = transition;
this.on_open = on_open;
this.on_close = on_close;
this.connect('notify::visible', () => {
// Make sure we have the right animation
setTransition(this, this.transition);
if (this.visible) {
this.on_open(this);
}
else {
this.on_close(this);
}
});
};
set_x_pos(
alloc: Gtk.Allocation,
side = 'right' as 'left' | 'right',
) {
const monitor = this.gdkmonitor ??
this.get_display().get_monitor_at_point(alloc.x, alloc.y);
// @ts-expect-error this should exist
const transform = get_hyprland_monitor(monitor)?.transform;
let width: number;
if (transform && (transform === 1 || transform === 3)) {
width = monitor.get_geometry().height;
}
else {
width = monitor.get_geometry().width;
}
this.margin_right = side === 'right' ?
(width - alloc.x - alloc.width) :
this.margin_right;
this.margin_left = side === 'right' ?
this.margin_left :
(alloc.x - alloc.width);
}
}
export default PopupWindow;

View file

@ -1,14 +0,0 @@
import { Widget } from 'astal/gtk3';
export default ({
size,
vertical = false,
css = '',
...rest
}: { size: number } & Widget.BoxProps) => (
<box
css={`${vertical ? 'min-height' : 'min-width'}: ${size}px; ${css}`}
{...rest}
/>
);

View file

@ -1,61 +0,0 @@
import { bind } from 'astal';
import { Gtk, Widget } from 'astal/gtk3';
import { register, property } from 'astal/gobject';
type SmoothProgressProps = Widget.BoxProps & {
transition_duration?: string
};
// PERF: this is kinda laggy
@register()
class SmoothProgress extends Widget.Box {
@property(Number)
declare fraction: number;
@property(String)
declare transition_duration: string;
constructor({
transition_duration = '1s',
...rest
}: SmoothProgressProps = {}) {
super(rest);
this.transition_duration = transition_duration;
const background = (
<box
className="background"
hexpand
vexpand
halign={Gtk.Align.FILL}
valign={Gtk.Align.FILL}
/>
);
const progress = (
<box
className="progress"
vexpand
valign={Gtk.Align.FILL}
css={bind(this, 'fraction').as((fraction) => {
return `
transition: margin-right ${this.transition_duration} linear;
margin-right: ${
Math.abs(fraction - 1) * background.get_allocated_width()
}px;
`;
})}
/>
);
this.add((
<overlay overlay={progress}>
{background}
</overlay>
));
this.show_all();
}
}
export default SmoothProgress;

View file

@ -1,193 +0,0 @@
// This is definitely not good practice but I couldn't figure out how to extend PopupWindow
// so here we are with a cursed function that returns a prop of the class.
import { Astal, Gtk, Widget } from 'astal/gtk3';
import { idle } from 'astal';
import { AsyncFzf, FzfOptions, FzfResultItem } from 'fzf';
import PopupWindow from '../misc/popup-window';
import { centerCursor } from '../../lib';
export interface SortedListProps<T> {
create_list: () => T[] | Promise<T[]>
create_row: (item: T) => Gtk.Widget
fzf_options?: FzfOptions<T>
compare_props?: (keyof T)[]
on_row_activated: (row: Gtk.ListBoxRow) => void
sort_func: (
a: Gtk.ListBoxRow,
b: Gtk.ListBoxRow,
entry: Widget.Entry,
fzf: FzfResultItem<T>[],
) => number
name: string
};
export class SortedList<T> {
private item_list: T[] = [];
private fzf_results: FzfResultItem<T>[] = [];
readonly window: PopupWindow;
private _item_map = new Map<T, Gtk.Widget>();
readonly create_list: () => T[] | Promise<T[]>;
readonly create_row: (item: T) => Gtk.Widget;
readonly fzf_options: FzfOptions<T> | undefined;
readonly compare_props: (keyof T)[] | undefined;
readonly on_row_activated: (row: Gtk.ListBoxRow) => void;
readonly sort_func: (
a: Gtk.ListBoxRow,
b: Gtk.ListBoxRow,
entry: Widget.Entry,
fzf: FzfResultItem<T>[],
) => number;
constructor({
create_list,
create_row,
fzf_options,
compare_props,
on_row_activated,
sort_func,
name,
}: SortedListProps<T>) {
const list = new Gtk.ListBox({
selectionMode: Gtk.SelectionMode.SINGLE,
});
list.connect('row-activated', (_, row) => {
this.on_row_activated(row);
});
const placeholder = (
<revealer>
<label
label=" Couldn't find a match"
className="placeholder"
/>
</revealer>
) as Widget.Revealer;
const on_text_change = (text: string) => {
// @ts-expect-error this should be okay
(new AsyncFzf(this.item_list, this.fzf_options)).find(text)
.then((out) => {
this.fzf_results = out;
list.invalidate_sort();
const visibleApplications = list.get_children().filter((row) => row.visible).length;
placeholder.reveal_child = visibleApplications <= 0;
})
.catch(() => { /**/ });
};
const entry = (
<entry
onChanged={(self) => on_text_change(self.text)}
hexpand
/>
) as Widget.Entry;
list.set_sort_func((a, b) => {
return this.sort_func(a, b, entry, this.fzf_results);
});
const refreshItems = () => idle(async() => {
// Delete items that don't exist anymore
const new_list = await this.create_list();
for (const [item, widget] of this._item_map) {
if (!new_list.some((child) =>
this.compare_props?.every((prop) => child[prop] === item[prop]) ?? child === item)) {
widget.destroy();
}
}
// Add missing items
for (const item of new_list) {
if (!this.item_list.some((child) =>
this.compare_props?.every((prop) => child[prop] === item[prop]) ?? child === item)) {
const _item = this.create_row(item);
list.add(_item);
}
}
this.item_list = new_list;
list.show_all();
on_text_change('');
});
this.window = (
<PopupWindow
name={name}
keymode={Astal.Keymode.ON_DEMAND}
on_open={() => {
entry.text = '';
centerCursor();
entry.grab_focus();
}}
>
<box
vertical
className={`${name} sorted-list`}
>
<box className="widget search">
<icon icon="preferences-system-search-symbolic" />
{entry}
<button
css="margin-left: 5px;"
cursor="pointer"
onButtonReleaseEvent={refreshItems}
>
<icon icon="view-refresh-symbolic" css="font-size: 26px;" />
</button>
</box>
<eventbox cursor="pointer">
<scrollable
className="widget list"
css="min-height: 600px; min-width: 700px;"
hscroll={Gtk.PolicyType.NEVER}
vscroll={Gtk.PolicyType.AUTOMATIC}
>
<box vertical>
{list}
{placeholder}
</box>
</scrollable>
</eventbox>
</box>
</PopupWindow>
) as PopupWindow;
this.create_list = create_list;
this.create_row = create_row;
this.fzf_options = fzf_options;
this.compare_props = compare_props;
this.on_row_activated = on_row_activated;
this.sort_func = sort_func;
refreshItems();
}
};
/**
* @param props props for a SortedList Widget
* @returns the widget
*/
export default function<Attr>(props: SortedListProps<Attr>) {
return (new SortedList(props)).window;
}

View file

@ -1,27 +0,0 @@
.sorted-list {
.search {
icon {
font-size: 20px;
min-width: 40px;
min-height: 40px
}
entry {}
}
.list {
row {
border-radius: 10px;
&:hover, &:selected {
icon {
-gtk-icon-shadow: 2px 2px $accent_color;
}
}
}
.placeholder {
font-size: 20px;
}
}
}

View file

@ -1,145 +0,0 @@
import { bind, timeout } from 'astal';
import { App, Gtk, Widget } from 'astal/gtk3';
import AstalNotifd from 'gi://AstalNotifd';
const Notifications = AstalNotifd.get_default();
import { Notification, HasNotifs } from './notification';
import NotifGestureWrapper from './gesture';
const addNotif = (box: Widget.Box, notifObj: AstalNotifd.Notification) => {
if (notifObj) {
const NewNotif = Notification({
id: notifObj.id,
slide_in_from: 'Right',
});
if (NewNotif) {
box.pack_end(NewNotif, false, false, 0);
box.show_all();
}
}
};
const NotificationList = () => (
<box
vertical
vexpand
valign={Gtk.Align.START}
visible={bind(HasNotifs)}
// It needs to be bigger than the notifs to not jiggle
css="min-width: 550px;"
setup={(self) => {
Notifications.get_notifications().forEach((n) => {
addNotif(self, n);
});
self
.hook(Notifications, 'notified', (_, id) => {
if (id) {
const notifObj = Notifications.get_notification(id);
if (notifObj) {
addNotif(self, notifObj);
}
}
})
.hook(Notifications, 'resolved', (_, id) => {
const notif = (self.get_children() as NotifGestureWrapper[])
.find((ch) => ch.id === id);
if (notif?.sensitive) {
notif.slideAway('Right');
}
});
}}
/>
);
const ClearButton = () => (
<eventbox
cursor={bind(HasNotifs).as((hasNotifs) => hasNotifs ? 'pointer' : 'not-allowed')}
>
<button
className="clear"
sensitive={bind(HasNotifs)}
onButtonReleaseEvent={() => {
Notifications.get_notifications().forEach((notif) => {
notif.dismiss();
});
timeout(1000, () => {
App.get_window('win-notif-center')?.set_visible(false);
});
}}
>
<box>
<label label="Clear " />
<icon icon={bind(Notifications, 'notifications')
.as((notifs) => notifs.length > 0 ?
'user-trash-full-symbolic' :
'user-trash-symbolic')}
/>
</box>
</button>
</eventbox>
);
const Header = () => (
<box className="header">
<label
label="Notifications"
hexpand
xalign={0}
/>
<ClearButton />
</box>
);
const Placeholder = () => (
<revealer
transitionType={Gtk.RevealerTransitionType.CROSSFADE}
revealChild={bind(HasNotifs).as((v) => !v)}
>
<box
className="placeholder"
vertical
valign={Gtk.Align.CENTER}
halign={Gtk.Align.CENTER}
vexpand
hexpand
>
<icon icon="notification-disabled-symbolic" />
<label label="Your inbox is empty" />
</box>
</revealer>
);
export default () => (
<box
className="notification-center widget"
vertical
>
<Header />
<box className="notification-wallpaper-box">
<scrollable
className="notification-list-box"
hscroll={Gtk.PolicyType.NEVER}
vscroll={Gtk.PolicyType.AUTOMATIC}
>
<box
className="notification-list"
vertical
>
<NotificationList />
<Placeholder />
</box>
</scrollable>
</box>
</box>
);

View file

@ -1,348 +0,0 @@
import { Gdk, Gtk, Widget } from 'astal/gtk3';
import { property, register } from 'astal/gobject';
import { idle, interval, timeout } from 'astal';
import AstalIO from 'gi://AstalIO';
import AstalNotifd from 'gi://AstalNotifd';
const Notifications = AstalNotifd.get_default();
import { hyprMessage } from '../../lib';
import { HasNotifs } from './notification';
import { get_hyprland_monitor } from '../../lib';
/* Types */
import { CursorPos, LayerResult } from '../../lib';
const display = Gdk.Display.get_default();
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 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} opacity: 0;`;
const slideRight = `${TRANSITION} ${MAX_RIGHT} opacity: 0;`;
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
type NotifGestureWrapperProps = Widget.BoxProps & {
id: number
slide_in_from?: 'Left' | 'Right'
popup_timer?: number
setup_notif?: (self: NotifGestureWrapper) => void
};
@register()
export class NotifGestureWrapper extends Widget.EventBox {
public static popups = new Map<number, NotifGestureWrapper>();
public static sliding_in = 0;
public static on_sliding_in: (amount: number) => void;
readonly id: number;
readonly slide_in_from: 'Left' | 'Right';
readonly is_popup: boolean;
private timer_object: AstalIO.Time | undefined;
@property(Number)
declare popup_timer: number;
@property(Boolean)
declare dragging: boolean;
private _sliding_away = false;
private async get_hovered(): Promise<boolean> {
const layers = JSON.parse(await hyprMessage('j/layers')) as LayerResult;
const cursorPos = JSON.parse(await hyprMessage('j/cursorpos')) as CursorPos;
const win = this.get_window();
if (!win) {
return false;
}
const monitor = display?.get_monitor_at_window(win);
if (!monitor) {
return false;
}
const plugName = get_hyprland_monitor(monitor)?.name;
const notifLayer = layers[plugName ?? '']?.levels['3']
?.find((n) => n.namespace === 'notifications');
if (!notifLayer) {
return false;
}
const index = [...NotifGestureWrapper.popups.keys()]
.sort((a, b) => b - a)
.indexOf(this.id);
const popups = [...NotifGestureWrapper.popups.entries()]
.sort((a, b) => b[0] - a[0])
.map(([key, val]) => [key, val.get_allocated_height()]);
const thisY = notifLayer.y + popups
.map((v) => v[1])
.slice(0, index)
.reduce((prev, curr) => prev + curr, 0);
if (cursorPos.y >= thisY && cursorPos.y <= thisY + (popups[index]?.at(1) ?? 0)) {
if (cursorPos.x >= notifLayer.x &&
cursorPos.x <= notifLayer.x + notifLayer.w) {
return true;
}
}
return false;
}
private setCursor(cursor: string) {
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
cursor,
));
}
public slideAway(side: 'Left' | 'Right'): void {
if (!this.sensitive || this._sliding_away) {
return;
}
// Make it uninteractable
this.sensitive = false;
this._sliding_away = true;
let rev = this.get_child() as Widget.Revealer | null;
if (!rev) {
return;
}
const revChild = rev.get_child() as Widget.Box | null;
if (!revChild) {
return;
}
revChild.css = side === 'Left' ? slideLeft : slideRight;
timeout(ANIM_DURATION - 100, () => {
rev = this.get_child() as Widget.Revealer | null;
if (!rev) {
return;
}
rev.revealChild = false;
timeout(ANIM_DURATION, () => {
// Kill notif if specified
if (!this.is_popup) {
Notifications.get_notification(this.id)?.dismiss();
// Update HasNotifs
HasNotifs.set(Notifications.get_notifications().length > 0);
}
else {
// Make sure we cleanup any references to this instance
NotifGestureWrapper.popups.delete(this.id);
}
// Get rid of disappeared widget
this.destroy();
});
});
}
constructor({
id,
slide_in_from = 'Left',
popup_timer = 0,
setup_notif = () => { /**/ },
...rest
}: NotifGestureWrapperProps) {
super({
on_button_press_event: () => {
this.setCursor('grabbing');
},
// OnRelease
on_button_release_event: () => {
this.setCursor('grab');
},
// OnHover
on_enter_notify_event: () => {
this.setCursor('grab');
},
// OnHoverLost
on_leave_notify_event: () => {
this.setCursor('grab');
},
onDestroy: () => {
this.timer_object?.cancel();
},
});
this.id = id;
this.slide_in_from = slide_in_from;
this.dragging = false;
this.popup_timer = popup_timer;
this.is_popup = this.popup_timer !== 0;
// Handle timeout before sliding away if it is a popup
if (this.popup_timer !== 0) {
this.timer_object = interval(1000, async() => {
try {
if (!(await this.get_hovered())) {
if (this.popup_timer === 0) {
this.slideAway('Left');
}
else {
--this.popup_timer;
}
}
}
catch (_e) {
this.timer_object?.cancel();
}
});
}
const gesture = Gtk.GestureDrag.new(this);
this.add(
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={500}
revealChild={false}
>
<box
{...rest}
setup={(self) => {
self
// When dragging
.hook(gesture, 'drag-update', () => {
let offset = gesture.get_offset()[1];
if (!offset || offset === 0) {
return;
}
// Slide right
if (offset > 0) {
self.css = `
opacity: 1; transition: none;
margin-left: ${offset}px;
margin-right: -${offset}px;
`;
}
// Slide left
else {
offset = Math.abs(offset);
self.css = `
opacity: 1; transition: none;
margin-right: ${offset}px;
margin-left: -${offset}px;
`;
}
// Put a threshold on if a click is actually dragging
this.dragging = Math.abs(offset) > SLIDE_MIN_THRESHOLD;
this.setCursor('grabbing');
})
// On drag end
.hook(gesture, 'drag-end', () => {
const offset = gesture.get_offset()[1];
if (!offset) {
return;
}
// If crosses threshold after letting go, slide away
if (Math.abs(offset) > MAX_OFFSET) {
this.slideAway(offset > 0 ? 'Right' : 'Left');
}
else {
self.css = defaultStyle;
this.dragging = false;
this.setCursor('grab');
}
});
if (this.is_popup) {
NotifGestureWrapper.on_sliding_in(++NotifGestureWrapper.sliding_in);
}
// Reverse of slideAway, so it started at squeeze, then we go to slide
self.css = this.slide_in_from === 'Left' ?
slideLeft :
slideRight;
idle(() => {
if (!Notifications.get_notification(id)) {
return;
}
const rev = self?.get_parent() as Widget.Revealer | null;
if (!rev) {
return;
}
rev.revealChild = true;
timeout(ANIM_DURATION, () => {
if (!Notifications.get_notification(id)) {
return;
}
// Then we go to center
self.css = defaultStyle;
if (this.is_popup) {
timeout(ANIM_DURATION, () => {
NotifGestureWrapper.on_sliding_in(
--NotifGestureWrapper.sliding_in,
);
});
}
});
});
}}
/>
</revealer>,
);
setup_notif(this);
}
}
export default NotifGestureWrapper;

View file

@ -1,27 +0,0 @@
import { Astal } from 'astal/gtk3';
import PopupWindow from '../misc/popup-window';
import Popups from './popups';
import Center from './center';
export const NotifPopups = () => (
<window
name="notifications"
namespace="notifications"
layer={Astal.Layer.OVERLAY}
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT}
>
<Popups />
</window>
);
export const NotifCenter = () => (
<PopupWindow
name="notif-center"
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT}
>
<Center />
</PopupWindow>
);

View file

@ -1,205 +0,0 @@
import { App, Gtk, Gdk, Widget } from 'astal/gtk3';
import { Variable } from 'astal';
import GLib from 'gi://GLib';
import AstalApps from 'gi://AstalApps';
const Applications = AstalApps.Apps.new();
import AstalNotifd from 'gi://AstalNotifd';
const Notifications = AstalNotifd.get_default();
import NotifGestureWrapper from './gesture';
// import SmoothProgress from '../misc/smooth-progress';
// Make a variable to connect to for Widgets
// to know when there are notifs or not
export const HasNotifs = Variable(false);
const setTime = (time: number): string => GLib.DateTime
.new_from_unix_local(time)
.format('%H:%M') ?? '';
const NotifIcon = ({ notifObj }: {
notifObj: AstalNotifd.Notification
}) => {
let icon: string;
if (notifObj.get_image() && notifObj.get_image() !== '') {
icon = notifObj.get_image();
App.add_icons(icon);
}
else if (notifObj.get_app_icon() !== '' && Widget.Icon.lookup_icon(notifObj.get_app_icon())) {
icon = notifObj.get_app_icon();
}
else {
icon = Applications.fuzzy_query(
notifObj.get_app_name(),
)[0].get_icon_name();
}
return (
<box
valign={Gtk.Align.CENTER}
className="icon"
css={`
min-width: 78px;
min-height: 78px;
`}
>
<icon
icon={icon}
css="font-size: 58px;"
halign={Gtk.Align.CENTER}
hexpand
valign={Gtk.Align.CENTER}
vexpand
/>
</box>
);
};
const setupButton = (self: Gtk.Widget) => {
const display = Gdk.Display.get_default();
// OnHover
self.connect('enter-notify-event', () => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
});
// OnHoverLost
self.connect('leave-notify-event', () => {
if (!display) {
return;
}
self.window.set_cursor(null);
});
};
const BlockedApps = [
'Spotify',
];
export const Notification = ({
id = 0,
popup_timer = 0,
slide_in_from = 'Left' as 'Left' | 'Right',
}): NotifGestureWrapper | undefined => {
const notifObj = Notifications.get_notification(id);
if (!notifObj) {
return;
}
if (BlockedApps.find((app) => app === notifObj.app_name)) {
notifObj.dismiss();
return;
}
HasNotifs.set(Notifications.get_notifications().length > 0);
// const progress = SmoothProgress({ className: 'smooth-progress' });
return (
<NotifGestureWrapper
id={id}
popup_timer={popup_timer}
slide_in_from={slide_in_from}
/* setup_notif={(self) => {
if (self.is_popup) {
self.connect('notify::popup-timer', () => {
progress.fraction = self.popup_timer / 5;
});
}
else {
progress.destroy();
}
}}*/
>
<box vertical className={`notification ${notifObj.urgency} widget`}>
{/* Content */}
<box>
<NotifIcon notifObj={notifObj} />
{/* Top of Content */}
<box vertical css="min-width: 400px">
<box>
{/* Title */}
<label
className="title"
halign={Gtk.Align.START}
valign={Gtk.Align.END}
xalign={0}
hexpand
max_width_chars={24}
truncate
wrap
label={notifObj.summary}
use_markup={notifObj.summary.startsWith('<')}
/>
{/* Time */}
<label
className="time"
valign={Gtk.Align.CENTER}
halign={Gtk.Align.END}
label={setTime(notifObj.time)}
/>
{/* Close button */}
<button
className="close-button"
valign={Gtk.Align.START}
halign={Gtk.Align.END}
setup={setupButton}
onButtonReleaseEvent={() => {
notifObj.dismiss();
}}
>
<icon icon="window-close-symbolic" />
</button>
</box>
{/* Description */}
<label
className="description"
hexpand
use_markup
xalign={0}
label={notifObj.body}
wrap
/>
</box>
</box>
{/* progress */}
{/* Actions */}
<box className="actions">
{notifObj.get_actions().map((action) => (
<button
className="action-button"
hexpand
setup={setupButton}
onButtonReleaseEvent={() => notifObj.invoke(action.id)}
>
<label label={action.label} />
</button>
))}
</box>
</box>
</NotifGestureWrapper>
) as NotifGestureWrapper;
};

View file

@ -1,65 +0,0 @@
import AstalNotifd from 'gi://AstalNotifd';
const Notifications = AstalNotifd.get_default();
import NotifGestureWrapper from './gesture';
import { Notification } from './notification';
export default () => (
<box
// Needed so it occupies space at the start
// It needs to be bigger than the notifs to not jiggle
css="min-width: 550px;"
vertical
setup={(self) => {
const notifQueue: number[] = [];
const addPopup = (id: number) => {
if (!id || !Notifications.get_notification(id)) {
return;
}
if (NotifGestureWrapper.sliding_in === 0) {
const NewNotif = Notification({ id, popup_timer: 5 });
if (NewNotif) {
// Use this instead of add to put it at the top
self.pack_end(NewNotif, false, false, 0);
self.show_all();
NotifGestureWrapper.popups.set(id, NewNotif);
}
}
else {
notifQueue.push(id);
}
};
NotifGestureWrapper.on_sliding_in = (n) => {
if (n === 0) {
const id = notifQueue.shift();
if (id) {
addPopup(id);
}
}
};
const handleResolved = (id: number) => {
const notif = NotifGestureWrapper.popups.get(id);
if (!notif) {
return;
}
notif.slideAway('Left');
NotifGestureWrapper.popups.delete(id);
};
self
.hook(Notifications, 'notified', (_, id) => addPopup(id))
.hook(Notifications, 'resolved', (_, id) => handleResolved(id));
}}
/>
);

View file

@ -1,130 +0,0 @@
.notification.widget {
// urgencies
// &.urgency ...
.icon {
margin-right: 10px;
}
.time {
margin: 3px;
}
.close-button {
margin: 3px;
}
.title {
font-weight: 800;
font-size: 20px;
margin-bottom: 5px;
}
.description {
margin-bottom: 5px;
}
.actions {
margin: 3px;
.action-button {}
}
.smooth-progress {
min-height: 7px;
margin: 3px;
.background {
background-color: darken($window_bg_color, 3%);
border-radius: 3px;
}
.progress {
background-color: $accent-color;
border-radius: 3px;
}
}
}
.notification-center {
margin-top: 0;
min-height: 700px;
min-width: 580px;
* {
font-size: 16px;
}
.header {
padding: 10px;
margin-bottom: 9px;
label {
font-size: 22px;
}
}
.notification-list-box {
padding: 0 12px;
.notification {
box-shadow: none;
}
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;
icon {
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

@ -1,182 +0,0 @@
import { bind, timeout } from 'astal';
import { register } from 'astal/gobject';
import { App, Astal, astalify, Gtk, Widget, type ConstructProps } from 'astal/gtk3';
import AstalWp from 'gi://AstalWp';
import PopupWindow from '../misc/popup-window';
import Brightness from '../../services/brightness';
/* Types */
declare global {
function popup_osd(osd: string): void;
}
@register()
class ProgressBar extends astalify(Gtk.ProgressBar) {
constructor(props: ConstructProps<
ProgressBar,
Gtk.ProgressBar.ConstructorProps
>) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
super(props as any);
}
}
const HIDE_DELAY = 2000;
const transition_duration = 300;
export default () => {
let n_showing = 0;
let stack: Widget.Stack | undefined;
const popup = (osd: string) => {
if (!stack) {
return;
}
++n_showing;
stack.shown = osd;
App.get_window('win-osd')?.set_visible(true);
timeout(HIDE_DELAY, () => {
--n_showing;
if (n_showing === 0) {
App.get_window('win-osd')?.set_visible(false);
}
});
};
globalThis.popup_osd = popup;
const speaker = AstalWp.get_default()?.audio.default_speaker;
const microphone = AstalWp.get_default()?.audio.default_microphone;
if (!speaker || !microphone) {
throw new Error('Could not find default audio devices.');
}
return (
<PopupWindow
name="osd"
anchor={Astal.WindowAnchor.BOTTOM}
exclusivity={Astal.Exclusivity.IGNORE}
close_on_unfocus="stay"
transition="slide bottom"
>
<stack
className="osd"
transitionDuration={transition_duration}
setup={(self) => {
timeout(1000, () => {
stack = self;
});
}}
>
<box
name="speaker"
css="margin-bottom: 80px;"
setup={(self) => {
self.hook(speaker, 'notify::mute', () => {
popup('speaker');
});
}}
>
<box className="osd-item widget">
<icon icon={bind(speaker, 'volumeIcon')} />
<ProgressBar
fraction={bind(speaker, 'volume')}
sensitive={bind(speaker, 'mute').as((v) => !v)}
valign={Gtk.Align.CENTER}
/>
</box>
</box>
<box
name="microphone"
css="margin-bottom: 80px;"
setup={(self) => {
self.hook(microphone, 'notify::mute', () => {
popup('microphone');
});
}}
>
<box className="osd-item widget">
<icon icon={bind(microphone, 'volumeIcon')} />
<ProgressBar
fraction={bind(microphone, 'volume')}
sensitive={bind(microphone, 'mute').as((v) => !v)}
valign={Gtk.Align.CENTER}
/>
</box>
</box>
<box
name="brightness"
css="margin-bottom: 80px;"
setup={(self) => {
self.hook(Brightness, 'notify::screen-icon', () => {
popup('brightness');
});
}}
>
<box className="osd-item widget">
<icon icon={bind(Brightness, 'screenIcon')} />
<ProgressBar
fraction={bind(Brightness, 'screen')}
valign={Gtk.Align.CENTER}
/>
</box>
</box>
<box
name="keyboard"
css="margin-bottom: 80px;"
setup={(self) => {
self.hook(Brightness, 'notify::kbd-level', () => {
popup('keyboard');
});
}}
>
<box className="osd-item widget">
<icon icon="keyboard-brightness-symbolic" />
<ProgressBar
fraction={bind(Brightness, 'kbdLevel').as((v) => v / 2)}
sensitive={bind(Brightness, 'kbdLevel').as((v) => v !== 0)}
valign={Gtk.Align.CENTER}
/>
</box>
</box>
<box
name="caps"
css="margin-bottom: 80px;"
setup={(self) => {
self.hook(Brightness, 'notify::caps-icon', () => {
popup('caps');
});
}}
>
<box className="osd-item widget">
<icon icon={bind(Brightness, 'capsIcon')} />
<label label="Caps Lock" />
</box>
</box>
</stack>
</PopupWindow>
);
};

View file

@ -1,21 +0,0 @@
.osd {
.osd-item {
padding: 12px 20px;
label {
min-width: 170px;
}
progressbar {
min-height: 6px;
min-width: 170px;
}
icon {
font-size: 2rem;
color: white;
margin-left: -0.4rem;
margin-right: 0.8rem;
}
}
}

View file

@ -1,46 +0,0 @@
import { execAsync } from 'astal';
import { Astal } from 'astal/gtk3';
import { hyprMessage } from '../../lib';
import PopupWindow from '../misc/popup-window';
const PowermenuWidget = () => (
<centerbox className="powermenu widget">
<button
className="shutdown button"
cursor="pointer"
onButtonReleaseEvent={() => execAsync(['systemctl', 'poweroff']).catch(print)}
>
<icon icon="system-shutdown-symbolic" />
</button>
<button
className="reboot button"
cursor="pointer"
onButtonReleaseEvent={() => execAsync(['systemctl', 'reboot']).catch(print)}
>
<icon icon="system-restart-symbolic" />
</button>
<button
className="logout button"
cursor="pointer"
onButtonReleaseEvent={() => hyprMessage('dispatch exit').catch(print)}
>
<icon icon="system-log-out-symbolic" />
</button>
</centerbox>
);
export default () => (
<PopupWindow
name="powermenu"
transition="slide bottom"
// To put it at the center of the screen
exclusivity={Astal.Exclusivity.IGNORE}
>
<PowermenuWidget />
</PopupWindow>
);

View file

@ -1,29 +0,0 @@
.powermenu {
font-size: 70px;
padding: 10px;
icon {
min-width: 130px;
min-height: 130px;
}
button {
margin: 5px 10px;
transition: all ease .2s;
&:hover,
&:active {
background-color: lighten($window_bg_color, 3%);
}
}
.shutdown {
color: $red_1;
}
.reboot {
color: $purple_1;
}
.logout {
color: $yellow_1;
}
}

View file

@ -1,163 +0,0 @@
import { bind, execAsync, Variable } from 'astal';
import { App, Gtk, Widget } from 'astal/gtk3';
import AstalApps from 'gi://AstalApps';
const Applications = AstalApps.Apps.new();
import AstalHyprland from 'gi://AstalHyprland';
const Hyprland = AstalHyprland.get_default();
import PopupWindow from '../misc/popup-window';
import Separator from '../misc/separator';
import { hyprMessage } from '../../lib';
const ICON_SEP = 6;
const takeScreenshot = (selector: string, delay = 1000): void => {
App.get_window('win-screenshot')?.set_visible(false);
setTimeout(() => {
execAsync([
'bash',
'-c',
`grim ${selector} - | satty -f - || true`,
]).catch(console.error);
}, delay);
};
export default () => {
const windowList = <box vertical /> as Widget.Box;
const updateWindows = async() => {
if (!App.get_window('win-screenshot')?.visible) {
return;
}
windowList.children = (JSON.parse(await hyprMessage('j/clients')) as AstalHyprland.Client[])
.filter((client) => client.workspace.id === Hyprland.get_focused_workspace().get_id())
.map((client) => (
<button
className="item-btn"
cursor="pointer"
onButtonReleaseEvent={() => {
takeScreenshot(`-w ${client.address}`);
}}
>
<box halign={Gtk.Align.CENTER}>
<icon icon={Applications.fuzzy_query(client.class)[0].iconName} />
<Separator size={ICON_SEP} />
<label
label={client.title}
truncate
max_width_chars={50}
/>
</box>
</button>
));
};
Hyprland.connect('notify::clients', updateWindows);
Hyprland.connect('notify::focused-workspace', updateWindows);
const Shown = Variable<string>('monitors');
const stack = (
<stack
shown={bind(Shown)}
transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
>
<scrollable name="monitors">
<box vertical>
{bind(Hyprland, 'monitors').as((monitors) => monitors.map((monitor) => (
<button
className="item-btn"
cursor="pointer"
onButtonReleaseEvent={() => {
takeScreenshot(`-o ${monitor.name}`);
}}
>
<label
label={`${monitor.name}: ${monitor.description}`}
truncate
maxWidthChars={50}
/>
</button>
)))}
</box>
</scrollable>
<scrollable name="windows">
{windowList}
</scrollable>
</stack>
) as Widget.Stack;
const StackButton = ({ label = '', iconName = '' }) => (
<button
cursor="pointer"
className={bind(Shown).as((shown) =>
`header-btn${shown === label ? ' active' : ''}`)}
onButtonReleaseEvent={() => {
Shown.set(label);
}}
>
<box halign={Gtk.Align.CENTER}>
<icon icon={iconName} />
<Separator size={ICON_SEP} />
{label}
</box>
</button>
) as Widget.Button;
const regionButton = (
<button
cursor="pointer"
className="header-btn"
onButtonReleaseEvent={() => {
takeScreenshot('-g "$(slurp)"', 0);
}}
>
<box halign={Gtk.Align.CENTER}>
<icon icon="tool-pencil-symbolic" />
<Separator size={ICON_SEP} />
region
</box>
</button>
) as Widget.Button;
return (
<PopupWindow
name="screenshot"
on_open={() => {
updateWindows();
}}
>
<box
className="screenshot widget"
vertical
>
<box
className="header"
homogeneous
>
<StackButton label="monitors" iconName="display-symbolic" />
<StackButton label="windows" iconName="window-symbolic" />
{regionButton}
</box>
{stack}
</box>
</PopupWindow>
);
};

View file

@ -1,25 +0,0 @@
.screenshot {
font-size: 30px;
.header {
.header-btn {
margin: 5px;
transition: background 400ms;
&.active {
background: $window_bg_color;
}
}
}
scrollable {
margin: 5px;
min-height: 400px;
box {
.item-btn {
margin: 3px;
}
}
}
}