Merge
This commit is contained in:
		
							
								
								
									
										131
									
								
								config/Thunar/accels.scm
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										131
									
								
								config/Thunar/accels.scm
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,131 @@ | ||||
| ; thunar GtkAccelMap rc-file         -*- scheme -*- | ||||
| ; this file is an automated accelerator map dump | ||||
| ; | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-type" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-last-modified" "") | ||||
| ; (gtk_accel_path "<Actions>/Thunarwindow/menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/cut" "<Primary>x") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-size" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/file-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarBookmarks/e92146de8ffbb74bfd99784ed40bdeef" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/close-tab" "<Primary>w") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/switch-previous-tab-alt" "<Primary><Shift>ISO_Left_Tab") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-size" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/new-window" "<Primary>n") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/clear-directory-specific-settings" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/close-window" "<Primary>q") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-parent" "<Alt>Up") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-side-pane-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-size-in-bytes" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/switch-previous-tab" "<Primary>Page_Up") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/open" "<Primary>o") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-ascending" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/toggle-split-view" "F3") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/copy-2" "<Primary>Insert") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/trash-delete" "Delete") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-configure-toolbar" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-recent" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarBookmarks/7a203ca58c14c76ec1b07d08eaba7e8a" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/forward" "<Alt>Right") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/restore" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-location-alt" "<Alt>d") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/select-by-pattern" "<Primary>s") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-out-alt" "<Primary>KP_Subtract") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/contents" "F1") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-file-menu" "F10") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/show-highlight" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-descending" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-name" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/select-all-files" "<Primary>a") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/execute" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/properties" "<Alt>Return") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/cut-2" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-dtime" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarBookmarks/de6d2099b95ef15820d04f5c3a6dc0f8" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-templates" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/paste-2" "<Shift>Insert") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/switch-next-tab" "<Primary>Page_Down") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-filetype" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/close-all-windows" "<Primary><Shift>w") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/create-document" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/detach-tab" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/cancel-search" "Escape") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-in-alt2" "<Primary>equal") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-hidden-count" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarShortcutsPane/sendto-shortcuts" "<Primary>d") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/undo" "<Primary>z") | ||||
| ; (gtk_accel_path "<Actions>/ThunarBookmarks/0064c8b8c2b8ae1954479b6f2feab576" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/toggle-sort-order" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-location-selector-entry" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/paste" "<Primary>v") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-in-alt1" "<Primary>KP_Add") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-menubar" "<Primary>m") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/back" "<Alt>Left") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-desktop" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-as-detailed-list" "<Primary>2") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/restore-show" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/sendto-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-display-name" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/go-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-out" "<Primary>minus") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/remove-from-recent" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/open-with-other" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/invert-selection" "<Primary><Shift>i") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-side-pane-shortcuts" "<Primary>b") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/reload-alt-2" "Reload") | ||||
| ; (gtk_accel_path "<Actions>/ThunarBookmarks/fb8a2fb8c18b440d557134b69e8bfc79" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-location-selector-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/edit-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/copy" "<Primary>c") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-mtime" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/reload-alt-1" "F5") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/forward-alt" "Forward") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/move-to-trash" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/delete-3" "<Shift>KP_Delete") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/bookmarks-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/reload" "<Primary>r") | ||||
| ; (gtk_accel_path "<Actions>/ThunarBookmarks/ab554ce947264f765ed1ba66309937f1" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/arrange-items-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/unselect-all-files" "Escape") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-computer" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/toggle-image-preview" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/toggle-side-pane" "F9") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-as-icons" "<Primary>1") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/delete-2" "<Shift>Delete") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-in" "<Primary>plus") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/rename" "F2") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-location" "<Primary>l") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-as-compact-list" "<Primary>3") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/search" "<Primary>f") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/new-tab" "<Primary>t") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-reset" "<Primary>0") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/contents/help-menu" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/open-in-new-tab" "<Primary><Shift>p") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-location-selector-buttons" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/back-alt2" "Back") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/redo" "<Primary><Shift>z") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-trash" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/open-in-new-window" "<Primary><Shift>o") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-statusbar" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/open-location" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/duplicate" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/trash-delete-2" "KP_Delete") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/back-alt1" "BackSpace") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/create-folder" "<Primary><Shift>n") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-home" "<Alt>Home") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/switch-focused-split-view-pane" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/show-hidden" "<Primary>h") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/set-default-app" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/empty-trash" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/preferences" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/delete" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-network" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/view-side-pane-tree" "<Primary>e") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/open-file-system" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/search-alt" "Search") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/switch-next-tab-alt" "<Primary>Tab") | ||||
| ; (gtk_accel_path "<Actions>/ThunarActionManager/sendto-desktop" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarStandardView/make-link" "") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/zoom-reset-alt" "<Primary>KP_0") | ||||
| ; (gtk_accel_path "<Actions>/ThunarWindow/about" "") | ||||
							
								
								
									
										25
									
								
								config/Thunar/uca.xml
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										25
									
								
								config/Thunar/uca.xml
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <actions> | ||||
| <action> | ||||
| 	<icon>utilities-terminal</icon> | ||||
| 	<name>Open Terminal Here</name> | ||||
| 	<submenu></submenu> | ||||
| 	<unique-id>1675076590898177-1</unique-id> | ||||
| 	<command>terminator --working-directory %f</command> | ||||
| 	<description>Example for a custom action</description> | ||||
| 	<range></range> | ||||
| 	<patterns>*</patterns> | ||||
| 	<startup-notify/> | ||||
| 	<directories/> | ||||
| </action> | ||||
| <action> | ||||
| 	<icon>com.github.artemanufrij.findfileconflicts</icon> | ||||
| 	<name>Fix filenames</name> | ||||
| 	<submenu></submenu> | ||||
| 	<unique-id>1691249342110327-1</unique-id> | ||||
| 	<command>/home/janis/scripts/removeSpaces.sh %f</command> | ||||
| 	<description>A script that fixes the filenames in a directory recursively</description> | ||||
| 	<range>*</range> | ||||
| 	<patterns>*</patterns> | ||||
| </action> | ||||
| </actions> | ||||
							
								
								
									
										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> | ||||
| } | ||||
							
								
								
									
										0
									
								
								config/ags/launcher/ui/components/large.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								config/ags/launcher/ui/components/large.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								config/ags/launcher/ui/components/list.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								config/ags/launcher/ui/components/list.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								config/ags/launcher/ui/components/medium.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								config/ags/launcher/ui/components/medium.tsx
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										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, | ||||
| }; | ||||
							
								
								
									
										55
									
								
								config/astal/components/QuickActions/modules/Battery.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								config/astal/components/QuickActions/modules/Battery.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| 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)})`)} | ||||
|             /> | ||||
|             <label | ||||
|                 cssClasses={["battery-time"]} | ||||
|                 visible={bind(battery, "charging")} | ||||
|                 label={bind(battery, "timeToFull").as(t => `(${toTime(t)})`)} | ||||
|             /> | ||||
|         </box> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| const toTime = (time: number) => { | ||||
|     const MINUTE = 60; | ||||
|     const HOUR = MINUTE * 60; | ||||
|  | ||||
|     if (time > 24 * HOUR) return ""; | ||||
|  | ||||
|     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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										154
									
								
								config/astal/components/QuickActions/modules/Player/Player.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								config/astal/components/QuickActions/modules/Player/Player.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| 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 minutes = Math.floor(time / 60); | ||||
|     const hours = Math.floor(minutes / 60); | ||||
|     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, | ||||
| }; | ||||
							
								
								
									
										143
									
								
								config/astal/components/bar/modules/Hyprland.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								config/astal/components/bar/modules/Hyprland.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| import AstalTray from "gi://AstalTray"; | ||||
| import { bind, GObject } from "astal"; | ||||
| import AstalHyprland from "gi://AstalHyprland"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
|  | ||||
| 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 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, "iconName").as(icon => icon)} | ||||
|                 cssClasses={["quick-view-symbol"]} | ||||
|                 tooltipText={bind(battery, 'percentage').as(p => `${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; | ||||
| } | ||||
							
								
								
									
										83
									
								
								config/astal/util/notifd.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								config/astal/util/notifd.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| // ┌                                               ┐ | ||||
