parent
1fc5a48bf9
commit
f522c984c4
64 changed files with 182 additions and 50 deletions
nixosModules/ags
default.nix
v2
.envrc.gitignoreapp.tscolors.scssdefault.nixenv.d.tseslint.config.tslib.tspackage-lock.jsonpackage.json
services
style.scsswidgets
applauncher
bar
bg-fade
clipboard
corners
date
icon-browser
lockscreen
misc
notifs
osd
powermenu
screenshot
|
@ -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 = [
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
use flake "$FLAKE#node"
|
3
nixosModules/ags/v2/.gitignore
vendored
3
nixosModules/ags/v2/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
|||
@girs
|
||||
node_modules
|
||||
tsconfig.json
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
26
nixosModules/ags/v2/env.d.ts
vendored
26
nixosModules/ags/v2/env.d.ts
vendored
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
};
|
1742
nixosModules/ags/v2/package-lock.json
generated
1742
nixosModules/ags/v2/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
.applauncher {
|
||||
.app {
|
||||
margin: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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)));
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
.clipboard .list row box {
|
||||
margin: 20px;
|
||||
font-size: 16px;
|
||||
}
|
|
@ -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(),
|
||||
];
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
.icon-browser .list row box {
|
||||
margin: 20px;
|
||||
font-size: 16px;
|
||||
}
|
|
@ -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();
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
.lock-clock {
|
||||
font-size: 80pt;
|
||||
font-family: 'Ubuntu Mono';
|
||||
}
|
|
@ -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;
|
|
@ -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}
|
||||
/>
|
||||
);
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
||||
};
|
|
@ -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));
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue