Repository: chikobara/dotfiles
Branch: main
Commit: d0b9365e830f
Files: 749
Total size: 2.5 MB
Directory structure:
gitextract_9l3ptfwt/
├── .config/
│ ├── ags/
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .github/
│ │ │ ├── ISSUE_TEMPLATE/
│ │ │ │ ├── bug_report.md
│ │ │ │ └── feature_request.md
│ │ │ └── workflows/
│ │ │ └── ci.yml
│ │ ├── .gitignore
│ │ ├── .gitmodules
│ │ ├── .prettierignore
│ │ ├── .prettierrc
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── config.js
│ │ ├── customModules/
│ │ │ ├── PollVar.ts
│ │ │ ├── config.ts
│ │ │ ├── cpu/
│ │ │ │ ├── computeCPU.ts
│ │ │ │ └── index.ts
│ │ │ ├── kblayout/
│ │ │ │ ├── getLayout.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layouts.ts
│ │ │ ├── module.ts
│ │ │ ├── netstat/
│ │ │ │ ├── computeNetwork.ts
│ │ │ │ └── index.ts
│ │ │ ├── power/
│ │ │ │ └── index.ts
│ │ │ ├── ram/
│ │ │ │ ├── computeRam.ts
│ │ │ │ └── index.ts
│ │ │ ├── storage/
│ │ │ │ ├── computeStorage.ts
│ │ │ │ └── index.ts
│ │ │ ├── submap/
│ │ │ │ ├── helpers.ts
│ │ │ │ └── index.ts
│ │ │ ├── theme.ts
│ │ │ ├── updates/
│ │ │ │ └── index.ts
│ │ │ ├── utils.ts
│ │ │ └── weather/
│ │ │ └── index.ts
│ │ ├── directoryMonitorService.ts
│ │ ├── flake.nix
│ │ ├── globals/
│ │ │ ├── dropdown.ts
│ │ │ ├── mousePos.ts
│ │ │ ├── network.ts
│ │ │ ├── notification.ts
│ │ │ ├── useTheme.ts
│ │ │ ├── utilities.ts
│ │ │ ├── variables.ts
│ │ │ ├── weather.ts
│ │ │ └── window.ts
│ │ ├── globals.d.ts
│ │ ├── install_fonts.sh
│ │ ├── lib/
│ │ │ ├── constants/
│ │ │ │ └── colors.ts
│ │ │ ├── icons.ts
│ │ │ ├── option.ts
│ │ │ ├── session.ts
│ │ │ ├── shared/
│ │ │ │ ├── media.ts
│ │ │ │ └── notifications.ts
│ │ │ ├── types/
│ │ │ │ ├── audio.d.ts
│ │ │ │ ├── bar.d.ts
│ │ │ │ ├── customModules/
│ │ │ │ │ ├── generic.d.ts
│ │ │ │ │ ├── kbLayout.d.ts
│ │ │ │ │ ├── network.d.ts
│ │ │ │ │ └── utils.d.ts
│ │ │ │ ├── defaults/
│ │ │ │ │ ├── bar.ts
│ │ │ │ │ ├── netstat.ts
│ │ │ │ │ ├── options.ts
│ │ │ │ │ └── weather.ts
│ │ │ │ ├── dropdownmenu.d.ts
│ │ │ │ ├── filechooser.d.ts
│ │ │ │ ├── globals.d.ts
│ │ │ │ ├── gpustat.d.ts
│ │ │ │ ├── mpris.d.ts
│ │ │ │ ├── network.d.ts
│ │ │ │ ├── notification.d.ts
│ │ │ │ ├── options.d.ts
│ │ │ │ ├── popupwindow.d.ts
│ │ │ │ ├── power.d.ts
│ │ │ │ ├── powerprofiles.d.ts
│ │ │ │ ├── systray.d.ts
│ │ │ │ ├── utils.d.ts
│ │ │ │ ├── variable.d.ts
│ │ │ │ ├── volume.d.ts
│ │ │ │ ├── weather.d.ts
│ │ │ │ ├── widget.d.ts
│ │ │ │ └── workspace.d.ts
│ │ │ ├── utils.ts
│ │ │ └── variables.ts
│ │ ├── main.ts
│ │ ├── modules/
│ │ │ ├── bar/
│ │ │ │ ├── Bar.ts
│ │ │ │ ├── Exports.ts
│ │ │ │ ├── SideEffects.ts
│ │ │ │ ├── battery/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── bluetooth/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── clock/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── media/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── menu/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── network/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── notifications/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── systray/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── utils.ts
│ │ │ │ ├── volume/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── window_title/
│ │ │ │ │ └── index.ts
│ │ │ │ └── workspaces/
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── utils.ts
│ │ │ │ └── variants/
│ │ │ │ ├── default.ts
│ │ │ │ └── occupied.ts
│ │ │ ├── icons/
│ │ │ │ ├── index.ts
│ │ │ │ └── weather.ts
│ │ │ ├── menus/
│ │ │ │ ├── audio/
│ │ │ │ │ ├── active/
│ │ │ │ │ │ ├── SelectedInput.ts
│ │ │ │ │ │ ├── SelectedPlayback.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── available/
│ │ │ │ │ │ ├── InputDevices.ts
│ │ │ │ │ │ ├── PlaybackDevices.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── bluetooth/
│ │ │ │ │ ├── devices/
│ │ │ │ │ │ ├── connectedControls.ts
│ │ │ │ │ │ ├── devicelist.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── label.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── calendar/
│ │ │ │ │ ├── calendar.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── time/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── weather/
│ │ │ │ │ ├── hourly/
│ │ │ │ │ │ ├── icon/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── temperature/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── time/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── icon/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── stats/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── temperature/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── dashboard/
│ │ │ │ │ ├── controls/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── directories/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── profile/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── shortcuts/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── stats/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── energy/
│ │ │ │ │ ├── brightness/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── profiles/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── main.ts
│ │ │ │ ├── media/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── bar.ts
│ │ │ │ │ │ ├── controls.ts
│ │ │ │ │ │ └── mediainfo.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── media.ts
│ │ │ │ ├── network/
│ │ │ │ │ ├── ethernet/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── utils.ts
│ │ │ │ │ └── wifi/
│ │ │ │ │ ├── APStaging.ts
│ │ │ │ │ ├── WirelessAPs.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── notifications/
│ │ │ │ │ ├── controls/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── notification/
│ │ │ │ │ │ ├── actions/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── body/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── close/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── header/
│ │ │ │ │ │ │ ├── icon.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── image/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── placeholder/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── pager/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── power/
│ │ │ │ │ ├── helpers/
│ │ │ │ │ │ └── actions.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── verification.ts
│ │ │ │ ├── powerDropdown/
│ │ │ │ │ ├── button.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── shared/
│ │ │ │ ├── dropdown/
│ │ │ │ │ ├── eventBoxes/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── locationHandler/
│ │ │ │ │ └── index.ts
│ │ │ │ └── popup/
│ │ │ │ └── index.ts
│ │ │ ├── notifications/
│ │ │ │ ├── actions/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── body/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── close/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── header/
│ │ │ │ │ ├── icon.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── image/
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── osd/
│ │ │ │ ├── bar/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── icon/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── label/
│ │ │ │ └── index.ts
│ │ │ └── shared/
│ │ │ └── barItemBox.ts
│ │ ├── nix/
│ │ │ └── default.nix
│ │ ├── options.ts
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ ├── README.md
│ │ │ ├── checkUpdates.sh
│ │ │ ├── color_generation/
│ │ │ │ ├── applycolor.sh
│ │ │ │ ├── colorgen.sh
│ │ │ │ ├── generate_colors_material.py
│ │ │ │ ├── pywal_to_material.scss
│ │ │ │ ├── randomwall.sh
│ │ │ │ ├── schemes/
│ │ │ │ │ └── scheme_morevibrant.py
│ │ │ │ ├── specials/
│ │ │ │ │ ├── _material_badapple-l.scss
│ │ │ │ │ └── _material_badapple.scss
│ │ │ │ ├── switchcolor.sh
│ │ │ │ └── switchwall.sh
│ │ │ ├── fillThemes.js
│ │ │ ├── fillThemes.sh
│ │ │ ├── grimblast.sh
│ │ │ ├── hyprland/
│ │ │ │ ├── get_keybinds.py
│ │ │ │ └── workspace_action.sh
│ │ │ ├── quickscripts/
│ │ │ │ └── nixos-trim-generations.sh
│ │ │ ├── record-script.sh
│ │ │ ├── sway/
│ │ │ │ └── swayToRelativeWs.sh
│ │ │ ├── templates/
│ │ │ │ ├── fuzzel/
│ │ │ │ │ └── fuzzel.ini
│ │ │ │ ├── gradience/
│ │ │ │ │ └── preset.json
│ │ │ │ ├── hypr/
│ │ │ │ │ ├── hyprland/
│ │ │ │ │ │ └── colors.conf
│ │ │ │ │ └── hyprlock.conf
│ │ │ │ └── terminal/
│ │ │ │ ├── scheme-base.json
│ │ │ │ ├── scheme-monochrome.json
│ │ │ │ └── sequences.txt
│ │ │ └── wayland-idle-inhibitor.py
│ │ ├── scss/
│ │ │ ├── main.scss
│ │ │ ├── optionsTrackers.ts
│ │ │ ├── style/
│ │ │ │ ├── bar/
│ │ │ │ │ ├── audio.scss
│ │ │ │ │ ├── bar.scss
│ │ │ │ │ ├── battery.scss
│ │ │ │ │ ├── bluetooth.scss
│ │ │ │ │ ├── clock.scss
│ │ │ │ │ ├── media.scss
│ │ │ │ │ ├── menu.scss
│ │ │ │ │ ├── network.scss
│ │ │ │ │ ├── notifications.scss
│ │ │ │ │ ├── power.scss
│ │ │ │ │ ├── systray.scss
│ │ │ │ │ ├── window_title.scss
│ │ │ │ │ └── workspace.scss
│ │ │ │ ├── colors.scss
│ │ │ │ ├── common/
│ │ │ │ │ ├── common.scss
│ │ │ │ │ ├── floating-widget.scss
│ │ │ │ │ ├── general.scss
│ │ │ │ │ └── widget-button.scss
│ │ │ │ ├── customModules/
│ │ │ │ │ └── style.scss
│ │ │ │ ├── highlights.scss
│ │ │ │ ├── menus/
│ │ │ │ │ ├── audiomenu.scss
│ │ │ │ │ ├── bluetooth.scss
│ │ │ │ │ ├── calendar.scss
│ │ │ │ │ ├── dashboard.scss
│ │ │ │ │ ├── energy.scss
│ │ │ │ │ ├── media.scss
│ │ │ │ │ ├── menu.scss
│ │ │ │ │ ├── network.scss
│ │ │ │ │ ├── notifications.scss
│ │ │ │ │ ├── power.scss
│ │ │ │ │ └── powerdropdown.scss
│ │ │ │ ├── notifications/
│ │ │ │ │ └── popups.scss
│ │ │ │ ├── osd/
│ │ │ │ │ └── index.scss
│ │ │ │ └── settings/
│ │ │ │ └── dialog.scss
│ │ │ └── style.ts
│ │ ├── services/
│ │ │ ├── Brightness.ts
│ │ │ ├── Cpu.ts
│ │ │ ├── Ram.ts
│ │ │ ├── Storage.ts
│ │ │ ├── Wallpaper.ts
│ │ │ ├── bluetooth.py
│ │ │ ├── matugen/
│ │ │ │ ├── index.ts
│ │ │ │ └── variations.ts
│ │ │ ├── screen_record.sh
│ │ │ └── snapshot.sh
│ │ ├── themes/
│ │ │ ├── catppuccin_frappe.json
│ │ │ ├── catppuccin_frappe_split.json
│ │ │ ├── catppuccin_latte.json
│ │ │ ├── catppuccin_latte_split.json
│ │ │ ├── catppuccin_macchiato.json
│ │ │ ├── catppuccin_macchiato_split.json
│ │ │ ├── catppuccin_mocha.json
│ │ │ ├── catppuccin_mocha_split.json
│ │ │ ├── cyberpunk.json
│ │ │ ├── cyberpunk_split.json
│ │ │ ├── dracula.json
│ │ │ ├── dracula_split.json
│ │ │ ├── everforest.json
│ │ │ ├── everforest_split.json
│ │ │ ├── gruvbox.json
│ │ │ ├── gruvbox_split.json
│ │ │ ├── monochrome.json
│ │ │ ├── monochrome_split.json
│ │ │ ├── nord.json
│ │ │ ├── nord_split.json
│ │ │ ├── one_dark.json
│ │ │ ├── one_dark_split.json
│ │ │ ├── rose_pine.json
│ │ │ ├── rose_pine_moon.json
│ │ │ ├── rose_pine_moon_split.json
│ │ │ ├── rose_pine_split.json
│ │ │ ├── tokyo_night.json
│ │ │ └── tokyo_night_split.json
│ │ ├── tsconfig.json
│ │ └── widget/
│ │ ├── RegularWindow.ts
│ │ └── settings/
│ │ ├── SettingsDialog.ts
│ │ ├── pages/
│ │ │ ├── config/
│ │ │ │ ├── bar/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── general/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── menus/
│ │ │ │ │ ├── clock.ts
│ │ │ │ │ ├── dashboard.ts
│ │ │ │ │ └── power.ts
│ │ │ │ ├── notifications/
│ │ │ │ │ └── index.ts
│ │ │ │ └── osd/
│ │ │ │ └── index.ts
│ │ │ └── theme/
│ │ │ ├── bar/
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── menus/
│ │ │ │ ├── battery.ts
│ │ │ │ ├── bluetooth.ts
│ │ │ │ ├── clock.ts
│ │ │ │ ├── dashboard.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── matugen.ts
│ │ │ │ ├── media.ts
│ │ │ │ ├── network.ts
│ │ │ │ ├── notifications.ts
│ │ │ │ ├── power.ts
│ │ │ │ ├── systray.ts
│ │ │ │ └── volume.ts
│ │ │ ├── notifications/
│ │ │ │ └── index.ts
│ │ │ └── osd/
│ │ │ └── index.ts
│ │ ├── shared/
│ │ │ ├── FileChooser.ts
│ │ │ ├── Header.ts
│ │ │ ├── Inputter.ts
│ │ │ ├── Label.ts
│ │ │ ├── Option.ts
│ │ │ └── components/
│ │ │ ├── boolean.ts
│ │ │ ├── color.ts
│ │ │ ├── enum.ts
│ │ │ ├── font.ts
│ │ │ ├── image.ts
│ │ │ ├── import.ts
│ │ │ ├── number.ts
│ │ │ ├── object.ts
│ │ │ ├── string.ts
│ │ │ └── wallpaper.ts
│ │ └── side_effects/
│ │ └── index.ts
│ ├── fuzzel/
│ │ └── fuzzel.ini
│ ├── hypr/
│ │ ├── hypridle.conf
│ │ ├── hyprland/
│ │ │ ├── colors.conf
│ │ │ ├── env.conf
│ │ │ ├── execs.conf
│ │ │ ├── general.conf
│ │ │ ├── keybinds.conf
│ │ │ └── rules.conf
│ │ ├── hyprland.conf
│ │ ├── hyprlock/
│ │ │ └── status.sh
│ │ ├── hyprlock.conf
│ │ ├── monitors.conf
│ │ ├── shaders/
│ │ │ ├── chromatic_abberation.frag
│ │ │ ├── crt.frag
│ │ │ ├── drugs.frag
│ │ │ ├── extradark.frag
│ │ │ ├── invert.frag
│ │ │ └── solarized.frag
│ │ └── workspaces.conf
│ ├── kitty/
│ │ ├── current-theme.conf
│ │ ├── kitty-hyprland.conf
│ │ ├── kitty-themes/
│ │ │ ├── .all-contributorsrc
│ │ │ ├── .tools/
│ │ │ │ ├── README.md
│ │ │ │ ├── color_table.sh
│ │ │ │ ├── convert.py
│ │ │ │ ├── convert_conf.swift
│ │ │ │ ├── extract-vscode.sh
│ │ │ │ ├── generate_conf.sh
│ │ │ │ ├── generate_theme_preview.sh
│ │ │ │ ├── generate_themes_previews.sh
│ │ │ │ ├── libcapture.sh
│ │ │ │ ├── markdown.sh
│ │ │ │ ├── palette.py
│ │ │ │ ├── preview.py
│ │ │ │ ├── previews.sh
│ │ │ │ ├── template.conf
│ │ │ │ ├── template.conf.j2
│ │ │ │ └── windowid.swift
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── LICENSE.md
│ │ │ ├── README.md
│ │ │ └── themes/
│ │ │ ├── 3024_Day.conf
│ │ │ ├── 3024_Night.conf
│ │ │ ├── AdventureTime.conf
│ │ │ ├── Afterglow.conf
│ │ │ ├── AlienBlood.conf
│ │ │ ├── Alucard.conf
│ │ │ ├── Apprentice.conf
│ │ │ ├── Argonaut.conf
│ │ │ ├── Arthur.conf
│ │ │ ├── AtelierSulphurpool.conf
│ │ │ ├── Atom.conf
│ │ │ ├── AtomOneLight.conf
│ │ │ ├── Batman.conf
│ │ │ ├── Belafonte_Day.conf
│ │ │ ├── Belafonte_Night.conf
│ │ │ ├── BirdsOfParadise.conf
│ │ │ ├── Blazer.conf
│ │ │ ├── Borland.conf
│ │ │ ├── Bright_Lights.conf
│ │ │ ├── Broadcast.conf
│ │ │ ├── Brogrammer.conf
│ │ │ ├── C64.conf
│ │ │ ├── CLRS.conf
│ │ │ ├── Chalk.conf
│ │ │ ├── Chalkboard.conf
│ │ │ ├── Ciapre.conf
│ │ │ ├── Cobalt2.conf
│ │ │ ├── Cobalt_Neon.conf
│ │ │ ├── CrayonPonyFish.conf
│ │ │ ├── Dark_Pastel.conf
│ │ │ ├── Darkside.conf
│ │ │ ├── Desert.conf
│ │ │ ├── DimmedMonokai.conf
│ │ │ ├── DotGov.conf
│ │ │ ├── Dracula.conf
│ │ │ ├── Dumbledore.conf
│ │ │ ├── Duotone_Dark.conf
│ │ │ ├── ENCOM.conf
│ │ │ ├── Earthsong.conf
│ │ │ ├── Elemental.conf
│ │ │ ├── Espresso.conf
│ │ │ ├── Espresso_Libre.conf
│ │ │ ├── Fideloper.conf
│ │ │ ├── FishTank.conf
│ │ │ ├── Flat.conf
│ │ │ ├── Flatland.conf
│ │ │ ├── Floraverse.conf
│ │ │ ├── FrontEndDelight.conf
│ │ │ ├── FunForrest.conf
│ │ │ ├── Galaxy.conf
│ │ │ ├── Github.conf
│ │ │ ├── Glacier.conf
│ │ │ ├── GoaBase.conf
│ │ │ ├── Grape.conf
│ │ │ ├── Grass.conf
│ │ │ ├── Hardcore.conf
│ │ │ ├── Harper.conf
│ │ │ ├── Highway.conf
│ │ │ ├── Hipster_Green.conf
│ │ │ ├── Homebrew.conf
│ │ │ ├── Hurtado.conf
│ │ │ ├── Hybrid.conf
│ │ │ ├── IC_Green_PPL.conf
│ │ │ ├── IC_Orange_PPL.conf
│ │ │ ├── IR_Black.conf
│ │ │ ├── Jackie_Brown.conf
│ │ │ ├── Japanesque.conf
│ │ │ ├── Jellybeans.conf
│ │ │ ├── JetBrains_Darcula.conf
│ │ │ ├── Kibble.conf
│ │ │ ├── Later_This_Evening.conf
│ │ │ ├── Lavandula.conf
│ │ │ ├── LiquidCarbon.conf
│ │ │ ├── LiquidCarbonTransparent.conf
│ │ │ ├── LiquidCarbonTransparentInverse.conf
│ │ │ ├── Man_Page.conf
│ │ │ ├── Material.conf
│ │ │ ├── MaterialDark.conf
│ │ │ ├── Mathias.conf
│ │ │ ├── Medallion.conf
│ │ │ ├── Misterioso.conf
│ │ │ ├── Molokai.conf
│ │ │ ├── MonaLisa.conf
│ │ │ ├── Monokai.conf
│ │ │ ├── Monokai_Classic.conf
│ │ │ ├── Monokai_Pro.conf
│ │ │ ├── Monokai_Pro_(Filter_Machine).conf
│ │ │ ├── Monokai_Pro_(Filter_Octagon).conf
│ │ │ ├── Monokai_Pro_(Filter_Ristretto).conf
│ │ │ ├── Monokai_Pro_(Filter_Spectrum).conf
│ │ │ ├── Monokai_Soda.conf
│ │ │ ├── N0tch2k.conf
│ │ │ ├── Neopolitan.conf
│ │ │ ├── Neutron.conf
│ │ │ ├── NightLion_v1.conf
│ │ │ ├── NightLion_v2.conf
│ │ │ ├── Nova.conf
│ │ │ ├── Novel.conf
│ │ │ ├── Obsidian.conf
│ │ │ ├── Ocean.conf
│ │ │ ├── OceanicMaterial.conf
│ │ │ ├── Ollie.conf
│ │ │ ├── OneDark.conf
│ │ │ ├── Parasio_Dark.conf
│ │ │ ├── PaulMillr.conf
│ │ │ ├── PencilDark.conf
│ │ │ ├── PencilLight.conf
│ │ │ ├── Piatto_Light.conf
│ │ │ ├── Pnevma.conf
│ │ │ ├── Pro.conf
│ │ │ ├── Red_Alert.conf
│ │ │ ├── Red_Sands.conf
│ │ │ ├── Relaxed_Afterglow.conf
│ │ │ ├── Renault_Style.conf
│ │ │ ├── Renault_Style_Light.conf
│ │ │ ├── Rippedcasts.conf
│ │ │ ├── Royal.conf
│ │ │ ├── SeaShells.conf
│ │ │ ├── Seafoam_Pastel.conf
│ │ │ ├── Seti.conf
│ │ │ ├── Shaman.conf
│ │ │ ├── Slate.conf
│ │ │ ├── Smyck.conf
│ │ │ ├── SoftServer.conf
│ │ │ ├── Solarized_Darcula.conf
│ │ │ ├── Solarized_Dark.conf
│ │ │ ├── Solarized_Dark_-_Patched.conf
│ │ │ ├── Solarized_Dark_Higher_Contrast.conf
│ │ │ ├── Solarized_Light.conf
│ │ │ ├── Source_Code_X.conf
│ │ │ ├── SpaceGray.conf
│ │ │ ├── SpaceGray_Eighties.conf
│ │ │ ├── SpaceGray_Eighties_Dull.conf
│ │ │ ├── Spacedust.conf
│ │ │ ├── Spiderman.conf
│ │ │ ├── Spring.conf
│ │ │ ├── Square.conf
│ │ │ ├── Sundried.conf
│ │ │ ├── Symfonic.conf
│ │ │ ├── Tango_Dark.conf
│ │ │ ├── Tango_Light.conf
│ │ │ ├── Teerb.conf
│ │ │ ├── Thayer_Bright.conf
│ │ │ ├── The_Hulk.conf
│ │ │ ├── Tomorrow.conf
│ │ │ ├── Tomorrow_Night.conf
│ │ │ ├── Tomorrow_Night_Blue.conf
│ │ │ ├── Tomorrow_Night_Bright.conf
│ │ │ ├── Tomorrow_Night_Eighties.conf
│ │ │ ├── ToyChest.conf
│ │ │ ├── Treehouse.conf
│ │ │ ├── Twilight.conf
│ │ │ ├── Ubuntu.conf
│ │ │ ├── Urple.conf
│ │ │ ├── Vaughn.conf
│ │ │ ├── VibrantInk.conf
│ │ │ ├── WarmNeon.conf
│ │ │ ├── Wez.conf
│ │ │ ├── WildCherry.conf
│ │ │ ├── Wombat.conf
│ │ │ ├── Wryan.conf
│ │ │ ├── Zenburn.conf
│ │ │ ├── ayu.conf
│ │ │ ├── ayu_light.conf
│ │ │ ├── ayu_mirage.conf
│ │ │ ├── gruvbox_dark.conf
│ │ │ ├── gruvbox_light.conf
│ │ │ ├── idleToes.conf
│ │ │ ├── rose-pine-moon.conf
│ │ │ └── snazzy.conf
│ │ └── kitty.conf
│ ├── qt5ct/
│ │ └── qt5ct.conf
│ ├── qt6ct/
│ │ ├── colors/
│ │ │ ├── Catppuccin-Latte.conf
│ │ │ └── Catppuccin-Mocha.conf
│ │ └── qt6ct.conf
│ ├── rofi/
│ │ ├── applets/
│ │ │ ├── bin/
│ │ │ │ ├── appasroot.sh
│ │ │ │ ├── apps.sh
│ │ │ │ ├── battery.sh
│ │ │ │ ├── brightness.sh
│ │ │ │ ├── mpd.sh
│ │ │ │ ├── powermenu.sh
│ │ │ │ ├── quicklinks.sh
│ │ │ │ ├── screenshot.sh
│ │ │ │ └── volume.sh
│ │ │ ├── shared/
│ │ │ │ ├── colors.rasi
│ │ │ │ ├── fonts.rasi
│ │ │ │ └── theme.bash
│ │ │ ├── type-1/
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ └── style-3.rasi
│ │ │ ├── type-2/
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ └── style-3.rasi
│ │ │ ├── type-3/
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ └── style-3.rasi
│ │ │ ├── type-4/
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ └── style-3.rasi
│ │ │ └── type-5/
│ │ │ ├── style-1.rasi
│ │ │ ├── style-2.rasi
│ │ │ └── style-3.rasi
│ │ ├── colors/
│ │ │ ├── adapta.rasi
│ │ │ ├── arc.rasi
│ │ │ ├── black.rasi
│ │ │ ├── catppuccin.rasi
│ │ │ ├── cyberpunk.rasi
│ │ │ ├── dracula.rasi
│ │ │ ├── everforest.rasi
│ │ │ ├── gruvbox.rasi
│ │ │ ├── lovelace.rasi
│ │ │ ├── navy.rasi
│ │ │ ├── nord.rasi
│ │ │ ├── onedark.rasi
│ │ │ ├── paper.rasi
│ │ │ ├── solarized.rasi
│ │ │ ├── tokyonight.rasi
│ │ │ └── yousai.rasi
│ │ ├── config.rasi
│ │ ├── launchers/
│ │ │ ├── type-1/
│ │ │ │ ├── launcher.sh
│ │ │ │ ├── shared/
│ │ │ │ │ ├── colors.rasi
│ │ │ │ │ └── fonts.rasi
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-10.rasi
│ │ │ │ ├── style-11.rasi
│ │ │ │ ├── style-12.rasi
│ │ │ │ ├── style-13.rasi
│ │ │ │ ├── style-14.rasi
│ │ │ │ ├── style-15.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ ├── style-3.rasi
│ │ │ │ ├── style-4.rasi
│ │ │ │ ├── style-5.rasi
│ │ │ │ ├── style-6.rasi
│ │ │ │ ├── style-7.rasi
│ │ │ │ ├── style-8.rasi
│ │ │ │ └── style-9.rasi
│ │ │ ├── type-2/
│ │ │ │ ├── launcher.sh
│ │ │ │ ├── shared/
│ │ │ │ │ ├── colors.rasi
│ │ │ │ │ └── fonts.rasi
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-10.rasi
│ │ │ │ ├── style-11.rasi
│ │ │ │ ├── style-12.rasi
│ │ │ │ ├── style-13.rasi
│ │ │ │ ├── style-14.rasi
│ │ │ │ ├── style-15.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ ├── style-3.rasi
│ │ │ │ ├── style-4.rasi
│ │ │ │ ├── style-5.rasi
│ │ │ │ ├── style-6.rasi
│ │ │ │ ├── style-7.rasi
│ │ │ │ ├── style-8.rasi
│ │ │ │ └── style-9.rasi
│ │ │ ├── type-3/
│ │ │ │ ├── launcher.sh
│ │ │ │ ├── shared/
│ │ │ │ │ ├── colors.rasi
│ │ │ │ │ └── fonts.rasi
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-10.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ ├── style-3.rasi
│ │ │ │ ├── style-4.rasi
│ │ │ │ ├── style-5.rasi
│ │ │ │ ├── style-6.rasi
│ │ │ │ ├── style-7.rasi
│ │ │ │ ├── style-8.rasi
│ │ │ │ └── style-9.rasi
│ │ │ ├── type-4/
│ │ │ │ ├── launcher.sh
│ │ │ │ ├── shared/
│ │ │ │ │ ├── colors.rasi
│ │ │ │ │ └── fonts.rasi
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-10.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ ├── style-3.rasi
│ │ │ │ ├── style-4.rasi
│ │ │ │ ├── style-5.rasi
│ │ │ │ ├── style-6.rasi
│ │ │ │ ├── style-7.rasi
│ │ │ │ ├── style-8.rasi
│ │ │ │ └── style-9.rasi
│ │ │ ├── type-5/
│ │ │ │ ├── launcher.sh
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ ├── style-3.rasi
│ │ │ │ ├── style-4.rasi
│ │ │ │ └── style-5.rasi
│ │ │ ├── type-6/
│ │ │ │ ├── launcher.sh
│ │ │ │ ├── style-1.rasi
│ │ │ │ ├── style-10.rasi
│ │ │ │ ├── style-2.rasi
│ │ │ │ ├── style-3.rasi
│ │ │ │ ├── style-4.rasi
│ │ │ │ ├── style-5.rasi
│ │ │ │ ├── style-6.rasi
│ │ │ │ ├── style-7.rasi
│ │ │ │ ├── style-8.rasi
│ │ │ │ └── style-9.rasi
│ │ │ └── type-7/
│ │ │ ├── launcher.sh
│ │ │ ├── style-1.rasi
│ │ │ ├── style-10.rasi
│ │ │ ├── style-2.rasi
│ │ │ ├── style-3.rasi
│ │ │ ├── style-4.rasi
│ │ │ ├── style-5.rasi
│ │ │ ├── style-6.rasi
│ │ │ ├── style-7.rasi
│ │ │ ├── style-8.rasi
│ │ │ └── style-9.rasi
│ │ └── powermenu/
│ │ ├── type-1/
│ │ │ ├── powermenu.sh
│ │ │ ├── shared/
│ │ │ │ ├── colors.rasi
│ │ │ │ └── fonts.rasi
│ │ │ ├── style-1.rasi
│ │ │ ├── style-2.rasi
│ │ │ ├── style-3.rasi
│ │ │ ├── style-4.rasi
│ │ │ └── style-5.rasi
│ │ ├── type-2/
│ │ │ ├── powermenu.sh
│ │ │ ├── shared/
│ │ │ │ ├── colors.rasi
│ │ │ │ └── fonts.rasi
│ │ │ ├── style-1.rasi
│ │ │ ├── style-10.rasi
│ │ │ ├── style-2.rasi
│ │ │ ├── style-3.rasi
│ │ │ ├── style-4.rasi
│ │ │ ├── style-5.rasi
│ │ │ ├── style-6.rasi
│ │ │ ├── style-7.rasi
│ │ │ ├── style-8.rasi
│ │ │ └── style-9.rasi
│ │ ├── type-3/
│ │ │ ├── powermenu.sh
│ │ │ ├── shared/
│ │ │ │ ├── colors.rasi
│ │ │ │ ├── confirm.rasi
│ │ │ │ └── fonts.rasi
│ │ │ ├── style-1.rasi
│ │ │ ├── style-2.rasi
│ │ │ ├── style-3.rasi
│ │ │ ├── style-4.rasi
│ │ │ └── style-5.rasi
│ │ ├── type-4/
│ │ │ ├── powermenu.sh
│ │ │ ├── shared/
│ │ │ │ ├── colors.rasi
│ │ │ │ ├── confirm.rasi
│ │ │ │ └── fonts.rasi
│ │ │ ├── style-1.rasi
│ │ │ ├── style-2.rasi
│ │ │ ├── style-3.rasi
│ │ │ ├── style-4.rasi
│ │ │ └── style-5.rasi
│ │ ├── type-5/
│ │ │ ├── powermenu.sh
│ │ │ ├── style-1.rasi
│ │ │ ├── style-2.rasi
│ │ │ ├── style-3.rasi
│ │ │ ├── style-4.rasi
│ │ │ └── style-5.rasi
│ │ └── type-6/
│ │ ├── powermenu.sh
│ │ ├── style-1.rasi
│ │ ├── style-2.rasi
│ │ ├── style-3.rasi
│ │ ├── style-4.rasi
│ │ └── style-5.rasi
│ ├── wlogout/
│ │ ├── layout
│ │ └── style.css
│ └── zshrc.d/
│ ├── auto-Hypr.sh
│ ├── dots-hyprland.zsh
│ └── shortcuts.zsh
├── .fastfetch_conf.jsonc
├── .fonts/
│ ├── .uuid
│ └── CustomTkinter_shapes_font.otf
├── .gitattributes
├── .zprofile
├── .zshenv
├── .zshrc
├── README.md
├── backup.sh
└── hyprpanel_config.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .config/ags/.eslintignore
================================================
types
node_modules
================================================
FILE: .config/ags/.eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'import'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
root: true,
ignorePatterns: ['.eslintrc.js', 'types/**/*.ts', 'scripts/**/*.js'],
env: {
es6: true,
browser: true,
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'import/extensions': ['off'],
'import/no-unresolved': 'off',
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
},
};
================================================
FILE: .config/ags/.github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help HyprPanel improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Distribution: [e.g. Arch Linux]
- Window Manager/Desktop Environment: [e.g. Hyprland]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .config/ags/.github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .config/ags/.github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
branches:
- master
jobs:
code_quality:
runs-on: ubuntu-latest
steps:
- name: Checkout main repository
uses: actions/checkout@v3
- name: Clone ags-types to temp dir
uses: actions/checkout@v3
with:
repository: Jas-SinghFSU/ags-types
path: temp-ags-types
- name: Copy types to types/
run: |
rm -rf types
mkdir -p types
cp -R temp-ags-types/types/* types/
rm -rf temp-ags-types
- name: Node Setup
uses: actions/setup-node@v3
with:
node-version: '21'
- name: Install Dependencies
run: npm install
- name: ESLint
run: npm run lint
- name: Type Check
run: npx tsc --noEmit --pretty --extendedDiagnostics
================================================
FILE: .config/ags/.gitignore
================================================
.weather.json
node_modules
================================================
FILE: .config/ags/.gitmodules
================================================
[submodule "external/ags-types"]
path = external/ags-types
url = https://github.com/Jas-SinghFSU/ags-types.git
================================================
FILE: .config/ags/.prettierignore
================================================
.eslintrc.js
types/**/*.ts
================================================
FILE: .config/ags/.prettierrc
================================================
{
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 4,
"useTabs": false
}
================================================
FILE: .config/ags/LICENSE
================================================
MIT License
Copyright (c) 2024 Jas Singh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: .config/ags/README.md
================================================
# HyprPanel 🚀
A panel built for Hyprland with [AGS](https://github.com/Aylur/ags)


## Installation
The [HyprPanel Wiki](https://hyprpanel.com/getting_started/installation.html) contains in depth instructions for installing the panel and all of its dependencies. The instructions below are general instructions for installing the panel.
## Requirements
Bun
```sh
curl -fsSL https://bun.sh/install | bash && \
sudo ln -s $HOME/.bun/bin/bun /usr/local/bin/bun
```
Additional dependencies:
```sh
pipewire
libgtop
bluez
bluez-utils
grimblast
gpu-screen-recorder
hyprpicker
btop
networkmanager
matugen
wl-clipboard
swww
dart-sass
brightnessctl
gnome-bluetooth-3.0
```
Optional Dependencies:
```sh
## Used for Tracking GPU Usage in your Dashboard (NVidia only)
python
python-gpustat
## Only if a pywal hook from wallpaper changes applied through settings is desired
pywal
## To check for pacman updates in the default script used in the updates module
pacman-contrib
## To switch between power profiles in battery module
power-profiles-daemon
```
### Arch
pacman:
```bash
sudo pacman -S pipewire libgtop bluez bluez-utils btop networkmanager dart-sass wl-clipboard brightnessctl swww python gnome-bluetooth-3.0 pacman-contrib power-profiles-daemon
```
AUR:
```bash
yay -S grimblast-git gpu-screen-recorder hyprpicker matugen-bin python-gpustat aylurs-gtk-shell-git
```
### Fedora
COPR - Add [solopasha/hyprland](https://copr.fedorainfracloud.org/coprs/solopasha/hyprland/) for most hyprland-related dependencies, and [hues-sueh/packages](https://copr.fedorainfracloud.org/coprs/heus-sueh/packages/) for matugen. Both provide the `swww` package, so prioritise the former repo:
```bash
sudo dnf copr enable solopasha/hyprland
sudo dnf copr enable heus-sueh/packages
sudo dnf config-manager --save --setopt=copr:copr.fedorainfracloud.org:heus-sueh:packages.priority=200
```
DNF:
```bash
sudo dnf install pipewire libgtop2 bluez bluez-tools grimblast hyprpicker btop NetworkManager wl-clipboard swww brightnessctl gnome-bluetooth aylurs-gtk-shell power-profiles-daemon gvfs
```
bun:
```bash
bun install -g sass
```
flatpak:
```bash
flatpak install flathub --system com.dec05eba.gpu_screen_recorder
```
#### Optional Dependencies
pip:
```bash
sudo dnf install python python3-pip; pip install gpustat pywal
```
### NixOS
For NixOS/Home-Manager, see [NixOS & Home-Manager instructions](#nixos--home-manager).
## Instructions
### AGS
Once everything is installed you need to put the contents of this repo in `~/.config/ags`.
If you already have something in `~/.config/ags`, it's recommended that you back it up with:
```bash
mv $HOME/.config/ags $HOME/.config/ags.bkup
```
Otherwise you can use this command to install the panel:
```bash
git clone https://github.com/Jas-SinghFSU/HyprPanel.git && \
ln -s $(pwd)/HyprPanel $HOME/.config/ags
```
### Nerd Fonts
Additionally, you need to ensure that you have a [Nerd Font](https://www.nerdfonts.com/font-downloads) installed for your icons to render properly.
### Launch the panel
Afterwards you can run the panel with the following command in your terminal:
```bash
ags
```
Or you can add it to your Hyprland config (hyprland.conf) to auto-start with:
```bash
exec-once = ags
```
### NixOS & Home-Manager
Alternatively, if you're using NixOS and/or Home-Manager, you can setup AGS using the provided Nix Flake. First, add the repository to your Flake's inputs, and enable the overlay.
```nix
# flake.nix
{
inputs.hyprpanel.url = "github:Jas-SinghFSU/HyprPanel";
# ...
outputs = { self, nixpkgs, ... }@inputs:
let
# ...
system = "x86_64-linux"; # change to whatever your system should be.
pkgs = import nixpkgs {
inherit system;
# ...
overlays = [
inputs.hyprpanel.overlay
];
};
in {
# ...
}
}
```
Once you've set up the overlay, you can reference HyprPanel with `pkgs.hyprpanel` as if it were any other Nix package. This means you can reference it as a NixOS system/user package, a Home-Manager user package, or as a direct reference in your Hyprland configuration (if your configuration is managed by Home-Manager). The first three methods will add it to your `$PATH` (first globally, second two user-only), however the final will not.
```nix
# configuration.nix
# install it as a system package
environment.systemPackages = with pkgs; [
# ...
hyprpanel
# ...
];
# or install it as a user package
users.users..packages = with pkgs; [
# ...
hyprpanel
# ...
];
# home.nix
# install it as a user package with home-manager
home.packages = with pkgs; [
# ...
hyprpanel
# ...
];
# or reference it directly in your Hyprland configuration
wayland.windowManager.hyprland.settings.exec-once = [
"${pkgs.hyprpanel}/bin/hyprpanel"
];
```
### Notifications
HyprPanel handles notifications through the AGS built-in notification service. If you're already using a notification daemon such as Dunst or Mako, you may have to stop them to prevent conflicts with HyprPanel.
> NOTE: If your system is in a language other than English, the resource monitor in the dashboard may not work properly.
## Configuration
The HyprPanel comes with a configuration menu which is available by opening the Dashboard menu (click the button in the bar with the default - Arch - icon) and then clicking the Gear icon.
### Size
The panel is automatically scaled based on your font size in `Configuration > General`.
### Specifying bar layouts per monitor
To specify layouts for each monitor you can create a JSON object such as:
```JSON
{
"0": {
"left": [
"dashboard",
"workspaces",
"windowtitle"
],
"middle": [
"media"
],
"right": [
"volume",
"clock",
"notifications"
]
},
"1": {
"left": [
"dashboard",
"workspaces",
"windowtitle"
],
"middle": [
"media"
],
"right": [
"volume",
"clock",
"notifications"
]
},
"2": {
"left": [
"dashboard",
"workspaces",
"windowtitle"
],
"middle": [
"media"
],
"right": [
"volume",
"network",
"bluetooth",
"systray",
"clock",
"notifications"
]
}
}
```
Where each monitor is defined by its index (0, 1, 2 in this case) and each section (left, middle, right) contains one or more of the following modules:
```js
'battery';
'dashboard';
'workspaces';
'windowtitle';
'media';
'notifications';
'volume';
'network';
'bluetooth';
'clock';
'systray';
```
Since the text-box in the options dialog isn't sufficient, it is recommended that you create this JSON configuration in a text editor elsewhere and paste it into the layout text-box under Configuration > Bar > "Bar Layouts for Monitors".
### Additional Configuration
#### GPU Tracking
If you have an NVidia GPU, you can track your GPU usage in your Dashboard by going to your `Settings > Configuration > Dashboard Menu > Track GPU` and turning it on.
================================================
FILE: .config/ags/config.js
================================================
import GLib from 'gi://GLib';
const main = '/tmp/ags/hyprpanel/main.js';
const entry = `${App.configDir}/main.ts`;
const bundler = GLib.getenv('AGS_BUNDLER') || 'bun';
const v = {
ags: pkg.version?.split('.').map(Number) || [],
expect: [1, 8, 1],
};
try {
switch (bundler) {
case 'bun':
await Utils.execAsync([
'bun',
'build',
entry,
'--outfile',
main,
'--external',
'resource://*',
'--external',
'gi://*',
'--external',
'file://*',
]);
break;
case 'esbuild':
await Utils.execAsync([
'esbuild',
'--bundle',
entry,
'--format=esm',
`--outfile=${main}`,
'--external:resource://*',
'--external:gi://*',
'--external:file://*',
]);
break;
default:
throw `"${bundler}" is not a valid bundler`;
}
if (v.ags[1] < v.expect[1] || v.ags[2] < v.expect[2]) {
print(`HyprPanel needs atleast v${v.expect.join('.')} of AGS, yours is v${v.ags.join('.')}`);
App.quit();
}
await import(`file://${main}`);
} catch (error) {
console.error(error);
App.quit();
}
export {};
================================================
FILE: .config/ags/customModules/PollVar.ts
================================================
import GLib from 'gi://GLib?version=2.0';
import { GenericFunction } from 'lib/types/customModules/generic';
import { Bind } from 'lib/types/variable';
import { Variable as VariableType } from 'types/variable';
/**
* @param {VariableType} targetVariable - The Variable to update with the function's result.
* @param {Array} trackers - Array of trackers to watch.
* @param {Bind} pollingInterval - The polling interval in milliseconds.
* @param {GenericFunction} someFunc - The function to execute at each interval, which updates the Variable.
* @param {...P} params - Parameters to pass to someFunc.
*/
export const pollVariable = >(
targetVariable: VariableType,
trackers: Array,
pollingInterval: Bind,
someFunc: F,
...params: P
): void => {
let intervalInstance: number | null = null;
const intervalFn = (pollIntrvl: number): void => {
if (intervalInstance !== null) {
GLib.source_remove(intervalInstance);
}
intervalInstance = Utils.interval(pollIntrvl, () => {
targetVariable.value = someFunc(...params);
});
};
Utils.merge([pollingInterval, ...trackers], (pollIntrvl: number) => {
intervalFn(pollIntrvl);
});
};
/**
* @param {VariableType} targetVariable - The Variable to update with the result of the command.
* @param {Array} trackers - Array of trackers to watch.
* @param {Bind} pollingInterval - The polling interval in milliseconds.
* @param {string} someCommand - The bash command to execute.
* @param {GenericFunction} someFunc - The function to execute after processing the command result;
* with the first argument being the result of the command execution.
* @param {...P} params - Additional parameters to pass to someFunc.
*/
export const pollVariableBash = >(
targetVariable: VariableType,
trackers: Array,
pollingInterval: Bind,
someCommand: string,
someFunc: F,
...params: P
): void => {
let intervalInstance: number | null = null;
const intervalFn = (pollIntrvl: number): void => {
if (intervalInstance !== null) {
GLib.source_remove(intervalInstance);
}
intervalInstance = Utils.interval(pollIntrvl, () => {
Utils.execAsync(`bash -c "${someCommand}"`)
.then((res: string) => {
try {
targetVariable.value = someFunc(res, ...params);
} catch (error) {
console.warn(`An error occurred when running interval bash function: ${error}`);
}
})
.catch((err) => console.error(`Error running command "${someCommand}": ${err}`));
});
};
Utils.merge([pollingInterval, ...trackers], (pollIntrvl: number) => {
intervalFn(pollIntrvl);
});
};
================================================
FILE: .config/ags/customModules/config.ts
================================================
import { Option } from 'widget/settings/shared/Option';
import { Header } from 'widget/settings/shared/Header';
import options from 'options';
import Scrollable from 'types/widgets/scrollable';
import { Attribute, GtkWidget } from 'lib/types/widget';
export const CustomModuleSettings = (): Scrollable =>
Widget.Scrollable({
vscroll: 'automatic',
hscroll: 'automatic',
class_name: 'menu-theme-page customModules paged-container',
child: Widget.Box({
class_name: 'menu-theme-page paged-container',
vertical: true,
children: [
/*
************************************
* GENERAL *
************************************
*/
Header('General'),
Option({
opt: options.bar.customModules.scrollSpeed,
title: 'Scrolling Speed',
type: 'number',
}),
/*
************************************
* RAM *
************************************
*/
Header('RAM'),
Option({
opt: options.theme.bar.buttons.modules.ram.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.ram.icon,
title: 'Ram Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.ram.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.labelType,
title: 'Label Type',
type: 'enum',
enums: ['used/total', 'used', 'free', 'percentage'],
}),
Option({
opt: options.bar.customModules.ram.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.ram.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.ram.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.middleClick,
title: 'Middle Click',
type: 'string',
}),
/*
************************************
* CPU *
************************************
*/
Header('CPU'),
Option({
opt: options.theme.bar.buttons.modules.cpu.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpu.icon,
title: 'Cpu Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.cpu.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpu.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.cpu.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* STORAGE *
************************************
*/
Header('Storage'),
Option({
opt: options.theme.bar.buttons.modules.storage.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.storage.icon,
title: 'Storage Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.storage.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.labelType,
title: 'Label Type',
type: 'enum',
enums: ['used/total', 'used', 'free', 'percentage'],
}),
Option({
opt: options.bar.customModules.storage.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.storage.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.storage.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.middleClick,
title: 'Middle Click',
type: 'string',
}),
/*
************************************
* NETSTAT *
************************************
*/
Header('Netstat'),
Option({
opt: options.theme.bar.buttons.modules.netstat.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.networkInterface,
title: 'Network Interface',
subtitle:
"Name of the network interface to poll.\nHINT: Get list of interfaces with 'cat /proc/net/dev'",
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.icon,
title: 'Netstat Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.rateUnit,
title: 'Rate Unit',
type: 'enum',
enums: ['GiB', 'MiB', 'KiB', 'auto'],
}),
Option({
opt: options.theme.bar.buttons.modules.netstat.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.labelType,
title: 'Label Type',
type: 'enum',
enums: ['full', 'in', 'out'],
}),
Option({
opt: options.bar.customModules.netstat.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.netstat.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.middleClick,
title: 'Middle Click',
type: 'string',
}),
/*
************************************
* KEYBOARD LAYOUT *
************************************
*/
Header('Keyboard Layout'),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.kbLayout.icon,
title: 'Keyboard Layout Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.kbLayout.labelType,
title: 'Label Type',
type: 'enum',
enums: ['layout', 'code'],
}),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* UPDATES *
************************************
*/
Header('Updates'),
Option({
opt: options.theme.bar.buttons.modules.updates.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.updates.updateCommand,
title: 'Check Updates Command',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.icon,
title: 'Updates Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.updates.padZero,
title: 'Pad with 0',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.updates.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.pollingInterval,
title: 'Polling Interval',
type: 'number',
subtitle: "WARNING: Be careful of your package manager's rate limit.",
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.updates.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* SUBMAP *
************************************
*/
Header('Submap'),
Option({
opt: options.theme.bar.buttons.modules.submap.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.submap.showSubmapName,
title: 'Show Submap Name',
subtitle:
'When enabled, the name of the current submap will be displayed' +
' instead of the Submap Enabled or Disabled text.',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.submap.enabledIcon,
title: 'Enabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.disabledIcon,
title: 'Disabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.enabledText,
title: 'Enabled Text',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.disabledText,
title: 'Disabled Text',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.submap.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* WEATHER *
************************************
*/
Header('Weather'),
Option({
opt: options.theme.bar.buttons.modules.weather.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.weather.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.weather.unit,
title: 'Units',
type: 'enum',
enums: ['imperial', 'metric'],
}),
Option({
opt: options.theme.bar.buttons.modules.weather.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* POWER *
************************************
*/
Header('Power'),
Option({
opt: options.theme.bar.buttons.modules.power.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.power.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.icon,
title: 'Power Button Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
],
}),
});
================================================
FILE: .config/ags/customModules/cpu/computeCPU.ts
================================================
// @ts-expect-error: This import is a special directive that tells the compiler to use the GTop library
import GTop from 'gi://GTop';
let previousCpuData = new GTop.glibtop_cpu();
GTop.glibtop_get_cpu(previousCpuData);
// FIX: Consolidate with Cpu service class
export const computeCPU = (): number => {
const currentCpuData = new GTop.glibtop_cpu();
GTop.glibtop_get_cpu(currentCpuData);
// Calculate the differences from the previous to current data
const totalDiff = currentCpuData.total - previousCpuData.total;
const idleDiff = currentCpuData.idle - previousCpuData.idle;
const cpuUsagePercentage = totalDiff > 0 ? ((totalDiff - idleDiff) / totalDiff) * 100 : 0;
previousCpuData = currentCpuData;
return cpuUsagePercentage;
};
================================================
FILE: .config/ags/customModules/cpu/index.ts
================================================
import options from 'options';
// Module initializer
import { module } from '../module';
import Button from 'types/widgets/button';
// Utility Methods
import { inputHandler } from 'customModules/utils';
import { computeCPU } from './computeCPU';
import { pollVariable } from 'customModules/PollVar';
import { BarBoxChild } from 'lib/types/bar';
import { Attribute, Child } from 'lib/types/widget';
// All the user configurable options for the cpu module that are needed
const { label, round, leftClick, rightClick, middleClick, scrollUp, scrollDown, pollingInterval, icon } =
options.bar.customModules.cpu;
export const cpuUsage = Variable(0);
pollVariable(
// Variable to poll and update with the result of the function passed in
cpuUsage,
// Variables that should trigger the polling function to update when they change
[round.bind('value')],
// Interval at which to poll
pollingInterval.bind('value'),
// Function to execute to get the network data
computeCPU,
);
export const Cpu = (): BarBoxChild => {
const renderLabel = (cpuUsg: number, rnd: boolean): string => {
return rnd ? `${Math.round(cpuUsg)}%` : `${cpuUsg.toFixed(2)}%`;
};
const cpuModule = module({
textIcon: icon.bind('value'),
label: Utils.merge([cpuUsage.bind('value'), round.bind('value')], (cpuUsg, rnd) => {
return renderLabel(cpuUsg, rnd);
}),
tooltipText: 'CPU',
boxClass: 'cpu',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return cpuModule;
};
================================================
FILE: .config/ags/customModules/kblayout/getLayout.ts
================================================
import {
HyprctlDeviceLayout,
HyprctlKeyboard,
KbLabelType,
LayoutKeys,
LayoutValues,
} from 'lib/types/customModules/kbLayout';
import { layoutMap } from './layouts';
export const getKeyboardLayout = (obj: string, format: KbLabelType): LayoutKeys | LayoutValues => {
const hyprctlDevices: HyprctlDeviceLayout = JSON.parse(obj);
const keyboards = hyprctlDevices['keyboards'];
if (keyboards.length === 0) {
return format === 'code' ? 'Unknown' : 'Unknown Layout';
}
let mainKb = keyboards.find((kb: HyprctlKeyboard) => kb.main);
if (!mainKb) {
mainKb = keyboards[keyboards.length - 1];
}
const layout: LayoutKeys = mainKb['active_keymap'] as LayoutKeys;
const foundLayout: LayoutValues = layoutMap[layout];
return format === 'code' ? foundLayout || layout : layout;
};
================================================
FILE: .config/ags/customModules/kblayout/index.ts
================================================
const hyprland = await Service.import('hyprland');
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0';
import Button from 'types/widgets/button';
import Label from 'types/widgets/label';
import { getKeyboardLayout } from './getLayout';
import { BarBoxChild } from 'lib/types/bar';
import { Attribute, Child } from 'lib/types/widget';
const { label, labelType, icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } =
options.bar.customModules.kbLayout;
export const KbInput = (): BarBoxChild => {
const keyboardModule = module({
textIcon: icon.bind('value'),
tooltipText: '',
labelHook: (self: Label): void => {
self.hook(
hyprland,
() => {
Utils.execAsync('hyprctl devices -j')
.then((obj) => {
self.label = getKeyboardLayout(obj, labelType.value);
})
.catch((err) => {
console.error(err);
});
},
'keyboard-layout',
);
self.hook(labelType, () => {
Utils.execAsync('hyprctl devices -j')
.then((obj) => {
self.label = getKeyboardLayout(obj, labelType.value);
})
.catch((err) => {
console.error(err);
});
});
},
boxClass: 'kblayout',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return keyboardModule;
};
================================================
FILE: .config/ags/customModules/kblayout/layouts.ts
================================================
export const layoutMap = {
'Abkhazian (Russia)': 'RU (Ab)',
Akan: 'GH (Akan)',
Albanian: 'AL',
'Albanian (Plisi)': 'AL (Plisi)',
'Albanian (Veqilharxhi)': 'AL (Veqilharxhi)',
Amharic: 'ET',
Arabic: 'ARA',
'Arabic (Algeria)': 'DZ (Ar)',
'Arabic (AZERTY, Eastern Arabic numerals)': 'ARA (Azerty Digits)',
'Arabic (AZERTY)': 'ARA (Azerty)',
'Arabic (Buckwalter)': 'ARA (Buckwalter)',
'Arabic (Eastern Arabic numerals)': 'ARA (Digits)',
'Arabic (Macintosh)': 'ARA (Mac)',
'Arabic (Morocco)': 'MA',
'Arabic (OLPC)': 'ARA (Olpc)',
'Arabic (Pakistan)': 'PK (Ara)',
'Arabic (QWERTY, Eastern Arabic numerals)': 'ARA (Qwerty Digits)',
'Arabic (QWERTY)': 'ARA (Qwerty)',
'Arabic (Syria)': 'SY',
Armenian: 'AM',
'Armenian (alt. eastern)': 'AM (Eastern-Alt)',
'Armenian (alt. phonetic)': 'AM (Phonetic-Alt)',
'Armenian (eastern)': 'AM (Eastern)',
'Armenian (phonetic)': 'AM (Phonetic)',
'Armenian (western)': 'AM (Western)',
'Asturian (Spain, with bottom-dot H and L)': 'ES (Ast)',
Avatime: 'GH (Avn)',
Azerbaijani: 'AZ',
'Azerbaijani (Cyrillic)': 'AZ (Cyrillic)',
'Azerbaijani (Iran)': 'IR (Azb)',
Bambara: 'ML',
Bangla: 'BD',
'Bangla (India, Baishakhi InScript)': 'IN (Ben Inscript)',
'Bangla (India, Baishakhi)': 'IN (Ben Baishakhi)',
'Bangla (India, Bornona)': 'IN (Ben Bornona)',
'Bangla (India, Gitanjali)': 'IN (Ben Gitanjali)',
'Bangla (India, Probhat)': 'IN (Ben Probhat)',
'Bangla (India)': 'IN (Ben)',
'Bangla (Probhat)': 'BD (Probhat)',
Bashkirian: 'RU (Bak)',
Belarusian: 'BY',
'Belarusian (intl.)': 'BY (Intl)',
'Belarusian (Latin)': 'BY (Latin)',
'Belarusian (legacy)': 'BY (Legacy)',
'Belarusian (phonetic)': 'BY (Phonetic)',
Belgian: 'BE',
'Belgian (alt.)': 'BE (Oss)',
'Belgian (ISO, alt.)': 'BE (Iso-Alternate)',
'Belgian (Latin-9 only, alt.)': 'BE (Oss Latin9)',
'Belgian (no dead keys)': 'BE (Nodeadkeys)',
'Belgian (Wang 724 AZERTY)': 'BE (Wang)',
'Berber (Algeria, Latin)': 'DZ',
'Berber (Algeria, Tifinagh)': 'DZ (Ber)',
'Berber (Morocco, Tifinagh alt.)': 'MA (Tifinagh-Alt)',
'Berber (Morocco, Tifinagh extended phonetic)': 'MA (Tifinagh-Extended-Phonetic)',
'Berber (Morocco, Tifinagh extended)': 'MA (Tifinagh-Extended)',
'Berber (Morocco, Tifinagh phonetic, alt.)': 'MA (Tifinagh-Alt-Phonetic)',
'Berber (Morocco, Tifinagh phonetic)': 'MA (Tifinagh-Phonetic)',
'Berber (Morocco, Tifinagh)': 'MA (Tifinagh)',
Bosnian: 'BA',
'Bosnian (US, with Bosnian digraphs)': 'BA (Unicodeus)',
'Bosnian (US)': 'BA (Us)',
'Bosnian (with Bosnian digraphs)': 'BA (Unicode)',
'Bosnian (with guillemets)': 'BA (Alternatequotes)',
Braille: 'BRAI',
'Braille (left-handed inverted thumb)': 'BRAI (Left Hand Invert)',
'Braille (left-handed)': 'BRAI (Left Hand)',
'Braille (right-handed inverted thumb)': 'BRAI (Right Hand Invert)',
'Braille (right-handed)': 'BRAI (Right Hand)',
'Breton (France)': 'FR (Bre)',
Bulgarian: 'BG',
'Bulgarian (enhanced)': 'BG (Bekl)',
'Bulgarian (new phonetic)': 'BG (Bas Phonetic)',
'Bulgarian (traditional phonetic)': 'BG (Phonetic)',
Burmese: 'MM',
'Burmese Zawgyi': 'MM (Zawgyi)',
'Cameroon (AZERTY, intl.)': 'CM (Azerty)',
'Cameroon (Dvorak, intl.)': 'CM (Dvorak)',
'Cameroon Multilingual (QWERTY, intl.)': 'CM (Qwerty)',
'Canadian (CSA)': 'CA (Multix)',
'Catalan (Spain, with middle-dot L)': 'ES (Cat)',
Cherokee: 'US (Chr)',
Chinese: 'CN',
Chuvash: 'RU (Cv)',
'Chuvash (Latin)': 'RU (Cv Latin)',
CloGaelach: 'IE (CloGaelach)',
'Crimean Tatar (Turkish Alt-Q)': 'UA (Crh Alt)',
'Crimean Tatar (Turkish F)': 'UA (Crh F)',
'Crimean Tatar (Turkish Q)': 'UA (Crh)',
Croatian: 'HR',
'Croatian (US, with Croatian digraphs)': 'HR (Unicodeus)',
'Croatian (US)': 'HR (Us)',
'Croatian (with Croatian digraphs)': 'HR (Unicode)',
'Croatian (with guillemets)': 'HR (Alternatequotes)',
Czech: 'CZ',
'Czech (QWERTY, extended backslash)': 'CZ (Qwerty Bksl)',
'Czech (QWERTY, Macintosh)': 'CZ (Qwerty-Mac)',
'Czech (QWERTY)': 'CZ (Qwerty)',
'Czech (UCW, only accented letters)': 'CZ (Ucw)',
'Czech (US, Dvorak, UCW support)': 'CZ (Dvorak-Ucw)',
'Czech (with <\\|> key)': 'CZ (Bksl)',
Danish: 'DK',
'Danish (Dvorak)': 'DK (Dvorak)',
'Danish (Macintosh, no dead keys)': 'DK (Mac Nodeadkeys)',
'Danish (Macintosh)': 'DK (Mac)',
'Danish (no dead keys)': 'DK (Nodeadkeys)',
'Danish (Windows)': 'DK (Winkeys)',
Dari: 'AF',
'Dari (Afghanistan, OLPC)': 'AF (Fa-Olpc)',
Dhivehi: 'MV',
Dutch: 'NL',
'Dutch (Macintosh)': 'NL (Mac)',
'Dutch (standard)': 'NL (Std)',
'Dutch (US)': 'NL (Us)',
Dzongkha: 'BT',
'English (Australian)': 'AU',
'English (Cameroon)': 'CM',
'English (Canada)': 'CA (Eng)',
'English (classic Dvorak)': 'US (Dvorak-Classic)',
'English (Colemak-DH ISO)': 'US (Colemak Dh Iso)',
'English (Colemak-DH)': 'US (Colemak Dh)',
'English (Colemak)': 'US (Colemak)',
'English (Dvorak, alt. intl.)': 'US (Dvorak-Alt-Intl)',
'English (Dvorak, intl., with dead keys)': 'US (Dvorak-Intl)',
'English (Dvorak, left-handed)': 'US (Dvorak-L)',
'English (Dvorak, Macintosh)': 'US (Dvorak-Mac)',
'English (Dvorak, right-handed)': 'US (Dvorak-R)',
'English (Dvorak)': 'US (Dvorak)',
'English (Ghana, GILLBT)': 'GH (Gillbt)',
'English (Ghana, multilingual)': 'GH (Generic)',
'English (Ghana)': 'GH',
'English (India, with rupee)': 'IN (Eng)',
'English (intl., with AltGr dead keys)': 'US (Altgr-Intl)',
'English (Macintosh)': 'US (Mac)',
'English (Mali, US, intl.)': 'ML (Us-Intl)',
'English (Mali, US, Macintosh)': 'ML (Us-Mac)',
'English (Nigeria)': 'NG',
'English (Norman)': 'US (Norman)',
'English (programmer Dvorak)': 'US (Dvp)',
'English (South Africa)': 'ZA',
'English (the divide/multiply toggle the layout)': 'US (Olpc2)',
'English (UK, Colemak-DH)': 'GB (Colemak Dh)',
'English (UK, Colemak)': 'GB (Colemak)',
'English (UK, Dvorak, with UK punctuation)': 'GB (Dvorakukp)',
'English (UK, Dvorak)': 'GB (Dvorak)',
'English (UK, extended, Windows)': 'GB (Extd)',
'English (UK, intl., with dead keys)': 'GB (Intl)',
'English (UK, Macintosh, intl.)': 'GB (Mac Intl)',
'English (UK, Macintosh)': 'GB (Mac)',
'English (UK)': 'GB',
'English (US, alt. intl.)': 'US (Alt-Intl)',
'English (US, euro on 5)': 'US (Euro)',
'English (US, intl., with dead keys)': 'US (Intl)',
'English (US, Symbolic)': 'US (Symbolic)',
'English (US)': 'US',
'English (Workman, intl., with dead keys)': 'US (Workman-Intl)',
'English (Workman)': 'US (Workman)',
Esperanto: 'EPO',
'Esperanto (Brazil, Nativo)': 'BR (Nativo-Epo)',
'Esperanto (legacy)': 'EPO (Legacy)',
'Esperanto (Portugal, Nativo)': 'PT (Nativo-Epo)',
Estonian: 'EE',
'Estonian (Dvorak)': 'EE (Dvorak)',
'Estonian (no dead keys)': 'EE (Nodeadkeys)',
'Estonian (US)': 'EE (Us)',
Ewe: 'GH (Ewe)',
Faroese: 'FO',
'Faroese (no dead keys)': 'FO (Nodeadkeys)',
Filipino: 'PH',
'Filipino (Capewell-Dvorak, Baybayin)': 'PH (Capewell-Dvorak-Bay)',
'Filipino (Capewell-Dvorak, Latin)': 'PH (Capewell-Dvorak)',
'Filipino (Capewell-QWERF 2006, Baybayin)': 'PH (Capewell-Qwerf2k6-Bay)',
'Filipino (Capewell-QWERF 2006, Latin)': 'PH (Capewell-Qwerf2k6)',
'Filipino (Colemak, Baybayin)': 'PH (Colemak-Bay)',
'Filipino (Colemak, Latin)': 'PH (Colemak)',
'Filipino (Dvorak, Baybayin)': 'PH (Dvorak-Bay)',
'Filipino (Dvorak, Latin)': 'PH (Dvorak)',
'Filipino (QWERTY, Baybayin)': 'PH (Qwerty-Bay)',
Finnish: 'FI',
'Finnish (classic, no dead keys)': 'FI (Nodeadkeys)',
'Finnish (classic)': 'FI (Classic)',
'Finnish (Macintosh)': 'FI (Mac)',
'Finnish (Windows)': 'FI (Winkeys)',
French: 'FR',
'French (alt., Latin-9 only)': 'FR (Oss Latin9)',
'French (alt., no dead keys)': 'FR (Oss Nodeadkeys)',
'French (alt.)': 'FR (Oss)',
'French (AZERTY, AFNOR)': 'FR (Afnor)',
'French (AZERTY)': 'FR (Azerty)',
'French (BEPO, AFNOR)': 'FR (Bepo Afnor)',
'French (BEPO, Latin-9 only)': 'FR (Bepo Latin9)',
'French (BEPO)': 'FR (Bepo)',
'French (Cameroon)': 'CM (French)',
'French (Canada, Dvorak)': 'CA (Fr-Dvorak)',
'French (Canada, legacy)': 'CA (Fr-Legacy)',
'French (Canada)': 'CA',
'French (Democratic Republic of the Congo)': 'CD',
'French (Dvorak)': 'FR (Dvorak)',
'French (legacy, alt., no dead keys)': 'FR (Latin9 Nodeadkeys)',
'French (legacy, alt.)': 'FR (Latin9)',
'French (Macintosh)': 'FR (Mac)',
'French (Mali, alt.)': 'ML (Fr-Oss)',
'French (Morocco)': 'MA (French)',
'French (no dead keys)': 'FR (Nodeadkeys)',
'French (Switzerland, Macintosh)': 'CH (Fr Mac)',
'French (Switzerland, no dead keys)': 'CH (Fr Nodeadkeys)',
'French (Switzerland)': 'CH (Fr)',
'French (Togo)': 'TG',
'French (US)': 'FR (Us)',
'Friulian (Italy)': 'IT (Fur)',
Fula: 'GH (Fula)',
Ga: 'GH (Ga)',
Georgian: 'GE',
'Georgian (ergonomic)': 'GE (Ergonomic)',
'Georgian (France, AZERTY Tskapo)': 'FR (Geo)',
'Georgian (Italy)': 'IT (Geo)',
'Georgian (MESS)': 'GE (Mess)',
German: 'DE',
'German (Austria, Macintosh)': 'AT (Mac)',
'German (Austria, no dead keys)': 'AT (Nodeadkeys)',
'German (Austria)': 'AT',
'German (dead acute)': 'DE (Deadacute)',
'German (dead grave acute)': 'DE (Deadgraveacute)',
'German (dead tilde)': 'DE (Deadtilde)',
'German (Dvorak)': 'DE (Dvorak)',
'German (E1)': 'DE (E1)',
'German (E2)': 'DE (E2)',
'German (Macintosh, no dead keys)': 'DE (Mac Nodeadkeys)',
'German (Macintosh)': 'DE (Mac)',
'German (Neo 2)': 'DE (Neo)',
'German (no dead keys)': 'DE (Nodeadkeys)',
'German (QWERTY)': 'DE (Qwerty)',
'German (Switzerland, legacy)': 'CH (Legacy)',
'German (Switzerland, Macintosh)': 'CH (De Mac)',
'German (Switzerland, no dead keys)': 'CH (De Nodeadkeys)',
'German (Switzerland)': 'CH',
'German (T3)': 'DE (T3)',
'German (US)': 'DE (Us)',
Greek: 'GR',
'Greek (extended)': 'GR (Extended)',
'Greek (no dead keys)': 'GR (Nodeadkeys)',
'Greek (polytonic)': 'GR (Polytonic)',
'Greek (simple)': 'GR (Simple)',
Gujarati: 'IN (Guj)',
'Hanyu Pinyin Letters (with AltGr dead keys)': 'CN (Altgr-Pinyin)',
'Hausa (Ghana)': 'GH (Hausa)',
'Hausa (Nigeria)': 'NG (Hausa)',
Hawaiian: 'US (Haw)',
Hebrew: 'IL',
'Hebrew (Biblical, Tiro)': 'IL (Biblical)',
'Hebrew (lyx)': 'IL (Lyx)',
'Hebrew (phonetic)': 'IL (Phonetic)',
'Hindi (Bolnagri)': 'IN (Bolnagri)',
'Hindi (KaGaPa, phonetic)': 'IN (Hin-Kagapa)',
'Hindi (Wx)': 'IN (Hin-Wx)',
Hungarian: 'HU',
'Hungarian (no dead keys)': 'HU (Nodeadkeys)',
'Hungarian (QWERTY, 101-key, comma, dead keys)': 'HU (101 Qwerty Comma Dead)',
'Hungarian (QWERTY, 101-key, comma, no dead keys)': 'HU (101 Qwerty Comma Nodead)',
'Hungarian (QWERTY, 101-key, dot, dead keys)': 'HU (101 Qwerty Dot Dead)',
'Hungarian (QWERTY, 101-key, dot, no dead keys)': 'HU (101 Qwerty Dot Nodead)',
'Hungarian (QWERTY, 102-key, comma, dead keys)': 'HU (102 Qwerty Comma Dead)',
'Hungarian (QWERTY, 102-key, comma, no dead keys)': 'HU (102 Qwerty Comma Nodead)',
'Hungarian (QWERTY, 102-key, dot, dead keys)': 'HU (102 Qwerty Dot Dead)',
'Hungarian (QWERTY, 102-key, dot, no dead keys)': 'HU (102 Qwerty Dot Nodead)',
'Hungarian (QWERTY)': 'HU (Qwerty)',
'Hungarian (QWERTZ, 101-key, comma, dead keys)': 'HU (101 Qwertz Comma Dead)',
'Hungarian (QWERTZ, 101-key, comma, no dead keys)': 'HU (101 Qwertz Comma Nodead)',
'Hungarian (QWERTZ, 101-key, dot, dead keys)': 'HU (101 Qwertz Dot Dead)',
'Hungarian (QWERTZ, 101-key, dot, no dead keys)': 'HU (101 Qwertz Dot Nodead)',
'Hungarian (QWERTZ, 102-key, comma, dead keys)': 'HU (102 Qwertz Comma Dead)',
'Hungarian (QWERTZ, 102-key, comma, no dead keys)': 'HU (102 Qwertz Comma Nodead)',
'Hungarian (QWERTZ, 102-key, dot, dead keys)': 'HU (102 Qwertz Dot Dead)',
'Hungarian (QWERTZ, 102-key, dot, no dead keys)': 'HU (102 Qwertz Dot Nodead)',
'Hungarian (standard)': 'HU (Standard)',
Icelandic: 'IS',
'Icelandic (Dvorak)': 'IS (Dvorak)',
'Icelandic (Macintosh, legacy)': 'IS (Mac Legacy)',
'Icelandic (Macintosh)': 'IS (Mac)',
Igbo: 'NG (Igbo)',
Indian: 'IN',
'Indic IPA': 'IN (Iipa)',
'Indonesian (Arab Melayu, extended phonetic)': 'ID (Melayu-Phoneticx)',
'Indonesian (Arab Melayu, phonetic)': 'ID (Melayu-Phonetic)',
'Indonesian (Arab Pegon, phonetic)': 'ID (Pegon-Phonetic)',
'Indonesian (Latin)': 'ID',
Inuktitut: 'CA (Ike)',
Iraqi: 'IQ',
Irish: 'IE',
'Irish (UnicodeExpert)': 'IE (UnicodeExpert)',
Italian: 'IT',
'Italian (IBM 142)': 'IT (Ibm)',
'Italian (intl., with dead keys)': 'IT (Intl)',
'Italian (Macintosh)': 'IT (Mac)',
'Italian (no dead keys)': 'IT (Nodeadkeys)',
'Italian (US)': 'IT (Us)',
'Italian (Windows)': 'IT (Winkeys)',
Japanese: 'JP',
'Japanese (Dvorak)': 'JP (Dvorak)',
'Japanese (Kana 86)': 'JP (Kana86)',
'Japanese (Kana)': 'JP (Kana)',
'Japanese (Macintosh)': 'JP (Mac)',
'Japanese (OADG 109A)': 'JP (OADG109A)',
Javanese: 'ID (Javanese)',
'Kabyle (AZERTY, with dead keys)': 'DZ (Azerty-Deadkeys)',
'Kabyle (QWERTY, UK, with dead keys)': 'DZ (Qwerty-Gb-Deadkeys)',
'Kabyle (QWERTY, US, with dead keys)': 'DZ (Qwerty-Us-Deadkeys)',
Kalmyk: 'RU (Xal)',
Kannada: 'IN (Kan)',
'Kannada (KaGaPa, phonetic)': 'IN (Kan-Kagapa)',
Kashubian: 'PL (Csb)',
Kazakh: 'KZ',
'Kazakh (extended)': 'KZ (Ext)',
'Kazakh (Latin)': 'KZ (Latin)',
'Kazakh (with Russian)': 'KZ (Kazrus)',
'Khmer (Cambodia)': 'KH',
Kikuyu: 'KE (Kik)',
Komi: 'RU (Kom)',
Korean: 'KR',
'Korean (101/104-key compatible)': 'KR (Kr104)',
'Kurdish (Iran, Arabic-Latin)': 'IR (Ku Ara)',
'Kurdish (Iran, F)': 'IR (Ku F)',
'Kurdish (Iran, Latin Alt-Q)': 'IR (Ku Alt)',
'Kurdish (Iran, Latin Q)': 'IR (Ku)',
'Kurdish (Iraq, Arabic-Latin)': 'IQ (Ku Ara)',
'Kurdish (Iraq, F)': 'IQ (Ku F)',
'Kurdish (Iraq, Latin Alt-Q)': 'IQ (Ku Alt)',
'Kurdish (Iraq, Latin Q)': 'IQ (Ku)',
'Kurdish (Syria, F)': 'SY (Ku F)',
'Kurdish (Syria, Latin Alt-Q)': 'SY (Ku Alt)',
'Kurdish (Syria, Latin Q)': 'SY (Ku)',
'Kurdish (Turkey, F)': 'TR (Ku F)',
'Kurdish (Turkey, Latin Alt-Q)': 'TR (Ku Alt)',
'Kurdish (Turkey, Latin Q)': 'TR (Ku)',
Kyrgyz: 'KG',
'Kyrgyz (phonetic)': 'KG (Phonetic)',
Lao: 'LA',
'Lao (STEA)': 'LA (Stea)',
Latvian: 'LV',
'Latvian (adapted)': 'LV (Adapted)',
'Latvian (apostrophe)': 'LV (Apostrophe)',
'Latvian (ergonomic, ŪGJRMV)': 'LV (Ergonomic)',
'Latvian (F)': 'LV (Fkey)',
'Latvian (modern)': 'LV (Modern)',
'Latvian (tilde)': 'LV (Tilde)',
Lithuanian: 'LT',
'Lithuanian (IBM LST 1205-92)': 'LT (Ibm)',
'Lithuanian (LEKP)': 'LT (Lekp)',
'Lithuanian (LEKPa)': 'LT (Lekpa)',
'Lithuanian (Ratise)': 'LT (Ratise)',
'Lithuanian (standard)': 'LT (Std)',
'Lithuanian (US)': 'LT (Us)',
'Lower Sorbian': 'DE (Dsb)',
'Lower Sorbian (QWERTZ)': 'DE (Dsb Qwertz)',
Macedonian: 'MK',
'Macedonian (no dead keys)': 'MK (Nodeadkeys)',
'Malay (Jawi, Arabic Keyboard)': 'MY',
'Malay (Jawi, phonetic)': 'MY (Phonetic)',
Malayalam: 'IN (Mal)',
'Malayalam (enhanced InScript, with rupee)': 'IN (Mal Enhanced)',
'Malayalam (Lalitha)': 'IN (Mal Lalitha)',
Maltese: 'MT',
'Maltese (UK, with AltGr overrides)': 'MT (Alt-Gb)',
'Maltese (US, with AltGr overrides)': 'MT (Alt-Us)',
'Maltese (US)': 'MT (Us)',
'Manipuri (Eeyek)': 'IN (Eeyek)',
Maori: 'MAO',
'Marathi (enhanced InScript)': 'IN (Marathi)',
'Marathi (KaGaPa, phonetic)': 'IN (Mar-Kagapa)',
Mari: 'RU (Chm)',
Mmuock: 'CM (Mmuock)',
Moldavian: 'MD',
'Moldavian (Gagauz)': 'MD (Gag)',
Mon: 'MM (Mnw)',
'Mon (A1)': 'MM (Mnw-A1)',
Mongolian: 'MN',
'Mongolian (Bichig)': 'CN (Mon Trad)',
'Mongolian (Galik)': 'CN (Mon Trad Galik)',
'Mongolian (Manchu Galik)': 'CN (Mon Manchu Galik)',
'Mongolian (Manchu)': 'CN (Mon Trad Manchu)',
'Mongolian (Todo Galik)': 'CN (Mon Todo Galik)',
'Mongolian (Todo)': 'CN (Mon Trad Todo)',
'Mongolian (Xibe)': 'CN (Mon Trad Xibe)',
Montenegrin: 'ME',
'Montenegrin (Cyrillic, with guillemets)': 'ME (Cyrillicalternatequotes)',
'Montenegrin (Cyrillic, ZE and ZHE swapped)': 'ME (Cyrillicyz)',
'Montenegrin (Cyrillic)': 'ME (Cyrillic)',
'Montenegrin (Latin, QWERTY)': 'ME (Latinyz)',
'Montenegrin (Latin, Unicode, QWERTY)': 'ME (Latinunicodeyz)',
'Montenegrin (Latin, Unicode)': 'ME (Latinunicode)',
'Montenegrin (Latin, with guillemets)': 'ME (Latinalternatequotes)',
"N'Ko (AZERTY)": 'GN',
Nepali: 'NP',
'Northern Saami (Finland)': 'FI (Smi)',
'Northern Saami (Norway, no dead keys)': 'NO (Smi Nodeadkeys)',
'Northern Saami (Norway)': 'NO (Smi)',
'Northern Saami (Sweden)': 'SE (Smi)',
Norwegian: 'NO',
'Norwegian (Colemak)': 'NO (Colemak)',
'Norwegian (Dvorak)': 'NO (Dvorak)',
'Norwegian (Macintosh, no dead keys)': 'NO (Mac Nodeadkeys)',
'Norwegian (Macintosh)': 'NO (Mac)',
'Norwegian (no dead keys)': 'NO (Nodeadkeys)',
'Norwegian (Windows)': 'NO (Winkeys)',
Occitan: 'FR (Oci)',
Ogham: 'IE (Ogam)',
'Ogham (IS434)': 'IE (Ogam Is434)',
'Ol Chiki': 'IN (Olck)',
'Old Turkic': 'TR (Otk)',
'Old Turkic (F)': 'TR (Otkf)',
Oriya: 'IN (Ori)',
'Oriya (Bolnagri)': 'IN (Ori-Bolnagri)',
'Oriya (Wx)': 'IN (Ori-Wx)',
'Ossetian (Georgia)': 'GE (Os)',
'Ossetian (legacy)': 'RU (Os Legacy)',
'Ossetian (Windows)': 'RU (Os Winkeys)',
'Ottoman (F)': 'TR (Otf)',
'Ottoman (Q)': 'TR (Ot)',
'Pannonian Rusyn': 'RS (Rue)',
Pashto: 'AF (Ps)',
'Pashto (Afghanistan, OLPC)': 'AF (Ps-Olpc)',
Persian: 'IR',
'Persian (with Persian keypad)': 'IR (Pes Keypad)',
Polish: 'PL',
'Polish (British keyboard)': 'GB (Pl)',
'Polish (Dvorak, with Polish quotes on key 1)': 'PL (Dvorak Altquotes)',
'Polish (Dvorak, with Polish quotes on quotemark key)': 'PL (Dvorak Quotes)',
'Polish (Dvorak)': 'PL (Dvorak)',
'Polish (legacy)': 'PL (Legacy)',
'Polish (programmer Dvorak)': 'PL (Dvp)',
'Polish (QWERTZ)': 'PL (Qwertz)',
Portuguese: 'PT',
'Portuguese (Brazil, Dvorak)': 'BR (Dvorak)',
'Portuguese (Brazil, IBM/Lenovo ThinkPad)': 'BR (Thinkpad)',
'Portuguese (Brazil, Nativo for US keyboards)': 'BR (Nativo-Us)',
'Portuguese (Brazil, Nativo)': 'BR (Nativo)',
'Portuguese (Brazil, no dead keys)': 'BR (Nodeadkeys)',
'Portuguese (Brazil)': 'BR',
'Portuguese (Macintosh, no dead keys)': 'PT (Mac Nodeadkeys)',
'Portuguese (Macintosh)': 'PT (Mac)',
'Portuguese (Nativo for US keyboards)': 'PT (Nativo-Us)',
'Portuguese (Nativo)': 'PT (Nativo)',
'Portuguese (no dead keys)': 'PT (Nodeadkeys)',
'Punjabi (Gurmukhi Jhelum)': 'IN (Jhelum)',
'Punjabi (Gurmukhi)': 'IN (Guru)',
Romanian: 'RO',
'Romanian (Germany, no dead keys)': 'DE (Ro Nodeadkeys)',
'Romanian (Germany)': 'DE (Ro)',
'Romanian (standard)': 'RO (Std)',
'Romanian (Windows)': 'RO (Winkeys)',
Russian: 'RU',
'Russian (Belarus)': 'BY (Ru)',
'Russian (Czech, phonetic)': 'CZ (Rus)',
'Russian (DOS)': 'RU (Dos)',
'Russian (engineering, EN)': 'RU (Ruchey En)',
'Russian (engineering, RU)': 'RU (Ruchey Ru)',
'Russian (Georgia)': 'GE (Ru)',
'Russian (Germany, phonetic)': 'DE (Ru)',
'Russian (Kazakhstan, with Kazakh)': 'KZ (Ruskaz)',
'Russian (legacy)': 'RU (Legacy)',
'Russian (Macintosh)': 'RU (Mac)',
'Russian (phonetic, AZERTY)': 'RU (Phonetic Azerty)',
'Russian (phonetic, Dvorak)': 'RU (Phonetic Dvorak)',
'Russian (phonetic, French)': 'RU (Phonetic Fr)',
'Russian (phonetic, Windows)': 'RU (Phonetic Winkeys)',
'Russian (phonetic, YAZHERTY)': 'RU (Phonetic YAZHERTY)',
'Russian (phonetic)': 'RU (Phonetic)',
'Russian (Poland, phonetic Dvorak)': 'PL (Ru Phonetic Dvorak)',
'Russian (Sweden, phonetic, no dead keys)': 'SE (Rus Nodeadkeys)',
'Russian (Sweden, phonetic)': 'SE (Rus)',
'Russian (typewriter, legacy)': 'RU (Typewriter-Legacy)',
'Russian (typewriter)': 'RU (Typewriter)',
'Russian (Ukraine, standard RSTU)': 'UA (Rstu Ru)',
'Russian (US, phonetic)': 'US (Rus)',
'Saisiyat (Taiwan)': 'TW (Saisiyat)',
Samogitian: 'LT (Sgs)',
'Sanskrit (KaGaPa, phonetic)': 'IN (San-Kagapa)',
'Scottish Gaelic': 'GB (Gla)',
Serbian: 'RS',
'Serbian (Cyrillic, with guillemets)': 'RS (Alternatequotes)',
'Serbian (Cyrillic, ZE and ZHE swapped)': 'RS (Yz)',
'Serbian (Latin, QWERTY)': 'RS (Latinyz)',
'Serbian (Latin, Unicode, QWERTY)': 'RS (Latinunicodeyz)',
'Serbian (Latin, Unicode)': 'RS (Latinunicode)',
'Serbian (Latin, with guillemets)': 'RS (Latinalternatequotes)',
'Serbian (Latin)': 'RS (Latin)',
'Serbian (Russia)': 'RU (Srp)',
'Serbo-Croatian (US)': 'US (Hbs)',
Shan: 'MM (Shn)',
'Shan (Zawgyi Tai)': 'MM (Zgt)',
Sicilian: 'IT (Scn)',
Silesian: 'PL (Szl)',
Sindhi: 'PK (Snd)',
'Sinhala (phonetic)': 'LK',
'Sinhala (US)': 'LK (Us)',
Slovak: 'SK',
'Slovak (extended backslash)': 'SK (Bksl)',
'Slovak (QWERTY, extended backslash)': 'SK (Qwerty Bksl)',
'Slovak (QWERTY)': 'SK (Qwerty)',
Slovenian: 'SI',
'Slovenian (US)': 'SI (Us)',
'Slovenian (with guillemets)': 'SI (Alternatequotes)',
Spanish: 'ES',
'Spanish (dead tilde)': 'ES (Deadtilde)',
'Spanish (Dvorak)': 'ES (Dvorak)',
'Spanish (Latin American, Colemak)': 'LATAM (Colemak)',
'Spanish (Latin American, dead tilde)': 'LATAM (Deadtilde)',
'Spanish (Latin American, Dvorak)': 'LATAM (Dvorak)',
'Spanish (Latin American, no dead keys)': 'LATAM (Nodeadkeys)',
'Spanish (Latin American)': 'LATAM',
'Spanish (Macintosh)': 'ES (Mac)',
'Spanish (no dead keys)': 'ES (Nodeadkeys)',
'Spanish (Windows)': 'ES (Winkeys)',
'Swahili (Kenya)': 'KE',
'Swahili (Tanzania)': 'TZ',
Swedish: 'SE',
'Swedish (Dvorak, intl.)': 'SE (Us Dvorak)',
'Swedish (Dvorak)': 'SE (Dvorak)',
'Swedish (Macintosh)': 'SE (Mac)',
'Swedish (no dead keys)': 'SE (Nodeadkeys)',
'Swedish (Svdvorak)': 'SE (Svdvorak)',
'Swedish (US)': 'SE (Us)',
'Swedish Sign Language': 'SE (Swl)',
Syriac: 'SY (Syc)',
'Syriac (phonetic)': 'SY (Syc Phonetic)',
Taiwanese: 'TW',
'Taiwanese (indigenous)': 'TW (Indigenous)',
Tajik: 'TJ',
'Tajik (legacy)': 'TJ (Legacy)',
'Tamil (InScript, with Arabic numerals)': 'IN (Tam)',
'Tamil (InScript, with Tamil numerals)': 'IN (Tam Tamilnumbers)',
"Tamil (Sri Lanka, TamilNet '99, TAB encoding)": 'LK (Tam TAB)',
"Tamil (Sri Lanka, TamilNet '99)": 'LK (Tam Unicode)',
"Tamil (TamilNet '99 with Tamil numerals)": 'IN (Tamilnet Tamilnumbers)',
"Tamil (TamilNet '99, TAB encoding)": 'IN (Tamilnet TAB)',
"Tamil (TamilNet '99, TSCII encoding)": 'IN (Tamilnet TSCII)',
"Tamil (TamilNet '99)": 'IN (Tamilnet)',
Tarifit: 'MA (Rif)',
Tatar: 'RU (Tt)',
Telugu: 'IN (Tel)',
'Telugu (KaGaPa, phonetic)': 'IN (Tel-Kagapa)',
'Telugu (Sarala)': 'IN (Tel-Sarala)',
Thai: 'TH',
'Thai (Pattachote)': 'TH (Pat)',
'Thai (TIS-820.2538)': 'TH (Tis)',
Tibetan: 'CN (Tib)',
'Tibetan (with ASCII numerals)': 'CN (Tib Asciinum)',
Tswana: 'BW',
Turkish: 'TR',
'Turkish (Alt-Q)': 'TR (Alt)',
'Turkish (E)': 'TR (E)',
'Turkish (F)': 'TR (F)',
'Turkish (Germany)': 'DE (Tr)',
'Turkish (intl., with dead keys)': 'TR (Intl)',
Turkmen: 'TM',
'Turkmen (Alt-Q)': 'TM (Alt)',
Udmurt: 'RU (Udm)',
Ukrainian: 'UA',
'Ukrainian (homophonic)': 'UA (Homophonic)',
'Ukrainian (legacy)': 'UA (Legacy)',
'Ukrainian (macOS)': 'UA (MacOS)',
'Ukrainian (phonetic)': 'UA (Phonetic)',
'Ukrainian (standard RSTU)': 'UA (Rstu)',
'Ukrainian (typewriter)': 'UA (Typewriter)',
'Ukrainian (Windows)': 'UA (Winkeys)',
'Urdu (alt. phonetic)': 'IN (Urd-Phonetic3)',
'Urdu (Pakistan, CRULP)': 'PK (Urd-Crulp)',
'Urdu (Pakistan, NLA)': 'PK (Urd-Nla)',
'Urdu (Pakistan)': 'PK',
'Urdu (phonetic)': 'IN (Urd-Phonetic)',
'Urdu (Windows)': 'IN (Urd-Winkeys)',
Uyghur: 'CN (Ug)',
Uzbek: 'UZ',
'Uzbek (Afghanistan, OLPC)': 'AF (Uz-Olpc)',
'Uzbek (Afghanistan)': 'AF (Uz)',
'Uzbek (Latin)': 'UZ (Latin)',
Vietnamese: 'VN',
'Vietnamese (France)': 'VN (Fr)',
'Vietnamese (US)': 'VN (Us)',
Wolof: 'SN',
Yakut: 'RU (Sah)',
Yoruba: 'NG (Yoruba)',
'Unknown Layout': 'Unknown',
} as const;
================================================
FILE: .config/ags/customModules/module.ts
================================================
import { BarBoxChild, Module } from 'lib/types/bar';
import { BarButtonStyles } from 'lib/types/options';
import { Bind } from 'lib/types/variable';
import { GtkWidget } from 'lib/types/widget';
import options from 'options';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0';
const { style } = options.theme.bar.buttons;
const undefinedVar = Variable(undefined);
export const module = ({
icon,
textIcon,
label,
tooltipText,
boxClass,
props = {},
showLabelBinding = undefinedVar.bind('value'),
showLabel,
labelHook,
hook,
}: Module): BarBoxChild => {
const getIconWidget = (): GtkWidget | undefined => {
let iconWidget: Gtk.Widget | undefined;
if (icon !== undefined) {
iconWidget = Widget.Icon({
class_name: `txt-icon bar-button-icon module-icon ${boxClass}`,
icon: icon,
}) as unknown as Gtk.Widget;
} else if (textIcon !== undefined) {
iconWidget = Widget.Label({
class_name: `txt-icon bar-button-icon module-icon ${boxClass}`,
label: textIcon,
}) as unknown as Gtk.Widget;
}
return iconWidget;
};
return {
component: Widget.Box({
className: Utils.merge(
[style.bind('value'), showLabelBinding],
(style: BarButtonStyles, shwLabel: boolean) => {
const shouldShowLabel = shwLabel || showLabel;
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `${boxClass} ${styleMap[style]} ${!shouldShowLabel ? 'no-label' : ''}`;
},
),
tooltip_text: tooltipText,
children: Utils.merge([showLabelBinding], (showLabelBinding): Gtk.Widget[] => {
const childrenArray: Gtk.Widget[] = [];
const iconWidget = getIconWidget();
if (iconWidget !== undefined) {
childrenArray.push(iconWidget);
}
if (showLabelBinding) {
childrenArray.push(
Widget.Label({
class_name: `bar-button-label module-label ${boxClass}`,
label: label,
setup: labelHook,
}) as unknown as Gtk.Widget,
);
}
return childrenArray;
}) as Bind,
setup: hook,
}),
tooltip_text: tooltipText,
isVisible: true,
boxClass,
props,
};
};
================================================
FILE: .config/ags/customModules/netstat/computeNetwork.ts
================================================
import GLib from 'gi://GLib';
import { Variable as VariableType } from 'types/variable';
import { NetworkResourceData } from 'lib/types/customModules/network';
import { GET_DEFAULT_NETSTAT_DATA } from 'lib/types/defaults/netstat';
import { RateUnit } from 'lib/types/bar';
let previousNetUsage = { rx: 0, tx: 0, time: 0 };
const formatRate = (rate: number, type: string, round: boolean): string => {
const fixed = round ? 0 : 2;
switch (true) {
case type === 'KiB':
return `${(rate / 1e3).toFixed(fixed)} KiB/s`;
case type === 'MiB':
return `${(rate / 1e6).toFixed(fixed)} MiB/s`;
case type === 'GiB':
return `${(rate / 1e9).toFixed(fixed)} GiB/s`;
case rate >= 1e9:
return `${(rate / 1e9).toFixed(fixed)} GiB/s`;
case rate >= 1e6:
return `${(rate / 1e6).toFixed(fixed)} MiB/s`;
case rate >= 1e3:
return `${(rate / 1e3).toFixed(fixed)} KiB/s`;
default:
return `${rate.toFixed(fixed)} bytes/s`;
}
};
interface NetworkUsage {
name: string;
rx: number;
tx: number;
}
const parseInterfaceData = (line: string): NetworkUsage | null => {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('Inter-') || trimmedLine.startsWith('face')) {
return null;
}
const [iface, rx, , , , , , , , tx] = trimmedLine.split(/\s+/);
const rxValue = parseInt(rx, 10);
const txValue = parseInt(tx, 10);
const cleanedIface = iface.replace(':', '');
return { name: cleanedIface, rx: rxValue, tx: txValue };
};
const isValidInterface = (iface: NetworkUsage | null, interfaceName: string): boolean => {
if (!iface) return false;
if (interfaceName) return iface.name === interfaceName;
return iface.name !== 'lo' && iface.rx > 0 && iface.tx > 0;
};
const getNetworkUsage = (interfaceName: string = ''): NetworkUsage => {
const [success, data] = GLib.file_get_contents('/proc/net/dev');
if (!success) {
console.error('Failed to read /proc/net/dev');
return { name: '', rx: 0, tx: 0 };
}
const lines = new TextDecoder('utf-8').decode(data).split('\n');
for (const line of lines) {
const iface = parseInterfaceData(line);
if (isValidInterface(iface, interfaceName)) {
return iface!;
}
}
return { name: '', rx: 0, tx: 0 };
};
export const computeNetwork = (
round: VariableType,
interfaceNameVar: VariableType,
dataType: VariableType,
): NetworkResourceData => {
const rateUnit = dataType.value;
const interfaceName = interfaceNameVar ? interfaceNameVar.value : '';
const DEFAULT_NETSTAT_DATA = GET_DEFAULT_NETSTAT_DATA(rateUnit);
try {
const { rx, tx, name } = getNetworkUsage(interfaceName);
const currentTime = Date.now();
if (!name) {
return DEFAULT_NETSTAT_DATA;
}
if (previousNetUsage.time === 0) {
previousNetUsage = { rx, tx, time: currentTime };
return DEFAULT_NETSTAT_DATA;
}
const timeDiff = Math.max((currentTime - previousNetUsage.time) / 1000, 1);
const rxRate = (rx - previousNetUsage.rx) / timeDiff;
const txRate = (tx - previousNetUsage.tx) / timeDiff;
previousNetUsage = { rx, tx, time: currentTime };
return {
in: formatRate(rxRate, rateUnit, round.value),
out: formatRate(txRate, rateUnit, round.value),
};
} catch (error) {
console.error('Error calculating network usage:', error);
return DEFAULT_NETSTAT_DATA;
}
};
================================================
FILE: .config/ags/customModules/netstat/index.ts
================================================
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import { computeNetwork } from './computeNetwork';
import { BarBoxChild, NetstatLabelType } from 'lib/types/bar';
import Button from 'types/widgets/button';
import { NetworkResourceData } from 'lib/types/customModules/network';
import { NETWORK_LABEL_TYPES } from 'lib/types/defaults/bar';
import { GET_DEFAULT_NETSTAT_DATA } from 'lib/types/defaults/netstat';
import { pollVariable } from 'customModules/PollVar';
import { Attribute, Child } from 'lib/types/widget';
const {
label,
labelType,
networkInterface,
rateUnit,
icon,
round,
leftClick,
rightClick,
middleClick,
pollingInterval,
} = options.bar.customModules.netstat;
export const networkUsage = Variable(GET_DEFAULT_NETSTAT_DATA(rateUnit.value));
pollVariable(
// Variable to poll and update with the result of the function passed in
networkUsage,
// Variables that should trigger the polling function to update when they change
[rateUnit.bind('value'), networkInterface.bind('value'), round.bind('value')],
// Interval at which to poll
pollingInterval.bind('value'),
// Function to execute to get the network data
computeNetwork,
// Optional parameters to pass to the function
// round is a boolean that determines whether to round the values
round,
// Optional parameters to pass to the function
// networkInterface is the interface name to filter the data
networkInterface,
// Optional parameters to pass to the function
// rateUnit is the unit to display the data in
// e.g. KiB, MiB, GiB, etc.
rateUnit,
);
export const Netstat = (): BarBoxChild => {
const renderNetworkLabel = (lblType: NetstatLabelType, network: NetworkResourceData): string => {
switch (lblType) {
case 'in':
return `↓ ${network.in}`;
case 'out':
return `↑ ${network.out}`;
default:
return `↓ ${network.in} ↑ ${network.out}`;
}
};
const netstatModule = module({
textIcon: icon.bind('value'),
label: Utils.merge(
[networkUsage.bind('value'), labelType.bind('value')],
(network: NetworkResourceData, lblTyp: NetstatLabelType) => renderNetworkLabel(lblTyp, network),
),
tooltipText: labelType.bind('value').as((lblTyp) => {
return lblTyp === 'full' ? 'Ingress / Egress' : lblTyp === 'in' ? 'Ingress' : 'Egress';
}),
boxClass: 'netstat',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
fn: () => {
labelType.value = NETWORK_LABEL_TYPES[
(NETWORK_LABEL_TYPES.indexOf(labelType.value) + 1) % NETWORK_LABEL_TYPES.length
] as NetstatLabelType;
},
},
onScrollDown: {
fn: () => {
labelType.value = NETWORK_LABEL_TYPES[
(NETWORK_LABEL_TYPES.indexOf(labelType.value) - 1 + NETWORK_LABEL_TYPES.length) %
NETWORK_LABEL_TYPES.length
] as NetstatLabelType;
},
},
});
},
},
});
return netstatModule;
};
================================================
FILE: .config/ags/customModules/power/index.ts
================================================
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import { Attribute, Child } from 'lib/types/widget';
import { BarBoxChild } from 'lib/types/bar';
const { icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.power;
export const Power = (): BarBoxChild => {
const powerModule = module({
tooltipText: 'Power Menu',
textIcon: icon.bind('value'),
boxClass: 'powermodule',
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return powerModule;
};
================================================
FILE: .config/ags/customModules/ram/computeRam.ts
================================================
const GLib = imports.gi.GLib;
import { divide } from 'customModules/utils';
import { GenericResourceData } from 'lib/types/customModules/generic';
import { Variable as VariableType } from 'types/variable';
// FIX: Consolidate with Ram service class
export const calculateRamUsage = (round: VariableType): GenericResourceData => {
try {
const [success, meminfoBytes] = GLib.file_get_contents('/proc/meminfo');
if (!success || !meminfoBytes) {
throw new Error('Failed to read /proc/meminfo or file content is null.');
}
const meminfo = new TextDecoder('utf-8').decode(meminfoBytes);
const totalMatch = meminfo.match(/MemTotal:\s+(\d+)/);
const availableMatch = meminfo.match(/MemAvailable:\s+(\d+)/);
if (!totalMatch || !availableMatch) {
throw new Error('Failed to parse /proc/meminfo for memory values.');
}
const totalRamInBytes = parseInt(totalMatch[1], 10) * 1024;
const availableRamInBytes = parseInt(availableMatch[1], 10) * 1024;
let usedRam = totalRamInBytes - availableRamInBytes;
usedRam = isNaN(usedRam) || usedRam < 0 ? 0 : usedRam;
return {
percentage: divide([totalRamInBytes, usedRam], round.value),
total: totalRamInBytes,
used: usedRam,
free: availableRamInBytes,
};
} catch (error) {
console.error('Error calculating RAM usage:', error);
return { total: 0, used: 0, percentage: 0, free: 0 };
}
};
================================================
FILE: .config/ags/customModules/ram/index.ts
================================================
import options from 'options';
// Module initializer
import { module } from '../module';
// Types
import { GenericResourceData } from 'lib/types/customModules/generic';
import Button from 'types/widgets/button';
// Helper Methods
import { calculateRamUsage } from './computeRam';
// Utility Methods
import { formatTooltip, inputHandler, renderResourceLabel } from 'customModules/utils';
import { BarBoxChild, ResourceLabelType } from 'lib/types/bar';
// Global Constants
import { LABEL_TYPES } from 'lib/types/defaults/bar';
import { pollVariable } from 'customModules/PollVar';
import { Attribute, Child } from 'lib/types/widget';
// All the user configurable options for the ram module that are needed
const { label, labelType, round, leftClick, rightClick, middleClick, pollingInterval, icon } =
options.bar.customModules.ram;
const defaultRamData: GenericResourceData = { total: 0, used: 0, percentage: 0, free: 0 };
const ramUsage = Variable(defaultRamData);
pollVariable(ramUsage, [round.bind('value')], pollingInterval.bind('value'), calculateRamUsage, round);
export const Ram = (): BarBoxChild => {
const ramModule = module({
textIcon: icon.bind('value'),
label: Utils.merge(
[ramUsage.bind('value'), labelType.bind('value'), round.bind('value')],
(rmUsg: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
const returnValue = renderResourceLabel(lblTyp, rmUsg, round);
return returnValue;
},
),
tooltipText: labelType.bind('value').as((lblTyp) => {
return formatTooltip('RAM', lblTyp);
}),
boxClass: 'ram',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) + 1) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
onScrollDown: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
});
},
},
});
return ramModule;
};
================================================
FILE: .config/ags/customModules/storage/computeStorage.ts
================================================
// @ts-expect-error is a special directive that tells the compiler to use the GTop library
import GTop from 'gi://GTop';
import { divide } from 'customModules/utils';
import { Variable as VariableType } from 'types/variable';
import { GenericResourceData } from 'lib/types/customModules/generic';
// FIX: Consolidate with Storage service class
export const computeStorage = (round: VariableType): GenericResourceData => {
try {
const currentFsUsage = new GTop.glibtop_fsusage();
GTop.glibtop_get_fsusage(currentFsUsage, '/');
const total = currentFsUsage.blocks * currentFsUsage.block_size;
const available = currentFsUsage.bavail * currentFsUsage.block_size;
const used = total - available;
return {
total,
used,
free: available,
percentage: divide([total, used], round.value),
};
} catch (error) {
console.error('Error calculating RAM usage:', error);
return { total: 0, used: 0, percentage: 0, free: 0 };
}
};
================================================
FILE: .config/ags/customModules/storage/index.ts
================================================
import options from 'options';
import { module } from '../module';
import { formatTooltip, inputHandler, renderResourceLabel } from 'customModules/utils';
import { computeStorage } from './computeStorage';
import { BarBoxChild, ResourceLabelType } from 'lib/types/bar';
import { GenericResourceData } from 'lib/types/customModules/generic';
import Button from 'types/widgets/button';
import { LABEL_TYPES } from 'lib/types/defaults/bar';
import { pollVariable } from 'customModules/PollVar';
import { Attribute, Child } from 'lib/types/widget';
const { label, labelType, icon, round, leftClick, rightClick, middleClick, pollingInterval } =
options.bar.customModules.storage;
const defaultStorageData = { total: 0, used: 0, percentage: 0, free: 0 };
const storageUsage = Variable(defaultStorageData);
pollVariable(storageUsage, [round.bind('value')], pollingInterval.bind('value'), computeStorage, round);
export const Storage = (): BarBoxChild => {
const storageModule = module({
textIcon: icon.bind('value'),
label: Utils.merge(
[storageUsage.bind('value'), labelType.bind('value'), round.bind('value')],
(storage: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
return renderResourceLabel(lblTyp, storage, round);
},
),
tooltipText: labelType.bind('value').as((lblTyp) => {
return formatTooltip('Storage', lblTyp);
}),
boxClass: 'storage',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) + 1) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
onScrollDown: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
});
},
},
});
return storageModule;
};
================================================
FILE: .config/ags/customModules/submap/helpers.ts
================================================
import { Variable } from 'types/variable';
const hyprland = await Service.import('hyprland');
export const isSubmapEnabled = (submap: string, enabled: string, disabled: string): string => {
return submap !== 'default' ? enabled : disabled;
};
export const getInitialSubmap = (submapStatus: Variable): void => {
let submap = hyprland.message('submap');
const newLineCarriage = /\n/g;
submap = submap.replace(newLineCarriage, '');
if (submap === 'unknown request') {
submap = 'default';
}
submapStatus.value = submap;
};
================================================
FILE: .config/ags/customModules/submap/index.ts
================================================
const hyprland = await Service.import('hyprland');
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import { Variable as VariableType } from 'types/variable';
import { Attribute, Child } from 'lib/types/widget';
import { BarBoxChild } from 'lib/types/bar';
import { capitalizeFirstLetter } from 'lib/utils';
import { getInitialSubmap, isSubmapEnabled } from './helpers';
const {
label,
showSubmapName,
enabledIcon,
disabledIcon,
enabledText,
disabledText,
leftClick,
rightClick,
middleClick,
scrollUp,
scrollDown,
} = options.bar.customModules.submap;
const submapStatus: VariableType = Variable('default');
hyprland.connect('submap', (_, currentSubmap) => {
if (currentSubmap.length === 0) {
submapStatus.value = 'default';
} else {
submapStatus.value = currentSubmap;
}
});
getInitialSubmap(submapStatus);
export const Submap = (): BarBoxChild => {
const submapModule = module({
textIcon: Utils.merge(
[submapStatus.bind('value'), enabledIcon.bind('value'), disabledIcon.bind('value')],
(status, enabled, disabled) => {
return isSubmapEnabled(status, enabled, disabled);
},
),
tooltipText: Utils.merge(
[
submapStatus.bind('value'),
enabledText.bind('value'),
disabledText.bind('value'),
showSubmapName.bind('value'),
],
(status, enabled, disabled, showSmName) => {
if (showSmName) {
return capitalizeFirstLetter(status);
}
return isSubmapEnabled(status, enabled, disabled);
},
),
boxClass: 'submap',
label: Utils.merge(
[
submapStatus.bind('value'),
enabledText.bind('value'),
disabledText.bind('value'),
showSubmapName.bind('value'),
],
(status, enabled, disabled, showSmName) => {
if (showSmName) {
return capitalizeFirstLetter(status);
}
return isSubmapEnabled(status, enabled, disabled);
},
),
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return submapModule;
};
================================================
FILE: .config/ags/customModules/theme.ts
================================================
import { Option } from 'widget/settings/shared/Option';
import { Header } from 'widget/settings/shared/Header';
import options from 'options';
import Scrollable from 'types/widgets/scrollable';
import { Attribute, GtkWidget } from 'lib/types/widget';
export const CustomModuleTheme = (): Scrollable => {
return Widget.Scrollable({
vscroll: 'automatic',
hscroll: 'automatic',
class_name: 'menu-theme-page customModules paged-container',
child: Widget.Box({
class_name: 'bar-theme-page paged-container',
vertical: true,
children: [
Header('RAM'),
Option({ opt: options.theme.bar.buttons.modules.ram.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.ram.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.ram.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.ram.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.ram.border, title: 'Border', type: 'color' }),
Header('CPU'),
Option({ opt: options.theme.bar.buttons.modules.cpu.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.cpu.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.cpu.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.cpu.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.cpu.border, title: 'Border', type: 'color' }),
Header('Storage'),
Option({ opt: options.theme.bar.buttons.modules.storage.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.storage.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.storage.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.storage.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.storage.border, title: 'Border', type: 'color' }),
Header('Netstat'),
Option({ opt: options.theme.bar.buttons.modules.netstat.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.netstat.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.netstat.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.netstat.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.netstat.border, title: 'Border', type: 'color' }),
Header('Keyboard Layout'),
Option({ opt: options.theme.bar.buttons.modules.kbLayout.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.kbLayout.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.kbLayout.border, title: 'Border', type: 'color' }),
Header('Updates'),
Option({ opt: options.theme.bar.buttons.modules.updates.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.updates.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.updates.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.updates.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.updates.border, title: 'Border', type: 'color' }),
Header('Submap'),
Option({ opt: options.theme.bar.buttons.modules.submap.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.submap.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.submap.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.submap.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.submap.border, title: 'Border', type: 'color' }),
Header('Weather'),
Option({ opt: options.theme.bar.buttons.modules.weather.icon, title: 'Icon', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.weather.text, title: 'Text', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.weather.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.weather.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.weather.border, title: 'Border', type: 'color' }),
Header('Power'),
Option({ opt: options.theme.bar.buttons.modules.power.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.power.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.power.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.power.border, title: 'Border', type: 'color' }),
],
}),
});
};
================================================
FILE: .config/ags/customModules/updates/index.ts
================================================
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import { Variable as VariableType } from 'types/variable';
import { pollVariableBash } from 'customModules/PollVar';
import { Attribute, Child } from 'lib/types/widget';
import { BarBoxChild } from 'lib/types/bar';
const {
updateCommand,
label,
padZero,
pollingInterval,
icon,
leftClick,
rightClick,
middleClick,
scrollUp,
scrollDown,
} = options.bar.customModules.updates;
const pendingUpdates: VariableType = Variable(' 0');
const processUpdateCount = (updateCount: string): string => {
if (!padZero.value) return updateCount;
return `${updateCount.padStart(2, '0')}`;
};
pollVariableBash(
pendingUpdates,
[padZero.bind('value')],
pollingInterval.bind('value'),
updateCommand.value,
processUpdateCount,
);
export const Updates = (): BarBoxChild => {
const updatesModule = module({
textIcon: icon.bind('value'),
tooltipText: pendingUpdates.bind('value').as((v) => `${v} updates available`),
boxClass: 'updates',
label: pendingUpdates.bind('value'),
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return updatesModule;
};
================================================
FILE: .config/ags/customModules/utils.ts
================================================
import { ResourceLabelType } from 'lib/types/bar';
import { GenericResourceData, Postfix } from 'lib/types/customModules/generic';
import { InputHandlerEvents, RunAsyncCommand } from 'lib/types/customModules/utils';
import { ThrottleFn, ThrottleFnCallback } from 'lib/types/utils';
import { Attribute, Child, EventArgs } from 'lib/types/widget';
import { Binding } from 'lib/utils';
import { openMenu } from 'modules/bar/utils';
import options from 'options';
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
import { Variable as VariableType } from 'types/variable';
import Button from 'types/widgets/button';
const { scrollSpeed } = options.bar.customModules;
export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn): void => {
if (cmd.startsWith('menu:')) {
const menuName = cmd.split(':')[1].trim().toLowerCase();
openMenu(events.clicked, events.event, `${menuName}menu`);
return;
}
Utils.execAsync(`bash -c "${cmd}"`)
.then((output) => {
if (fn !== undefined) {
fn(output);
}
})
.catch((err) => console.error(`Error running command "${cmd}": ${err})`));
};
export function throttle(func: T, limit: number): T {
let inThrottle: boolean;
return function (this: ThisParameterType, ...args: Parameters) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
} as T;
}
export const throttledScrollHandler = (interval: number): ThrottleFn =>
throttle((cmd: string, events: EventArgs, fn: ThrottleFnCallback) => {
runAsyncCommand(cmd, events, fn);
}, 200 / interval);
const dummyVar = Variable('');
export const inputHandler = (
self: Button,
{ onPrimaryClick, onSecondaryClick, onMiddleClick, onScrollUp, onScrollDown }: InputHandlerEvents,
): void => {
const sanitizeInput = (input: VariableType): string => {
if (input === undefined) {
return '';
}
return input.value;
};
const updateHandlers = (): void => {
const interval = scrollSpeed.value;
const throttledHandler = throttledScrollHandler(interval);
self.on_primary_click = (clicked: Button, event: Gdk.Event): void =>
runAsyncCommand(sanitizeInput(onPrimaryClick?.cmd || dummyVar), { clicked, event }, onPrimaryClick.fn);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void =>
runAsyncCommand(sanitizeInput(onSecondaryClick?.cmd || dummyVar), { clicked, event }, onSecondaryClick.fn);
self.on_middle_click = (clicked: Button, event: Gdk.Event): void =>
runAsyncCommand(sanitizeInput(onMiddleClick?.cmd || dummyVar), { clicked, event }, onMiddleClick.fn);
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void =>
throttledHandler(sanitizeInput(onScrollUp?.cmd || dummyVar), { clicked, event }, onScrollUp.fn);
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void =>
throttledHandler(sanitizeInput(onScrollDown?.cmd || dummyVar), { clicked, event }, onScrollDown.fn);
};
// Initial setup of event handlers
updateHandlers();
const sanitizeVariable = (someVar: VariableType | undefined): Binding => {
if (someVar === undefined || typeof someVar.bind !== 'function') {
return dummyVar.bind('value');
}
return someVar.bind('value');
};
// Re-run the update whenever scrollSpeed changes
Utils.merge(
[
scrollSpeed.bind('value'),
sanitizeVariable(onPrimaryClick),
sanitizeVariable(onSecondaryClick),
sanitizeVariable(onMiddleClick),
sanitizeVariable(onScrollUp),
sanitizeVariable(onScrollDown),
],
updateHandlers,
);
};
export const divide = ([total, used]: number[], round: boolean): number => {
const percentageTotal = (used / total) * 100;
if (round) {
return total > 0 ? Math.round(percentageTotal) : 0;
}
return total > 0 ? parseFloat(percentageTotal.toFixed(2)) : 0;
};
export const formatSizeInKiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 1;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const formatSizeInMiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 2;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const formatSizeInGiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 3;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const formatSizeInTiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 4;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const autoFormatSize = (sizeInBytes: number, round: boolean): number => {
// auto convert to GiB, MiB, KiB, TiB, or bytes
if (sizeInBytes >= 1024 ** 4) return formatSizeInTiB(sizeInBytes, round);
if (sizeInBytes >= 1024 ** 3) return formatSizeInGiB(sizeInBytes, round);
if (sizeInBytes >= 1024 ** 2) return formatSizeInMiB(sizeInBytes, round);
if (sizeInBytes >= 1024 ** 1) return formatSizeInKiB(sizeInBytes, round);
return sizeInBytes;
};
export const getPostfix = (sizeInBytes: number): Postfix => {
if (sizeInBytes >= 1024 ** 4) return 'TiB';
if (sizeInBytes >= 1024 ** 3) return 'GiB';
if (sizeInBytes >= 1024 ** 2) return 'MiB';
if (sizeInBytes >= 1024 ** 1) return 'KiB';
return 'B';
};
export const renderResourceLabel = (lblType: ResourceLabelType, rmUsg: GenericResourceData, round: boolean): string => {
const { used, total, percentage, free } = rmUsg;
const formatFunctions = {
TiB: formatSizeInTiB,
GiB: formatSizeInGiB,
MiB: formatSizeInMiB,
KiB: formatSizeInKiB,
B: (size: number): number => size,
};
// Get the data in proper GiB, MiB, KiB, TiB, or bytes
const totalSizeFormatted = autoFormatSize(total, round);
// get the postfix: one of [TiB, GiB, MiB, KiB, B]
const postfix = getPostfix(total);
// Determine which format function to use
const formatUsed = formatFunctions[postfix] || formatFunctions['B'];
const usedSizeFormatted = formatUsed(used, round);
if (lblType === 'used/total') {
return `${usedSizeFormatted}/${totalSizeFormatted} ${postfix}`;
}
if (lblType === 'used') {
return `${autoFormatSize(used, round)} ${getPostfix(used)}`;
}
if (lblType === 'free') {
return `${autoFormatSize(free, round)} ${getPostfix(free)}`;
}
return `${percentage}%`;
};
export const formatTooltip = (dataType: string, lblTyp: ResourceLabelType): string => {
switch (lblTyp) {
case 'used':
return `Used ${dataType}`;
case 'free':
return `Free ${dataType}`;
case 'used/total':
return `Used/Total ${dataType}`;
case 'percentage':
return `Percentage ${dataType} Usage`;
default:
return '';
}
};
================================================
FILE: .config/ags/customModules/weather/index.ts
================================================
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import { getWeatherStatusTextIcon, globalWeatherVar } from 'globals/weather';
import { Attribute, Child } from 'lib/types/widget';
import { BarBoxChild } from 'lib/types/bar';
const { label, unit, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.weather;
export const Weather = (): BarBoxChild => {
const weatherModule = module({
textIcon: Utils.merge([globalWeatherVar.bind('value')], (wthr) => {
const weatherStatusIcon = getWeatherStatusTextIcon(wthr);
return weatherStatusIcon;
}),
tooltipText: globalWeatherVar.bind('value').as((v) => `Weather Status: ${v.current.condition.text}`),
boxClass: 'weather-custom',
label: Utils.merge([globalWeatherVar.bind('value'), unit.bind('value')], (wthr, unt) => {
if (unt === 'imperial') {
return `${Math.ceil(wthr.current.temp_f)}° F`;
} else {
return `${Math.ceil(wthr.current.temp_c)}° C`;
}
}),
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return weatherModule;
};
================================================
FILE: .config/ags/directoryMonitorService.ts
================================================
import Service from 'resource:///com/github/Aylur/ags/service.js';
import App from 'resource:///com/github/Aylur/ags/app.js';
import { monitorFile } from 'resource:///com/github/Aylur/ags/utils.js';
import Gio from 'gi://Gio';
import { FileInfo } from 'types/@girs/gio-2.0/gio-2.0.cjs';
class DirectoryMonitorService extends Service {
static {
Service.register(this, {}, {});
}
constructor() {
super();
this.recursiveDirectoryMonitor(`${App.configDir}/scss`);
}
recursiveDirectoryMonitor(directoryPath: string): void {
monitorFile(directoryPath, (_, eventType) => {
if (eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT) {
this.emit('changed');
}
});
const directory = Gio.File.new_for_path(directoryPath);
const enumerator = directory.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
let fileInfo: FileInfo;
while ((fileInfo = enumerator.next_file(null) as FileInfo) !== null) {
const childPath = directoryPath + '/' + fileInfo.get_name();
if (fileInfo.get_file_type() === Gio.FileType.DIRECTORY) {
this.recursiveDirectoryMonitor(childPath);
}
}
}
}
const service = new DirectoryMonitorService();
export default service;
================================================
FILE: .config/ags/flake.nix
================================================
{
description = "A Bar/Panel for Hyprland with extensive customizability.";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
ags.url = "github:Aylur/ags";
};
outputs = inputs: let
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
"aarch64-linux"
];
forEachSystem = inputs.nixpkgs.lib.genAttrs systems;
pkgsFor = forEachSystem (
system:
import inputs.nixpkgs {
inherit system;
config.allowUnfree = true;
}
);
devShellFor = system:
inputs.nixpkgs.lib.genAttrs ["default"] (
_:
inputs.nixpkgs.legacyPackages.${system}.mkShell {
buildInputs = [
pkgsFor.${system}.esbuild
pkgsFor.${system}.fish
pkgsFor.${system}.typescript
pkgsFor.${system}.bun
pkgsFor.${system}.libnotify
pkgsFor.${system}.dart-sass
pkgsFor.${system}.fd
pkgsFor.${system}.btop
pkgsFor.${system}.bluez
pkgsFor.${system}.libgtop
pkgsFor.${system}.gobject-introspection
pkgsFor.${system}.glib
pkgsFor.${system}.bluez-tools
pkgsFor.${system}.grimblast
pkgsFor.${system}.gpu-screen-recorder
pkgsFor.${system}.brightnessctl
pkgsFor.${system}.gnome-bluetooth
pkgsFor.${system}.python3
pkgsFor.${system}.matugen
inputs.ags.packages.${system}.agsWithTypes
];
nativeBuildInputs = with pkgsFor.${system}; [
nixfmt-rfc-style
nil
];
shellHook = ''
export GDK_BACKEND=wayland
export GI_TYPELIB_PATH=${pkgsFor.${system}.libgtop}/lib/girepository-1.0:${pkgsFor.${system}.glib}/lib/girepository-1.0:$GI_TYPELIB_PATH
'';
}
);
in {
devShells = forEachSystem devShellFor;
overlay = final: prev: {
hyprpanel =
if final ? callPackage
then (final.callPackage ./nix {inherit inputs;}).desktop.script
else inputs.self.packages.${prev.stdenv.system}.default;
};
packages = forEachSystem (
system: let
pkgs = pkgsFor.${system};
in {
default = (pkgs.callPackage ./nix {inherit inputs;}).desktop.script;
}
);
};
}
================================================
FILE: .config/ags/globals/dropdown.ts
================================================
import { Variable as VariableType } from 'types/variable';
type GlobalEventBoxes = {
[key: string]: unknown;
};
export const globalEventBoxes: VariableType = Variable({});
================================================
FILE: .config/ags/globals/mousePos.ts
================================================
import { Variable as VariableType } from 'types/variable';
const globalMousePosVar: VariableType = Variable([0, 0]);
globalThis['globalMousePos'] = globalMousePosVar;
================================================
FILE: .config/ags/globals/network.ts
================================================
export const WIFI_STATUS_MAP = {
unknown: 'Status Unknown',
unmanaged: 'Unmanaged',
unavailable: 'Unavailable',
disconnected: 'Disconnected',
prepare: 'Preparing Connecting',
config: 'Connecting',
need_auth: 'Needs Authentication',
ip_config: 'Requesting IP',
ip_check: 'Checking Access',
secondaries: 'Waiting on Secondaries',
activated: 'Connected',
deactivating: 'Disconnecting',
failed: 'Connection Failed',
} as const;
================================================
FILE: .config/ags/globals/notification.ts
================================================
import icons from 'modules/icons/index';
import { Notification } from 'types/service/notifications';
export const removingNotifications = Variable(false);
export const getNotificationIcon = (app_name: string, app_icon: string, app_entry: string): string => {
let icon: string = icons.fallback.notification;
if (Utils.lookUpIcon(app_name) || Utils.lookUpIcon(app_name.toLowerCase() || '')) {
icon = Utils.lookUpIcon(app_name)
? app_name
: Utils.lookUpIcon(app_name.toLowerCase())
? app_name.toLowerCase()
: '';
}
if (Utils.lookUpIcon(app_icon) && icon === '') {
icon = app_icon;
}
if (Utils.lookUpIcon(app_entry || '') && icon === '') {
icon = app_entry || '';
}
return icon;
};
export const closeNotifications = async (notifications: Notification[], delay: number): Promise => {
removingNotifications.value = true;
for (const notif of notifications) {
notif.close();
await new Promise((resolve) => setTimeout(resolve, delay));
}
removingNotifications.value = false;
};
globalThis['removingNotifications'] = removingNotifications;
================================================
FILE: .config/ags/globals/useTheme.ts
================================================
import Gio from 'gi://Gio';
import { bash, Notify } from 'lib/utils';
import icons from 'lib/icons';
import { filterConfigForThemeOnly, loadJsonFile, saveConfigToFile } from 'widget/settings/shared/FileChooser';
export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
globalThis.useTheme = (filePath: string): void => {
const importedConfig = loadJsonFile(filePath);
if (!importedConfig) {
return;
}
Notify({
summary: `Importing Theme`,
body: `Importing: ${filePath}`,
iconName: icons.ui.info,
timeout: 7000,
});
const tmpConfigFile = Gio.File.new_for_path(`${TMP}/config.json`);
const optionsConfigFile = Gio.File.new_for_path(OPTIONS);
const [tmpSuccess, tmpContent] = tmpConfigFile.load_contents(null);
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
if (!tmpSuccess || !optionsSuccess) {
console.error('Failed to read existing configuration files.');
return;
}
let tmpConfig = JSON.parse(new TextDecoder('utf-8').decode(tmpContent));
let optionsConfig = JSON.parse(new TextDecoder('utf-8').decode(optionsContent));
const filteredConfig = filterConfigForThemeOnly(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
saveConfigToFile(tmpConfig, `${TMP}/config.json`);
saveConfigToFile(optionsConfig, OPTIONS);
bash('pkill ags && ags');
};
================================================
FILE: .config/ags/globals/utilities.ts
================================================
globalThis.isWindowVisible = (windowName: string): boolean => {
const appWindow = App.getWindow(windowName);
if (appWindow === undefined) {
return false;
}
return appWindow.visible;
};
================================================
FILE: .config/ags/globals/variables.ts
================================================
import { Opt } from 'lib/option';
import { HexColor, MatugenTheme, RecursiveOptionsObject } from 'lib/types/options';
export const isOpt = (value: unknown): value is Opt =>
typeof value === 'object' && value !== null && 'value' in value && value instanceof Opt;
export const isOptString = (value: unknown): value is Opt => {
return value instanceof Opt && typeof value.value === 'string';
};
export const isOptNumber = (value: unknown): value is Opt => {
return value instanceof Opt && typeof value.value === 'number';
};
export const isOptBoolean = (value: unknown): value is Opt => {
return value instanceof Opt && typeof value.value === 'boolean';
};
export const isOptMatugenTheme = (value: unknown): value is Opt => {
return value instanceof Opt && typeof value.value === 'object' && 'specificProperty' in value.value; // Replace 'specificProperty' with an actual property of MatugenTheme
};
export const isRecursiveOptionsObject = (value: unknown): value is RecursiveOptionsObject => {
return typeof value === 'object' && value !== null && !(value instanceof Opt);
};
export const isHexColor = (val: unknown): val is HexColor => {
return typeof val === 'string' && /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val);
};
================================================
FILE: .config/ags/globals/weather.ts
================================================
import options from 'options';
import { UnitType, Weather, WeatherIconTitle, WeatherIcon } from 'lib/types/weather.js';
import { DEFAULT_WEATHER } from 'lib/types/defaults/weather.js';
import GLib from 'gi://GLib?version=2.0';
import { weatherIcons } from 'modules/icons/weather.js';
const { key, interval, location } = options.menus.clock.weather;
export const globalWeatherVar = Variable(DEFAULT_WEATHER);
let weatherIntervalInstance: null | number = null;
const weatherIntervalFn = (weatherInterval: number, loc: string, weatherKey: string): void => {
if (weatherIntervalInstance !== null) {
GLib.source_remove(weatherIntervalInstance);
}
const formattedLocation = loc.replace(' ', '%20');
weatherIntervalInstance = Utils.interval(weatherInterval, () => {
Utils.execAsync(
`curl "https://api.weatherapi.com/v1/forecast.json?key=${weatherKey}&q=${formattedLocation}&days=1&aqi=no&alerts=no"`,
)
.then((res) => {
try {
if (typeof res !== 'string') {
return (globalWeatherVar.value = DEFAULT_WEATHER);
}
const parsedWeather = JSON.parse(res);
if (Object.keys(parsedWeather).includes('error')) {
return (globalWeatherVar.value = DEFAULT_WEATHER);
}
return (globalWeatherVar.value = parsedWeather);
} catch (error) {
globalWeatherVar.value = DEFAULT_WEATHER;
console.warn(`Failed to parse weather data: ${error}`);
}
})
.catch((err) => {
console.error(`Failed to fetch weather: ${err}`);
globalWeatherVar.value = DEFAULT_WEATHER;
});
});
};
Utils.merge([key.bind('value'), interval.bind('value'), location.bind('value')], (weatherKey, weatherInterval, loc) => {
if (!weatherKey) {
return (globalWeatherVar.value = DEFAULT_WEATHER);
}
weatherIntervalFn(weatherInterval, loc, weatherKey);
});
export const getTemperature = (wthr: Weather, unt: UnitType): string => {
if (unt === 'imperial') {
return `${Math.ceil(wthr.current.temp_f)}° F`;
} else {
return `${Math.ceil(wthr.current.temp_c)}° C`;
}
};
export const getWeatherIcon = (fahren: number): Record => {
const icons = {
100: '',
75: '',
50: '',
25: '',
0: '',
} as const;
const colors = {
100: 'weather-color red',
75: 'weather-color orange',
50: 'weather-color lavender',
25: 'weather-color blue',
0: 'weather-color sky',
} as const;
type IconKeys = keyof typeof icons;
const threshold: IconKeys =
fahren < 0 ? 0 : ([100, 75, 50, 25, 0] as IconKeys[]).find((threshold) => threshold <= fahren) || 0;
const icon = icons[threshold || 50];
const color = colors[threshold || 50];
return {
icon,
color,
};
};
export const getWindConditions = (wthr: Weather, unt: UnitType): string => {
if (unt === 'imperial') {
return `${Math.floor(wthr.current.wind_mph)} mph`;
}
return `${Math.floor(wthr.current.wind_kph)} kph`;
};
export const getRainChance = (wthr: Weather): string => `${wthr.forecast.forecastday[0].day.daily_chance_of_rain}%`;
export const isValidWeatherIconTitle = (title: string): title is WeatherIconTitle => {
return title in weatherIcons;
};
export const getWeatherStatusTextIcon = (wthr: Weather): WeatherIcon => {
let iconQuery = wthr.current.condition.text.trim().toLowerCase().replaceAll(' ', '_');
if (!wthr.current.is_day && iconQuery === 'partly_cloudy') {
iconQuery = 'partly_cloudy_night';
}
if (isValidWeatherIconTitle(iconQuery)) {
return weatherIcons[iconQuery];
} else {
console.warn(`Unknown weather icon title: ${iconQuery}`);
return weatherIcons['warning'];
}
};
globalThis['globalWeatherVar'] = globalWeatherVar;
================================================
FILE: .config/ags/globals/window.ts
================================================
export const WINDOW_LAYOUTS: string[] = [
'center',
'top',
'top-right',
'top-center',
'top-left',
'bottom-left',
'bottom-center',
'bottom-right',
];
================================================
FILE: .config/ags/globals.d.ts
================================================
// globals.d.ts
/* eslint-disable no-var */
import { Options, Variable as VariableType } from 'types/variable';
declare global {
var useTheme: (filePath: string) => void;
var isWindowVisible: (windowName: string) => boolean;
var globalWeatherVar: VariableType;
var options: Options;
var removingNotifications: VariableType;
}
export {};
================================================
FILE: .config/ags/install_fonts.sh
================================================
#!/bin/bash
SOURCE_DIR="./assets/fonts"
DEST_DIR="$HOME/.local/share/fonts"
DEST_PATH="$DEST_DIR/NFP"
if [ ! -d "$SOURCE_DIR" ]; then
echo "Source directory '$SOURCE_DIR' does not exist."
exit 1
fi
if [ ! -d "$DEST_PATH" ]; then
echo "Destination directory '$DEST_PATH' does not exist. Creating it..."
mkdir -p "$DEST_PATH"
fi
if [ -z "$(ls -A "$SOURCE_DIR")" ]; then
echo "Source directory '$SOURCE_DIR' is empty. No files to copy."
exit 1
fi
echo "Copying fonts from '$SOURCE_DIR' to '$DEST_PATH'..."
cp -r "$SOURCE_DIR"/* "$DEST_PATH"
echo "Updating font cache..."
fc-cache -fv
echo "Fonts installed successfully."
================================================
FILE: .config/ags/lib/constants/colors.ts
================================================
export const namedColors = new Set([
'alice blue',
'antique white',
'aqua',
'aquamarine',
'azure',
'beige',
'bisque',
'black',
'blanched almond',
'blue',
'blue violet',
'brown',
'burlywood',
'cadet blue',
'chartreuse',
'chocolate',
'coral',
'cornflower blue',
'cornsilk',
'crimson',
'cyan',
'dark blue',
'dark cyan',
'dark goldenrod',
'dark gray',
'dark green',
'dark khaki',
'dark magenta',
'dark olive green',
'dark orange',
'dark orchid',
'dark red',
'dark salmon',
'dark sea green',
'dark slate blue',
'dark slate gray',
'dark turquoise',
'dark violet',
'deep pink',
'deep sky blue',
'dim gray',
'dodger blue',
'firebrick',
'floral white',
'forest green',
'fuchsia',
'gainsboro',
'ghost white',
'gold',
'goldenrod',
'gray',
'green',
'green yellow',
'honeydew',
'hot pink',
'indian red',
'indigo',
'ivory',
'khaki',
'lavender',
'lavender blush',
'lawn green',
'lemon chiffon',
'light blue',
'light coral',
'light cyan',
'light goldenrod yellow',
'light green',
'light grey',
'light pink',
'light salmon',
'light sea green',
'light sky blue',
'light slate gray',
'light steel blue',
'light yellow',
'lime',
'lime green',
'linen',
'magenta',
'maroon',
'medium aquamarine',
'medium blue',
'medium orchid',
'medium purple',
'medium sea green',
'medium slate blue',
'medium spring green',
'medium turquoise',
'medium violet red',
'midnight blue',
'mint cream',
'misty rose',
'moccasin',
'navajo white',
'navy',
'old lace',
'olive',
'olive drab',
'orange',
'orange red',
'orchid',
'pale goldenrod',
'pale green',
'pale turquoise',
'pale violet red',
'papaya whip',
'peach puff',
'peru',
'pink',
'plum',
'powder blue',
'purple',
'red',
'rosy brown',
'royal blue',
'saddle brown',
'salmon',
'sandy brown',
'sea green',
'seashell',
'sienna',
'silver',
'sky blue',
'slate blue',
'slate gray',
'snow',
'spring green',
'steel blue',
'tan',
'teal',
'thistle',
'tomato',
'turquoise',
'violet',
'wheat',
'white',
'white smoke',
'yellow',
'yellow green',
]);
================================================
FILE: .config/ags/lib/icons.ts
================================================
export const substitutes = {
'transmission-gtk': 'transmission',
'blueberry.py': 'blueberry',
Caprine: 'facebook-messenger',
'com.raggesilver.BlackBox-symbolic': 'terminal-symbolic',
'org.wezfurlong.wezterm-symbolic': 'terminal-symbolic',
'audio-headset-bluetooth': 'audio-headphones-symbolic',
'audio-card-analog-usb': 'audio-speakers-symbolic',
'audio-card-analog-pci': 'audio-card-symbolic',
'preferences-system': 'emblem-system-symbolic',
'com.github.Aylur.ags-symbolic': 'controls-symbolic',
'com.github.Aylur.ags': 'controls-symbolic',
} as const;
export default {
missing: 'image-missing-symbolic',
nix: {
nix: 'nix-snowflake-symbolic',
},
app: {
terminal: 'terminal-symbolic',
},
fallback: {
executable: 'application-x-executable',
notification: 'dialog-information-symbolic',
video: 'video-x-generic-symbolic',
audio: 'audio-x-generic-symbolic',
},
ui: {
close: 'window-close-symbolic',
colorpicker: 'color-select-symbolic',
info: 'info-symbolic',
link: 'external-link-symbolic',
lock: 'system-lock-screen-symbolic',
menu: 'open-menu-symbolic',
refresh: 'view-refresh-symbolic',
search: 'system-search-symbolic',
settings: 'emblem-system-symbolic',
themes: 'preferences-desktop-theme-symbolic',
tick: 'object-select-symbolic',
time: 'hourglass-symbolic',
toolbars: 'toolbars-symbolic',
warning: 'dialog-warning-symbolic',
arrow: {
right: 'pan-end-symbolic',
left: 'pan-start-symbolic',
down: 'pan-down-symbolic',
up: 'pan-up-symbolic',
},
},
audio: {
mic: {
muted: 'microphone-disabled-symbolic',
low: 'microphone-sensitivity-low-symbolic',
medium: 'microphone-sensitivity-medium-symbolic',
high: 'microphone-sensitivity-high-symbolic',
},
volume: {
muted: 'audio-volume-muted-symbolic',
low: 'audio-volume-low-symbolic',
medium: 'audio-volume-medium-symbolic',
high: 'audio-volume-high-symbolic',
overamplified: 'audio-volume-overamplified-symbolic',
},
type: {
headset: 'audio-headphones-symbolic',
speaker: 'audio-speakers-symbolic',
card: 'audio-card-symbolic',
},
mixer: 'mixer-symbolic',
},
powerprofile: {
balanced: 'power-profile-balanced-symbolic',
'power-saver': 'power-profile-power-saver-symbolic',
performance: 'power-profile-performance-symbolic',
},
asusctl: {
profile: {
Balanced: 'power-profile-balanced-symbolic',
Quiet: 'power-profile-power-saver-symbolic',
Performance: 'power-profile-performance-symbolic',
},
mode: {
Integrated: 'processor-symbolic',
Hybrid: 'controller-symbolic',
},
},
battery: {
charging: 'battery-flash-symbolic',
warning: 'battery-empty-symbolic',
},
bluetooth: {
enabled: 'bluetooth-active-symbolic',
disabled: 'bluetooth-disabled-symbolic',
},
brightness: {
indicator: 'display-brightness-symbolic',
keyboard: 'keyboard-brightness-symbolic',
screen: 'display-brightness-symbolic',
},
powermenu: {
sleep: 'weather-clear-night-symbolic',
reboot: 'system-reboot-symbolic',
logout: 'system-log-out-symbolic',
shutdown: 'system-shutdown-symbolic',
},
recorder: {
recording: 'media-record-symbolic',
},
notifications: {
noisy: 'org.gnome.Settings-notifications-symbolic',
silent: 'notifications-disabled-symbolic',
message: 'chat-bubbles-symbolic',
},
trash: {
full: 'user-trash-full-symbolic',
empty: 'user-trash-symbolic',
},
mpris: {
shuffle: {
enabled: 'media-playlist-shuffle-symbolic',
disabled: 'media-playlist-consecutive-symbolic',
},
loop: {
none: 'media-playlist-repeat-symbolic',
track: 'media-playlist-repeat-song-symbolic',
playlist: 'media-playlist-repeat-symbolic',
},
playing: 'media-playback-pause-symbolic',
paused: 'media-playback-start-symbolic',
stopped: 'media-playback-start-symbolic',
prev: 'media-skip-backward-symbolic',
next: 'media-skip-forward-symbolic',
},
system: {
cpu: 'org.gnome.SystemMonitor-symbolic',
ram: 'drive-harddisk-solidstate-symbolic',
temp: 'temperature-symbolic',
},
color: {
dark: 'dark-mode-symbolic',
light: 'light-mode-symbolic',
},
};
================================================
FILE: .config/ags/lib/option.ts
================================================
import { isHexColor } from 'globals/variables';
import { Variable } from 'resource:///com/github/Aylur/ags/variable.js';
import { MkOptionsResult } from './types/options';
type OptProps = {
persistent?: boolean;
};
export class Opt extends Variable {
static {
Service.register(this);
}
constructor(initial: T, { persistent = false }: OptProps = {}) {
super(initial);
this.initial = initial;
this.persistent = persistent;
}
initial: T;
id = '';
persistent: boolean;
toString(): string {
return `${this.value}`;
}
toJSON(): string {
return `opt:${this.value}`;
}
getValue = (): T => {
return super.getValue();
};
init(cacheFile: string): void {
const cacheV = JSON.parse(Utils.readFile(cacheFile) || '{}')[this.id];
if (cacheV !== undefined) this.value = cacheV;
this.connect('changed', () => {
const cache = JSON.parse(Utils.readFile(cacheFile) || '{}');
cache[this.id] = this.value;
Utils.writeFileSync(JSON.stringify(cache, null, 2), cacheFile);
});
}
reset(): string | undefined {
if (this.persistent) return;
if (JSON.stringify(this.value) !== JSON.stringify(this.initial)) {
this.value = this.initial;
return this.id;
}
}
doResetColor(): string | undefined {
if (this.persistent) return;
const isColor = isHexColor(this.value as string);
if (JSON.stringify(this.value) !== JSON.stringify(this.initial) && isColor) {
this.value = this.initial;
return this.id;
}
return;
}
}
export const opt = (initial: T, opts?: OptProps): Opt => new Opt(initial, opts);
const getOptions = (object: Record, path = ''): Opt[] => {
return Object.keys(object).flatMap((key) => {
const obj = object[key];
const id = path ? path + '.' + key : key;
if (obj instanceof Variable) {
const optValue = obj as Opt;
optValue.id = id;
return optValue;
}
if (typeof obj === 'object' && obj !== null) {
return getOptions(obj as Record, id); // Recursively process nested objects
}
return [];
});
};
export function mkOptions(
cacheFile: string,
object: T,
confFile: string = 'config.json',
): T & MkOptionsResult {
for (const opt of getOptions(object as Record)) opt.init(cacheFile);
Utils.ensureDirectory(cacheFile.split('/').slice(0, -1).join('/'));
const configFile = `${TMP}/${confFile}`;
const values = getOptions(object as Record).reduce(
(obj, { id, value }) => ({ [id]: value, ...obj }),
{},
);
Utils.writeFileSync(JSON.stringify(values, null, 2), configFile);
Utils.monitorFile(configFile, () => {
const cache = JSON.parse(Utils.readFile(configFile) || '{}');
for (const opt of getOptions(object as Record)) {
if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) opt.value = cache[opt.id];
}
});
function sleep(ms = 0): Promise {
return new Promise((r) => setTimeout(r, ms));
}
const reset = async (
[opt, ...list] = getOptions(object as Record),
id = opt?.reset(),
): Promise> => {
if (!opt) return sleep().then(() => []);
return id ? [id, ...(await sleep(50).then(() => reset(list)))] : await sleep().then(() => reset(list));
};
const resetTheme = async (
[opt, ...list] = getOptions(object as Record),
id = opt?.doResetColor(),
): Promise> => {
if (!opt) return sleep().then(() => []);
return id
? [id, ...(await sleep(50).then(() => resetTheme(list)))]
: await sleep().then(() => resetTheme(list));
};
return Object.assign(object, {
configFile,
array: () => getOptions(object as Record),
async reset() {
return (await reset()).join('\n');
},
async resetTheme() {
return (await resetTheme()).join('\n');
},
handler(deps: string[], callback: () => void) {
for (const opt of getOptions(object as Record)) {
if (deps.some((i) => opt.id.startsWith(i))) opt.connect('changed', callback);
}
},
});
}
================================================
FILE: .config/ags/lib/session.ts
================================================
import GLib from 'gi://GLib?version=2.0';
declare global {
const OPTIONS: string;
const TMP: string;
const USER: string;
}
Object.assign(globalThis, {
OPTIONS: `${GLib.get_user_cache_dir()}/ags/hyprpanel/options.json`,
TMP: `${GLib.get_tmp_dir()}/ags/hyprpanel`,
USER: GLib.get_user_name(),
});
Utils.ensureDirectory(TMP);
App.addIcons(`${App.configDir}/assets`);
================================================
FILE: .config/ags/lib/shared/media.ts
================================================
import { MprisPlayer } from 'types/service/mpris';
const mpris = await Service.import('mpris');
export const getCurrentPlayer = (activePlayer: MprisPlayer = mpris.players[0]): MprisPlayer => {
const statusOrder = {
Playing: 1,
Paused: 2,
Stopped: 3,
};
if (mpris.players.length === 0) {
return mpris.players[0];
}
const isPlaying = mpris.players.some((p: MprisPlayer) => p.play_back_status === 'Playing');
const playerStillExists = mpris.players.some((p) => activePlayer.bus_name === p.bus_name);
const nextPlayerUp = mpris.players.sort(
(a: MprisPlayer, b: MprisPlayer) => statusOrder[a.play_back_status] - statusOrder[b.play_back_status],
)[0];
if (isPlaying || !playerStillExists) {
return nextPlayerUp;
}
return activePlayer;
};
================================================
FILE: .config/ags/lib/shared/notifications.ts
================================================
import { Notification } from 'types/service/notifications';
export const filterNotifications = (notifications: Notification[], filter: string[]): Notification[] => {
const notifFilter = new Set(filter.map((name: string) => name.toLowerCase().replace(/\s+/g, '_')));
const filteredNotifications = notifications.filter((notif: Notification) => {
const normalizedAppName = notif.app_name.toLowerCase().replace(/\s+/g, '_');
return !notifFilter.has(normalizedAppName);
});
return filteredNotifications;
};
================================================
FILE: .config/ags/lib/types/audio.d.ts
================================================
export type InputDevices = Button, Attribute>, Attribute>, Attribute>[];
type DummyDevices = Button, Attribute>, Attribute>, Attribute>[];
type RealPlaybackDevices = Button, Attribute>, Attribute>, Attribute>[];
export type PlaybackDevices = DummyDevices | RealPlaybackDevices;
================================================
FILE: .config/ags/lib/types/bar.d.ts
================================================
import { Binding, Connectable } from 'types/service';
import { Variable } from 'types/variable';
import Box from 'types/widgets/box';
import Button, { ButtonProps } from 'types/widgets/button';
import Label from 'types/widgets/label';
import { Attribute, Child } from './widget';
export type BarBoxChild = {
component: Box;
isVisible?: boolean;
isVis?: Variable;
boxClass: string;
tooltip_text?: string | Binding;
props: ButtonProps;
};
export type SelfButton = Button;
export type BoxHook = (self: Box) => void;
export type LabelHook = (self: Label) => void;
export type Module = {
icon?: string | Binding;
textIcon?: string | Binding;
label?: string | Binding;
labelHook?: LabelHook;
boundLabel?: string;
tooltipText?: string | Binding;
boxClass: string;
props?: ButtonProps;
showLabel?: boolean;
showLabelBinding?: Binding;
hook?: BoxHook;
connection?: Binding;
};
export type ResourceLabelType = 'used/total' | 'used' | 'percentage' | 'free';
export type NetstatLabelType = 'full' | 'in' | 'out';
export type RateUnit = 'GiB' | 'MiB' | 'KiB' | 'auto';
================================================
FILE: .config/ags/lib/types/customModules/generic.d.ts
================================================
export type GenericFunction = (...args: P) => T;
export type GenericResourceMetrics = {
total: number;
used: number;
percentage: number;
};
export type GenericResourceData = GenericResourceMetrics & {
free: number;
};
export type Postfix = 'TiB' | 'GiB' | 'MiB' | 'KiB' | 'B';
================================================
FILE: .config/ags/lib/types/customModules/kbLayout.d.ts
================================================
import { layoutMap } from 'customModules/kblayout/layouts';
export type KbLabelType = 'layout' | 'code';
export type HyprctlKeyboard = {
address: string;
name: string;
rules: string;
model: string;
layout: string;
variant: string;
options: string;
active_keymap: string;
main: boolean;
};
export type HyprctlMouse = {
address: string;
name: string;
defaultSpeed: number;
};
export type HyprctlDeviceLayout = {
mice: HyprctlMouse[];
keyboards: HyprctlKeyboard[];
tablets: unknown[];
touch: unknown[];
switches: unknown[];
};
export type LayoutKeys = keyof typeof layoutMap;
export type LayoutValues = (typeof layoutMap)[LayoutKeys];
================================================
FILE: .config/ags/lib/types/customModules/network.d.ts
================================================
export type NetworkResourceData = {
in: string;
out: string;
};
================================================
FILE: .config/ags/lib/types/customModules/utils.d.ts
================================================
import { Binding } from 'lib/utils';
export type InputHandlerEvents = {
onPrimaryClick?: Binding;
onSecondaryClick?: Binding;
onMiddleClick?: Binding;
onScrollUp?: Binding;
onScrollDown?: Binding;
};
export type RunAsyncCommand = (cmd: string, args: EventArgs, fn?: (output: string) => void) => void;
================================================
FILE: .config/ags/lib/types/defaults/bar.ts
================================================
import { NetstatLabelType, ResourceLabelType } from '../bar';
export const LABEL_TYPES: ResourceLabelType[] = ['used/total', 'used', 'free', 'percentage'];
export const NETWORK_LABEL_TYPES: NetstatLabelType[] = ['full', 'in', 'out'];
================================================
FILE: .config/ags/lib/types/defaults/netstat.ts
================================================
import { RateUnit } from '../bar';
import { NetworkResourceData } from '../customModules/network';
export const GET_DEFAULT_NETSTAT_DATA = (dataType: RateUnit): NetworkResourceData => {
if (dataType === 'auto') {
return { in: `0 Kib/s`, out: `0 Kib/s` };
}
return { in: `0 ${dataType}/s`, out: `0 ${dataType}/s` };
};
================================================
FILE: .config/ags/lib/types/defaults/options.ts
================================================
export const defaultColorMap = {
rosewater: '#f5e0dc',
flamingo: '#f2cdcd',
pink: '#f5c2e7',
mauve: '#cba6f7',
red: '#f38ba8',
maroon: '#eba0ac',
peach: '#fab387',
yellow: '#f9e2af',
green: '#a6e3a1',
teal: '#94e2d5',
sky: '#89dceb',
sapphire: '#74c7ec',
blue: '#89b4fa',
lavender: '#b4befe',
text: '#cdd6f4',
subtext1: '#bac2de',
subtext2: '#a6adc8',
overlay2: '#9399b2',
overlay1: '#7f849c',
overlay0: '#6c7086',
surface2: '#585b70',
surface1: '#45475a',
surface0: '#313244',
base2: '#242438',
base: '#1e1e2e',
mantle: '#181825',
crust: '#11111b',
surface1_2: '#454759',
text2: '#cdd6f3',
pink2: '#f5c2e6',
red2: '#f38ba7',
peach2: '#fab386',
mantle2: '#181824',
surface0_2: '#313243',
surface2_2: '#585b69',
overlay1_2: '#7f849b',
lavender2: '#b4befd',
mauve2: '#cba6f6',
green2: '#a6e3a0',
sky2: '#89dcea',
teal2: '#94e2d4',
yellow2: '#f9e2ad',
maroon2: '#eba0ab',
crust2: '#11111a',
pink3: '#f5c2e8',
red3: '#f38ba9',
mantle3: '#181826',
surface0_3: '#313245',
surface2_3: '#585b71',
overlay1_3: '#7f849d',
lavender3: '#b4beff',
mauve3: '#cba6f8',
green3: '#a6e3a2',
sky3: '#89dcec',
teal3: '#94e2d6',
yellow3: '#f9e2ae',
maroon3: '#eba0ad',
crust3: '#11111c',
} as const;
================================================
FILE: .config/ags/lib/types/defaults/weather.ts
================================================
export const DEFAULT_WEATHER = {
location: {
name: 'Tahiti',
region: 'Somewhere',
country: 'United States of America',
lat: 0,
lon: 0,
tz_id: 'Tahiti',
localtime_epoch: 1721981457,
localtime: '2024-07-26 1:10',
},
current: {
last_updated_epoch: 1721980800,
last_updated: '2024-07-26 01:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'NW',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0.0,
precip_in: 0.0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
vis_km: 0,
vis_miles: 0,
uv: 0,
gust_mph: 0,
gust_kph: 0,
},
forecast: {
forecastday: [
{
date: '2024-07-26',
date_epoch: 1721952000,
day: {
maxtemp_c: 0,
maxtemp_f: 0,
mintemp_c: 0,
mintemp_f: 0,
avgtemp_c: 0,
avgtemp_f: 0,
maxwind_mph: 0,
maxwind_kph: 0,
totalprecip_mm: 0,
totalprecip_in: 0,
totalsnow_cm: 0,
avgvis_km: 0,
avgvis_miles: 0,
avghumidity: 0,
daily_will_it_rain: 0,
daily_chance_of_rain: 0,
daily_will_it_snow: 0,
daily_chance_of_snow: 0,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
uv: 0,
},
astro: {
sunrise: '06:01 AM',
sunset: '08:10 PM',
moonrise: '11:32 PM',
moonset: '12:01 PM',
moon_phase: 'Waning Gibbous',
moon_illumination: 0,
is_moon_up: 0,
is_sun_up: 0,
},
hour: [
{
time_epoch: 1721977200,
time: '2024-07-26 00:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1721980800,
time: '2024-07-26 01:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1721984400,
time: '2024-07-26 02:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1721988000,
time: '2024-07-26 03:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1721991600,
time: '2024-07-26 04:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1721995200,
time: '2024-07-26 05:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1721998800,
time: '2024-07-26 06:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1722002400,
time: '2024-07-26 07:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 7.0,
},
{
time_epoch: 1722006000,
time: '2024-07-26 08:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 7.0,
},
{
time_epoch: 1722009600,
time: '2024-07-26 09:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 8.0,
},
{
time_epoch: 1722013200,
time: '2024-07-26 10:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 8.0,
},
{
time_epoch: 1722016800,
time: '2024-07-26 11:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 9.0,
},
{
time_epoch: 1722020400,
time: '2024-07-26 12:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 9.0,
},
{
time_epoch: 1722024000,
time: '2024-07-26 13:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 9.0,
},
{
time_epoch: 1722027600,
time: '2024-07-26 14:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 10.0,
},
{
time_epoch: 1722031200,
time: '2024-07-26 15:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 10.0,
},
{
time_epoch: 1722034800,
time: '2024-07-26 16:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 10.0,
},
{
time_epoch: 1722038400,
time: '2024-07-26 17:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 10.0,
},
{
time_epoch: 1722042000,
time: '2024-07-26 18:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 9.0,
},
{
time_epoch: 1722045600,
time: '2024-07-26 19:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 9.0,
},
{
time_epoch: 1722049200,
time: '2024-07-26 20:00',
temp_c: 0,
temp_f: 0,
is_day: 1,
condition: {
text: 'Sunny',
icon: '//cdn.weatherapi.com/weather/64x64/day/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 8.0,
},
{
time_epoch: 1722052800,
time: '2024-07-26 21:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1722056400,
time: '2024-07-26 22:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
{
time_epoch: 1722060000,
time: '2024-07-26 23:00',
temp_c: 0,
temp_f: 0,
is_day: 0,
condition: {
text: 'Clear ',
icon: '//cdn.weatherapi.com/weather/64x64/night/113.png',
code: 1000,
},
wind_mph: 0,
wind_kph: 0,
wind_degree: 0,
wind_dir: 'N',
pressure_mb: 0,
pressure_in: 0,
precip_mm: 0,
precip_in: 0,
snow_cm: 0,
humidity: 0,
cloud: 0,
feelslike_c: 0,
feelslike_f: 0,
windchill_c: 0,
windchill_f: 0,
heatindex_c: 0,
heatindex_f: 0,
dewpoint_c: 0,
dewpoint_f: 0,
will_it_rain: 0,
chance_of_rain: 0,
will_it_snow: 0,
chance_of_snow: 0,
vis_km: 0,
vis_miles: 0,
gust_mph: 0,
gust_kph: 0,
uv: 0,
},
],
},
],
},
};
================================================
FILE: .config/ags/lib/types/dropdownmenu.d.ts
================================================
import { WindowProps } from 'types/widgets/window';
import { GtkWidget, Transition } from './widget';
import { Binding } from 'types/service';
export type DropdownMenuProps = {
name: string;
child: GtkWidget;
layout?: string;
transition?: Transition | Binding;
exclusivity?: Exclusivity;
fixed?: boolean;
} & WindowProps;
================================================
FILE: .config/ags/lib/types/filechooser.d.ts
================================================
export type Config = {
[key: string]: string | number | boolean | object;
};
================================================
FILE: .config/ags/lib/types/globals.d.ts
================================================
export type MousePos = {
source: string;
pos: number[];
};
================================================
FILE: .config/ags/lib/types/gpustat.d.ts
================================================
export type GPU_Stat_Process = {
username: string;
command: string;
full_command: string[];
gpu_memory_usage: number;
cpu_percent: number;
cpu_memory_usage: number;
pid: number;
};
export type GPU_Stat = {
index: number;
uuid: string;
name: string;
'temperature.gpu': number;
'fan.speed': number;
'utilization.gpu': number;
'utilization.enc': number;
'utilization.dec': number;
'power.draw': number;
'enforced.power.limit': number;
'memory.used': number;
'memory.total': number;
processes: Process[];
};
================================================
FILE: .config/ags/lib/types/mpris.d.ts
================================================
export type LoopStatus = 'none' | 'track' | 'playlist';
export type PlaybackStatus = 'playing' | 'paused' | 'stopped';
================================================
FILE: .config/ags/lib/types/network.d.ts
================================================
import { WIFI_STATUS_MAP } from 'globals/network';
export type AccessPoint = {
bssid: string | null;
address: string | null;
lastSeen: number;
ssid: string | null;
active: boolean;
strength: number;
frequency: number;
iconName: string | undefined;
};
export type WifiStatus = keyof typeof WIFI_STATUS_MAP;
export type WifiIcon = '' | '' | '' | '' | '' | '' | '' | '' | '' | '' | '';
================================================
FILE: .config/ags/lib/types/notification.d.ts
================================================
import icons from 'modules/icons/index';
export interface NotificationArgs {
appName?: string;
body?: string;
iconName?: string;
id?: number;
summary?: string;
urgency?: Urgency;
category?: string;
timeout?: number;
transient?: boolean;
}
export type NotificationIcon = keyof typeof icons.notifications;
================================================
FILE: .config/ags/lib/types/options.d.ts
================================================
import { Opt } from 'lib/option';
import { Variable } from 'types/variable';
import { defaultColorMap } from './defaults/options';
export type MkOptionsResult = {
configFile: string;
array: () => Opt[];
reset: () => Promise;
resetTheme: () => Promise;
handler: (deps: string[], callback: () => void) => void;
};
export type RecursiveOptionsObject = {
[key: string]: RecursiveOptionsObject | Opt | Opt | Opt;
};
export type BarLocation = 'top' | 'bottom';
export type Unit = 'imperial' | 'metric';
export type PowerOptions = 'sleep' | 'reboot' | 'logout' | 'shutdown';
export type NotificationAnchor =
| 'top'
| 'top right'
| 'top left'
| 'bottom'
| 'bottom right'
| 'bottom left'
| 'left'
| 'right';
export type OSDAnchor = 'top left' | 'top' | 'top right' | 'right' | 'bottom right' | 'bottom' | 'bottom left' | 'left';
export type BarButtonStyles = 'default' | 'split' | 'wave' | 'wave2';
export type ThemeExportData = {
filePath: string;
themeOnly: boolean;
};
export type RowProps = {
opt: Opt;
title: string;
note?: string;
type?:
| 'number'
| 'color'
| 'float'
| 'object'
| 'string'
| 'enum'
| 'boolean'
| 'img'
| 'wallpaper'
| 'export'
| 'import'
| 'config_import'
| 'font';
enums?: T[];
max?: number;
min?: number;
disabledBinding?: Variable;
exportData?: ThemeExportData;
subtitle?: string | VarType | Opt;
subtitleLink?: string;
dependencies?: string[];
increment?: number;
};
export type OSDOrientation = 'horizontal' | 'vertical';
export type HexColor = `#${string}`;
export type WindowLayer = 'top' | 'bottom' | 'overlay' | 'background';
export type ActiveWsIndicator = 'underline' | 'highlight' | 'color';
export type MatugenColors = {
background: HexColor;
error: HexColor;
error_container: HexColor;
inverse_on_surface: HexColor;
inverse_primary: HexColor;
inverse_surface: HexColor;
on_background: HexColor;
on_error: HexColor;
on_error_container: HexColor;
on_primary: HexColor;
on_primary_container: HexColor;
on_primary_fixed: HexColor;
on_primary_fixed_variant: HexColor;
on_secondary: HexColor;
on_secondary_container: HexColor;
on_secondary_fixed: HexColor;
on_secondary_fixed_variant: HexColor;
on_surface: HexColor;
on_surface_variant: HexColor;
on_tertiary: HexColor;
on_tertiary_container: HexColor;
on_tertiary_fixed: HexColor;
on_tertiary_fixed_variant: HexColor;
outline: HexColor;
outline_variant: HexColor;
primary: HexColor;
primary_container: HexColor;
primary_fixed: HexColor;
primary_fixed_dim: HexColor;
scrim: HexColor;
secondary: HexColor;
secondary_container: HexColor;
secondary_fixed: HexColor;
secondary_fixed_dim: HexColor;
shadow: HexColor;
surface: HexColor;
surface_bright: HexColor;
surface_container: HexColor;
surface_container_high: HexColor;
surface_container_highest: HexColor;
surface_container_low: HexColor;
surface_container_lowest: HexColor;
surface_dim: HexColor;
surface_variant: HexColor;
tertiary: HexColor;
tertiary_container: HexColor;
tertiary_fixed: HexColor;
tertiary_fixed_dim: HexColor;
};
export type MatugenVariation = {
rosewater: HexColor;
flamingo: HexColor;
pink: HexColor;
mauve: HexColor;
red: HexColor;
maroon: HexColor;
peach: HexColor;
yellow: HexColor;
green: HexColor;
teal: HexColor;
sky: HexColor;
sapphire: HexColor;
blue: HexColor;
lavender: HexColor;
text: HexColor;
subtext1: HexColor;
subtext2: HexColor;
overlay2: HexColor;
overlay1: HexColor;
overlay0: HexColor;
surface2: HexColor;
surface1: HexColor;
surface0: HexColor;
base2: HexColor;
base: HexColor;
mantle: HexColor;
crust: HexColor;
notifications_closer: HexColor;
notifications_background: HexColor;
dashboard_btn_text: HexColor;
red2: HexColor;
peach2: HexColor;
pink2: HexColor;
mantle2: HexColor;
surface1_2: HexColor;
surface0_2: HexColor;
overlay1_2: HexColor;
text2: HexColor;
lavender2: HexColor;
crust2: HexColor;
maroon2: HexColor;
mauve2: HexColor;
green2: HexColor;
surface2_2: HexColor;
sky2: HexColor;
teal2: HexColor;
yellow2: HexColor;
pink3: HexColor;
red3: HexColor;
mantle3: HexColor;
surface0_3: HexColor;
surface2_3: HexColor;
overlay1_3: HexColor;
lavender3: HexColor;
mauve3: HexColor;
green3: HexColor;
sky3: HexColor;
teal3: HexColor;
yellow3: HexColor;
maroon3: HexColor;
crust3: HexColor;
notifications_closer?: HexColor;
notifications_background?: HexColor;
dashboard_btn_text?: HexColor;
};
export type MatugenScheme =
| 'content'
| 'expressive'
| 'fidelity'
| 'fruit-salad'
| 'monochrome'
| 'neutral'
| 'rainbow'
| 'tonal-spot';
export type MatugenVariations =
| 'standard_1'
| 'standard_2'
| 'standard_3'
| 'monochrome_1'
| 'monochrome_2'
| 'monochrome_3'
| 'vivid_1'
| 'vivid_2'
| 'vivid_3';
type MatugenTheme = 'light' | 'dark';
export type ColorMapKey = keyof typeof defaultColorMap;
export type ColorMapValue = (typeof defaultColorMap)[ColorMapKey];
export type ScalingPriority = 'gdk' | 'hyprland' | 'both';
================================================
FILE: .config/ags/lib/types/popupwindow.d.ts
================================================
import { Widget } from 'types/widgets/widget';
import { WindowProps } from 'types/widgets/window';
import { Transition } from './widget';
export type PopupWindowProps = {
name: string;
child: Widget;
layout?: Layouts;
transition?: Transition | Binding;
exclusivity?: Exclusivity;
} & WindowProps;
export type LayoutFunction = (
name: string,
child: Widget,
transition: Transition,
) => {
center: () => Widget;
top: () => Widget;
'top-right': () => Widget;
'top-center': () => Widget;
'top-left': () => Widget;
'bottom-left': () => Widget;
'bottom-center': () => Widget;
'bottom-right': () => Widget;
};
export type Layouts =
| 'center'
| 'top'
| 'top-right'
| 'top-center'
| 'top-left'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
================================================
FILE: .config/ags/lib/types/power.d.ts
================================================
export type Action = 'sleep' | 'reboot' | 'logout' | 'shutdown';
================================================
FILE: .config/ags/lib/types/powerprofiles.d.ts
================================================
import PowerProfiles from 'types/service/powerprofiles.js';
export type PowerProfiles = InstanceType;
export type PowerProfile = 'power-saver' | 'balanced' | 'performance';
export type PowerProfileObject = {
[key: string]: string;
};
================================================
FILE: .config/ags/lib/types/systray.d.ts
================================================
================================================
FILE: .config/ags/lib/types/utils.d.ts
================================================
import { substitutes } from 'lib/icons';
import { EventArgs } from './widget';
type SubstituteKeys = keyof typeof substitutes;
export type ThrottleFn = (cmd: string, args: EventArgs, fn?: (output: string) => void) => void;
export type ThrottleFnCallback = ((output: string) => void) | undefined;
================================================
FILE: .config/ags/lib/types/variable.d.ts
================================================
export type Bind = OriginalBinding, unknown>;
================================================
FILE: .config/ags/lib/types/volume.d.ts
================================================
export type VolumeIcons = {
[index: number]: string;
};
================================================
FILE: .config/ags/lib/types/weather.d.ts
================================================
import { weatherIcons } from 'modules/icons/weather';
export type UnitType = 'imperial' | 'metric';
export type Weather = {
location: Location;
current: Current;
forecast: Forecast;
};
export type Current = {
last_updated_epoch?: number;
last_updated?: string;
temp_c: number;
temp_f: number;
is_day: number;
condition: Condition;
wind_mph: number;
wind_kph: number;
wind_degree: number;
wind_dir: string;
pressure_mb: number;
pressure_in: number;
precip_mm: number;
precip_in: number;
humidity: number;
cloud: number;
feelslike_c: number;
feelslike_f: number;
windchill_c: number;
windchill_f: number;
heatindex_c: number;
heatindex_f: number;
dewpoint_c: number;
dewpoint_f: number;
vis_km: number;
vis_miles: number;
uv: number;
gust_mph: number;
gust_kph: number;
time_epoch?: number;
time?: string;
snow_cm?: number;
will_it_rain?: number;
chance_of_rain?: number;
will_it_snow?: number;
chance_of_snow?: number;
};
export type Condition = {
text: string;
icon: string;
code: number;
};
export type Forecast = {
forecastday: Forecastday[];
};
export type Forecastday = {
date: string;
date_epoch: number;
day: Day;
astro: Astro;
hour: Current[];
};
export type Astro = {
sunrise: string;
sunset: string;
moonrise: string;
moonset: string;
moon_phase: string;
moon_illumination: number;
is_moon_up: number;
is_sun_up: number;
};
export type Day = {
maxtemp_c: number;
maxtemp_f: number;
mintemp_c: number;
mintemp_f: number;
avgtemp_c: number;
avgtemp_f: number;
maxwind_mph: number;
maxwind_kph: number;
totalprecip_mm: number;
totalprecip_in: number;
totalsnow_cm: number;
avgvis_km: number;
avgvis_miles: number;
avghumidity: number;
daily_will_it_rain: number;
daily_chance_of_rain: number;
daily_will_it_snow: number;
daily_chance_of_snow: number;
condition: Condition;
uv: number;
};
export type Location = {
name: string;
region: string;
country: string;
lat: number;
lon: number;
tz_id: string;
localtime_epoch: number;
localtime: string;
};
export type TemperatureIconColorMap = {
[key: number]: string;
};
export type WeatherIconTitle = keyof typeof weatherIcons;
export type WeatherIcon = (typeof weatherIcons)[WeatherIconTitle];
================================================
FILE: .config/ags/lib/types/widget.d.ts
================================================
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0';
import Box from 'types/widgets/box';
export type Exclusivity = 'normal' | 'ignore' | 'exclusive';
export type Anchor = 'left' | 'right' | 'top' | 'down';
export type Transition = 'none' | 'crossfade' | 'slide_right' | 'slide_left' | 'slide_up' | 'slide_down';
export type Layouts =
| 'center'
| 'top'
| 'top-right'
| 'top-center'
| 'top-left'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
export type Attribute = unknown;
export type Child = Gtk.Widget;
export type GtkWidget = Gtk.Widget;
export type BoxWidget = Box;
export type GButton = Gtk.Button;
export type GBox = Gtk.Box;
export type GLabel = Gtk.Label;
export type GCenterBox = Gtk.Box;
export type EventHandler = (self: Self, event: Gdk.Event) => boolean | unknown;
export type EventArgs = { clicked: Button; event: Gdk.Event };
================================================
FILE: .config/ags/lib/types/workspace.d.ts
================================================
export type WorkspaceRule = {
workspaceString: string;
monitor: string;
};
export type WorkspaceMap = {
[key: string]: number[];
};
export type MonitorMap = {
[key: number]: string;
};
export type WorkspaceIcons = {
[key: string]: string;
};
export type WorkspaceIconsColored = {
[key: string]: {
color: string;
icon: string;
};
};
export type WorkspaceIconMap = WorkspaceIcons | WorkspaceIconsColored;
================================================
FILE: .config/ags/lib/utils.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Application } from 'types/service/applications';
import { NotificationAnchor } from './types/options';
import { OSDAnchor } from 'lib/types/options';
import icons, { substitutes } from './icons';
import Gtk from 'gi://Gtk?version=3.0';
import Gdk from 'gi://Gdk';
import GLib from 'gi://GLib?version=2.0';
import GdkPixbuf from 'gi://GdkPixbuf';
import { NotificationArgs } from 'types/utils/notify';
import { SubstituteKeys } from './types/utils';
import { Window } from 'types/@girs/gtk-3.0/gtk-3.0.cjs';
import { namedColors } from './constants/colors';
export type Binding = import('types/service').Binding;
/**
* @returns substitute icon || name || fallback icon
*/
export function icon(name: string | null, fallback = icons.missing): string {
const validateSubstitute = (name: string): name is SubstituteKeys => name in substitutes;
if (!name) return fallback || '';
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
let icon: string = name;
if (validateSubstitute(name)) {
icon = substitutes[name];
}
if (Utils.lookUpIcon(icon)) return icon;
print(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`);
return fallback;
}
/**
* @returns execAsync(["bash", "-c", cmd])
*/
export async function bash(strings: TemplateStringsArray | string, ...values: unknown[]): Promise {
const cmd =
typeof strings === 'string' ? strings : strings.flatMap((str, i) => str + `${values[i] ?? ''}`).join('');
return Utils.execAsync(['bash', '-c', cmd]).catch((err) => {
console.error(cmd, err);
return '';
});
}
/**
* @returns execAsync(cmd)
*/
export async function sh(cmd: string | string[]): Promise {
return Utils.execAsync(cmd).catch((err) => {
console.error(typeof cmd === 'string' ? cmd : cmd.join(' '), err);
return '';
});
}
export function forMonitors(widget: (monitor: number) => Gtk.Window): Window[] {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
return range(n, 0).flatMap(widget);
}
/**
* @returns [start...length]
*/
export function range(length: number, start = 1): number[] {
return Array.from({ length }, (_, i) => i + start);
}
/**
* @returns true if all of the `bins` are found
*/
export function dependencies(...bins: string[]): boolean {
const missing = bins.filter((bin) =>
Utils.exec({
cmd: `which ${bin}`,
out: () => false,
err: () => true,
}),
);
if (missing.length > 0) {
console.warn(Error(`missing dependencies: ${missing.join(', ')}`));
Notify({
summary: 'Dependencies not found!',
body: `The following dependencies are missing: ${missing.join(', ')}`,
iconName: icons.ui.warning,
timeout: 7000,
});
}
return missing.length === 0;
}
/**
* run app detached
*/
export function launchApp(app: Application): void {
const exe = app.executable
.split(/\s+/)
.filter((str) => !str.startsWith('%') && !str.startsWith('@'))
.join(' ');
bash(`${exe} &`);
app.frequency += 1;
}
/**
* to use with drag and drop
*/
export function createSurfaceFromWidget(widget: Gtk.Widget): GdkPixbuf.Pixbuf {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cairo = imports.gi.cairo as any;
const alloc = widget.get_allocation();
const surface = new cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height);
const cr = new cairo.Context(surface);
cr.setSourceRGBA(255, 255, 255, 0);
cr.rectangle(0, 0, alloc.width, alloc.height);
cr.fill();
widget.draw(cr);
return surface;
}
/**
* Ensure that the provided filepath is a valid image
*/
export const isAnImage = (imgFilePath: string): boolean => {
try {
GdkPixbuf.Pixbuf.new_from_file(imgFilePath);
return true;
} catch (error) {
console.error(error);
return false;
}
};
export const Notify = (notifPayload: NotificationArgs): void => {
let command = 'notify-send';
command += ` "${notifPayload.summary} "`;
if (notifPayload.body) command += ` "${notifPayload.body}" `;
if (notifPayload.appName) command += ` -a "${notifPayload.appName}"`;
if (notifPayload.iconName) command += ` -i "${notifPayload.iconName}"`;
if (notifPayload.urgency) command += ` -u "${notifPayload.urgency}"`;
if (notifPayload.timeout !== undefined) command += ` -t ${notifPayload.timeout}`;
if (notifPayload.category) command += ` -c "${notifPayload.category}"`;
if (notifPayload.transient) command += ` -e`;
if (notifPayload.id !== undefined) command += ` -r ${notifPayload.id}`;
Utils.execAsync(command);
};
export const getPosition = (pos: NotificationAnchor | OSDAnchor): ('top' | 'bottom' | 'left' | 'right')[] => {
const positionMap: { [key: string]: ('top' | 'bottom' | 'left' | 'right')[] } = {
top: ['top'],
'top right': ['top', 'right'],
'top left': ['top', 'left'],
bottom: ['bottom'],
'bottom right': ['bottom', 'right'],
'bottom left': ['bottom', 'left'],
right: ['right'],
left: ['left'],
};
return positionMap[pos] || ['top'];
};
export const isValidGjsColor = (color: string): boolean => {
const colorLower = color.toLowerCase().trim();
if (namedColors.has(colorLower)) {
return true;
}
const hexColorRegex = /^#(?:[a-fA-F0-9]{3,4}|[a-fA-F0-9]{6,8})$/;
const rgbRegex = /^rgb\(\s*(\d{1,3}%?\s*,\s*){2}\d{1,3}%?\s*\)$/;
const rgbaRegex = /^rgba\(\s*(\d{1,3}%?\s*,\s*){3}(0|1|0?\.\d+)\s*\)$/;
if (hexColorRegex.test(color)) {
return true;
}
if (rgbRegex.test(colorLower) || rgbaRegex.test(colorLower)) {
return true;
}
return false;
};
export const capitalizeFirstLetter = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
================================================
FILE: .config/ags/lib/variables.ts
================================================
import GLib from 'gi://GLib';
import { DateTime } from 'types/@girs/glib-2.0/glib-2.0.cjs';
export const clock = Variable(GLib.DateTime.new_now_local(), {
poll: [1000, (): DateTime => GLib.DateTime.new_now_local()],
});
export const uptime = Variable(0, {
poll: [60_000, 'cat /proc/uptime', (line): number => Number.parseInt(line.split('.')[0]) / 60],
});
export const distro = {
id: GLib.get_os_info('ID'),
logo: GLib.get_os_info('LOGO'),
};
================================================
FILE: .config/ags/main.ts
================================================
import 'lib/session';
import 'scss/style';
import 'globals/useTheme';
import 'globals/dropdown.js';
import 'globals/utilities';
import { Bar } from 'modules/bar/Bar';
import MenuWindows from './modules/menus/main.js';
import SettingsDialog from 'widget/settings/SettingsDialog';
import Notifications from './modules/notifications/index.js';
import { forMonitors } from 'lib/utils';
import OSD from 'modules/osd/index';
App.config({
onConfigParsed: () => Utils.execAsync(`python3 ${App.configDir}/services/bluetooth.py`),
windows: [...MenuWindows, Notifications(), SettingsDialog(), ...forMonitors(Bar), OSD()],
closeWindowDelay: {
sideright: 350,
launcher: 350,
bar0: 350,
},
});
================================================
FILE: .config/ags/modules/bar/Bar.ts
================================================
const hyprland = await Service.import('hyprland');
import {
Menu,
Workspaces,
ClientTitle,
Media,
Notifications,
Volume,
Network,
Bluetooth,
BatteryLabel,
Clock,
SysTray,
// Custom Modules
Ram,
Cpu,
Storage,
Netstat,
KbInput,
Updates,
Submap,
Weather,
Power,
} from './Exports';
import { BarItemBox as WidgetContainer } from '../shared/barItemBox.js';
import options from 'options';
import Gdk from 'gi://Gdk?version=3.0';
import Button from 'types/widgets/button.js';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
import './SideEffects';
import { WindowLayer } from 'lib/types/options.js';
import { Attribute, Child } from 'lib/types/widget.js';
import Window from 'types/widgets/window.js';
const { layouts } = options.bar;
const { location } = options.theme.bar;
export type BarWidget = keyof typeof widget;
type Section =
| 'battery'
| 'dashboard'
| 'workspaces'
| 'windowtitle'
| 'media'
| 'notifications'
| 'volume'
| 'network'
| 'bluetooth'
| 'clock'
| 'ram'
| 'cpu'
| 'storage'
| 'netstat'
| 'kbinput'
| 'updates'
| 'submap'
| 'weather'
| 'power'
| 'systray';
type Layout = {
left: Section[];
middle: Section[];
right: Section[];
};
type BarLayout = {
[key: string]: Layout;
};
const getLayoutForMonitor = (monitor: number, layouts: BarLayout): Layout => {
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
const wildcard = Object.keys(layouts).find((key) => key === '*');
if (matchingKey) {
return layouts[matchingKey];
}
if (wildcard) {
return layouts[wildcard];
}
return {
left: ['dashboard', 'workspaces', 'windowtitle'],
middle: ['media'],
right: ['volume', 'network', 'bluetooth', 'battery', 'systray', 'clock', 'notifications'],
};
};
const widget = {
battery: (): Button => WidgetContainer(BatteryLabel()),
dashboard: (): Button => WidgetContainer(Menu()),
workspaces: (monitor: number): Button => WidgetContainer(Workspaces(monitor)),
windowtitle: (): Button => WidgetContainer(ClientTitle()),
media: (): Button => WidgetContainer(Media()),
notifications: (): Button => WidgetContainer(Notifications()),
volume: (): Button => WidgetContainer(Volume()),
network: (): Button => WidgetContainer(Network()),
bluetooth: (): Button => WidgetContainer(Bluetooth()),
clock: (): Button => WidgetContainer(Clock()),
systray: (): Button => WidgetContainer(SysTray()),
ram: (): Button => WidgetContainer(Ram()),
cpu: (): Button => WidgetContainer(Cpu()),
storage: (): Button => WidgetContainer(Storage()),
netstat: (): Button => WidgetContainer(Netstat()),
kbinput: (): Button => WidgetContainer(KbInput()),
updates: (): Button => WidgetContainer(Updates()),
submap: (): Button => WidgetContainer(Submap()),
weather: (): Button => WidgetContainer(Weather()),
power: (): Button => WidgetContainer(Power()),
};
type GdkMonitors = {
[key: string]: {
key: string;
model: string;
used: boolean;
};
};
function getGdkMonitors(): GdkMonitors {
const display = Gdk.Display.get_default();
if (display === null) {
console.error('Failed to get Gdk display.');
return {};
}
const numGdkMonitors = display.get_n_monitors();
const gdkMonitors: GdkMonitors = {};
for (let i = 0; i < numGdkMonitors; i++) {
const curMonitor = display.get_monitor(i);
if (curMonitor === null) {
console.warn(`Monitor at index ${i} is null.`);
continue;
}
const model = curMonitor.get_model() || '';
const geometry = curMonitor.get_geometry();
const scaleFactor = curMonitor.get_scale_factor();
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
gdkMonitors[i] = { key, model, used: false };
}
return gdkMonitors;
}
/**
* NOTE: Some more funky stuff being done by GDK.
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
*
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
* the id 0 will ALWAYS be DP-1.
*
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
* is at id 2.
*
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
* is being determined as by Hyprland so the bars show up on the right monitors.
*
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
* in the case that there are multiple models in the same resolution with the same scale. We find the
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
*
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
*
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
* an existing one or shutting it off.
*
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
*
* Fun stuff really... :facepalm:
*/
const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set): number => {
const gdkMonitors = getGdkMonitors();
if (Object.keys(gdkMonitors).length === 0) {
console.error('No GDK monitors were found.');
return monitor;
}
// Get the GDK monitor for the given monitor index
const gdkMonitor = gdkMonitors[monitor];
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
const directMatch = hyprland.monitors.find((hypMon) => {
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
});
if (directMatch) {
usedHyprlandMonitors.add(directMatch.id);
return directMatch.id;
}
// Second pass: Relaxed matching without considering the monitor index
const hyprlandMonitor = hyprland.monitors.find((hypMon) => {
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id);
});
if (hyprlandMonitor) {
usedHyprlandMonitors.add(hyprlandMonitor.id);
return hyprlandMonitor.id;
}
// Fallback: Find the first available monitor ID that hasn't been used
const fallbackMonitor = hyprland.monitors.find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
if (fallbackMonitor) {
usedHyprlandMonitors.add(fallbackMonitor.id);
return fallbackMonitor.id;
}
// Ensure we return a valid monitor ID that actually exists
for (let i = 0; i < hyprland.monitors.length; i++) {
if (!usedHyprlandMonitors.has(i)) {
usedHyprlandMonitors.add(i);
return i;
}
}
// As a last resort, return the original monitor index if no unique monitor can be found
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
return monitor;
};
export const Bar = (() => {
const usedHyprlandMonitors = new Set();
return (monitor: number): Window => {
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
return Widget.Window({
name: `bar-${hyprlandMonitor}`,
class_name: 'bar',
monitor,
visible: true,
anchor: location.bind('value').as((ln) => [ln, 'left', 'right']),
exclusivity: 'exclusive',
layer: Utils.merge(
[options.theme.bar.layer.bind('value'), options.tear.bind('value')],
(barLayer: WindowLayer, tear: boolean) => {
if (tear && barLayer === 'overlay') {
return 'top';
}
return barLayer;
},
),
child: Widget.Box({
class_name: 'bar-panel-container',
child: Widget.CenterBox({
class_name: 'bar-panel',
css: 'padding: 1px',
startWidget: Widget.Box({
class_name: 'box-left',
hexpand: true,
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value as BarLayout);
self.children = foundLayout.left
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button);
});
},
}),
centerWidget: Widget.Box({
class_name: 'box-center',
hpack: 'center',
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value as BarLayout);
self.children = foundLayout.middle
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button);
});
},
}),
endWidget: Widget.Box({
class_name: 'box-right',
hpack: 'end',
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value as BarLayout);
self.children = foundLayout.right
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button);
});
},
}),
}),
}),
});
};
})();
================================================
FILE: .config/ags/modules/bar/Exports.ts
================================================
import { Menu } from './menu/index';
import { Workspaces } from './workspaces/index';
import { ClientTitle } from './window_title/index';
import { Media } from './media/index';
import { Notifications } from './notifications/index';
import { Volume } from './volume/index';
import { Network } from './network/index';
import { Bluetooth } from './bluetooth/index';
import { BatteryLabel } from './battery/index';
import { Clock } from './clock/index';
import { SysTray } from './systray/index';
// Custom Modules
import { Ram } from '../../customModules/ram/index';
import { Cpu } from '../../customModules/cpu/index';
import { Storage } from 'customModules/storage/index';
import { Netstat } from 'customModules/netstat/index';
import { KbInput } from 'customModules/kblayout/index';
import { Updates } from 'customModules/updates/index';
import { Submap } from 'customModules/submap/index';
import { Weather } from 'customModules/weather/index';
import { Power } from 'customModules/power/index';
export {
Menu,
Workspaces,
ClientTitle,
Media,
Notifications,
Volume,
Network,
Bluetooth,
BatteryLabel,
Clock,
SysTray,
// Custom Modules
Ram,
Cpu,
Storage,
Netstat,
KbInput,
Updates,
Submap,
Weather,
Power,
};
================================================
FILE: .config/ags/modules/bar/SideEffects.ts
================================================
import options from 'options';
const { showIcon, showTime } = options.bar.clock;
showIcon.connect('changed', () => {
if (!showTime.value && !showIcon.value) {
showTime.value = true;
}
});
showTime.connect('changed', () => {
if (!showTime.value && !showIcon.value) {
showIcon.value = true;
}
});
const { label, icon } = options.bar.windowtitle;
label.connect('changed', () => {
if (!label.value && !icon.value) {
icon.value = true;
}
});
icon.connect('changed', () => {
if (!label.value && !icon.value) {
label.value = true;
}
});
================================================
FILE: .config/ags/modules/bar/battery/index.ts
================================================
const battery = await Service.import('battery');
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { label: show_label, rightClick, middleClick, scrollUp, scrollDown, hideLabelWhenFull } = options.bar.battery;
const BatteryLabel = (): BarBoxChild => {
const isVis = Variable(battery.available);
const batIcon = Utils.merge(
[battery.bind('percent'), battery.bind('charging'), battery.bind('charged')],
(batPercent: number, batCharging, batCharged) => {
if (batCharged) return `battery-level-100-charged-symbolic`;
else return `battery-level-${Math.floor(batPercent / 10) * 10}${batCharging ? '-charging' : ''}-symbolic`;
},
);
battery.connect('changed', ({ available }) => {
isVis.value = available;
});
const formatTime = (seconds: number): Record => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return { hours, minutes };
};
const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean): string => {
if (isCharged) {
return 'Fully Charged!!!';
}
const { hours, minutes } = formatTime(timeSeconds);
if (isCharging) {
return `${hours} hours ${minutes} minutes until full`;
} else {
return `${hours} hours ${minutes} minutes left`;
}
};
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `battery-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
visible: battery.bind('available'),
tooltip_text: battery.bind('time_remaining').as((t) => t.toString()),
children: Utils.merge(
[
battery.bind('available'),
show_label.bind('value'),
battery.bind('charged'),
hideLabelWhenFull.bind('value'),
],
(batAvail, showLabel, isCharged, hideWhenFull) => {
if (batAvail && showLabel) {
return [
Widget.Icon({
class_name: 'bar-button-icon battery',
icon: batIcon,
}),
...(hideWhenFull && isCharged
? []
: [
Widget.Label({
class_name: 'bar-button-label battery',
label: battery.bind('percent').as((p) => `${Math.floor(p)}%`),
}),
]),
];
} else if (batAvail && !showLabel) {
return [
Widget.Icon({
class_name: 'bar-button-icon battery',
icon: batIcon,
}),
];
} else {
return [];
}
},
),
setup: (self) => {
self.hook(battery, () => {
if (battery.available) {
self.tooltip_text = generateTooltip(battery.time_remaining, battery.charging, battery.charged);
}
});
},
}),
isVis,
boxClass: 'battery',
props: {
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
onPrimaryClick: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'energymenu');
},
},
};
};
export { BatteryLabel };
================================================
FILE: .config/ags/modules/bar/bluetooth/index.ts
================================================
const bluetooth = await Service.import('bluetooth');
import Gdk from 'gi://Gdk?version=3.0';
import options from 'options';
import { openMenu } from '../utils.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { label, rightClick, middleClick, scrollDown, scrollUp } = options.bar.bluetooth;
const Bluetooth = (): BarBoxChild => {
const btIcon = Widget.Label({
label: bluetooth.bind('enabled').as((v) => (v ? '' : '')),
class_name: 'bar-button-icon bluetooth txt-icon bar',
});
const btText = Widget.Label({
label: Utils.merge([bluetooth.bind('enabled'), bluetooth.bind('connected_devices')], (btEnabled, btDevices) => {
return btEnabled && btDevices.length ? ` Connected (${btDevices.length})` : btEnabled ? 'On' : 'Off';
}),
class_name: 'bar-button-label bluetooth',
});
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `bluetooth-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: options.bar.bluetooth.label.bind('value').as((showLabel) => {
if (showLabel) {
return [btIcon, btText];
}
return [btIcon];
}),
}),
isVisible: true,
boxClass: 'bluetooth',
props: {
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
on_primary_click: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'bluetoothmenu');
},
},
};
};
export { Bluetooth };
================================================
FILE: .config/ags/modules/bar/clock/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
import GLib from 'gi://GLib';
import { openMenu } from '../utils.js';
import options from 'options';
import { DateTime } from 'types/@girs/glib-2.0/glib-2.0.cjs';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { format, icon, showIcon, showTime, rightClick, middleClick, scrollUp, scrollDown } = options.bar.clock;
const { style } = options.theme.bar.buttons;
const date = Variable(GLib.DateTime.new_now_local(), {
poll: [1000, (): DateTime => GLib.DateTime.new_now_local()],
});
const time = Utils.derive([date, format], (c, f) => c.format(f) || '');
const Clock = (): BarBoxChild => {
const clockTime = Widget.Label({
class_name: 'bar-button-label clock bar',
label: time.bind(),
});
const clockIcon = Widget.Label({
label: icon.bind('value'),
class_name: 'bar-button-icon clock txt-icon bar',
});
return {
component: Widget.Box({
className: Utils.merge(
[style.bind('value'), showIcon.bind('value'), showTime.bind('value')],
(btnStyle, shwIcn, shwLbl) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `clock-container ${styleMap[btnStyle]} ${!shwLbl ? 'no-label' : ''} ${!shwIcn ? 'no-icon' : ''}`;
},
),
children: Utils.merge([showIcon.bind('value'), showTime.bind('value')], (shIcn, shTm) => {
if (shIcn && !shTm) {
return [clockIcon];
} else if (shTm && !shIcn) {
return [clockTime];
}
return [clockIcon, clockTime];
}),
}),
isVisible: true,
boxClass: 'clock',
props: {
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
on_primary_click: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'calendarmenu');
},
},
};
};
export { Clock };
================================================
FILE: .config/ags/modules/bar/media/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
const mpris = await Service.import('mpris');
import { openMenu } from '../utils.js';
import options from 'options';
import { getCurrentPlayer } from 'lib/shared/media.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand } from 'customModules/utils.js';
const { show_artist, truncation, truncation_size, show_label, show_active_only, rightClick, middleClick } =
options.bar.media;
const Media = (): BarBoxChild => {
const activePlayer = Variable(mpris.players[0]);
const isVis = Variable(!show_active_only.value);
show_active_only.connect('changed', () => {
isVis.value = !show_active_only.value || mpris.players.length > 0;
});
mpris.connect('changed', () => {
const curPlayer = getCurrentPlayer(activePlayer.value);
activePlayer.value = curPlayer;
isVis.value = !show_active_only.value || mpris.players.length > 0;
});
const getIconForPlayer = (playerName: string): string => {
const windowTitleMap = [
['Firefox', ''],
['Microsoft Edge', ''],
['Discord', ''],
['Plex', ''],
['Spotify', ''],
['(.*)', ''],
];
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0], 'i').test(playerName));
return foundMatch ? foundMatch[1] : '';
};
const songIcon = Variable('');
const mediaLabel = Utils.watch('Media', [mpris, show_artist, truncation, truncation_size, show_label], () => {
if (activePlayer.value && show_label.value) {
const { track_title, identity, track_artists } = activePlayer.value;
songIcon.value = getIconForPlayer(identity);
const trackArtist = show_artist.value ? ` - ${track_artists.join(', ')}` : ``;
const truncatedLabel = truncation.value
? `${track_title + trackArtist}`.substring(0, truncation_size.value)
: `${track_title + trackArtist}`;
return track_title.length === 0
? `No media playing...`
: truncatedLabel.length < truncation_size.value || !truncation.value
? `${truncatedLabel}`
: `${truncatedLabel.substring(0, truncatedLabel.length - 3)}...`;
} else {
songIcon.value = getIconForPlayer(activePlayer.value?.identity || '');
return `Media`;
}
});
return {
component: Widget.Box({
visible: false,
child: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
(style) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `media-container ${styleMap[style]}`;
},
),
child: Widget.Box({
children: [
Widget.Label({
class_name: 'bar-button-icon media txt-icon bar',
label: songIcon.bind('value').as((v) => v || ''),
}),
Widget.Label({
class_name: 'bar-button-label media',
label: mediaLabel,
}),
],
}),
}),
}),
isVis,
boxClass: 'media',
props: {
on_scroll_up: () => activePlayer.value?.next(),
on_scroll_down: () => activePlayer.value?.previous(),
on_primary_click: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'mediamenu');
},
onSecondaryClick: (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
},
onMiddleClick: (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
},
},
};
};
export { Media };
================================================
FILE: .config/ags/modules/bar/menu/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.launcher;
const Menu = (): BarBoxChild => {
return {
component: Widget.Box({
className: Utils.merge([options.theme.bar.buttons.style.bind('value')], (style) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `dashboard ${styleMap[style]}`;
}),
child: Widget.Label({
class_name: 'bar-menu_label bar-button_icon txt-icon bar',
label: options.bar.launcher.icon.bind('value'),
}),
}),
isVisible: true,
boxClass: 'dashboard',
props: {
on_primary_click: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'dashboardmenu');
},
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Menu };
================================================
FILE: .config/ags/modules/bar/network/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
const network = await Service.import('network');
import options from 'options';
import { openMenu } from '../utils.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const {
label: networkLabel,
truncation,
truncation_size,
rightClick,
middleClick,
scrollDown,
scrollUp,
} = options.bar.network;
const Network = (): BarBoxChild => {
return {
component: Widget.Box({
vpack: 'fill',
vexpand: true,
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), networkLabel.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `network-container ${styleMap[style]}${!showLabel ? ' no-label' : ''}`;
},
),
children: [
Widget.Icon({
class_name: 'bar-button-icon network-icon',
icon: Utils.merge(
[network.bind('primary'), network.bind('wifi'), network.bind('wired')],
(pmry, wfi, wrd) => {
if (pmry === 'wired') {
return wrd.icon_name;
}
return wfi.icon_name;
},
),
}),
Widget.Box({
child: Utils.merge(
[
network.bind('primary'),
network.bind('wifi'),
networkLabel.bind('value'),
truncation.bind('value'),
truncation_size.bind('value'),
],
(pmry, wfi, showLbl, trunc, tSize) => {
if (!showLbl) {
return Widget.Box();
}
if (pmry === 'wired') {
return Widget.Label({
class_name: 'bar-button-label network-label',
label: 'Wired'.substring(0, tSize),
});
}
return Widget.Label({
class_name: 'bar-button-label network-label',
label: wfi.ssid ? `${trunc ? wfi.ssid.substring(0, tSize) : wfi.ssid}` : '--',
});
},
),
}),
],
}),
isVisible: true,
boxClass: 'network',
props: {
on_primary_click: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'networkmenu');
},
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Network };
================================================
FILE: .config/ags/modules/bar/notifications/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { filterNotifications } from 'lib/shared/notifications.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { show_total, rightClick, middleClick, scrollUp, scrollDown, hideCountWhenZero } = options.bar.notifications;
const { ignore } = options.notifications;
const notifs = await Service.import('notifications');
export const Notifications = (): BarBoxChild => {
return {
component: Widget.Box({
hpack: 'start',
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_total.bind('value')],
(style, showTotal) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `notifications-container ${styleMap[style]} ${!showTotal ? 'no-label' : ''}`;
},
),
child: Widget.Box({
hpack: 'start',
class_name: 'bar-notifications',
children: Utils.merge(
[
notifs.bind('notifications'),
notifs.bind('dnd'),
show_total.bind('value'),
ignore.bind('value'),
hideCountWhenZero.bind('value'),
],
(notif, dnd, showTotal, ignoredNotifs, hideCountForZero) => {
const filteredNotifications = filterNotifications(notif, ignoredNotifs);
const notifIcon = Widget.Label({
hpack: 'center',
class_name: 'bar-button-icon notifications txt-icon bar',
label: dnd ? '' : filteredNotifications.length > 0 ? '' : '',
});
const notifLabel = Widget.Label({
hpack: 'center',
class_name: 'bar-button-label notifications',
label: filteredNotifications.length.toString(),
});
if (showTotal) {
if (hideCountForZero && filteredNotifications.length === 0) {
return [notifIcon];
}
return [notifIcon, notifLabel];
}
return [notifIcon];
},
),
}),
}),
isVisible: true,
boxClass: 'notifications',
props: {
on_primary_click: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'notificationsmenu');
},
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
================================================
FILE: .config/ags/modules/bar/systray/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
import { BarBoxChild, SelfButton } from 'lib/types/bar';
import { Notify } from 'lib/utils';
const systemtray = await Service.import('systemtray');
import options from 'options';
const { ignore } = options.bar.systray;
const SysTray = (): BarBoxChild => {
const isVis = Variable(false);
const items = Utils.merge([systemtray.bind('items'), ignore.bind('value')], (items, ignored) => {
const filteredTray = items.filter(({ id }) => !ignored.includes(id));
isVis.value = filteredTray.length > 0;
return filteredTray.map((item) => {
return Widget.Button({
cursor: 'pointer',
child: Widget.Icon({
class_name: 'systray-icon',
icon: item.bind('icon'),
}),
on_primary_click: (_: SelfButton, event: Gdk.Event) => item.activate(event),
on_secondary_click: (_, event) => item.openMenu(event),
onMiddleClick: () => Notify({ summary: 'App Name', body: item.id }),
tooltip_markup: item.bind('tooltip_markup'),
});
});
});
return {
component: Widget.Box({
class_name: 'systray-container',
children: items,
}),
isVisible: true,
boxClass: 'systray',
isVis,
props: {},
};
};
export { SysTray };
================================================
FILE: .config/ags/modules/bar/utils.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
import { Attribute, Child } from 'lib/types/widget';
import { calculateMenuPosition } from 'modules/menus/shared/dropdown/locationHandler/index';
import Button from 'types/widgets/button';
export const closeAllMenus = (): void => {
const menuWindows = App.windows
.filter((w) => {
if (w.name) {
return /.*menu/.test(w.name);
}
return false;
})
.map((w) => w.name);
menuWindows.forEach((w) => {
if (w) {
App.closeWindow(w);
}
});
};
export const openMenu = async (clicked: Button, event: Gdk.Event, window: string): Promise => {
/*
* NOTE: We have to make some adjustments so the menu pops up relatively
* to the center of the button clicked. We don't want the menu to spawn
* offcenter depending on which edge of the button you click on.
* -------------
* To fix this, we take the x coordinate of the click within the button's bounds.
* If you click the left edge of a 100 width button, then the x axis will be 0
* and if you click the right edge then the x axis will be 100.
* -------------
* Then we divide the width of the button by 2 to get the center of the button and then get
* the offset by subtracting the clicked x coordinate. Then we can apply that offset
* to the x coordinate of the click relative to the screen to get the center of the
* icon click.
*/
const middleOfButton = Math.floor(clicked.get_allocated_width() / 2);
const xAxisOfButtonClick = clicked.get_pointer()[0];
const middleOffset = middleOfButton - xAxisOfButtonClick;
const clickPos = event.get_root_coords();
const adjustedXCoord = clickPos[1] + middleOffset;
const coords = [adjustedXCoord, clickPos[2]];
try {
await calculateMenuPosition(coords, window);
} catch (error) {
console.error(`Error calculating menu position: ${error}`);
}
closeAllMenus();
App.toggleWindow(window);
};
================================================
FILE: .config/ags/modules/bar/volume/index.ts
================================================
import Gdk from 'gi://Gdk?version=3.0';
const audio = await Service.import('audio');
import { openMenu } from '../utils.js';
import options from 'options';
import { Binding } from 'lib/utils.js';
import { VolumeIcons } from 'lib/types/volume.js';
import { BarBoxChild } from 'lib/types/bar.js';
import { Bind } from 'lib/types/variable.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.volume;
const Volume = (): BarBoxChild => {
const icons: VolumeIcons = {
101: '',
66: '',
34: '',
1: '',
0: '',
};
const getIcon = (): Bind => {
const icon: Binding = Utils.merge(
[audio.speaker.bind('is_muted'), audio.speaker.bind('volume')],
(isMuted, vol) => {
if (isMuted) return 0;
const foundVol = [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
if (foundVol !== undefined) {
return foundVol;
}
return 101;
},
);
return icon.as((i: number) => (i !== undefined ? icons[i] : icons[101]));
};
const volIcn = Widget.Label({
hexpand: true,
label: getIcon(),
class_name: 'bar-button-icon volume txt-icon bar',
});
const volPct = Widget.Label({
hexpand: true,
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
class_name: 'bar-button-label volume',
});
return {
component: Widget.Box({
hexpand: true,
vexpand: true,
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), options.bar.volume.label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `volume-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: options.bar.volume.label.bind('value').as((showLabel) => {
if (showLabel) {
return [volIcn, volPct];
}
return [volIcn];
}),
}),
isVisible: true,
boxClass: 'volume',
props: {
onPrimaryClick: (clicked: Button, event: Gdk.Event): void => {
openMenu(clicked, event, 'audiomenu');
},
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Volume };
================================================
FILE: .config/ags/modules/bar/window_title/index.ts
================================================
const hyprland = await Service.import('hyprland');
import { BarBoxChild } from 'lib/types/bar';
import options from 'options';
import { Attribute, Child } from 'lib/types/widget';
import { ActiveClient } from 'types/service/hyprland';
import Label from 'types/widgets/label';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
const { leftClick, rightClick, middleClick, scrollDown, scrollUp } = options.bar.windowtitle;
const filterTitle = (windowtitle: ActiveClient): Record => {
const windowTitleMap = [
// user provided values
...options.bar.windowtitle.title_map.value,
// Original Entries
['kitty', '', 'Kitty Terminal'],
['firefox', '', 'Firefox'],
['microsoft-edge', '', 'Edge'],
['discord', '', 'Discord'],
['vesktop', '', 'Vesktop'],
['org.kde.dolphin', '', 'Dolphin'],
['plex', '', 'Plex'],
['steam', '', 'Steam'],
['spotify', '', 'Spotify'],
['ristretto', '', 'Ristretto'],
['obsidian', '', 'Obsidian'],
// Browsers
['google-chrome', '', 'Google Chrome'],
['brave-browser', '', 'Brave Browser'],
['chromium', '', 'Chromium'],
['opera', '', 'Opera'],
['vivaldi', '', 'Vivaldi'],
['waterfox', '', 'Waterfox'],
['thorium', '', 'Waterfox'],
['tor-browser', '', 'Tor Browser'],
['floorp', '', 'Floorp'],
// Terminals
['gnome-terminal', '', 'GNOME Terminal'],
['konsole', '', 'Konsole'],
['alacritty', '', 'Alacritty'],
['wezterm', '', 'Wezterm'],
['foot', '', 'Foot Terminal'],
['tilix', '', 'Tilix'],
['xterm', '', 'XTerm'],
['urxvt', '', 'URxvt'],
['st', '', 'st Terminal'],
// Development Tools
['code', '', 'Visual Studio Code'],
['vscode', '', 'VS Code'],
['sublime-text', '', 'Sublime Text'],
['atom', '', 'Atom'],
['android-studio', '', 'Android Studio'],
['intellij-idea', '', 'IntelliJ IDEA'],
['pycharm', '', 'PyCharm'],
['webstorm', '', 'WebStorm'],
['phpstorm', '', 'PhpStorm'],
['eclipse', '', 'Eclipse'],
['netbeans', '', 'NetBeans'],
['docker', '', 'Docker'],
['vim', '', 'Vim'],
['neovim', '', 'Neovim'],
['neovide', '', 'Neovide'],
['emacs', '', 'Emacs'],
// Communication Tools
['slack', '', 'Slack'],
['telegram-desktop', '', 'Telegram'],
['org.telegram.desktop', '', 'Telegram'],
['whatsapp', '', 'WhatsApp'],
['teams', '', 'Microsoft Teams'],
['skype', '', 'Skype'],
['thunderbird', '', 'Thunderbird'],
// File Managers
['nautilus', '', 'Files (Nautilus)'],
['thunar', '', 'Thunar'],
['pcmanfm', '', 'PCManFM'],
['nemo', '', 'Nemo'],
['ranger', '', 'Ranger'],
['doublecmd', '', 'Double Commander'],
['krusader', '', 'Krusader'],
// Media Players
['vlc', '', 'VLC Media Player'],
['mpv', '', 'MPV'],
['rhythmbox', '', 'Rhythmbox'],
// Graphics Tools
['gimp', '', 'GIMP'],
['inkscape', '', 'Inkscape'],
['krita', '', 'Krita'],
['blender', '', 'Blender'],
// Video Editing
['kdenlive', '', 'Kdenlive'],
// Games and Gaming Platforms
['lutris', '', 'Lutris'],
['heroic', '', 'Heroic Games Launcher'],
['minecraft', '', 'Minecraft'],
['csgo', '', 'CS:GO'],
['dota2', '', 'Dota 2'],
// Office and Productivity
['evernote', '', 'Evernote'],
['sioyek', '', 'Sioyek'],
// Cloud Services and Sync
['dropbox', '', 'Dropbox'],
// Desktop
['^$', '', 'Desktop'],
// Fallback icon
['(.+)', '', `${windowtitle.class.charAt(0).toUpperCase() + windowtitle.class.slice(1)}`],
];
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(windowtitle.class.toLowerCase()));
// return the default icon if no match is found or
// if the array element matched is not of size 3
if (!foundMatch || foundMatch.length !== 3) {
return {
icon: windowTitleMap[windowTitleMap.length - 1][1],
label: windowTitleMap[windowTitleMap.length - 1][2],
};
}
return {
icon: foundMatch[1],
label: foundMatch[2],
};
};
const getTitle = (client: ActiveClient, useCustomTitle: boolean, useClassName: boolean): string => {
if (useCustomTitle) return filterTitle(client).label;
if (useClassName) return client.class;
const title = client.title;
// If the title is empty or only filled with spaces, fallback to the class name
if (title.length === 0 || title.match(/^ *$/)) {
return client.class;
}
return title;
};
const truncateTitle = (title: string, max_size: number): string => {
if (max_size > 0 && title.length > max_size) {
return title.substring(0, max_size).trim() + '...';
}
return title;
};
const ClientTitle = (): BarBoxChild => {
const { custom_title, class_name, label, icon, truncation, truncation_size } = options.bar.windowtitle;
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `windowtitle-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: Utils.merge(
[
hyprland.active.bind('client'),
custom_title.bind('value'),
class_name.bind('value'),
label.bind('value'),
icon.bind('value'),
truncation.bind('value'),
truncation_size.bind('value'),
],
(client, useCustomTitle, useClassName, showLabel, showIcon, truncate, truncationSize) => {
const children: Label[] = [];
if (showIcon) {
children.push(
Widget.Label({
class_name: 'bar-button-icon windowtitle txt-icon bar',
label: filterTitle(client).icon,
}),
);
}
if (showLabel) {
children.push(
Widget.Label({
class_name: `bar-button-label windowtitle ${showIcon ? '' : 'no-icon'}`,
label: truncateTitle(
getTitle(client, useCustomTitle, useClassName),
truncate ? truncationSize : -1,
),
}),
);
}
return children;
},
),
}),
isVisible: true,
boxClass: 'windowtitle',
props: {
setup: (self: Button): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_primary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(leftClick.value, { clicked, event });
};
self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { ClientTitle };
================================================
FILE: .config/ags/modules/bar/workspaces/helpers.ts
================================================
const hyprland = await Service.import('hyprland');
import { MonitorMap, WorkspaceMap, WorkspaceRule } from 'lib/types/workspace';
import options from 'options';
import { Variable } from 'types/variable';
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
export const getWorkspacesForMonitor = (curWs: number, wsRules: WorkspaceMap, monitor: number): boolean => {
if (!wsRules || !Object.keys(wsRules).length) {
return true;
}
const monitorMap: MonitorMap = {};
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({ id: m.monitorID, name: m.monitor }));
const monitors = [
...new Map([...workspaceMonitorList, ...hyprland.monitors].map((item) => [item.id, item])).values(),
];
monitors.forEach((m) => (monitorMap[m.id] = m.name));
const currentMonitorName = monitorMap[monitor];
const monitorWSRules = wsRules[currentMonitorName];
if (monitorWSRules === undefined) {
return true;
}
return monitorWSRules.includes(curWs);
};
export const getWorkspaceRules = (): WorkspaceMap => {
try {
const rules = Utils.exec('hyprctl workspacerules -j');
const workspaceRules: WorkspaceMap = {};
JSON.parse(rules).forEach((rule: WorkspaceRule) => {
const workspaceNum = parseInt(rule.workspaceString, 10);
if (isNaN(workspaceNum)) {
return;
}
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
workspaceRules[rule.monitor].push(workspaceNum);
} else {
workspaceRules[rule.monitor] = [workspaceNum];
}
});
return workspaceRules;
} catch (err) {
console.error(err);
return {};
}
};
export const getCurrentMonitorWorkspaces = (monitor: number): number[] => {
if (hyprland.monitors.length === 1) {
return Array.from({ length: workspaces.value }, (_, i) => i + 1);
}
const monitorWorkspaces = getWorkspaceRules();
const monitorMap: MonitorMap = {};
hyprland.monitors.forEach((m) => (monitorMap[m.id] = m.name));
const currentMonitorName = monitorMap[monitor];
return monitorWorkspaces[currentMonitorName];
};
type ThrottledScrollHandlers = {
throttledScrollUp: () => void;
throttledScrollDown: () => void;
};
export const isWorkspaceIgnored = (ignoredWorkspaces: Variable, workspaceNumber: number): boolean => {
if (ignoredWorkspaces.value === '') return false;
const ignoredWsRegex = new RegExp(ignoredWorkspaces.value);
return ignoredWsRegex.test(workspaceNumber.toString());
};
const navigateWorkspace = (
direction: 'next' | 'prev',
currentMonitorWorkspaces: Variable,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable,
): void => {
const workspacesList = activeWorkspaces
? hyprland.workspaces.filter((ws) => hyprland.active.monitor.id === ws.monitorID).map((ws) => ws.id)
: currentMonitorWorkspaces.value || Array.from({ length: workspaces.value }, (_, i) => i + 1);
if (workspacesList.length === 0) return;
const currentIndex = workspacesList.indexOf(hyprland.active.workspace.id);
const step = direction === 'next' ? 1 : -1;
let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length;
let attempts = 0;
while (attempts < workspacesList.length) {
const targetWS = workspacesList[newIndex];
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) {
hyprland.messageAsync(`dispatch workspace ${targetWS}`);
return;
}
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length;
attempts++;
}
};
export const goToNextWS = (
currentMonitorWorkspaces: Variable,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable,
): void => {
navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
};
export const goToPrevWS = (
currentMonitorWorkspaces: Variable,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable,
): void => {
navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
};
export function throttle void>(func: T, limit: number): T {
let inThrottle: boolean;
return function (this: ThisParameterType, ...args: Parameters) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
} as T;
}
export const createThrottledScrollHandlers = (
scrollSpeed: number,
currentMonitorWorkspaces: Variable,
activeWorkspaces: boolean = false,
): ThrottledScrollHandlers => {
const throttledScrollUp = throttle(() => {
if (reverse_scroll.value) {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
} else {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
}
}, 200 / scrollSpeed);
const throttledScrollDown = throttle(() => {
if (reverse_scroll.value) {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
} else {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
}
}, 200 / scrollSpeed);
return { throttledScrollUp, throttledScrollDown };
};
================================================
FILE: .config/ags/modules/bar/workspaces/index.ts
================================================
import options from 'options';
import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers';
import { BarBoxChild, SelfButton } from 'lib/types/bar';
import { occupiedWses } from './variants/occupied';
import { defaultWses } from './variants/default';
const { workspaces, scroll_speed } = options.bar.workspaces;
const Workspaces = (monitor = -1): BarBoxChild => {
const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor));
workspaces.connect('changed', () => {
currentMonitorWorkspaces.value = getCurrentMonitorWorkspaces(monitor);
});
return {
component: Widget.Box({
class_name: 'workspaces-box-container',
child: options.bar.workspaces.hideUnoccupied.bind('value').as((hideUnoccupied) => {
return hideUnoccupied ? occupiedWses(monitor) : defaultWses(monitor);
}),
}),
isVisible: true,
boxClass: 'workspaces',
props: {
setup: (self: SelfButton): void => {
Utils.merge(
[scroll_speed.bind('value'), options.bar.workspaces.hideUnoccupied.bind('value')],
(scroll_speed, hideUnoccupied) => {
const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers(
scroll_speed,
currentMonitorWorkspaces,
hideUnoccupied,
);
self.on_scroll_up = throttledScrollUp;
self.on_scroll_down = throttledScrollDown;
},
);
},
},
};
};
export { Workspaces };
================================================
FILE: .config/ags/modules/bar/workspaces/utils.ts
================================================
import { WorkspaceIconMap } from 'lib/types/workspace';
import { isValidGjsColor } from 'lib/utils';
import options from 'options';
import { Monitor } from 'types/service/hyprland';
const hyprland = await Service.import('hyprland');
const { monochrome, background } = options.theme.bar.buttons;
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
const { showWsIcons, showAllActive, numbered_active_indicator: activeIndicator } = options.bar.workspaces;
const isWorkspaceActiveOnMonitor = (monitor: number, monitors: Monitor[], i: number): boolean => {
return showAllActive.value && monitors[monitor]?.activeWorkspace?.id === i;
};
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
const iconEntry = wsIconMap[i];
if (!iconEntry) {
return `${i}`;
}
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
if (typeof iconEntry === 'string' && iconEntry !== '') {
return iconEntry;
}
if (hasIcon) {
return iconEntry.icon;
}
return `${i}`;
};
export const getWsColor = (
wsIconMap: WorkspaceIconMap,
i: number,
smartHighlight: boolean,
monitor: number,
monitors: Monitor[],
): string => {
const iconEntry = wsIconMap[i];
const hasColor = typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
if (!iconEntry) {
return '';
}
if (
showWsIcons.value &&
smartHighlight &&
activeIndicator.value === 'highlight' &&
(hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i))
) {
const iconColor = monochrome.value ? background : wsBackground;
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.value;
const colorCss = `color: ${iconColor};`;
const backgroundCss = `background: ${iconBackground};`;
return colorCss + backgroundCss;
}
if (hasColor && isValidGjsColor(iconEntry.color)) {
return `color: ${iconEntry.color}; border-bottom-color: ${iconEntry.color};`;
}
return '';
};
export const renderClassnames = (
showIcons: boolean,
showNumbered: boolean,
numberedActiveIndicator: string,
showWsIcons: boolean,
smartHighlight: boolean,
monitor: number,
monitors: Monitor[],
i: number,
): string => {
if (showIcons) {
return 'workspace-icon txt-icon bar';
}
if (showNumbered || showWsIcons) {
const numActiveInd =
hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)
? numberedActiveIndicator
: '';
const wsIconClass = showWsIcons ? 'txt-icon' : '';
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass}`;
return className.trim();
}
return 'default';
};
export const renderLabel = (
showIcons: boolean,
available: string,
active: string,
occupied: string,
workspaceMask: boolean,
showWsIcons: boolean,
wsIconMap: WorkspaceIconMap,
i: number,
index: number,
monitor: number,
monitors: Monitor[],
): string => {
if (showIcons) {
if (hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)) {
return active;
}
if ((hyprland.getWorkspace(i)?.windows || 0) > 0) {
return occupied;
}
if (monitor !== -1) {
return available;
}
}
if (showWsIcons) {
return getWsIcon(wsIconMap, i);
}
return workspaceMask ? `${index + 1}` : `${i}`;
};
================================================
FILE: .config/ags/modules/bar/workspaces/variants/default.ts
================================================
const hyprland = await Service.import('hyprland');
import options from 'options';
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
import { range } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import { getWsColor, renderClassnames, renderLabel } from '../utils';
import { WorkspaceIconMap } from 'lib/types/workspace';
import { Monitor } from 'types/service/hyprland';
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored } = options.bar.workspaces;
export const defaultWses = (monitor: number): BoxWidget => {
return Widget.Box({
children: Utils.merge(
[workspaces.bind('value'), monitorSpecific.bind('value'), ignored.bind('value')],
(workspaces: number, monitorSpecific: boolean) => {
return range(workspaces || 8)
.filter((workspaceNumber) => {
if (!monitorSpecific) {
return true;
}
const workspaceRules = getWorkspaceRules();
return (
getWorkspacesForMonitor(workspaceNumber, workspaceRules, monitor) &&
!isWorkspaceIgnored(ignored, workspaceNumber)
);
})
.sort((a, b) => {
return a - b;
})
.map((i, index) => {
return Widget.Button({
class_name: 'workspace-button',
on_primary_click: () => {
hyprland.messageAsync(`dispatch workspace ${i}`);
},
child: Widget.Label({
attribute: i,
vpack: 'center',
css: Utils.merge(
[
spacing.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.theme.matugen.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
],
(
sp: number,
showWsIcons: boolean,
workspaceIconMap: WorkspaceIconMap,
matugen: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
return (
`margin: 0rem ${0.375 * sp}rem;` +
`${showWsIcons && !matugen ? getWsColor(workspaceIconMap, i, smartHighlight, monitor, monitors) : ''}`
);
},
),
class_name: Utils.merge(
[
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.show_numbered.bind('value'),
options.bar.workspaces.numbered_active_indicator.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
],
(
showIcons: boolean,
showNumbered: boolean,
numberedActiveIndicator: string,
showWsIcons: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
return renderClassnames(
showIcons,
showNumbered,
numberedActiveIndicator,
showWsIcons,
smartHighlight,
monitor,
monitors,
i,
);
},
),
label: Utils.merge(
[
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
options.bar.workspaces.icons.occupied.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
workspaceMask.bind('value'),
hyprland.bind('monitors'),
],
(
showIcons: boolean,
available: string,
active: string,
occupied: string,
wsIconMap: WorkspaceIconMap,
showWsIcons: boolean,
workspaceMask: boolean,
monitors: Monitor[],
) => {
return renderLabel(
showIcons,
available,
active,
occupied,
workspaceMask,
showWsIcons,
wsIconMap,
i,
index,
monitor,
monitors,
);
},
),
setup: (self) => {
self.hook(hyprland, () => {
self.toggleClassName('active', hyprland.active.workspace.id === i);
self.toggleClassName('occupied', (hyprland.getWorkspace(i)?.windows || 0) > 0);
});
},
}),
});
});
},
),
});
};
================================================
FILE: .config/ags/modules/bar/workspaces/variants/occupied.ts
================================================
const hyprland = await Service.import('hyprland');
import options from 'options';
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
import { Monitor, Workspace } from 'types/service/hyprland';
import { getWsColor, renderClassnames, renderLabel } from '../utils';
import { range } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import { WorkspaceIconMap } from 'lib/types/workspace';
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored, showAllActive } = options.bar.workspaces;
export const occupiedWses = (monitor: number): BoxWidget => {
return Widget.Box({
children: Utils.merge(
[
monitorSpecific.bind('value'),
hyprland.bind('workspaces'),
workspaceMask.bind('value'),
workspaces.bind('value'),
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
options.bar.workspaces.icons.occupied.bind('value'),
options.bar.workspaces.show_numbered.bind('value'),
options.bar.workspaces.numbered_active_indicator.bind('value'),
spacing.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.theme.matugen.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
ignored.bind('value'),
showAllActive.bind('value'),
],
(
monitorSpecific: boolean,
wkSpaces: Workspace[],
workspaceMask: boolean,
totalWkspcs: number,
showIcons: boolean,
available: string,
active: string,
occupied: string,
showNumbered: boolean,
numberedActiveIndicator: string,
spacing: number,
wsIconMap: WorkspaceIconMap,
showWsIcons: boolean,
matugen: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
const activeId = hyprland.active.workspace.id;
let allWkspcs = range(totalWkspcs || 8);
const activeWorkspaces = wkSpaces.map((w) => w.id);
const workspaceRules = getWorkspaceRules();
// Sometimes hyprland doesn't have all the monitors in the list
// so we complement it with monitors from the workspace list
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({
id: m.monitorID,
name: m.monitor,
}));
const curMonitor =
hyprland.monitors.find((m) => m.id === monitor) ||
workspaceMonitorList.find((m) => m.id === monitor);
const workspacesWithRules = Object.keys(workspaceRules).reduce((acc: number[], k: string) => {
return [...acc, ...workspaceRules[k]];
}, [] as number[]);
const activesForMonitor = activeWorkspaces.filter((w) => {
if (
curMonitor &&
Object.hasOwnProperty.call(workspaceRules, curMonitor.name) &&
workspacesWithRules.includes(w)
) {
return workspaceRules[curMonitor.name].includes(w);
}
return true;
});
if (monitorSpecific) {
const wrkspcsInRange = range(totalWkspcs).filter((w) => {
return getWorkspacesForMonitor(w, workspaceRules, monitor);
});
allWkspcs = [...new Set([...activesForMonitor, ...wrkspcsInRange])];
} else {
allWkspcs = [...new Set([...allWkspcs, ...activeWorkspaces])];
}
return allWkspcs
.sort((a, b) => {
return a - b;
})
.map((i, index) => {
if (isWorkspaceIgnored(ignored, i)) {
return Widget.Box();
}
return Widget.Button({
class_name: 'workspace-button',
on_primary_click: () => {
hyprland.messageAsync(`dispatch workspace ${i}`);
},
child: Widget.Label({
attribute: i,
vpack: 'center',
css:
`margin: 0rem ${0.375 * spacing}rem;` +
`${showWsIcons && !matugen ? getWsColor(wsIconMap, i, smartHighlight, monitor, monitors) : ''}`,
class_name: renderClassnames(
showIcons,
showNumbered,
numberedActiveIndicator,
showWsIcons,
smartHighlight,
monitor,
monitors,
i,
),
label: renderLabel(
showIcons,
available,
active,
occupied,
workspaceMask,
showWsIcons,
wsIconMap,
i,
index,
monitor,
monitors,
),
setup: (self) => {
self.toggleClassName('active', activeId === i);
self.toggleClassName('occupied', (hyprland.getWorkspace(i)?.windows || 0) > 0);
},
}),
});
});
},
),
});
};
================================================
FILE: .config/ags/modules/icons/index.ts
================================================
export const substitutes = {
'transmission-gtk': 'transmission',
'blueberry.py': 'blueberry',
Caprine: 'facebook-messenger',
'com.raggesilver.BlackBox-symbolic': 'terminal-symbolic',
'org.wezfurlong.wezterm-symbolic': 'terminal-symbolic',
'audio-headset-bluetooth': 'audio-headphones-symbolic',
'audio-card-analog-usb': 'audio-speakers-symbolic',
'audio-card-analog-pci': 'audio-card-symbolic',
'preferences-system': 'emblem-system-symbolic',
'com.github.Aylur.ags-symbolic': 'controls-symbolic',
'com.github.Aylur.ags': 'controls-symbolic',
};
export default {
missing: 'image-missing-symbolic',
nix: {
nix: 'nix-snowflake-symbolic',
},
app: {
terminal: 'terminal-symbolic',
},
fallback: {
executable: 'application-x-executable',
notification: 'dialog-information-symbolic',
video: 'video-x-generic-symbolic',
audio: 'audio-x-generic-symbolic',
},
ui: {
close: 'window-close-symbolic',
colorpicker: 'color-select-symbolic',
info: 'info-symbolic',
link: 'external-link-symbolic',
lock: 'system-lock-screen-symbolic',
menu: 'open-menu-symbolic',
refresh: 'view-refresh-symbolic',
search: 'system-search-symbolic',
settings: 'emblem-system-symbolic',
themes: 'preferences-desktop-theme-symbolic',
tick: 'object-select-symbolic',
time: 'hourglass-symbolic',
toolbars: 'toolbars-symbolic',
warning: 'dialog-warning-symbolic',
arrow: {
right: 'pan-end-symbolic',
left: 'pan-start-symbolic',
down: 'pan-down-symbolic',
up: 'pan-up-symbolic',
},
},
audio: {
mic: {
muted: 'microphone-disabled-symbolic',
low: 'microphone-sensitivity-low-symbolic',
medium: 'microphone-sensitivity-medium-symbolic',
high: 'microphone-sensitivity-high-symbolic',
},
volume: {
muted: 'audio-volume-muted-symbolic',
low: 'audio-volume-low-symbolic',
medium: 'audio-volume-medium-symbolic',
high: 'audio-volume-high-symbolic',
overamplified: 'audio-volume-overamplified-symbolic',
},
type: {
headset: 'audio-headphones-symbolic',
speaker: 'audio-speakers-symbolic',
card: 'audio-card-symbolic',
},
mixer: 'mixer-symbolic',
},
powerprofile: {
balanced: 'power-profile-balanced-symbolic',
'power-saver': 'power-profile-power-saver-symbolic',
performance: 'power-profile-performance-symbolic',
},
asusctl: {
profile: {
Balanced: 'power-profile-balanced-symbolic',
Quiet: 'power-profile-power-saver-symbolic',
Performance: 'power-profile-performance-symbolic',
},
mode: {
Integrated: 'processor-symbolic',
Hybrid: 'controller-symbolic',
},
},
battery: {
charging: 'battery-flash-symbolic',
warning: 'battery-empty-symbolic',
},
bluetooth: {
enabled: 'bluetooth-active-symbolic',
disabled: 'bluetooth-disabled-symbolic',
},
brightness: {
indicator: 'display-brightness-symbolic',
keyboard: 'keyboard-brightness-symbolic',
screen: 'display-brightness-symbolic',
},
powermenu: {
sleep: 'weather-clear-night-symbolic',
reboot: 'system-reboot-symbolic',
logout: 'system-log-out-symbolic',
shutdown: 'system-shutdown-symbolic',
},
recorder: {
recording: 'media-record-symbolic',
},
notifications: {
noisy: 'org.gnome.Settings-notifications-symbolic',
silent: 'notifications-disabled-symbolic',
message: 'chat-bubbles-symbolic',
},
trash: {
full: 'user-trash-full-symbolic',
empty: 'user-trash-symbolic',
},
mpris: {
shuffle: {
enabled: 'media-playlist-shuffle-symbolic',
disabled: 'media-playlist-consecutive-symbolic',
},
loop: {
none: 'media-playlist-repeat-symbolic',
track: 'media-playlist-repeat-song-symbolic',
playlist: 'media-playlist-repeat-symbolic',
},
playing: 'media-playback-pause-symbolic',
paused: 'media-playback-start-symbolic',
stopped: 'media-playback-start-symbolic',
prev: 'media-skip-backward-symbolic',
next: 'media-skip-forward-symbolic',
},
system: {
cpu: 'org.gnome.SystemMonitor-symbolic',
ram: 'drive-harddisk-solidstate-symbolic',
temp: 'temperature-symbolic',
},
color: {
dark: 'dark-mode-symbolic',
light: 'light-mode-symbolic',
},
weather: {
warning: 'dialog-warning-symbolic',
sunny: 'weather-clear-symbolic',
clear: 'weather-clear-night-symbolic',
partly_cloudy: 'weather-few-clouds-symbolic',
partly_cloudy_night: 'weather-few-clouds-night-symbolic',
cloudy: 'weather-overcast-symbolic',
overcast: 'weather-overcast-symbolic',
mist: 'weather-overcast-symbolic',
patchy_rain_nearby: 'weather-showers-scattered-symbolic',
patchy_rain_possible: 'weather-showers-scattered-symbolic',
patchy_snow_possible: 'weather-snow-symbolic',
patchy_sleet_possible: 'weather-snow-symbolic',
patchy_freezing_drizzle_possible: 'weather-showers-scattered-symbolic',
thundery_outbreaks_possible: 'weather-overcast-symbolic',
blowing_snow: 'weather-snow-symbolic',
blizzard: 'weather-snow-symbolic',
fog: 'weather-fog-symbolic',
freezing_fog: 'weather-fog-symbolic',
patchy_light_drizzle: 'weather-showers-scattered-symbolic',
light_drizzle: 'weather-showers-symbolic',
freezing_drizzle: 'weather-showers-symbolic',
heavy_freezing_drizzle: 'weather-showers-symbolic',
patchy_light_rain: 'weather-showers-scattered-symbolic',
light_rain: 'weather-showers-symbolic',
moderate_rain_at_times: 'weather-showers-symbolic',
moderate_rain: 'weather-showers-symbolic',
heavy_rain_at_times: 'weather-showers-symbolic',
heavy_rain: 'weather-showers-symbolic',
light_freezing_rain: 'weather-showers-symbolic',
moderate_or_heavy_freezing_rain: 'weather-showers-symbolic',
light_sleet: 'weather-snow-symbolic',
moderate_or_heavy_sleet: 'weather-snow-symbolic',
patchy_light_snow: 'weather-snow-symbolic',
light_snow: 'weather-snow-symbolic',
patchy_moderate_snow: 'weather-snow-symbolic',
moderate_snow: 'weather-snow-symbolic',
patchy_heavy_snow: 'weather-snow-symbolic',
heavy_snow: 'weather-snow-symbolic',
ice_pellets: 'weather-showers-symbolic',
light_rain_shower: 'weather-showers-symbolic',
moderate_or_heavy_rain_shower: 'weather-showers-symbolic',
torrential_rain_shower: 'weather-showers-symbolic',
light_sleet_showers: 'weather-showers-symbolic',
moderate_or_heavy_sleet_showers: 'weather-showers-symbolic',
light_snow_showers: 'weather-snow-symbolic',
moderate_or_heavy_snow_showers: 'weather-snow-symbolic',
light_showers_of_ice_pellets: 'weather-showers-symbolic',
moderate_or_heavy_showers_of_ice_pellets: 'weather-showers-symbolic',
patchy_light_rain_with_thunder: 'weather-showers-scattered-symbolic',
moderate_or_heavy_rain_with_thunder: 'weather-showers-symbolic',
patchy_light_snow_with_thunder: 'weather-snow-symbolic',
moderate_or_heavy_snow_with_thunder: 'weather-snow-symbolic',
},
} as const;
================================================
FILE: .config/ags/modules/icons/weather.ts
================================================
export const weatherIcons = {
warning: '',
sunny: '',
clear: '',
partly_cloudy: '',
partly_cloudy_night: '',
cloudy: '',
overcast: '',
mist: '',
patchy_rain_nearby: '',
patchy_rain_possible: '',
patchy_snow_possible: '',
patchy_sleet_possible: '',
patchy_freezing_drizzle_possible: '',
thundery_outbreaks_possible: '',
blowing_snow: '',
blizzard: '',
fog: '',
freezing_fog: '',
patchy_light_drizzle: '',
light_drizzle: '',
freezing_drizzle: '',
heavy_freezing_drizzle: '',
patchy_light_rain: '',
light_rain: '',
moderate_rain_at_times: '',
moderate_rain: '',
heavy_rain_at_times: '',
heavy_rain: '',
light_freezing_rain: '',
moderate_or_heavy_freezing_rain: '',
light_sleet: '',
moderate_or_heavy_sleet: '',
patchy_light_snow: '',
light_snow: '',
patchy_moderate_snow: '',
moderate_snow: '',
patchy_heavy_snow: '',
heavy_snow: '',
ice_pellets: '',
light_rain_shower: '',
moderate_or_heavy_rain_shower: '',
torrential_rain_shower: '',
light_sleet_showers: '',
moderate_or_heavy_sleet_showers: '',
light_snow_showers: '',
moderate_or_heavy_snow_showers: '',
light_showers_of_ice_pellets: '',
moderate_or_heavy_showers_of_ice_pellets: '',
patchy_light_rain_with_thunder: '',
moderate_or_heavy_rain_with_thunder: '',
patchy_light_snow_with_thunder: '',
moderate_or_heavy_snow_with_thunder: '',
} as const;
================================================
FILE: .config/ags/modules/menus/audio/active/SelectedInput.ts
================================================
const audio = await Service.import('audio');
import { getIcon } from '../utils.js';
import Box from 'types/widgets/box.js';
import { Attribute, Child } from 'lib/types/widget.js';
const renderActiveInput = (): Box[] => {
return [
Widget.Box({
class_name: 'menu-slider-container input',
children: [
Widget.Button({
vexpand: false,
vpack: 'end',
setup: (self) => {
const updateClass = (): void => {
const mic = audio.microphone;
const className = `menu-active-button input ${mic.is_muted ? 'muted' : ''}`;
self.class_name = className;
};
self.hook(audio.microphone, updateClass, 'notify::is-muted');
},
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
child: Widget.Icon({
class_name: 'menu-active-icon input',
setup: (self) => {
const updateIcon = (): void => {
const isMicMuted =
audio.microphone.is_muted !== null ? audio.microphone.is_muted : true;
if (audio.microphone.volume > 0) {
self.icon = getIcon(audio.microphone.volume, isMicMuted)['mic'];
} else {
self.icon = getIcon(100, true)['mic'];
}
};
self.hook(audio.microphone, updateIcon, 'notify::volume');
self.hook(audio.microphone, updateIcon, 'notify::is-muted');
},
}),
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
class_name: 'menu-active input',
hpack: 'start',
truncate: 'end',
wrap: true,
label: audio.bind('microphone').as((v) => {
return v.description === null ? 'No input device found...' : v.description;
}),
}),
Widget.Slider({
value: audio.microphone.bind('volume').as((v) => v),
class_name: 'menu-active-slider menu-slider inputs',
draw_value: false,
hexpand: true,
min: 0,
max: 1,
onChange: ({ value }) => (audio.microphone.volume = value),
}),
],
}),
Widget.Label({
class_name: 'menu-active-percentage input',
vpack: 'end',
label: audio.microphone.bind('volume').as((v) => `${Math.round(v * 100)}%`),
}),
],
}),
];
};
export { renderActiveInput };
================================================
FILE: .config/ags/modules/menus/audio/active/SelectedPlayback.ts
================================================
const audio = await Service.import('audio');
import { getIcon } from '../utils.js';
import Box from 'types/widgets/box.js';
import { Attribute, Child } from 'lib/types/widget.js';
const renderActivePlayback = (): Box[] => {
return [
Widget.Box({
class_name: 'menu-slider-container playback',
children: [
Widget.Button({
vexpand: false,
vpack: 'end',
setup: (self) => {
const updateClass = (): void => {
const spkr = audio.speaker;
const className = `menu-active-button playback ${spkr.is_muted ? 'muted' : ''}`;
self.class_name = className;
};
self.hook(audio.speaker, updateClass, 'notify::is-muted');
},
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
child: Widget.Icon({
class_name: 'menu-active-icon playback',
setup: (self) => {
const updateIcon = (): void => {
const isSpeakerMuted = audio.speaker.is_muted !== null ? audio.speaker.is_muted : true;
self.icon = getIcon(audio.speaker.volume, isSpeakerMuted)['spkr'];
};
self.hook(audio.speaker, updateIcon, 'notify::volume');
self.hook(audio.speaker, updateIcon, 'notify::is-muted');
},
}),
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
class_name: 'menu-active playback',
hpack: 'start',
truncate: 'end',
expand: true,
wrap: true,
label: audio.bind('speaker').as((v) => v.description || ''),
}),
Widget.Slider({
value: audio['speaker'].bind('volume'),
class_name: 'menu-active-slider menu-slider playback',
draw_value: false,
hexpand: true,
min: 0,
max: 1,
onChange: ({ value }) => (audio.speaker.volume = value),
}),
],
}),
Widget.Label({
vpack: 'end',
class_name: 'menu-active-percentage playback',
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
}),
],
}),
];
};
export { renderActivePlayback };
================================================
FILE: .config/ags/modules/menus/audio/active/index.ts
================================================
import { renderActiveInput } from './SelectedInput.js';
import { renderActivePlayback } from './SelectedPlayback.js';
import Box from 'types/widgets/box.js';
import { Attribute, Child } from 'lib/types/widget.js';
const activeDevices = (): Box => {
return Widget.Box({
class_name: 'menu-section-container volume',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-label-container volume selected',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label audio volume',
hexpand: true,
hpack: 'start',
label: 'Volume',
}),
}),
Widget.Box({
class_name: 'menu-items-section selected',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-active-container playback',
vertical: true,
children: renderActivePlayback(),
}),
Widget.Box({
class_name: 'menu-active-container input',
vertical: true,
children: renderActiveInput(),
}),
],
}),
],
});
};
export { activeDevices };
================================================
FILE: .config/ags/modules/menus/audio/available/InputDevices.ts
================================================
const audio = await Service.import('audio');
import { InputDevices } from 'lib/types/audio';
import { Stream } from 'types/service/audio';
const renderInputDevices = (inputDevices: Stream[]): InputDevices => {
if (inputDevices.length === 0) {
return [
Widget.Button({
class_name: `menu-unfound-button input`,
child: Widget.Box({
children: [
Widget.Box({
hpack: 'start',
children: [
Widget.Label({
class_name: 'menu-button-name input',
label: 'No input devices found...',
}),
],
}),
],
}),
}),
];
}
return inputDevices.map((device) => {
return Widget.Button({
on_primary_click: () => (audio.microphone = device),
class_name: `menu-button audio input ${device}`,
child: Widget.Box({
children: [
Widget.Box({
hpack: 'start',
children: [
Widget.Label({
wrap: true,
class_name: audio.microphone
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-icon active input txt-icon'
: 'menu-button-icon input txt-icon',
),
label: '',
}),
Widget.Label({
truncate: 'end',
wrap: true,
class_name: audio.microphone
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-name active input'
: 'menu-button-name input',
),
label: device.description,
}),
],
}),
],
}),
});
});
};
export { renderInputDevices };
================================================
FILE: .config/ags/modules/menus/audio/available/PlaybackDevices.ts
================================================
const audio = await Service.import('audio');
import { PlaybackDevices } from 'lib/types/audio';
import { Stream } from 'types/service/audio';
const renderPlaybacks = (playbackDevices: Stream[]): PlaybackDevices => {
return playbackDevices.map((device) => {
if (device.description === 'Dummy Output') {
return Widget.Box({
class_name: 'menu-unfound-button playback',
child: Widget.Box({
children: [
Widget.Label({
class_name: 'menu-button-name playback',
label: 'No playback devices found...',
}),
],
}),
});
}
return Widget.Button({
class_name: `menu-button audio playback ${device}`,
on_primary_click: () => (audio.speaker = device),
child: Widget.Box({
children: [
Widget.Box({
hpack: 'start',
children: [
Widget.Label({
truncate: 'end',
wrap: true,
class_name: audio.speaker
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-icon active playback txt-icon'
: 'menu-button-icon playback txt-icon',
),
label: '',
}),
Widget.Label({
truncate: 'end',
wrap: true,
class_name: audio.speaker
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-name active playback'
: 'menu-button-name playback',
),
label: device.description,
}),
],
}),
],
}),
});
});
};
export { renderPlaybacks };
================================================
FILE: .config/ags/modules/menus/audio/available/index.ts
================================================
const audio = await Service.import('audio');
import { BoxWidget } from 'lib/types/widget.js';
import { renderInputDevices } from './InputDevices.js';
import { renderPlaybacks } from './PlaybackDevices.js';
const availableDevices = (): BoxWidget => {
return Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: 'menu-section-container playback',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-label-container playback',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label audio playback',
hexpand: true,
hpack: 'start',
label: 'Playback Devices',
}),
}),
Widget.Box({
class_name: 'menu-items-section playback',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-container playback',
vertical: true,
children: [
Widget.Box({
vertical: true,
children: audio.bind('speakers').as((v) => renderPlaybacks(v)),
}),
],
}),
],
}),
Widget.Box({
class_name: 'menu-label-container input',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label audio input',
hexpand: true,
hpack: 'start',
label: 'Input Devices',
}),
}),
Widget.Box({
class_name: 'menu-items-section input',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-container input',
vertical: true,
children: [
Widget.Box({
vertical: true,
children: audio.bind('microphones').as((v) => renderInputDevices(v)),
}),
],
}),
],
}),
],
}),
],
});
};
export { availableDevices };
================================================
FILE: .config/ags/modules/menus/audio/index.ts
================================================
import Window from 'types/widgets/window.js';
import DropdownMenu from '../shared/dropdown/index.js';
import { activeDevices } from './active/index.js';
import { availableDevices } from './available/index.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options.js';
export default (): Window => {
return DropdownMenu({
name: 'audiomenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'menu-items audio',
hpack: 'fill',
hexpand: true,
child: Widget.Box({
vertical: true,
hpack: 'fill',
hexpand: true,
class_name: 'menu-items-container audio',
children: [activeDevices(), availableDevices()],
}),
}),
});
};
================================================
FILE: .config/ags/modules/menus/audio/utils.ts
================================================
const speakerIcons = {
101: 'audio-volume-overamplified-symbolic',
66: 'audio-volume-high-symbolic',
34: 'audio-volume-medium-symbolic',
1: 'audio-volume-low-symbolic',
0: 'audio-volume-muted-symbolic',
} as const;
const inputIcons = {
101: 'microphone-sensitivity-high-symbolic',
66: 'microphone-sensitivity-high-symbolic',
34: 'microphone-sensitivity-medium-symbolic',
1: 'microphone-sensitivity-low-symbolic',
0: 'microphone-disabled-symbolic',
};
type IconVolumes = keyof typeof speakerIcons;
const getIcon = (audioVol: number, isMuted: boolean): Record => {
const thresholds: IconVolumes[] = [101, 66, 34, 1, 0];
const icon = isMuted ? 0 : thresholds.find((threshold) => threshold <= audioVol * 100) || 0;
return {
spkr: speakerIcons[icon],
mic: inputIcons[icon],
};
};
export { getIcon };
================================================
FILE: .config/ags/modules/menus/bluetooth/devices/connectedControls.ts
================================================
import { BoxWidget } from 'lib/types/widget';
import { BluetoothDevice } from 'types/service/bluetooth';
const connectedControls = (dev: BluetoothDevice, connectedDevices: BluetoothDevice[]): BoxWidget => {
if (!connectedDevices.includes(dev.address)) {
return Widget.Box({});
}
return Widget.Box({
vpack: 'start',
class_name: 'bluetooth-controls',
children: [
Widget.Button({
class_name: 'menu-icon-button unpair bluetooth',
child: Widget.Label({
tooltip_text: dev.paired ? 'Unpair' : 'Pair',
class_name: 'menu-icon-button-label unpair bluetooth txt-icon',
label: dev.paired ? '' : '',
}),
on_primary_click: () =>
Utils.execAsync([
'bash',
'-c',
`bluetoothctl ${dev.paired ? 'unpair' : 'pair'} ${dev.address}`,
]).catch((err) =>
console.error(`bluetoothctl ${dev.paired ? 'unpair' : 'pair'} ${dev.address}`, err),
),
}),
Widget.Button({
class_name: 'menu-icon-button disconnect bluetooth',
child: Widget.Label({
tooltip_text: dev.connected ? 'Disconnect' : 'Connect',
class_name: 'menu-icon-button-label disconnect bluetooth txt-icon',
label: dev.connected ? '' : '',
}),
on_primary_click: () => dev.setConnection(!dev.connected),
}),
Widget.Button({
class_name: 'menu-icon-button untrust bluetooth',
child: Widget.Label({
tooltip_text: dev.trusted ? 'Untrust' : 'Trust',
class_name: 'menu-icon-button-label untrust bluetooth txt-icon',
label: dev.trusted ? '' : '',
}),
on_primary_click: () =>
Utils.execAsync([
'bash',
'-c',
`bluetoothctl ${dev.trusted ? 'untrust' : 'trust'} ${dev.address}`,
]).catch((err) =>
console.error(`bluetoothctl ${dev.trusted ? 'untrust' : 'trust'} ${dev.address}`, err),
),
}),
Widget.Button({
class_name: 'menu-icon-button delete bluetooth',
child: Widget.Label({
tooltip_text: 'Forget',
class_name: 'menu-icon-button-label delete bluetooth txt-icon',
label: '',
}),
on_primary_click: () => {
Utils.execAsync(['bash', '-c', `bluetoothctl remove ${dev.address}`]).catch((err) =>
console.error('Bluetooth Remove', err),
);
},
}),
],
});
};
export { connectedControls };
================================================
FILE: .config/ags/modules/menus/bluetooth/devices/devicelist.ts
================================================
import { Bluetooth } from 'types/service/bluetooth.js';
import Box from 'types/widgets/box.js';
import { connectedControls } from './connectedControls.js';
import { getBluetoothIcon } from '../utils.js';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
import { Attribute, Child } from 'lib/types/widget.js';
const devices = (bluetooth: Bluetooth, self: Box): Box => {
return self.hook(bluetooth, () => {
if (!bluetooth.enabled) {
return (self.child = Widget.Box({
class_name: 'bluetooth-items',
vertical: true,
expand: true,
vpack: 'center',
hpack: 'center',
children: [
Widget.Label({
class_name: 'bluetooth-disabled dim',
hexpand: true,
label: 'Bluetooth is disabled',
}),
],
}));
}
const availableDevices = bluetooth.devices
.filter((btDev) => btDev.name !== null)
.sort((a, b) => {
if (a.connected || a.paired) {
return -1;
}
if (b.connected || b.paired) {
return 1;
}
return b.name - a.name;
});
const conDevNames = availableDevices.filter((d) => d.connected || d.paired).map((d) => d.address);
if (!availableDevices.length) {
return (self.child = Widget.Box({
class_name: 'bluetooth-items',
vertical: true,
expand: true,
vpack: 'center',
hpack: 'center',
children: [
Widget.Label({
class_name: 'no-bluetooth-devices dim',
hexpand: true,
label: 'No devices currently found',
}),
Widget.Label({
class_name: 'search-bluetooth-label dim',
hexpand: true,
label: "Press '' to search",
}),
],
}));
}
return (self.child = Widget.Box({
vertical: true,
children: availableDevices.map((device) => {
return Widget.Box({
children: [
Widget.Button({
hexpand: true,
class_name: `bluetooth-element-item ${device}`,
on_primary_click: () => {
if (!conDevNames.includes(device.address)) device.setConnection(true);
},
child: Widget.Box({
hexpand: true,
children: [
Widget.Box({
hexpand: true,
hpack: 'start',
class_name: 'menu-button-container',
children: [
Widget.Label({
vpack: 'start',
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? 'active' : ''} txt-icon`,
label: getBluetoothIcon(`${device['icon_name']}-symbolic`),
}),
Widget.Box({
vertical: true,
vpack: 'center',
children: [
Widget.Label({
vpack: 'center',
hpack: 'start',
class_name: 'menu-button-name bluetooth',
truncate: 'end',
wrap: true,
label: device.alias,
}),
Widget.Revealer({
hpack: 'start',
reveal_child: device.connected || device.paired,
child: Widget.Label({
hpack: 'start',
class_name: 'connection-status dim',
label: device.connected ? 'Connected' : 'Paired',
}),
}),
],
}),
],
}),
Widget.Box({
hpack: 'end',
children: device.connecting
? [
Widget.Spinner({
vpack: 'start',
class_name: 'spinner bluetooth',
}),
]
: [],
}),
],
}),
}),
connectedControls(device, conDevNames),
],
});
}),
}));
});
};
export { devices };
================================================
FILE: .config/ags/modules/menus/bluetooth/devices/index.ts
================================================
const bluetooth = await Service.import('bluetooth');
import { label } from './label.js';
import { devices } from './devicelist.js';
import { BoxWidget } from 'lib/types/widget.js';
const Devices = (): BoxWidget => {
return Widget.Box({
class_name: 'menu-section-container',
vertical: true,
children: [
label(bluetooth),
Widget.Box({
class_name: 'menu-items-section',
child: Widget.Box({
class_name: 'menu-content',
vertical: true,
setup: (self) => {
devices(bluetooth, self);
},
}),
}),
],
});
};
export { Devices };
================================================
FILE: .config/ags/modules/menus/bluetooth/devices/label.ts
================================================
import { BoxWidget } from 'lib/types/widget';
import { Bluetooth } from 'types/service/bluetooth';
const label = (bluetooth: Bluetooth): BoxWidget => {
const searchInProgress = Variable(false);
const startRotation = (): void => {
searchInProgress.value = true;
setTimeout(() => {
searchInProgress.value = false;
}, 10 * 1000);
};
return Widget.Box({
class_name: 'menu-label-container',
hpack: 'fill',
vpack: 'start',
children: [
Widget.Label({
class_name: 'menu-label',
vpack: 'center',
hpack: 'start',
label: 'Bluetooth',
}),
Widget.Box({
class_name: 'controls-container',
vpack: 'start',
children: [
Widget.Switch({
class_name: 'menu-switch bluetooth',
hexpand: true,
hpack: 'end',
active: bluetooth.bind('enabled'),
on_activate: ({ active }) => {
searchInProgress.value = false;
Utils.execAsync(['bash', '-c', `bluetoothctl power ${active ? 'on' : 'off'}`]).catch(
(err) => console.error(`bluetoothctl power ${active ? 'on' : 'off'}`, err),
);
},
}),
Widget.Separator({
class_name: 'menu-separator bluetooth',
}),
Widget.Button({
vpack: 'center',
class_name: 'menu-icon-button search',
on_primary_click: () => {
startRotation();
Utils.execAsync(['bash', '-c', 'bluetoothctl --timeout 120 scan on']).catch((err) => {
searchInProgress.value = false;
console.error('bluetoothctl --timeout 120 scan on', err);
});
},
child: Widget.Icon({
class_name: searchInProgress.bind('value').as((v) => (v ? 'spinning' : '')),
icon: 'view-refresh-symbolic',
}),
}),
],
}),
],
});
};
export { label };
================================================
FILE: .config/ags/modules/menus/bluetooth/index.ts
================================================
import Window from 'types/widgets/window.js';
import DropdownMenu from '../shared/dropdown/index.js';
import { Devices } from './devices/index.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options.js';
export default (): Window => {
return DropdownMenu({
name: 'bluetoothmenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'menu-items bluetooth',
hpack: 'fill',
hexpand: true,
child: Widget.Box({
vertical: true,
hpack: 'fill',
hexpand: true,
class_name: 'menu-items-container bluetooth',
child: Devices(),
}),
}),
});
};
================================================
FILE: .config/ags/modules/menus/bluetooth/utils.ts
================================================
const getBluetoothIcon = (iconName: string): string => {
const deviceIconMap = [
['^audio-card*', ''],
['^audio-headphones*', ''],
['^audio-headset*', ''],
['^audio-input*', ''],
['^audio-speakers*', ''],
['^bluetooth*', ''],
['^camera*', ''],
['^computer*', ''],
['^input-gaming*', ''],
['^input-keyboard*', ''],
['^input-mouse*', ''],
['^input-tablet*', ''],
['^media*', ''],
['^modem*', ''],
['^network*', ''],
['^phone*', ''],
['^printer*', ''],
['^scanner*', ''],
['^video-camera*', ''],
];
const foundMatch = deviceIconMap.find((icon) => RegExp(icon[0]).test(iconName.toLowerCase()));
return foundMatch ? foundMatch[1] : '';
};
export { getBluetoothIcon };
================================================
FILE: .config/ags/modules/menus/calendar/calendar.ts
================================================
import { BoxWidget } from 'lib/types/widget';
const CalendarWidget = (): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-item-container calendar',
hpack: 'fill',
vpack: 'fill',
expand: true,
child: Widget.Box({
class_name: 'calendar-container-box',
child: Widget.Calendar({
expand: true,
hpack: 'fill',
vpack: 'fill',
class_name: 'calendar-menu-widget',
showDayNames: true,
showDetails: false,
showHeading: true,
}),
}),
});
};
export { CalendarWidget };
================================================
FILE: .config/ags/modules/menus/calendar/index.ts
================================================
import DropdownMenu from 'modules/menus/shared/dropdown/index';
import { TimeWidget } from './time/index';
import { CalendarWidget } from './calendar';
import { WeatherWidget } from './weather/index';
import options from 'options';
import Window from 'types/widgets/window';
import { Attribute, Child } from 'lib/types/widget';
const { enabled: weatherEnabled } = options.menus.clock.weather;
export default (): Window => {
return DropdownMenu({
name: 'calendarmenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'calendar-menu-content',
css: 'padding: 1px; margin: -1px;',
vexpand: false,
children: [
Widget.Box({
class_name: 'calendar-content-container',
vertical: true,
children: [
Widget.Box({
class_name: 'calendar-content-items',
vertical: true,
children: weatherEnabled.bind('value').as((isWeatherEnabled) => {
return [TimeWidget(), CalendarWidget(), ...(isWeatherEnabled ? [WeatherWidget()] : [])];
}),
}),
],
}),
],
}),
});
};
================================================
FILE: .config/ags/modules/menus/calendar/time/index.ts
================================================
import { BoxWidget } from 'lib/types/widget';
import options from 'options';
const { military, hideSeconds } = options.menus.clock.time;
const time = Variable('', {
poll: [1000, 'date "+%I:%M:%S"'],
});
const period = Variable('', {
poll: [1000, 'date "+%p"'],
});
const militaryTime = Variable('', {
poll: [1000, 'date "+%H:%M:%S"'],
});
const TimeWidget = (): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-item-container clock',
hexpand: true,
vpack: 'center',
hpack: 'fill',
child: Widget.Box({
hexpand: true,
vpack: 'center',
hpack: 'center',
class_name: 'clock-content-items',
children: Utils.merge(
[military.bind('value'), hideSeconds.bind('value')],
(is24hr: boolean, hideSeconds: boolean) => {
if (!is24hr) {
return [
Widget.Box({
hpack: 'center',
children: [
Widget.Label({
class_name: 'clock-content-time',
label: hideSeconds ? time.bind().as((str) => str.slice(0, -3)) : time.bind(),
}),
],
}),
Widget.Box({
hpack: 'center',
children: [
Widget.Label({
vpack: 'end',
class_name: 'clock-content-period',
label: period.bind(),
}),
],
}),
];
}
return [
Widget.Box({
hpack: 'center',
children: [
Widget.Label({
class_name: 'clock-content-time',
label: hideSeconds
? militaryTime.bind().as((str) => str.slice(0, -3))
: militaryTime.bind(),
}),
],
}),
];
},
),
}),
});
};
export { TimeWidget };
================================================
FILE: .config/ags/modules/menus/calendar/weather/hourly/icon/index.ts
================================================
import { Weather, WeatherIconTitle } from 'lib/types/weather.js';
import { Variable } from 'types/variable.js';
import { weatherIcons } from 'modules/icons/weather.js';
import { isValidWeatherIconTitle } from 'globals/weather';
import { BoxWidget } from 'lib/types/widget';
import { getNextEpoch } from '../utils';
export const HourlyIcon = (theWeather: Variable, hoursFromNow: number): BoxWidget => {
const getIconQuery = (wthr: Weather): WeatherIconTitle => {
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
if (weatherAtEpoch === undefined) {
return 'warning';
}
let iconQuery = weatherAtEpoch.condition.text.trim().toLowerCase().replaceAll(' ', '_');
if (!weatherAtEpoch?.is_day && iconQuery === 'partly_cloudy') {
iconQuery = 'partly_cloudy_night';
}
if (isValidWeatherIconTitle(iconQuery)) {
return iconQuery;
} else {
return 'warning';
}
};
return Widget.Box({
hpack: 'center',
child: theWeather.bind('value').as((w) => {
const iconQuery = getIconQuery(w);
const weatherIcn = weatherIcons[iconQuery] || weatherIcons['warning'];
return Widget.Label({
hpack: 'center',
class_name: 'hourly-weather-icon txt-icon',
label: weatherIcn,
});
}),
});
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/hourly/index.ts
================================================
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import { HourlyIcon } from './icon/index.js';
import { HourlyTemp } from './temperature/index.js';
import { HourlyTime } from './time/index.js';
import { BoxWidget } from 'lib/types/widget.js';
export const Hourly = (theWeather: Variable): BoxWidget => {
return Widget.Box({
vertical: false,
hexpand: true,
hpack: 'fill',
class_name: 'hourly-weather-container',
children: [1, 2, 3, 4].map((hoursFromNow) => {
return Widget.Box({
class_name: 'hourly-weather-item',
hexpand: true,
vertical: true,
children: [
HourlyTime(theWeather, hoursFromNow),
HourlyIcon(theWeather, hoursFromNow),
HourlyTemp(theWeather, hoursFromNow),
],
});
}),
});
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/hourly/temperature/index.ts
================================================
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import options from 'options';
import Label from 'types/widgets/label';
import { Child } from 'lib/types/widget';
import { getNextEpoch } from '../utils';
const { unit } = options.menus.clock.weather;
export const HourlyTemp = (theWeather: Variable, hoursFromNow: number): Label => {
return Widget.Label({
class_name: 'hourly-weather-temp',
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
if (!Object.keys(wthr).length) {
return '-';
}
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
if (unt === 'imperial') {
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : '-'}° F`;
}
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : '-'}° C`;
}),
});
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/hourly/time/index.ts
================================================
import { Weather } from 'lib/types/weather';
import { Child } from 'lib/types/widget';
import { Variable } from 'types/variable';
import Label from 'types/widgets/label';
import { getNextEpoch } from '../utils';
export const HourlyTime = (theWeather: Variable, hoursFromNow: number): Label => {
return Widget.Label({
class_name: 'hourly-weather-time',
label: theWeather.bind('value').as((w) => {
if (!Object.keys(w).length) {
return '-';
}
const nextEpoch = getNextEpoch(w, hoursFromNow);
const dateAtEpoch = new Date(nextEpoch * 1000);
let hours = dateAtEpoch.getHours();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
return `${hours}${ampm}`;
}),
});
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/hourly/utils.ts
================================================
import { Weather } from 'lib/types/weather';
export const getNextEpoch = (wthr: Weather, hoursFromNow: number): number => {
const currentEpoch = wthr.location.localtime_epoch;
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
const curHour = new Date(currentEpoch * 1000).getHours();
/*
* NOTE: Since the API is only capable of showing the current day; if
* the hours left in the day are less than 4 (aka spilling into the next day),
* then rewind to contain the prediction within the current day.
*/
if (curHour > 19) {
const hoursToRewind = curHour - 19;
nextEpoch = 3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600;
}
return nextEpoch;
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/icon/index.ts
================================================
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import { getWeatherStatusTextIcon } from 'globals/weather.js';
import { BoxWidget } from 'lib/types/widget';
export const TodayIcon = (theWeather: Variable): BoxWidget => {
return Widget.Box({
vpack: 'center',
hpack: 'start',
class_name: 'calendar-menu-weather today icon container',
child: Widget.Label({
class_name: 'calendar-menu-weather today icon txt-icon',
label: theWeather.bind('value').as((w) => {
return getWeatherStatusTextIcon(w);
}),
}),
});
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/index.ts
================================================
import { TodayIcon } from './icon/index.js';
import { TodayStats } from './stats/index.js';
import { TodayTemperature } from './temperature/index.js';
import { Hourly } from './hourly/index.js';
import { globalWeatherVar } from 'globals/weather.js';
import { BoxWidget } from 'lib/types/widget.js';
const WeatherWidget = (): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-item-container weather',
child: Widget.Box({
class_name: 'weather-container-box',
setup: (self) => {
return (self.child = Widget.Box({
vertical: true,
hexpand: true,
children: [
Widget.Box({
class_name: 'calendar-menu-weather today',
hexpand: true,
children: [
TodayIcon(globalWeatherVar),
TodayTemperature(globalWeatherVar),
TodayStats(globalWeatherVar),
],
}),
Widget.Separator({
class_name: 'menu-separator weather',
}),
Hourly(globalWeatherVar),
],
}));
},
}),
});
};
export { WeatherWidget };
================================================
FILE: .config/ags/modules/menus/calendar/weather/stats/index.ts
================================================
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import options from 'options';
import { Unit } from 'lib/types/options';
import { getRainChance, getWindConditions } from 'globals/weather';
import { BoxWidget } from 'lib/types/widget';
const { unit } = options.menus.clock.weather;
export const TodayStats = (theWeather: Variable): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-weather today stats container',
hpack: 'end',
vpack: 'center',
vertical: true,
children: [
Widget.Box({
class_name: 'weather wind',
children: [
Widget.Label({
class_name: 'weather wind icon txt-icon',
label: '',
}),
Widget.Label({
class_name: 'weather wind label',
label: Utils.merge(
[theWeather.bind('value'), unit.bind('value')],
(wthr: Weather, unt: Unit) => {
return getWindConditions(wthr, unt);
},
),
}),
],
}),
Widget.Box({
class_name: 'weather precip',
children: [
Widget.Label({
class_name: 'weather precip icon txt-icon',
label: '',
}),
Widget.Label({
class_name: 'weather precip label',
label: theWeather.bind('value').as((v) => getRainChance(v)),
}),
],
}),
],
});
};
================================================
FILE: .config/ags/modules/menus/calendar/weather/temperature/index.ts
================================================
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import options from 'options';
import { getTemperature, getWeatherIcon } from 'globals/weather';
import { BoxWidget } from 'lib/types/widget';
const { unit } = options.menus.clock.weather;
export const TodayTemperature = (theWeather: Variable): BoxWidget => {
return Widget.Box({
hpack: 'center',
vpack: 'center',
vertical: true,
children: [
Widget.Box({
hexpand: true,
vpack: 'center',
class_name: 'calendar-menu-weather today temp container',
vertical: false,
children: [
Widget.Box({
hexpand: true,
hpack: 'center',
children: [
Widget.Label({
class_name: 'calendar-menu-weather today temp label',
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
return getTemperature(wthr, unt);
}),
}),
Widget.Label({
class_name: theWeather
.bind('value')
.as(
(v) =>
`calendar-menu-weather today temp label icon txt-icon ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
),
label: theWeather
.bind('value')
.as((v) => getWeatherIcon(Math.ceil(v.current.temp_f)).icon),
}),
],
}),
],
}),
Widget.Box({
hpack: 'center',
child: Widget.Label({
max_width_chars: 17,
truncate: 'end',
lines: 2,
class_name: theWeather
.bind('value')
.as(
(v) =>
`calendar-menu-weather today condition label ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
),
label: theWeather.bind('value').as((v) => v.current.condition.text),
}),
}),
],
});
};
================================================
FILE: .config/ags/modules/menus/dashboard/controls/index.ts
================================================
import { BoxWidget } from 'lib/types/widget';
const network = await Service.import('network');
const bluetooth = await Service.import('bluetooth');
const notifications = await Service.import('notifications');
const audio = await Service.import('audio');
const Controls = (): BoxWidget => {
return Widget.Box({
class_name: 'dashboard-card controls-container',
hpack: 'fill',
vpack: 'fill',
expand: true,
children: [
Widget.Button({
tooltip_text: 'Toggle Wifi',
expand: true,
setup: (self) => {
self.hook(network, () => {
return (self.class_name = `dashboard-button wifi ${!network.wifi.enabled ? 'disabled' : ''}`);
});
},
on_primary_click: () => network.toggleWifi(),
child: Widget.Label({
class_name: 'txt-icon',
setup: (self) => {
self.hook(network, () => {
return (self.label = network.wifi.enabled ? '' : '');
});
},
}),
}),
Widget.Button({
tooltip_text: 'Toggle Bluetooth',
expand: true,
class_name: bluetooth
.bind('enabled')
.as((btOn) => `dashboard-button bluetooth ${!btOn ? 'disabled' : ''}`),
on_primary_click: () => bluetooth.toggle(),
child: Widget.Label({
class_name: 'txt-icon',
label: bluetooth.bind('enabled').as((btOn) => (btOn ? '' : '')),
}),
}),
Widget.Button({
tooltip_text: 'Toggle Notifications',
expand: true,
class_name: notifications
.bind('dnd')
.as((dnd) => `dashboard-button notifications ${dnd ? 'disabled' : ''}`),
on_primary_click: () => (notifications.dnd = !notifications.dnd),
child: Widget.Label({
class_name: 'txt-icon',
label: notifications.bind('dnd').as((dnd) => (dnd ? '' : '')),
}),
}),
Widget.Button({
tooltip_text: 'Toggle Mute (Playback)',
expand: true,
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
setup: (self) => {
self.hook(
audio.speaker,
() => {
return (self.class_name = `dashboard-button playback ${audio.speaker.is_muted ? 'disabled' : ''}`);
},
'notify::is-muted',
);
},
child: Widget.Label({
class_name: 'txt-icon',
setup: (self) => {
self.hook(
audio.speaker,
() => {
return (self.label = audio.speaker.is_muted ? '' : '');
},
'notify::is-muted',
);
},
}),
}),
Widget.Button({
tooltip_text: 'Toggle Mute (Microphone)',
expand: true,
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
setup: (self) => {
self.hook(
audio.microphone,
() => {
return (self.class_name = `dashboard-button input ${audio.microphone.is_muted ? 'disabled' : ''}`);
},
'notify::is-muted',
);
},
child: Widget.Label({
class_name: 'txt-icon',
setup: (self) => {
self.hook(
audio.microphone,
() => {
return (self.label = audio.microphone.is_muted ? '' : '');
},
'notify::is-muted',
);
},
}),
}),
],
});
};
export { Controls };
================================================
FILE: .config/ags/modules/menus/dashboard/directories/index.ts
================================================
import { BoxWidget } from 'lib/types/widget';
import options from 'options';
const { left, right } = options.menus.dashboard.directories;
const Directories = (): BoxWidget => {
return Widget.Box({
class_name: 'dashboard-card directories-container',
vpack: 'fill',
hpack: 'fill',
expand: true,
children: [
Widget.Box({
vertical: true,
expand: true,
class_name: 'section right',
children: [
Widget.Button({
hpack: 'start',
expand: true,
class_name: 'directory-link left top',
on_primary_click: left.directory1.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: left.directory1.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link left middle',
on_primary_click: left.directory2.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: left.directory2.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link left bottom',
on_primary_click: left.directory3.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: left.directory3.label.bind('value'),
}),
}),
],
}),
Widget.Box({
vertical: true,
expand: true,
class_name: 'section left',
children: [
Widget.Button({
hpack: 'start',
expand: true,
class_name: 'directory-link right top',
on_primary_click: right.directory1.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: right.directory1.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link right middle',
on_primary_click: right.directory2.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: right.directory2.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link right bottom',
on_primary_click: right.directory3.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: right.directory3.label.bind('value'),
}),
}),
],
}),
],
});
};
export { Directories };
================================================
FILE: .config/ags/modules/menus/dashboard/index.ts
================================================
import DropdownMenu from '../shared/dropdown/index.js';
import { Profile } from './profile/index.js';
import { Shortcuts } from './shortcuts/index.js';
import { Controls } from './controls/index.js';
import { Stats } from './stats/index.js';
import { Directories } from './directories/index.js';
import Window from 'types/widgets/window.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options.js';
const { controls, shortcuts, stats, directories } = options.menus.dashboard;
export default (): Window => {
return DropdownMenu({
name: 'dashboardmenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'dashboard-menu-content',
css: 'padding: 1px; margin: -1px;',
vexpand: false,
children: [
Widget.Box({
class_name: 'dashboard-content-container',
vertical: true,
children: Utils.merge(
[
controls.enabled.bind('value'),
shortcuts.enabled.bind('value'),
stats.enabled.bind('value'),
directories.enabled.bind('value'),
],
(isControlsEnabled, isShortcutsEnabled, isStatsEnabled, isDirectoriesEnabled) => {
return [
Widget.Box({
class_name: 'dashboard-content-items',
vertical: true,
children: [
Profile(),
...(isShortcutsEnabled ? [Shortcuts()] : []),
...(isControlsEnabled ? [Controls()] : []),
...(isDirectoriesEnabled ? [Directories()] : []),
...(isStatsEnabled ? [Stats()] : []),
],
}),
];
},
),
}),
],
}),
});
};
================================================
FILE: .config/ags/modules/menus/dashboard/profile/index.ts
================================================
import powermenu from '../../power/helpers/actions.js';
import { PowerOptions } from 'lib/types/options.js';
import GdkPixbuf from 'gi://GdkPixbuf';
import options from 'options';
import { BoxWidget, Child } from 'lib/types/widget.js';
import Label from 'types/widgets/label.js';
const { image, name } = options.menus.dashboard.powermenu.avatar;
const { confirmation, shutdown, logout, sleep, reboot } = options.menus.dashboard.powermenu;
const Profile = (): BoxWidget => {
const handleClick = (action: PowerOptions): void => {
const actions = {
shutdown: shutdown.value,
reboot: reboot.value,
logout: logout.value,
sleep: sleep.value,
};
App.closeWindow('dashboardmenu');
if (!confirmation.value) {
Utils.execAsync(actions[action]).catch((err) =>
console.error(`Failed to execute ${action} command. Error: ${err}`),
);
} else {
powermenu.action(action);
}
};
const getIconForButton = (txtIcon: string): Label => {
return Widget.Label({
className: 'txt-icon',
label: txtIcon,
});
};
return Widget.Box({
class_name: 'profiles-container',
hpack: 'fill',
hexpand: true,
children: [
Widget.Box({
class_name: 'profile-picture-container dashboard-card',
hexpand: true,
vertical: true,
children: [
Widget.Box({
hpack: 'center',
class_name: 'profile-picture',
css: image.bind('value').as((i) => {
try {
GdkPixbuf.Pixbuf.new_from_file(i);
return `background-image: url("${i}")`;
} catch {
return `background-image: url("${App.configDir}/assets/hyprpanel.png")`;
}
}),
}),
Widget.Label({
hpack: 'center',
class_name: 'profile-name',
label: name.bind('value').as((v) => {
if (v === 'system') {
return Utils.exec('bash -c whoami');
}
return v;
}),
}),
],
}),
Widget.Box({
class_name: 'power-menu-container dashboard-card',
vertical: true,
vexpand: true,
children: [
Widget.Button({
class_name: 'dashboard-button shutdown',
on_clicked: () => handleClick('shutdown'),
tooltip_text: 'Shut Down',
vexpand: true,
child: getIconForButton(''),
}),
Widget.Button({
class_name: 'dashboard-button restart',
on_clicked: () => handleClick('reboot'),
tooltip_text: 'Restart',
vexpand: true,
child: getIconForButton(''),
}),
Widget.Button({
class_name: 'dashboard-button lock',
on_clicked: () => handleClick('logout'),
tooltip_text: 'Log Out',
vexpand: true,
child: getIconForButton(''),
}),
Widget.Button({
class_name: 'dashboard-button sleep',
on_clicked: () => handleClick('sleep'),
tooltip_text: 'Sleep',
vexpand: true,
child: getIconForButton(''),
}),
],
}),
],
});
};
export { Profile };
================================================
FILE: .config/ags/modules/menus/dashboard/shortcuts/index.ts
================================================
const hyprland = await Service.import('hyprland');
import { Attribute, BoxWidget, Child } from 'lib/types/widget';
import options from 'options';
import { Variable as VarType } from 'types/variable';
import Box from 'types/widgets/box';
import Button from 'types/widgets/button';
import Label from 'types/widgets/label';
const { left, right } = options.menus.dashboard.shortcuts;
const Shortcuts = (): BoxWidget => {
const isRecording = Variable(false, {
poll: [
1000,
`${App.configDir}/services/screen_record.sh status`,
(out): boolean => {
if (out === 'recording') {
return true;
}
return false;
},
],
});
const handleClick = (action: string, tOut: number = 250): void => {
App.closeWindow('dashboardmenu');
setTimeout(() => {
Utils.execAsync(action)
.then((res) => {
return res;
})
.catch((err) => err);
}, tOut);
};
const recordingDropdown = Widget.Menu({
class_name: 'dropdown recording',
hpack: 'fill',
hexpand: true,
setup: (self) => {
const renderMonitorList = (): void => {
const displays = hyprland.monitors.map((mon) => {
return Widget.MenuItem({
label: `Display ${mon.name}`,
on_activate: () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(`${App.configDir}/services/screen_record.sh start ${mon.name}`).catch(
(err) => console.error(err),
);
},
});
});
// NOTE: This is disabled since window recording isn't available on wayland
// const apps = hyprland.clients.map((clt) => {
// return Widget.MenuItem({
// label: `${clt.class.charAt(0).toUpperCase() + clt.class.slice(1)} (Workspace ${clt.workspace.name})`,
// on_activate: () => {
// App.closeWindow('dashboardmenu');
// Utils.execAsync(
// `${App.configDir}/services/screen_record.sh start ${clt.focusHistoryID}`,
// ).catch((err) => console.error(err));
// },
// });
// });
self.children = [
...displays,
// Disabled since window recording isn't available on wayland
// ...apps
];
};
self.hook(hyprland, renderMonitorList, 'monitor-added');
self.hook(hyprland, renderMonitorList, 'monitor-removed');
},
});
type ShortcutFixed = {
tooltip: string;
command: string;
icon: string;
configurable: false;
};
type ShortcutVariable = {
tooltip: VarType;
command: VarType;
icon: VarType;
configurable?: true;
};
type Shortcut = ShortcutFixed | ShortcutVariable;
const cmdLn = (sCut: ShortcutVariable): boolean => {
return sCut.command.value.length > 0;
};
const leftCardHidden = Variable(
!(cmdLn(left.shortcut1) || cmdLn(left.shortcut2) || cmdLn(left.shortcut3) || cmdLn(left.shortcut4)),
);
const createButton = (shortcut: Shortcut, className: string): Button