| // │  From https://github.com/Neurarian/matshell   │ | ||||
| // └                                               ┘ | ||||
|  | ||||
| import Notifd from "gi://AstalNotifd"; | ||||
| import { GLib } from "astal"; | ||||
| import { Gtk, Gdk } from "astal/gtk4"; | ||||
|  | ||||
| type TimeoutManager = { | ||||
|   setupTimeout: () => void; | ||||
|   clearTimeout: () => void; | ||||
|   handleHover: () => void; | ||||
|   handleHoverLost: () => void; | ||||
|   cleanup: () => void; | ||||
| }; | ||||
|  | ||||
| export const createTimeoutManager = ( | ||||
|   dismissCallback: () => void, | ||||
|   timeoutDelay: number, | ||||
| ): TimeoutManager => { | ||||
|   let isHovered = false; | ||||
|   let timeoutId: number | null = null; | ||||
|  | ||||
|   const clearTimeout = () => { | ||||
|     if (timeoutId !== null) { | ||||
|       GLib.source_remove(timeoutId); | ||||
|       timeoutId = null; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const setupTimeout = () => { | ||||
|     clearTimeout(); | ||||
|  | ||||
|     if (!isHovered) { | ||||
|       timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeoutDelay, () => { | ||||
|         clearTimeout(); | ||||
|         dismissCallback(); | ||||
|         return GLib.SOURCE_REMOVE; | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|     setupTimeout, | ||||
|     clearTimeout, | ||||
|     handleHover: () => { | ||||
|       isHovered = true; | ||||
|       clearTimeout(); | ||||
|     }, | ||||
|     handleHoverLost: () => { | ||||
|       isHovered = false; | ||||
|       setupTimeout(); | ||||
|     }, | ||||
|     cleanup: clearTimeout, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export const time = (time: number, format = "%H:%M") => | ||||
|   GLib.DateTime.new_from_unix_local(time).format(format)!; | ||||
|  | ||||
| export const urgency = (notification: Notifd.Notification) => { | ||||
|   const { LOW, NORMAL, CRITICAL } = Notifd.Urgency; | ||||
|  | ||||
|   switch (notification.urgency) { | ||||
|     case LOW: | ||||
|       return "low"; | ||||
|     case CRITICAL: | ||||
|       return "critical"; | ||||
|     case NORMAL: | ||||
|     default: | ||||
|       return "normal"; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| 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); | ||||
							
								
								
									
										3
									
								
								config/astal/util/state.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								config/astal/util/state.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import { Variable } from "astal"; | ||||
|  | ||||
| export const quickActionsState = Variable( false ); | ||||
							
								
								
									
										137
									
								
								config/fastfetch/config.jsonc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								config/fastfetch/config.jsonc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| { | ||||
|     "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", | ||||
|     "logo": { | ||||
|         "padding": { | ||||
|             "top": 2, | ||||
|             "left": 1, | ||||
|             "right": 2, | ||||
|         }, | ||||
|     }, | ||||
|     "display": { | ||||
|         "separator": "  ", | ||||
|     }, | ||||
|     "modules": [ | ||||
|         // Title | ||||
|         { | ||||
|             "type": "title", | ||||
|             "format": "{#1}╭───────────── {#}{user-name-colored}", | ||||
|         }, | ||||
|         // System Information | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│ {#}System Information", | ||||
|         }, | ||||
|         { | ||||
|             "type": "os", | ||||
|             "key": "{#separator}│  {#keys} OS", | ||||
|         }, | ||||
|         { | ||||
|             "type": "kernel", | ||||
|             "key": "{#separator}│  {#keys} Kernel", | ||||
|         }, | ||||
|         { | ||||
|             "type": "uptime", | ||||
|             "key": "{#separator}│  {#keys} Uptime", | ||||
|         }, | ||||
|         { | ||||
|             "type": "packages", | ||||
|             "key": "{#separator}│  {#keys} Packages", | ||||
|             "format": "{all}", | ||||
|         }, | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│", | ||||
|         }, | ||||
|         // Desktop Environment | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│ {#}Desktop Environment", | ||||
|         }, | ||||
|         { | ||||
|             "type": "de", | ||||
|             "key": "{#separator}│  {#keys} DE", | ||||
|         }, | ||||
|         { | ||||
|             "type": "wm", | ||||
|             "key": "{#separator}│  {#keys} WM", | ||||
|         }, | ||||
|         { | ||||
|             "type": "wmtheme", | ||||
|             "key": "{#separator}│  {#keys} Theme", | ||||
|         }, | ||||
|         { | ||||
|             "type": "display", | ||||
|             "key": "{#separator}│  {#keys} Resolution", | ||||
|         }, | ||||
|         { | ||||
|             "type": "shell", | ||||
|             "key": "{#separator}│  {#keys} Shell", | ||||
|         }, | ||||
|         { | ||||
|             "type": "terminalfont", | ||||
|             "key": "{#separator}│  {#keys} Font", | ||||
|         }, | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│", | ||||
|         }, | ||||
|         // Hardware Information | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│ {#}Hardware Information", | ||||
|         }, | ||||
|         { | ||||
|             "type": "cpu", | ||||
|             "key": "{#separator}│  {#keys} CPU", | ||||
|         }, | ||||
|         { | ||||
|             "type": "gpu", | ||||
|             "key": "{#separator}│  {#keys} GPU", | ||||
|         }, | ||||
|         { | ||||
|             "type": "memory", | ||||
|             "key": "{#separator}│  {#keys} Memory", | ||||
|         }, | ||||
|         { | ||||
|             "type": "disk", | ||||
|             "key": "{#separator}│  {#keys} Disk (/)", | ||||
|             "folders": "/", | ||||
|         }, | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│", | ||||
|         }, | ||||
|         // Network | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│ {#}Network", | ||||
|         }, | ||||
|         { | ||||
|             "type": "wifi", | ||||
|             "key": "{#separator}│  {#keys}⮃ WiFi", | ||||
|         }, | ||||
|         { | ||||
|             "type": "dns", | ||||
|             "key": "{#separator}│  {#keys}🖧 DNS", | ||||
|         }, | ||||
|         { | ||||
|             "type": "localip", | ||||
|             "key": "{#separator}│  {#keys}⍰ IP", | ||||
|         }, | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}│", | ||||
|         }, | ||||
|         // Colors | ||||
|         { | ||||
|             "type": "colors", | ||||
|             "key": "{#separator}│", | ||||
|             "symbol": "circle", | ||||
|         }, | ||||
|         // Footer | ||||
|         { | ||||
|             "type": "custom", | ||||
|             "format": "{#1}╰───────────────────────────────╯", | ||||
|         }, | ||||
|     ], | ||||
| } | ||||
							
								
								
									
										126
									
								
								config/fish/config.fish
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										126
									
								
								config/fish/config.fish
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| alias ls='ls -l --color' | ||||
| alias ll='ls -la --color' | ||||
| alias v='nvim' | ||||
| alias c='clear' | ||||
| alias zs='zathura-sandbox' | ||||
| alias bt='bluetui' | ||||
| alias vicfg='cd ~/.config/nvim/ && nvim' | ||||
| alias fm='thunar .' | ||||
| alias gl='git ls-files --others --exclude-standard' | ||||
| alias gm='gti ls-files -m' | ||||
| alias gpu='git push' | ||||
| alias gp='git pull' | ||||
| alias gc='git commit -a -m' | ||||
| alias ga='git add ./*' | ||||
| alias cfh='nvim ~/projects/active/dotfiles/config/hypr/' | ||||
| alias cfn='nvim ~/projects/active/nvim/' | ||||
| alias cff='nvim ~/projects/active/dotfiles/config/fish/' | ||||
| alias cfw='nvim ~/projects/archive/dotfiles-old/pc/configs/waybar/' | ||||
| alias cfa='nvim ~/projects/active/dotfiles/config/ags/' | ||||
| alias cf='nvim ~/projects/active/dotfiles/' | ||||
| alias g='lazygit' | ||||
| alias open-webui='sudo systemctl start docker && sudo docker start -i open-webui' | ||||
| alias ai='ollama serve' | ||||
| alias ff='fastfetch' | ||||
|  | ||||
| # Add scripts in ~/projects/active/dotfiles/general/scripts/ to path | ||||
| fish_add_path -P ~/projects/active/dotfiles/scripts/ | ||||
|  | ||||
| function y | ||||
| 	set tmp (mktemp -t "yazi-cwd.XXXXXX") | ||||
| 	yazi $argv --cwd-file="$tmp" | ||||
| 	if set cwd (command cat -- "$tmp"); and [ -n "$cwd" ]; and [ "$cwd" != "$PWD" ] | ||||
| 		builtin cd -- "$cwd" | ||||
| 	end | ||||
| 	rm -f -- "$tmp" | ||||
| end | ||||
|  | ||||
| zoxide init --cmd j fish | source | ||||
| # [ -f /usr/share/autojump/autojump.fish ]; and source /usr/share/autojump/autojump.fish | ||||
|  | ||||
| if status is-interactive | ||||
| 	function fish_prompt | ||||
|  | ||||
|   # Cache exit status | ||||
|   set -l last_status $status | ||||
|  | ||||
|   # Set color for variables in prompt | ||||
|   set -l normal (set_color normal) | ||||
|   set -l white (set_color FFFFFF) | ||||
|   set -l turquoise (set_color 5fdfff) | ||||
|   set -l orange (set_color df5f00) | ||||
|   set -l hotpink (set_color df005f) | ||||
|   set -l blue (set_color blue) | ||||
|   set -l limegreen (set_color 87ff00) | ||||
|   set -l purple (set_color af5fff) | ||||
|   set -l red (set_color e70e0e) | ||||
|  | ||||
|   # Configure __fish_git_prompt | ||||
|   set -g __fish_git_prompt_char_stateseparator ' ' | ||||
|   set -g __fish_git_prompt_color 5fdfff | ||||
|   set -g __fish_git_prompt_color_flags df5f00 | ||||
|   set -g __fish_git_prompt_color_prefix white | ||||
|   set -g __fish_git_prompt_color_suffix white | ||||
|   set -g __fish_git_prompt_showdirtystate true | ||||
|   set -g __fish_git_prompt_showuntrackedfiles true | ||||
|   set -g __fish_git_prompt_showstashstate true | ||||
|  | ||||
|   set -g __fish_git_prompt_show_informative_status true  | ||||
|  | ||||
|  | ||||
|   # Only calculate once, to save a few CPU cycles when displaying the prompt | ||||
|   if not set -q __fish_prompt_hostname | ||||
|     # set -g __fish_prompt_hostname (hostname|cut -d . -f 1) | ||||
|     set -g __fish_prompt_hostname $orange(prompt_hostname)(set_color normal) | ||||
|   end | ||||
|   if not set -q __fish_prompt_char | ||||
|     if [ (id -u) -eq 0 ] | ||||
|       set -g __fish_prompt_char (set_color red)'λ'(set_color normal) | ||||
|     else   | ||||
|       set -g __fish_prompt_char 'λ' | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   # change `at` to `ssh` when an interactive ssh session is present | ||||
|   if [ "$SSH_TTY" = "" ] | ||||
|     set -g location at | ||||
|     # set -g __fish_prompt_hostname (set_color orange)(hostname|cut -d . -f 1) | ||||
|   else # connected via ssh | ||||
|     if [ "$TERM" = "xterm-256color-italic" -o "$TERM" = "tmux-256color" ] | ||||
|       set -g location (echo -e "\e[3mssh\e[23m") | ||||
|       # set -g ssh_hostname (echo -e $blue$__fish_prompt_hostname) | ||||
|       set -g __fish_prompt_hostname $blue(prompt_hostname)(set_color normal) | ||||
|     else | ||||
|       set -g location ssh | ||||
|       # set -g ssh_hostname (echo -e $blue$__fish_prompt_hostname) | ||||
|       set -g __fish_prompt_hostname $blue(prompt_hostname)(set_color normal) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   if [ (id -u) -eq 0 ] | ||||
|     # top line > Superuser | ||||
|     echo -n $red'╭─'$hotpink$USER $white$location $__fish_prompt_hostname$white' in '$limegreen(pwd)$turquoise | ||||
|     __fish_git_prompt " (%s)" | ||||
|     echo | ||||
|     # bottom line > Superuser | ||||
|     echo -n $red'╰' | ||||
|     echo -n $red'─'$__fish_prompt_char $normal | ||||
|   else # top line > non superuser's | ||||
|     echo -n $white'╭─'$hotpink$USER $white$location $__fish_prompt_hostname$white' in '$limegreen(pwd)$turquoise | ||||
|     __fish_git_prompt " (%s)" | ||||
|     echo | ||||
|     # bottom line > non superuser's | ||||
|     echo -n $white'╰' | ||||
|     echo -n $white'─'$__fish_prompt_char $normal | ||||
|   end | ||||
|    | ||||
|   # NOTE: disable `VIRTUAL_ENV_DISABLE_PROMPT` in `config.fish` | ||||
|   # see:  https://virtualenv.pypa.io/en/latest/reference/#envvar-VIRTUAL_ENV_DISABLE_PROMPT | ||||
|   # support for virtual env name | ||||
|   if set -q VIRTUAL_ENV | ||||
|       echo -n "($turquoise"(basename "$VIRTUAL_ENV")"$white)" | ||||
|   end | ||||
| end | ||||
|  | ||||
|  | ||||
| end | ||||
							
								
								
									
										44
									
								
								config/hypr/hypridle.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								config/hypr/hypridle.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| #░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | ||||
