Compare commits
123 Commits
Author | SHA1 | Date | |
---|---|---|---|
4fef18041f | |||
df3384c667 | |||
7a97d7425e | |||
73ca687358 | |||
88441958d9 | |||
e59af388b1 | |||
39da9176ad | |||
e4ee9c9dc8 | |||
feef8452f4 | |||
c3ff5a58d6 | |||
a005189aef | |||
e98f5c2bd7 | |||
e7f6be4291 | |||
4fa7dc3c1a | |||
2ccb49f24b | |||
ec04ebee66 | |||
2b2f1c2b66 | |||
21f7e53b2b | |||
5f09b7b915 | |||
688013a6b8 | |||
4e7c3ceee8 | |||
390874374e | |||
7f7a755e32 | |||
ac6b47449a | |||
ce5f530ed2 | |||
62789224a7 | |||
30a2b2a0ad | |||
710eeb7fb4 | |||
364b5cb0ef | |||
d9777df0fb | |||
dabb325436 | |||
08347196f2 | |||
3553d03b25 | |||
f4b58b08ba | |||
1a4610a2d6 | |||
8c4af4f817 | |||
4aa4bc57b9 | |||
11c961bfe7 | |||
090492ae51 | |||
b4b8d04e6a | |||
2fd32da1b2 | |||
86e963e408 | |||
678afa99d6 | |||
acca70d47f | |||
bec7474793 | |||
414d42072a | |||
1239f49cc7 | |||
a7e6584f78 | |||
d72c4fdf86 | |||
a8e0c903c4 | |||
69d9b510f1 | |||
f66c8639d2 | |||
519eea601c | |||
609f662eb5 | |||
651030fd75 | |||
212296a7d5 | |||
f85c5f1b10 | |||
590ccb5c9e | |||
c7064dbea0 | |||
afe25a322a | |||
02861efcae | |||
6c1c9ad8cf | |||
f2bdddb9b6 | |||
e19a1179d5 | |||
33fcdd72f7 | |||
bf7d61945b | |||
03d9d89102 | |||
3e5136fdbb | |||
99a7a59cf7 | |||
54e216b5ec | |||
19d59347b6 | |||
9f754b454b | |||
7f21fb3ee3 | |||
0b349fb038 | |||
db16e830d1 | |||
d120b7a49e | |||
722be458ec | |||
aba9e0f30a | |||
49446646f3 | |||
b5ad7e3034 | |||
10136ab9de | |||
e93e051094 | |||
69484fc302 | |||
8a2180e120 | |||
8b70f80e60 | |||
7380c75818 | |||
f4b259dd13 | |||
b2f1d8fd9e | |||
78e472beb8 | |||
a9c7b7d7ee | |||
196d553627 | |||
ed17459e2a | |||
d9fdf1ee6d | |||
4dc14cf4e3 | |||
c70a3ffdb4 | |||
3bb9c6e661 | |||
dda261ac3c | |||
791f10aad4 | |||
666a047814 | |||
c0e9062230 | |||
c9442acce8 | |||
527f2012de | |||
c2f38bc39a | |||
bab328c2d3 | |||
c53069f0df | |||
7c15b0b320 | |||
3d74676f6d | |||
994d1ac1b4 | |||
aea9229c54 | |||
49595c3811 | |||
4a769d9eb0 | |||
840691ec67 | |||
d387d02534 | |||
7ff986e7a6 | |||
709af40296 | |||
3060c2b06e | |||
1f9feeb85f | |||
9d0574ecab | |||
e483d7de23 | |||
7fe86701b3 | |||
ca3d9c32e9 | |||
c5b1f64376 | |||
847ecc8ef7 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
package-lock.json
|
45
README.md
45
README.md
@ -1,10 +1,39 @@
|
|||||||
# my personal dotfiles for Hyprland and others
|
<div id="title" align="center">
|
||||||
|
<img src="https://static.janishutz.com/logo.jpg" width="300">
|
||||||
|
<h1>janishutz Hyprland</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
Contains the following config files:
|
Collection of dotfiles for my personal Hyprland setup, running on Arch Linux. Includes a setup and install script (that one is not complete yet though). For my neovim config, see [here](https://git.janishutz.com/janishutz/nvim)
|
||||||
|
|
||||||
- waybar conifg (including CSS styling)
|
## Features
|
||||||
- hyprland config (hyprland.conf)
|
- Astal4 based Status Bar and Quick Actions menu
|
||||||
- dunst config
|
- System info
|
||||||
- fish config
|
- Hyprland info
|
||||||
- rofi config
|
- Date & Time
|
||||||
- vscodium settings
|
- Bluetooth picker
|
||||||
|
- some networking settings (more coming later)
|
||||||
|
- Audio and brightness control
|
||||||
|
- battery monitoring
|
||||||
|
- Logout, Reboot, Shutdown, etc
|
||||||
|
- Rofi config for App launcher
|
||||||
|
- Wlogout config
|
||||||
|
- Theming script that generates a GTK theme and theming for bar, Hyprland, etc
|
||||||
|
- Fish config (with some handy aliases, based on one from ohh-my-fish)
|
||||||
|
- Fastfetch config
|
||||||
|
- kitty config with cursor trail
|
||||||
|
- Linter configs (currently only eslint, which is not complete yet)
|
||||||
|
- mpv config
|
||||||
|
- zathura configs
|
||||||
|
- yazi configs with links to various directories I use commonly plus a few plugins and themes that are applied by the script
|
||||||
|
- Astal3 based Notifications (due to be migrated to Astal4)
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
Clone your repo to any folder and adapt what you need. Some folders are still hard-coded, which I will be changing later on, so you will likely run into issues.
|
||||||
|
|
||||||
|
You may then run the `setup` script. That won't install all dependencies though. The `install` script is what serves that purpose, but that is not complete yet
|
||||||
|
|
||||||
|
|
||||||
|
## Setting up to develop
|
||||||
|
Clone this repo locally. For `config/astal` and `config/ags`, you will want to run `ags types -d .` in every directory where there is a `app.ts` file as well as `mkdir node_modules && cd node_modules && ln -sf /usr/share/astal/gjs/ ./astal` to prepare for development.
|
||||||
|
|
||||||
|
The `config/ags` directory contains gtk3 config for astal, whereas in `config/astal`, gtk4 configs can be found. All modules but for the notifications are written in Gtk 4.
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
docker pull ghcr.io/open-webui/open-webui:latest
|
|
||||||
docker container rm open-webui
|
|
||||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v /home/janis/projects/otherProjects/open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:latest
|
|
24
ai.sh
24
ai.sh
@ -1,24 +0,0 @@
|
|||||||
echo "
|
|
||||||
▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄
|
|
||||||
█ █ █ █ █ █ █ █▄█ █ █
|
|
||||||
█ ▄ █ █ █ █ █ ▄ █ █ ▄ █
|
|
||||||
█ █ █ █ █ █ █ █ █▄█ █ █ █▄█ █
|
|
||||||
█ █▄█ █ █▄▄▄█ █▄▄▄█ █ █ █
|
|
||||||
█ █ █ █ ▄ █ ██▄██ █ ▄ █
|
|
||||||
█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄█ █▄▄█▄█ █▄█▄█ █▄▄█
|
|
||||||
|
|
||||||
==> Starting ollama
|
|
||||||
"
|
|
||||||
|
|
||||||
cmd="ollama serve";
|
|
||||||
eval "${cmd}" &>/dev/null & disown;
|
|
||||||
|
|
||||||
echo "==> Starting docker"
|
|
||||||
systemctl start docker
|
|
||||||
|
|
||||||
echo "==> Starting open-webui."
|
|
||||||
docker start -i open-webui
|
|
||||||
|
|
||||||
echo "==> Done. Visit http://localhost:3000 for a web-ui, http://localhost:11434 to check ollama status"
|
|
||||||
|
|
||||||
read "Press enter to finish"
|
|
58
build/build.js
Normal file
58
build/build.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Using commonjs instead of ejs, because more widely compatible
|
||||||
|
const inquirer = require( 'inquirer' );
|
||||||
|
const fs = require( 'fs' );
|
||||||
|
const path = require( 'path' );
|
||||||
|
const os = require( 'os' );
|
||||||
|
const render = require( './helpers/render' );
|
||||||
|
const { treeWalker } = require('./helpers/util');
|
||||||
|
|
||||||
|
|
||||||
|
// Prompt user to select a wallpaper (if no path is passed as argument)
|
||||||
|
const wallpapers = treeWalker( path.join( os.homedir(), '/NextCloud/Wallpapers' ), '*' );
|
||||||
|
// const wallpapers = fs.readdirSync( path.join( os.homedir(), '/NextCloud/Wallpapers' ) );
|
||||||
|
const wallpaperChoices = [];
|
||||||
|
wallpapers.forEach(element => {
|
||||||
|
const name = element.split( '/' );
|
||||||
|
wallpaperChoices.push( { 'name': name[ name.length - 1 ].split( '.' )[ 0 ], 'value': element } );
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Selection options
|
||||||
|
const chooseWallpaper = {
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'wallpaper',
|
||||||
|
'message': 'Choose the wallpaper to be used',
|
||||||
|
'choices': wallpaperChoices,
|
||||||
|
};
|
||||||
|
|
||||||
|
const chooseLockpaper = {
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'lockpaper',
|
||||||
|
'message': 'Choose the lockscreen wallpaper to be used',
|
||||||
|
'choices': wallpaperChoices,
|
||||||
|
};
|
||||||
|
|
||||||
|
const chooseTheme = {
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'theme',
|
||||||
|
'message': 'Choose the general colourway to be used',
|
||||||
|
'choices': [
|
||||||
|
{ name: 'Nordic', value: 'nordic' },
|
||||||
|
{ name: 'Deep-Dark', value: 'deep-dark' },
|
||||||
|
{ name: 'Material-You', value: 'material' },
|
||||||
|
{ name: 'Light', value: 'light' },
|
||||||
|
{ name: 'Bright', value: 'bright' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add argument parsing
|
||||||
|
const args = process.argv.slice( 2 );
|
||||||
|
inquirer.default.prompt( [
|
||||||
|
chooseWallpaper,
|
||||||
|
chooseLockpaper,
|
||||||
|
chooseTheme
|
||||||
|
] ).then( answers => {
|
||||||
|
render( answers.wallpaper, answers.lockpaper, answers.theme );
|
||||||
|
} ).catch( () => {
|
||||||
|
process.exit( 1 );
|
||||||
|
} );
|
280
build/helpers/generateTheme.js
Normal file
280
build/helpers/generateTheme.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
const util = require( './util' );
|
||||||
|
|
||||||
|
const renderColourAsRGB = util.renderColourAsRGB;
|
||||||
|
const renderColourAsRGBA = util.renderColourAsRGBA;
|
||||||
|
const renderColourAsHex = util.renderColourAsHex;
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// ╭───────────────────────────────────────────────╮
|
||||||
|
// │ Theme generator (returns theme as object) │
|
||||||
|
// ╰───────────────────────────────────────────────╯
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
module.exports.generateTheme = ( theme, wallpaper, lockpaper, palette ) => {
|
||||||
|
return {
|
||||||
|
'wallpaper-path': wallpaper,
|
||||||
|
'lockpaper-path': lockpaper,
|
||||||
|
|
||||||
|
// ┌ ┐
|
||||||
|
// │ Colours │
|
||||||
|
// └ ┘
|
||||||
|
// ── Foreground ───────────────────────────────────────────────────
|
||||||
|
'colour-foreground-hex': renderColourAsHex( colours.foreground[ theme ] ),
|
||||||
|
'colour-foreground-rgb': renderColourAsRGB( colours.foreground[ theme ] ),
|
||||||
|
'colour-foreground-rgba': renderColourAsRGBA( colours.foreground[ theme ], 0.5 ),
|
||||||
|
'colour-foreground-rgba-07': renderColourAsRGBA( colours.foreground[ theme ], 0.7 ),
|
||||||
|
'colour-foreground-rgba-06': renderColourAsRGBA( colours.foreground[ theme ], 0.6 ),
|
||||||
|
'colour-foreground-rgba-05': renderColourAsRGBA( colours.foreground[ theme ], 0.5 ),
|
||||||
|
'colour-foreground-rgba-03': renderColourAsRGBA( colours.foreground[ theme ], 0.3 ),
|
||||||
|
'colour-foreground-rgba-02': renderColourAsRGBA( colours.foreground[ theme ], 0.2 ),
|
||||||
|
'colour-foreground-rgba-01': renderColourAsRGBA( colours.foreground[ theme ], 0.1 ),
|
||||||
|
|
||||||
|
// ── Accent foreground colour ─────────────────────────────────────
|
||||||
|
'colour-foreground-accent-hex': renderColourAsHex( colours['foreground-accent'][ theme ] ),
|
||||||
|
'colour-foreground-accent-rgba-07': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.7 ),
|
||||||
|
'colour-foreground-accent-rgba-06': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.6 ),
|
||||||
|
'colour-foreground-accent-rgba-05': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.5 ),
|
||||||
|
'colour-foreground-accent-rgba-03': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.3 ),
|
||||||
|
'colour-foreground-accent-rgba-02': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.2 ),
|
||||||
|
|
||||||
|
// ── Accent colour ────────────────────────────────────────────────
|
||||||
|
'colour-accent-hex': renderColourAsHex( palette[ 0 ] ),
|
||||||
|
'colour-accent-rgb': renderColourAsRGB( palette[ 0 ] ),
|
||||||
|
'colour-accent-rgba': renderColourAsRGBA( palette[ 0 ], 0.3 ),
|
||||||
|
'colour-accent-rgba-07': renderColourAsRGBA( palette[ 0 ], 0.7 ),
|
||||||
|
'colour-accent-rgba-05': renderColourAsRGBA( palette[ 0 ], 0.5 ),
|
||||||
|
'colour-accent-rgba-03': renderColourAsRGBA( palette[ 0 ], 0.3 ),
|
||||||
|
'colour-accent-rgba-02': renderColourAsRGBA( palette[ 0 ], 0.2 ),
|
||||||
|
'colour-accent-rgba-015': renderColourAsRGBA( palette[ 0 ], 0.15 ),
|
||||||
|
'colour-accent-rgba-011': renderColourAsRGBA( palette[ 0 ], 0.11 ),
|
||||||
|
'colour-accent-rgba-007': renderColourAsRGBA( palette[ 0 ], 0.07 ),
|
||||||
|
'colour-accent-hyprland': util.renderColourAsRGBAHex( palette[ 0 ], 0.8 ),
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
'colour-accent-gradient-1-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 1, gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-2-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 2, gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-3-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 3, gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-4-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 4, gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-5-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 5, gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-inverse-1-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 1, 1 / gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-inverse-2-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 1, 1 / gradientMultipliers[ theme ] ) ),
|
||||||
|
'colour-accent-gradient-inverse-3-hex': renderColourAsHex( util.getGradientColour( palette[ 0 ], 1, 1 / gradientMultipliers[ theme ] ) ),
|
||||||
|
|
||||||
|
// ── Secondary accent ─────────────────────────────────────────────
|
||||||
|
'colour-accent-2-hex': renderColourAsHex( palette[ 1 ] ),
|
||||||
|
'colour-accent-2-rgb': renderColourAsRGB( palette[ 1 ] ),
|
||||||
|
'colour-accent-2-rgba-07': renderColourAsRGBA( palette[ 1 ], 0.7 ),
|
||||||
|
'colour-accent-2-rgba-05': renderColourAsRGBA( palette[ 1 ], 0.5 ),
|
||||||
|
'colour-accent-2-rgba-03': renderColourAsRGBA( palette[ 1 ], 0.3 ),
|
||||||
|
'colour-accent-2-rgba-02': renderColourAsRGBA( palette[ 1 ], 0.2 ),
|
||||||
|
'colour-accent-2-rgba-015': renderColourAsRGBA( palette[ 1 ], 0.15 ),
|
||||||
|
'colour-accent-2-rgba-01': renderColourAsRGBA( palette[ 1 ], 0.1 ),
|
||||||
|
'colour-accent-2-hyprland': util.renderColourAsRGBAHex( palette[ 1 ], 0.8 ),
|
||||||
|
|
||||||
|
// ── Tertiary accent ──────────────────────────────────────────────
|
||||||
|
'colour-accent-3-hex': renderColourAsHex( palette[ 2 ] ),
|
||||||
|
'colour-accent-3-rgb': renderColourAsRGB( palette[ 2 ] ),
|
||||||
|
'colour-accent-3-hyprland': util.renderColourAsRGBAHex( palette[ 2 ], 0.8 ),
|
||||||
|
|
||||||
|
// ── Background ───────────────────────────────────────────────────
|
||||||
|
'colour-background-hex': renderColourAsHex( colours.background[ theme ] ),
|
||||||
|
'colour-background-rgb': renderColourAsRGB( colours.background[ theme ] ),
|
||||||
|
'colour-background-rgba': renderColourAsRGBA( colours.background[ theme ], 0.5 ),
|
||||||
|
'colour-background-rgba-07': renderColourAsRGBA( colours.background[ theme ], 0.7 ),
|
||||||
|
'colour-background-rgba-05': renderColourAsRGBA( colours.background[ theme ], 0.5 ),
|
||||||
|
'colour-background-rgba-03': renderColourAsRGBA( colours.background[ theme ], 0.3 ),
|
||||||
|
'colour-background-rgba-02': renderColourAsRGBA( colours.background[ theme ], 0.2 ),
|
||||||
|
'colour-background-rgba-015': renderColourAsRGBA( colours.background[ theme ], 0.15 ),
|
||||||
|
'colour-background-rgba-011': renderColourAsRGBA( colours.background[ theme ], 0.11 ),
|
||||||
|
|
||||||
|
// ── Background Alternative ───────────────────────────────────────
|
||||||
|
'colour-background-alternative-hex': renderColourAsHex( colours[ 'background-alternative' ][ theme ] ),
|
||||||
|
'colour-background-alternative-rgb': renderColourAsRGB( colours[ 'background-alternative' ][ theme ] ),
|
||||||
|
'colour-background-alternative-rgba-07': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.7 ),
|
||||||
|
'colour-background-alternative-rgba-06': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.6 ),
|
||||||
|
'colour-background-alternative-rgba-05': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.5 ),
|
||||||
|
'colour-background-alternative-rgba-04': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.4 ),
|
||||||
|
'colour-background-alternative-rgba-03': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.3 ),
|
||||||
|
'colour-background-alternative-rgba-02': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.2 ),
|
||||||
|
'colour-background-alternative-rgba-015': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.15 ),
|
||||||
|
'colour-background-alternative-rgba-01': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.1 ),
|
||||||
|
|
||||||
|
// ── Background Tertiary ──────────────────────────────────────────
|
||||||
|
'colour-background-tertiary-hex': renderColourAsHex( colours[ 'background-tertiary' ][ theme ] ),
|
||||||
|
'colour-background-tertiary-rgb': renderColourAsRGB( colours[ 'background-tertiary' ][ theme ] ),
|
||||||
|
'colour-background-tertiary-rgba-05': renderColourAsRGBA( colours[ 'background-tertiary' ][ theme ], 0.5 ),
|
||||||
|
'colour-background-tertiary-rgba-02': renderColourAsRGBA( colours[ 'background-tertiary' ][ theme ], 0.2 ),
|
||||||
|
|
||||||
|
// ── Shadow ───────────────────────────────────────────────────────
|
||||||
|
'colour-shadow-hex': renderColourAsHex( colours.shadow[ theme ] ),
|
||||||
|
'colour-shadow-rgb': renderColourAsRGB( colours.shadow[ theme ] ),
|
||||||
|
'colour-shadow-hyprland': util.renderColourAsRGBHex( colours.shadow[ theme ] ),
|
||||||
|
'colour-shadow-rgba-07': renderColourAsRGBA( colours.shadow[ theme ], 0.7 ),
|
||||||
|
'colour-shadow-rgba-05': renderColourAsRGBA( colours.shadow[ theme ], 0.5 ),
|
||||||
|
'colour-shadow-rgba-03': renderColourAsRGBA( colours.shadow[ theme ], 0.3 ),
|
||||||
|
'colour-shadow-rgba-02': renderColourAsRGBA( colours.shadow[ theme ], 0.2 ),
|
||||||
|
'colour-shadow-rgba-015': renderColourAsRGBA( colours.shadow[ theme ], 0.15 ),
|
||||||
|
'colour-shadow-rgba-011': renderColourAsRGBA( colours.shadow[ theme ], 0.11 ),
|
||||||
|
|
||||||
|
// ── Inactive ─────────────────────────────────────────────────────
|
||||||
|
'colour-inactive-hex': renderColourAsHex( colours.inactive[ theme ] ),
|
||||||
|
'colour-inactive-rgb': renderColourAsRGB( colours.inactive[ theme ] ),
|
||||||
|
'colour-inactive-rgba-07': renderColourAsRGBA( colours.inactive[ theme ], 0.7 ),
|
||||||
|
'colour-inactive-rgba-05': renderColourAsRGBA( colours.inactive[ theme ], 0.5 ),
|
||||||
|
'colour-inactive-rgba-03': renderColourAsRGBA( colours.inactive[ theme ], 0.3 ),
|
||||||
|
'colour-inactive-rgba-02': renderColourAsRGBA( colours.inactive[ theme ], 0.2 ),
|
||||||
|
'colour-inactive-rgba-015': renderColourAsRGBA( colours.inactive[ theme ], 0.15 ),
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
'colour-inactive-background-hex': renderColourAsHex( colours[ 'inactive-background' ][ theme ] ),
|
||||||
|
'colour-inactive-background-rgb': renderColourAsRGB( colours[ 'inactive-background' ][ theme ] ),
|
||||||
|
'colour-inactive-background-hyprland': util.renderColourAsRGBHex( colours[ 'inactive-background' ][ theme ] ),
|
||||||
|
'colour-inactive-background-rgba-07': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.7 ),
|
||||||
|
'colour-inactive-background-rgba-05': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.5 ),
|
||||||
|
'colour-inactive-background-rgba-04': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.4 ),
|
||||||
|
'colour-inactive-background-rgba-03': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.3 ),
|
||||||
|
'colour-inactive-background-rgba-02': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.2 ),
|
||||||
|
'colour-inactive-background-rgba-015': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.15 ),
|
||||||
|
'colour-inactive-background-rgba-01': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.1 ),
|
||||||
|
|
||||||
|
// ┌ ┐
|
||||||
|
// │ Fonts │
|
||||||
|
// └ ┘
|
||||||
|
'font-primary': fonts.primary[ theme ],
|
||||||
|
'font-accent': fonts.accent[ theme ],
|
||||||
|
'font-mono': fonts.mono[ theme ],
|
||||||
|
|
||||||
|
// ┌ ┐
|
||||||
|
// │ Icon Theme │
|
||||||
|
// └ ┘
|
||||||
|
'icon-theme': iconTheme[ theme ],
|
||||||
|
|
||||||
|
// ┌ ┐
|
||||||
|
// │ yazi theme │
|
||||||
|
// └ ┘
|
||||||
|
'yazi-theme': yaziThemes[ theme ],
|
||||||
|
|
||||||
|
// ┌ ┐
|
||||||
|
// │ Path to this repo on disk │
|
||||||
|
// └ ┘
|
||||||
|
'path-to-dotfiles': __dirname.slice(0, __dirname.length - 5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// ╭───────────────────────────────────────────────╮
|
||||||
|
// │ Theme definitions │
|
||||||
|
// ╰───────────────────────────────────────────────╯
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
const gradientMultipliers = {
|
||||||
|
'nordic': 0.9,
|
||||||
|
'deep-dark': 0.8,
|
||||||
|
'material': 0.85,
|
||||||
|
'light': 1.1,
|
||||||
|
'bright': 1.15,
|
||||||
|
'test': 0.75
|
||||||
|
}
|
||||||
|
const colours = {
|
||||||
|
foreground: {
|
||||||
|
'nordic': [ 200, 220, 255 ],
|
||||||
|
'deep-dark': [ 230, 230, 230 ],
|
||||||
|
'material': [ 255, 255, 255 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 40, 40, 40 ],
|
||||||
|
'bright': [ 0, 0, 0 ],
|
||||||
|
'test': [ 0, 0, 0 ],
|
||||||
|
},
|
||||||
|
'foreground-accent': {
|
||||||
|
'nordic': [ 255, 255, 255 ],
|
||||||
|
'deep-dark': [ 255, 255, 255 ],
|
||||||
|
'material': [ 200, 200, 200 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 0, 0, 0 ],
|
||||||
|
'bright': [ 50, 50, 50 ],
|
||||||
|
'test': [ 0, 0, 0 ],
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
'nordic': [ 10, 10, 15 ],
|
||||||
|
'deep-dark': [ 20, 20, 20 ],
|
||||||
|
'material': [ 30, 30, 30 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 230, 230, 230 ],
|
||||||
|
'bright': [ 255, 255, 255 ],
|
||||||
|
'test': [ 255, 255, 255 ],
|
||||||
|
},
|
||||||
|
'background-alternative': {
|
||||||
|
'nordic': [ 20, 20, 25 ],
|
||||||
|
'deep-dark': [ 30, 30, 30 ],
|
||||||
|
'material': [ 40, 40, 40 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 210, 210, 210 ],
|
||||||
|
'bright': [ 230, 230, 230 ],
|
||||||
|
'test': [ 255, 255, 0 ] // brown
|
||||||
|
},
|
||||||
|
'background-tertiary': {
|
||||||
|
'nordic': [ 0, 0, 0 ],
|
||||||
|
'deep-dark': [ 45, 45, 45 ],
|
||||||
|
'material': [ 0, 0, 0 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 180, 180, 180 ],
|
||||||
|
'bright': [ 200, 200, 200 ],
|
||||||
|
'test': [ 255, 0, 255 ] // purple
|
||||||
|
},
|
||||||
|
shadow: {
|
||||||
|
'nordic': [ 0, 0, 2 ],
|
||||||
|
'deep-dark': [ 40, 40, 40 ],
|
||||||
|
'material': [ 30, 30, 30 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 190, 190, 190 ],
|
||||||
|
'bright': [ 150, 150, 150 ],
|
||||||
|
'test': [ 120, 0, 0 ] // dark red
|
||||||
|
},
|
||||||
|
inactive: {
|
||||||
|
'nordic': [ 200, 200, 200 ],
|
||||||
|
'deep-dark': [ 200, 200, 200 ],
|
||||||
|
'material': [ 200, 200, 200 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 65, 65, 65 ],
|
||||||
|
'bright': [ 60, 60, 60 ],
|
||||||
|
'test': [ 150, 150, 150 ]
|
||||||
|
},
|
||||||
|
'inactive-background': {
|
||||||
|
'nordic': [ 0, 0, 0 ],
|
||||||
|
'deep-dark': [ 0, 0, 0 ],
|
||||||
|
'material': [ 255, 255, 255 ], // TODO: Will be calculated by material theme generator
|
||||||
|
'light': [ 80, 80, 80 ],
|
||||||
|
'bright': [ 60, 60, 60 ],
|
||||||
|
'test': [ 60, 60, 60 ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fonts = {
|
||||||
|
'primary': {
|
||||||
|
'nordic': 'Comfortaa',
|
||||||
|
'deep-dark': 'Comfortaa',
|
||||||
|
'material': 'Comfortaa',
|
||||||
|
'light': 'Adwaita Sans',
|
||||||
|
'bright': 'Adwaita Sans Extralight'
|
||||||
|
},
|
||||||
|
'accent': {
|
||||||
|
'nordic': 'Adwaita Sans',
|
||||||
|
'deep-dark': 'Adwaita Sans',
|
||||||
|
'material': 'Adwaita Sans',
|
||||||
|
'light': 'Cantarell',
|
||||||
|
'bright': 'Contarell Thin'
|
||||||
|
},
|
||||||
|
'mono': {
|
||||||
|
'nordic': 'Source Code Pro',
|
||||||
|
'deep-dark': 'Source Code Pro',
|
||||||
|
'material': 'Source Code Pro',
|
||||||
|
'light': 'Jetbrains Mono',
|
||||||
|
'bright': 'Jetbrains Mono',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconTheme = {
|
||||||
|
'nordic': 'Candy',
|
||||||
|
'deep-dark': 'Candy',
|
||||||
|
'material': 'Candy',
|
||||||
|
'light': 'Candy',
|
||||||
|
'bright': 'Candy'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yaziThemes = {
|
||||||
|
'nordic': 'tokyo-night',
|
||||||
|
'deep-dark': 'vscode-dark-modern',
|
||||||
|
'material': 'dracula',
|
||||||
|
'light': 'vscode-light-modern',
|
||||||
|
'bright': 'vscode-light-modern',
|
||||||
|
}
|
132
build/helpers/render.js
Normal file
132
build/helpers/render.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
const mustache = require( 'mustache' );
|
||||||
|
const colorThief = require( '@janishutz/colorthief' );
|
||||||
|
const fs = require( 'fs' );
|
||||||
|
const path = require( 'path' );
|
||||||
|
const util = require( './util' );
|
||||||
|
const generateTheme = require( './generateTheme' );
|
||||||
|
const chalk = require( 'chalk' ).default;
|
||||||
|
const inquirer = require( 'inquirer' ).default;
|
||||||
|
|
||||||
|
const build = ( wallpaper, lockpaper, theme ) => {
|
||||||
|
console.log( '\n=> Extracting colours' );
|
||||||
|
// Extract colour palette from chosen wallpaper using Color-Thief
|
||||||
|
colorThief.getPalette( wallpaper ).then( palette => {
|
||||||
|
palette = util.removeUselessColours( palette );
|
||||||
|
// Define view options (for rendering with mustache)
|
||||||
|
if ( theme === 'test' ) {
|
||||||
|
palette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log( 'The following colours will be used based on your wallpaper: ' );
|
||||||
|
let col = palette[ 0 ];
|
||||||
|
console.log( ' => Primary accent colour: ' + chalk.rgb( col[ 0 ], col[ 1 ], col[ 2 ] )( util.renderColourAsHex( col ) ) );
|
||||||
|
col = palette[ 1 ];
|
||||||
|
console.log( ' => Secondary accent colour: ' + chalk.rgb( col[ 0 ], col[ 1 ], col[ 2 ] )( util.renderColourAsHex( col ) ) );
|
||||||
|
col = palette[ 2 ];
|
||||||
|
console.log( ' => Tertiary accent colour: ' + chalk.rgb( col[ 0 ], col[ 1 ], col[ 2 ] )( util.renderColourAsHex( col ) ) );
|
||||||
|
|
||||||
|
inquirer.prompt( [{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirm-proceed-build',
|
||||||
|
message: 'Okay to proceed with these colours?'
|
||||||
|
} ] ).then( answer => {
|
||||||
|
if ( answer['confirm-proceed-build'] ) proceedWithBuild( wallpaper, lockpaper, theme, palette );
|
||||||
|
else {
|
||||||
|
// Have the user pick any other of the extracted colours instead
|
||||||
|
let counter = -1;
|
||||||
|
const colourOptions = palette.map( c => {
|
||||||
|
counter++;
|
||||||
|
return {
|
||||||
|
name: chalk.rgb( c[ 0 ], c[ 1 ], c[ 2 ] )( util.renderColourAsHex( c ) ),
|
||||||
|
value: counter
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
inquirer.prompt( [
|
||||||
|
{
|
||||||
|
'type': 'list',
|
||||||
|
'message': 'Pick the primary accent colour',
|
||||||
|
'choices': colourOptions,
|
||||||
|
'name': 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'list',
|
||||||
|
'message': 'Pick the secondary accent colour',
|
||||||
|
'choices': colourOptions,
|
||||||
|
'name': 'secondary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'list',
|
||||||
|
'message': 'Pick the tertiary accent colour',
|
||||||
|
'choices': colourOptions,
|
||||||
|
'name': 'tertiary'
|
||||||
|
}
|
||||||
|
] ).then( result => {
|
||||||
|
const p = [ palette[ result.primary ], palette[ result.secondary ], palette[ result.tertiary ] ];
|
||||||
|
|
||||||
|
proceedWithBuild( wallpaper, lockpaper, theme, p );
|
||||||
|
} ).catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
process.exit( 1 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} ).catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
process.exit( 1 );
|
||||||
|
} );
|
||||||
|
} ).catch( e => {
|
||||||
|
console.error( e );
|
||||||
|
console.error( '\n=> Failed to load image or retrieve colour palette from it' );
|
||||||
|
process.exit( 1 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const proceedWithBuild = ( wallpaper, lockpaper, theme, palette ) => {
|
||||||
|
const view = generateTheme.generateTheme( theme, wallpaper, lockpaper, palette );
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdir( path.join( __dirname, '/dist' ) );
|
||||||
|
} catch ( e ) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively index files from config directory -> Maybe add a file to each
|
||||||
|
// directory to indicate whether or not to index files in it?
|
||||||
|
const fileList = util.treeWalker( path.join( __dirname, '/../../renderable/' ), '*', [ 'node_modules', '@girs', '.gitignore', '.git', 'flavours' ] );
|
||||||
|
|
||||||
|
for (let index = 0; index < fileList.length; index++) {
|
||||||
|
try {
|
||||||
|
render( fileList[ index ], view );
|
||||||
|
} catch ( e ) {
|
||||||
|
console.error( '=> Render failed for ' + fileList[ index ] + ' with error ' + e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util.themePreProcessor( path.join( __dirname, '/../../gtk-theme/src/gtk-4.0/gtk.css' ), 'src', 'dist' );
|
||||||
|
util.themePreProcessor( path.join( __dirname, '/../../gtk-theme/src/gtk-3.0/gtk.css' ), 'src', 'dist' );
|
||||||
|
render( path.join( __dirname, '/../../gtk-theme/src/colours.css' ), view, 'src', 'dist' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} templatePath - absolute path to config directory
|
||||||
|
* @param {object} view - rendering config passed to mustache
|
||||||
|
* @param {string} originalDir - The original directory, defaults to renderable
|
||||||
|
* @param {string} newDir - the output directory override, defaults to config
|
||||||
|
*/
|
||||||
|
const render = ( templatePath, view, originalDir = 'renderable', newDir = 'config' ) => {
|
||||||
|
// Load template from disk (all can be found in <project-root>/renderable)
|
||||||
|
// TODO: Make exclusion better plus copy other files maybe?
|
||||||
|
const template = '' + fs.readFileSync( templatePath );
|
||||||
|
const outPath = path.join( templatePath.replace( originalDir, newDir ) );
|
||||||
|
console.log( '=> Rendering to ' + outPath );
|
||||||
|
try {
|
||||||
|
fs.mkdirSync( path.dirname( outPath ), {
|
||||||
|
recursive: true,
|
||||||
|
} );
|
||||||
|
} catch ( e ) {
|
||||||
|
console.error( e );
|
||||||
|
}
|
||||||
|
fs.writeFileSync( outPath, mustache.render( template, view ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = build;
|
170
build/helpers/util.js
Normal file
170
build/helpers/util.js
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
const convert = require( 'color-convert' );
|
||||||
|
const fs = require( 'fs' );
|
||||||
|
const path = require( 'path' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find all files with extension in a directory
|
||||||
|
* @param {string} dir The directory to search. Either absolute or relative path
|
||||||
|
* @param {string} extension The file extension to look for
|
||||||
|
* @returns {string[]} returns a list of html files with their full path
|
||||||
|
*/
|
||||||
|
const treeWalker = ( dir, extension, ignoreList ) => {
|
||||||
|
const ls = fs.readdirSync( dir );
|
||||||
|
const fileList = [];
|
||||||
|
for ( let file in ls ) {
|
||||||
|
if ( fs.statSync( path.join( dir, ls[ file ] ) ).isDirectory() ) {
|
||||||
|
// Filter ignored directories
|
||||||
|
if ( ignoreList === undefined || !ignoreList.includes( ls[ file ] ) ) {
|
||||||
|
const newFiles = treeWalker( path.join( dir, ls[ file ] ), extension, ignoreList );
|
||||||
|
for (let file = 0; file < newFiles.length; file++) {
|
||||||
|
fileList.push( newFiles[ file ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( extension == '*' || ls[ file ].includes( extension ) ) {
|
||||||
|
if ( ignoreList === undefined || !ignoreList.includes( ls[ file ] ) ) {
|
||||||
|
fileList.push( path.join( dir, ls[ file ] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderColourAsHex = ( colour ) => {
|
||||||
|
return '#' + convert.default.rgb.hex( colour[ 0 ], colour[ 1 ], colour[ 2 ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderColourAsRGB = ( colour ) => {
|
||||||
|
return `rgb(${ colour[ 0 ] }, ${ colour[ 1 ] }, ${ colour[ 2 ] })`
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderColourAsRGBA = ( colour, ambiance ) => {
|
||||||
|
return `rgba(${ colour[ 0 ] }, ${ colour[ 1 ] }, ${ colour[ 2 ] }, ${ ambiance })`
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderColourAsRGBHex = ( colour ) => {
|
||||||
|
const hexCol = convert.default.rgb.hex( colour[ 0 ], colour[ 1 ], colour[ 2 ] );
|
||||||
|
return `rgb(${hexCol})`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decimalToHex(decimal) {
|
||||||
|
const hexValue = Math.round(decimal * 255);
|
||||||
|
return hexValue.toString(16).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderColourAsRGBAHex = ( colour, ambiance ) => {
|
||||||
|
const hexCol = convert.default.rgb.hex( colour[ 0 ], colour[ 1 ], colour[ 2 ] );
|
||||||
|
return `rgba(${hexCol}${decimalToHex(ambiance)})`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUselessColours = ( palette ) => {
|
||||||
|
const p = [];
|
||||||
|
for ( let i = 0; i < palette.length; i++ ) {
|
||||||
|
const luminance = calculateLuminance( palette[ i ] );
|
||||||
|
if ( luminance < 215 && luminance > 40 ) {
|
||||||
|
p.push( palette[ i ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateLuminance = ( colour ) => {
|
||||||
|
return colour[ 0 ] + colour[ 1 ] + colour[ 2 ] / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Replace the colours with variable names
|
||||||
|
*/
|
||||||
|
const replacements = {
|
||||||
|
'#0f1011': '@bg',
|
||||||
|
'rgba(9, 9, 10, 0.9)': '@bg_rgba_07',
|
||||||
|
'rgba(26, 28, 30, 0.3)': '@bg_rgba_05',
|
||||||
|
'#000': '@bg_accent',
|
||||||
|
'#000000': '@bg_accent',
|
||||||
|
'rgba(0, 0, 0, 0.7)': '@bg_accent_rgba_07',
|
||||||
|
'rgba(0, 0, 0, 0.6)': '@bg_accent_rgba_06',
|
||||||
|
'rgba(0, 0, 0, 0.5)': '@bg_accent_rgba_05',
|
||||||
|
'rgba(0, 0, 0, 0.4)': '@bg_accent_rgba_04',
|
||||||
|
'rgba(0, 0, 0, 0.3)': '@bg_accent_rgba_03',
|
||||||
|
'rgba(0, 0, 0, 0.12)': '@bg_accent_rgba_015',
|
||||||
|
'rgba(0, 0, 0, 0.08)': '@bg_accent_rgba_01',
|
||||||
|
'rgba(9, 9, 10, 0.9)': '@bg_inactive',
|
||||||
|
'#80868b': '@inactive',
|
||||||
|
'rgba(128, 134, 139, 0.7)': '@inactive_rgba_07',
|
||||||
|
'rgba(128, 134, 139, 0.5)': '@inactive_rgba_05',
|
||||||
|
'rgba(128, 134, 139, 0.3)': '@inactive_rgba_03',
|
||||||
|
'rgba(128, 134, 139, 0.2)': '@inactive_rgba_02',
|
||||||
|
// '#555A': '@shadow_rgba',
|
||||||
|
// '#555': '@shadow',
|
||||||
|
'#387db7': '@accent',
|
||||||
|
'rgba(56, 125, 183, 0.5)': '@accent_rgba_05',
|
||||||
|
'rgba(56, 125, 183, 0.32)': '@accent_rgba_03',
|
||||||
|
'rgba(56, 125, 183, 0.24)': '@accent_rgba_02',
|
||||||
|
'rgba(56, 125, 183, 0.16)': '@accent_rgba_015',
|
||||||
|
'rgba(56, 125, 183, 0.12)': '@accent_rgba_011',
|
||||||
|
'rgba(56, 125, 183, 0.08)': '@accent_rgba_007',
|
||||||
|
'#1a1a1b': '@accent_gradient_5',
|
||||||
|
'#1f1f21': '@accent_gradient_4',
|
||||||
|
'#1a2530': '@accent_gradient_3',
|
||||||
|
'#1c2c3b': '@accent_gradient_2',
|
||||||
|
'#1e3040': '@accent_gradient_1',
|
||||||
|
'#4887bd': '@accent_gradient_inverse_1',
|
||||||
|
'#508dc0': '@accent_gradient_inverse_2',
|
||||||
|
'#5892c3': '@accent_gradient_inverse_3',
|
||||||
|
'#673ab7': '@accent2',
|
||||||
|
'rgba(103, 58, 183, 0.12)': '@accent2_rgba_015',
|
||||||
|
'#fff': '@fg_accent',
|
||||||
|
'rgba(255, 255, 255, 0.7)': '@fg_accent_rgba_07',
|
||||||
|
'rgba(255, 255, 255, 0.6)': '@fg_accent_rgba_06',
|
||||||
|
'rgba(255, 255, 255, 0.5)': '@fg_accent_rgba_05',
|
||||||
|
'rgba(255, 255, 255, 0.3)': '@fg_accent_rgba_03',
|
||||||
|
'rgba(255, 255, 255, 0.2)': '@fg_accent_rgba_02',
|
||||||
|
'#9e9e9e': '@fg',
|
||||||
|
'rgba(158, 158, 158, 0.7)': '@fg_rgba_07',
|
||||||
|
'rgba(158, 158, 158, 0.6)': '@fg_rgba_06',
|
||||||
|
'rgba(158, 158, 158, 0.5)': '@fg_rgba_05',
|
||||||
|
'rgba(158, 158, 158, 0.3)': '@fg_rgba_03',
|
||||||
|
'rgba(158, 158, 158, 0.2)': '@fg_rgba_02',
|
||||||
|
'rgba(158, 158, 158, 0.1168)': '@fg_rgba_01'
|
||||||
|
};
|
||||||
|
const themePreProcessor = ( file, replacement, out ) => {
|
||||||
|
const colours = Object.keys( replacements );
|
||||||
|
let data = '' + fs.readFileSync( file );
|
||||||
|
for (let index = 0; index < colours.length; index++) {
|
||||||
|
const colour = colours[index];
|
||||||
|
data = data.replaceAll(colour, replacements[ colour ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outPath = file.replace( replacement, out );
|
||||||
|
try {
|
||||||
|
fs.mkdirSync( path.dirname( outPath ), {
|
||||||
|
recursive: true,
|
||||||
|
} );
|
||||||
|
} catch ( e ) {
|
||||||
|
console.error( e );
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync( outPath, data );
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGradientColour = ( colour, index, multiplier ) => {
|
||||||
|
if ( index === 0 ) {
|
||||||
|
return [ colour[ 0 ] * multiplier, colour[ 1 ] * multiplier, colour[ 2 ] * multiplier ];
|
||||||
|
}
|
||||||
|
const gradient = getGradientColour( colour, index - 1, multiplier );
|
||||||
|
return [ Math.min( 255, gradient[ 0 ] * multiplier ), Math.min( 255, gradient[ 1 ] * multiplier ), Math.min( 255, gradient[ 2 ] * multiplier ) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
treeWalker,
|
||||||
|
renderColourAsHex,
|
||||||
|
renderColourAsRGB,
|
||||||
|
renderColourAsRGBA,
|
||||||
|
renderColourAsRGBHex,
|
||||||
|
renderColourAsRGBAHex,
|
||||||
|
themePreProcessor,
|
||||||
|
getGradientColour,
|
||||||
|
removeUselessColours
|
||||||
|
}
|
23
build/package.json
Normal file
23
build/package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "janishutz-config-build",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Build janishutz's dotfiles configs",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.janishutz.com/janishutz/dotfiles"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"author": "janishutz",
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "build.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@janishutz/colorthief": "^3.0.2",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
|
"color-convert": "^3.0.1",
|
||||||
|
"inquirer": "^12.5.0",
|
||||||
|
"mustache": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
41
build/themalizer.js
Normal file
41
build/themalizer.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const fs = require( 'fs' );
|
||||||
|
const path = require( 'path' );
|
||||||
|
|
||||||
|
const data = '' + fs.readFileSync( '/usr/share/themes/Material-Black-Blueberry/gtk-4.0/gtk.css' );
|
||||||
|
|
||||||
|
let lineNumber = 1;
|
||||||
|
const indexer = {};
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const char = data[i];
|
||||||
|
|
||||||
|
if ( char === '\n' ) {
|
||||||
|
lineNumber++;
|
||||||
|
} else if ( char === '#' ) {
|
||||||
|
const extract = data.substring( i );
|
||||||
|
const col = extract.slice( 0, extract.indexOf( '\n' ) );
|
||||||
|
if ( !indexer[ col ] ) {
|
||||||
|
indexer[ col ] = [];
|
||||||
|
}
|
||||||
|
indexer[ col ].push( lineNumber );
|
||||||
|
} else if ( char === 'r' ) {
|
||||||
|
const extract = data.substring( i );
|
||||||
|
if ( extract.slice( 0, 3 ) === 'rgb' ) {
|
||||||
|
const col = extract.slice( 0, extract.indexOf( '\n' ) );
|
||||||
|
if ( !indexer[ col ] ) {
|
||||||
|
indexer[ col ] = [];
|
||||||
|
}
|
||||||
|
indexer[ col ].push( lineNumber );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Output
|
||||||
|
const keys = Object.keys( indexer );
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const element = keys[i];
|
||||||
|
|
||||||
|
if ( element.length <= 25 ) {
|
||||||
|
console.log( 'Colour ' + element.slice( 0, element.length - 1 ) + ' appears on ' + indexer[ element ] );
|
||||||
|
}
|
||||||
|
}
|
58
collect.sh
58
collect.sh
@ -1,58 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
# Copy VSCodium settings
|
|
||||||
mkdir -p ./general/configs/VSCodium/User
|
|
||||||
cp ~/.config/VSCodium/product.json ./general/configs/VSCodium/
|
|
||||||
cp -r ~/.config/VSCodium/User/snippets ./general/configs/VSCodium/User/
|
|
||||||
cp -r ~/.config/VSCodium/User/globalStorage ./general/configs/VSCodium/User/
|
|
||||||
cp ~/.config/VSCodium/User/keybindings.json ./general/configs/VSCodium/User/
|
|
||||||
cp ~/.config/VSCodium/User/settings.json ./general/configs/VSCodium/User/
|
|
||||||
cp ~/.config/VSCodium/User/syncLocalSettings.json ./general/configs/VSCodium/User/
|
|
||||||
rm -rf ./general/configs/VSCodium/User/globalStorage
|
|
||||||
|
|
||||||
rm ./pc/installedPackages.txt
|
|
||||||
pacman -Qq > ./pc/installedPackages.txt
|
|
||||||
sudo cat /etc/pacman.conf > ./general/pacman.conf
|
|
||||||
sudo cat /etc/mkinitcpio.conf > ./general/mkinitcpio.conf
|
|
||||||
sudo cat /etc/environment > ./pc/environment
|
|
||||||
sudo cat /etc/default/grub > ./pc/grub
|
|
||||||
mkdir ./general/docker
|
|
||||||
sudo cat /etc/docker/daemon.json > ./general/docker/daemon.json
|
|
||||||
|
|
||||||
cp ~/.bash_history ./general
|
|
||||||
cp ~/.bashrc ./general
|
|
||||||
|
|
||||||
# copy other config files
|
|
||||||
mkdir ./pc/configs/OpenRGB
|
|
||||||
mkdir ./pc/configs/Portmaster
|
|
||||||
cp -r ~/.config/hypr ./pc/configs/
|
|
||||||
cp -r ~/.config/waybar ./pc/configs/
|
|
||||||
mkdir ./pc/configs/rofi
|
|
||||||
cp ~/.config/rofi/config.rasi ./pc/configs/rofi/
|
|
||||||
cp -r ~/.config/OpenRGB/OpenRGB.json ./pc/configs/OpenRGB/OpenRGB.json
|
|
||||||
cp ~/.config/Portmaster/Preferences ./pc/configs/Portmaster/
|
|
||||||
cp -r ~/.config/mpv ./general/configs
|
|
||||||
cp -r ~/.config/wlogout ./general/configs/
|
|
||||||
cp -r ~/.config/fish ./general/configs/
|
|
||||||
cp -r ~/.config/dunst ./general/configs/
|
|
||||||
cp -r ~/.config/rofi ./general/configs/
|
|
||||||
cp -r ~/.config/kitty ./general/configs/
|
|
||||||
cp -r ~/.config/terminator ./general/configs/
|
|
||||||
cp -r ~/.config/Thunar ./general/configs/
|
|
||||||
cp -r ~/.config/yazi ./general/configs/
|
|
||||||
cp -r ~/.config/Kvantum/ ./general/configs/
|
|
||||||
cp -r ~/.config/lazygit/ ./general/configs/
|
|
||||||
cp -r ~/.config/zathura/ ./general/configs/
|
|
||||||
|
|
||||||
rm -rf ./pc/configs/hypr/hyprland/
|
|
||||||
cp -r ~/.config/hypr/hyprland/* ./general/hyprland/
|
|
||||||
|
|
||||||
# Get systemd mount jobs
|
|
||||||
sudo cp -r /etc/systemd/system/mnt* ./pc/mnt
|
|
||||||
sudo chmod -R 777 ./pc/mnt
|
|
||||||
|
|
||||||
|
|
||||||
vscodium --list-extensions > ./general/vscode-extensions
|
|
||||||
|
|
||||||
|
|
||||||
echo "Collected. Also consider running nvim-collect!"
|
|
@ -1,33 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
# Copy VSCodium settings
|
|
||||||
mkdir -p ./general/configs/VSCodium/User
|
|
||||||
cp ~/.config/VSCodium/product.json ./general/configs/VSCodium/
|
|
||||||
cp -r ~/.config/VSCodium/User/snippets ./general/configs/VSCodium/User/
|
|
||||||
cp ~/.config/VSCodium/User/keybindings.json ./general/configs/VSCodium/User/
|
|
||||||
cp ~/.config/VSCodium/User/settings.json ./general/configs/VSCodium/User/
|
|
||||||
cp ~/.config/VSCodium/User/syncLocalSettings.json ./general/configs/VSCodium/User/
|
|
||||||
|
|
||||||
rm ./laptop/installedPackages.txt
|
|
||||||
pacman -Qq > ./laptop/installedPackages.txt
|
|
||||||
|
|
||||||
# copy other config files
|
|
||||||
cp -r ~/.config/hypr ./laptop/configs
|
|
||||||
cp -r ~/.config/waybar ./laptop/configs
|
|
||||||
mkdir ./laptop/configs/rofi
|
|
||||||
cp ~/.config/rofi/config.rasi ./laptop/configs/rofi/
|
|
||||||
cp -r ~/.config/wlogout ./general/configs
|
|
||||||
cp -r ~/.config/mpv ./general/configs
|
|
||||||
cp -r ~/.config/dunst ./general/configs
|
|
||||||
cp -r ~/.config/fish ./general/configs
|
|
||||||
cp -r ~/.config/rofi ./general/configs
|
|
||||||
cp -r ~/.config/yazi ./general/configs
|
|
||||||
cp -r ~/.config/kitty ./general/configs
|
|
||||||
cp -r ~/.config/Kvantum/ ./general/configs/
|
|
||||||
cp -r ~/.config/lazygit/ ./general/configs/
|
|
||||||
rm -rf ./laptop/configs/hypr/hyprland/
|
|
||||||
cp -r ~/.config/hypr/hyprland/* ./general/hyprland/
|
|
||||||
|
|
||||||
echo '=> Collected all laptop config files successfully'
|
|
||||||
|
|
||||||
echo "Also consider running nvim-collect!"
|
|
5
config.json
Normal file
5
config.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"gen": {
|
||||||
|
"overrides": {}
|
||||||
|
}
|
||||||
|
}
|
2
config/ags/launcher/.gitignore
vendored
Normal file
2
config/ags/launcher/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
@girs/
|
19
config/ags/launcher/app.ts
Normal file
19
config/ags/launcher/app.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { App } from "astal/gtk4"
|
||||||
|
import style from "./style.scss"
|
||||||
|
import Bar from "./ui/Launcher"
|
||||||
|
|
||||||
|
App.start({
|
||||||
|
css: style,
|
||||||
|
main() {
|
||||||
|
App.get_monitors().map(Bar)
|
||||||
|
},
|
||||||
|
requestHandler(request, res) {
|
||||||
|
if ( request === 'open' ) {
|
||||||
|
res( 'ok' );
|
||||||
|
} else if ( request === 'close' ) {
|
||||||
|
res( 'ok' );
|
||||||
|
} else if ( request === 'toggle' ) {
|
||||||
|
res( 'ok' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
75
config/ags/launcher/definitions/components.d.ts
vendored
Normal file
75
config/ags/launcher/definitions/components.d.ts
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - components.d.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { UIComponent, ResultElement } from "./rendering";
|
||||||
|
|
||||||
|
|
||||||
|
export interface App extends ResultElement {
|
||||||
|
/**
|
||||||
|
* The app start command that will be executed
|
||||||
|
*/
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Finish
|
||||||
|
export interface DictionaryEntry extends ResultElement {
|
||||||
|
/**
|
||||||
|
* Execute no command
|
||||||
|
*/
|
||||||
|
action: null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dictionary definition
|
||||||
|
*/
|
||||||
|
definition: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CMDOutput extends ResultElement {
|
||||||
|
/**
|
||||||
|
* Stdout from the command that was run
|
||||||
|
*/
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Calculation extends ResultElement {
|
||||||
|
/**
|
||||||
|
* THe calculation result
|
||||||
|
*/
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ************* *
|
||||||
|
* UI Components *
|
||||||
|
* ************* */
|
||||||
|
|
||||||
|
export interface LargeUIComponent extends UIComponent {
|
||||||
|
/**
|
||||||
|
* The number of items to display per line. Image size will automatically be scaled
|
||||||
|
* based on width
|
||||||
|
*/
|
||||||
|
itemsPerLine: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediumUIComponent extends UIComponent {}
|
||||||
|
|
||||||
|
export interface ListUIComponent extends UIComponent {}
|
||||||
|
|
||||||
|
export interface DictionaryUIComponent extends UIComponent {
|
||||||
|
elements: DictionaryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CMDOutputUIComponent extends UIComponent {
|
||||||
|
elements: CMDOutput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculationUIComponent extends UIComponent {
|
||||||
|
elements: Calculation[];
|
||||||
|
}
|
60
config/ags/launcher/definitions/rendering.d.ts
vendored
Normal file
60
config/ags/launcher/definitions/rendering.d.ts
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - rendering.d.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UIComponent {
|
||||||
|
/**
|
||||||
|
* The title of the component (like a category name), shown above small divider line
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResultElement list, made up of all elements that should be shown
|
||||||
|
*/
|
||||||
|
elements: ResultElement[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose how many elements to show before truncating (will expand when command is run)
|
||||||
|
*/
|
||||||
|
truncate: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The weight of the element (determines order)
|
||||||
|
*/
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ResultElement {
|
||||||
|
/**
|
||||||
|
* The name of the result element
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the image to be displayed in the UI
|
||||||
|
*/
|
||||||
|
img: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The weight of the element (determines order)
|
||||||
|
*/
|
||||||
|
weight: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action to be executed
|
||||||
|
*/
|
||||||
|
action: Action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The font size of the text (optional)
|
||||||
|
*/
|
||||||
|
fontSize: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = '' | null;
|
21
config/ags/launcher/env.d.ts
vendored
Normal file
21
config/ags/launcher/env.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
declare const SRC: string
|
||||||
|
|
||||||
|
declare module "inline:*" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.scss" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.blp" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.css" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
32
config/ags/launcher/helpers/fzf/dist/fzf.js
vendored
Normal file
32
config/ags/launcher/helpers/fzf/dist/fzf.js
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fast_fuzzy_1 = require("fast-fuzzy");
|
||||||
|
const fs_1 = __importDefault(require("fs"));
|
||||||
|
// Get list source from args
|
||||||
|
// ARGS: type source
|
||||||
|
// Then we read query from stdin to not restart indexing & the like for each keystroke
|
||||||
|
let data = [];
|
||||||
|
if (process.argv[2] === 'fs') {
|
||||||
|
if (process.argv[3].includes('.json')) {
|
||||||
|
data = JSON.parse('' + fs_1.default.readFileSync(process.argv[3]));
|
||||||
|
}
|
||||||
|
else if (process.argv[3].includes('.txt')) {
|
||||||
|
data = ('' + fs_1.default.readFileSync(process.argv[3])).split(',');
|
||||||
|
}
|
||||||
|
else if (fs_1.default.statSync(process.argv[3]).isDirectory()) {
|
||||||
|
data = fs_1.default.readdirSync(process.argv[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (process.argv[2] === 'arg') {
|
||||||
|
data = process.argv[3].split(',');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('Invalid argument at position 1. Can be either fs or arg, not ' + process.argv[2]);
|
||||||
|
}
|
||||||
|
process.stdin.on("data", (query) => {
|
||||||
|
// On stdin submit (which the other client will have to support) process data
|
||||||
|
console.log((0, fast_fuzzy_1.search)(query.toString(), data));
|
||||||
|
});
|
18
config/ags/launcher/helpers/fzf/package.json
Normal file
18
config/ags/launcher/helpers/fzf/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "fzf",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "",
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-fuzzy": "^1.12.0"
|
||||||
|
}
|
||||||
|
}
|
27
config/ags/launcher/helpers/fzf/src/fzf.ts
Normal file
27
config/ags/launcher/helpers/fzf/src/fzf.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { search } from 'fast-fuzzy';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
// Get list source from args
|
||||||
|
// ARGS: type source
|
||||||
|
// Then we read query from stdin to not restart indexing & the like for each keystroke
|
||||||
|
|
||||||
|
let data: string[] = [];
|
||||||
|
if ( process.argv[ 2 ] === 'fs' ) {
|
||||||
|
if ( process.argv[ 3 ].includes( '.json' ) ) {
|
||||||
|
data = JSON.parse( '' + fs.readFileSync( process.argv[ 3 ] ) );
|
||||||
|
} else if ( process.argv[ 3 ].includes( '.txt' ) ) {
|
||||||
|
data = ( '' + fs.readFileSync( process.argv[ 3 ] ) ).split( ',' );
|
||||||
|
} else if ( fs.statSync( process.argv[ 3 ] ).isDirectory() ) {
|
||||||
|
data = fs.readdirSync( process.argv[ 3 ] );
|
||||||
|
}
|
||||||
|
} else if ( process.argv[ 2 ] === 'arg' ) {
|
||||||
|
data = process.argv[ 3 ].split( ',' );
|
||||||
|
} else {
|
||||||
|
throw new Error( 'Invalid argument at position 1. Can be either fs or arg, not ' + process.argv[ 2 ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.on( "data", ( query ) => {
|
||||||
|
// On stdin submit (which the other client will have to support) process data
|
||||||
|
|
||||||
|
console.log( search( query.toString(), data ) );
|
||||||
|
} );
|
13
config/ags/launcher/helpers/fzf/tsconfig.json
Normal file
13
config/ags/launcher/helpers/fzf/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"allowJs": true,
|
||||||
|
"target": "ES6",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext"
|
||||||
|
},
|
||||||
|
"include": [ "./src/**/*" ],
|
||||||
|
}
|
6
config/ags/launcher/package.json
Normal file
6
config/ags/launcher/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "astal-shell",
|
||||||
|
"dependencies": {
|
||||||
|
"astal": "/usr/share/astal/gjs"
|
||||||
|
}
|
||||||
|
}
|
20
config/ags/launcher/style.scss
Normal file
20
config/ags/launcher/style.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
|
||||||
|
$fg-color: #{"@theme_fg_color"};
|
||||||
|
$bg-color: #{"@theme_bg_color"};
|
||||||
|
|
||||||
|
window.Bar {
|
||||||
|
background: transparent;
|
||||||
|
color: $fg-color;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
>centerbox {
|
||||||
|
background: $bg-color;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
}
|
14
config/ags/launcher/tsconfig.json
Normal file
14
config/ags/launcher/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
// "checkJs": true,
|
||||||
|
// "allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "astal/gtk4",
|
||||||
|
}
|
||||||
|
}
|
32
config/ags/launcher/ui/Launcher.tsx
Normal file
32
config/ags/launcher/ui/Launcher.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { App, Astal, Gtk, Gdk } from "astal/gtk4"
|
||||||
|
import { Variable } from "astal"
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
App.get_window("launcher")!.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Launcher(monitor: Gdk.Monitor) {
|
||||||
|
const { CENTER } = Gtk.Align
|
||||||
|
const width = Variable(1000)
|
||||||
|
|
||||||
|
const text = Variable("")
|
||||||
|
|
||||||
|
return <window
|
||||||
|
name="launcher"
|
||||||
|
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
|
||||||
|
exclusivity={Astal.Exclusivity.IGNORE}
|
||||||
|
keymode={Astal.Keymode.ON_DEMAND}
|
||||||
|
application={App}
|
||||||
|
onShow={(self) => {
|
||||||
|
text.set("")
|
||||||
|
width.set(self.get_current_monitor().get_width_mm())
|
||||||
|
}}
|
||||||
|
onKeyPressed={(self, keyval, keycode) => {
|
||||||
|
print( 'key pressed: ' + keyval + ' which has code ' + keycode );
|
||||||
|
}}>
|
||||||
|
<box>
|
||||||
|
|
||||||
|
</box>
|
||||||
|
|
||||||
|
</window>
|
||||||
|
}
|
3
config/ags/launcher/ui/components/calc.tsx
Normal file
3
config/ags/launcher/ui/components/calc.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default () => {
|
||||||
|
return <box></box>
|
||||||
|
}
|
12
config/ags/launcher/util/file.ts
Normal file
12
config/ags/launcher/util/file.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - file.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileAsync, writeFileAsync, monitorFile } from "astal";
|
||||||
|
|
||||||
|
|
24
config/ags/launcher/util/fzf.ts
Normal file
24
config/ags/launcher/util/fzf.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - fzf.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/30/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import AstalApps from "gi://AstalApps?version=0.1"
|
||||||
|
|
||||||
|
// TODO: For all astal apps, read a global colours config file
|
||||||
|
const fzfApplication = ( query: string ) => {
|
||||||
|
const apps = new AstalApps.Apps()
|
||||||
|
return apps.fuzzy_query( query );
|
||||||
|
}
|
||||||
|
|
||||||
|
const fzfCmd = ( query: string ) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fzfApplication
|
||||||
|
}
|
51
config/ags/launcher/util/search.ts
Normal file
51
config/ags/launcher/util/search.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - search.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import subprocessRunner from "./subprocessRunner";
|
||||||
|
import fzf from "./fzf";
|
||||||
|
|
||||||
|
const preprocess = ( input: string ) => {
|
||||||
|
// Find out what kind of instruction to process
|
||||||
|
if ( input.startsWith( ':' ) ) {
|
||||||
|
processCommand( input.substring( 1, input.indexOf( ' ' ) ), input.substring( input.indexOf( ' ' ) ).split( ' ' ) );
|
||||||
|
} else if ( input.startsWith( '!' ) ) {
|
||||||
|
processBang( input.substring( 1, input.indexOf( ' ' ) ), input.substring( input.indexOf( ' ' ) ) );
|
||||||
|
} else {
|
||||||
|
// Determine if entered string is calculation or not
|
||||||
|
// We can easily do that by asking qalc (qalculate cli) if this is fine
|
||||||
|
subprocessRunner.executeCommand( 'qalc "' + input + '"' ).then( out => {
|
||||||
|
// we get a calculation result here
|
||||||
|
print( out );
|
||||||
|
processCalculation( out );
|
||||||
|
} ).catch( err => {
|
||||||
|
processSearch( input );
|
||||||
|
print( err );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processSearch = ( input: string ) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const processCalculation = ( output: string ) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const processCommand = ( cmd: string, args: string[] ) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const processBang = ( bang: string, input: string ) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess
|
||||||
|
}
|
43
config/ags/launcher/util/subprocessRunner.ts
Normal file
43
config/ags/launcher/util/subprocessRunner.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - subprocessRunner.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { subprocess, execAsync, Process } from "astal/process";
|
||||||
|
|
||||||
|
// TODO: Get cwd and the likes to then use that to run JavaScript files with node / python with python, etc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a subprocess. If you simply want to run a command that doesn't need continuous updates
|
||||||
|
* run executeCommand instead.
|
||||||
|
* @param cmd - The command to be run
|
||||||
|
* @param onOut - Calback function for stdout of the subprocess
|
||||||
|
* @param onErr - [TODO:description]
|
||||||
|
* @returns [TODO:return]
|
||||||
|
*/
|
||||||
|
const startSubProcess = (
|
||||||
|
cmd: string | string[],
|
||||||
|
onOut: (stdout: string) => void,
|
||||||
|
onErr: (stderr: string) => void | undefined,
|
||||||
|
): Process => {
|
||||||
|
return subprocess( cmd, onOut, onErr );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a command. If you need continuous updates, run startSubProcess instead
|
||||||
|
* @param cmd - The command to be run. Either a string or an array of strings
|
||||||
|
* @returns A Promise resolving to stdout of the command
|
||||||
|
*/
|
||||||
|
const executeCommand = (cmd: string | string[]): Promise<string> => {
|
||||||
|
return execAsync( cmd );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startSubProcess,
|
||||||
|
executeCommand
|
||||||
|
}
|
2
config/ags/notifications/.gitignore
vendored
Normal file
2
config/ags/notifications/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
@girs/
|
32
config/ags/notifications/app.ts
Normal file
32
config/ags/notifications/app.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { App } from "astal/gtk3"
|
||||||
|
import style from "./style.scss"
|
||||||
|
|
||||||
|
import not from "./handler"
|
||||||
|
|
||||||
|
App.start({
|
||||||
|
instanceName: "notifier",
|
||||||
|
css: style,
|
||||||
|
main() {
|
||||||
|
not.startNotificationHandler( 0, App.get_monitors()[0] )
|
||||||
|
},
|
||||||
|
requestHandler(request, res) {
|
||||||
|
if ( request == 'show' ) {
|
||||||
|
not.openNotificationMenu( 0 );
|
||||||
|
res( 'Showing all open notifications' );
|
||||||
|
} else if ( request == 'hide' ) {
|
||||||
|
not.closeNotificationMenu( 0 );
|
||||||
|
res( 'Hid all notifications' );
|
||||||
|
} else if ( request == 'clear' ) {
|
||||||
|
not.clearAllNotifications( 0 );
|
||||||
|
res( 'Cleared all notifications' );
|
||||||
|
} else if ( request == 'clear-newest' ) {
|
||||||
|
not.clearNewestNotifications( 0 );
|
||||||
|
res( 'Cleared newest notification' );
|
||||||
|
} else if ( request == 'toggle' ) {
|
||||||
|
not.toggleNotificationMenu( 0 );
|
||||||
|
res( 'Toggled notifications' );
|
||||||
|
} else {
|
||||||
|
res( 'Unknown command!' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
21
config/ags/notifications/env.d.ts
vendored
Normal file
21
config/ags/notifications/env.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
declare const SRC: string
|
||||||
|
|
||||||
|
declare module "inline:*" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.scss" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.blp" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.css" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
206
config/ags/notifications/handler.tsx
Normal file
206
config/ags/notifications/handler.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - handler.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Astal, Gtk, Gdk } from "astal/gtk3"
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import Notification from "./notifications/notifications";
|
||||||
|
import { type Subscribable } from "astal/binding";
|
||||||
|
import { Variable, bind, timeout } from "astal"
|
||||||
|
|
||||||
|
// Config
|
||||||
|
const TIMEOUT_DELAY = 5000;
|
||||||
|
|
||||||
|
class Notifier implements Subscribable {
|
||||||
|
private display: Map<number, Gtk.Widget> = new Map();
|
||||||
|
private notifications: Map<number, Notifd.Notification> = new Map();
|
||||||
|
|
||||||
|
private notifd: Notifd.Notifd;
|
||||||
|
private subscriberData: Variable<Gtk.Widget[]> = Variable( [] );
|
||||||
|
private instanceID: number;
|
||||||
|
private menuOpen: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the notifier
|
||||||
|
*/
|
||||||
|
constructor( id: number ) {
|
||||||
|
this.instanceID = id;
|
||||||
|
this.menuOpen = false;
|
||||||
|
this.notifd = Notifd.get_default();
|
||||||
|
this.notifd.ignoreTimeout = true;
|
||||||
|
|
||||||
|
this.notifd.connect( 'notified', ( _, id ) => {
|
||||||
|
this.add( id );
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.notifd.connect( 'resolved', ( _, id ) => {
|
||||||
|
this.delete( id );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify () {
|
||||||
|
this.subscriberData.set( [ ...this.display.values() ].reverse() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private add ( id: number ) {
|
||||||
|
this.notifications.set( id, this.notifd.get_notification( id )! );
|
||||||
|
this.show( id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an element on screen
|
||||||
|
* @param id - The id of the element to be shown
|
||||||
|
*/
|
||||||
|
private show ( id: number ) {
|
||||||
|
this.set( id, Notification( {
|
||||||
|
notification: this.notifications.get( id )!,
|
||||||
|
onHoverLost: () => {
|
||||||
|
if ( !this.menuOpen ) {
|
||||||
|
this.hide( id );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup: () => timeout( TIMEOUT_DELAY, () => {
|
||||||
|
if ( !this.menuOpen ) {
|
||||||
|
this.hide( id );
|
||||||
|
}
|
||||||
|
} ),
|
||||||
|
id: id,
|
||||||
|
delete: deleteHelper,
|
||||||
|
instanceID: this.instanceID
|
||||||
|
} ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a selected widget to be shown
|
||||||
|
* @param id - The id of the element to be referenced for later
|
||||||
|
* @param widget - A GTK widget instance
|
||||||
|
*/
|
||||||
|
private set ( id: number, widget: Gtk.Widget ) {
|
||||||
|
this.display.get( id )?.destroy();
|
||||||
|
this.display.set( id, widget );
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide, not delete notification (= send to notification centre)
|
||||||
|
* @param id - The id of the notification to hide
|
||||||
|
*/
|
||||||
|
private hide ( id: number ) {
|
||||||
|
this.display.get( id )?.destroy();
|
||||||
|
this.display.delete( id );
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a notification (from notification centre too)
|
||||||
|
* @param id - The id of the notification to hide
|
||||||
|
*/
|
||||||
|
delete ( id: number ) {
|
||||||
|
this.hide( id );
|
||||||
|
this.notifications.get( id )?.dismiss();
|
||||||
|
this.notifications.delete( id );
|
||||||
|
if ( this.notifications.size == 0 ) {
|
||||||
|
this.menuOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openNotificationMenu () {
|
||||||
|
// Show all notifications that have not been cleared
|
||||||
|
if ( this.notifications.size > 0 ) {
|
||||||
|
this.menuOpen = true;
|
||||||
|
this.notifications.forEach( ( _, id ) => {
|
||||||
|
this.show( id );
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideNotifications () {
|
||||||
|
this.menuOpen = false;
|
||||||
|
this.notifications.forEach( ( _, id ) => {
|
||||||
|
this.hide( id );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleNotificationMenu () {
|
||||||
|
if ( this.menuOpen ) {
|
||||||
|
this.hideNotifications();
|
||||||
|
} else {
|
||||||
|
this.openNotificationMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllNotifications () {
|
||||||
|
this.menuOpen = false;
|
||||||
|
this.notifications.forEach( ( _, id ) => {
|
||||||
|
this.delete( id );
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
|
||||||
|
clearNewestNotification () {
|
||||||
|
this.delete( [ ...this.notifications.keys() ][0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(callback: (value: unknown) => void): () => void {
|
||||||
|
return this.subscriberData.subscribe( callback );
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.subscriberData.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifiers: Map<number, Notifier> = new Map();
|
||||||
|
const deleteHelper = ( id: number, instanceID: number ) => {
|
||||||
|
notifiers.get( instanceID )?.delete( id );
|
||||||
|
}
|
||||||
|
|
||||||
|
const openNotificationMenu = ( id: number ) => {
|
||||||
|
notifiers.get( id )?.openNotificationMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeNotificationMenu = ( id: number ) => {
|
||||||
|
notifiers.get( id )?.hideNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleNotificationMenu = ( id: number ) => {
|
||||||
|
notifiers.get( id )?.toggleNotificationMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAllNotifications = ( id: number ) => {
|
||||||
|
notifiers.get( id )?.clearAllNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearNewestNotifications = ( id: number ) => {
|
||||||
|
notifiers.get( id )?.clearNewestNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNotificationHandler = (id: number, gdkmonitor: Gdk.Monitor) => {
|
||||||
|
const { TOP, RIGHT } = Astal.WindowAnchor
|
||||||
|
const notifier: Notifier = new Notifier( id );
|
||||||
|
notifiers.set( id, notifier );
|
||||||
|
|
||||||
|
return <window
|
||||||
|
className="NotificationHandler"
|
||||||
|
gdkmonitor={gdkmonitor}
|
||||||
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
|
anchor={TOP | RIGHT}>
|
||||||
|
<box vertical noImplicitDestroy>
|
||||||
|
{bind(notifier)}
|
||||||
|
</box>
|
||||||
|
</window>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startNotificationHandler,
|
||||||
|
openNotificationMenu,
|
||||||
|
closeNotificationMenu,
|
||||||
|
clearAllNotifications,
|
||||||
|
clearNewestNotifications,
|
||||||
|
toggleNotificationMenu
|
||||||
|
}
|
125
config/ags/notifications/notifications/notifications.scss
Normal file
125
config/ags/notifications/notifications/notifications.scss
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
@use "sass:string";
|
||||||
|
|
||||||
|
@function gtkalpha($c, $a) {
|
||||||
|
@return string.unquote("alpha(#{$c},#{$a})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
|
||||||
|
$fg-color: #{"@theme_fg_color"};
|
||||||
|
$bg-color: #{"@theme_bg_color"};
|
||||||
|
$error: red;
|
||||||
|
|
||||||
|
window.NotificationPopups {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventbox.Notification {
|
||||||
|
|
||||||
|
&:first-child>box {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child>box {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventboxes can not take margins so we style its inner box instead
|
||||||
|
>box {
|
||||||
|
min-width: 400px;
|
||||||
|
border-radius: 13px;
|
||||||
|
background-color: $bg-color;
|
||||||
|
margin: .5rem 1rem .5rem 1rem;
|
||||||
|
box-shadow: 2px 3px 8px 0 gtkalpha(black, .4);
|
||||||
|
border: 1pt solid gtkalpha($fg-color, .03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.critical>box {
|
||||||
|
border: 1pt solid gtkalpha($error, .4);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
color: gtkalpha($error, .8);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
color: gtkalpha($error, .6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: .5rem;
|
||||||
|
color: gtkalpha($fg-color, 0.5);
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
margin: 0 .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
margin-right: .3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: .4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
margin: 0 .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: .2rem;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
separator {
|
||||||
|
margin: 0 .4rem;
|
||||||
|
background-color: gtkalpha($fg-color, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-top: .5rem;
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: $fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
color: gtkalpha($fg-color, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border: 1px solid gtkalpha($fg-color, .02);
|
||||||
|
margin-right: .5rem;
|
||||||
|
border-radius: 9px;
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 .3rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
config/ags/notifications/notifications/notifications.tsx
Normal file
112
config/ags/notifications/notifications/notifications.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// From astal examples
|
||||||
|
|
||||||
|
import { GLib } from "astal"
|
||||||
|
import { Gtk, Astal } from "astal/gtk3"
|
||||||
|
import { type EventBox } from "astal/gtk3/widget"
|
||||||
|
import Notifd from "gi://AstalNotifd"
|
||||||
|
|
||||||
|
const isIcon = (icon: string) =>
|
||||||
|
!!Astal.Icon.lookup_icon(icon)
|
||||||
|
|
||||||
|
const fileExists = (path: string) =>
|
||||||
|
GLib.file_test(path, GLib.FileTest.EXISTS)
|
||||||
|
|
||||||
|
const time = (time: number, format = "%H:%M") => GLib.DateTime
|
||||||
|
.new_from_unix_local(time)
|
||||||
|
.format(format)!
|
||||||
|
|
||||||
|
const urgency = (n: Notifd.Notification) => {
|
||||||
|
const { LOW, NORMAL, CRITICAL } = Notifd.Urgency
|
||||||
|
// match operator when?
|
||||||
|
switch (n.urgency) {
|
||||||
|
case LOW: return "low"
|
||||||
|
case CRITICAL: return "critical"
|
||||||
|
case NORMAL:
|
||||||
|
default: return "normal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
delete( id: number, instanceID: number ): void
|
||||||
|
setup(self: EventBox): void
|
||||||
|
onHoverLost(self: EventBox): void
|
||||||
|
notification: Notifd.Notification
|
||||||
|
id: number
|
||||||
|
instanceID: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Notification(props: Props) {
|
||||||
|
const { notification: n, onHoverLost, setup, id: id, delete: del, instanceID: instance } = props
|
||||||
|
const { START, CENTER, END } = Gtk.Align
|
||||||
|
|
||||||
|
return <eventbox
|
||||||
|
className={`Notification ${urgency(n)}`}
|
||||||
|
setup={setup}
|
||||||
|
onHoverLost={onHoverLost}>
|
||||||
|
<box vertical>
|
||||||
|
<box className="header">
|
||||||
|
{(n.appIcon || n.desktopEntry) && <icon
|
||||||
|
className="app-icon"
|
||||||
|
visible={Boolean(n.appIcon || n.desktopEntry)}
|
||||||
|
icon={n.appIcon || n.desktopEntry}
|
||||||
|
/>}
|
||||||
|
<label
|
||||||
|
className="app-name"
|
||||||
|
halign={START}
|
||||||
|
truncate
|
||||||
|
label={n.appName || "Unknown"}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="time"
|
||||||
|
hexpand
|
||||||
|
halign={END}
|
||||||
|
label={time(n.time)}
|
||||||
|
/>
|
||||||
|
<button onClicked={() => del( id, instance )}>
|
||||||
|
<icon icon="window-close-symbolic" />
|
||||||
|
</button>
|
||||||
|
</box>
|
||||||
|
<Gtk.Separator visible />
|
||||||
|
<box className="content">
|
||||||
|
{n.image && fileExists(n.image) && <box
|
||||||
|
valign={START}
|
||||||
|
className="image"
|
||||||
|
css={`background-image: url('${n.image}')`}
|
||||||
|
/>}
|
||||||
|
{n.image && isIcon(n.image) && <box
|
||||||
|
expand={false}
|
||||||
|
valign={START}
|
||||||
|
className="icon-image">
|
||||||
|
<icon icon={n.image} expand halign={CENTER} valign={CENTER} />
|
||||||
|
</box>}
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
className="summary"
|
||||||
|
halign={START}
|
||||||
|
xalign={0}
|
||||||
|
label={n.summary}
|
||||||
|
truncate
|
||||||
|
/>
|
||||||
|
{n.body && <label
|
||||||
|
className="body"
|
||||||
|
wrap
|
||||||
|
useMarkup
|
||||||
|
halign={START}
|
||||||
|
xalign={0}
|
||||||
|
justifyFill
|
||||||
|
label={n.body}
|
||||||
|
/>}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
{n.get_actions().length > 0 && <box className="actions">
|
||||||
|
{n.get_actions().map(({ label, id }) => (
|
||||||
|
<button
|
||||||
|
hexpand
|
||||||
|
onClicked={() => n.invoke(id)}>
|
||||||
|
<label label={label} halign={CENTER} hexpand />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</box>}
|
||||||
|
</box>
|
||||||
|
</eventbox>
|
||||||
|
}
|
6
config/ags/notifications/package.json
Normal file
6
config/ags/notifications/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "astal-shell",
|
||||||
|
"dependencies": {
|
||||||
|
"astal": "/usr/share/astal/gjs"
|
||||||
|
}
|
||||||
|
}
|
2
config/ags/notifications/style.scss
Normal file
2
config/ags/notifications/style.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Import notification box style
|
||||||
|
@use "./notifications/notifications.scss"
|
14
config/ags/notifications/tsconfig.json
Normal file
14
config/ags/notifications/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
// "checkJs": true,
|
||||||
|
// "allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "astal/gtk3",
|
||||||
|
}
|
||||||
|
}
|
2
config/astal/.gitignore
vendored
Normal file
2
config/astal/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
@girs/
|
103
config/astal/app.ts
Normal file
103
config/astal/app.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { App } from "astal/gtk4"
|
||||||
|
import style from "./style.scss"
|
||||||
|
import Bar from "./components/bar/Bar";
|
||||||
|
import AstalHyprland from "gi://AstalHyprland?version=0.1";
|
||||||
|
import { hyprToGdk } from "./util/hyprland";
|
||||||
|
import Brightness from "./util/brightness";
|
||||||
|
|
||||||
|
App.start({
|
||||||
|
instanceName: "runner",
|
||||||
|
css: style,
|
||||||
|
main() {
|
||||||
|
const hypr = AstalHyprland.get_default();
|
||||||
|
const bars = new Map<number, string>();
|
||||||
|
|
||||||
|
const barCreator = ( monitor: AstalHyprland.Monitor ) => {
|
||||||
|
const gdkMonitor = hyprToGdk( monitor );
|
||||||
|
if ( gdkMonitor ) {
|
||||||
|
print( 'Bar added for screen ' + monitor.get_id() );
|
||||||
|
bars.set( monitor.get_id(), Bar.BarLauncher( gdkMonitor ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const monitor of hypr.monitors) {
|
||||||
|
barCreator( monitor );
|
||||||
|
}
|
||||||
|
|
||||||
|
hypr.connect( 'monitor-added', ( _, monitor ) => {
|
||||||
|
barCreator( monitor );
|
||||||
|
} );
|
||||||
|
|
||||||
|
hypr.connect( 'monitor-removed', ( _, monitor ) => {
|
||||||
|
const windowName = bars.get( monitor );
|
||||||
|
if ( windowName ) {
|
||||||
|
const win = App.get_window( windowName );
|
||||||
|
if ( win ) {
|
||||||
|
App.toggle_window( windowName );
|
||||||
|
win.set_child( null );
|
||||||
|
App.remove_window( win );
|
||||||
|
print( 'Bar removed for screen', monitor );
|
||||||
|
}
|
||||||
|
bars.delete( monitor );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// const monitors = App.get_monitors();
|
||||||
|
// print( "adding bar to monitors" );
|
||||||
|
// for (let index = 0; index < monitors.length; index++) {
|
||||||
|
// Bar.BarLauncher( monitors[ index ] );
|
||||||
|
// }
|
||||||
|
// Launcher();
|
||||||
|
},
|
||||||
|
requestHandler(request, res) {
|
||||||
|
const args = request.trimStart().split( ' ' );
|
||||||
|
|
||||||
|
if ( args[ 0 ] === 'notifier' ) {
|
||||||
|
res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] );
|
||||||
|
// res( notifications.cliHandler( args ) );
|
||||||
|
} else if ( args[ 0 ] === 'bar' ) {
|
||||||
|
res( Bar.cliHandler( args ) );
|
||||||
|
} else if ( args[ 0 ] === 'brightness' ) {
|
||||||
|
try {
|
||||||
|
const brightness = Brightness.get_default();
|
||||||
|
if ( brightness.screenAvailable ) {
|
||||||
|
if ( args[ 1 ] === 'increase' ) {
|
||||||
|
brightness.screen += args.length > 1 ? parseInt( args[ 2 ] ) / 100 : 1;
|
||||||
|
} else if ( args[ 1 ] === 'decrease' ) {
|
||||||
|
brightness.screen -= args.length > 1 ? parseInt( args[ 2 ] ) / 100 : 1;
|
||||||
|
} else if ( args[ 1 ] === 'set' ) {
|
||||||
|
if ( args.length > 1 ) {
|
||||||
|
brightness.screen = parseInt( args[ 2 ] ) / 100;
|
||||||
|
} else {
|
||||||
|
res( 'Argument <brightness> unspecified' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res( 'Unknown command ' + args[ 1 ] );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res( 'Ok' );
|
||||||
|
} else {
|
||||||
|
res( 'No controllable screen available' );
|
||||||
|
}
|
||||||
|
} catch ( e ) {
|
||||||
|
res( 'Error running brightness change' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// } else if ( args[ 0 ] === 'launcher' ) {
|
||||||
|
// if ( args[ 1 ] === 'show' ) {
|
||||||
|
// App.get_window( 'launcher' )?.show();
|
||||||
|
// res( '[Launcher] Shown' );
|
||||||
|
// } else if ( args[ 1 ] === 'hide' ) {
|
||||||
|
// App.get_window( 'launcher' )?.hide();
|
||||||
|
// res( '[Launcher] Hidden' );
|
||||||
|
// } else if ( args[ 1 ] === 'toggle' ) {
|
||||||
|
// App.toggle_window( 'launcher' );
|
||||||
|
// res( '[Launcher] Toggled' );
|
||||||
|
// } else {
|
||||||
|
// res( '[Launcher] unknown command' );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
1
config/astal/btconf
Normal file
1
config/astal/btconf
Normal file
@ -0,0 +1 @@
|
|||||||
|
false
|
76
config/astal/components/QuickActions/QuickActions.tsx
Normal file
76
config/astal/components/QuickActions/QuickActions.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Power from "./modules/Power";
|
||||||
|
import Audio from "./modules/Audio/Audio";
|
||||||
|
import Bluetooth from "./modules/Bluetooth/Bluetooth";
|
||||||
|
import Brightness from "./modules/Brightness/Brightness";
|
||||||
|
import Player from "./modules/Player/Player";
|
||||||
|
import { BatteryBox } from "./modules/Battery";
|
||||||
|
import { exec } from "astal";
|
||||||
|
import Network from "./modules/Networking/Network";
|
||||||
|
|
||||||
|
const QuickActions = () => {
|
||||||
|
const popover = new Gtk.Popover({ cssClasses: ["quick-actions-wrapper"] });
|
||||||
|
popover.set_child(renderQuickActions());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderQuickActions = () => {
|
||||||
|
const user = exec("/bin/sh -c whoami");
|
||||||
|
const profile = exec("/bin/fish -c get-profile-picture");
|
||||||
|
const cwd = exec("pwd");
|
||||||
|
const um = Power.UserMenu();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box visible cssClasses={["quick-actions", "popover-box"]} vertical>
|
||||||
|
<centerbox
|
||||||
|
startWidget={
|
||||||
|
<button
|
||||||
|
onClicked={() => um.popup()}
|
||||||
|
cssClasses={["stealthy-button"]}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
{um}
|
||||||
|
<Gtk.Frame
|
||||||
|
cssClasses={["avatar-icon"]}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
file={
|
||||||
|
profile !== ""
|
||||||
|
? profile
|
||||||
|
: cwd +
|
||||||
|
"/no-avatar-icon.jpg"
|
||||||
|
}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
></Gtk.Frame>
|
||||||
|
<label label={user}></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
}
|
||||||
|
endWidget={
|
||||||
|
<box
|
||||||
|
hexpand={false}
|
||||||
|
>
|
||||||
|
<BatteryBox></BatteryBox>
|
||||||
|
<Power.Power></Power.Power>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
<Gtk.Separator marginTop={10} marginBottom={20}></Gtk.Separator>
|
||||||
|
<box>
|
||||||
|
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
|
||||||
|
<Network.Network></Network.Network>
|
||||||
|
</box>
|
||||||
|
<Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator>
|
||||||
|
<Brightness.BrightnessModule></Brightness.BrightnessModule>
|
||||||
|
<Audio.AudioModule></Audio.AudioModule>
|
||||||
|
<Player.PlayerModule></Player.PlayerModule>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Expose additional functions to be usable through CLI
|
||||||
|
export default {
|
||||||
|
QuickActions,
|
||||||
|
};
|
35
config/astal/components/QuickActions/dump
Normal file
35
config/astal/components/QuickActions/dump
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Power from "./modules/Power";
|
||||||
|
import Audio from "./modules/Audio/Audio";
|
||||||
|
import Bluetooth from "./modules/Bluetooth/Bluetooth";
|
||||||
|
import Brightness from "./modules/Brightness/Brightness";
|
||||||
|
import Player from "./modules/Player/Player";
|
||||||
|
import { BatteryBox } from "./modules/Battery";
|
||||||
|
|
||||||
|
const QuickActions = () => {
|
||||||
|
const popover = new Gtk.Overlay( { cssClasses: [ 'quick-actions-wrapper' ] } );
|
||||||
|
popover.set_child(renderQuickActions());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderQuickActions = () => {
|
||||||
|
return (
|
||||||
|
<box visible cssClasses={["quick-actions", "popover-box"]} vertical setup={ self }>
|
||||||
|
<box halign={Gtk.Align.END}>
|
||||||
|
<BatteryBox></BatteryBox>
|
||||||
|
<Power></Power>
|
||||||
|
</box>
|
||||||
|
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
|
||||||
|
<Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator>
|
||||||
|
<Brightness.BrightnessModule></Brightness.BrightnessModule>
|
||||||
|
<Audio.AudioModule></Audio.AudioModule>
|
||||||
|
<Gtk.Separator marginTop={20} marginBottom={10}></Gtk.Separator>
|
||||||
|
<Player.PlayerModule></Player.PlayerModule>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Expose additional functions to be usable through CLI
|
||||||
|
export default {
|
||||||
|
QuickActions,
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
.audio-box {
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
185
config/astal/components/QuickActions/modules/Audio/Audio.tsx
Normal file
185
config/astal/components/QuickActions/modules/Audio/Audio.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { bind, Binding } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalWp from "gi://AstalWp";
|
||||||
|
|
||||||
|
const wp = AstalWp.get_default()!;
|
||||||
|
|
||||||
|
const AudioModule = () => {
|
||||||
|
const setVolumeSpeaker = (volume: number) => {
|
||||||
|
wp.defaultSpeaker.set_volume(volume / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setVolumeMicrophone = (volume: number) => {
|
||||||
|
wp.defaultMicrophone.set_volume(volume / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const speakerSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_SPEAKER);
|
||||||
|
const micSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_MICROPHONE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box cssClasses={["audio-box"]} vertical>
|
||||||
|
<box hexpand vexpand>
|
||||||
|
<button
|
||||||
|
onClicked={() =>
|
||||||
|
wp.defaultSpeaker.set_mute(
|
||||||
|
!wp.defaultSpeaker.get_mute(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tooltipText={"Mute audio output"}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={bind(wp.defaultSpeaker, "volumeIcon")}
|
||||||
|
marginEnd={3}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
<label
|
||||||
|
label={bind(wp.defaultSpeaker, "volume").as(
|
||||||
|
v => Math.round(100 * v) + "%",
|
||||||
|
)}
|
||||||
|
></label>
|
||||||
|
<slider
|
||||||
|
value={bind(wp.defaultSpeaker, "volume").as(v => 100 * v)}
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
hexpand
|
||||||
|
vexpand
|
||||||
|
onChangeValue={self => setVolumeSpeaker(self.value)}
|
||||||
|
></slider>
|
||||||
|
<button
|
||||||
|
cssClasses={["sink-select-button"]}
|
||||||
|
tooltipText={"Pick audio output"}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={"speaker-symbolic"}></image>
|
||||||
|
{speakerSelector}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
onClicked={() => speakerSelector.popup()}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
<box hexpand vexpand>
|
||||||
|
<button
|
||||||
|
onClicked={() =>
|
||||||
|
wp.defaultMicrophone.set_mute(
|
||||||
|
!wp.defaultMicrophone.get_mute(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tooltipText={"Mute audio input"}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={bind(wp.defaultMicrophone, "volumeIcon")}
|
||||||
|
marginEnd={3}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
<label
|
||||||
|
label={bind(wp.defaultMicrophone, "volume").as(
|
||||||
|
v => Math.round(100 * v) + "%",
|
||||||
|
)}
|
||||||
|
></label>
|
||||||
|
<slider
|
||||||
|
value={bind(wp.defaultMicrophone, "volume").as(
|
||||||
|
v => 100 * v,
|
||||||
|
)}
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
hexpand
|
||||||
|
vexpand
|
||||||
|
onChangeValue={self => setVolumeMicrophone(self.value)}
|
||||||
|
></slider>
|
||||||
|
<button
|
||||||
|
cssClasses={["sink-select-button"]}
|
||||||
|
tooltipText={"Select audio input"}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={"microphone"}></image>
|
||||||
|
{micSelector}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
onClicked={() => micSelector.popup()}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SinkPicker = (type: AstalWp.MediaClass) => {
|
||||||
|
const devices = bind(wp, "endpoints");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={`Available Audio ${type === AstalWp.MediaClass.AUDIO_SPEAKER ? "Output" : type === AstalWp.MediaClass.AUDIO_MICROPHONE ? "Input" : ""} Devices`}
|
||||||
|
cssClasses={[ 'title-2' ]}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator>
|
||||||
|
<box vertical cssClasses={["sink-picker"]}>
|
||||||
|
{devices.as(d => {
|
||||||
|
return d.map(device => {
|
||||||
|
if (device.get_media_class() !== type) {
|
||||||
|
return <box cssClasses={[ 'empty' ]}></box>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
cssClasses={bind(device, "id").as(id => {
|
||||||
|
if (
|
||||||
|
id ===
|
||||||
|
(type ===
|
||||||
|
AstalWp.MediaClass.AUDIO_SPEAKER
|
||||||
|
? wp.defaultSpeaker.id
|
||||||
|
: type ===
|
||||||
|
AstalWp.MediaClass
|
||||||
|
.AUDIO_MICROPHONE
|
||||||
|
? wp.defaultMicrophone.id
|
||||||
|
: "")
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
"sink-option",
|
||||||
|
"currently-selected-sink-option",
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return ["sink-option"];
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
child={
|
||||||
|
<box halign={Gtk.Align.START}>
|
||||||
|
<image
|
||||||
|
iconName={bind(device, "icon").as(
|
||||||
|
icon => icon,
|
||||||
|
)}
|
||||||
|
marginEnd={3}
|
||||||
|
></image>
|
||||||
|
<label
|
||||||
|
label={bind(
|
||||||
|
device,
|
||||||
|
"description",
|
||||||
|
).as(t => t ?? "")}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
onClicked={() => {
|
||||||
|
device.set_is_default(true);
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SinkSelectPopover = (type: AstalWp.MediaClass) => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
popover.set_child(SinkPicker(type));
|
||||||
|
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
AudioModule,
|
||||||
|
};
|
57
config/astal/components/QuickActions/modules/Battery.tsx
Normal file
57
config/astal/components/QuickActions/modules/Battery.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import Battery from "gi://AstalBattery";
|
||||||
|
|
||||||
|
export const BatteryBox = () => {
|
||||||
|
const battery = Battery.get_default();
|
||||||
|
const batteryEnergy = (energyRate: number) => {
|
||||||
|
return energyRate > 0.1 ? `${Math.round(energyRate * 10) / 10} W ` : "";
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
cssClasses={["battery-info"]}
|
||||||
|
visible={bind(battery, "isBattery")}
|
||||||
|
hexpand={false}
|
||||||
|
vexpand={false}
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
iconName={bind(battery, "batteryIconName")}
|
||||||
|
tooltipText={bind(battery, "energyRate").as(er =>
|
||||||
|
batteryEnergy(er),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
label={bind(battery, "percentage").as(
|
||||||
|
p => ` ${Math.round(p * 100)}%`,
|
||||||
|
)}
|
||||||
|
marginEnd={3}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
cssClasses={["battery-time"]}
|
||||||
|
visible={bind(battery, "charging").as(c => !c)}
|
||||||
|
label={bind(battery, "timeToEmpty").as(t => `(${toTime(t)})`)}
|
||||||
|
tooltipText={bind(battery, 'energyRate').as(er => `Time to empty. Power usage: ${batteryEnergy(er)}`)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
cssClasses={["battery-time"]}
|
||||||
|
visible={bind(battery, "charging")}
|
||||||
|
label={bind(battery, "timeToFull").as(t => `(${toTime(t)})`)}
|
||||||
|
tooltipText={bind(battery, 'energyRate').as(er => `Time to full. Charge rate: ${batteryEnergy(er)}`)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTime = (time: number) => {
|
||||||
|
const MINUTE = 60;
|
||||||
|
const HOUR = MINUTE * 60;
|
||||||
|
|
||||||
|
if (time > 24 * HOUR) return "24h+";
|
||||||
|
|
||||||
|
const hours = Math.round(time / HOUR);
|
||||||
|
const minutes = Math.round((time - hours * HOUR) / MINUTE);
|
||||||
|
|
||||||
|
const hoursDisplay = hours > 0 ? `${hours}h` : "";
|
||||||
|
const minutesDisplay = minutes > 0 ? `${minutes}m` : "";
|
||||||
|
|
||||||
|
return `${hoursDisplay}${minutesDisplay}`;
|
||||||
|
};
|
@ -0,0 +1,222 @@
|
|||||||
|
import { bind, interval, readFile, timeout, writeFile } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import BTDevice from "./Device";
|
||||||
|
const ALIGN = Gtk.Align;
|
||||||
|
|
||||||
|
const bt = AstalBluetooth.get_default();
|
||||||
|
|
||||||
|
const BluetoothModule = () => {
|
||||||
|
const picker = BluetoothPicker();
|
||||||
|
|
||||||
|
const openBTPicker = () => {
|
||||||
|
try {
|
||||||
|
bt.adapter.start_discovery();
|
||||||
|
} catch (e) {
|
||||||
|
printerr(e);
|
||||||
|
}
|
||||||
|
picker.popup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={bind(bt, "isPowered").as(powered =>
|
||||||
|
powered
|
||||||
|
? ["toggle-button", "toggle-on"]
|
||||||
|
: ["toggle-button"],
|
||||||
|
)}
|
||||||
|
onClicked={() => {
|
||||||
|
try {
|
||||||
|
bt.adapter.set_powered(!bt.adapter.get_powered())
|
||||||
|
} catch (_) { }
|
||||||
|
}}
|
||||||
|
child={
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
label={"Bluetooth"}
|
||||||
|
halign={ALIGN.CENTER}
|
||||||
|
valign={ALIGN.CENTER}
|
||||||
|
></label>
|
||||||
|
<box halign={ALIGN.CENTER} valign={ALIGN.CENTER}>
|
||||||
|
<label
|
||||||
|
visible={bind(bt, "isPowered").as(
|
||||||
|
p => !p,
|
||||||
|
)}
|
||||||
|
label="Disabled"
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
visible={bind(bt, "isPowered")}
|
||||||
|
label={bind(bt, "devices").as(devices => {
|
||||||
|
let count = 0;
|
||||||
|
devices.forEach(device => {
|
||||||
|
if (device.connected) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return `On (${count} ${count === 1 ? "client" : "clients"} connected)`;
|
||||||
|
})}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
<label></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["actions-button"]}
|
||||||
|
visible={bind(bt, "isPowered")}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={"arrow-right-symbolic"}></image>
|
||||||
|
{picker}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
tooltipText={"View available devices"}
|
||||||
|
onClicked={() => openBTPicker()}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BluetoothPickerList = () => {
|
||||||
|
let btEnableState = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
btEnableState = readFile("./btconf") === "true" ? true : false;
|
||||||
|
} catch (_) { }
|
||||||
|
|
||||||
|
if (bt.get_adapter()) {
|
||||||
|
print('Setting BT state to ' + btEnableState);
|
||||||
|
bt.adapter.set_powered(btEnableState);
|
||||||
|
} else {
|
||||||
|
timeout(5000, () => {
|
||||||
|
if (bt.get_adapter()) {
|
||||||
|
print('Setting BT state to ' + btEnableState);
|
||||||
|
bt.adapter.set_powered(btEnableState);
|
||||||
|
} else {
|
||||||
|
timeout(5000, () => {
|
||||||
|
try {
|
||||||
|
bt.adapter.set_powered(btEnableState);
|
||||||
|
} catch (_) { }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateState = () => {
|
||||||
|
btEnableState = !btEnableState;
|
||||||
|
writeFile("./btconf", "" + btEnableState);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
vertical
|
||||||
|
onDestroy={() => {
|
||||||
|
try {
|
||||||
|
bt.adapter.stop_discovery()
|
||||||
|
} catch (_) { }
|
||||||
|
}}
|
||||||
|
cssClasses={["popover-box"]}
|
||||||
|
>
|
||||||
|
<label cssClasses={["title"]} label={"Bluetooth"}></label>
|
||||||
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
||||||
|
<centerbox
|
||||||
|
startWidget={<label label={"Turn on at startup"}></label>}
|
||||||
|
endWidget={
|
||||||
|
<switch
|
||||||
|
valign={ALIGN.END}
|
||||||
|
halign={ALIGN.END}
|
||||||
|
active={btEnableState}
|
||||||
|
onButtonPressed={() => updateState()}
|
||||||
|
></switch>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
<label
|
||||||
|
marginTop={10}
|
||||||
|
label={"Connected & Trusted devices"}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
||||||
|
<box vertical cssClasses={["devices-list"]}>
|
||||||
|
{bind(bt, "devices").as(devices => {
|
||||||
|
return devices
|
||||||
|
.filter(device => {
|
||||||
|
if (device.get_connected() || device.get_paired()) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(device => {
|
||||||
|
return <BTDevice device={device}></BTDevice>;
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
<label
|
||||||
|
visible={bind(bt, "devices").as(devices => {
|
||||||
|
return (
|
||||||
|
devices.filter(device => {
|
||||||
|
if (device.get_connected() || device.get_paired()) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}).length === 0
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
label={"No connected / trusted devices"}
|
||||||
|
cssClasses={["bt-no-found", "bt-conn-list"]}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
label={"Discovered bluetooth devices"}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator>
|
||||||
|
<box vertical>
|
||||||
|
{bind(bt, "devices").as(devices => {
|
||||||
|
return devices
|
||||||
|
.filter(data => {
|
||||||
|
if (!data.get_connected() && !data.get_paired()) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(device => {
|
||||||
|
return <BTDevice device={device}></BTDevice>;
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
<label
|
||||||
|
visible={bind(bt, "devices").as(devices => {
|
||||||
|
return (
|
||||||
|
devices.filter(device => {
|
||||||
|
if (
|
||||||
|
!device.get_connected() &&
|
||||||
|
!device.get_paired()
|
||||||
|
) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}).length === 0
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
label={"No discovered devices"}
|
||||||
|
cssClasses={["bt-no-found"]}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BluetoothPicker = () => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
popover.set_child(BluetoothPickerList());
|
||||||
|
popover.connect("closed", () => {
|
||||||
|
try {
|
||||||
|
bt.adapter.stop_discovery();
|
||||||
|
} catch (e) {
|
||||||
|
printerr(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
BluetoothModule,
|
||||||
|
};
|
@ -0,0 +1,72 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
|
||||||
|
const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
visible={bind(device, "name").as(n => n !== null)}
|
||||||
|
child={
|
||||||
|
<centerbox
|
||||||
|
startWidget={
|
||||||
|
<box>
|
||||||
|
<image
|
||||||
|
iconName={"chronometer-reset"}
|
||||||
|
tooltipText={"Device is currently connecting"}
|
||||||
|
visible={bind(device, "connecting")}
|
||||||
|
></image>
|
||||||
|
<image
|
||||||
|
iconName={bind(device, "icon")}
|
||||||
|
marginEnd={3}
|
||||||
|
></image>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
centerWidget={
|
||||||
|
<label
|
||||||
|
label={bind(device, "name").as(n => n ?? "No name")}
|
||||||
|
marginEnd={5}
|
||||||
|
></label>
|
||||||
|
}
|
||||||
|
endWidget={
|
||||||
|
<box>
|
||||||
|
<label
|
||||||
|
label={bind(device, "batteryPercentage").as(
|
||||||
|
bat => (bat >= 0 ? bat + "%" : "?%"),
|
||||||
|
)}
|
||||||
|
tooltipText={"Device's battery percentage"}
|
||||||
|
marginEnd={3}
|
||||||
|
></label>
|
||||||
|
<image
|
||||||
|
iconName={bind(device, "paired").as(v =>
|
||||||
|
v ? "network-bluetooth-activated-symbolic" : "bluetooth-disconnected-symbolic",
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
<button tooltipText={"Device trusted status"} child={
|
||||||
|
<image
|
||||||
|
iconName={bind(device, "trusted").as(v =>
|
||||||
|
v ? "checkbox" : "window-close-symbolic",
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
} onClicked={() => device.set_trusted( !device.get_trusted() )}
|
||||||
|
cssClasses={[ 'button-no-margin' ]}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
}
|
||||||
|
onClicked={() => {
|
||||||
|
connectOrPair( device );
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectOrPair = (device: AstalBluetooth.Device) => {
|
||||||
|
if ( device.get_paired() ) {
|
||||||
|
device.connect_device(() => { });
|
||||||
|
// Show failed message if tried to connect and failed
|
||||||
|
} else {
|
||||||
|
device.pair();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BTDevice;
|
@ -0,0 +1,30 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import Brightness from "../../../../util/brightness";
|
||||||
|
|
||||||
|
const brightness = Brightness.get_default();
|
||||||
|
|
||||||
|
const BrightnessModule = () => {
|
||||||
|
print( brightness.screen * 100 );
|
||||||
|
const setBrightness = (value: number) => {
|
||||||
|
brightness.screen = value;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<box visible={bind(brightness, 'screenAvailable')}>
|
||||||
|
<image iconName={"brightness-high-symbolic"}></image>
|
||||||
|
<label label={bind(brightness, "screen").as(b => `${Math.round(100 * b)}%`)}></label>
|
||||||
|
<slider
|
||||||
|
value={Math.round( brightness.screen * 100) / 100}
|
||||||
|
hexpand
|
||||||
|
max={1}
|
||||||
|
min={0.01}
|
||||||
|
step={0.01}
|
||||||
|
vexpand
|
||||||
|
onChangeValue={self => setBrightness(self.value)}
|
||||||
|
></slider>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
BrightnessModule
|
||||||
|
};
|
@ -0,0 +1,221 @@
|
|||||||
|
import { execAsync, bind } from "astal";
|
||||||
|
import Network from "gi://AstalNetwork";
|
||||||
|
import { App, Gtk } from "astal/gtk4";
|
||||||
|
import { NetworkItem } from "./modules/NetworkItem";
|
||||||
|
import { PasswordDialog } from "./modules/PasswordDialog";
|
||||||
|
import {
|
||||||
|
availableNetworks,
|
||||||
|
savedNetworks,
|
||||||
|
activeNetwork,
|
||||||
|
showPasswordDialog,
|
||||||
|
scanNetworks,
|
||||||
|
getSavedNetworks,
|
||||||
|
disconnectNetwork,
|
||||||
|
forgetNetwork,
|
||||||
|
isExpanded,
|
||||||
|
refreshIntervalId,
|
||||||
|
} from "./networkinghelper";
|
||||||
|
|
||||||
|
// Main WiFi Box component
|
||||||
|
export const WiFiBox = () => {
|
||||||
|
const network = Network.get_default();
|
||||||
|
|
||||||
|
// Initial scan when component is first used
|
||||||
|
setTimeout(() => {
|
||||||
|
scanNetworks();
|
||||||
|
getSavedNetworks();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box vertical cssClasses={["wifi-menu", "toggle"]}>
|
||||||
|
{/* WiFi Toggle Header */}
|
||||||
|
<box cssClasses={["toggle", "wifi-toggle"]}>
|
||||||
|
<button
|
||||||
|
onClicked={() => {
|
||||||
|
if (network.wifi.enabled) {
|
||||||
|
network.wifi.set_enabled(false);
|
||||||
|
} else network.wifi.set_enabled(true);
|
||||||
|
}}
|
||||||
|
cssClasses={bind(network.wifi, "enabled").as((enabled) =>
|
||||||
|
enabled ? ["button"] : ["button-disabled"],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<image iconName={bind(network.wifi, "icon_name")} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
hexpand={true}
|
||||||
|
onClicked={() => {
|
||||||
|
if (network.wifi.enabled) {
|
||||||
|
isExpanded.set(!isExpanded.get());
|
||||||
|
if (isExpanded.get()) {
|
||||||
|
scanNetworks();
|
||||||
|
getSavedNetworks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box hexpand={true}>
|
||||||
|
<label
|
||||||
|
hexpand={true}
|
||||||
|
xalign={0}
|
||||||
|
label={bind(network.wifi, "ssid").as(
|
||||||
|
(ssid) =>
|
||||||
|
ssid || (network.wifi.enabled ? "Not Connected" : "WiFi Off"),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
iconName="pan-end-symbolic"
|
||||||
|
halign={Gtk.Align.END}
|
||||||
|
cssClasses={bind(isExpanded).as((expanded) =>
|
||||||
|
expanded
|
||||||
|
? ["arrow-indicator", "arrow-down"]
|
||||||
|
: ["arrow-indicator"],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</button>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
{/* Networks List Revealer */}
|
||||||
|
<revealer
|
||||||
|
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
||||||
|
transitionDuration={300}
|
||||||
|
revealChild={bind(isExpanded)}
|
||||||
|
setup={() => {
|
||||||
|
const clearScanInterval = () => {
|
||||||
|
if (refreshIntervalId.get()) {
|
||||||
|
clearInterval( parseInt( '' + refreshIntervalId.get() ));
|
||||||
|
refreshIntervalId.set(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bind(isExpanded).subscribe((expanded) => {
|
||||||
|
// Clear existing interval
|
||||||
|
clearScanInterval();
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
// Scan networks
|
||||||
|
network.wifi?.scan();
|
||||||
|
|
||||||
|
// Set up new interval if WiFi is enabled
|
||||||
|
if (network.wifi?.enabled) {
|
||||||
|
refreshIntervalId.set(
|
||||||
|
setInterval(() => {
|
||||||
|
scanNetworks();
|
||||||
|
getSavedNetworks();
|
||||||
|
print("updated");
|
||||||
|
}, 10000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Apply revealer bug fix when collapsed
|
||||||
|
App.toggle_window("system-menu");
|
||||||
|
App.toggle_window("system-menu");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor window toggling
|
||||||
|
const windowListener = App.connect("window-toggled", (_, window) => {
|
||||||
|
if (window.name === "system-menu" && isExpanded.get()) {
|
||||||
|
isExpanded.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up resources when component is destroyed
|
||||||
|
return () => {
|
||||||
|
App.disconnect(windowListener);
|
||||||
|
clearScanInterval();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box vertical cssClasses={["network-list"]}>
|
||||||
|
<box visible={showPasswordDialog( v => v )}>
|
||||||
|
<PasswordDialog />
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<label label="Available Networks" cssClasses={["section-label"]} />
|
||||||
|
<label label="No networks found" cssClasses={["empty-label"]} visible={availableNetworks( net => net.length === 0 )}/>
|
||||||
|
<box visible={availableNetworks( networks => networks.length > 1 )}>
|
||||||
|
{availableNetworks( networks =>
|
||||||
|
networks.map( (network) => <NetworkItem network={network} />)
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
|
||||||
|
{savedNetworks((networks) => {
|
||||||
|
// Filter out networks already shown in available networks
|
||||||
|
const filteredNetworks = networks.filter(
|
||||||
|
(ssid) => !availableNetworks.get().some((n) => n.ssid === ssid)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only render the section if there are filtered networks to show
|
||||||
|
return filteredNetworks.length > 0 ? (
|
||||||
|
<box vertical>
|
||||||
|
<label label="Saved Networks" cssClasses={["section-label"]} />
|
||||||
|
{filteredNetworks.map((ssid) => (
|
||||||
|
<box cssClasses={["saved-network"]}>
|
||||||
|
<label label={ssid} />
|
||||||
|
<box hexpand={true} />
|
||||||
|
<button
|
||||||
|
label="Forget"
|
||||||
|
cssClasses={["forget-button", "button"]}
|
||||||
|
onClicked={() => forgetNetwork(ssid)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
) : (
|
||||||
|
<box></box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<box hexpand>
|
||||||
|
<button
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
cssClasses={["refresh-button"]}
|
||||||
|
onClicked={() => {
|
||||||
|
scanNetworks();
|
||||||
|
getSavedNetworks();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<image iconName="view-refresh-symbolic" />
|
||||||
|
</button>
|
||||||
|
{/* Connected Network Options */}
|
||||||
|
<box hexpand>
|
||||||
|
{activeNetwork((active) =>
|
||||||
|
active ? (
|
||||||
|
<box vertical cssClasses={["connected-network"]} hexpand>
|
||||||
|
<button
|
||||||
|
label="Disconnect"
|
||||||
|
cssClasses={["disconnect-button"]}
|
||||||
|
onClicked={() => disconnectNetwork(active.ssid)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
<button
|
||||||
|
cssClasses={["settings-button"]}
|
||||||
|
halign={Gtk.Align.END}
|
||||||
|
hexpand={false}
|
||||||
|
onClicked={() => {
|
||||||
|
execAsync([
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"XDG_CURRENT_DESKTOP=GNOME gnome-control-center wifi",
|
||||||
|
]);
|
||||||
|
isExpanded.set(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<image iconName={"emblem-system-symbolic"} />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</revealer>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
# Source
|
||||||
|
This is a modified version from [MatShell](https://github.com/Neurian/matshell)
|
14
config/astal/components/QuickActions/modules/Networking-old/network.d.ts
vendored
Normal file
14
config/astal/components/QuickActions/modules/Networking-old/network.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import AstalNetwork from "gi://AstalNetwork?version=0.1";
|
||||||
|
|
||||||
|
interface CurrentWiFi {
|
||||||
|
ssid: string;
|
||||||
|
strength: number;
|
||||||
|
secured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WiFiDetails extends CurrentWiFi {
|
||||||
|
active: boolean;
|
||||||
|
accessPoint: AstalNetwork.AccessPoint;
|
||||||
|
iconName: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
|||||||
|
// From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts
|
||||||
|
|
||||||
|
import { execAsync, Variable } from "astal";
|
||||||
|
import Network from "gi://AstalNetwork";
|
||||||
|
import { CurrentWiFi, WiFiDetails } from "./network";
|
||||||
|
|
||||||
|
// State trackers
|
||||||
|
export const availableNetworks: Variable<WiFiDetails[]> = Variable([]);
|
||||||
|
export const savedNetworks: Variable<string[]> = Variable([]);
|
||||||
|
export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null);
|
||||||
|
export const isConnecting: Variable<boolean> = Variable(false);
|
||||||
|
export const showPasswordDialog: Variable<boolean> = Variable(false);
|
||||||
|
export const errorMessage: Variable<string> = Variable("");
|
||||||
|
export const isExpanded: Variable<boolean> = Variable(false);
|
||||||
|
export const passwordInput: Variable<string> = Variable("");
|
||||||
|
export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null);
|
||||||
|
export const refreshIntervalId: Variable<
|
||||||
|
number | null | ReturnType<typeof setTimeout>
|
||||||
|
> = Variable(null);
|
||||||
|
|
||||||
|
// Function to scan for available networks
|
||||||
|
export const scanNetworks = () => {
|
||||||
|
const network = Network.get_default();
|
||||||
|
if (network && network.wifi) {
|
||||||
|
network.wifi.scan();
|
||||||
|
|
||||||
|
// Get available networks from access points
|
||||||
|
const networks: WiFiDetails[] = network.wifi.accessPoints
|
||||||
|
.map(ap => ({
|
||||||
|
ssid: ap.ssid,
|
||||||
|
strength: ap.strength,
|
||||||
|
secured: ap.flags !== 0,
|
||||||
|
active: network.wifi.activeAccessPoint?.ssid === ap.ssid,
|
||||||
|
accessPoint: ap,
|
||||||
|
iconName: ap.iconName,
|
||||||
|
}))
|
||||||
|
.filter(n => n.ssid);
|
||||||
|
|
||||||
|
// Sort by signal strength
|
||||||
|
networks.sort((a, b) => b.strength - a.strength);
|
||||||
|
|
||||||
|
// Remove duplicates (same SSID)
|
||||||
|
const uniqueNetworks: WiFiDetails[] = [];
|
||||||
|
const seen = new Set();
|
||||||
|
networks.forEach(network => {
|
||||||
|
if (!seen.has(network.ssid)) {
|
||||||
|
seen.add(network.ssid);
|
||||||
|
uniqueNetworks.push(network);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
availableNetworks.set(uniqueNetworks);
|
||||||
|
|
||||||
|
// Update active network
|
||||||
|
if (network.wifi.activeAccessPoint) {
|
||||||
|
activeNetwork.set({
|
||||||
|
ssid: network.wifi.activeAccessPoint.ssid,
|
||||||
|
strength: network.wifi.activeAccessPoint.strength,
|
||||||
|
secured: network.wifi.activeAccessPoint.flags !== 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activeNetwork.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to list saved networks
|
||||||
|
export const getSavedNetworks = () => {
|
||||||
|
execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"])
|
||||||
|
.then(output => {
|
||||||
|
if (typeof output === "string") {
|
||||||
|
const savedWifiNetworks = output
|
||||||
|
.split("\n")
|
||||||
|
.filter(line => line.includes("802-11-wireless"))
|
||||||
|
.map(line => line.split(":")[0].trim());
|
||||||
|
savedNetworks.set(savedWifiNetworks);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Error fetching saved networks:", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to connect to a network
|
||||||
|
|
||||||
|
export const connectToNetwork = (
|
||||||
|
ssid: string,
|
||||||
|
password: null | string = null,
|
||||||
|
) => {
|
||||||
|
isConnecting.set(true);
|
||||||
|
errorMessage.set("");
|
||||||
|
const network = Network.get_default();
|
||||||
|
const currentSsid = network.wifi.ssid;
|
||||||
|
|
||||||
|
// Function to perform the actual connection
|
||||||
|
const performConnection = () => {
|
||||||
|
let command = "";
|
||||||
|
if (password) {
|
||||||
|
// Connect with password
|
||||||
|
command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`;
|
||||||
|
} else {
|
||||||
|
// Connect without password (saved or open network)
|
||||||
|
command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
execAsync(["bash", "-c", command])
|
||||||
|
.then(() => {
|
||||||
|
showPasswordDialog.set(false);
|
||||||
|
isConnecting.set(false);
|
||||||
|
scanNetworks(); // Refresh network list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Connection error:", error);
|
||||||
|
errorMessage.set("Failed to connect. Check password.");
|
||||||
|
isConnecting.set(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// If already connected to a network, disconnect first
|
||||||
|
if (currentSsid && currentSsid !== ssid) {
|
||||||
|
console.log(
|
||||||
|
`Disconnecting from ${currentSsid} before connecting to ${ssid}`,
|
||||||
|
);
|
||||||
|
execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`])
|
||||||
|
.then(() => {
|
||||||
|
// Wait a moment for the disconnection to complete fully
|
||||||
|
setTimeout(() => {
|
||||||
|
performConnection();
|
||||||
|
}, 500); // 500ms delay for clean disconnection
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Disconnect error:", error);
|
||||||
|
// Continue with connection attempt even if disconnect fails
|
||||||
|
performConnection();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No active connection or connecting to same network (reconnect case)
|
||||||
|
performConnection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to disconnect from a network
|
||||||
|
export const disconnectNetwork = (ssid: string) => {
|
||||||
|
execAsync(["bash", "-c", `nmcli connection down "${ssid}"`])
|
||||||
|
.then(() => {
|
||||||
|
scanNetworks(); // Refresh network list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Disconnect error:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to forget a saved network
|
||||||
|
export const forgetNetwork = (ssid: string) => {
|
||||||
|
execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`])
|
||||||
|
.then(() => {
|
||||||
|
getSavedNetworks(); // Refresh saved networks list
|
||||||
|
scanNetworks(); // Refresh network list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Forget network error:", error);
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,106 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import networkHelper from "./network-helper";
|
||||||
|
import NetworkMenu from "./NetworkMenu";
|
||||||
|
|
||||||
|
const net = AstalNetwork.get_default();
|
||||||
|
const STATE = AstalNetwork.DeviceState;
|
||||||
|
|
||||||
|
const Network = () => {
|
||||||
|
const netMenu = NetworkMenu.NetworkMenu();
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={networkHelper.networkEnabled(en => {
|
||||||
|
if (en) return ["toggle-button", "toggle-on"];
|
||||||
|
else return ["toggle-button"];
|
||||||
|
})}
|
||||||
|
onClicked={() =>
|
||||||
|
networkHelper.setNetworking(
|
||||||
|
!networkHelper.networkEnabled.get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
child={
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={bind(net.wifi, "enabled").as(
|
||||||
|
stat => `Network (${stat ? "WiFi" : "Wired"})`,
|
||||||
|
)}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<box child=
|
||||||
|
{bind(net, 'wired').as(v => {
|
||||||
|
if (v) {
|
||||||
|
return <label
|
||||||
|
label={bind(net.wired, "state").as(state => {
|
||||||
|
if (state === STATE.ACTIVATED) {
|
||||||
|
return (
|
||||||
|
"Wired. IP: " + networkHelper.getIP()
|
||||||
|
);
|
||||||
|
} else if (state === STATE.DISCONNECTED) {
|
||||||
|
return "Disconnected";
|
||||||
|
} else if (state === STATE.FAILED) {
|
||||||
|
return "Error";
|
||||||
|
} else if (
|
||||||
|
state === STATE.PREPARE ||
|
||||||
|
state === STATE.CONFIG ||
|
||||||
|
state === STATE.IP_CHECK ||
|
||||||
|
state === STATE.IP_CONFIG
|
||||||
|
) {
|
||||||
|
return "Connecting...";
|
||||||
|
} else {
|
||||||
|
return "Unavailable";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
visible={bind(net.wifi, "enabled").as(v => !v)}
|
||||||
|
></label>
|
||||||
|
} else {
|
||||||
|
return <label
|
||||||
|
label={"State unavailable"}
|
||||||
|
visible={bind(net.wifi, "enabled").as(v => !v)}
|
||||||
|
></label>
|
||||||
|
}
|
||||||
|
})}></box>
|
||||||
|
<label
|
||||||
|
label={bind(net.wifi, "state").as(state => {
|
||||||
|
if (state === STATE.ACTIVATED) {
|
||||||
|
return `${net.wifi.get_ssid()} (${networkHelper.getIP()})`;
|
||||||
|
} else if (state === STATE.DISCONNECTED) {
|
||||||
|
return "Disconnected";
|
||||||
|
} else if (state === STATE.FAILED) {
|
||||||
|
return "Error";
|
||||||
|
} else if (
|
||||||
|
state === STATE.PREPARE ||
|
||||||
|
state === STATE.CONFIG ||
|
||||||
|
state === STATE.IP_CHECK ||
|
||||||
|
state === STATE.IP_CONFIG
|
||||||
|
) {
|
||||||
|
return "Connecting...";
|
||||||
|
} else {
|
||||||
|
return "Unavailable";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
visible={bind(net.wifi, "enabled")}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button >
|
||||||
|
<button
|
||||||
|
cssClasses={["actions-button"]}
|
||||||
|
visible={networkHelper.networkEnabled()}
|
||||||
|
onClicked={() => netMenu.popup()}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={"arrow-right-symbolic"}></image>
|
||||||
|
{netMenu}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
tooltipText={"View available devices"}
|
||||||
|
></button>
|
||||||
|
</box >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Network,
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
const NetworkMenu = () => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
popover.set_child( renderMenu() );
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMenu = () => {
|
||||||
|
return <box vertical>
|
||||||
|
<image iconName={"appointment-soon-symbolic"} iconSize={Gtk.IconSize.LARGE}></image>
|
||||||
|
<label label={"Coming later"}></label>
|
||||||
|
</box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
NetworkMenu,
|
||||||
|
};
|
91
config/astal/components/QuickActions/modules/Networking/dump
Normal file
91
config/astal/components/QuickActions/modules/Networking/dump
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import networkHelper from "./network-helper";
|
||||||
|
|
||||||
|
const net = AstalNetwork.get_default();
|
||||||
|
const STATE = AstalNetwork.DeviceState;
|
||||||
|
|
||||||
|
const WiFiList = () => {
|
||||||
|
const popover = new Gtk.Popover({ cssClasses: ["WiFiPicker"] });
|
||||||
|
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderWiFiList = () => {
|
||||||
|
return <box>
|
||||||
|
<label label="Test"></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Network = () => {
|
||||||
|
const wifiList = WiFiList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={networkHelper.networkEnabled(en => {
|
||||||
|
if (en) return ["network-button", "net-on"];
|
||||||
|
else return ["network-button"];
|
||||||
|
})}
|
||||||
|
onClicked={() =>
|
||||||
|
networkHelper.setNetworking(
|
||||||
|
!networkHelper.networkEnabled.get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
child={<box vertical>
|
||||||
|
<label label="Wired" cssClasses={[ 'button-name' ]}></label>
|
||||||
|
<label label={bind( net.wired, 'state' ).as( state => {
|
||||||
|
if ( state === STATE.ACTIVATED ) {
|
||||||
|
return 'Connected. IP: ' + networkHelper.getIP();
|
||||||
|
} else if ( state === STATE.DISCONNECTED ) {
|
||||||
|
return 'Disconnected';
|
||||||
|
} else if ( state === STATE.FAILED ) {
|
||||||
|
return 'Error';
|
||||||
|
} else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) {
|
||||||
|
return 'Connecting...';
|
||||||
|
} else {
|
||||||
|
return 'Unavailable';
|
||||||
|
}
|
||||||
|
} )}></label>
|
||||||
|
</box>}
|
||||||
|
></button>
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={bind(net.wifi, "enabled").as(b => {
|
||||||
|
const classes = ["network-button"];
|
||||||
|
if (b) {
|
||||||
|
classes.push("wifi-on");
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
})}
|
||||||
|
child={<box vertical>
|
||||||
|
<label label="WiFi" cssClasses={[ 'button-name' ]}></label>
|
||||||
|
<label label={bind( net.wifi, 'state' ).as( state => {
|
||||||
|
if ( state === STATE.ACTIVATED ) {
|
||||||
|
return 'Connected. IP: ' + networkHelper.getIP();
|
||||||
|
} else if ( state === STATE.DISCONNECTED ) {
|
||||||
|
return 'Disconnected';
|
||||||
|
} else if ( state === STATE.FAILED ) {
|
||||||
|
return 'Error';
|
||||||
|
} else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) {
|
||||||
|
return 'Connecting...';
|
||||||
|
} else {
|
||||||
|
return 'Unavailable';
|
||||||
|
}
|
||||||
|
} )} visible={bind(net.wifi, 'enabled').as( en => en )}></label>
|
||||||
|
<label label="Disabled" visible={bind(net.wifi, 'enabled').as( en => !en )}></label>
|
||||||
|
</box>}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["network-button-context"]}
|
||||||
|
visible={bind(net.wifi, "enabled").as(b => b)}
|
||||||
|
onClicked={() => wifiList.popup()}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
{wifiList}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Network;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { exec, Variable } from "astal";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
|
||||||
|
const networkEnabled = Variable( exec( 'nmcli networking connectivity' ) !== 'none' );
|
||||||
|
const network = AstalNetwork.get_default();
|
||||||
|
|
||||||
|
|
||||||
|
const setNetworking = ( status: boolean ) => {
|
||||||
|
if ( status === true ) {
|
||||||
|
exec( 'nmcli networking on' );
|
||||||
|
networkEnabled.set( true );
|
||||||
|
} else {
|
||||||
|
exec( 'nmcli networking off' );
|
||||||
|
networkEnabled.set( false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getIP = () => {
|
||||||
|
return exec( `/bin/bash -c "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'"` ).split( '/' )[ 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
networkEnabled,
|
||||||
|
setNetworking,
|
||||||
|
getIP
|
||||||
|
}
|
14
config/astal/components/QuickActions/modules/Networking/network.d.ts
vendored
Normal file
14
config/astal/components/QuickActions/modules/Networking/network.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import AstalNetwork from "gi://AstalNetwork?version=0.1";
|
||||||
|
|
||||||
|
interface CurrentWiFi {
|
||||||
|
ssid: string;
|
||||||
|
strength: number;
|
||||||
|
secured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WiFiDetails extends CurrentWiFi {
|
||||||
|
active: boolean;
|
||||||
|
accessPoint: AstalNetwork.AccessPoint;
|
||||||
|
iconName: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
|||||||
|
// From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts
|
||||||
|
|
||||||
|
import { execAsync, Variable } from "astal";
|
||||||
|
import Network from "gi://AstalNetwork";
|
||||||
|
import { CurrentWiFi, WiFiDetails } from "./network";
|
||||||
|
|
||||||
|
// State trackers
|
||||||
|
export const availableNetworks: Variable<WiFiDetails[]> = Variable([]);
|
||||||
|
export const savedNetworks: Variable<string[]> = Variable([]);
|
||||||
|
export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null);
|
||||||
|
export const isConnecting: Variable<boolean> = Variable(false);
|
||||||
|
export const showPasswordDialog: Variable<boolean> = Variable(false);
|
||||||
|
export const errorMessage: Variable<string> = Variable("");
|
||||||
|
export const isExpanded: Variable<boolean> = Variable(false);
|
||||||
|
export const passwordInput: Variable<string> = Variable("");
|
||||||
|
export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null);
|
||||||
|
export const refreshIntervalId: Variable<
|
||||||
|
number | null | ReturnType<typeof setTimeout>
|
||||||
|
> = Variable(null);
|
||||||
|
|
||||||
|
// Function to scan for available networks
|
||||||
|
export const scanNetworks = () => {
|
||||||
|
const network = Network.get_default();
|
||||||
|
if (network && network.wifi) {
|
||||||
|
network.wifi.scan();
|
||||||
|
|
||||||
|
// Get available networks from access points
|
||||||
|
const networks: WiFiDetails[] = network.wifi.accessPoints
|
||||||
|
.map(ap => ({
|
||||||
|
ssid: ap.ssid,
|
||||||
|
strength: ap.strength,
|
||||||
|
secured: ap.flags !== 0,
|
||||||
|
active: network.wifi.activeAccessPoint?.ssid === ap.ssid,
|
||||||
|
accessPoint: ap,
|
||||||
|
iconName: ap.iconName,
|
||||||
|
}))
|
||||||
|
.filter(n => n.ssid);
|
||||||
|
|
||||||
|
// Sort by signal strength
|
||||||
|
networks.sort((a, b) => b.strength - a.strength);
|
||||||
|
|
||||||
|
// Remove duplicates (same SSID)
|
||||||
|
const uniqueNetworks: WiFiDetails[] = [];
|
||||||
|
const seen = new Set();
|
||||||
|
networks.forEach(network => {
|
||||||
|
if (!seen.has(network.ssid)) {
|
||||||
|
seen.add(network.ssid);
|
||||||
|
uniqueNetworks.push(network);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
availableNetworks.set(uniqueNetworks);
|
||||||
|
|
||||||
|
// Update active network
|
||||||
|
if (network.wifi.activeAccessPoint) {
|
||||||
|
activeNetwork.set({
|
||||||
|
ssid: network.wifi.activeAccessPoint.ssid,
|
||||||
|
strength: network.wifi.activeAccessPoint.strength,
|
||||||
|
secured: network.wifi.activeAccessPoint.flags !== 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activeNetwork.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to list saved networks
|
||||||
|
export const getSavedNetworks = () => {
|
||||||
|
execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"])
|
||||||
|
.then(output => {
|
||||||
|
if (typeof output === "string") {
|
||||||
|
const savedWifiNetworks = output
|
||||||
|
.split("\n")
|
||||||
|
.filter(line => line.includes("802-11-wireless"))
|
||||||
|
.map(line => line.split(":")[0].trim());
|
||||||
|
savedNetworks.set(savedWifiNetworks);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Error fetching saved networks:", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to connect to a network
|
||||||
|
|
||||||
|
export const connectToNetwork = (
|
||||||
|
ssid: string,
|
||||||
|
password: null | string = null,
|
||||||
|
) => {
|
||||||
|
isConnecting.set(true);
|
||||||
|
errorMessage.set("");
|
||||||
|
const network = Network.get_default();
|
||||||
|
const currentSsid = network.wifi.ssid;
|
||||||
|
|
||||||
|
// Function to perform the actual connection
|
||||||
|
const performConnection = () => {
|
||||||
|
let command = "";
|
||||||
|
if (password) {
|
||||||
|
// Connect with password
|
||||||
|
command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`;
|
||||||
|
} else {
|
||||||
|
// Connect without password (saved or open network)
|
||||||
|
command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
execAsync(["bash", "-c", command])
|
||||||
|
.then(() => {
|
||||||
|
showPasswordDialog.set(false);
|
||||||
|
isConnecting.set(false);
|
||||||
|
scanNetworks(); // Refresh network list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Connection error:", error);
|
||||||
|
errorMessage.set("Failed to connect. Check password.");
|
||||||
|
isConnecting.set(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// If already connected to a network, disconnect first
|
||||||
|
if (currentSsid && currentSsid !== ssid) {
|
||||||
|
console.log(
|
||||||
|
`Disconnecting from ${currentSsid} before connecting to ${ssid}`,
|
||||||
|
);
|
||||||
|
execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`])
|
||||||
|
.then(() => {
|
||||||
|
// Wait a moment for the disconnection to complete fully
|
||||||
|
setTimeout(() => {
|
||||||
|
performConnection();
|
||||||
|
}, 500); // 500ms delay for clean disconnection
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Disconnect error:", error);
|
||||||
|
// Continue with connection attempt even if disconnect fails
|
||||||
|
performConnection();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No active connection or connecting to same network (reconnect case)
|
||||||
|
performConnection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to disconnect from a network
|
||||||
|
export const disconnectNetwork = (ssid: string) => {
|
||||||
|
execAsync(["bash", "-c", `nmcli connection down "${ssid}"`])
|
||||||
|
.then(() => {
|
||||||
|
scanNetworks(); // Refresh network list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Disconnect error:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to forget a saved network
|
||||||
|
export const forgetNetwork = (ssid: string) => {
|
||||||
|
execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`])
|
||||||
|
.then(() => {
|
||||||
|
getSavedNetworks(); // Refresh saved networks list
|
||||||
|
scanNetworks(); // Refresh network list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Forget network error:", error);
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,56 @@
|
|||||||
|
$fg-color: #{"@theme_fg_color"};
|
||||||
|
$bg-color: #{"@theme_bg_color"};
|
||||||
|
|
||||||
|
box.players-box {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.player {
|
||||||
|
padding: 0.6rem;
|
||||||
|
|
||||||
|
.cover-art {
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 9px;
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
scale {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
trough {
|
||||||
|
min-height: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight {
|
||||||
|
background-color: $fg-color;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
slider {
|
||||||
|
all: unset;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
centerbox.actions {
|
||||||
|
min-width: 220px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0.4rem;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
config/astal/components/QuickActions/modules/Player/Player.tsx
Normal file
155
config/astal/components/QuickActions/modules/Player/Player.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalMpris from "gi://AstalMpris";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
const ALIGN = Gtk.Align;
|
||||||
|
|
||||||
|
const mpris = AstalMpris.get_default();
|
||||||
|
mpris.connect("player-added", p => {
|
||||||
|
print("Player added:", p);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PlayerModule = () => {
|
||||||
|
return (
|
||||||
|
<box vertical cssClasses={ [ 'players-box' ] }>
|
||||||
|
<label label={"Music Players"} halign={ALIGN.CENTER} cssClasses={[ 'title-2' ]}></label>
|
||||||
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
||||||
|
<box cssClasses={["players"]}>
|
||||||
|
{bind(mpris, "players").as(players => {
|
||||||
|
return players.map(player => {
|
||||||
|
return <PlayerItem player={player}></PlayerItem>;
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
<label label={"No playback active"} visible={bind(mpris, "players").as( players => players.length === 0 )}></label>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Update widths
|
||||||
|
const pbStatus = AstalMpris.PlaybackStatus;
|
||||||
|
const PlayerItem = ({ player }: { player: AstalMpris.Player }) => {
|
||||||
|
return (
|
||||||
|
<box cssClasses={["player"]}>
|
||||||
|
<image
|
||||||
|
cssClasses={["cover-art"]}
|
||||||
|
file={bind(player, "coverArt")}
|
||||||
|
hexpand
|
||||||
|
vexpand
|
||||||
|
></image>
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={bind(player, "title").as(
|
||||||
|
title => title ?? "Unknown title",
|
||||||
|
)}
|
||||||
|
cssClasses={["title"]}
|
||||||
|
halign={ALIGN.START}
|
||||||
|
valign={ALIGN.START}
|
||||||
|
maxWidthChars={30}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
label={bind(player, "artist").as(
|
||||||
|
artist => artist ?? "Unknown artist",
|
||||||
|
)}
|
||||||
|
halign={ALIGN.START}
|
||||||
|
valign={ALIGN.START}
|
||||||
|
maxWidthChars={30}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
></label>
|
||||||
|
<slider
|
||||||
|
visible={bind(player, "length").as(l => l > 0)}
|
||||||
|
value={bind(player, "position")}
|
||||||
|
min={0}
|
||||||
|
max={bind(player, "length")}
|
||||||
|
onChangeValue={v =>
|
||||||
|
player.set_position(v.get_value())
|
||||||
|
}
|
||||||
|
></slider>
|
||||||
|
<centerbox
|
||||||
|
cssClasses={["actions"]}
|
||||||
|
startWidget={
|
||||||
|
<label
|
||||||
|
label={bind(player, "position").as(v =>
|
||||||
|
secondsToFriendlyTime(v),
|
||||||
|
)}
|
||||||
|
hexpand
|
||||||
|
cssClasses={["position"]}
|
||||||
|
></label>
|
||||||
|
}
|
||||||
|
centerWidget={
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
visible={bind(player, "canGoPrevious")}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={
|
||||||
|
"media-skip-backward-symbolic"
|
||||||
|
}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
onClicked={() => player.previous()}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
visible={bind(player, "canControl")}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={bind(
|
||||||
|
player,
|
||||||
|
"playbackStatus",
|
||||||
|
).as(status => {
|
||||||
|
if (status === pbStatus.PLAYING) {
|
||||||
|
return "media-playback-pause-symbolic";
|
||||||
|
} else {
|
||||||
|
return "media-playback-start-symbolic";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
onClicked={() => player.play_pause()}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
visible={bind(player, "canGoNext")}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={"media-skip-forward-symbolic"}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
onClicked={() => player.next()}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
endWidget={
|
||||||
|
<label
|
||||||
|
cssClasses={["length"]}
|
||||||
|
hexpand
|
||||||
|
label={bind(player, "length").as(v =>
|
||||||
|
secondsToFriendlyTime(v),
|
||||||
|
)}
|
||||||
|
></label>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondsToFriendlyTime = (time: number) => {
|
||||||
|
const m = Math.floor(time / 60);
|
||||||
|
const minutes = Math.floor(m % 60);
|
||||||
|
const hours = Math.floor(m / 60 % 24);
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${expandTime(minutes)}:${expandTime(seconds)}`;
|
||||||
|
} else {
|
||||||
|
return `${minutes}:${expandTime(seconds)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandTime = (time: number): string => {
|
||||||
|
return time < 10 ? `0${time}` : "" + time;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
PlayerModule,
|
||||||
|
};
|
85
config/astal/components/QuickActions/modules/Power.tsx
Normal file
85
config/astal/components/QuickActions/modules/Power.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { exec } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
const PowerMenu = (): Gtk.Popover => {
|
||||||
|
const popover = new Gtk.Popover({ cssClasses: ["PowerMenu"] });
|
||||||
|
|
||||||
|
const powerMenuBox = () => {
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={
|
||||||
|
<image iconName={"system-shutdown-symbolic"}></image>
|
||||||
|
}
|
||||||
|
onClicked={() => exec("/bin/sh -c 'shutdown now'")}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={<image iconName={"system-reboot-symbolic"}></image>}
|
||||||
|
onClicked={() => exec("/bin/sh -c 'reboot'")}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={<image iconName={"system-suspend-symbolic"}></image>}
|
||||||
|
onClicked={() => exec("/bin/sh -c 'systemctl suspend'")}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
popover.set_child(powerMenuBox());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Power = () => {
|
||||||
|
const pm = PowerMenu();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
widthRequest={0}
|
||||||
|
hexpand={false}
|
||||||
|
vexpand={false}
|
||||||
|
cssClasses={["power-menu-button"]}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={"system-shutdown-symbolic"}></image>
|
||||||
|
{pm}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
onClicked={() => pm.popup()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserMenu = (): Gtk.Popover => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
const powerMenuBox = () => {
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={
|
||||||
|
<image iconName={"system-lock-screen-symbolic"}></image>
|
||||||
|
}
|
||||||
|
onClicked={() => exec("/bin/sh -c 'hyprlock'")}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={<image iconName={"system-log-out-symbolic"}></image>}
|
||||||
|
onClicked={() =>
|
||||||
|
exec("/bin/sh -c 'hyprctl dispatch exit 0'")
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
popover.set_child(powerMenuBox());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Power,
|
||||||
|
UserMenu
|
||||||
|
};
|
56
config/astal/components/QuickActions/quickactions.scss
Normal file
56
config/astal/components/QuickActions/quickactions.scss
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
@use "./modules/Player/Player.scss";
|
||||||
|
@use "./modules/Audio/Audio.scss";
|
||||||
|
@use "../../util/colours.scss" as *;
|
||||||
|
|
||||||
|
.quick-actions-wrapper {
|
||||||
|
min-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.quick-actions {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
popover * {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-no-margin {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toggle-button {
|
||||||
|
min-width: 220px;
|
||||||
|
border-radius: 50px;
|
||||||
|
|
||||||
|
&.toggle-on {
|
||||||
|
min-width: 190px;
|
||||||
|
margin-right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.actions-button {
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: $accent-color;
|
||||||
|
border-top-right-radius: 50px;
|
||||||
|
border-bottom-right-radius: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon {
|
||||||
|
border-radius: 100px;
|
||||||
|
min-height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
70
config/astal/components/bar/Bar.tsx
Normal file
70
config/astal/components/bar/Bar.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { App, Astal, Gdk, Gtk } from "astal/gtk4";
|
||||||
|
import Hyprland from "./modules/Hyprland";
|
||||||
|
import Calendar from "./modules/Calendar";
|
||||||
|
import QuickView from "./modules/QuickView";
|
||||||
|
import SystemInfo from "./modules/SystemInfo";
|
||||||
|
import { CenterBox } from "astal/gtk4/widget";
|
||||||
|
|
||||||
|
const Bar = ( { gdkmonitor, name }: { gdkmonitor: Gdk.Monitor, name: string } ) => {
|
||||||
|
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window
|
||||||
|
gdkmonitor={gdkmonitor}
|
||||||
|
cssClasses={["Bar"]}
|
||||||
|
name={name}
|
||||||
|
namespace={"bar"}
|
||||||
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
|
anchor={TOP | LEFT | RIGHT}
|
||||||
|
visible
|
||||||
|
application={App}
|
||||||
|
child={
|
||||||
|
<CenterBox
|
||||||
|
orientation={Gtk.Orientation.HORIZONTAL}
|
||||||
|
start_widget={
|
||||||
|
<box
|
||||||
|
hexpand
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
>
|
||||||
|
<Calendar.Time />
|
||||||
|
<SystemInfo.SystemInfo />
|
||||||
|
<Hyprland.Workspace />
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
centerWidget={<Hyprland.ActiveWindow />}
|
||||||
|
endWidget={
|
||||||
|
<box
|
||||||
|
hexpand
|
||||||
|
halign={Gtk.Align.END}
|
||||||
|
cssClasses={["BarRight"]}
|
||||||
|
>
|
||||||
|
<Hyprland.SysTray />
|
||||||
|
<QuickView.QuickView />
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></CenterBox>
|
||||||
|
}
|
||||||
|
></window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cliHandler = (args: string[]): string => {
|
||||||
|
return "Not implemented";
|
||||||
|
};
|
||||||
|
|
||||||
|
const BarLauncher = ( monitor: Gdk.Monitor ) => {
|
||||||
|
const windowName = `bar-${monitor.get_connector()}`
|
||||||
|
const createBar = () => {
|
||||||
|
return <Bar gdkmonitor={monitor} name={windowName}></Bar>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually start the bar
|
||||||
|
createBar();
|
||||||
|
|
||||||
|
return windowName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
BarLauncher,
|
||||||
|
cliHandler,
|
||||||
|
};
|
66
config/astal/components/bar/bar.scss
Normal file
66
config/astal/components/bar/bar.scss
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
@use "../../util/colours.scss" as *;
|
||||||
|
|
||||||
|
window.Bar {
|
||||||
|
font-family: "Comfortaa, sans-serif";
|
||||||
|
background: transparent;
|
||||||
|
color: $fg-color;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
/* >centerbox { */
|
||||||
|
/* background: $bg-color; */
|
||||||
|
/* border-radius: 10px; */
|
||||||
|
/* margin: 8px; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
.bar-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 2px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
background-color: $bg-color;
|
||||||
|
|
||||||
|
& button {
|
||||||
|
background-color: $bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 2px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
background-color: $bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.workspace-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 1px;
|
||||||
|
|
||||||
|
&.focused-workspace-button {
|
||||||
|
color: $accent-color-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray-item {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& button {
|
||||||
|
margin: 2px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
min-width: 11rem;
|
||||||
|
padding: 3px;
|
||||||
|
& button {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-view-symbol {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
}
|
26
config/astal/components/bar/modules/Calendar.tsx
Normal file
26
config/astal/components/bar/modules/Calendar.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GLib, Variable } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
const Time = ({ format = "%a, %e.%m %H:%M:%S" }) => {
|
||||||
|
const time = Variable<string>("").poll(
|
||||||
|
1000,
|
||||||
|
() => GLib.DateTime.new_now_local().format(format)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<menubutton
|
||||||
|
cssClasses={["time", "bar-button"]}
|
||||||
|
hexpand
|
||||||
|
halign={Gtk.Align.CENTER}
|
||||||
|
>
|
||||||
|
<label onDestroy={() => time.drop()} label={time()} halign={Gtk.Align.CENTER}></label>
|
||||||
|
<popover>
|
||||||
|
<Gtk.Calendar />
|
||||||
|
</popover>
|
||||||
|
</menubutton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Time,
|
||||||
|
};
|
147
config/astal/components/bar/modules/Hyprland.tsx
Normal file
147
config/astal/components/bar/modules/Hyprland.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import AstalTray from "gi://AstalTray";
|
||||||
|
import { bind, GObject } from "astal";
|
||||||
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
const hypr = AstalHyprland.get_default();
|
||||||
|
const SYNC = GObject.BindingFlags.SYNC_CREATE;
|
||||||
|
|
||||||
|
const SysTray = () => {
|
||||||
|
const trayBox = new Gtk.Box({ cssClasses: ["bar-button"] });
|
||||||
|
const tray = AstalTray.get_default();
|
||||||
|
|
||||||
|
const trayItems = new Map<string, Gtk.MenuButton>();
|
||||||
|
const trayAddedHandler = tray.connect("item-added", (_, id) => {
|
||||||
|
const item = tray.get_item(id);
|
||||||
|
const popover = Gtk.PopoverMenu.new_from_model(item.menu_model);
|
||||||
|
const icon = new Gtk.Image();
|
||||||
|
const button = new Gtk.MenuButton({
|
||||||
|
popover,
|
||||||
|
child: icon,
|
||||||
|
cssClasses: ["tray-item"],
|
||||||
|
});
|
||||||
|
|
||||||
|
item.bind_property("gicon", icon, "gicon", SYNC);
|
||||||
|
popover.insert_action_group("dbusmenu", item.action_group);
|
||||||
|
item.connect("notify::action-group", () => {
|
||||||
|
popover.insert_action_group("dbusmenu", item.action_group);
|
||||||
|
});
|
||||||
|
|
||||||
|
trayItems.set(id, button);
|
||||||
|
trayBox.append(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
const trayRemovedHandler = tray.connect("item-removed", (_, id) => {
|
||||||
|
const button = trayItems.get(id);
|
||||||
|
if (button) {
|
||||||
|
trayBox.remove(button);
|
||||||
|
button.run_dispose();
|
||||||
|
trayItems.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trayBox.connect("destroy", () => {
|
||||||
|
tray.disconnect(trayAddedHandler);
|
||||||
|
tray.disconnect(trayRemovedHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
return trayBox;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Workspace = () => {
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
{bind(hypr, "workspaces").as(wss =>
|
||||||
|
wss
|
||||||
|
.filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
.map(ws => (
|
||||||
|
<button
|
||||||
|
cssClasses={bind(hypr, "focusedWorkspace").as(fw =>
|
||||||
|
ws === fw
|
||||||
|
? [
|
||||||
|
"focused-workspace-button",
|
||||||
|
"workspace-button",
|
||||||
|
]
|
||||||
|
: ["workspace-button"],
|
||||||
|
)}
|
||||||
|
onButtonPressed={() => ws.focus()}
|
||||||
|
child={<label label={String(ws.id)}></label>}
|
||||||
|
></button>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the name of the currently active window and provides a popover for
|
||||||
|
* displaying all available clients
|
||||||
|
*/
|
||||||
|
const ActiveWindow = () => {
|
||||||
|
const focused = bind(hypr, "focusedClient");
|
||||||
|
|
||||||
|
const WindowPopover = (): Gtk.Popover => {
|
||||||
|
// Set up boxes + Popover
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
const popoverBox = WindowPopoverBox();
|
||||||
|
|
||||||
|
popover.set_child(popoverBox);
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const windowPopover = WindowPopover();
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// Return fully assembled HyprlandFocusedClient box
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<box visible={focused.as(Boolean)}>
|
||||||
|
<button
|
||||||
|
onClicked={() => windowPopover.popup()}
|
||||||
|
cssClasses={["bar-button"]}
|
||||||
|
child={
|
||||||
|
focused.as(
|
||||||
|
client =>
|
||||||
|
client && (
|
||||||
|
<label
|
||||||
|
maxWidthChars={40}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
label={bind(client, "title").as(String)} />
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}></button>
|
||||||
|
{windowPopover}
|
||||||
|
</box >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WindowPopoverBox = () => {
|
||||||
|
return <box vertical>
|
||||||
|
<label label={"Available Windows"} cssClasses={['title-2']}></label>
|
||||||
|
<Gtk.Separator marginTop={5} marginBottom={5}></Gtk.Separator>
|
||||||
|
<box vertical>
|
||||||
|
{bind(hypr, 'clients').as(clients => {
|
||||||
|
return clients.map(client => {
|
||||||
|
return <button child={
|
||||||
|
<box>
|
||||||
|
<label label={bind(client, 'workspace').as(w => `(WS ${w.name})`)}></label>
|
||||||
|
<label label={bind(client, 'initialClass').as(c => `[${c}]`)}></label>
|
||||||
|
<label label={bind(client, 'title')}></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
onClicked={() => client.focus()}
|
||||||
|
></button>
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Workspace,
|
||||||
|
ActiveWindow,
|
||||||
|
SysTray,
|
||||||
|
};
|
188
config/astal/components/bar/modules/QuickView.tsx
Normal file
188
config/astal/components/bar/modules/QuickView.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import AstalBattery from "gi://AstalBattery";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import AstalWp from "gi://AstalWp";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Brightness from "../../../util/brightness";
|
||||||
|
import QuickActions from "../../QuickActions/QuickActions";
|
||||||
|
|
||||||
|
const STATE = AstalNetwork.DeviceState;
|
||||||
|
|
||||||
|
const QuickView = () => {
|
||||||
|
const qa = QuickActions.QuickActions();
|
||||||
|
const showQuickActions = () => {
|
||||||
|
qa.popup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClicked={() => showQuickActions()}
|
||||||
|
cssClasses={["quick-action-button"]}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<BatteryWidget></BatteryWidget>
|
||||||
|
<Audio></Audio>
|
||||||
|
<BluetoothWidget></BluetoothWidget>
|
||||||
|
<NetworkWidget></NetworkWidget>
|
||||||
|
<image iconName={"system-shutdown-symbolic"}></image>
|
||||||
|
{qa}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NetworkWidget = () => {
|
||||||
|
const network = AstalNetwork.get_default();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<image
|
||||||
|
iconName={bind(network, "state").as(state => {
|
||||||
|
if (state === AstalNetwork.State.CONNECTING) {
|
||||||
|
return "chronometer-reset-symbolic";
|
||||||
|
} else if (
|
||||||
|
state === AstalNetwork.State.CONNECTED_LOCAL ||
|
||||||
|
state === AstalNetwork.State.CONNECTED_SITE ||
|
||||||
|
state === AstalNetwork.State.CONNECTED_GLOBAL
|
||||||
|
) {
|
||||||
|
return "network-wired-activated-symbolic";
|
||||||
|
} else {
|
||||||
|
return "paint-unknown-symbolic";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
cssClasses={["network-widget", "quick-view-symbol"]}
|
||||||
|
visible={bind(network.wifi, "state").as(
|
||||||
|
state => state !== STATE.ACTIVATED,
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
<image
|
||||||
|
iconName={bind(network.wifi, "state").as(state => {
|
||||||
|
if (state === STATE.ACTIVATED) {
|
||||||
|
return network.wifi.iconName;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
tooltipText={bind(network.wifi, 'ssid')}
|
||||||
|
cssClasses={["network-widget", "quick-view-symbol"]}
|
||||||
|
visible={bind(network.wifi, "state").as(
|
||||||
|
state => state === STATE.ACTIVATED,
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BluetoothWidget = () => {
|
||||||
|
const bluetooth = AstalBluetooth.get_default();
|
||||||
|
const enabled = bind(bluetooth, "isPowered");
|
||||||
|
const connected = bind(bluetooth, "isConnected");
|
||||||
|
|
||||||
|
// For each connected BT device, render status
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<box visible={enabled.as(e => e)}>
|
||||||
|
<image
|
||||||
|
iconName={"bluetooth-active-symbolic"}
|
||||||
|
visible={connected.as(c => c)}
|
||||||
|
></image>
|
||||||
|
<image
|
||||||
|
iconName={"bluetooth-disconnected-symbolic"}
|
||||||
|
visible={connected.as(c => !c)}
|
||||||
|
></image>
|
||||||
|
</box>
|
||||||
|
<image
|
||||||
|
iconName={"bluetooth-disabled-symbolic"}
|
||||||
|
visible={enabled.as(e => !e)}
|
||||||
|
></image>
|
||||||
|
<box>
|
||||||
|
{bind(bluetooth, "devices").as(devices => {
|
||||||
|
return devices.map(device => {
|
||||||
|
return (
|
||||||
|
<image
|
||||||
|
iconName={bind(device, "icon").as(
|
||||||
|
icon => icon,
|
||||||
|
)}
|
||||||
|
visible={bind(device, "connected")}
|
||||||
|
tooltipText={bind(device, "batteryPercentage").as(
|
||||||
|
n => {
|
||||||
|
return device.get_name() + ': ' + n + "%";
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BatteryWidget = () => {
|
||||||
|
const battery = AstalBattery.get_default();
|
||||||
|
if (battery.get_is_present()) {
|
||||||
|
return (
|
||||||
|
<image
|
||||||
|
iconName={bind(battery, "batteryIconName").as(icon => icon)}
|
||||||
|
cssClasses={["quick-view-symbol"]}
|
||||||
|
tooltipText={bind(battery, 'percentage').as(p => `Battery Level: ${Math.round(p * 100)}%`)}
|
||||||
|
></image>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <box></box>;
|
||||||
|
}
|
||||||
|
// Else, no battery available -> Don't show the widget
|
||||||
|
};
|
||||||
|
|
||||||
|
const BrightnessWidget = () => {
|
||||||
|
const brightness = Brightness.get_default();
|
||||||
|
const screen_brightness = bind(brightness, "screen");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box cssClasses={["quick-view-symbol"]}>
|
||||||
|
<image iconName={"brightness-high-symbolic"}></image>
|
||||||
|
<label
|
||||||
|
label={screen_brightness.as(b => '' + Math.round(100 * b))}
|
||||||
|
visible={bind(brightness, "screenAvailable")}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Audio = () => {
|
||||||
|
const wireplumber = AstalWp.get_default();
|
||||||
|
if (wireplumber) {
|
||||||
|
return (
|
||||||
|
<box orientation={Gtk.Orientation.HORIZONTAL}>
|
||||||
|
<image
|
||||||
|
iconName={bind(wireplumber.defaultSpeaker, "volumeIcon").as(
|
||||||
|
icon => icon,
|
||||||
|
)}
|
||||||
|
cssClasses={["quick-view-symbol"]}
|
||||||
|
tooltipText={bind(wireplumber.defaultSpeaker, 'volume').as(v => Math.round(100 * v) + '%')}
|
||||||
|
></image>
|
||||||
|
<image
|
||||||
|
iconName={bind(
|
||||||
|
wireplumber.defaultMicrophone,
|
||||||
|
"volumeIcon",
|
||||||
|
).as(icon => icon)}
|
||||||
|
cssClasses={["quick-view-symbol"]}
|
||||||
|
tooltipText={bind(wireplumber.defaultMicrophone, 'volume').as(v => Math.round(100 * v) + '%')}
|
||||||
|
></image>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
"[ WirePlumber ] Could not connect, Audio support in bar will be missing",
|
||||||
|
);
|
||||||
|
return <image iconName={"action-unavailable-symbolic"}></image>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// cssClasses={[ 'quick-view-symbol' ]}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
QuickView,
|
||||||
|
};
|
101
config/astal/components/bar/modules/SystemInfo.tsx
Normal file
101
config/astal/components/bar/modules/SystemInfo.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { execAsync } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import sysinfo from "../sysinfo";
|
||||||
|
|
||||||
|
const info = () => {
|
||||||
|
return (
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={"System Information"}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginTop={5} marginBottom={10}></Gtk.Separator>
|
||||||
|
<label
|
||||||
|
vexpand
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
hexpand
|
||||||
|
label={sysinfo.ramUsed(used => {
|
||||||
|
return "RAM: " + used + ` (${sysinfo.ramUtil.get()}%)`;
|
||||||
|
})}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
label={sysinfo.systemStats(stats => {
|
||||||
|
return `CPU: ${stats.cpuTemp}, ${stats.cpuClk}
|
||||||
|
GPU: ${stats.gpuTemp}, ${stats.gpuClk} (${stats.vram} / ${stats.availableVRAM})
|
||||||
|
Kernel: ${stats.kernel}`;
|
||||||
|
})}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginTop={10}></Gtk.Separator>
|
||||||
|
<button
|
||||||
|
onClicked={() => execAsync(`/bin/sh -c "kitty --hold fish -c 'fastfetch'"`)}
|
||||||
|
child={
|
||||||
|
<label label={"View FastFetch"}></label>
|
||||||
|
}></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemInformationPanel = () => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
popover.set_child(info());
|
||||||
|
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const panel = SystemInformationPanel();
|
||||||
|
|
||||||
|
const SystemInfo = () => {
|
||||||
|
sysinfo.startSysInfoFetcher();
|
||||||
|
|
||||||
|
const openSysInfo = async () => {
|
||||||
|
panel.popup();
|
||||||
|
sysinfo.refreshStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sysinfo.enabled) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClicked={() => openSysInfo()}
|
||||||
|
child={
|
||||||
|
<box tooltipText={sysinfo.ramUsed(v => v)}>
|
||||||
|
<box
|
||||||
|
cssClasses={[ 'quick-view-symbol' ]}
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
iconName={"power-profile-performance-symbolic"}
|
||||||
|
marginEnd={1}
|
||||||
|
></image>
|
||||||
|
<label
|
||||||
|
label={sysinfo.cpuUtil(util => util)}
|
||||||
|
marginEnd={5}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
cssClasses={[ 'quick-view-symbol' ]}
|
||||||
|
>
|
||||||
|
<image iconName={"histogram-symbolic"}></image>
|
||||||
|
<label label={sysinfo.ramUtil(util => util)}></label>
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
cssClasses={[ 'quick-view-symbol' ]}
|
||||||
|
>
|
||||||
|
<image iconName={"show-gpu-effects-symbolic"}></image>
|
||||||
|
<label label={sysinfo.gpuUtil(util => util)}></label>
|
||||||
|
</box>
|
||||||
|
{panel}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
cssClasses={["bar-button"]}
|
||||||
|
></button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <image iconName={"action-unavailable-symbolic"}></image>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
SystemInfo,
|
||||||
|
panel,
|
||||||
|
};
|
9
config/astal/components/bar/modules/stats.d.ts
vendored
Normal file
9
config/astal/components/bar/modules/stats.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
interface Stats {
|
||||||
|
kernel: string;
|
||||||
|
cpuTemp: string;
|
||||||
|
cpuClk: string;
|
||||||
|
gpuTemp: string;
|
||||||
|
gpuClk: string;
|
||||||
|
vram: string;
|
||||||
|
availableVRAM: string;
|
||||||
|
}
|
137
config/astal/components/bar/sysinfo.ts
Normal file
137
config/astal/components/bar/sysinfo.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { exec, execAsync, interval, Variable } from "astal";
|
||||||
|
|
||||||
|
const FETCH_INTERVAL = 2000;
|
||||||
|
const cpuUtil = Variable("0%");
|
||||||
|
const ramUtil = Variable("0%");
|
||||||
|
const ramUsed = Variable("0MiB");
|
||||||
|
const gpuUtil = Variable("0%");
|
||||||
|
let gpuName = "card1";
|
||||||
|
let enabled = true;
|
||||||
|
|
||||||
|
const getStats = (): Stats => {
|
||||||
|
gpuName = exec(`/bin/bash -c "ls /sys/class/drm/ | grep '^card[0-9]*$'"`);
|
||||||
|
const cpuNameInSensors = "CPUTIN";
|
||||||
|
const stats = {
|
||||||
|
kernel: exec("uname -sr"),
|
||||||
|
cpuTemp: exec(
|
||||||
|
`/bin/bash -c "sensors | grep -m1 ${cpuNameInSensors} | awk '{print $2}'"`,
|
||||||
|
),
|
||||||
|
cpuClk: exec(
|
||||||
|
`awk '/cpu MHz/ {sum+=$4; ++n} END {print sum/n " MHz"}' /proc/cpuinfo`,
|
||||||
|
),
|
||||||
|
gpuTemp: exec(
|
||||||
|
`/bin/bash -c "sensors | grep -E 'edge' | awk '{print $2}'"`,
|
||||||
|
),
|
||||||
|
gpuClk: exec(
|
||||||
|
`/bin/bash -c "cat /sys/class/drm/${gpuName}/device/pp_dpm_sclk | grep '\\*' | awk '{print $2 $3}'"`,
|
||||||
|
),
|
||||||
|
vram:
|
||||||
|
Math.round(
|
||||||
|
parseInt(
|
||||||
|
exec(
|
||||||
|
`cat /sys/class/drm/${gpuName}/device/mem_info_vram_used`,
|
||||||
|
),
|
||||||
|
) /
|
||||||
|
1024 /
|
||||||
|
1024,
|
||||||
|
) + "MiB",
|
||||||
|
availableVRAM:
|
||||||
|
Math.round(
|
||||||
|
parseInt(
|
||||||
|
exec(
|
||||||
|
`cat /sys/class/drm/${gpuName}/device/mem_info_vram_total`,
|
||||||
|
),
|
||||||
|
) /
|
||||||
|
1024 /
|
||||||
|
1024,
|
||||||
|
) + "MiB",
|
||||||
|
};
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemStats: Variable<Stats> = Variable(getStats());
|
||||||
|
const availableFeatures = {
|
||||||
|
cpu: true,
|
||||||
|
ram: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshStats = () => {
|
||||||
|
systemStats.set(getStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureTest = () => {
|
||||||
|
print('[SysInfo] Feature test started...');
|
||||||
|
// Check if awk & sed are available
|
||||||
|
try {
|
||||||
|
exec("awk -V");
|
||||||
|
exec("sed --version");
|
||||||
|
} catch (e) {
|
||||||
|
printerr(
|
||||||
|
"[ SysInfo ] AWK or SED missing! No system info will be available",
|
||||||
|
);
|
||||||
|
enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if mpstat is available
|
||||||
|
try {
|
||||||
|
exec("mpstat -V");
|
||||||
|
} catch (e) {
|
||||||
|
availableFeatures.cpu = false;
|
||||||
|
printerr(
|
||||||
|
"[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
print('[SysInfo] Feature test complete');
|
||||||
|
};
|
||||||
|
|
||||||
|
const sysInfoFetcher = () => {
|
||||||
|
if (enabled) {
|
||||||
|
if (availableFeatures.cpu) {
|
||||||
|
execAsync(`/bin/fish -c cpu-utilization`).then(v => {
|
||||||
|
cpuUtil.set("" + Math.round(parseFloat(v)));
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (availableFeatures.ram) {
|
||||||
|
execAsync(
|
||||||
|
`/bin/bash -c "free | awk '/Mem:/ {print $3 \\" \\" $2}'"`,
|
||||||
|
).then(v => {
|
||||||
|
const util = parseInt(v.split(' ')[0]);
|
||||||
|
const available = parseInt(v.split(' ')[1]);
|
||||||
|
ramUtil.set("" + Math.round(util / available * 100));
|
||||||
|
ramUsed.set(`${Math.round(util / 1024 / 1024 * 10) / 10} GiB of ${Math.round(available / 1024 / 1024 * 10) / 10} GiB used`);
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
gpuUtil.set(exec("cat /sys/class/drm/card1/device/gpu_busy_percent"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sysInfoFetcherRunning = false;
|
||||||
|
const startSysInfoFetcher = () => {
|
||||||
|
if (!sysInfoFetcherRunning) {
|
||||||
|
sysInfoFetcherRunning = true;
|
||||||
|
|
||||||
|
featureTest();
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
// Start interval
|
||||||
|
interval(FETCH_INTERVAL, sysInfoFetcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startSysInfoFetcher,
|
||||||
|
enabled,
|
||||||
|
gpuUtil,
|
||||||
|
cpuUtil,
|
||||||
|
ramUsed,
|
||||||
|
ramUtil,
|
||||||
|
refreshStats,
|
||||||
|
systemStats
|
||||||
|
}
|
87
config/astal/components/launcher/Launcher.tsx
Normal file
87
config/astal/components/launcher/Launcher.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Variable } from "astal";
|
||||||
|
import { App, Astal, Gdk, Gtk, hook } from "astal/gtk4";
|
||||||
|
import AstalApps from "gi://AstalApps";
|
||||||
|
import AppList from "./modules/Apps";
|
||||||
|
|
||||||
|
const prefixes = ['='];
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
App.get_window("launcher")!.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Launcher = () => {
|
||||||
|
const apps = new AstalApps.Apps();
|
||||||
|
const width = Variable(1000);
|
||||||
|
const height = Variable(1000);
|
||||||
|
|
||||||
|
const text = Variable("");
|
||||||
|
const visible = Variable(false);
|
||||||
|
const onEnter = () => {
|
||||||
|
// TODO handle custom stuff
|
||||||
|
apps.fuzzy_query(text.get())?.[0].launch();
|
||||||
|
hide();
|
||||||
|
};
|
||||||
|
return <window
|
||||||
|
name="launcher"
|
||||||
|
visible={visible()}
|
||||||
|
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
|
||||||
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
|
keymode={Astal.Keymode.ON_DEMAND}
|
||||||
|
application={App}
|
||||||
|
onShow={(self) => {
|
||||||
|
width.set(self.get_current_monitor().geometry.width);
|
||||||
|
height.set(self.get_current_monitor().geometry.height);
|
||||||
|
}}
|
||||||
|
onKeyPressed={(self, keyval) => {
|
||||||
|
if (keyval === Gdk.KEY_Escape) self.hide();
|
||||||
|
}}
|
||||||
|
child={
|
||||||
|
<box
|
||||||
|
vertical
|
||||||
|
cssClasses={["app-launcher-wrapper"]}
|
||||||
|
widthRequest={width()}
|
||||||
|
heightRequest={height()}
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
>
|
||||||
|
<button onClicked={hide} visible={false} />
|
||||||
|
<box
|
||||||
|
vertical
|
||||||
|
cssClasses={["app-launcher"]}
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
halign={Gtk.Align.CENTER}
|
||||||
|
widthRequest={500}
|
||||||
|
>
|
||||||
|
<button onClicked={hide} visible={false}></button>
|
||||||
|
<box cssClasses={["search"]}>
|
||||||
|
<image iconName={"system-search-symbolic"}></image>
|
||||||
|
<entry
|
||||||
|
placeholderText={"Search..."}
|
||||||
|
text={text.get()}
|
||||||
|
setup={self => {
|
||||||
|
hook(self, App, 'window-toggled', (_, win) => {
|
||||||
|
if (win.name == 'launcher') {
|
||||||
|
self.set_text('');
|
||||||
|
self.grab_focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onNotifyText={self => text.set(self.text)}
|
||||||
|
primaryIconSensitive
|
||||||
|
onActivate={onEnter}
|
||||||
|
hexpand></entry>
|
||||||
|
</box>
|
||||||
|
<AppList
|
||||||
|
hide={hide}
|
||||||
|
query={text}
|
||||||
|
visible={text(v => {
|
||||||
|
return !prefixes.includes(v.slice(0, 1));
|
||||||
|
})}
|
||||||
|
></AppList>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</window>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Launcher;
|
16
config/astal/components/launcher/launcher.scss
Normal file
16
config/astal/components/launcher/launcher.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@use '../../util/colours.scss' as *;
|
||||||
|
|
||||||
|
window {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.app-launcher-wrapper {
|
||||||
|
background-color: $shadow-color;
|
||||||
|
|
||||||
|
>box.app-launcher {
|
||||||
|
background-color: $bg-color;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid $accent-color-2;
|
||||||
|
}
|
||||||
|
}
|
59
config/astal/components/launcher/modules/Apps.tsx
Normal file
59
config/astal/components/launcher/modules/Apps.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Binding, Variable } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalApps from "gi://AstalApps";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
|
||||||
|
const MAX_ITEMS = 8;
|
||||||
|
|
||||||
|
const AppList = ({ hide, query, visible }: { hide: () => void, query: Variable<string>, visible: Binding<Boolean> }) => {
|
||||||
|
const apps = new AstalApps.Apps();
|
||||||
|
const list = query((text) => apps.fuzzy_query(text).slice(0, MAX_ITEMS));
|
||||||
|
return <box>
|
||||||
|
<box
|
||||||
|
spacing={6}
|
||||||
|
vertical
|
||||||
|
cssClasses={["app-list"]}
|
||||||
|
visible={list.as(l => l.length > 0)}
|
||||||
|
>
|
||||||
|
{list.as(l => l.map(app => <AppButton app={app} hide={hide}></AppButton>))}
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
halign={Gtk.Align.CENTER}
|
||||||
|
cssClasses={["list-empty"]}
|
||||||
|
vertical
|
||||||
|
visible={list.as(l => l.length === 0)}
|
||||||
|
>
|
||||||
|
<image iconName={"system-search-symbolic"}></image>
|
||||||
|
<label label={"No match found"}></label>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppButton = ({ app, hide }: { app: AstalApps.Application, hide: () => void }) => {
|
||||||
|
return <button
|
||||||
|
onClicked={() => {
|
||||||
|
hide();
|
||||||
|
app.launch();
|
||||||
|
}}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={app.iconName}></image>
|
||||||
|
<box valign={Gtk.Align.CENTER} vertical>
|
||||||
|
<label
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
maxWidthChars={40}
|
||||||
|
xalign={0}
|
||||||
|
label={app.name}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
wrap xalign={0}
|
||||||
|
label={app.description}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
}>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppList;
|
6
config/astal/components/notifications-opt/README.md
Normal file
6
config/astal/components/notifications-opt/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Source
|
||||||
|
This has been copied from [matshell](https://github.com/Neurarian/matshell)
|
||||||
|
|
||||||
|
It is not yet used, as it has not been adapted yet to feature a notification history.
|
||||||
|
|
||||||
|
Potentially, a notification centre will be added to make this here work better. Styling is also missing
|
32
config/astal/components/notifications-opt/main.tsx
Normal file
32
config/astal/components/notifications-opt/main.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Astal } from "astal/gtk4";
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import Hyprland from "gi://AstalHyprland";
|
||||||
|
import { bind } from "astal";
|
||||||
|
import { NotificationWidget } from "./modules/Notification";
|
||||||
|
import { hyprToGdk } from "../../util/hyprland";
|
||||||
|
|
||||||
|
export default function Notifications() {
|
||||||
|
const notifd = Notifd.get_default();
|
||||||
|
const hyprland = Hyprland.get_default();
|
||||||
|
const { TOP, RIGHT } = Astal.WindowAnchor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window
|
||||||
|
name="notifications"
|
||||||
|
gdkmonitor={bind(hyprland, "focusedMonitor").as(
|
||||||
|
(focused: Hyprland.Monitor) => hyprToGdk(focused),
|
||||||
|
)}
|
||||||
|
anchor={TOP | RIGHT}
|
||||||
|
visible={bind(notifd, "notifications").as(
|
||||||
|
(notifications) => notifications.length > 0,
|
||||||
|
)}
|
||||||
|
child={
|
||||||
|
<box vertical={true} cssClasses={["notifications"]}>
|
||||||
|
{bind(notifd, "notifications").as((notifications) =>
|
||||||
|
notifications.map((n) => <NotificationWidget notification={n} />),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
25
config/astal/components/notifications-opt/modules/Icon.tsx
Normal file
25
config/astal/components/notifications-opt/modules/Icon.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import { fileExists, isIcon } from "../../../util/notifd";
|
||||||
|
|
||||||
|
|
||||||
|
export function NotificationIcon(notification: Notifd.Notification) {
|
||||||
|
if (notification.image || notification.appIcon || notification.desktopEntry) {
|
||||||
|
const icon = notification.image || notification.appIcon || notification.desktopEntry;
|
||||||
|
if (fileExists(icon)) {
|
||||||
|
return (
|
||||||
|
<box expand={false} valign={Gtk.Align.CENTER}>
|
||||||
|
<image file={icon} />
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
} else if (isIcon(icon)) {
|
||||||
|
return (
|
||||||
|
<box expand={false} valign={Gtk.Align.CENTER}>
|
||||||
|
<image iconName={icon} />
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import { NotificationIcon } from "./Icon";
|
||||||
|
import { createTimeoutManager } from "../../../util/notifd";
|
||||||
|
|
||||||
|
export function NotificationWidget({
|
||||||
|
notification,
|
||||||
|
}: {
|
||||||
|
notification: Notifd.Notification;
|
||||||
|
}) {
|
||||||
|
const { START, CENTER, END } = Gtk.Align;
|
||||||
|
const actions = notification.actions || [];
|
||||||
|
const TIMEOUT_DELAY = 3000;
|
||||||
|
|
||||||
|
// Keep track of notification validity
|
||||||
|
const notifd = Notifd.get_default();
|
||||||
|
const timeoutManager = createTimeoutManager(
|
||||||
|
() => notification.dismiss(),
|
||||||
|
TIMEOUT_DELAY,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
setup={(self) => {
|
||||||
|
// Set up timeout
|
||||||
|
timeoutManager.setupTimeout();
|
||||||
|
const clickGesture = Gtk.GestureClick.new();
|
||||||
|
clickGesture.set_button(0); // 0 means any button
|
||||||
|
clickGesture.connect("pressed", (gesture, _) => {
|
||||||
|
try {
|
||||||
|
// Get which button was pressed (1=left, 2=middle, 3=right)
|
||||||
|
const button = gesture.get_current_button();
|
||||||
|
|
||||||
|
if (button === 1) {
|
||||||
|
// PRIMARY/LEFT
|
||||||
|
if (actions.length > 0) n.invoke(actions[0]);
|
||||||
|
} else if (button === 2) {
|
||||||
|
// MIDDLE
|
||||||
|
notifd.notifications?.forEach((n) => {
|
||||||
|
n.dismiss();
|
||||||
|
});
|
||||||
|
} else if (button === 3) {
|
||||||
|
// SECONDARY/RIGHT
|
||||||
|
notification.dismiss();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.add_controller(clickGesture);
|
||||||
|
|
||||||
|
self.connect("unrealize", () => {
|
||||||
|
timeoutManager.cleanup();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onHoverEnter={timeoutManager.handleHover}
|
||||||
|
onHoverLeave={timeoutManager.handleHoverLost}
|
||||||
|
vertical
|
||||||
|
vexpand={false}
|
||||||
|
cssClasses={["notification", `${urgency(notification)}`]}
|
||||||
|
name={notification.id.toString()}
|
||||||
|
>
|
||||||
|
<box cssClasses={["header"]}>
|
||||||
|
<label
|
||||||
|
cssClasses={["app-name"]}
|
||||||
|
halign={CENTER}
|
||||||
|
label={bind(notification, "app_name")}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
cssClasses={["time"]}
|
||||||
|
hexpand
|
||||||
|
halign={END}
|
||||||
|
label={time(notification.time)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
<Gtk.Separator />
|
||||||
|
<box cssClasses={["content"]}>
|
||||||
|
<box
|
||||||
|
cssClasses={["thumb"]}
|
||||||
|
visible={Boolean(NotificationIcon(notification))}
|
||||||
|
halign={CENTER}
|
||||||
|
valign={CENTER}
|
||||||
|
vexpand={true}
|
||||||
|
>
|
||||||
|
{NotificationIcon(notification)}
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
vertical
|
||||||
|
cssClasses={["text-content"]}
|
||||||
|
hexpand={true}
|
||||||
|
halign={CENTER}
|
||||||
|
valign={CENTER}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
cssClasses={["title"]}
|
||||||
|
valign={CENTER}
|
||||||
|
wrap={false}
|
||||||
|
label={bind(notification, "summary")}
|
||||||
|
/>
|
||||||
|
{notification.body && (
|
||||||
|
<label
|
||||||
|
cssClasses={["body"]}
|
||||||
|
valign={CENTER}
|
||||||
|
wrap={true}
|
||||||
|
maxWidthChars={50}
|
||||||
|
label={bind(notification, "body")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
{actions.length > 0 && (
|
||||||
|
<box cssClasses={["actions"]}>
|
||||||
|
{actions.map(({ label, action }) => (
|
||||||
|
<button
|
||||||
|
hexpand
|
||||||
|
cssClasses={["action-button"]}
|
||||||
|
onClicked={() => notification.invoke(action)}
|
||||||
|
>
|
||||||
|
<label label={label} halign={CENTER} hexpand />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
299
config/astal/components/notifications/handler.tsx
Normal file
299
config/astal/components/notifications/handler.tsx
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
/*
|
||||||
|
* dotfiles - handler.ts
|
||||||
|
*
|
||||||
|
* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License
|
||||||
|
* https://janishutz.com, development@janishutz.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, Astal, Gdk } from "astal/gtk4"
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import Notification from "./notifications";
|
||||||
|
import { timeout, Variable } from "astal"
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// Config
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
const TIMEOUT_DELAY = 5000;
|
||||||
|
let isRunning = false;
|
||||||
|
let notificationMenuOpen = false;
|
||||||
|
|
||||||
|
interface NotificationDetails {
|
||||||
|
notification: Notifd.Notification;
|
||||||
|
backendID: number;
|
||||||
|
notifdID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// ╭───────────────────────────────────────────────╮
|
||||||
|
// │ Handler │
|
||||||
|
// ╰───────────────────────────────────────────────╯
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
let ShownNotifications: Variable<number[]> = Variable( [] );
|
||||||
|
let Notifications: NotificationDetails[] = [];
|
||||||
|
|
||||||
|
const notifd = Notifd.get_default();
|
||||||
|
notifd.ignoreTimeout = true;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a notification by its internal ID
|
||||||
|
* @param index The notifd ID of the notification
|
||||||
|
*/
|
||||||
|
const deleteNotification = ( index: number ): void => {
|
||||||
|
hideNotification( index );
|
||||||
|
Notifications.splice( index, 1 );
|
||||||
|
if ( Notifications.length === 0 ) {
|
||||||
|
notificationMenuOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a notification by notifd id
|
||||||
|
* @param id The notifd ID of the notification
|
||||||
|
*/
|
||||||
|
const deleteNotificationByNotificationID = ( id: number ): void => {
|
||||||
|
const index = findNotificationByNotificationID( id );
|
||||||
|
if ( index > -1 ) {
|
||||||
|
deleteNotification( index );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the internal ID from the notifd id for a notification (helper function)
|
||||||
|
* @param id The notifd ID of the notification
|
||||||
|
* @returns The internal ID or -1 if not found
|
||||||
|
*/
|
||||||
|
const findNotificationByNotificationID = ( id: number ): number => {
|
||||||
|
// Find index in Notifications array
|
||||||
|
for (let index = 0; index < Notifications.length; index++) {
|
||||||
|
if ( Notifications[ index ].notifdID === id ) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a notification to the notification handler
|
||||||
|
* @param id The notifd ID of the notification
|
||||||
|
*/
|
||||||
|
const addNotification = ( id: number ): void => {
|
||||||
|
const currIndex = Notifications.length;
|
||||||
|
Notifications.push( {
|
||||||
|
notifdID: id,
|
||||||
|
backendID: currIndex,
|
||||||
|
notification: notifd.get_notification( id )
|
||||||
|
} );
|
||||||
|
|
||||||
|
showNotification( currIndex );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the notifd runner and handle notifications.
|
||||||
|
*/
|
||||||
|
const hookToNotificationDaemon = (): void => {
|
||||||
|
if ( isRunning ) {
|
||||||
|
printerr( '[ Notifications ] Error: Already running' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
notifd.connect( 'notified', ( _, id ) => {
|
||||||
|
addNotification( id );
|
||||||
|
} );
|
||||||
|
|
||||||
|
notifd.connect( 'resolved', ( _, id ) => {
|
||||||
|
deleteNotificationByNotificationID( id );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a notification. It will stay on screen (regardless of removeAgain passed in), if
|
||||||
|
* critical urgency
|
||||||
|
* @param id The internal id (index in Notifications array)
|
||||||
|
* @param removeAgain = true If to remove the notification from the screen again automatically
|
||||||
|
*/
|
||||||
|
const showNotification = ( id: number, removeAgain: boolean = true ) => {
|
||||||
|
// Add notification to UI for display
|
||||||
|
const not = [...ShownNotifications.get()].reverse();
|
||||||
|
not.push( id );
|
||||||
|
ShownNotifications.set( not.reverse() );
|
||||||
|
|
||||||
|
// Set delay to remove the notification again
|
||||||
|
if ( removeAgain && Notifications[ id ].notification.get_urgency() !== Notifd.Urgency.CRITICAL ) {
|
||||||
|
timeout( TIMEOUT_DELAY, () => {
|
||||||
|
hideNotification( id );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop displaying notification
|
||||||
|
* @param id The internal id (index in the Notifications array)
|
||||||
|
*/
|
||||||
|
const hideNotification = ( id: number ) => {
|
||||||
|
if ( !notificationMenuOpen ) {
|
||||||
|
const not = [...ShownNotifications.get()];
|
||||||
|
not.splice( not.indexOf( id ), 1 );
|
||||||
|
ShownNotifications.set( not );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the notification menu. Called by toggleNotificationMenu too
|
||||||
|
*/
|
||||||
|
const openNotificationMenu = () => {
|
||||||
|
// Simply show all notifications
|
||||||
|
notificationMenuOpen = true;
|
||||||
|
const not = [];
|
||||||
|
for (let index = 0; index < Notifications.length; index++) {
|
||||||
|
not.push( index );
|
||||||
|
}
|
||||||
|
ShownNotifications.set( not.reverse() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the notification menu. Called by toggleNotificationMenu too
|
||||||
|
*/
|
||||||
|
const closeNotificationMenu = () => {
|
||||||
|
// Hide all notifications
|
||||||
|
notificationMenuOpen = true;
|
||||||
|
ShownNotifications.set( [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the notification menu (i.e. show all notifications)
|
||||||
|
*/
|
||||||
|
const toggleNotificationMenu = (): string => {
|
||||||
|
if ( notificationMenuOpen ) {
|
||||||
|
closeNotificationMenu();
|
||||||
|
return 'Toggle notification menu closed';
|
||||||
|
} else {
|
||||||
|
openNotificationMenu();
|
||||||
|
return 'Toggled notification menu open';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all notifications
|
||||||
|
*/
|
||||||
|
const clearAllNotifications = () => {
|
||||||
|
Notifications = [];
|
||||||
|
ShownNotifications.set( [] );
|
||||||
|
// TODO: Hiding for each individual deleteNotification
|
||||||
|
notificationMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the newest notifications
|
||||||
|
*/
|
||||||
|
const clearNewestNotifications = () => {
|
||||||
|
const not = [...ShownNotifications.get()];
|
||||||
|
not.splice( 0, 1 );
|
||||||
|
ShownNotifications.set( not );
|
||||||
|
|
||||||
|
Notifications.splice( Notifications.length - 1, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// ╭───────────────────────────────────────────────╮
|
||||||
|
// │ User Interface │
|
||||||
|
// ╰───────────────────────────────────────────────╯
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
const startNotificationHandler = (gdkmonitor: Gdk.Monitor) => {
|
||||||
|
const { TOP, RIGHT } = Astal.WindowAnchor
|
||||||
|
|
||||||
|
hookToNotificationDaemon();
|
||||||
|
|
||||||
|
return <window
|
||||||
|
cssClasses={["NotificationHandler"]}
|
||||||
|
gdkmonitor={gdkmonitor}
|
||||||
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
|
anchor={TOP | RIGHT}
|
||||||
|
visible={ShownNotifications( list => list.length > 0 )}
|
||||||
|
application={App}>
|
||||||
|
<box vertical>
|
||||||
|
{ShownNotifications( list => list.map( i => {
|
||||||
|
// i is index in ShownNotifications array
|
||||||
|
return <Notification id={i} delete={deleteNotification} notification={Notifications[ i ].notification}></Notification>
|
||||||
|
} ) ) }
|
||||||
|
</box>
|
||||||
|
</window>
|
||||||
|
}
|
||||||
|
|
||||||
|
const cliHandler = ( args: string[] ): string => {
|
||||||
|
if ( args[ 1 ] == 'show' ) {
|
||||||
|
openNotificationMenu();
|
||||||
|
return 'Showing all open notifications';
|
||||||
|
} else if ( args[ 1 ] == 'hide' ) {
|
||||||
|
closeNotificationMenu();
|
||||||
|
return 'Hid all notifications';
|
||||||
|
} else if ( args[ 1 ] == 'clear' ) {
|
||||||
|
clearAllNotifications();
|
||||||
|
return 'Cleared all notifications';
|
||||||
|
} else if ( args[ 1 ] == 'clear-newest' ) {
|
||||||
|
clearNewestNotifications();
|
||||||
|
return 'Cleared newest notification';
|
||||||
|
} else if ( args[ 1 ] == 'toggle' ) {
|
||||||
|
return toggleNotificationMenu();
|
||||||
|
} else if ( args[ 1 ] == 'list' ){
|
||||||
|
if ( Notifications.length > 0 ) {
|
||||||
|
let list = 'Currently unviewed notifications: ';
|
||||||
|
for (let index = 0; index < Notifications.length; index++) {
|
||||||
|
const element = Notifications[index];
|
||||||
|
|
||||||
|
list += `\n - (${element.notifdID}) ${element.notification.get_app_name()}: ${element.notification.get_summary()}`;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
} else {
|
||||||
|
return 'No currently unviewed notifications'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'Unknown command!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startNotificationHandler,
|
||||||
|
cliHandler
|
||||||
|
}
|
12
config/astal/components/notifications/helper.ts
Normal file
12
config/astal/components/notifications/helper.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Gtk, Gdk } from "astal/gtk4";
|
||||||
|
import { GLib } from "astal";
|
||||||
|
|
||||||
|
export const isIcon = (icon: string) => {
|
||||||
|
const display = Gdk.Display.get_default();
|
||||||
|
if (!display) return false;
|
||||||
|
const iconTheme = Gtk.IconTheme.get_for_display(display);
|
||||||
|
return iconTheme.has_icon(icon);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileExists = (path: string) =>
|
||||||
|
GLib.file_test(path, GLib.FileTest.EXISTS);
|
24
config/astal/components/notifications/icon.tsx
Normal file
24
config/astal/components/notifications/icon.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import { fileExists, isIcon } from "./helper";
|
||||||
|
|
||||||
|
|
||||||
|
export function NotificationIcon(notification: Notifd.Notification) {
|
||||||
|
if ( notification.image || notification.appIcon || notification.desktopEntry) {
|
||||||
|
const icon = notification.image || notification.appIcon || notification.desktopEntry;
|
||||||
|
if (fileExists(icon)) {
|
||||||
|
return (
|
||||||
|
<box expand={false} valign={Gtk.Align.CENTER}>
|
||||||
|
<image file={icon} />
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
} else if (isIcon(icon)) {
|
||||||
|
return (
|
||||||
|
<box expand={false} valign={Gtk.Align.CENTER}>
|
||||||
|
<image iconName={icon} />
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
124
config/astal/components/notifications/notifications.scss
Normal file
124
config/astal/components/notifications/notifications.scss
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
@use "sass:string";
|
||||||
|
|
||||||
|
@function gtkalpha($c, $a) {
|
||||||
|
@return string.unquote("alpha(#{$c},#{$a})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
|
||||||
|
$fg-color: #{"@theme_fg_color"};
|
||||||
|
$bg-color: #{"@theme_bg_color"};
|
||||||
|
$error: red;
|
||||||
|
|
||||||
|
window.NotificationHandler {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.notification {
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& {
|
||||||
|
min-width: 400px;
|
||||||
|
border-radius: 13px;
|
||||||
|
background-color: $bg-color;
|
||||||
|
margin: .5rem 1rem .5rem 1rem;
|
||||||
|
box-shadow: 2px 3px 8px 0 gtkalpha(black, .4);
|
||||||
|
border: 1pt solid gtkalpha($fg-color, .03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.critical {
|
||||||
|
border: 1pt solid gtkalpha($error, .4);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
color: gtkalpha($error, .8);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
color: gtkalpha($error, .6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: .5rem;
|
||||||
|
color: gtkalpha($fg-color, 0.5);
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
margin: 0 .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
margin-right: .3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: .4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
margin: 0 .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: .2rem;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
separator {
|
||||||
|
margin: 0 .4rem;
|
||||||
|
background-color: gtkalpha($fg-color, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-top: .5rem;
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: $fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
color: gtkalpha($fg-color, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border: 1px solid gtkalpha($fg-color, .02);
|
||||||
|
margin-right: .5rem;
|
||||||
|
border-radius: 9px;
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 .3rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
config/astal/components/notifications/notifications.tsx
Normal file
113
config/astal/components/notifications/notifications.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// From astal examples
|
||||||
|
|
||||||
|
import { bind, GLib } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Notifd from "gi://AstalNotifd";
|
||||||
|
import { NotificationIcon } from "./icon";
|
||||||
|
// import Pango from "gi://Pango?version=1.0"
|
||||||
|
|
||||||
|
const fileExists = (path: string) => GLib.file_test(path, GLib.FileTest.EXISTS);
|
||||||
|
|
||||||
|
const time = (time: number, format = "%H:%M") =>
|
||||||
|
GLib.DateTime.new_from_unix_local(time).format(format)!;
|
||||||
|
|
||||||
|
const urgency = (n: Notifd.Notification) => {
|
||||||
|
const { LOW, NORMAL, CRITICAL } = Notifd.Urgency;
|
||||||
|
// match operator when?
|
||||||
|
switch (n.urgency) {
|
||||||
|
case LOW:
|
||||||
|
return "low";
|
||||||
|
case CRITICAL:
|
||||||
|
return "critical";
|
||||||
|
case NORMAL:
|
||||||
|
default:
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
delete: (id: number) => void;
|
||||||
|
notification: Notifd.Notification;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Notification(props: Props) {
|
||||||
|
const { notification: n, id: id, delete: del } = props;
|
||||||
|
const { START, CENTER, END } = Gtk.Align;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box vertical cssClasses={["notification", `${urgency(n)}`]}>
|
||||||
|
<box cssClasses={["header"]}>
|
||||||
|
{n.appIcon || n.desktopEntry ? (
|
||||||
|
<Gtk.Image
|
||||||
|
cssClasses={["app-icon"]}
|
||||||
|
visible={Boolean(n.appIcon || n.desktopEntry)}
|
||||||
|
iconName={n.appIcon || n.desktopEntry}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<image iconName={"window-close-symbolic"}></image>
|
||||||
|
)}
|
||||||
|
<label
|
||||||
|
cssClasses={["app-name"]}
|
||||||
|
halign={START}
|
||||||
|
// ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
label={n.appName || "Unknown"}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
cssClasses={["time"]}
|
||||||
|
hexpand
|
||||||
|
halign={END}
|
||||||
|
label={time(n.time)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClicked={() => {
|
||||||
|
del(id);
|
||||||
|
}}
|
||||||
|
child={<image iconName="window-close-symbolic" />}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
<Gtk.Separator visible />
|
||||||
|
<box cssClasses={["content"]}>
|
||||||
|
<box
|
||||||
|
cssClasses={["image"]}
|
||||||
|
visible={Boolean(NotificationIcon(n))}
|
||||||
|
halign={CENTER}
|
||||||
|
valign={CENTER}
|
||||||
|
vexpand={true}
|
||||||
|
>
|
||||||
|
{NotificationIcon(n)}
|
||||||
|
</box>
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
cssClasses={["summary"]}
|
||||||
|
halign={START}
|
||||||
|
xalign={0}
|
||||||
|
useMarkup
|
||||||
|
label={bind(n, "summary")}
|
||||||
|
// ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
/>
|
||||||
|
{n.body && (
|
||||||
|
<label
|
||||||
|
cssClasses={["body"]}
|
||||||
|
valign={CENTER}
|
||||||
|
wrap={true}
|
||||||
|
maxWidthChars={50}
|
||||||
|
label={bind(n, "body")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
{n.get_actions().length > 0 ? (
|
||||||
|
<box cssClasses={["actions"]}>
|
||||||
|
{n.get_actions().map(({ label, id }) => (
|
||||||
|
<button hexpand onClicked={() => n.invoke(id)}>
|
||||||
|
<label label={label} halign={CENTER} hexpand />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
) : (
|
||||||
|
<box></box>
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
22
config/astal/env.d.ts
vendored
Normal file
22
config/astal/env.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
declare const SRC: string
|
||||||
|
declare const DATADIR: string
|
||||||
|
|
||||||
|
declare module "inline:*" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.scss" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.blp" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.css" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
28
config/astal/meson.build
Normal file
28
config/astal/meson.build
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
project('bar-launcher-tools', version: '1.0')
|
||||||
|
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
|
||||||
|
main = meson.project_name() + '.built'
|
||||||
|
|
||||||
|
custom_target(
|
||||||
|
command: [
|
||||||
|
find_program('ags'),
|
||||||
|
'bundle',
|
||||||
|
'--root', meson.project_source_root(),
|
||||||
|
meson.project_source_root() / 'app.ts',
|
||||||
|
main,
|
||||||
|
],
|
||||||
|
output: main,
|
||||||
|
input: files('app.ts'),
|
||||||
|
install: true,
|
||||||
|
install_dir: pkgdatadir,
|
||||||
|
)
|
||||||
|
|
||||||
|
configure_file(
|
||||||
|
input: files('wrapper.sh'),
|
||||||
|
output: meson.project_name(),
|
||||||
|
configuration: {
|
||||||
|
'MAIN_PROGRAM': pkgdatadir / main,
|
||||||
|
'LAYER_SHELL_LIBDIR': dependency('gtk4-layer-shell-0').get_variable('libdir'),
|
||||||
|
},
|
||||||
|
install: true,
|
||||||
|
install_dir: get_option('prefix') / get_option('bindir'),
|
||||||
|
)
|
BIN
config/astal/no-avatar-icon.jpg
Normal file
BIN
config/astal/no-avatar-icon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
6
config/astal/package.json
Normal file
6
config/astal/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "astal-shell",
|
||||||
|
"dependencies": {
|
||||||
|
"astal": "/usr/share/astal/gjs"
|
||||||
|
}
|
||||||
|
}
|
24
config/astal/style.scss
Normal file
24
config/astal/style.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* @use './components/notifications/notifications.scss'; */
|
||||||
|
@use "./components/bar/bar.scss";
|
||||||
|
@use "./components/QuickActions/quickactions.scss";
|
||||||
|
@use "./util/colours.scss" as *;
|
||||||
|
/* @use "./components/launcher/launcher.scss"; */
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
empty {
|
||||||
|
min-width: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
14
config/astal/tsconfig.json
Normal file
14
config/astal/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
// "checkJs": true,
|
||||||
|
// "allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "astal/gtk4",
|
||||||
|
}
|
||||||
|
}
|
82
config/astal/util/brightness.ts
Normal file
82
config/astal/util/brightness.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import GObject, { register, property } from "astal/gobject"
|
||||||
|
import { monitorFile, readFileAsync } from "astal/file"
|
||||||
|
import { exec, execAsync } from "astal/process"
|
||||||
|
|
||||||
|
const get = (args: string) => Number(exec(`brightnessctl ${args}`))
|
||||||
|
const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`)
|
||||||
|
const kbd = exec(`bash -c "ls -w1 /sys/class/leds | head -1"`)
|
||||||
|
|
||||||
|
@register({ GTypeName: "Brightness" })
|
||||||
|
export default class Brightness extends GObject.Object {
|
||||||
|
static instance: Brightness
|
||||||
|
static get_default() {
|
||||||
|
if (!this.instance)
|
||||||
|
this.instance = new Brightness()
|
||||||
|
|
||||||
|
return this.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
#kbdMax = get(`--device ${kbd} max`)
|
||||||
|
#kbd = get(`--device ${kbd} get`)
|
||||||
|
#screenMax = get("max")
|
||||||
|
#screen = get("get") / (get("max") || 1)
|
||||||
|
#screenAvailable = true
|
||||||
|
|
||||||
|
@property(Boolean)
|
||||||
|
get screenAvailable() { return this.#screenAvailable }
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
|
get kbd() { return this.#kbd }
|
||||||
|
|
||||||
|
set kbd(value) {
|
||||||
|
if (value < 0 || value > this.#kbdMax)
|
||||||
|
return
|
||||||
|
|
||||||
|
execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => {
|
||||||
|
this.#kbd = value
|
||||||
|
this.notify("kbd")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
|
get screen() { return this.#screen }
|
||||||
|
|
||||||
|
set screen(percent) {
|
||||||
|
if (percent < 0)
|
||||||
|
percent = 0
|
||||||
|
|
||||||
|
if (percent > 1)
|
||||||
|
percent = 1
|
||||||
|
|
||||||
|
execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => {
|
||||||
|
this.#screen = percent
|
||||||
|
this.notify("screen")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
const screenPath = `/sys/class/backlight/${screen}/brightness`
|
||||||
|
const kbdPath = `/sys/class/leds/${kbd}/brightness`
|
||||||
|
|
||||||
|
monitorFile(screenPath, async f => {
|
||||||
|
const v = await readFileAsync(f)
|
||||||
|
this.#screen = Number(v) / this.#screenMax
|
||||||
|
this.notify("screen")
|
||||||
|
})
|
||||||
|
|
||||||
|
monitorFile(kbdPath, async f => {
|
||||||
|
const v = await readFileAsync(f)
|
||||||
|
this.#kbd = Number(v) / this.#kbdMax
|
||||||
|
this.notify("kbd")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if there is a screen available
|
||||||
|
try {
|
||||||
|
get( 'g -c backlight' );
|
||||||
|
} catch ( _ ) {
|
||||||
|
this.#screenAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
config/astal/util/colours.scss
Normal file
5
config/astal/util/colours.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
$fg-color: #E6E6E6;
|
||||||
|
$bg-color: #141414;
|
||||||
|
$accent-color: #591641;
|
||||||
|
$accent-color-2: #97103A;
|
||||||
|
$shadow-color: rgba(40, 40, 40, 0.3);
|
28
config/astal/util/hyprland.ts
Normal file
28
config/astal/util/hyprland.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// ┌ ┐
|
||||||
|
// │ From https://github.com/Neurarian/matshell │
|
||||||
|
// └ ┘
|
||||||
|
import { App, Gdk } from "astal/gtk4";
|
||||||
|
import Hyprland from "gi://AstalHyprland";
|
||||||
|
|
||||||
|
/* Match Hyprland monitor to GDK monitor
|
||||||
|
THIS MAY NOT WORK AS INTENDED IF YOU HAVE MONITORS OF THE SAME MODEL
|
||||||
|
I did not find a more elegant solution to this.
|
||||||
|
On my setup GDK coordinates and hyprland coordinates are flipped,
|
||||||
|
so I cant match by coordinates. */
|
||||||
|
|
||||||
|
export function hyprToGdk(monitor: Hyprland.Monitor): Gdk.Monitor | null {
|
||||||
|
const monitors = App.get_monitors();
|
||||||
|
if (!monitors || monitors.length === 0) return null;
|
||||||
|
|
||||||
|
for (let gdkmonitor of monitors) {
|
||||||
|
if (
|
||||||
|
monitor &&
|
||||||
|
gdkmonitor &&
|
||||||
|
monitor.get_name() === gdkmonitor.get_connector()
|
||||||
|
)
|
||||||
|
return gdkmonitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default monitor with null safety
|
||||||
|
return monitors.length > 0 ? monitors[0] : null;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user