| #░   ░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░   ░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░ | ||||
| #▒   ▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒   ▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒ | ||||
| #▒   ▒▒▒▒   ▒   ▒▒▒   ▒  ▒   ▒▒▒  ▒    ▒▒▒▒▒▒▒▒▒▒   ▒▒   ▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒   ▒   ▒▒▒    ▒  ▒▒▒▒▒▒▒     ▒▒ | ||||
| #▓          ▓▓   ▓   ▓▓  ▓▓   ▓▓▓   ▓▓▓▓   ▓▓   ▓   ▓▓   ▓▓  ▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓    ▓▓▓  ▓▓▓   ▓▓   ▓▓▓▓   ▓  ▓▓▓   ▓▓   ▓         ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓  ▓▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓▓   ▓▓▓   ▓   ▓▓▓   ▓▓▓▓   ▓  ▓▓▓   ▓▓   ▓  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓    ▓    | ||||
| #█   ████   ████   ████   ██████    ████   ██   █   ██   ███     █████████████     ██████   █████    ██   ███   ████   █████   █ | ||||
| #██████████████   █████   ████████████████████████████████████████████████████████████████████████████████████████████████    ██ | ||||
|  | ||||
| general { | ||||
|     lock_cmd = hyprlock --immediate | ||||
|     unlock_cmd = loginctl unlock-session | ||||
|     before_sleep_cmd = hyprlock --immediate | ||||
|     after_sleep_cmd = hyprlock --immediate | ||||
| } | ||||
|  | ||||
| listener { | ||||
|     timeout = 100 | ||||
|     on-timeout = notify-send "Entering idle state... (200s to screen off)" | ||||
|     on-resume = notify-send "Welcome back!" | ||||
| } | ||||
|  | ||||
| listener { | ||||
|     timeout = 200 | ||||
|     on-timeout = notify-send "100s to screen off" | ||||
| } | ||||
|  | ||||
| listener { | ||||
|     timeout = 210 | ||||
|     on-timeout = hyprlock | ||||
| } | ||||
|  | ||||
| listener { | ||||
|     timeout = 300 | ||||
|     on-timeout = hyprctl dispatch dpms off | ||||
|     on-resume = hyprctl dispatch dpms on | ||||
| } | ||||
|  | ||||
| listener { | ||||
|     timeout = 600 | ||||
|     on-timeout = systemctl suspend | ||||
|     on-resume = hyprctl dispatch dpms on | ||||
| } | ||||
							
								
								
									
										210
									
								
								config/hypr/hyprland/binds.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								config/hypr/hyprland/binds.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                    KEYBINDS                    │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| $mainMod = SUPER | ||||
| # These key-binds are non-specific, laptop config has a separate file that includes  | ||||
| # extra config for it specifically | ||||
| bind = $mainMod, Q, killactive | ||||
| # bind = $mainMod SHIFT, Q, exit | ||||
| bind = $mainMod, Return, exec, kitty | ||||
| # bind = $mainMod, E, exec, thunar | ||||
| bind = $mainMod SHIFT, E, exec, thunar | ||||
| bind = $mainMod, E, exec, kitty --hold fish -c "y" | ||||
| bind = $mainMod, V, togglefloating, | ||||
| bind = $mainMod, F, fullscreen, | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                      AGS                       │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                 Notifications                  │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, N, exec, astal -i notifier toggle | ||||
| bind = $mainMod, C, exec, astal -i notifier clear-newest | ||||
| bind = $mainMod SHIFT, C, exec, astal -i notifier clear | ||||
| bind = $mainMod SHIFT, N, exec, astal -i notifier show | ||||
| bind = $mainMod CTRL SHIFT, N, exec, astal -i notifier hide | ||||
|  | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                    General                     │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                Launch commands                 │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod SHIFT, L, exec, librewolf | ||||
| bind = $mainMod SHIFT, D, exec, vesktop | ||||
| bind = $mainMod SHIFT, V, exec, vscodium | ||||
| bind = $mainMod SHIFT, T, exec, thunderbird | ||||
| bind = $mainMod SHIFT, H, exec, heroic | ||||
| bind = $mainMod SHIFT, Z, exec, zathura | ||||
| bind = $mainMod SHIFT, I, exec, notify-send 'AirPlay video server starting...' --app-name="AirPlay Video" && terminator -e "systemctl start avahi-daemon && sleep 5 && uxplay -n LinuxVideoPlay -nh" | ||||
| bind = $mainMod SHIFT, A, exec, notify-send 'AirPlay server starting...' --app-name="AirPlay Audio" && terminator -T "hidden-terminator" -e "systemctl start avahi-daemon && shairport-sync -a LinuxPlay" | ||||
| bind = $mainMod SHIFT, G, exec, notify-send 'Preparing system for gaming...' --app-name="Util" && corectrl | ||||
| bind = $mainMod SHIFT, P, exec, notify-send 'Steam is launching...' --app-name="Steam" && steam | ||||
| bind = $mainMod SHIFT, R, exec, notify-send 'Launching in Remoteplay optimised session' --app-name="Steam" && steam -pipewire | ||||
| bind = $mainMod CTRL, K, exec, notify-send 'Insta-Kill activated' --app-name="Hyprctl" && hyprctl kill | ||||
| bind = $mainMod SHIFT, O, exec, terminator -e "~/projects/dotfiles/ai.sh" | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                  Screenshots                   │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod SHIFT, S, exec, grimblast --notify copy area | ||||
| bind = $mainMod CTRL, S, exec, grimblast --notify copysave area | ||||
| bind = $mainMod ALT, S, exec, grimblast --notify save area | ||||
| bind = , PRINT, exec, grimblast --notify copy screen | ||||
| bind = CTRL, PRINT, exec, grimblast --notify copysave screen | ||||
| bind = SHIFT, PRINT, exec, grimblast --notify save screen | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                 Rofi commands                  │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, Space, exec, killall rofi || rofi -show combi -modes combi -combi-modes "window,drun,run" | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                Logout commands                 │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, escape, exec, wlogout | ||||
| bind = $mainMod ALT CTRL, L, exec, hyprlock --immediate | ||||
|  | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                  Layout binds                  │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │      Move focus with mainMod + arrow keys      │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, left, movefocus, l | ||||
| bind = $mainMod, right, movefocus, r | ||||
| bind = $mainMod, up, movefocus, u | ||||
| bind = $mainMod, down, movefocus, d | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │             Same with vim-motions              │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, h, movefocus, l | ||||
| bind = $mainMod, l, movefocus, r | ||||
| bind = $mainMod, j, movefocus, d | ||||
| bind = $mainMod, k, movefocus, u | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │     Switch workspaces with mainMod + [0-9]     │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, 1, workspace, 1 | ||||
| bind = $mainMod, 2, workspace, 2 | ||||
| bind = $mainMod, 3, workspace, 3 | ||||
| bind = $mainMod, 4, workspace, 4 | ||||
| bind = $mainMod, 5, workspace, 5 | ||||
| bind = $mainMod, 6, workspace, 6 | ||||
| bind = $mainMod, 7, workspace, 7 | ||||
| bind = $mainMod, 8, workspace, 8 | ||||
| bind = $mainMod, 9, workspace, 9 | ||||
| bind = $mainMod, 0, workspace, 10 | ||||
| bind = $mainMod CTRL, left, workspace, e-1 | ||||
| bind = $mainMod CTRL, right, workspace, e+1 | ||||
| bind = $mainMod CTRL, h, workspace, e-1 | ||||
| bind = $mainMod CTRL, l, workspace, e+1 | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │     Move active window to a workspace with     │ | ||||
| # │            mainMod + SHIFT + [0-9]             │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod SHIFT, 1, movetoworkspace, 1 | ||||
| bind = $mainMod SHIFT, 2, movetoworkspace, 2 | ||||
| bind = $mainMod SHIFT, 3, movetoworkspace, 3 | ||||
| bind = $mainMod SHIFT, 4, movetoworkspace, 4 | ||||
| bind = $mainMod SHIFT, 5, movetoworkspace, 5 | ||||
| bind = $mainMod SHIFT, 6, movetoworkspace, 6 | ||||
| bind = $mainMod SHIFT, 7, movetoworkspace, 7 | ||||
| bind = $mainMod SHIFT, 8, movetoworkspace, 8 | ||||
| bind = $mainMod SHIFT, 9, movetoworkspace, 9 | ||||
| bind = $mainMod SHIFT, 0, movetoworkspace, 10 | ||||
| bind = $mainMod SHIFT, left, movetoworkspace, e-1 | ||||
| bind = $mainMod SHIFT, right, movetoworkspace, e+1 | ||||
| bind = $mainMod SHIFT, j, movetoworkspace, e-1 | ||||
| bind = $mainMod SHIFT, k, movetoworkspace, e+1 | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │    Scroll through existing workspaces with     │ | ||||
| # │                mainMod + scroll                │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod, mouse_down, workspace, e+1 | ||||
| bind = $mainMod, mouse_up, workspace, e-1 | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │      Tile window to a part of the screen       │ | ||||
| # └                                                ┘ | ||||
| # bind = $mainMod CTRL, left, movewindow, left | ||||
| # bind = $mainMod CTRL, right, movewindow, right | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │   move to next window / previous window with   │ | ||||
| # │         ALT + Tab / SHIFT + ALT + Tab          │ | ||||
| # └                                                ┘ | ||||
| bind = ALT SHIFT, tab, cyclenext, prev | ||||
| # bind = ALT, tab, cyclenext, next | ||||
| # bind = ALT CTRL, tab, focusurgentorlast | ||||
| bind = ALT, tab, focusurgentorlast | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │             Master layout commands             │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod CTRL, M, layoutmsg, swapwithmaster | ||||
| bind = $mainMod SHIFT, A, layoutmsg, addmaster | ||||
| bind = $mainMod SHIFT CTRL, right, layoutmsg, orientationnext | ||||
| bind = $mainMod SHIFT CTRL, left, layoutmsg, orientationprev | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │   Move/resize windows with mainMod + LMB/RMB   │ | ||||
| # │                  and dragging                  │ | ||||
| # └                                                ┘ | ||||
| bindm = $mainMod, mouse:272, movewindow | ||||
| bindm = $mainMod, mouse:273, resizewindow | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                     Freeze                     │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod SHIFT, escape, exec, hyprfreeze -a | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                 Volume control                 │ | ||||
| # └                                                ┘ | ||||
| bind = ,code:123, exec, pamixer -i 5 | ||||
| bind = ,code:122, exec, pamixer -d 5 | ||||
| bind = ,code:121, exec, pamixer -t | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │               Brightness-Control               │ | ||||
| # └                                                ┘ | ||||
| bind = ,code:232, exec, light -U 5 && notify-send 'Display brightness decreased by 5%' --app-name="Brightness" | ||||
| bind = ,code:233, exec, light -A 5 && notify-send 'Display brightness increased by 5%' --app-name="Brightness" | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │              Monitor config binds              │ | ||||
| # └                                                ┘ | ||||
| bind = $mainMod CTRL, D, exec, hyprctl keyword monitor HDMI-A-1, 1280x720@60, 1920x0, 1, mirror, DP-1 && notify-send 'Set FPV goggles to mirror main screen' --app-name="Hyprctl" | ||||
| bind = $mainMod CTRL, E, exec, hyprctl keyword monitor HDMI-A-1, 1280x720@60, 3840x0, 1 && notify-send 'Set to expand FPV goggles' --app-name="Hyprctl" | ||||
							
								
								
									
										18
									
								
								config/hypr/hyprland/colors.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								config/hypr/hyprland/colors.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                    COLOURS                     │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| exec = swaybg -m fill -i /home/janis/NextCloud/Wallpapers/dark/colour-explosion.jpg | ||||
|  | ||||
| general { | ||||
|     col.active_border = rgba(591641cc) rgba(97103acc) rgba(2d2057cc) 45deg | ||||
|     col.inactive_border = rgb(000000) | ||||
| } | ||||
|  | ||||
| decoration { | ||||
|     shadow { | ||||
|         color = rgb(282828) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										137
									
								
								config/hypr/hyprland/general.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								config/hypr/hyprland/general.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │             LAUNCHING OF PROGRAMS              │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| source = ./colors.conf | ||||
| exec-once = ~/.config/hypr/xdg-portal-hyprland | ||||
| exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XAUTHORITY DISPLAY | ||||
| exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP | ||||
| exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 | ||||
| # exec-once = waybar | ||||
| exec-once = hypridle | ||||
| exec-once = nm-applet | ||||
| exec-once = nextcloud --background | ||||
| # exec-once = sleep 2 && bash -c "ags run -d ~/projects/active/dotfiles/config/astal/ --gtk4 >> ~/log 2>&1" | ||||
| exec-once = sleep 2 && bash -c "ags run -d ~/projects/active/dotfiles/config/astal/ --gtk4" | ||||
| # exec-once = bash -c "ags run -d ~/projects/active/dotfiles/config/ags/notifications/ >> ~/logn 2>&1" | ||||
| exec-once = bash -c "ags run -d ~/projects/active/dotfiles/config/ags/notifications/" | ||||
|  | ||||
|  | ||||
|  | ||||
| exec = hyprctl setcursor oreo_spark_blue_cursors 24 | ||||
| env = QT_QPA_PLATFORM,wayland | ||||
| env = QT_QPA_PLATFORM_THEME,qt6ct | ||||
| env = OLLAMA_HOST,0.0.0.0 | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                     INPUT                      │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| input { | ||||
|     kb_layout = ch | ||||
|     kb_options = caps:swapescape | ||||
|     natural_scroll = true | ||||
|  | ||||
|     numlock_by_default = true | ||||
|  | ||||
|     follow_mouse = 2 | ||||
|     mouse_refocus = true | ||||
|  | ||||
|     touchpad { | ||||
|         disable_while_typing = true | ||||
|         natural_scroll = true | ||||
|     } | ||||
|  | ||||
|     focus_on_close = 1 | ||||
|  | ||||
|     sensitivity = 0 # -1.0 - 1.0, 0 means no modification. | ||||
| } | ||||
|  | ||||
| gestures { | ||||
|     # See https://wiki.hyprland.org/Configuring/Variables/ for more | ||||
|     workspace_swipe = true | ||||
|     workspace_swipe_distance = 300 | ||||
| } | ||||
|  | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                 GENERAL CONFIG                 │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| general { | ||||
|     # See https://wiki.hyprland.org/Configuring/Variables/ for more | ||||
|  | ||||
|     gaps_in = 3 | ||||
|     gaps_out = 4 | ||||
|     border_size = 1 | ||||
|     # col.active_border = rgba(cc5c00ff) rgba(cc5c00ff) rgba(ff0000ff) 45deg | ||||
|     # col.inactive_border = rgba(595959aa) | ||||
|  | ||||
|     layout = master | ||||
|     no_border_on_floating = false | ||||
| } | ||||
|  | ||||
| decoration { | ||||
|     # See https://wiki.hyprland.org/Configuring/Variables/ for more | ||||
|     inactive_opacity = 1 | ||||
|     rounding = 10 | ||||
|  | ||||
|     shadow { | ||||
|         enabled = true | ||||
|         range = 4 | ||||
|         # color = rgba(1a1a1aee) | ||||
|     } | ||||
|  | ||||
|     blur { | ||||
|         enabled = true | ||||
|         xray = true | ||||
|         new_optimizations = true | ||||
|         size = 1 | ||||
|         passes = 2 | ||||
|     } | ||||
|     dim_inactive = true | ||||
|     dim_strength = 0.1 | ||||
| } | ||||
|  | ||||
| animations { | ||||
|     enabled = yes | ||||
|  | ||||
|     # Some default animations, see https://wiki.hyprland.org/Configuring/Animations/ for more | ||||
|  | ||||
|     bezier = myBezier, 0.05, 0.9, 0.1, 1.05 | ||||
|  | ||||
|     animation = windows, 1, 7, myBezier | ||||
|     animation = windowsOut, 1, 7, default, popin 80% | ||||
|     animation = border, 1, 10, default | ||||
|     animation = fade, 1, 7, default | ||||
|     animation = workspaces, 1, 6, default | ||||
| } | ||||
|  | ||||
| misc { | ||||
|     disable_hyprland_logo = true | ||||
|     disable_splash_rendering = false | ||||
|     vrr = 2 | ||||
|     allow_session_lock_restore = true | ||||
| } | ||||
|  | ||||
| dwindle { | ||||
|     # See https://wiki.hyprland.org/Configuring/Dwindle-Layout/ for more | ||||
|     pseudotile = yes # master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below | ||||
|     preserve_split = yes # you probably want this | ||||
| } | ||||
|  | ||||
| master { | ||||
|     # See https://wiki.hyprland.org/Configuring/Master-Layout/ for more | ||||
| #     no_gaps_when_only = true | ||||
| } | ||||
|  | ||||
|  | ||||
| cursor { | ||||
|     no_warps = false | ||||
|     inactive_timeout = 60 | ||||
| } | ||||
|  | ||||
							
								
								
									
										121
									
								
								config/hypr/hyprland/windowrules.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								config/hypr/hyprland/windowrules.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                WORKSPACE RULES                 │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| # Display full sized (without gaps), if only window on screen | ||||
| # workspace = w[tv1], gapsout:0, gapsin:0 | ||||
| # workspace = f[1], gapsout:0, gapsin:0 | ||||
| # windowrule = bordersize 0, floating:0, onworkspace:w[tv1] | ||||
| # windowrule = rounding 0, floating:0, onworkspace:w[tv1] | ||||
| # windowrule = bordersize 0, floating:0, onworkspace:f[1] | ||||
| # windowrule = rounding 0, floating:0, onworkspace:f[1] | ||||
|  | ||||
| $mainMod = SUPER | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                  WINDOW RULES                  │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| windowrule = float, title:.*(rofi).* | ||||
| windowrule = animation popin, title:.*(rofi).* | ||||
| windowrule = center, title:(rofi)(.*) | ||||
|  | ||||
| windowrule = move 1450 50, title:^(.*)(Power menu)$ | ||||
| windowrule = workspace 2, class:evince | ||||
| windowrule = workspace 2, title:.*(Okular).* | ||||
| windowrule = workspace 2, class:org.pwmt.zathura | ||||
| windowrule = fullscreen, title:wlogout | ||||
| windowrule = workspace 2, class:librewolf | ||||
| windowrule = workspace 2, title:LibreWolf | ||||
| windowrule = workspace 2, title:(.*)(Discord)(.*) | ||||
| windowrule = workspace 3, title:^(Steam)(.*)$ | ||||
| windowrule = workspace 1, title:^(.*)(VSCodium)$ | ||||
| windowrule = center, title:^(.*)(VSCodium)$ | ||||
| windowrule = workspace 3, class:minecraft-launcher | ||||
| windowrule = tile, class:minecraft-launcher | ||||
| windowrule = fullscreen, title:^(.*)(Minecraft)(.*)$ | ||||
| windowrule = workspace 3, title:^(.*)(Minecraft)(.*)$ | ||||
|  | ||||
| windowrule = workspace 1, title:^(.*)hidden-terminator*(.*)$ | ||||
| windowrule = size 0 0, title:^(.*)hidden-terminator*(.*)$ | ||||
| windowrule = move 0 0, title:^(.*)hidden-terminator*(.*)$ | ||||
| windowrule = float, title:^(.*)hidden-terminator*(.*)$ | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │          Set rule for yazi filepicker          │ | ||||
| # └                                                ┘ | ||||
| windowrule = float, title:^(.*)termfilechooser*(.*)$ | ||||
| windowrule = size 1400 800, title:^(.*)termfilechooser*(.*)$ | ||||
| windowrule = center, title:^(.*)termfilechooser*(.*)$ | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │ Set floating windows & position them centered  │ | ||||
| # └                                                ┘ | ||||
| windowrule = float, class:file_progress | ||||
| windowrule = center, class:file_progress | ||||
|  | ||||
| windowrule = float, class:confirm | ||||
| windowrule = center, class:confirm | ||||
|  | ||||
| windowrule = float, class:dialog | ||||
| windowrule = center, class:dialog | ||||
|  | ||||
| windowrule = float, class:download | ||||
| windowrule = center, class:download | ||||
|  | ||||
| windowrule = float, class:notification | ||||
| windowrule = center, class:notification | ||||
|  | ||||
| windowrule = float, class:error | ||||
| windowrule = center, class:error | ||||
|  | ||||
| windowrule = float, class:splash | ||||
| windowrule = center, class:splash | ||||
|  | ||||
| windowrule = float, class:confirmreset | ||||
| windowrule = center, class:confirmreset | ||||
|  | ||||
| windowrule = float, title:Open File | ||||
| windowrule = center, title:Open File | ||||
|  | ||||
| windowrule = float, title:branchdialog | ||||
| windowrule = center, title:branchdialog | ||||
|  | ||||
| windowrule = float, class:Lxappearance | ||||
| windowrule = center, class:Lxappearance | ||||
|  | ||||
| windowrule = float, title:^(Media viewer)$ | ||||
| windowrule = center, title:^(Media viewer)$ | ||||
|  | ||||
| windowrule = float, title:^(Volume Control)$ | ||||
| windowrule = center, title:^(Volume Control)$ | ||||
|  | ||||
| windowrule = float, title:^(Picture-in-Picture)$ | ||||
| windowrule = center, title:^(Picture-in-Picture)$ | ||||
|  | ||||
| windowrule = float, title:^(File Operation Progress)$ | ||||
| windowrule = center, title:^(File Operation Progress)$ | ||||
|  | ||||
| windowrule = float, title:^(Loading)(.*)$ | ||||
| windowrule = center, title:^(Loading)(.*)$ | ||||
|  | ||||
| windowrule = float, class:pavucontrol-qt | ||||
| windowrule = center, class:pavucontrol-qt | ||||
|  | ||||
| windowrule = float, class:pavucontrol | ||||
| windowrule = center, class:pavucontrol | ||||
|  | ||||
| windowrule = float, class:file-roller | ||||
| windowrule = center, class:file-roller | ||||
|  | ||||
| windowrule = idleinhibit focus, title:^(Rocket League)(.*)$ | ||||
| windowrule = fullscreen, title:^(Steam Big Picture)$ | ||||
| windowrule = idleinhibit always, class:steam | ||||
| windowrule = idleinhibit always, class:lutris | ||||
| windowrule = idleinhibit focus, class:vlc | ||||
| windowrule = idleinhibit focus, class:supertuxkart | ||||
| windowrule = idleinhibit fullscreen, title:^(.*)(Discord)(.*)$ | ||||
| windowrule = idleinhibit fullscreen, title:^(.*)(~)(.*)$ | ||||
							
								
								
									
										41
									
								
								config/hypr/hyprland_desktop.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								config/hypr/hyprland_desktop.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| #░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | ||||
| #░   ░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░ | ||||
| #▒   ▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒   ▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒ | ||||
| #▒   ▒▒▒▒   ▒   ▒▒▒   ▒  ▒   ▒▒▒  ▒    ▒   ▒▒▒▒   ▒▒▒▒▒   ▒   ▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒   ▒   ▒▒▒    ▒  ▒▒▒▒▒▒▒     ▒▒ | ||||
| #▓          ▓▓   ▓   ▓▓  ▓▓   ▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓   ▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓    ▓▓▓  ▓▓▓   ▓▓   ▓▓▓▓   ▓   ▓▓▓   ▓▓▓   ▓▓   ▓  ▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓  ▓▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓▓   ▓▓▓   ▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓▓   ▓▓▓   ▓▓   ▓  ▓▓▓   ▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓    ▓    | ||||
| #█   ████   ████   ████   ██████    ████   ███   █    █    ██   ██   █   ███████████     ██████   █████    ██   ███   ████   █████   █ | ||||
| #██████████████   █████   ██████████████████████████████████████████████████████████████████████████████████████████████████████    ██ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                    MONITORS                    │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| # monitor=DP-1, preferred, 0x0, 1, vrr, 2 | ||||
| monitor=DP-1, 1920x1080@144, 0x0, 1, vrr, 2 | ||||
| monitor=DP-2, 1920x1080@75, 1920x0, 1 | ||||
| # monitor=,highres highrr, auto, 1 | ||||
|  | ||||
|  | ||||
| # exec = swaybg -m fill -i /mnt/storage/SORTED/Pictures/Wallpapers/wallpaper/arch-bg-matterhorn.jpg | ||||
| # exec = swaybg -m fill -i /mnt/storage/SORTED/Pictures/Wallpapers/McLaren/main_livery_upscaled.jpg | ||||
|  | ||||
|  | ||||
| source=./hyprland/binds.conf | ||||
| source=./hyprland/general.conf | ||||
| source=./hyprland/windowrules.conf | ||||
|  | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                    DESKTOP                     │ | ||||
| # └                                                ┘ | ||||
| workspace = 1, monitor:DP-1, default:1 | ||||
| workspace = 2, monitor:DP-2, default:1 | ||||
| workspace = 3, monitor:DP-1 | ||||
| workspace = 4, monitor:DP-2 | ||||
							
								
								
									
										79
									
								
								config/hypr/hyprland_laptop.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								config/hypr/hyprland_laptop.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| #░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | ||||
| #░   ░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░ | ||||
| #▒   ▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒   ▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒ | ||||
| #▒   ▒▒▒▒   ▒   ▒▒▒   ▒  ▒   ▒▒▒  ▒    ▒   ▒▒▒▒   ▒▒▒▒▒   ▒   ▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒   ▒   ▒▒▒    ▒  ▒▒▒▒▒▒▒     ▒▒ | ||||
| #▓          ▓▓   ▓   ▓▓  ▓▓   ▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓   ▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓    ▓▓▓  ▓▓▓   ▓▓   ▓▓▓▓   ▓   ▓▓▓   ▓▓▓   ▓▓   ▓  ▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓  ▓▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓▓   ▓▓▓   ▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓▓   ▓▓▓   ▓▓   ▓  ▓▓▓   ▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓    ▓    | ||||
| #█   ████   ████   ████   ██████    ████   ███   █    █    ██   ██   █   ███████████     ██████   █████    ██   ███   ████   █████   █ | ||||
| #██████████████   █████   ██████████████████████████████████████████████████████████████████████████████████████████████████████    ██ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
| #          ╭────────────────────────────────────────────────╮ | ||||
| #          │                    MONITORS                    │ | ||||
| #          ╰────────────────────────────────────────────────╯ | ||||
| # ──────────────────────────────────────────────────────────────────── | ||||
|  | ||||
| monitor=eDP-1, 2880x1800@60, 0x0, 1.5 | ||||
| # monitor=,highres highrr, auto, 1 | ||||
|  | ||||
|  | ||||
| # exec = swaybg -m fill -i /home/janis/Pictures/arch-bg.png | ||||
|  | ||||
|  | ||||
| source=./hyprland/binds.conf | ||||
| source=./hyprland/general.conf | ||||
| source=./hyprland/windowrules.conf | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                     LAPTOP                     │ | ||||
| # └                                                ┘ | ||||
| exec = hyprctl setcursor oreo_spark_blue_cursors 36 | ||||
| env = HYPRCURSOR_THEME, Oreo_spark_blue_cursor | ||||
| env = X_CURSOR_THEME, Oreo_spark_blue_cursor | ||||
| env = XCURSOR_SIZE,24 | ||||
| env = ELECTRON_ENABLE_HIGHDPI_SUPPORT, 1 | ||||
| env = XDG_SESSION_TYPE, wayland | ||||
| env = QT_QPA_PLATFORM,wayland | ||||
| # env = QT_QPA_PLATFORM_THEME,qt6ct | ||||
| env = ELECTRON_OZONE_PLATFORM_HINT,wayland | ||||
| env = GDK_SCALE,2 | ||||
| exec-once = hyprpm reload -nn | ||||
|  | ||||
| xwayland { | ||||
|     force_zero_scaling = true | ||||
| } | ||||
|  | ||||
| # ── Volume control ────────────────────────────────────────────────── | ||||
| bind = ,code:123, exec, pamixer -i 5 | ||||
| bind = ,code:122, exec, pamixer -d 5 | ||||
| bind = ,code:121, exec, pamixer -t | ||||
|  | ||||
|  | ||||
| # ── Brightness-Control ────────────────────────────────────────────── | ||||
| bind = ,code:232, exec, light -U 5 && notify-send 'Display brightness decreased by 5%' | ||||
| bind = ,code:233, exec, light -A 5 && notify-send 'Display brightness increased by 5%' | ||||
|  | ||||
|  | ||||
| # ── Monitor config binds ──────────────────────────────────────────── | ||||
| bind = $mainMod CTRL, D, exec, hyprctl keyword monitor HDMI-A-1, 1920x1080@60, 1920x0, 1, mirror, eDP-1 && notify-send 'Set to mirror internal display' | ||||
| bind = $mainMod CTRL, E, exec, hyprctl keyword monitor HDMI-A-1, 1920x1080@60, 1920x0, 1 && notify-send 'Set to expand external display' | ||||
|  | ||||
|  | ||||
| # ── Internal display controls ─────────────────────────────────────── | ||||
| bind = $mainMod ALT, E, exec, hyprctl keyword monitor eDP-1, 2880x1800@60, 0x0, 1.5 && cpupower-gui -b && notify-send 'Set to battery optimized settings' | ||||
| bind = $mainMod ALT, P, exec, hyprctl keyword monitor eDP-1, 2880x1800@120, 0x0, 1.5 && cpupower-gui -p && notify-send 'Set to performance optimized settings' | ||||
|  | ||||
| <<<<<<< HEAD:laptop/configs/hypr/hyprland.conf | ||||
| # Vivado inversion | ||||
|  | ||||
| ======= | ||||
|  | ||||
| # ── Vivado inversion ──────────────────────────────────────────────── | ||||
| >>>>>>> v2:config/hypr/hyprland_laptop.conf | ||||
| windowrule = plugin:invertwindow, class:Vivado | ||||
| windowrule = tile, title:(.*)Vivado(.*) | ||||
							
								
								
									
										100
									
								
								config/hypr/hyprlock.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								config/hypr/hyprlock.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| #░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | ||||
| #░   ░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   ░░░░░░░░░░░░░░ | ||||
| #▒   ▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒ | ||||
| #▒   ▒▒▒▒   ▒   ▒▒▒   ▒  ▒   ▒▒▒  ▒    ▒   ▒▒▒▒   ▒▒▒▒▒▒▒▒    ▒   ▒▒   ▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒   ▒   ▒▒▒    ▒  ▒▒▒▒▒▒▒     ▒▒ | ||||
| #▓          ▓▓   ▓   ▓▓  ▓▓   ▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓    ▓▓▓  ▓▓▓   ▓▓   ▓▓▓▓   ▓   ▓▓▓▓   ▓   ▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓  ▓▓▓    | ||||
| #▓   ▓▓▓▓   ▓▓▓▓▓   ▓▓▓   ▓   ▓▓▓   ▓▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓   ▓▓▓▓▓▓▓▓▓   ▓▓▓   ▓▓   ▓▓   ▓▓▓   ▓▓   ▓▓▓   ▓▓▓▓   ▓    ▓    | ||||
| #█   ████   ████   ████   ██████    ████   ████   ████████    █   ██   ██████████     ██████   █████    ██   ███   ████   █████   █ | ||||
| #██████████████   █████   ███████████████████████████████████████████████████████████████████████████████████████████████████    ██ | ||||
|  | ||||
|  | ||||
| general { | ||||
|     grace = 15 | ||||
| } | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                   BACKGROUND                   │ | ||||
| # └                                                ┘ | ||||
| background { | ||||
|     monitor =  | ||||
|     path = /home/janis/NextCloud/Wallpapers/dark/colour-splash.jpg # Or screenshot | ||||
|  | ||||
|     blur_passes = 1 | ||||
| } | ||||
|  | ||||
| # ┌                                                ┐ | ||||
| # │                 PASSWORD INPUT                 │ | ||||
| # └                                                ┘ | ||||
| input-field { | ||||
|     monitor = | ||||
|     size = 300, 40 | ||||
|     outline_thickness = 3 | ||||
|     dots_size = 0.33 # Scale of input-field height, 0.2 - 0.8 | ||||
|     dots_spacing = 0.15 # Scale of dots' absolute size, 0.0 - 1.0 | ||||
|     dots_center = false | ||||
|     outer_color = rgb(89, 22, 65) | ||||
|     inner_color = rgb(200, 200, 200) | ||||
|     font_color = rgb(10, 10, 10) | ||||
|     fade_on_empty = true | ||||
|     placeholder_text = <i>Input Password...</i> # Text rendered in the input box when it's empty. | ||||
|     hide_input = false | ||||
|  | ||||
|     position = 0, -80 | ||||
|     halign = center | ||||
|     valign = center | ||||
| } | ||||
|  | ||||
| label { | ||||
|     monitor = | ||||
|     text = <b>$TIME</b> | ||||
|     color =  | ||||
|     font_size = 100 | ||||
|     font_family = Comfortaa | ||||
|  | ||||
|     position = 0, 80 | ||||
|     halign = center | ||||
|     valign = center | ||||
| } | ||||
|  | ||||
| label { | ||||
|     monitor = | ||||
|     text = $LAYOUT | ||||
|     color = rgba(200, 200, 200, 1.0) | ||||
|     font_size = 12 | ||||
|     font_family = Comfortaa | ||||
|  | ||||
|     position = 0, 0 | ||||
|     halign = right | ||||
|     valign = bottom | ||||
| } | ||||
|  | ||||
| label { | ||||
|     monitor = | ||||
|     text = $USER | ||||
|     color = rgba(200, 200, 200, 1.0) | ||||
|     font_size = 12 | ||||
|     font_family = Comfortaa | ||||
|  | ||||
|     position = 0, 0 | ||||
|     halign = left | ||||
|     valign = bottom | ||||
|     shadow_passes = 3 | ||||
| } | ||||
|  | ||||
| label { | ||||
|     monitor = | ||||
|     text = <i>Failed attempts: $ATTEMPTS</i> | ||||
|     color = rgba(200, 0, 0, 1.0) | ||||
|     font_size = 12 | ||||
|     font_family = Adwaita Sans | ||||
|  | ||||
|     position = 0, 20 | ||||
|     halign = center | ||||
|     valign = bottom | ||||
|     shadow_passes = 3 | ||||
|     shadow_size = 5 | ||||
|     shadow_boost = 3 | ||||
|     shadow_color = rgb(255,255,255) | ||||
| } | ||||
|  | ||||
							
								
								
									
										8
									
								
								config/hypr/xdg-portal-hyprland
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										8
									
								
								config/hypr/xdg-portal-hyprland
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| #!/bin/bash | ||||
| sleep 1 | ||||
| killall xdg-desktop-portal-hyprland | ||||
| killall xdg-desktop-portal-wlr | ||||
| killall xdg-desktop-portal | ||||
| /usr/libexec/xdg-desktop-portal-hyprland & | ||||
| sleep 2 | ||||
| /usr/lib/xdg-desktop-portal & | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user