├── .envrc ├── .github └── workflows │ └── build_and_release.yml ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── cli ├── cli.vala └── meson.build ├── cliff.toml ├── data ├── assets │ ├── NavBar │ │ ├── browser-symbolic.svg │ │ ├── code-symbolic.svg │ │ ├── docs-symbolic.svg │ │ ├── explorer-symbolic.svg │ │ ├── gaming-symbolic.svg │ │ ├── media-symbolic.svg │ │ ├── settings-symbolic.svg │ │ ├── social-symbolic.svg │ │ └── terminal-symbolic.svg │ ├── NoNotif.png │ ├── System │ │ ├── hibernate-symbolic.svg │ │ ├── lockscreen-symbolic.svg │ │ ├── logout-symbolic.svg │ │ ├── reboot-symbolic.svg │ │ ├── shutdown-symbolic.svg │ │ └── suspend-symbolic.svg │ ├── colloid-notif-sound.opus │ └── wyvern-svgrepo-com.svg ├── desktop │ ├── com.github.ARKye03.morghulis.desktop.in │ ├── com.github.ARKye03.morghulis.gschema.xml │ ├── com.github.ARKye03.morghulis.metainfo.xml.in │ └── meson.build ├── meson.build ├── morghulis.gresource.xml ├── po │ ├── LINGUAS │ └── POTFILES.in ├── scss │ ├── _NavBar.scss │ ├── _Notif.scss │ ├── _OnScreenDisplay.scss │ ├── _adw.scss │ ├── main.scss │ └── meson.build └── ui │ ├── BatteryBox.blp │ ├── MprisPlayer.blp │ ├── NavBar.blp │ ├── NotificationItem.blp │ ├── OnScreenDisplay.blp │ ├── PowerBox.blp │ ├── PowerMenu.blp │ ├── QAudioBox.blp │ ├── QAudioItem.blp │ ├── QBluetooth.blp │ ├── QBluetoothItem.blp │ ├── QButton.blp │ ├── QNetwork.blp │ ├── QNotifications.blp │ ├── QuickMenu.blp │ ├── Runner.blp │ ├── RunnerButton.blp │ ├── Settings.blp │ └── meson.build ├── flake.lock ├── flake.nix ├── justfile ├── lib ├── Backlight │ ├── backlight.rules │ ├── backlight.vala │ ├── meson.build │ └── reload_udev_rules.sh ├── Math │ ├── ast.h │ ├── evaluator.c │ ├── evaluator.h │ ├── lexer.c │ ├── lexer.h │ ├── meson.build │ ├── mpars.c │ ├── mpars.h │ ├── parser.c │ └── parser.h └── meson.build ├── meson.build ├── public └── morghulis.webp ├── scripts ├── ensureTypes.awk └── genVersion.sh ├── src ├── App.vala ├── Main.vala ├── Modules │ ├── CircularProgress.vala │ ├── Gizmo.vala │ ├── ScrollingLabel.vala │ └── meson.build ├── NavBar │ ├── HyprWorkspaces.vala │ ├── NavBar.vala │ ├── RiverTags.vala │ └── meson.build ├── Notifications │ ├── NotificationItem.vala │ ├── NotificationPopupsCenter.vala │ ├── QNotifications.vala │ └── meson.build ├── OnScreenDisplay │ ├── OnScreenDisplay.vala │ └── meson.build ├── PowerMenu │ ├── PowerMenu.vala │ └── meson.build ├── QuickMenu │ ├── BatteryBox.vala │ ├── MprisPlayer.vala │ ├── PowerBox.vala │ ├── QAudioBox.vala │ ├── QAudioItem.vala │ ├── QBluetooth.vala │ ├── QBluetoothItem.vala │ ├── QButton.vala │ ├── QNetwork.vala │ ├── QuickMenu.vala │ ├── Settings.vala │ ├── SysInfo.vala │ └── meson.build ├── Runner │ ├── Runner.vala │ ├── RunnerButton.vala │ └── meson.build ├── Tray │ ├── Tray.vala │ └── meson.build ├── meson.build └── vapi │ └── libgtop-2.0.vapi ├── uncrustify.cfg ├── vala-lint.conf └── version /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.github/workflows/build_and_release.yml: -------------------------------------------------------------------------------- 1 | name: "Build and Release" 2 | on: 3 | pull_request: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: cachix/install-nix-action@v30 15 | with: 16 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 17 | - uses: cachix/cachix-action@v15 18 | with: 19 | name: nixarkye03 20 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 21 | - name: Extract tag version 22 | id: get_version 23 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 24 | - run: nix build .#tarball 25 | - run: nix flake check 26 | - uses: ncipollo/release-action@v1.14.0 27 | with: 28 | prerelease: true 29 | artifactErrorsFailBuild: true 30 | artifacts: "result/morghulis-${{ steps.get_version.outputs.VERSION }}.tar.xz" 31 | bodyFile: "CHANGELOG.md" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/tasks.json 3 | !.vscode/launch.json 4 | !.vscode/extensions.json 5 | !.vscode/*.code-snippets 6 | 7 | # Local History for Visual Studio Code 8 | .history/ 9 | 10 | # Built Visual Studio Code Extensions 11 | *.vsix 12 | 13 | # Linux 14 | *~ 15 | 16 | # temporary files which can be created if a process still has a handle open of a deleted file 17 | .fuse_hidden* 18 | 19 | # KDE directory preferences 20 | .directory 21 | 22 | # Linux trash folder which might appear on any partition or disk 23 | .Trash-* 24 | 25 | # .nfs files are created when an open file is removed but is still being accessed 26 | .nfs* 27 | 28 | .direnv/ 29 | result 30 | .vscode/settings.json 31 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "tamasfe.even-better-toml", 4 | "bierner.github-markdown-preview", 5 | "bodil.blueprint-gtk", 6 | "mesonbuild.mesonbuild", 7 | "prince781.vala", 8 | "nefrob.vscode-just-syntax" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceFolder}/build/src/morghulis", 12 | "args": [], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Morghulis 2 | 3 | - [Morghulis](#morghulis) 4 | - [Requirements](#requirements) 5 | - [Installation](#installation) 6 | - [From source](#from-source) 7 | - [Arch Linux](#arch-linux) 8 | - [Usage](#usage) 9 | - [Style](#style) 10 | - [Development](#development) 11 | - [Nix](#nix) 12 | - [Features](#features) 13 | - [Preview](#preview) 14 | - [Thanks to](#thanks-to) 15 | - [License](#license) 16 | 17 | Desktop Shell created with GTK4, Libadwaita, and Astal. 18 | 19 | ## Requirements 20 | 21 | - [River](https://codeberg.org/river/river/), or [Hyprland](https://hyprland.org/) 22 | - [Vala](https://vala.dev/), [Meson](https://mesonbuild.com/), [Just](https://github.com/casey/just) 23 | - [Libadwaita](https://gitlab.gnome.org/GNOME/libadwaita) & Adwaita Theme. 24 | - [Blueprint-Compiler](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) 25 | - [GTK](https://www.gtk.org/) 26 | - [gtk4](https://docs.gtk.org/gtk4/) 27 | - [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) 28 | - [GSound](https://gitlab.gnome.org/GNOME/gsound) 29 | - [Astal](https://github.com/Aylur/astal) 30 | - 4 31 | - Tray 32 | - IO 33 | - Wireplumber 34 | - Mpris 35 | - NotifD 36 | - Network 37 | - Bluetooth 38 | - Apps 39 | - River (Optional) 40 | - Hyprland (Optional) 41 | -
Battery 42 | While it might not be used, its mandatory to install it (For now). 43 |
44 | -
Power Profiles 45 | While it might not be used, its mandatory to install it (For now). 46 |
47 | - [libgtop](https://gitlab.gnome.org/GNOME/libgtop) 48 | 49 | > [!NOTE] 50 | > Optional dependencies are not required only if built from source; the binary release requires all. 51 | 52 | ## Installation 53 | 54 | ### From source 55 | 56 | ```shell 57 | git clone https://github.com/ARKye03/morghulis 58 | cd morghulis 59 | just init 60 | just install 61 | ``` 62 | 63 | ### Arch Linux 64 | 65 | Build and install using my `PKGBUILD` file: 66 | 67 | ```sh 68 | mkdir /tmp/morghulis && cd /tmp/morghulis 69 | wget https://raw.githubusercontent.com/ARKye03/PKGBUILDS/refs/heads/main/morghulis-git/PKGBUILD 70 | makepkg -si 71 | ``` 72 | 73 | Alternatively, use a binary from [releases](https://github.com/ARKye03/morghulis/releases). 74 | 75 | ## Usage 76 | 77 | Morghulis is a desktop shell that uses Astal under the hood, so the Astal CLI is available to use via `astal -i morghulis `; nevertheless, the `morghulis-cli` called `morghulctl` is dedicated to this project itself. 78 | 79 | ```shell 80 | morghulctl --help 81 | ``` 82 | 83 | > [!NOTE] 84 | > The CLI currently offers simple commands to start the application, toggle windows, and show the inspector. 85 | 86 | ### Style 87 | 88 | You can change the style of Morghulis by creating the file `$XDG_CONFIG_HOME/morghulis/main.css`. _Hot Reload_ is supported. As previously mentioned, an Adwaita theme is encouraged. 89 | 90 | ## Development 91 | 92 | ```shell 93 | just init 94 | just 95 | ``` 96 | 97 | ### Nix 98 | 99 | Use `flake.nix` for development: 100 | 101 | ```shell 102 | nix develop 103 | ``` 104 | 105 | Or run it with: 106 | 107 | ```shell 108 | nix run github:ARKye03/morghulis -- --help 109 | # For non-NixOS distro 110 | nix run github:ARKye03/morghulis#fhs -- --help 111 | ``` 112 | 113 | ## Features 114 | 115 | - [x] Status Bar 116 | - [x] Tags/Workspaces Module 117 | - [x] Focused View/Client 118 | - [x] Active Mode/Submap 119 | - [x] Systray 120 | - [x] Quick Menu 121 | - [x] Mpris Media Player 122 | - [x] Power Buttons 123 | - [x] Power Profiles 124 | - [x] Bluetooth 125 | - [x] (WIP) Network 126 | - [x] Audio 127 | - [x] Battery Support 128 | - [x] Runner 129 | - [x] Run apps 130 | - [x] Solve math expressions 131 | - [ ] Handle Hyprland Clients 132 | - [ ] Handle River views? 133 | - [x] Notifications 134 | - [x] Center 135 | - [x] Popup 136 | - [x] Don't Disturb logic 137 | - [x] Power Popup Menu 138 | - [x] OnScreenDisplay 139 | - [x] Audio 140 | - [x] Brightness 141 | - [x] Backligh 142 | - [x] Hot Reload CSS 143 | 144 | > [!WARNING] 145 | > **Users must be part of the `video` group to use backlight services.** 146 | > 147 | > For OSD to work, you need to append `"morghulctl -r change_volume" and/or" morghulctl -r change_brightness"` to whatever keybinding you want to use to raise/lower the volume/brightness. 148 | > Example: 149 | > 150 | > ```hyprlang 151 | > binde=, XF86MonBrightnessUp, exec, brightnessctl set +10% & morghulctl -r change_brightness 152 | > ``` 153 | 154 | ## Preview 155 | 156 | ![Morghulis](public/morghulis.webp) 157 | 158 | > [!NOTE] 159 | > The preview uses a custom Adwaita theme, loaded directly from `$XDG_CONFIG_HOME/morghulis/main.css`. This allows custom shell colors without affecting the system-wide GTK theme. 160 | 161 | ### Thanks to 162 | 163 | - [kotontrion](https://github.com/kotontrion) and its [kompass](https://github.com/kotontrion/kompass) project for inspiration, code snippets, and guidance. 164 | - [Aylur](https://github.com/Aylur) for the awesome project [Astal](https://github.com/Aylur/astal) is. 165 | 166 | ## License 167 | 168 | Licensed under the General Public License v3.0. See the [LICENSE](./LICENSE) file for details. 169 | -------------------------------------------------------------------------------- /cli/cli.vala: -------------------------------------------------------------------------------- 1 | using GLib; 2 | 3 | public class MorghulCTL { 4 | private static string request = ""; 5 | private static bool start = false; 6 | private static string? toggle_window = null; 7 | private static bool show_inspector = false; 8 | private static bool quit = false; 9 | private static bool show_version = false; 10 | private static string? autostart = null; 11 | private static string version = "1.0-alpha"; 12 | 13 | public static int main(string[] args) { 14 | var options = new OptionEntry[] { 15 | { "request", 'r', OptionFlags.NONE, OptionArg.STRING, out request, "Send request to the application", "REQUEST" }, 16 | { "start", 0, OptionFlags.NONE, OptionArg.NONE, out start, "Start the application", null }, 17 | { "toggle-window", 't', OptionFlags.NONE, OptionArg.STRING, out toggle_window, "Toggle window(s)", "WINDOW" }, 18 | { "show-inspector", 'i', OptionFlags.NONE, OptionArg.NONE, out show_inspector, "Show inspector", null }, 19 | { "autostart", 'a', OptionFlags.NONE, OptionArg.STRING, out autostart, "Control autostart (on/off/status)", "STATE" }, 20 | { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, out quit, "Quit the application", null }, 21 | { "version", 'v', OptionFlags.NONE, OptionArg.NONE, out show_version, "Show version", null }, 22 | }; 23 | 24 | var context = new OptionContext(null); 25 | 26 | context.add_main_entries(options, null); 27 | 28 | try { 29 | context.parse(ref args); 30 | } catch (OptionError e) { 31 | stderr.printf("Option parsing failed: %s\n", e.message); 32 | return 1; 33 | } 34 | 35 | if (show_version) { 36 | stdout.printf("Morghulis version %s\n", version); 37 | return 0; 38 | } else if (start) { 39 | return start_morghulis(); 40 | } else if (toggle_window != null) { 41 | return toggle_window_func(toggle_window); 42 | } else if (show_inspector) { 43 | return toggle_inspector(); 44 | } else if (autostart != null) { 45 | return manage_autostart(autostart); 46 | } else if (quit) { 47 | return exit_morghulis(); 48 | } else { 49 | return send_request(request); 50 | } 51 | } 52 | 53 | private static int send_request(string req) { 54 | try { 55 | Process.spawn_command_line_async(@"astal -i morghulis $req"); 56 | } catch (SpawnError e) { 57 | stderr.printf("Failed to send request: %s\n", e.message); 58 | return 1; 59 | } 60 | return 0; 61 | } 62 | 63 | private static int exit_morghulis() { 64 | try { 65 | Process.spawn_command_line_async("astal -i morghulis -q"); 66 | } catch (SpawnError e) { 67 | stderr.printf("Failed to quit the application: %s\n", e.message); 68 | return 1; 69 | } 70 | return 0; 71 | } 72 | 73 | private static int toggle_inspector() { 74 | try { 75 | Process.spawn_command_line_async("astal -i morghulis -I"); 76 | } catch (SpawnError e) { 77 | stderr.printf("Failed to show inspector: %s\n", e.message); 78 | return 1; 79 | } 80 | return 0; 81 | } 82 | 83 | private static int manage_autostart(string state) { 84 | string autostart_dir = Path.build_filename(Environment.get_user_config_dir(), "autostart"); 85 | string target_file = Path.build_filename(autostart_dir, "morghulis.desktop"); 86 | 87 | switch (state.down()) { 88 | case "on": 89 | try { 90 | DirUtils.create_with_parents(autostart_dir, 0755); 91 | File source = File.new_for_path("/usr/share/applications/com.github.ARKye03.morghulis.desktop"); 92 | File dest = File.new_for_path(target_file); 93 | source.copy(dest, FileCopyFlags.NONE); 94 | stdout.printf("Autostart enabled\n"); 95 | return 0; 96 | } catch (Error e) { 97 | stderr.printf("Failed to enable autostart: %s\n", e.message); 98 | return 1; 99 | } 100 | 101 | case "off": 102 | try { 103 | File file = File.new_for_path(target_file); 104 | if (file.query_exists()) { 105 | file.delete(); 106 | } 107 | stdout.printf("Autostart disabled\n"); 108 | return 0; 109 | } catch (Error e) { 110 | stderr.printf("Failed to disable autostart: %s\n", e.message); 111 | return 1; 112 | } 113 | 114 | case "status": 115 | File file = File.new_for_path(target_file); 116 | stdout.printf("Autostart is %s\n", file.query_exists() ? "enabled" : "disabled"); 117 | return 0; 118 | 119 | default: 120 | stderr.printf("Invalid autostart option. Use 'on', 'off' or 'status'\n"); 121 | return 1; 122 | } 123 | } 124 | 125 | private static int toggle_window_func(string window) { 126 | try { 127 | Process.spawn_command_line_async(@"astal -i morghulis -t $window"); 128 | } catch (SpawnError e) { 129 | stderr.printf("Failed to toggle window: %s\n", e.message); 130 | return 1; 131 | } 132 | return 0; 133 | } 134 | 135 | private static string? find_morghulis_binary() { 136 | try { 137 | string output; 138 | string error; 139 | int exit_status; 140 | Process.spawn_command_line_sync("which morghulis", out output, out error, out exit_status); 141 | if (exit_status == 0 && output.strip() != "") { 142 | return output.strip(); 143 | } else { 144 | return null; 145 | } 146 | } catch (SpawnError e) { 147 | stderr.printf("Failed to find morghulis binary: %s\n", e.message); 148 | return null; 149 | } 150 | } 151 | 152 | private static bool is_process_running(string process_name) { 153 | try { 154 | string output; 155 | string error; 156 | int exit_status; 157 | Process.spawn_command_line_sync("pgrep " + process_name, out output, out error, out exit_status); 158 | return !(output == null || output == ""); 159 | } catch (SpawnError e) { 160 | stderr.printf("Failed to check if process is running: %s\n", e.message); 161 | return false; 162 | } 163 | } 164 | 165 | private static int start_morghulis() { 166 | if (is_process_running("morghulis")) { 167 | stdout.printf("Morghulis process is already running.\n"); 168 | return 0; 169 | } 170 | 171 | string? morghulis_path = find_morghulis_binary(); 172 | if (morghulis_path == null) { 173 | stderr.printf("Morghulis binary not found in PATH.\n"); 174 | return 1; 175 | } 176 | 177 | try { 178 | Pid child_pid; 179 | Process.spawn_async( 180 | null, 181 | new string[] { morghulis_path }, 182 | null, 183 | SpawnFlags.DO_NOT_REAP_CHILD, 184 | null, 185 | out child_pid 186 | ); 187 | stdout.printf("Starting the application…\n"); 188 | } catch (SpawnError e) { 189 | stderr.printf("Failed to start the application: %s\n", e.message); 190 | return 1; 191 | } 192 | return 0; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /cli/meson.build: -------------------------------------------------------------------------------- 1 | executable( 2 | 'morghulctl', 3 | 'cli.vala', 4 | dependencies: [ 5 | dependency('gio-unix-2.0'), 6 | dependency('gio-2.0'), 7 | ], 8 | install: true, 9 | ) -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # template for the changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | # render body even when there are no releases to process 42 | # render_always = true 43 | # output file path 44 | # output = "test.md" 45 | 46 | [git] 47 | # parse the commits based on https://www.conventionalcommits.org 48 | conventional_commits = true 49 | # filter out the commits that are not conventional 50 | filter_unconventional = true 51 | # process each line of a commit as an individual commit 52 | split_commits = false 53 | # regex for preprocessing the commit messages 54 | commit_preprocessors = [ 55 | # Replace issue numbers 56 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 57 | # Check spelling of the commit with https://github.com/crate-ci/typos 58 | # If the spelling is incorrect, it will be automatically fixed. 59 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 60 | ] 61 | # regex for parsing and grouping commits 62 | commit_parsers = [ 63 | { message = "^feat", group = "🚀 Features" }, 64 | { message = "^fix", group = "🐛 Bug Fixes" }, 65 | { message = "^doc", group = "📚 Documentation" }, 66 | { message = "^perf", group = "⚡ Performance" }, 67 | { message = "^refactor", group = "🚜 Refactor" }, 68 | { message = "^style", group = "🎨 Styling" }, 69 | { message = "^test", group = "🧪 Testing" }, 70 | { message = "^chore\\(release\\): prepare for", skip = true }, 71 | { message = "^chore\\(deps.*\\)", skip = true }, 72 | { message = "^chore\\(pr\\)", skip = true }, 73 | { message = "^chore\\(pull\\)", skip = true }, 74 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 75 | { body = ".*security", group = "🛡️ Security" }, 76 | { message = "^revert", group = "◀️ Revert" }, 77 | ] 78 | # filter out the commits that are not matched by commit parsers 79 | filter_commits = false 80 | # sort the tags topologically 81 | topo_order = false 82 | # sort the commits inside sections by oldest/newest order 83 | sort_commits = "oldest" 84 | 85 | [bump] 86 | features_always_bump_minor = true 87 | breaking_always_bump_major = true 88 | initial_tag = "0.1.0" 89 | -------------------------------------------------------------------------------- /data/assets/NavBar/browser-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/assets/NavBar/code-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/assets/NavBar/docs-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/assets/NavBar/explorer-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /data/assets/NavBar/gaming-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/assets/NavBar/media-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/assets/NavBar/settings-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /data/assets/NavBar/social-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /data/assets/NavBar/terminal-symbolic.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/assets/NoNotif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARKye03/morghulis/b5fc77f00ccebdb870538cff6bc4a3723de929a4/data/assets/NoNotif.png -------------------------------------------------------------------------------- /data/assets/System/hibernate-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/assets/System/lockscreen-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/assets/System/logout-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/assets/System/reboot-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /data/assets/System/shutdown-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /data/assets/System/suspend-symbolic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/assets/colloid-notif-sound.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARKye03/morghulis/b5fc77f00ccebdb870538cff6bc4a3723de929a4/data/assets/colloid-notif-sound.opus -------------------------------------------------------------------------------- /data/assets/wyvern-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wyvern 5 | 6 | -------------------------------------------------------------------------------- /data/desktop/com.github.ARKye03.morghulis.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Morghulis 3 | Exec=/usr/bin/morghulis 4 | Terminal=false 5 | Type=Application 6 | Categories=Utility; 7 | Keywords=GTK; 8 | StartupNotify=true 9 | DBusActivatable=true -------------------------------------------------------------------------------- /data/desktop/com.github.ARKye03.morghulis.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/desktop/com.github.ARKye03.morghulis.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.github.ARKye03.morghulis 4 | CC0-1.0 5 | GPL-3.0-or-later 6 | 7 | Morghulis 8 | Desktop Shell created with GTK4, Blueprint, and Vala. 9 | 10 |

Morghulis is a customizable desktop shell for enhancing user experience with modern graphical 11 | interfaces.

12 |
13 | 14 | 15 | ARKye03 16 | 17 | 18 | 19 | https://github.com/ARKye03/morghulis 20 | 22 | https://github.com/ARKye03/morghulis 23 | 24 | https://github.com/ARKye03/morghulis/issues 25 | 27 | 29 | https://example.org/translate 30 | https://example.org/faq 31 | 33 | https://example.org/help 34 | 36 | https://example.org/donate 37 | 40 | https://example.org/contact 41 | 44 | https://example.org/contribute 45 | 46 | Morghulis 47 | 50 | com.github.ARKye03.morghulis.desktop 51 | 53 | 54 | 55 | 56 | 57 | #ff00ff 58 | #993d3d 59 | 60 | 61 | 62 | 63 | https://example.org/example1.png 64 | A caption 65 | 66 | 67 | https://example.org/example2.png 68 | A caption 69 | 70 | 71 | 72 | 73 | 74 | https://example.org/changelog.html#version_1.0.1 75 | 76 |

Release description

77 |
    78 |
  • List of changes
  • 79 |
  • List of changes
  • 80 |
81 |
82 |
83 |
84 | 85 |
-------------------------------------------------------------------------------- /data/desktop/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'com.github.ARKye03.morghulis.desktop.in', 3 | output: 'com.github.ARKye03.morghulis.desktop', 4 | type: 'desktop', 5 | po_dir: '../po', 6 | install: true, 7 | install_dir: get_option('datadir') / 'applications', 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | 12 | if desktop_utils.found() 13 | test('Validate desktop file', desktop_utils, args: [desktop_file]) 14 | endif 15 | appstream_file = i18n.merge_file( 16 | input: 'com.github.ARKye03.morghulis.metainfo.xml.in', 17 | output: 'com.github.ARKye03.morghulis.metainfo.xml', 18 | po_dir: '../po', 19 | install: true, 20 | install_dir: get_option('datadir') / 'metainfo', 21 | ) 22 | 23 | appstreamcli = find_program('appstreamcli', required: false, disabler: true) 24 | test( 25 | 'Validate appstream file', 26 | appstreamcli, 27 | args: ['validate', '--no-net', '--explain', appstream_file], 28 | ) 29 | 30 | install_data( 31 | 'com.github.ARKye03.morghulis.gschema.xml', 32 | install_dir: get_option('datadir') / 'glib-2.0' / 'schemas', 33 | ) 34 | 35 | compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true) 36 | test( 37 | 'Validate schema file', 38 | compile_schemas, 39 | args: ['--strict', '--dry-run', meson.current_source_dir()], 40 | ) 41 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('ui') 2 | subdir('scss') 3 | subdir('desktop') 4 | 5 | project_resources += gnome.compile_resources( 6 | project_name, 7 | project_name + '.gresource.xml', 8 | dependencies: [blueprints, scss], 9 | source_dir: meson.current_build_dir(), 10 | ) -------------------------------------------------------------------------------- /data/morghulis.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | assets/NavBar/terminal-symbolic.svg 6 | assets/NavBar/browser-symbolic.svg 7 | assets/NavBar/code-symbolic.svg 8 | assets/NavBar/explorer-symbolic.svg 9 | assets/NavBar/docs-symbolic.svg 10 | assets/NavBar/social-symbolic.svg 11 | assets/NavBar/media-symbolic.svg 12 | assets/NavBar/settings-symbolic.svg 13 | assets/NavBar/gaming-symbolic.svg 14 | 15 | assets/System/hibernate-symbolic.svg 16 | assets/System/logout-symbolic.svg 17 | assets/System/reboot-symbolic.svg 18 | assets/System/shutdown-symbolic.svg 19 | assets/System/suspend-symbolic.svg 20 | assets/System/lockscreen-symbolic.svg 21 | 22 | 23 | morghulis.css 24 | assets/colloid-notif-sound.opus 25 | assets/wyvern-svgrepo-com.svg 26 | ui/NavBar.ui 27 | ui/MprisPlayer.ui 28 | ui/QuickMenu.ui 29 | ui/QButton.ui 30 | ui/QNetwork.ui 31 | ui/QBluetooth.ui 32 | ui/QBluetoothItem.ui 33 | ui/BatteryBox.ui 34 | ui/PowerBox.ui 35 | ui/Settings.ui 36 | ui/Runner.ui 37 | ui/RunnerButton.ui 38 | ui/OnScreenDisplay.ui 39 | ui/NotificationItem.ui 40 | ui/QNotifications.ui 41 | ui/QAudioBox.ui 42 | ui/QAudioItem.ui 43 | ui/PowerMenu.ui 44 | 45 | -------------------------------------------------------------------------------- /data/po/LINGUAS: -------------------------------------------------------------------------------- 1 | # Please keep this file sorted alphabetically. 2 | -------------------------------------------------------------------------------- /data/po/POTFILES.in: -------------------------------------------------------------------------------- 1 | # List of source files containing translatable strings. 2 | # Please keep this file sorted alphabetically. 3 | data/com.github.ARKye03.morghulis.desktop.in 4 | data/com.github.ARKye03.morghulis.gschema.xml 5 | data/com.github.ARKye03.morghulis.metainfo.xml.in -------------------------------------------------------------------------------- /data/scss/_NavBar.scss: -------------------------------------------------------------------------------- 1 | @use "adw"; 2 | 3 | .workspaces button { 4 | transition: all .2s ease-in-out; 5 | } 6 | 7 | .empty { 8 | color: adw.$dark_1; 9 | } 10 | 11 | .occupied { 12 | @extend .focused; 13 | filter: hue-rotate(90deg) brightness(0.7); 14 | } 15 | 16 | .urgent { 17 | color: adw.$red_5; 18 | } 19 | 20 | .focused { 21 | color: adw.$accent_color; 22 | 23 | * { 24 | color: adw.$accent_color; 25 | } 26 | } 27 | 28 | .tray_button { 29 | &>button { 30 | image { 31 | transition: all 0.2s ease-in-out; 32 | -gtk-icon-transform: rotate(0deg); 33 | } 34 | 35 | &:checked image { 36 | -gtk-icon-transform: rotate(180deg); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /data/scss/_Notif.scss: -------------------------------------------------------------------------------- 1 | @use "adw"; 2 | 3 | .notification_badge { 4 | &.critical { 5 | label.app_name { 6 | color: adw.$red_1; 7 | } 8 | 9 | separator { 10 | color: adw.$red_1; 11 | } 12 | } 13 | 14 | &.normal { 15 | label.app_name { 16 | color: adw.$blue_1; 17 | } 18 | 19 | separator { 20 | color: adw.$blue_1; 21 | } 22 | } 23 | 24 | &.low { 25 | label.app_name { 26 | color: adw.$green_1; 27 | } 28 | 29 | separator { 30 | color: adw.$green_1; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /data/scss/_OnScreenDisplay.scss: -------------------------------------------------------------------------------- 1 | @use "adw"; 2 | 3 | .osd_slider { 4 | min-width: 250px; 5 | 6 | & trough { 7 | background-color: adw.$window_bg_color; 8 | color: adw.$window_fg_color; 9 | border-radius: adw.$master-border-radius; 10 | padding: 0; 11 | min-height: 30px; 12 | 13 | & progress { 14 | background-color: adw.$accent_color; 15 | border-radius: adw.$master-border-radius; 16 | min-height: 30px; 17 | transition: all 1s ease-in-out; 18 | } 19 | 20 | } 21 | } 22 | 23 | .osd_icon { 24 | color: #000; 25 | } -------------------------------------------------------------------------------- /data/scss/_adw.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --master-border-radius: 5px; 3 | } 4 | 5 | $master-border-radius: var(--master-border-radius); 6 | 7 | 8 | $accent_color: #{"@accent_color"}; 9 | $accent_hover_color: #{"@accent_hover_color"}; 10 | $accent_bg_color: #{"@accent_bg_color"}; 11 | $accent_fg_color: #{"@accent_fg_color"}; 12 | $destructive_color: #{"@destructive_color"}; 13 | $destructive_bg_color: #{"@destructive_bg_color"}; 14 | $destructive_fg_color: #{"@destructive_fg_color"}; 15 | $success_color: #{"@success_color"}; 16 | $success_bg_color: #{"@success_bg_color"}; 17 | $success_fg_color: #{"@success_fg_color"}; 18 | $warning_color: #{"@warning_color"}; 19 | $warning_bg_color: #{"@warning_bg_color"}; 20 | $warning_fg_color: #{"@warning_fg_color"}; 21 | $error_color: #{"@error_color"}; 22 | $error_bg_color: #{"@error_bg_color"}; 23 | $error_fg_color: #{"@error_fg_color"}; 24 | $window_bg_color: #{"@window_bg_color"}; 25 | $window_fg_color: #{"@window_fg_color"}; 26 | $view_bg_color: #{"@view_bg_color"}; 27 | $view_fg_color: #{"@view_fg_color"}; 28 | $headerbar_bg_color: #{"@headerbar_bg_color"}; 29 | $headerbar_fg_color: #{"@headerbar_fg_color"}; 30 | $headerbar_border_color: #{"@headerbar_border_color"}; 31 | $headerbar_backdrop_color: #{"@headerbar_backdrop_color"}; 32 | $headerbar_shade_color: #{"@headerbar_shade_color"}; 33 | $card_bg_color: #{"@card_bg_color"}; 34 | $card_fg_color: #{"@card_fg_color"}; 35 | $card_shade_color: #{"@card_shade_color"}; 36 | $dialog_bg_color: #{"@dialog_bg_color"}; 37 | $dialog_fg_color: #{"@dialog_fg_color"}; 38 | $popover_bg_color: #{"@popover_bg_color"}; 39 | $popover_fg_color: #{"@popover_fg_color"}; 40 | $shade_color: #{"@shade_color"}; 41 | $scrollbar_outline_color: #{"@scrollbar_outline_color"}; 42 | $borders_color: #{"@borders"}; 43 | 44 | $blue_1: #{"@blue_1"}; 45 | $blue_2: #{"@blue_2"}; 46 | $blue_3: #{"@blue_3"}; 47 | $blue_4: #{"@blue_4"}; 48 | $blue_5: #{"@blue_5"}; 49 | 50 | $green_1: #{"@green_1"}; 51 | $green_2: #{"@green_2"}; 52 | $green_3: #{"@green_3"}; 53 | $green_4: #{"@green_4"}; 54 | $green_5: #{"@green_5"}; 55 | 56 | $yellow_1: #{"@yellow_1"}; 57 | $yellow_2: #{"@yellow_2"}; 58 | $yellow_3: #{"@yellow_3"}; 59 | $yellow_4: #{"@yellow_4"}; 60 | $yellow_5: #{"@yellow_5"}; 61 | 62 | $orange_1: #{"@orange_1"}; 63 | $orange_2: #{"@orange_2"}; 64 | $orange_3: #{"@orange_3"}; 65 | $orange_4: #{"@orange_4"}; 66 | $orange_5: #{"@orange_5"}; 67 | 68 | $red_1: #{"@red_1"}; 69 | $red_2: #{"@red_2"}; 70 | $red_3: #{"@red_3"}; 71 | $red_4: #{"@red_4"}; 72 | $red_5: #{"@red_5"}; 73 | 74 | $purple_1: #{"@purple_1"}; 75 | $purple_2: #{"@purple_2"}; 76 | $purple_3: #{"@purple_3"}; 77 | $purple_4: #{"@purple_4"}; 78 | $purple_5: #{"@purple_5"}; 79 | 80 | $brown_1: #{"@brown_1"}; 81 | $brown_2: #{"@brown_2"}; 82 | $brown_3: #{"@brown_3"}; 83 | $brown_4: #{"@brown_4"}; 84 | $brown_5: #{"@brown_5"}; 85 | 86 | $light_1: #{"@light_1"}; 87 | $light_2: #{"@light_2"}; 88 | $light_3: #{"@light_3"}; 89 | $light_4: #{"@light_4"}; 90 | $light_5: #{"@light_5"}; 91 | 92 | $dark_1: #{"@dark_1"}; 93 | $dark_2: #{"@dark_2"}; 94 | $dark_3: #{"@dark_3"}; 95 | $dark_4: #{"@dark_4"}; 96 | $dark_5: #{"@dark_5"}; -------------------------------------------------------------------------------- /data/scss/main.scss: -------------------------------------------------------------------------------- 1 | @use "adw"; 2 | @use "NavBar"; 3 | @use "OnScreenDisplay"; 4 | @use "Notif"; 5 | 6 | .all_unset { 7 | all: unset; 8 | } 9 | 10 | button { 11 | border-radius: adw.$master-border-radius; 12 | 13 | &:hover, 14 | &:checked { 15 | border-radius: adw.$master-border-radius; 16 | } 17 | } 18 | 19 | .flats button { 20 | background-color: unset; 21 | 22 | &:hover { 23 | background-color: color-mix(in srgb, currentColor 15%, transparent); 24 | } 25 | 26 | &:checked { 27 | background-color: color-mix(in srgb, currentColor 30%, transparent); 28 | } 29 | } 30 | 31 | .master_border { 32 | // border: 1px solid adw.$borders_color; 33 | border: unset; 34 | } 35 | 36 | .rounded { 37 | border-radius: adw.$master-border-radius; 38 | } 39 | 40 | .squares button { 41 | border-radius: unset; 42 | } 43 | 44 | .bg_transparent { 45 | background-color: transparent; 46 | } 47 | 48 | .top_left_right_corner_borders { 49 | border-radius: unset; 50 | border-top-left-radius: adw.$master-border-radius; 51 | border-top-right-radius: adw.$master-border-radius; 52 | } 53 | 54 | .bottom_left_right_corner_borders { 55 | border-radius: unset; 56 | border-bottom-left-radius: adw.$master-border-radius; 57 | border-bottom-right-radius: adw.$master-border-radius; 58 | } 59 | 60 | .x_padding_5 { 61 | padding: 0 5px; 62 | } 63 | 64 | .x_padding_10 { 65 | padding: 0 10px; 66 | } 67 | 68 | .y_padding_5 { 69 | padding: 5px 0; 70 | } 71 | 72 | .padding_5 { 73 | padding: 5px; 74 | } 75 | 76 | .padding_10 { 77 | padding: 10px; 78 | } 79 | 80 | .title_5 { 81 | font-size: 15px; 82 | } 83 | 84 | .title_6 { 85 | font-size: 12px; 86 | } 87 | 88 | .qbutton { 89 | &>button { 90 | background-color: adw.$accent_bg_color; 91 | color: adw.$accent_fg_color; 92 | 93 | &:hover { 94 | background-color: #{"hsl(from @accent_bg_color h s calc(l + 5%))"}; 95 | } 96 | 97 | } 98 | } 99 | 100 | circularprogress { 101 | & progress { 102 | color: adw.$accent_color; 103 | } 104 | 105 | & radius { 106 | color: adw.$dark_1; 107 | } 108 | 109 | & center { 110 | color: adw.$headerbar_shade_color; 111 | } 112 | } 113 | 114 | .hslider { 115 | & trough { 116 | @extend .rounded; 117 | padding: 0; 118 | min-height: 10px; 119 | 120 | & highlight { 121 | @extend .rounded; 122 | background-color: adw.$accent_color; 123 | } 124 | 125 | & slider { 126 | all: unset; 127 | } 128 | } 129 | } 130 | 131 | .hprogress { 132 | & trough { 133 | @extend .rounded; 134 | padding: 0; 135 | min-height: 10px; 136 | 137 | & progress { 138 | @extend .rounded; 139 | background-color: adw.$accent_color; 140 | min-height: 10px; 141 | } 142 | 143 | } 144 | } 145 | 146 | .vslider { 147 | & trough { 148 | padding: 0; 149 | min-width: 10px; 150 | 151 | & highlight { 152 | background-color: adw.$accent_color; 153 | } 154 | 155 | & slider { 156 | all: unset; 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /data/scss/meson.build: -------------------------------------------------------------------------------- 1 | sass = find_program('sass') 2 | 3 | scss_deps = files( 4 | '_OnScreenDisplay.scss', 5 | '_NavBar.scss', 6 | '_adw.scss', 7 | '_Notif.scss', 8 | 'main.scss', 9 | ) 10 | 11 | scss = custom_target( 12 | 'scss', 13 | input: 'main.scss', 14 | output: 'morghulis.css', 15 | command: [ 16 | sass, 17 | '@INPUT@', 18 | '@OUTPUT@', 19 | ], 20 | depend_files: scss_deps, 21 | ) -------------------------------------------------------------------------------- /data/ui/BatteryBox.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using AstalBattery 0.1; 3 | using AstalPowerProfiles 0.1; 4 | 5 | template $BatteryBox: Box { 6 | spacing: 10; 7 | 8 | styles [ 9 | "background", 10 | "padding_10", 11 | "master_border", 12 | "rounded", 13 | "flats", 14 | ] 15 | 16 | Stack bb_stack { 17 | transition-duration: 300; 18 | transition-type: slide_up_down; 19 | 20 | StackPage { 21 | name: "sliders"; 22 | 23 | child: Box { 24 | orientation: vertical; 25 | spacing: 10; 26 | 27 | Box { 28 | orientation: horizontal; 29 | spacing: 10; 30 | 31 | Image { 32 | icon-name: bind template.backlight as <$Backlight>.icon-name; 33 | } 34 | 35 | Scale { 36 | hexpand: true; 37 | adjustment: bind template.brightness_adj; 38 | 39 | styles [ 40 | "hslider", 41 | "rounded", 42 | "padding_5", 43 | ] 44 | } 45 | } 46 | 47 | Box { 48 | orientation: horizontal; 49 | spacing: 10; 50 | 51 | Image { 52 | icon-name: bind template.battery as .icon-name; 53 | } 54 | 55 | ProgressBar { 56 | hexpand: true; 57 | fraction: bind template.battery as .percentage; 58 | 59 | styles [ 60 | "hprogress", 61 | "rounded", 62 | "padding_5", 63 | ] 64 | } 65 | } 66 | }; 67 | } 68 | 69 | StackPage { 70 | name: "profiles"; 71 | 72 | child: Box { 73 | orientation: horizontal; 74 | spacing: 10; 75 | homogeneous: true; 76 | 77 | Button power_saver { 78 | icon-name: "power-profile-power-saver-symbolic"; 79 | clicked => $set_power_saver(); 80 | tooltip-text: _("Power Saver"); 81 | } 82 | 83 | Button balanced { 84 | icon-name: "power-profile-balanced-symbolic"; 85 | clicked => $set_balanced(); 86 | tooltip-text: _("Balanced"); 87 | } 88 | 89 | Button performance { 90 | icon-name: "power-profile-performance-symbolic"; 91 | clicked => $set_performance(); 92 | tooltip-text: _("Performance"); 93 | } 94 | }; 95 | } 96 | } 97 | 98 | Button ppd_stack_btn { 99 | clicked => $show_power_profiles(); 100 | tooltip-text: _("Power Profiles"); 101 | 102 | Image { 103 | icon-name: bind template.power_profiles as .icon-name; 104 | icon-size: large; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /data/ui/MprisPlayer.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using AstalMpris 0.1; 3 | 4 | template $MprisPlayer: Box { 5 | orientation: vertical; 6 | hexpand: true; 7 | spacing: 5; 8 | width-request: 270; 9 | height-request: 150; 10 | 11 | styles [ 12 | "master_border", 13 | "rounded", 14 | "padding_10", 15 | "view", 16 | ] 17 | 18 | Box { 19 | spacing: 10; 20 | hexpand: true; 21 | 22 | Image { 23 | styles [ 24 | "rounded", 25 | ] 26 | 27 | overflow: hidden; 28 | halign: start; 29 | file: bind $art_url(template.player as .art-url) as ; 30 | pixel-size: 100; 31 | } 32 | 33 | Box { 34 | orientation: vertical; 35 | hexpand: true; 36 | valign: center; 37 | spacing: 10; 38 | 39 | Label { 40 | label: bind template.player as .identity; 41 | halign: start; 42 | ellipsize: end; 43 | max-width-chars: 25; 44 | tooltip-text: "Player Name"; 45 | 46 | styles [ 47 | "title_5", 48 | "dim-label", 49 | ] 50 | } 51 | 52 | Label { 53 | label: bind template.player as .title; 54 | halign: start; 55 | ellipsize: end; 56 | max-width-chars: 18; 57 | tooltip-text: "Title"; 58 | 59 | styles [ 60 | "title-3", 61 | ] 62 | } 63 | 64 | Label { 65 | label: bind template.player as .artist; 66 | halign: start; 67 | ellipsize: end; 68 | max-width-chars: 20; 69 | tooltip-text: "Artist"; 70 | 71 | styles [ 72 | "title-4", 73 | ] 74 | } 75 | } 76 | } 77 | 78 | CenterBox { 79 | vexpand: true; 80 | 81 | [start] 82 | Label { 83 | label: bind $current_pos(template.player as .position) as ; 84 | valign: center; 85 | 86 | styles [ 87 | "numeric", 88 | ] 89 | } 90 | 91 | [center] 92 | Scale { 93 | hexpand: true; 94 | 95 | adjustment: Adjustment media_len_adjust { 96 | lower: 0; 97 | upper: bind template.player as .length; 98 | }; 99 | 100 | styles [ 101 | "hslider", 102 | ] 103 | } 104 | 105 | [end] 106 | Label { 107 | label: bind $total_pos(template.player as .length) as ; 108 | valign: center; 109 | 110 | styles [ 111 | "numeric", 112 | ] 113 | } 114 | } 115 | 116 | Box { 117 | spacing: 10; 118 | hexpand: true; 119 | halign: center; 120 | 121 | styles [ 122 | "mpris_master_lower_box", 123 | ] 124 | 125 | Button { 126 | icon-name: "media-skip-backward-symbolic"; 127 | clicked => $prev(); 128 | } 129 | 130 | Button { 131 | icon-name: bind $pause_icon(template.player as .playback-status) as ; 132 | clicked => $play_pause(); 133 | } 134 | 135 | Button { 136 | icon-name: "media-skip-forward-symbolic"; 137 | clicked => $next(); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /data/ui/NavBar.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | using Astal 4.0; 4 | using AstalBattery 0.1; 5 | using AstalWp 0.1; 6 | 7 | template $NavBar: Astal.Window { 8 | styles [ 9 | "top_left_right_corner_borders", 10 | "x_padding_5", 11 | "flats", 12 | ] 13 | 14 | title: "NavBar"; 15 | namespace: "Morghulis.NavBar"; 16 | anchor: left | bottom | right; 17 | exclusivity: exclusive; 18 | 19 | CenterBox { 20 | styles [ 21 | "y_padding_5", 22 | ] 23 | 24 | [start] 25 | Box { 26 | spacing: 10; 27 | 28 | Adw.Bin { 29 | styles [ 30 | "master_border", 31 | "view", 32 | "rounded", 33 | ] 34 | 35 | Button { 36 | icon-name: "view-grid-symbolic"; 37 | name: "apps_button"; 38 | clicked => $toggle_runner(); 39 | } 40 | } 41 | 42 | Adw.Bin active_client { 43 | visible: false; 44 | 45 | styles [ 46 | "x_padding_5", 47 | "master_border", 48 | "rounded", 49 | "title-4", 50 | "view", 51 | ] 52 | } 53 | 54 | Adw.Bin active_submap { 55 | visible: false; 56 | 57 | styles [ 58 | "x_padding_5", 59 | "master_border", 60 | "rounded", 61 | "title-4", 62 | "view", 63 | ] 64 | } 65 | } 66 | 67 | [center] 68 | Adw.Bin workspaces { 69 | styles [ 70 | "workspaces", 71 | "master_border", 72 | "rounded", 73 | "view", 74 | ] 75 | } 76 | 77 | [end] 78 | Box right_box { 79 | spacing: 10; 80 | 81 | Adw.Bin { 82 | styles [ 83 | "master_border", 84 | "view", 85 | "rounded", 86 | ] 87 | 88 | MenuButton { 89 | icon-name: "go-up-symbolic"; 90 | 91 | styles [ 92 | "tray_button", 93 | ] 94 | 95 | popover: Popover { 96 | position: top; 97 | height-request: 50; 98 | autohide: true; 99 | has-arrow: false; 100 | margin-bottom: 10; 101 | 102 | $Tray tray {} 103 | }; 104 | } 105 | } 106 | 107 | $CircularProgressBar { 108 | tooltip-text: bind $current_volume(template.speaker as .volume) as ; 109 | percentage: bind template.speaker as .volume; 110 | line-width: 5; 111 | line-cap: round; 112 | radius-filled: true; 113 | inverted: true; 114 | start-at: -0.25; 115 | end-at: 0.75; 116 | 117 | styles [ 118 | "master_border", 119 | "view", 120 | "rounded", 121 | ] 122 | 123 | Gtk.EventControllerScroll { 124 | scroll => $scroll_volume(); 125 | flags: vertical; 126 | } 127 | 128 | Gtk.GestureClick { 129 | button: 1; 130 | pressed => $toggle_volume(); 131 | } 132 | 133 | Image { 134 | icon-name: bind template.speaker as .volume-icon; 135 | } 136 | } 137 | 138 | Adw.Bin { 139 | styles [ 140 | "master_border", 141 | "view", 142 | "rounded", 143 | ] 144 | 145 | MenuButton { 146 | styles [ 147 | "title-4", 148 | "numeric", 149 | ] 150 | 151 | Label { 152 | label: bind template.current_time; 153 | } 154 | 155 | popover: Popover { 156 | position: top; 157 | width-request: 350; 158 | height-request: 250; 159 | autohide: true; 160 | has-arrow: false; 161 | margin-bottom: 10; 162 | 163 | Calendar {} 164 | }; 165 | } 166 | } 167 | 168 | Adw.Bin { 169 | styles [ 170 | "master_border", 171 | "view", 172 | "rounded", 173 | ] 174 | 175 | Button power_button { 176 | name: "power_button"; 177 | clicked => $toggle_side_dashboard(); 178 | 179 | Box { 180 | spacing: 10; 181 | 182 | Box { 183 | visible: bind template.battery as .is-present; 184 | spacing: 5; 185 | 186 | Label { 187 | label: bind $current_battery(template.battery as .percentage) as ; 188 | 189 | styles [ 190 | "title-4", 191 | "numeric", 192 | ] 193 | } 194 | 195 | Image { 196 | icon-name: bind template.battery as .icon-name; 197 | } 198 | } 199 | 200 | Image { 201 | icon-name: "system-shutdown-symbolic"; 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /data/ui/NotificationItem.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using AstalNotifd 0.1; 3 | 4 | template $NotificationItem: ListBoxRow { 5 | styles [ 6 | "notification_badge", 7 | "view", 8 | ] 9 | 10 | activatable: false; 11 | selectable: false; 12 | overflow: hidden; 13 | 14 | Box { 15 | orientation: vertical; 16 | 17 | CenterBox { 18 | styles [ 19 | "x_padding_10", 20 | ] 21 | 22 | [start] 23 | Image app_image { 24 | overflow: hidden; 25 | icon-name: bind template.notification as .app-icon; 26 | tooltip-text: "App Icon"; 27 | 28 | styles [ 29 | "notification_image", 30 | ] 31 | } 32 | 33 | [center] 34 | Label app_name { 35 | label: bind template.notification as .app-name; 36 | tooltip-text: "App Name"; 37 | margin-bottom: 10; 38 | margin-top: 10; 39 | hexpand: true; 40 | 41 | styles [ 42 | "app_name", 43 | ] 44 | } 45 | 46 | [end] 47 | Box { 48 | Label { 49 | label: bind $current_time(template.notification as .time) as ; 50 | tooltip-text: "Time"; 51 | } 52 | 53 | Button { 54 | valign: center; 55 | icon-name: "window-close-symbolic"; 56 | clicked => $dismiss_notif(); 57 | 58 | styles [ 59 | "flat", 60 | ] 61 | } 62 | } 63 | } 64 | 65 | Separator { 66 | height-request: 2; 67 | } 68 | 69 | Box { 70 | styles [ 71 | "padding_10", 72 | ] 73 | 74 | spacing: 10; 75 | orientation: vertical; 76 | 77 | Label { 78 | label: bind template.notification as .summary; 79 | justify: center; 80 | ellipsize: end; 81 | max-width-chars: 15; 82 | } 83 | 84 | Label { 85 | label: bind template.notification as .body; 86 | wrap: true; 87 | wrap-mode: word_char; 88 | justify: left; 89 | lines: 3; 90 | ellipsize: end; 91 | use-markup: true; 92 | } 93 | 94 | Box actions_box { 95 | homogeneous: true; 96 | halign: center; 97 | hexpand: true; 98 | 99 | styles [ 100 | "linked", 101 | ] 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /data/ui/OnScreenDisplay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Astal 4.0; 3 | using AstalWp 0.1; 4 | 5 | template $OnScreenDisplay: Astal.Window { 6 | styles [ 7 | "all_unset", 8 | ] 9 | 10 | title: "OnScreenDisplay"; 11 | namespace: "Morghulis.OnScreenDisplay"; 12 | layer: overlay; 13 | anchor: bottom; 14 | margin-bottom: 5; 15 | 16 | Stack stack_osd { // Stack for volume and brightness 17 | StackPage { 18 | name: "volume_osd"; 19 | 20 | child: Overlay volume_osd { 21 | hexpand: true; 22 | vexpand: true; 23 | width-request: 300; 24 | 25 | ProgressBar { 26 | hexpand: true; 27 | orientation: horizontal; 28 | fraction: bind template.speaker as .volume; 29 | vexpand: true; 30 | 31 | styles [ 32 | "osd_slider", 33 | "master_border", 34 | "rounded", 35 | ] 36 | } 37 | 38 | [overlay] 39 | Image { 40 | halign: start; 41 | valign: center; 42 | margin-start: 10; 43 | icon-name: bind template.speaker as .volume-icon; 44 | 45 | styles [ 46 | "osd_icon", 47 | ] 48 | } 49 | }; 50 | } 51 | 52 | StackPage { 53 | name: "brightness_osd"; 54 | 55 | child: Overlay brightness_osd { 56 | hexpand: true; 57 | vexpand: true; 58 | width-request: 300; 59 | 60 | ProgressBar { 61 | hexpand: true; 62 | orientation: horizontal; 63 | fraction: bind template.backlight as <$Backlight>.percentage; 64 | vexpand: true; 65 | 66 | styles [ 67 | "osd_slider", 68 | "master_border", 69 | "rounded", 70 | ] 71 | } 72 | 73 | [overlay] 74 | Image { 75 | halign: start; 76 | valign: center; 77 | margin-start: 10; 78 | icon-name: bind template.backlight as <$Backlight>.icon-name; 79 | 80 | styles [ 81 | "osd_icon", 82 | ] 83 | } 84 | }; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /data/ui/PowerBox.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $PowerBox: Box { 5 | hexpand: true; 6 | 7 | styles [ 8 | "background", 9 | "padding_10", 10 | "master_border", 11 | "rounded", 12 | "flats", 13 | ] 14 | 15 | Box { 16 | hexpand: true; 17 | halign: start; 18 | spacing: 10; 19 | 20 | Adw.Avatar { 21 | tooltip-text: bind template.user_name; 22 | text: bind template.user_name; 23 | custom-image: bind template.user_image_paintable; 24 | show-initials: true; 25 | size: 30; 26 | } 27 | 28 | Label { 29 | label: bind template.uptime; 30 | valign: center; 31 | 32 | styles [ 33 | "title_5", 34 | ] 35 | } 36 | } 37 | 38 | Stack main_stack { 39 | transition-type: slide_up_down; 40 | 41 | StackPage { 42 | name: "main"; 43 | 44 | child: Box { 45 | hexpand: true; 46 | spacing: 2; 47 | halign: end; 48 | 49 | Button { 50 | name: "Suspend"; 51 | visible: false; 52 | icon-name: "radio-symbolic"; 53 | tooltip-text: "Suspend"; 54 | clicked => $suspend(); 55 | } 56 | 57 | Button { 58 | name: "Hibernate"; 59 | visible: false; 60 | icon-name: "weather-clear-night-symbolic"; 61 | tooltip-text: "Hibernate"; 62 | clicked => $hibernate(); 63 | } 64 | 65 | Button { 66 | name: "Lock"; 67 | icon-name: "system-lock-screen-symbolic"; 68 | tooltip-text: "Lock"; 69 | clicked => $lock(); 70 | } 71 | 72 | Button { 73 | name: "Logout"; 74 | icon-name: "system-log-out-symbolic"; 75 | tooltip-text: "Logout"; 76 | clicked => $show_logout_confirm(); 77 | } 78 | 79 | Button { 80 | name: "Reboot"; 81 | icon-name: "system-reboot-symbolic"; 82 | tooltip-text: "Reboot"; 83 | clicked => $show_reboot_confirm(); 84 | } 85 | 86 | Button { 87 | name: "Shutdown"; 88 | icon-name: "system-shutdown-symbolic"; 89 | tooltip-text: "Shutdown"; 90 | clicked => $show_shutdown_confirm(); 91 | } 92 | }; 93 | } 94 | 95 | StackPage { 96 | name: "confirm"; 97 | 98 | child: Box { 99 | hexpand: true; 100 | spacing: 5; 101 | halign: end; 102 | 103 | Button { 104 | icon-name: "window-close-symbolic"; 105 | clicked => $cancel_action(); 106 | 107 | styles [ 108 | "destructive-action", 109 | ] 110 | } 111 | 112 | Button confirm_button { 113 | icon-name: "object-select-symbolic"; 114 | clicked => $confirm_action(); 115 | 116 | styles [ 117 | "suggested-action", 118 | ] 119 | } 120 | }; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /data/ui/PowerMenu.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Astal 4.0; 3 | 4 | template $PowerMenu: Astal.Window { 5 | title: "PowerMenu"; 6 | namespace: "Morghulis.PowerMenu"; 7 | anchor: bottom; 8 | margin-bottom: 20; 9 | keymode: on_demand; 10 | layer: overlay; 11 | 12 | styles [ 13 | "all_unset", 14 | ] 15 | 16 | Box { 17 | orientation: vertical; 18 | spacing: 10; 19 | 20 | styles [ 21 | "padding_10", 22 | "rounded", 23 | "background", 24 | ] 25 | 26 | Label { 27 | label: bind template.uptime; 28 | halign: center; 29 | } 30 | 31 | Stack stapel { 32 | transition-type: slide_up_down; 33 | transition-duration: 200; 34 | 35 | StackPage { 36 | name: "actions"; 37 | 38 | child: Box { 39 | orientation: horizontal; 40 | spacing: 10; 41 | homogeneous: true; 42 | 43 | Button { 44 | tooltip-text: _("Lock Screen"); 45 | clicked => $just_lock(); 46 | 47 | Image { 48 | icon-name: "lockscreen-symbolic"; 49 | icon-size: large; 50 | } 51 | } 52 | 53 | Button { 54 | tooltip-text: _("Shutdown"); 55 | clicked => $set_shutdown(); 56 | 57 | Image { 58 | icon-name: "shutdown-symbolic"; 59 | icon-size: large; 60 | } 61 | } 62 | 63 | Button { 64 | tooltip-text: _("Restart"); 65 | clicked => $set_reboot(); 66 | 67 | Image { 68 | icon-name: "reboot-symbolic"; 69 | icon-size: large; 70 | } 71 | } 72 | 73 | Button { 74 | tooltip-text: _("Suspend"); 75 | clicked => $set_suspend(); 76 | 77 | Image { 78 | icon-name: "suspend-symbolic"; 79 | icon-size: large; 80 | } 81 | } 82 | 83 | Button { 84 | visible: false; 85 | tooltip-text: _("Hibernate"); 86 | clicked => $set_hibernate(); 87 | 88 | Image { 89 | icon-name: "hibernate-symbolic"; 90 | icon-size: large; 91 | } 92 | } 93 | 94 | Button { 95 | tooltip-text: _("Logout"); 96 | clicked => $set_logout(); 97 | 98 | Image { 99 | icon-name: "logout-symbolic"; 100 | icon-size: large; 101 | } 102 | } 103 | }; 104 | } 105 | 106 | StackPage { 107 | name: "confirmation"; 108 | 109 | child: Box { 110 | spacing: 10; 111 | halign: center; 112 | hexpand: true; 113 | homogeneous: true; 114 | 115 | Button { 116 | icon-name: "window-close-symbolic"; 117 | clicked => $cancel(); 118 | } 119 | 120 | Button { 121 | icon-name: "object-select-symbolic"; 122 | clicked => $confirm(); 123 | 124 | styles [ 125 | "destructive-action", 126 | ] 127 | } 128 | }; 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /data/ui/QAudioBox.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | using AstalWp 0.1; 4 | 5 | template $QAudioBox: Gtk.Box { 6 | orientation: vertical; 7 | 8 | styles [ 9 | "background", 10 | "rounded", 11 | "flats", 12 | ] 13 | 14 | Adw.HeaderBar { 15 | show-end-title-buttons: false; 16 | } 17 | 18 | Overlay { 19 | ScrolledWindow scrolled_window { 20 | hscrollbar-policy: never; 21 | vexpand: true; 22 | 23 | Box { 24 | vexpand: true; 25 | spacing: 10; 26 | orientation: vertical; 27 | 28 | styles [ 29 | "rounded", 30 | "padding_10", 31 | ] 32 | 33 | Box { 34 | spacing: 5; 35 | 36 | styles [ 37 | "view", 38 | "rounded", 39 | ] 40 | 41 | Button { 42 | icon-name: bind template.speaker as .volume-icon; 43 | clicked => $toggle_speaker(); 44 | } 45 | 46 | Scale { 47 | hexpand: true; 48 | adjustment: bind template.speaker_adj; 49 | 50 | styles [ 51 | "hslider", 52 | "rounded", 53 | "padding_5", 54 | ] 55 | } 56 | } 57 | 58 | Box { 59 | spacing: 5; 60 | 61 | styles [ 62 | "view", 63 | "rounded", 64 | ] 65 | 66 | Button { 67 | icon-name: bind template.microphone as .volume-icon; 68 | clicked => $toggle_microphone(); 69 | } 70 | 71 | Scale { 72 | hexpand: true; 73 | adjustment: bind template.microphone_adj; 74 | 75 | styles [ 76 | "hslider", 77 | "rounded", 78 | "padding_5", 79 | ] 80 | } 81 | } 82 | 83 | Separator {} 84 | 85 | Label { 86 | label: "Sinks"; 87 | halign: center; 88 | hexpand: true; 89 | } 90 | 91 | ListBox sinks { 92 | show-separators: false; 93 | selection-mode: none; 94 | overflow: hidden; 95 | 96 | styles [ 97 | "rounded", 98 | ] 99 | } 100 | 101 | Separator {} 102 | 103 | Label { 104 | label: "Sources"; 105 | halign: center; 106 | hexpand: true; 107 | } 108 | 109 | ListBox sources { 110 | show-separators: false; 111 | selection-mode: none; 112 | overflow: hidden; 113 | 114 | styles [ 115 | "rounded", 116 | ] 117 | } 118 | 119 | Separator {} 120 | 121 | Label { 122 | label: "Mixers"; 123 | halign: center; 124 | hexpand: true; 125 | } 126 | 127 | ListBox mixers { 128 | show-separators: false; 129 | selection-mode: none; 130 | overflow: hidden; 131 | 132 | styles [ 133 | "rounded", 134 | ] 135 | } 136 | } 137 | } 138 | 139 | [overlay] 140 | Revealer go_down_revealer { 141 | transition-duration: 200; 142 | transition-type: slide_up; 143 | reveal-child: true; 144 | halign: center; 145 | valign: end; 146 | 147 | Image { 148 | icon-name: "go-down-symbolic"; 149 | pixel-size: 24; 150 | margin-bottom: 10; 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /data/ui/QAudioItem.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using AstalWp 0.1; 3 | 4 | template $QAudioItem: Gtk.ListBoxRow { 5 | selectable: false; 6 | activatable: false; 7 | 8 | styles [ 9 | "padding_5", 10 | ] 11 | 12 | Box { 13 | spacing: 10; 14 | 15 | styles [ 16 | "x_padding_10", 17 | ] 18 | 19 | Image { 20 | icon-name: bind $fallback_icon(template.endpoint as .icon) as ; 21 | } 22 | 23 | Box { 24 | orientation: vertical; 25 | spacing: 5; 26 | 27 | Label { 28 | label: bind template.endpoint as .description; 29 | halign: start; 30 | 31 | styles [ 32 | "title-3", 33 | "x_padding_10", 34 | ] 35 | } 36 | 37 | Scale { 38 | hexpand: true; 39 | 40 | adjustment: Adjustment volume_adjust { 41 | lower: 0; 42 | upper: 1; 43 | }; 44 | 45 | styles [ 46 | "hslider", 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /data/ui/QBluetooth.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | using AstalBluetooth 0.1; 4 | 5 | template $QBluetooth: Box { 6 | orientation: vertical; 7 | 8 | Adw.HeaderBar { 9 | show-end-title-buttons: false; 10 | 11 | [end] 12 | Box { 13 | spacing: 10; 14 | 15 | Revealer { 16 | transition-duration: 200; 17 | transition-type: slide_left; 18 | reveal-child: bind template.bluetooth as .adapter as .discovering; 19 | 20 | Adw.Spinner spinner { 21 | tooltip-text: "What do you think?"; 22 | } 23 | } 24 | 25 | Button { 26 | icon-name: "view-refresh-symbolic"; 27 | tooltip-text: "Discover"; 28 | clicked => $toggle_discover(); 29 | } 30 | } 31 | } 32 | 33 | ScrolledWindow { 34 | vexpand: true; 35 | 36 | styles [ 37 | "background", 38 | "padding_10", 39 | ] 40 | 41 | ListBox blue_list { 42 | show-separators: true; 43 | selection-mode: none; 44 | 45 | styles [ 46 | "boxed-list", 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /data/ui/QBluetoothItem.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using AstalBluetooth 0.1; 3 | using Adw 1; 4 | 5 | template $QBluetoothItem: Gtk.ListBoxRow { 6 | GestureClick { 7 | pressed => $switch_connection(); 8 | } 9 | 10 | selectable: false; 11 | active: bind template.device as .connected; 12 | 13 | Gtk.Box { 14 | spacing: 10; 15 | hexpand: true; 16 | 17 | styles [ 18 | "padding_10", 19 | ] 20 | 21 | Image { 22 | icon-size: large; 23 | icon-name: bind $device_icon(template.device as .icon) as ; 24 | } 25 | 26 | Box { 27 | orientation: vertical; 28 | hexpand: true; 29 | spacing: 5; 30 | 31 | Label { 32 | halign: start; 33 | label: bind template.device as .name; 34 | 35 | styles [ 36 | "title-4", 37 | ] 38 | } 39 | 40 | Label { 41 | halign: start; 42 | label: bind template.device as .address; 43 | 44 | styles [ 45 | "dim-label", 46 | ] 47 | } 48 | } 49 | 50 | Revealer { 51 | halign: end; 52 | transition-duration: 200; 53 | transition-type: slide_left; 54 | reveal-child: bind template.device as .connecting; 55 | 56 | Adw.Spinner {} 57 | } 58 | 59 | Revealer { 60 | halign: end; 61 | transition-duration: 200; 62 | transition-type: slide_left; 63 | reveal-child: bind template.device as .connected; 64 | 65 | Box { 66 | orientation: vertical; 67 | halign: end; 68 | spacing: 5; 69 | 70 | Label battery_label { 71 | visible: bind $is_battery_a_real_thing(template.device as .battery-percentage) as ; 72 | label: bind $battery_percent(template.device as .battery-percentage) as ; 73 | halign: center; 74 | } 75 | 76 | Label { 77 | label: "Connected"; 78 | valign: center; 79 | vexpand: true; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /data/ui/QButton.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | template $QButton: Box { 4 | styles [ 5 | "master_border", 6 | "rounded", 7 | ] 8 | 9 | overflow: hidden; 10 | width-request: 70; 11 | height-request: 60; 12 | 13 | Button { 14 | clicked => $on_clicked(); 15 | hexpand: true; 16 | 17 | Box { 18 | orientation: horizontal; 19 | spacing: 10; 20 | hexpand: true; 21 | 22 | Image { 23 | icon-name: bind template.icon; 24 | pixel-size: 25; 25 | } 26 | 27 | Box { 28 | orientation: vertical; 29 | halign: center; 30 | valign: center; 31 | hexpand: true; 32 | spacing: 5; 33 | 34 | Label { 35 | label: bind template.identity; 36 | max-width-chars: 12; 37 | ellipsize: end; 38 | 39 | styles [ 40 | "title_5", 41 | ] 42 | } 43 | 44 | Label { 45 | label: bind template.status; 46 | max-width-chars: 12; 47 | ellipsize: end; 48 | 49 | styles [ 50 | "title_6", 51 | ] 52 | } 53 | } 54 | } 55 | } 56 | 57 | Separator { 58 | orientation: vertical; 59 | } 60 | 61 | Button { 62 | icon-name: "pan-end-symbolic"; 63 | clicked => $on_clicked_extras(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /data/ui/QNetwork.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | using AstalNetwork 0.1; 4 | 5 | template $QNetwork: Box { 6 | orientation: vertical; 7 | 8 | Adw.HeaderBar { 9 | show-end-title-buttons: false; 10 | 11 | [end] 12 | Box { 13 | spacing: 10; 14 | 15 | Revealer { 16 | transition-duration: 200; 17 | transition-type: slide_left; 18 | reveal-child: bind template.network as .scanning; 19 | 20 | Adw.Spinner spinner {} 21 | } 22 | 23 | Button { 24 | icon-name: "view-refresh-symbolic"; 25 | clicked => $refresh(); 26 | } 27 | } 28 | } 29 | 30 | ScrolledWindow { 31 | vexpand: true; 32 | 33 | styles [ 34 | "background", 35 | "padding_10", 36 | ] 37 | 38 | ListView wifi_list { 39 | show-separators: true; 40 | 41 | styles [ 42 | "rounded", 43 | ] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /data/ui/QNotifications.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $QNotifications: Box { 5 | orientation: vertical; 6 | 7 | Adw.HeaderBar { 8 | show-end-title-buttons: false; 9 | 10 | [end] 11 | Button { 12 | icon-name: "user-trash-symbolic"; 13 | clicked => $clear_notifications(); 14 | } 15 | } 16 | 17 | ScrolledWindow { 18 | overflow: hidden; 19 | vexpand: true; 20 | hscrollbar-policy: never; 21 | 22 | styles [ 23 | "background", 24 | "padding_10", 25 | ] 26 | 27 | ListBox notifications { 28 | selection-mode: none; 29 | show-separators: true; 30 | 31 | styles [ 32 | "boxed-list", 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /data/ui/QuickMenu.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Astal 4.0; 3 | 4 | template $QuickMenu: Astal.Window { 5 | styles [ 6 | "all_unset", 7 | ] 8 | 9 | default-width: 400; 10 | default-height: 1; 11 | title: "QuickMenu"; 12 | namespace: "Morghulis.QuickMenu"; 13 | anchor: bottom | right; 14 | margin-right: 5; 15 | margin-bottom: 5; 16 | 17 | Box { 18 | vexpand: true; 19 | orientation: vertical; 20 | spacing: 10; 21 | 22 | $Settings { 23 | // I hate this below 24 | height-request: 398; 25 | hexpand: true; 26 | vexpand: true; 27 | } 28 | 29 | $BatteryBox { 30 | hexpand: true; 31 | vexpand: false; 32 | } 33 | 34 | $PowerBox { 35 | hexpand: true; 36 | vexpand: false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /data/ui/Runner.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Astal 4.0; 3 | using Adw 1; 4 | 5 | template $Runner: Astal.Window { 6 | styles [ 7 | "all_unset", 8 | ] 9 | 10 | title: "Runner"; 11 | namespace: "Morghulis.Runner"; 12 | anchor: top; 13 | keymode: on_demand; 14 | 15 | EventControllerKey { 16 | key-released => $key_released(); 17 | } 18 | 19 | Box { 20 | orientation: vertical; 21 | width-request: 500; 22 | overflow: hidden; 23 | 24 | styles [ 25 | "bottom_left_right_corner_borders", 26 | ] 27 | 28 | Entry entry { 29 | height-request: 50; 30 | primary-icon-name: "system-search-symbolic"; 31 | changed => $update_list(); 32 | activate => $launch_first_runner_button(); 33 | placeholder-text: "Search..."; 34 | 35 | styles [ 36 | "background", 37 | "title-4", 38 | "top_left_right_corner_borders", 39 | ] 40 | } 41 | 42 | Adw.Bin math_bin { 43 | visible: false; 44 | 45 | styles [ 46 | "bottom_left_right_corner_borders", 47 | "padding_10", 48 | "view", 49 | ] 50 | 51 | Label math_label { 52 | justify: left; 53 | halign: start; 54 | valign: center; 55 | 56 | styles [ 57 | "title-2", 58 | "numeric-result", 59 | ] 60 | } 61 | } 62 | 63 | ScrolledWindow { 64 | max-content-height: 400; 65 | propagate-natural-height: true; 66 | 67 | ListBox app_list { 68 | selection-mode: none; 69 | overflow: hidden; 70 | 71 | styles [ 72 | "bg_transparent", 73 | "bottom_left_right_corner_borders", 74 | ] 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /data/ui/RunnerButton.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using AstalApps 0.1; 3 | 4 | template $RunnerButton: ListBoxRow { 5 | selectable: false; 6 | activatable: false; 7 | activate => $activated(); 8 | 9 | styles [ 10 | "padding_10", 11 | "view", 12 | ] 13 | 14 | child: Box { 15 | spacing: 10; 16 | 17 | Image { 18 | icon-size: large; 19 | icon-name: bind template.app as .icon-name; 20 | } 21 | 22 | Box { 23 | orientation: vertical; 24 | 25 | Label { 26 | halign: start; 27 | label: bind template.app as .name; 28 | ellipsize: end; 29 | } 30 | 31 | Label { 32 | halign: start; 33 | label: bind template.app as .description; 34 | ellipsize: end; 35 | 36 | styles [ 37 | "dim-label", 38 | ] 39 | } 40 | } 41 | }; 42 | 43 | GestureClick { 44 | pressed => $clicked(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /data/ui/Settings.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | using AstalNetwork 0.1; 4 | using AstalBluetooth 0.1; 5 | using AstalWp 0.1; 6 | using AstalNotifd 0.1; 7 | 8 | template $Settings: Adw.Bin { 9 | Adw.NavigationView quick_settings_navigation_view { 10 | styles [ 11 | "rounded", 12 | ] 13 | 14 | Adw.NavigationPage { 15 | title: "Quick settings"; 16 | 17 | Box { 18 | orientation: vertical; 19 | spacing: 10; 20 | 21 | Grid { 22 | column-homogeneous: true; 23 | hexpand: true; 24 | column-spacing: 10; 25 | row-spacing: 10; 26 | 27 | styles [ 28 | "background", 29 | "padding_10", 30 | "master_border", 31 | "rounded", 32 | "squares", 33 | ] 34 | 35 | $QButton { 36 | layout { 37 | row: 0; 38 | column: 0; 39 | } 40 | 41 | name: "Network"; 42 | identity: bind $network_identity(template.network as .wifi as .ssid) as ; 43 | status: bind $conn_status(template.network as .wifi as .enabled) as ; 44 | icon: bind template.network as .wifi as .icon-name; 45 | active: bind template.network as .wifi as .enabled; 46 | clicked => $network_clicked(); 47 | clicked_extras => $network_clicked_extras(); 48 | } 49 | 50 | $QButton { 51 | layout { 52 | row: 0; 53 | column: 1; 54 | } 55 | 56 | name: "Bluetooth"; 57 | identity: bind $bluetooth_identity(template.bluetooth as .adapter as .name) as ; 58 | status: bind $conn_status(template.bluetooth as .adapter as .powered) as ; 59 | icon: bind $bluetooth_icon_name(template.bluetooth as .is-powered) as ; 60 | active: bind template.bluetooth as .is-powered; 61 | clicked => $bluetooth_clicked(); 62 | clicked_extras => $bluetooth_clicked_extras(); 63 | } 64 | 65 | $QButton { 66 | layout { 67 | row: 1; 68 | column: 0; 69 | } 70 | 71 | name: "Audio"; 72 | identity: "Audio"; 73 | status: bind $audio_status(template.wp as .audio as .default-speaker as .mute) as ; 74 | icon: "audio-volume-high-symbolic"; 75 | inactive: bind template.wp as .audio as .default-speaker as .mute; 76 | clicked => $audio_clicked(); 77 | clicked_extras => $audio_clicked_extras(); 78 | } 79 | 80 | $QButton { 81 | layout { 82 | row: 1; 83 | column: 1; 84 | } 85 | 86 | name: "Don't disturb"; 87 | identity: "Notifications"; 88 | clicked => $notifications_clicked(); 89 | clicked_extras => $notifications_clicked_extras(); 90 | inactive: bind template.notifd as .dont-disturb; 91 | status: bind $notif_status(template.notifd as .dont-disturb) as ; 92 | icon: bind $notif_icon(template.notifd as .dont-disturb) as ; 93 | } 94 | } 95 | 96 | Stack { 97 | transition-type: slide_left_right; 98 | transition-duration: 200; 99 | visible-child-name: bind $mpris_stack(players.n-pages) as ; 100 | vexpand: true; 101 | 102 | StackPage { 103 | name: "no_mpris"; 104 | 105 | child: Box { 106 | orientation: vertical; 107 | spacing: 10; 108 | 109 | styles [ 110 | "background", 111 | "padding_10", 112 | "master_border", 113 | "rounded", 114 | ] 115 | 116 | Picture { 117 | paintable: bind template.no_media_players; 118 | } 119 | 120 | Label { 121 | label: "No Media Players"; 122 | 123 | styles [ 124 | "dim-label", 125 | "title-1", 126 | ] 127 | } 128 | }; 129 | } 130 | 131 | StackPage { 132 | name: "mpris"; 133 | 134 | child: Box { 135 | hexpand: true; 136 | orientation: vertical; 137 | 138 | styles [ 139 | "background", 140 | "padding_10", 141 | "master_border", 142 | "rounded", 143 | "flats", 144 | ] 145 | 146 | Adw.Carousel players { 147 | orientation: horizontal; 148 | spacing: 5; 149 | hexpand: true; 150 | } 151 | 152 | Adw.CarouselIndicatorDots { 153 | carousel: players; 154 | } 155 | }; 156 | } 157 | } 158 | } 159 | } 160 | 161 | Adw.NavigationPage { 162 | tag: "network"; 163 | title: "Network"; 164 | 165 | $QNetwork {} 166 | } 167 | 168 | Adw.NavigationPage { 169 | tag: "bluetooth"; 170 | title: "Bluetooth"; 171 | 172 | $QBluetooth {} 173 | } 174 | 175 | Adw.NavigationPage { 176 | tag: "audio"; 177 | title: "Audio"; 178 | 179 | $QAudioBox {} 180 | } 181 | 182 | Adw.NavigationPage { 183 | tag: "notifications"; 184 | title: "Notifications"; 185 | 186 | $QNotifications {} 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /data/ui/meson.build: -------------------------------------------------------------------------------- 1 | blueprints = custom_target( 2 | 'blueprints', 3 | input: files( 4 | 'MprisPlayer.blp', 5 | 'NavBar.blp', 6 | 'NotificationItem.blp', 7 | 'OnScreenDisplay.blp', 8 | 'PowerBox.blp', 9 | 'QBluetooth.blp', 10 | 'QBluetoothItem.blp', 11 | 'QButton.blp', 12 | 'QNetwork.blp', 13 | 'QNotifications.blp', 14 | 'BatteryBox.blp', 15 | 'QuickMenu.blp', 16 | 'Runner.blp', 17 | 'RunnerButton.blp', 18 | 'Settings.blp', 19 | 'QAudioBox.blp', 20 | 'QAudioItem.blp', 21 | 'PowerMenu.blp' 22 | ), 23 | output: '.', 24 | command: [ 25 | find_program('blueprint-compiler'), 26 | 'batch-compile', 27 | '@OUTPUT@', 28 | '@CURRENT_SOURCE_DIR@', 29 | '@INPUT@', 30 | ], 31 | ) -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "astal": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1740602158, 11 | "narHash": "sha256-tJKha3biAEnrA92vYsL/00jSfnrT/ONJRdAuj11Fc9E=", 12 | "owner": "aylur", 13 | "repo": "astal", 14 | "rev": "b7f10b99bc810e7ad6a949d6670cb440d33045a0", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "aylur", 19 | "repo": "astal", 20 | "type": "github" 21 | } 22 | }, 23 | "flake-utils": { 24 | "inputs": { 25 | "systems": "systems" 26 | }, 27 | "locked": { 28 | "lastModified": 1731533236, 29 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 30 | "owner": "numtide", 31 | "repo": "flake-utils", 32 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "numtide", 37 | "repo": "flake-utils", 38 | "type": "github" 39 | } 40 | }, 41 | "nixpkgs": { 42 | "locked": { 43 | "lastModified": 1740547748, 44 | "narHash": "sha256-Ly2fBL1LscV+KyCqPRufUBuiw+zmWrlJzpWOWbahplg=", 45 | "owner": "NixOS", 46 | "repo": "nixpkgs", 47 | "rev": "3a05eebede89661660945da1f151959900903b6a", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "id": "nixpkgs", 52 | "type": "indirect" 53 | } 54 | }, 55 | "root": { 56 | "inputs": { 57 | "astal": "astal", 58 | "flake-utils": "flake-utils", 59 | "nixpkgs": "nixpkgs" 60 | } 61 | }, 62 | "systems": { 63 | "locked": { 64 | "lastModified": 1681028828, 65 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 66 | "owner": "nix-systems", 67 | "repo": "default", 68 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "owner": "nix-systems", 73 | "repo": "default", 74 | "type": "github" 75 | } 76 | } 77 | }, 78 | "root": "root", 79 | "version": 7 80 | } 81 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Morghulis"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | astal = { 7 | url = "github:aylur/astal"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | 12 | outputs = 13 | { 14 | self, 15 | nixpkgs, 16 | flake-utils, 17 | astal, 18 | }: 19 | flake-utils.lib.eachDefaultSystem ( 20 | system: 21 | let 22 | pkgs = nixpkgs.legacyPackages.${system}; 23 | version = builtins.replaceStrings [ "\n" ] [ "" ] (builtins.readFile ./version); 24 | buildName = "morghulis"; 25 | appName = "${buildName}-${version}"; 26 | cliBuildName = "morghulctl"; 27 | cliAppName = "morghulctl-${version}"; 28 | stdenv = pkgs.gcc14Stdenv; 29 | 30 | nix-utils = with pkgs; [ 31 | nixd 32 | nixfmt-rfc-style 33 | ]; 34 | nix-morghulis = stdenv.mkDerivation { 35 | name = buildName; 36 | src = ./.; 37 | version = version; 38 | 39 | buildInputs = 40 | with pkgs; 41 | [ 42 | pkg-config 43 | ] 44 | ++ gtk-utils 45 | ++ runtime-utils 46 | ++ compiler-utils 47 | ++ build-utils 48 | ++ astal-libs; 49 | 50 | buildPhase = '' 51 | mkdir -p $TMPDIR/buildNix 52 | cd $TMPDIR/buildNix 53 | meson setup $TMPDIR/buildNix $src 54 | ninja -C $TMPDIR/buildNix 55 | ''; 56 | 57 | installPhase = '' 58 | mkdir -p $out/bin 59 | cp -r $TMPDIR/buildNix/src/${buildName} $out/bin/${appName} 60 | cp -r $TMPDIR/buildNix/cli/${cliBuildName} $out/bin/${cliAppName} 61 | chmod +x $out/bin/${appName} $out/bin/${cliAppName} 62 | ''; 63 | 64 | meta = with pkgs.lib; { 65 | description = "A Desktop Shell for Wayland"; 66 | license = licenses.wtfpl; 67 | homepage = "https://github.com/ARKye03/morghulis"; 68 | maintainers = with maintainers; [ ARKye03 ]; 69 | }; 70 | }; 71 | fhs-morghulis = nix-morghulis.overrideAttrs { 72 | installPhase = '' 73 | mkdir -p $out/bin 74 | cp -r $TMPDIR/buildNix/src/${buildName} $out/bin/${appName} 75 | cp -r $TMPDIR/buildNix/cli/${cliBuildName} $out/bin/${cliAppName} 76 | 77 | # Patch the binary 78 | ${pkgs.patchelf}/bin/patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 $out/bin/${appName} 79 | ${pkgs.patchelf}/bin/patchelf --set-rpath /lib:/usr/lib $out/bin/${appName} 80 | ${pkgs.patchelf}/bin/patchelf --shrink-rpath $out/bin/${appName} 81 | 82 | # Patch the cli binary 83 | ${pkgs.patchelf}/bin/patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 $out/bin/${cliAppName} 84 | ${pkgs.patchelf}/bin/patchelf --set-rpath /lib:/usr/lib $out/bin/${cliAppName} 85 | ${pkgs.patchelf}/bin/patchelf --shrink-rpath $out/bin/${cliAppName} 86 | chmod +x $out/bin/${appName} $out/bin/${cliAppName} 87 | ''; 88 | }; 89 | pkg-tarball = 90 | pkgs.runCommand "morghulis-tarball" 91 | { 92 | src = ./.; 93 | buildInputs = [ 94 | pkgs.gnutar 95 | pkgs.xz 96 | ]; 97 | } 98 | '' 99 | # Create staging directory 100 | staging="$TMPDIR/staging" 101 | mkdir -p "$staging" 102 | 103 | # Define asset files 104 | filenames=( 105 | "data/desktop/com.github.ARKye03.morghulis.desktop.in" 106 | ) 107 | 108 | # Copy binaries to staging 109 | cp ${fhs-morghulis}/bin/${appName} "$staging/" 110 | cp ${fhs-morghulis}/bin/${cliAppName} "$staging/" 111 | 112 | # Copy assets to staging with directory structure 113 | for filename in "''${filenames[@]}"; do 114 | mkdir -p "$staging/$(dirname "$filename")" 115 | cp "$src/$filename" "$staging/$filename" 116 | done 117 | 118 | # Create output directory 119 | mkdir -p $out 120 | 121 | # Create tarball from staging directory 122 | cd "$staging" 123 | tar --sort=name \ 124 | --owner=0 --group=0 \ 125 | --mtime='1970-01-01 00:00:00' \ 126 | -cJf "$out/morghulis-${version}.tar.xz" . 127 | ''; 128 | gtk-utils = with pkgs; [ 129 | gtk4 130 | gtk4-layer-shell 131 | libadwaita 132 | ]; 133 | runtime-utils = with pkgs; [ 134 | gsound 135 | libgtop 136 | ]; 137 | compiler-utils = with pkgs; [ 138 | vala 139 | vala-language-server 140 | vala-lint 141 | uncrustify 142 | dart-sass 143 | blueprint-compiler 144 | desktop-file-utils 145 | ]; 146 | build-utils = with pkgs.buildPackages; [ 147 | muon 148 | meson 149 | ninja 150 | ]; 151 | astal-libs = with astal.packages.${system}; [ 152 | hyprland 153 | wireplumber 154 | mpris 155 | network 156 | notifd 157 | river 158 | apps 159 | tray 160 | io 161 | astal4 162 | battery 163 | powerprofiles 164 | bluetooth 165 | ]; 166 | gstPlugins = with pkgs.gst_all_1; [ 167 | gstreamer 168 | gst-plugins-base 169 | ]; 170 | shell = 171 | pkgs.mkShell.override 172 | { 173 | stdenv = stdenv; 174 | } 175 | { 176 | nativeBuildInputs = 177 | with pkgs.buildPackages; 178 | [ 179 | glfw-wayland 180 | gobject-introspection 181 | ] 182 | ++ nix-utils 183 | ++ gtk-utils 184 | ++ runtime-utils 185 | ++ compiler-utils 186 | ++ build-utils 187 | ++ astal-libs; 188 | buildInputs = 189 | with pkgs; 190 | [ 191 | pkg-config 192 | networkmanager 193 | glib 194 | gdk-pixbuf 195 | json-glib 196 | ] 197 | ++ gstPlugins; 198 | GTK_THEME = "adw-gtk3:dark"; 199 | XCURSOR_THEME = "Bibata-Modern-Classic"; 200 | XCURSOR_SIZE = "20"; 201 | }; 202 | in 203 | { 204 | packages = { 205 | default = nix-morghulis; 206 | fhs = fhs-morghulis; 207 | tarball = pkg-tarball; 208 | }; 209 | apps = { 210 | default = { 211 | type = "app"; 212 | program = "${nix-morghulis}/bin/${appName}"; 213 | }; 214 | fhs = { 215 | type = "app"; 216 | program = "${fhs-morghulis}/bin/${appName}"; 217 | }; 218 | }; 219 | devShells.default = shell; 220 | } 221 | ); 222 | } 223 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | APP_NAME := "morghulis" 2 | BIN_DIR := "build" 3 | CLI_APP_NAME := "morghulctl" 4 | 5 | run: build 6 | ./{{BIN_DIR}}/src/{{APP_NAME}} 7 | 8 | cli: build 9 | ./{{BIN_DIR}}/cli/{{CLI_APP_NAME}} 10 | 11 | init: 12 | meson setup build 13 | 14 | rinit: 15 | meson setup --reconfigure build 16 | 17 | build: 18 | ninja -C {{BIN_DIR}} 19 | 20 | dist: 21 | meson -C {{BIN_DIR}} --no-tests dist 22 | 23 | install: 24 | meson install -C {{BIN_DIR}} 25 | 26 | uninstall: 27 | ninja uninstall -C {{BIN_DIR}} 28 | 29 | clean: 30 | ninja clean -C {{BIN_DIR}} 31 | 32 | prune: 33 | rm -rf {{BIN_DIR}} -------------------------------------------------------------------------------- /lib/Backlight/backlight.rules: -------------------------------------------------------------------------------- 1 | # Allow members of the video group to change the brightness 2 | ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chgrp video /sys/class/backlight/%k/brightness" 3 | ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chmod g+w /sys/class/backlight/%k/brightness" -------------------------------------------------------------------------------- /lib/Backlight/backlight.vala: -------------------------------------------------------------------------------- 1 | // Refactor this shite 2 | public class Backlight : Object { 3 | private static Backlight _instance; 4 | private FileMonitor? _brightness_monitor; 5 | private uint _brightness; 6 | private double _percentage; 7 | private uint _max_brightness; 8 | private string _brightness_file_path; 9 | 10 | public string brightness_interface_name { get; private set; } 11 | 12 | public static Backlight get_default() { 13 | if (_instance == null) { 14 | _instance = new Backlight(); 15 | } 16 | return _instance; 17 | } 18 | 19 | public uint brightness { 20 | get { 21 | return _brightness; 22 | } 23 | set { 24 | if (value < 0) { 25 | _brightness = 0; 26 | } else if (value > _max_brightness) { 27 | _brightness = _max_brightness; 28 | } else { 29 | _brightness = value; 30 | set_brightness_file(_brightness); 31 | } 32 | } 33 | } 34 | 35 | public string icon_name { owned get; private set; default = "display-brightness-symbolic"; } 36 | public double percentage { 37 | get { return _percentage; } 38 | set { 39 | if (value < 0.0) { 40 | _percentage = 0.0; 41 | } else if (value > 1.0) { 42 | _percentage = 1.0; 43 | } else { 44 | _percentage = value; 45 | } 46 | uint new_brightness = (uint)(_percentage * _max_brightness); 47 | if (new_brightness != _brightness) { 48 | brightness = new_brightness; 49 | } 50 | } 51 | } 52 | 53 | construct { 54 | if (!load_interface()) { 55 | return; 56 | } 57 | 58 | load_max_brightness(); 59 | load_brightness_sync(); 60 | load_brightness(); 61 | 62 | // Create bidirectional binding between brightness and percentage 63 | this.bind_property( 64 | "brightness", this, "percentage", 65 | BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE, 66 | (binding, src_val, ref target_val) => { 67 | uint brightness_val = (uint)src_val; 68 | target_val = brightness_val / (double)_max_brightness; 69 | return true; 70 | }, 71 | (binding, src_val, ref target_val) => { 72 | double percentage_val = (double)src_val; 73 | target_val = (uint)(percentage_val * _max_brightness); 74 | return true; 75 | } 76 | ); 77 | } 78 | 79 | private bool load_interface() { 80 | try { 81 | string? name = null; 82 | string[] interfaces = {}; 83 | 84 | Dir dir = Dir.open("/sys/class/backlight", 0); 85 | if (dir == null) { 86 | debug("No backlight interface found"); 87 | return false; 88 | } 89 | 90 | // Read all available interfaces (not just the first one) 91 | while ((name = dir.read_name()) != null) { 92 | if (name != "." && name != "..") { 93 | interfaces += name; 94 | } 95 | } 96 | 97 | if (interfaces.length == 0) { 98 | debug("No backlight interfaces found"); 99 | return false; 100 | } 101 | 102 | // Use the first valid interface 103 | brightness_interface_name = interfaces[0]; 104 | _brightness_file_path = "/sys/class/backlight/" + brightness_interface_name; 105 | debug("Using backlight interface: %s", brightness_interface_name); 106 | return true; 107 | } catch (FileError e) { 108 | if (e.code == FileError.NOENT) { 109 | debug("No backlight interface found"); 110 | return false; 111 | } else { 112 | critical("Error opening backlight directory: %s", e.message); 113 | return false; 114 | } 115 | } 116 | } 117 | 118 | private bool set_brightness_file(uint value) { 119 | try { 120 | var file = File.new_for_path(@"$(_brightness_file_path)/brightness"); 121 | if (file.query_exists()) { 122 | var os = file.replace(null, false, FileCreateFlags.NONE); 123 | var dos = new DataOutputStream(os); 124 | dos.put_string(value.to_string()); 125 | debug("Set brightness file to: %u", value); 126 | return true; 127 | } 128 | } catch (Error e) { 129 | critical("Error writing brightness: %s", e.message); 130 | } 131 | return false; 132 | } 133 | 134 | private void load_max_brightness() { 135 | var max_brightness_file = File.new_for_path(@"$(_brightness_file_path)/max_brightness"); 136 | if (max_brightness_file.query_exists()) { 137 | try { 138 | uint8[] contents; 139 | string etag_out; 140 | if (max_brightness_file.load_contents(null, out contents, out etag_out)) { 141 | string content = (string)contents; 142 | _max_brightness = uint.parse(content.strip()); 143 | debug("Max brightness: %u", _max_brightness); 144 | } 145 | } catch (Error e) { 146 | critical("Error reading max brightness: %s", e.message); 147 | } 148 | } 149 | } 150 | 151 | private void load_brightness() { 152 | try { 153 | var brightness_file = File.new_for_path(@"$(_brightness_file_path)/brightness"); 154 | 155 | _brightness_monitor = brightness_file.monitor_file(FileMonitorFlags.NONE); 156 | _brightness_monitor.changed.connect((file, other_file, event_type) => { 157 | if (event_type == FileMonitorEvent.CHANGED || 158 | event_type == FileMonitorEvent.CREATED) { 159 | debug("Brightness file changed externally, syncing..."); 160 | sync_brightness.begin(brightness_file); 161 | } 162 | }); 163 | 164 | // Initial sync 165 | sync_brightness.begin(brightness_file); 166 | } catch (Error e) { 167 | critical("Error setting up brightness monitor: %s", e.message); 168 | } 169 | } 170 | 171 | private async void sync_brightness(File brightness_file) { 172 | try { 173 | uint8[] contents; 174 | string etag_out; 175 | 176 | yield brightness_file.load_contents_async(null, out contents, out etag_out); 177 | 178 | if (contents != null) { 179 | string content = (string)contents; 180 | uint new_brightness = uint.parse(content.strip()); 181 | debug("External brightness change detected: %u", new_brightness); 182 | 183 | if (new_brightness != _brightness) { 184 | this.brightness = new_brightness; 185 | } 186 | } 187 | } catch (Error e) { 188 | critical("Error reading brightness: %s", e.message); 189 | } 190 | } 191 | 192 | private void load_brightness_sync() { 193 | try { 194 | var file = File.new_for_path(@"$(_brightness_file_path)/actual_brightness"); 195 | if (!file.query_exists()) { 196 | file = File.new_for_path(@"$(_brightness_file_path)/brightness"); 197 | if (!file.query_exists()) { 198 | critical("Cannot find brightness file"); 199 | return; 200 | } 201 | } 202 | 203 | uint8[] contents; 204 | string etag_out; 205 | if (file.load_contents(null, out contents, out etag_out)) { 206 | string content = (string)contents; 207 | uint new_val = uint.parse(content.strip()); 208 | brightness = new_val; 209 | debug("Initial brightness: %u (%.1f%%)", _brightness, _percentage * 100); 210 | } 211 | } catch (Error e) { 212 | critical("Error reading initial brightness: %s", e.message); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/Backlight/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('backlight.vala') 2 | 3 | install_data( 4 | 'backlight.rules', 5 | install_dir: get_option('sysconfdir') / 'udev' / 'rules.d', 6 | rename: '90-backlight.rules' 7 | ) 8 | 9 | meson.add_install_script('reload_udev_rules.sh') -------------------------------------------------------------------------------- /lib/Backlight/reload_udev_rules.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Reload udev rules 4 | if command -v udevadm &> /dev/null; then 5 | echo "Reloading udev rules..." 6 | udevadm control --reload-rules 7 | udevadm trigger --subsystem-match=backlight 8 | fi 9 | 10 | # Ensure the script is executable 11 | chmod +x "$0" -------------------------------------------------------------------------------- /lib/Math/ast.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef enum { 4 | NODE_NUMBER, 5 | NODE_BINARY_OP 6 | } NodeType; 7 | 8 | typedef enum { 9 | OP_ADD, 10 | OP_SUBTRACT, 11 | OP_MULTIPLY, 12 | OP_DIVIDE 13 | } OperatorType; 14 | 15 | typedef struct ASTNode { 16 | NodeType type; 17 | union { 18 | double number; 19 | struct { 20 | OperatorType op; 21 | struct ASTNode* left; 22 | struct ASTNode* right; 23 | } binary; 24 | } data; 25 | } ASTNode; 26 | -------------------------------------------------------------------------------- /lib/Math/evaluator.c: -------------------------------------------------------------------------------- 1 | #include "evaluator.h" 2 | #include "parser.h" 3 | #include 4 | 5 | EvalResult evaluate_expression(const char* input) { 6 | if (!input) { 7 | return create_error("Null input"); 8 | } 9 | 10 | Parser* parser = parser_create(input); 11 | if (!parser) { 12 | return create_error("Failed to create parser"); 13 | } 14 | 15 | ParseResult parse_result = parser_parse(parser); 16 | 17 | if (parse_result.error) { 18 | parser_destroy(parser); 19 | return create_error(parse_result.error); 20 | } 21 | 22 | EvalResult result = evaluate_node(parse_result.node); 23 | 24 | free_ast_node(parse_result.node); 25 | parser_destroy(parser); 26 | 27 | return result; 28 | } 29 | 30 | EvalResult evaluate_node(ASTNode* node) { 31 | switch (node->type) { 32 | case NODE_NUMBER: 33 | return create_result(node->data.number); 34 | 35 | case NODE_BINARY_OP: { 36 | EvalResult left = evaluate_node(node->data.binary.left); 37 | if (left.error) { 38 | return left; 39 | } 40 | 41 | EvalResult right = evaluate_node(node->data.binary.right); 42 | if (right.error) { 43 | return right; 44 | } 45 | 46 | switch (node->data.binary.op) { 47 | case OP_ADD: return create_result(left.value + right.value); 48 | 49 | case OP_SUBTRACT: return create_result(left.value - right.value); 50 | 51 | case OP_MULTIPLY: return create_result(left.value * right.value); 52 | 53 | case OP_DIVIDE: 54 | if (right.value == 0) { 55 | return create_error("Division by zero"); 56 | } 57 | return create_result(left.value / right.value); 58 | } 59 | } 60 | } 61 | return create_error("Unknown node type"); 62 | } 63 | 64 | EvalResult create_result(double value) { 65 | EvalResult result = { .value = value, .error = NULL }; 66 | 67 | return result; 68 | } 69 | 70 | EvalResult create_error(const char* error) { 71 | EvalResult result = { .value = 0, .error = error }; 72 | 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /lib/Math/evaluator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "parser.h" 3 | 4 | typedef struct { 5 | double value; 6 | const char* error; 7 | } EvalResult; 8 | 9 | EvalResult create_result(double value); 10 | EvalResult create_error(const char* error); 11 | EvalResult evaluate_node(ASTNode* node); 12 | EvalResult evaluate_expression(const char* input); 13 | -------------------------------------------------------------------------------- /lib/Math/lexer.c: -------------------------------------------------------------------------------- 1 | #include "lexer.h" 2 | #include 3 | #include 4 | #include 5 | 6 | Lexer* lexer_create(const char* input) { 7 | Lexer* lexer = malloc(sizeof(Lexer)); 8 | 9 | lexer->input = input; 10 | lexer->position = 0; 11 | lexer->length = strlen(input); 12 | return lexer; 13 | } 14 | 15 | void lexer_destroy(Lexer* lexer) { 16 | free(lexer); 17 | } 18 | 19 | static void skip_whitespace(Lexer* lexer) { 20 | while (lexer->position < lexer->length && 21 | isspace(lexer->input[lexer->position])) { 22 | lexer->position++; 23 | } 24 | } 25 | 26 | static Token create_token(TokenType type) { 27 | Token token = { type, 0.0, NULL }; 28 | 29 | return token; 30 | } 31 | 32 | static Token create_number(double value) { 33 | Token token = { NUMBER, value, NULL }; 34 | 35 | return token; 36 | } 37 | 38 | static Token create_error(const char* message) { 39 | Token token = { ERROR, 0.0, message }; 40 | 41 | return token; 42 | } 43 | 44 | Token lexer_next_token(Lexer* lexer) { 45 | skip_whitespace(lexer); 46 | 47 | if (lexer->position >= lexer->length) { 48 | return create_token(EOF); 49 | } 50 | 51 | char c = lexer->input[lexer->position]; 52 | lexer->position++; 53 | 54 | switch (c) { 55 | case '+': return create_token(PLUS); 56 | 57 | case '-': return create_token(MINUS); 58 | 59 | case '*': return create_token(MULTIPLY); 60 | 61 | case '/': return create_token(DIVIDE); 62 | 63 | case '(': return create_token(LPAREN); 64 | 65 | case ')': return create_token(RPAREN); 66 | 67 | default: 68 | if (isdigit(c) || c == '.') { 69 | lexer->position--; 70 | char* endptr; 71 | double value = strtod(lexer->input + lexer->position, &endptr); 72 | if (endptr == lexer->input + lexer->position) { 73 | return create_error("Invalid number"); 74 | } 75 | lexer->position = endptr - lexer->input; 76 | return create_number(value); 77 | } 78 | return create_error("Invalid character"); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/Math/lexer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef enum { 4 | NUMBER, 5 | PLUS, 6 | MINUS, 7 | MULTIPLY, 8 | DIVIDE, 9 | LPAREN, 10 | RPAREN, 11 | EOF, 12 | ERROR 13 | } TokenType; 14 | 15 | typedef struct { 16 | TokenType type; 17 | double value; 18 | const char* error_msg; 19 | } Token; 20 | 21 | typedef struct { 22 | const char* input; 23 | int position; 24 | int length; 25 | } Lexer; 26 | 27 | Lexer* lexer_create(const char* input); 28 | void lexer_destroy(Lexer* lexer); 29 | Token lexer_next_token(Lexer* lexer); 30 | -------------------------------------------------------------------------------- /lib/Math/meson.build: -------------------------------------------------------------------------------- 1 | c_sources = files('evaluator.c', 'lexer.c', 'mpars.c', 'parser.c') 2 | c_headers = files('ast.h', 'evaluator.h', 'lexer.h', 'mpars.h', 'parser.h') 3 | 4 | project_resources = c_sources + c_headers -------------------------------------------------------------------------------- /lib/Math/mpars.c: -------------------------------------------------------------------------------- 1 | #include "mpars.h" 2 | #include 3 | #include "evaluator.h" 4 | 5 | double mpars_evaluate(const char* expression, char** error) { 6 | EvalResult result = evaluate_expression(expression); 7 | 8 | if (result.error) { 9 | *error = strdup(result.error); 10 | return 0.0; 11 | } 12 | *error = NULL; 13 | return result.value; 14 | } 15 | -------------------------------------------------------------------------------- /lib/Math/mpars.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "glib-2.0/glib.h" 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | double mpars_evaluate(const char* expression, char** error); 9 | 10 | #ifdef __cplusplus 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /lib/Math/parser.c: -------------------------------------------------------------------------------- 1 | #include "parser.h" 2 | #include 3 | 4 | static OperatorType token_to_op(TokenType token) { 5 | switch (token) { 6 | case PLUS: return OP_ADD; 7 | 8 | case MINUS: return OP_SUBTRACT; 9 | 10 | case MULTIPLY: return OP_MULTIPLY; 11 | 12 | case DIVIDE: return OP_DIVIDE; 13 | 14 | // Should never happen 15 | default: return OP_ADD; 16 | } 17 | } 18 | 19 | static ParseResult create_result(ASTNode* node) { 20 | ParseResult result = { node, NULL }; 21 | 22 | return result; 23 | } 24 | 25 | static ParseResult create_error(const char* message) { 26 | ParseResult result = { NULL, message }; 27 | 28 | return result; 29 | } 30 | 31 | static ASTNode* create_number_node(double value) { 32 | ASTNode* node = malloc(sizeof(ASTNode)); 33 | 34 | node->type = NODE_NUMBER; 35 | node->data.number = value; 36 | return node; 37 | } 38 | 39 | static ASTNode* create_binary_op_node(ASTNode* left, TokenType op, ASTNode* right) { 40 | ASTNode* node = malloc(sizeof(ASTNode)); 41 | 42 | node->type = NODE_BINARY_OP; 43 | node->data.binary.left = left; 44 | node->data.binary.op = token_to_op(op); 45 | node->data.binary.right = right; 46 | return node; 47 | } 48 | 49 | static void advance_token(Parser* parser) { 50 | parser->current_token = lexer_next_token(parser->lexer); 51 | } 52 | 53 | Parser* parser_create(const char* input) { 54 | Parser* parser = malloc(sizeof(Parser)); 55 | 56 | parser->lexer = lexer_create(input); 57 | advance_token(parser); 58 | return parser; 59 | } 60 | 61 | void parser_destroy(Parser* parser) { 62 | lexer_destroy(parser->lexer); 63 | free(parser); 64 | } 65 | 66 | void free_ast_node(ASTNode* node) { 67 | if (node) { 68 | switch (node->type) { 69 | case NODE_NUMBER: 70 | free(node); 71 | break; 72 | 73 | case NODE_BINARY_OP: 74 | free_ast_node(node->data.binary.left); 75 | free_ast_node(node->data.binary.right); 76 | free(node); 77 | break; 78 | } 79 | } 80 | } 81 | 82 | // Forward declarations for recursive descent 83 | static ParseResult parse_expression(Parser* parser); 84 | static ParseResult parse_term(Parser* parser); 85 | static ParseResult parse_factor(Parser* parser); 86 | 87 | static ParseResult parse_factor(Parser* parser) { 88 | Token token = parser->current_token; 89 | 90 | if (token.type == NUMBER) { 91 | advance_token(parser); 92 | return create_result(create_number_node(token.value)); 93 | } 94 | 95 | if (token.type == LPAREN) { 96 | advance_token(parser); 97 | ParseResult result = parse_expression(parser); 98 | if (result.error) { 99 | return result; 100 | } 101 | 102 | if (parser->current_token.type != RPAREN) { 103 | free_ast_node(result.node); 104 | return create_error("Expected ')'"); 105 | } 106 | advance_token(parser); 107 | return result; 108 | } 109 | 110 | return create_error("Expected number or '('"); 111 | } 112 | 113 | static ParseResult parse_term(Parser* parser) { 114 | ParseResult left = parse_factor(parser); 115 | 116 | if (left.error) { 117 | return left; 118 | } 119 | 120 | while (parser->current_token.type == MULTIPLY || 121 | parser->current_token.type == DIVIDE) { 122 | TokenType op = parser->current_token.type; 123 | advance_token(parser); 124 | 125 | ParseResult right = parse_factor(parser); 126 | if (right.error) { 127 | free_ast_node(left.node); 128 | return right; 129 | } 130 | 131 | if (op == DIVIDE && right.node->data.number == 0) { 132 | free_ast_node(left.node); 133 | free_ast_node(right.node); 134 | return create_error("Division by zero"); 135 | } 136 | 137 | left.node = create_binary_op_node(left.node, op, right.node); 138 | } 139 | return left; 140 | } 141 | 142 | static ParseResult parse_expression(Parser* parser) { 143 | ParseResult left = parse_term(parser); 144 | 145 | if (left.error) { 146 | return left; 147 | } 148 | 149 | while (parser->current_token.type == PLUS || 150 | parser->current_token.type == MINUS) { 151 | TokenType op = parser->current_token.type; 152 | advance_token(parser); 153 | 154 | ParseResult right = parse_term(parser); 155 | if (right.error) { 156 | free_ast_node(left.node); 157 | return right; 158 | } 159 | 160 | left.node = create_binary_op_node(left.node, op, right.node); 161 | } 162 | 163 | return left; 164 | } 165 | 166 | ParseResult parser_parse(Parser* parser) { 167 | ParseResult result = parse_expression(parser); 168 | 169 | if (result.error) { 170 | return result; 171 | } 172 | 173 | if (parser->current_token.type != EOF) { 174 | free_ast_node(result.node); 175 | return create_error("Unexpected token"); 176 | } 177 | 178 | return result; 179 | } 180 | -------------------------------------------------------------------------------- /lib/Math/parser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "lexer.h" 3 | #include "ast.h" 4 | 5 | typedef struct { 6 | Lexer* lexer; 7 | Token current_token; 8 | } Parser; 9 | 10 | typedef struct { 11 | ASTNode* node; 12 | const char* error; 13 | } ParseResult; 14 | 15 | Parser* parser_create(const char* input); 16 | void parser_destroy(Parser* parser); 17 | ParseResult parser_parse(Parser* parser); 18 | void free_ast_node(ASTNode* node); 19 | -------------------------------------------------------------------------------- /lib/meson.build: -------------------------------------------------------------------------------- 1 | subdir('Math') 2 | subdir('Backlight') -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | 2 | project( 3 | 'morghulis', 4 | ['vala', 'c'], 5 | version: run_command('cat', 'version').stdout().strip(), 6 | meson_version: '>= 1.4.0', 7 | ) 8 | 9 | gnome = import('gnome') 10 | i18n = import('i18n') 11 | 12 | i18n.gettext('morghulis', preset: 'glib') 13 | 14 | project_name = meson.project_name() 15 | project_resources = [] 16 | 17 | subdir('lib') 18 | subdir('data') 19 | subdir('src') 20 | subdir('cli') 21 | 22 | gnome.post_install( 23 | glib_compile_schemas: true, 24 | gtk_update_icon_cache: true, 25 | update_desktop_database: true, 26 | ) 27 | -------------------------------------------------------------------------------- /public/morghulis.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARKye03/morghulis/b5fc77f00ccebdb870538cff6bc4a3723de929a4/public/morghulis.webp -------------------------------------------------------------------------------- /scripts/ensureTypes.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | BEGIN { print "void ensure_types() {" } 3 | /^\s*namespace\s*/ { namespace = $2 } 4 | /\s*(public\s+)(sealed\s+)?class\s*/ { 5 | class_name = ($2 == "class" ? $3 : ($3 == "class" ? $4: $NF)) 6 | if(namespace) 7 | print " typeof(" namespace "." class_name ").ensure();" 8 | else 9 | print " typeof(" class_name ").ensure();" 10 | } 11 | ENDFILE { namespace = "" } 12 | END { print "}" } 13 | -------------------------------------------------------------------------------- /scripts/genVersion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # Exit immediately if a command exits with a non-zero status. 4 | 5 | # Check if a tag argument is provided 6 | if [ $# -eq 0 ]; then 7 | echo "Please provide a version tag as an argument (e.g., v0.1.1)" 8 | exit 1 9 | fi 10 | 11 | tag=$1 12 | 13 | # Check if the tag already exists 14 | if git rev-parse "$tag" >/dev/null 2>&1; then 15 | echo "Error: Tag $tag already exists. Please choose a different tag." 16 | exit 1 17 | fi 18 | 19 | # Generate version file 20 | echo "$tag" >version 21 | 22 | git cliff --unreleased --tag "$tag" -o CHANGELOG.md 23 | git add version CHANGELOG.md 24 | git commit -m "chore: bump version to $tag" 25 | git tag -s "$tag" -m "Release $tag" 26 | 27 | echo "Version bumped to $tag, CHANGELOG.md updated, and signed tag created." 28 | echo "Please review the changes and push with:" 29 | echo "git push origin main $tag" 30 | -------------------------------------------------------------------------------- /src/App.vala: -------------------------------------------------------------------------------- 1 | public class Morghulis : Astal.Application { 2 | private bool _css_loaded; 3 | private File _css_file; 4 | private FileMonitor _css_file_monitor; 5 | private GTop.Uptime _g_uptime; 6 | 7 | public static Morghulis instance { get; private set; } 8 | public static Gdk.Display? display { get; private set; } 9 | public static Gdk.Monitor? primary_monitor { get; private set; } 10 | public static string clock_format { get; private set; default = "%H:%M %b %e"; } 11 | public static string user_name { get; private set; } 12 | 13 | public string uptime { get; private set; } 14 | 15 | public override void request(string msg, SocketConnection conn) { 16 | switch (msg) { 17 | case "change_volume": 18 | OnScreenDisplay.instance.change_volume(); 19 | break; 20 | 21 | case "change_brightness": 22 | OnScreenDisplay.instance.change_brightness(); 23 | break; 24 | 25 | default: 26 | AstalIO.write_sock.begin(conn, @"missing response implementation on $instance_name"); 27 | break; 28 | } 29 | } 30 | 31 | construct { 32 | Adw.init(); 33 | instance_name = "morghulis"; 34 | 35 | try { 36 | acquire_socket(); 37 | } catch (Error e) { 38 | critical("%s", e.message); 39 | } 40 | instance = this; 41 | 42 | _css_file = File.new_for_path(@"$(Environment.get_user_config_dir())/morghulis/main.css"); 43 | try { 44 | _css_file_monitor = _css_file.monitor_file( 45 | GLib.FileMonitorFlags.WATCH_HARD_LINKS 46 | | GLib.FileMonitorFlags.WATCH_MOUNTS 47 | | GLib.FileMonitorFlags.WATCH_MOVES 48 | ); 49 | uint count = 0; 50 | _css_file_monitor.changed.connect((file, other_file, event_type) => { 51 | if (event_type == FileMonitorEvent.CHANGED) { 52 | apply_css(_css_file.get_path(), true); 53 | print(@"\033[34mCSS Reloaded:\033[0m \033[33mx$(++count)\033[0m\n"); 54 | } else if (event_type == FileMonitorEvent.CREATED) { 55 | apply_css(_css_file.get_path(), true); 56 | print("\033[34mCSS File Created!\033[0m\n"); 57 | } 58 | }); 59 | } catch (IOError e) { 60 | critical("Error: %s\n", e.message); 61 | } 62 | } 63 | 64 | [DBus(visible = false)] 65 | public override void activate() { 66 | base.activate(); 67 | setup_display_and_monitor(); 68 | Gtk.IconTheme.get_for_display(display).add_resource_path("/com/github/ARKye03/morghulis/icons"); 69 | user_name = Environment.get_user_name(); 70 | 71 | if (!_css_loaded) { 72 | load_css(); 73 | _css_loaded = true; 74 | } 75 | 76 | if (_css_file.query_exists()) { 77 | apply_css(_css_file.get_path(), true); 78 | } 79 | 80 | add_window(new NavBar()); 81 | add_window(new Runner()); 82 | add_window(new QuickMenu()); 83 | add_window(new OnScreenDisplay()); 84 | add_window(new NotifPopItemsCenter()); 85 | add_window(new PowerMenu()); 86 | 87 | Timeout.add_seconds(60, () => { 88 | sync_uptime(); 89 | return true; 90 | }); 91 | sync_uptime(); 92 | 93 | this.hold(); 94 | } 95 | 96 | private void setup_display_and_monitor() { 97 | display = Gdk.Display.get_default(); 98 | if (display == null) { 99 | critical("Failed to get default display"); 100 | return; 101 | } 102 | var monitors = display.get_monitors(); 103 | if (monitors == null) { 104 | critical("Failed to get monitors"); 105 | return; 106 | } 107 | // Morghulis assume there is only one monitor 108 | primary_monitor = (Gdk.Monitor)monitors.get_item(0); 109 | if (primary_monitor == null) { 110 | critical("Failed to get primary monitor"); 111 | return; 112 | } 113 | message("Successfully initialized primary monitor"); 114 | } 115 | 116 | // Function made to HAVE ONLY ONE: `Gtk.StyleContext' has been deprecated since 4.10 117 | private void add_css_provider(Gtk.CssProvider provider) { 118 | Gtk.StyleContext.add_provider_for_display( 119 | Gdk.Display.get_default(), 120 | provider, 121 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 122 | ); 123 | } 124 | 125 | private void load_css() { 126 | var provider = new Gtk.CssProvider(); 127 | 128 | provider.load_from_resource("com/github/ARKye03/morghulis/morghulis.css"); 129 | add_css_provider(provider); 130 | } 131 | 132 | private void sync_uptime() { 133 | GTop.get_uptime(out _g_uptime); 134 | var uptime_hours = Math.floor(_g_uptime.uptime / 3600); 135 | var uptime_minutes = Math.floor((_g_uptime.uptime % 3600) / 60); 136 | 137 | if (uptime_hours <= 0) { 138 | uptime = @"Up for $uptime_minutes minutes"; 139 | } else { 140 | uptime = @"Up $uptime_hours hours, and $uptime_minutes minutes"; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Main.vala: -------------------------------------------------------------------------------- 1 | public static void main(string[] args) { 2 | var app = new Morghulis(); 3 | 4 | init_types(); 5 | app.run(args); 6 | } 7 | 8 | private void init_types() { 9 | #if river 10 | typeof(RiverTags).ensure(); 11 | #endif 12 | #if hyprland 13 | typeof(HyprWorkspaces).ensure(); 14 | #endif 15 | typeof(QuickMenu).ensure(); 16 | typeof(Backlight).ensure(); 17 | typeof(SysInfo).ensure(); 18 | typeof(QButton).ensure(); 19 | typeof(QNetwork).ensure(); 20 | typeof(QBluetooth).ensure(); 21 | typeof(BatteryBox).ensure(); 22 | typeof(QAudioBox).ensure(); 23 | typeof(PowerBox).ensure(); 24 | typeof(Settings).ensure(); 25 | typeof(OnScreenDisplay).ensure(); 26 | typeof(NotificationItem).ensure(); 27 | typeof(QNotifications).ensure(); 28 | typeof(CircularProgressBar).ensure(); 29 | typeof(Tray).ensure(); 30 | } 31 | -------------------------------------------------------------------------------- /src/Modules/Gizmo.vala: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper widget to create custom widgets 3 | * 4 | * Based on private GtkGizmo widget 5 | */ 6 | internal class Gizmo : Gtk.Widget { 7 | public delegate void MeasureFunc( 8 | Gtk.Orientation orientation, int for_size, 9 | out int minimum, out int natural, 10 | out int minimum_baseline, out int natural_baseline 11 | ); 12 | 13 | public delegate void AllocateFunc( 14 | int width, 15 | int height, 16 | int baseline 17 | ); 18 | 19 | public delegate void SnapshotFunc( 20 | Gtk.Snapshot snapshot 21 | ); 22 | 23 | public delegate bool ContainsFunc( 24 | double x, 25 | double y 26 | ); 27 | 28 | public delegate bool FocusFunc( 29 | Gtk.DirectionType direction 30 | ); 31 | 32 | public delegate bool GrabFocusFunc(); 33 | 34 | private unowned MeasureFunc? measure_func; 35 | private unowned AllocateFunc? allocate_func; 36 | private unowned SnapshotFunc? snapshot_func; 37 | private unowned ContainsFunc? contains_func; 38 | private unowned FocusFunc? focus_func; 39 | private unowned GrabFocusFunc? grab_focus_func; 40 | 41 | public Gizmo( 42 | string css_name, 43 | MeasureFunc? measure_func, 44 | AllocateFunc? allocate_func, 45 | SnapshotFunc? snapshot_func, 46 | ContainsFunc? contains_func, 47 | FocusFunc? focus_func, 48 | GrabFocusFunc? grab_focus_func 49 | ) { 50 | Object(css_name: css_name); 51 | this.measure_func = measure_func; 52 | this.allocate_func = allocate_func; 53 | this.snapshot_func = snapshot_func; 54 | this.contains_func = contains_func; 55 | this.focus_func = focus_func; 56 | this.grab_focus_func = grab_focus_func; 57 | } 58 | 59 | public Gizmo.with_role( 60 | string css_name, 61 | Gtk.AccessibleRole role, 62 | MeasureFunc? measure_func, 63 | AllocateFunc? allocate_func, 64 | SnapshotFunc? snapshot_func, 65 | ContainsFunc? contains_func, 66 | FocusFunc? focus_func, 67 | GrabFocusFunc? grab_focus_func 68 | ) { 69 | Object(css_name: css_name, accessible_role: role); 70 | this.measure_func = measure_func; 71 | this.allocate_func = allocate_func; 72 | this.snapshot_func = snapshot_func; 73 | this.contains_func = contains_func; 74 | this.focus_func = focus_func; 75 | this.grab_focus_func = grab_focus_func; 76 | } 77 | 78 | public override void measure( 79 | Gtk.Orientation orientation, 80 | int for_size, 81 | out int minimum, 82 | out int natural, 83 | out int minimum_baseline, 84 | out int natural_baseline 85 | ) { 86 | if (measure_func != null) { 87 | measure_func(orientation, for_size, 88 | out minimum, out natural, 89 | out minimum_baseline, out natural_baseline); 90 | } else { 91 | minimum = natural = minimum_baseline = natural_baseline = 0; 92 | } 93 | } 94 | 95 | public override void size_allocate(int width, int height, int baseline) { 96 | if (allocate_func != null) { 97 | allocate_func(width, height, baseline); 98 | } 99 | } 100 | 101 | public override void snapshot(Gtk.Snapshot snapshot) { 102 | if (snapshot_func != null) { 103 | snapshot_func(snapshot); 104 | } else { 105 | base.snapshot(snapshot); 106 | } 107 | } 108 | 109 | public override bool contains(double x, double y) { 110 | if (contains_func != null) { 111 | return contains_func(x, y); 112 | } 113 | return base.contains(x, y); 114 | } 115 | 116 | public override bool focus(Gtk.DirectionType direction) { 117 | if (focus_func != null) { 118 | return focus_func(direction); 119 | } 120 | return false; 121 | } 122 | 123 | public override bool grab_focus() { 124 | if (grab_focus_func != null) { 125 | return grab_focus_func(); 126 | } 127 | return false; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Modules/ScrollingLabel.vala: -------------------------------------------------------------------------------- 1 | /** 2 | * Original work from https://github.com/kotontrion/kompass 3 | */ 4 | public enum ScrollBehaviour { 5 | ALTERNATE, 6 | SLIDE 7 | } 8 | 9 | public class ScrollingLabel : Gtk.Widget { 10 | private Gtk.Label _scroll_label; 11 | private double _position = 0; 12 | private int _scroll_direction = -1; 13 | private int64 _last_time = 0; 14 | private int64 _delay = 0; 15 | 16 | public double speed { get; set; default = 0.5; } 17 | 18 | public int direction_change_delay { get; set; default = 500; } 19 | 20 | public Gtk.Orientation direction { get; set; default = Gtk.Orientation.HORIZONTAL; } 21 | 22 | public ScrollBehaviour behaviour { get; set; default = ScrollBehaviour.ALTERNATE; } 23 | 24 | public string label { 25 | get { return this._scroll_label.label; } 26 | set { this._scroll_label.label = value; } 27 | } 28 | 29 | construct { 30 | this._scroll_label = new Gtk.Label(""); 31 | 32 | this.overflow = Gtk.Overflow.HIDDEN; 33 | 34 | this._scroll_label.set_parent(this); 35 | this.add_tick_callback(update_position); 36 | } 37 | 38 | protected override void measure(Gtk.Orientation orientation, 39 | int for_size, 40 | out int minimum, 41 | out int natural, 42 | out int minimum_baseline, 43 | out int natural_baseline) { 44 | int min = 0; 45 | int nat = 0; 46 | 47 | this._scroll_label.measure(orientation, -1, out min, out nat, null, null); 48 | minimum = 0; 49 | natural = nat; 50 | minimum_baseline = -1; 51 | natural_baseline = -1; 52 | } 53 | 54 | protected override void size_allocate(int width, int height, int baseline) { 55 | int child_width = 0; 56 | int child_height = 0; 57 | 58 | Gtk.Requisition child_req; 59 | this._scroll_label.get_preferred_size(out child_req, null); 60 | 61 | child_width = child_req.width; 62 | child_height = child_req.height; 63 | if (this.direction == Gtk.Orientation.HORIZONTAL) { 64 | this._scroll_label.allocate_size({ (int)this._position, 0, child_width, child_height }, -1); 65 | } else { 66 | this._scroll_label.allocate_size({ 0, (int)this._position, child_width, child_height }, -1); 67 | } 68 | } 69 | 70 | protected override Gtk.SizeRequestMode get_request_mode() { 71 | return Gtk.SizeRequestMode.CONSTANT_SIZE; 72 | } 73 | 74 | private bool update_position(Gtk.Widget widget, Gdk.FrameClock clock) { 75 | int64 current_time = clock.get_frame_time(); 76 | 77 | if (this._last_time == 0) { 78 | this._last_time = current_time; 79 | return Source.CONTINUE; 80 | } 81 | 82 | int64 elapsed = current_time - this._last_time; 83 | this._last_time = current_time; 84 | double delta = this.speed * elapsed / 10000; 85 | 86 | bool is_horizontal = (this.direction == Gtk.Orientation.HORIZONTAL); 87 | double limit = is_horizontal ? this.get_width() : this.get_height(); 88 | double label_size = is_horizontal ? this._scroll_label.get_width() : this._scroll_label.get_height(); 89 | 90 | if (this._delay >= 0) { 91 | this._delay += elapsed / 1000; 92 | if (this._delay > this.direction_change_delay) { 93 | this._delay = -1; 94 | } else { 95 | return Source.CONTINUE; 96 | } 97 | } 98 | 99 | if (this.behaviour == ScrollBehaviour.ALTERNATE) { 100 | if (this._scroll_direction < 0) { 101 | if (this._position + label_size > limit) { 102 | this._position = double.max(limit - label_size, this._position - delta); 103 | } else { 104 | this._scroll_direction = 1; 105 | this._delay = 0; 106 | } 107 | } else { 108 | if (this._position < 0) { 109 | this._position = double.min(0, this._position + delta); 110 | } else { 111 | this._scroll_direction = -1; 112 | this._delay = 0; 113 | } 114 | } 115 | } else { 116 | if (this._position + label_size > 0) { 117 | this._position -= delta; 118 | } else { 119 | this._position = limit; 120 | } 121 | } 122 | 123 | this.queue_resize(); 124 | return Source.CONTINUE; 125 | } 126 | 127 | ~ScrollingLabel() { 128 | this._scroll_label.unparent(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Modules/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('CircularProgress.vala', 'Gizmo.vala', 'ScrollingLabel.vala') -------------------------------------------------------------------------------- /src/NavBar/HyprWorkspaces.vala: -------------------------------------------------------------------------------- 1 | public class HyprWorkspaces : Gtk.Box { 2 | private AstalHyprland.Hyprland _hyprland; 3 | 4 | public HyprWorkspaces(AstalHyprland.Hyprland hyprland) { 5 | this._hyprland = hyprland; 6 | this.spacing = 5; 7 | 8 | for (var i = 1; i <= 9; i++) { 9 | var workspace_button = new Gtk.Button() { 10 | child = new Gtk.Image.from_icon_name(NavBar.icon_names[i - 1]) { 11 | pixel_size = 20 12 | }, 13 | }; 14 | connect_button_to_workspace(workspace_button, i); 15 | this.append(workspace_button); 16 | } 17 | update_workspaces(); 18 | setup_workspace_event_handlers(); 19 | setup_workspace_scroll(); 20 | } 21 | 22 | private void setup_workspace_event_handlers() { 23 | _hyprland.notify["focused-workspace"].connect(update_workspaces); 24 | _hyprland.client_added.connect(update_workspaces); 25 | _hyprland.client_removed.connect(update_workspaces); 26 | _hyprland.client_moved.connect(update_workspaces); 27 | } 28 | 29 | private void setup_workspace_scroll() { 30 | var scroll_controller = new Gtk.EventControllerScroll(Gtk.EventControllerScrollFlags.VERTICAL); 31 | 32 | scroll_controller.scroll.connect((delta_x, delta_y) => { 33 | string direction = delta_y > 0 ? "e-1" : "e+1"; 34 | _hyprland.dispatch("workspace", direction); 35 | return true; 36 | }); 37 | this.add_controller(scroll_controller); 38 | } 39 | 40 | private void update_workspaces() { 41 | int index = 0; 42 | var current = (Gtk.Button)this.get_first_child(); 43 | var focused_workspace_id = this._hyprland.focused_workspace.id; 44 | 45 | while (current != null) { 46 | if (index + 1 == focused_workspace_id) { 47 | current.set_css_classes({ "focused" }); 48 | } else if (workspace_exists(index + 1)) { 49 | current.set_css_classes({ "occupied" }); 50 | } else { 51 | current.set_css_classes({ "empty" }); 52 | } 53 | current = (Gtk.Button)current.get_next_sibling(); 54 | index++; 55 | } 56 | } 57 | 58 | private void connect_button_to_workspace(Gtk.Button button, int workspace_number) { 59 | var middle_click = new Gtk.GestureClick() { 60 | button = Gdk.BUTTON_MIDDLE 61 | }; 62 | 63 | middle_click.pressed.connect(() => { 64 | _hyprland.dispatch("movetoworkspacesilent", workspace_number.to_string()); 65 | }); 66 | button.add_controller(middle_click); 67 | button.clicked.connect(() => { 68 | _hyprland.dispatch("workspace", workspace_number.to_string()); 69 | }); 70 | } 71 | 72 | private bool workspace_exists(int workspace_number) { 73 | var workspace = _hyprland.get_workspace(workspace_number); 74 | 75 | return workspace != null && workspace.clients != null && workspace.clients.length() > 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/NavBar/NavBar.vala: -------------------------------------------------------------------------------- 1 | using GtkLayerShell; 2 | 3 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/NavBar.ui")] 4 | public class NavBar : Astal.Window { 5 | private GLib.DateTime _clock_time; 6 | 7 | public static NavBar instance { get; private set; } 8 | public AstalBattery.Device battery { get; private set; } 9 | public AstalWp.Endpoint speaker { get; private set; } 10 | public string current_time { get; private set; } 11 | public static string[] icon_names = { 12 | "terminal-symbolic", 13 | "browser-symbolic", 14 | "code-symbolic", 15 | "explorer-symbolic", 16 | "social-symbolic", 17 | "docs-symbolic", 18 | "media-symbolic", 19 | "settings-symbolic", 20 | "gaming-symbolic", 21 | }; 22 | 23 | [GtkChild] 24 | private unowned Adw.Bin workspaces; 25 | 26 | [GtkChild] 27 | private unowned Adw.Bin active_client; 28 | 29 | [GtkChild] 30 | private unowned Adw.Bin active_submap; 31 | 32 | public NavBar() { 33 | battery = AstalBattery.Device.get_default(); 34 | speaker = AstalWp.get_default().audio.default_speaker; 35 | 36 | init_compositor(); 37 | init_clock(); 38 | 39 | instance = this; 40 | present(); 41 | } 42 | 43 | [GtkCallback] 44 | public void toggle_side_dashboard() { 45 | QuickMenu.instance.visible = !QuickMenu.instance.visible; 46 | } 47 | 48 | [GtkCallback] 49 | public void toggle_runner() { 50 | Runner.instance.visible = !Runner.instance.visible; 51 | } 52 | 53 | [GtkCallback] 54 | public bool scroll_volume(double dx, double dy) { 55 | if (dy > 0) { 56 | speaker.volume = double.max(speaker.volume - 0.05, 0); 57 | } else { 58 | speaker.volume = double.min(speaker.volume + 0.05, 1); 59 | } 60 | return true; 61 | } 62 | 63 | [GtkCallback] 64 | public void toggle_volume() { 65 | speaker.mute = !speaker.mute; 66 | } 67 | 68 | [GtkCallback] 69 | public string current_volume(double volume) { 70 | return @"$(Math.round(volume * 100))%"; 71 | } 72 | 73 | [GtkCallback] 74 | public string current_battery(double percentage) { 75 | return @"$(Math.round(percentage * 100))%"; 76 | } 77 | 78 | private void update_clock() { 79 | _clock_time = new DateTime.now_local(); 80 | current_time = _clock_time.format(Morghulis.clock_format); 81 | } 82 | 83 | private void init_clock() { 84 | Timeout.add(60000, () => { 85 | update_clock(); 86 | return true; 87 | }); 88 | update_clock(); 89 | } 90 | 91 | private void init_compositor() { 92 | string current_session = Environment.get_variable("XDG_CURRENT_DESKTOP"); 93 | 94 | #if hyprland 95 | if (current_session == "Hyprland") { 96 | setup_hyprland(); 97 | } 98 | #if river 99 | else if (current_session == "river") { 100 | setup_river(); 101 | } 102 | #endif 103 | #else 104 | #if river 105 | if (current_session == "river") { 106 | setup_river(); 107 | } 108 | #endif 109 | #endif 110 | if (current_session != "Hyprland" && current_session != "river") { 111 | warning("No Hyprland or River detected"); 112 | } 113 | } 114 | 115 | #if hyprland 116 | private AstalHyprland.Hyprland _hyprland; 117 | 118 | private void setup_hyprland() { 119 | message("Setting up Hyprland"); 120 | _hyprland = AstalHyprland.Hyprland.get_default(); 121 | workspaces.child = new HyprWorkspaces(_hyprland); 122 | 123 | Gtk.Label submap_label = new Gtk.Label("default") { 124 | halign = Gtk.Align.START, 125 | ellipsize = Pango.EllipsizeMode.END, 126 | max_width_chars = 20, 127 | tooltip_text = "Active submap" 128 | }; 129 | 130 | active_submap.child = submap_label; 131 | 132 | _hyprland.submap.connect((value) => { 133 | if (value != null && value != "") { 134 | submap_label.label = value; 135 | active_submap.visible = true; 136 | } else { 137 | active_submap.visible = false; 138 | } 139 | }); 140 | 141 | Gtk.Label client_label = new Gtk.Label("default") { 142 | halign = Gtk.Align.START, 143 | ellipsize = Pango.EllipsizeMode.END, 144 | max_width_chars = 20, 145 | tooltip_text = "Active client", 146 | }; 147 | 148 | _hyprland.bind_property("focused_client", client_label, "label", BindingFlags.SYNC_CREATE, (binding, srcval, ref targetval) => { 149 | var client = (AstalHyprland.Client)srcval; 150 | if (client != null && client.title != null && client.title != "") { 151 | targetval = client.title; 152 | active_client.visible = true; 153 | } else { 154 | active_client.visible = false; 155 | } 156 | return true; 157 | }); 158 | 159 | active_client.child = client_label; 160 | } 161 | #endif 162 | 163 | #if river 164 | private AstalRiver.River _river; 165 | 166 | private void setup_river() { 167 | message("Setting up River"); 168 | _river = AstalRiver.River.get_default(); 169 | 170 | workspaces.child = new RiverTags(_river); 171 | 172 | Gtk.Label view_label = new Gtk.Label("default") { 173 | halign = Gtk.Align.START, 174 | ellipsize = Pango.EllipsizeMode.END, 175 | max_width_chars = 20, 176 | tooltip_text = "Active View" 177 | }; 178 | active_client.child = view_label; 179 | 180 | _river.bind_property("focused-view", view_label, "label", BindingFlags.SYNC_CREATE, (_, src, ref trgt) => { 181 | var view_title = (string)src; 182 | if (view_title != null && view_title != "") { 183 | trgt = view_title; 184 | active_client.visible = true; 185 | } else { 186 | active_client.visible = false; 187 | } 188 | return true; 189 | }); 190 | } 191 | #endif 192 | } 193 | -------------------------------------------------------------------------------- /src/NavBar/RiverTags.vala: -------------------------------------------------------------------------------- 1 | public class TagButton : Gtk.Button { 2 | private AstalRiver.Output _output; 3 | private Gtk.GestureClick _rclick; 4 | private int _index; 5 | 6 | public TagButton(AstalRiver.Output output, int index, string icon) { 7 | this._output = output; 8 | this._index = index; 9 | child = new Gtk.Image.from_icon_name(icon) { 10 | pixel_size = 20 11 | }; 12 | add_css_class("empty"); 13 | this._rclick = new Gtk.GestureClick() { 14 | button = Gdk.BUTTON_SECONDARY, 15 | }; 16 | 17 | clicked.connect(() => { 18 | this._output.focused_tags = 1 << this._index; 19 | }); 20 | _rclick.pressed.connect(() => { 21 | this._output.focused_tags ^= 1 << this._index; 22 | }); 23 | add_controller(_rclick); 24 | } 25 | 26 | public void update_css() { 27 | uint occupied_tags = _output.occupied_tags; 28 | uint focused_tags = _output.focused_tags; 29 | uint urgent_tags = _output.urgent_tags; 30 | 31 | if ((focused_tags & (1 << _index)) != 0) { 32 | set_css_classes({ "focused" }); 33 | } else if ((urgent_tags & (1 << _index)) != 0) { 34 | set_css_classes({ "urgent" }); 35 | } else if ((occupied_tags & (1 << _index)) != 0) { 36 | set_css_classes({ "occupied" }); 37 | } else { 38 | set_css_classes({ "empty" }); 39 | } 40 | } 41 | } 42 | 43 | public class RiverTags : Gtk.Box { 44 | private AstalRiver.River _river; 45 | private AstalRiver.Output _output; 46 | private uint _total_tags; 47 | private List _tags; 48 | private const string SHIFTTAGS_PREV = "river-shifttags --occupied --shifts -1"; 49 | private const string SHIFTTAGS_NEXT = "river-shifttags --occupied"; 50 | 51 | public RiverTags(AstalRiver.River river, uint max_tags = 9) { 52 | this._river = river; 53 | string focused_output = river.get_focused_output(); 54 | this._output = river.get_output(focused_output); 55 | this._tags = new List(); 56 | this._total_tags = max_tags; 57 | 58 | spacing = 5; 59 | 60 | for (int i = 0; i < _total_tags; i++) { 61 | var tag_button = new TagButton(_output, i, NavBar.icon_names[i]); 62 | this.append(tag_button); 63 | _tags.append(tag_button); 64 | } 65 | 66 | _output.changed.connect(update_css); 67 | update_css(); 68 | 69 | setup_scroll_handler(); 70 | } 71 | 72 | private void setup_scroll_handler() { 73 | bool shifttags_available = check_shifttags(); 74 | 75 | if (shifttags_available) { 76 | var scroll_controller = new Gtk.EventControllerScroll(Gtk.EventControllerScrollFlags.VERTICAL); 77 | scroll_controller.scroll.connect((delta_x, delta_y) => { 78 | string command = delta_y > 0 ? SHIFTTAGS_PREV : SHIFTTAGS_NEXT; 79 | try { 80 | Process.spawn_command_line_async(command); 81 | } catch (SpawnError e) { 82 | warning("Failed to execute %s: %s", command, e.message); 83 | } 84 | return true; 85 | }); 86 | this.add_controller(scroll_controller); 87 | } else { 88 | warning("River-shifttags not found, please install it to use the tags feature"); 89 | } 90 | } 91 | 92 | private bool check_shifttags() { 93 | try { 94 | string standard_output; 95 | string standard_error; 96 | int wait_status; 97 | Process.spawn_command_line_sync("which river-shifttags", 98 | out standard_output, 99 | out standard_error, 100 | out wait_status); 101 | return wait_status == 0; 102 | } catch (SpawnError e) { 103 | warning("Failed to check for command river-shifttags: %s", e.message); 104 | return false; 105 | } 106 | } 107 | 108 | private void update_css() { 109 | foreach (var tag_button in _tags) { 110 | tag_button.update_css(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/NavBar/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('NavBar.vala') 2 | 3 | if astal_hyprland_dep.found() 4 | project_resources += files('HyprWorkspaces.vala') 5 | project_args += ['--define=hyprland'] 6 | endif 7 | 8 | if astal_river_dep.found() 9 | project_resources += files('RiverTags.vala') 10 | project_args += ['--define=river'] 11 | endif -------------------------------------------------------------------------------- /src/Notifications/NotificationItem.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/NotificationItem.ui")] 2 | public class NotificationItem : Gtk.ListBoxRow { 3 | public AstalNotifd.Notification notification { get; set; } 4 | 5 | [GtkChild] 6 | public unowned Gtk.Box actions_box; 7 | 8 | public NotificationItem(AstalNotifd.Notification notification) { 9 | Object( 10 | notification: notification 11 | ); 12 | setup_actions(); 13 | setup_urgency(); 14 | } 15 | 16 | [GtkCallback] 17 | public string current_time(int64 t) { 18 | DateTime dt = new DateTime.from_unix_local(t); 19 | 20 | return dt.format("%H:%M"); 21 | } 22 | 23 | [GtkCallback] 24 | public void dismiss_notif() { 25 | this.notification.dismiss(); 26 | } 27 | 28 | private void setup_urgency() { 29 | if (notification.urgency == AstalNotifd.Urgency.CRITICAL) { 30 | this.add_css_class("critical"); 31 | } else if (notification.urgency == AstalNotifd.Urgency.LOW) { 32 | this.add_css_class("low"); 33 | } else { 34 | this.add_css_class("normal"); 35 | } 36 | } 37 | 38 | private void setup_actions() { 39 | notification.actions.@foreach(a => { 40 | Gtk.Button action = new Gtk.Button.with_label(a.label); 41 | action.clicked.connect(() => this.notification.invoke(a.id)); 42 | this.actions_box.append(action); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Notifications/NotificationPopupsCenter.vala: -------------------------------------------------------------------------------- 1 | public class NotifPopItemsCenter : Astal.Window { 2 | private AstalNotifd.Notifd _notifd; 3 | private Gtk.ListBox _notif_list_box; 4 | private GSound.Context _scontext; 5 | private uint _notif_count = 0; 6 | 7 | public NotifPopItemsCenter(Astal.WindowAnchor x_anchor = Astal.WindowAnchor.RIGHT) { 8 | Object( 9 | title: "Notifications", 10 | anchor: Astal.WindowAnchor.TOP | x_anchor 11 | ); 12 | 13 | setup_sound(); 14 | setup_window(); 15 | setup_notifications(); 16 | } 17 | 18 | private void setup_window() { 19 | this.default_width = 330; 20 | this.default_height = 0; 21 | this.margin = 5; 22 | this.css_classes = { "all_unset", "rounded" }; 23 | this.overflow = Gtk.Overflow.HIDDEN; 24 | this.notify["visible"].connect(() => { 25 | if (visible) { 26 | this.default_height = -1; 27 | } else { 28 | this.default_height = 0; 29 | } 30 | }); 31 | 32 | this._notif_list_box = new Gtk.ListBox() { 33 | selection_mode = Gtk.SelectionMode.NONE, 34 | css_classes = { "boxed-list" } 35 | }; 36 | 37 | this.child = _notif_list_box; 38 | } 39 | 40 | private void setup_notifications() { 41 | this._notifd = AstalNotifd.Notifd.get_default(); 42 | this._notifd.notified.connect((id, replace) => this.handle_notification(id, replace)); 43 | this._notifd.resolved.connect((id) => this.remove_notification(id)); 44 | } 45 | 46 | private void handle_notification(uint notification_id, bool replace) { 47 | if (replace) { 48 | remove_notification(notification_id); 49 | } 50 | 51 | var notification = _notifd.get_notification(notification_id); 52 | var notif_item = new NotificationItem(notification); 53 | this._notif_list_box.prepend(notif_item); 54 | this._notif_count++; 55 | 56 | uint timeout_ms = notification.expire_timeout > 0 ? notification.expire_timeout * 1000 : 3000; 57 | Timeout.add(timeout_ms, () => { 58 | remove_notification(notification_id); 59 | return false; 60 | }); 61 | this.visible = true; 62 | this.play_notification_sound.begin(); 63 | } 64 | 65 | private void setup_sound() { 66 | try { 67 | this._scontext = new GSound.Context(); 68 | this._scontext.init(); 69 | } catch (Error e) { 70 | warning("Failed to create sound context: %s", e.message); 71 | } 72 | } 73 | 74 | private async void play_notification_sound() { 75 | if (!this._notifd.dont_disturb) { 76 | try { 77 | yield this._scontext.play_full( 78 | null, 79 | GSound.Attribute.EVENT_ID, 80 | "message" 81 | ); 82 | } catch (Error e) { 83 | warning("Failed to play sound: %s", e.message); 84 | } 85 | } 86 | } 87 | 88 | private void remove_notification(uint notification_id) { 89 | NotificationItem? notif_popup = (NotificationItem)_notif_list_box.get_first_child(); 90 | 91 | while (notif_popup != null) { 92 | if (notif_popup.notification.id == notification_id) { 93 | this._notif_list_box.remove(notif_popup); 94 | this._notif_count--; 95 | break; 96 | } 97 | notif_popup = (NotificationItem)notif_popup.get_next_sibling(); 98 | } 99 | if (this._notif_count == 0) { 100 | this.visible = false; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Notifications/QNotifications.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QNotifications.ui")] 2 | public class QNotifications : Gtk.Box { 3 | private AstalNotifd.Notifd notifd { get; set; } 4 | 5 | [GtkChild] 6 | private unowned Gtk.ListBox notifications; 7 | [GtkCallback] 8 | public void clear_notifications() { 9 | this.notifd.notifications.@foreach(n => n.dismiss()); 10 | } 11 | 12 | construct { 13 | notifd = AstalNotifd.get_default(); 14 | 15 | this.notifd.notifications.@foreach(n => this.on_notification_added(n.id, false, this.notifications)); 16 | this.notifd.notified.connect((id, replace) => { 17 | this.on_notification_added(id, replace, this.notifications); 18 | }); 19 | this.notifd.resolved.connect((id) => this.remove_notification(id, this.notifications)); 20 | } 21 | 22 | private void on_notification_added(uint notification_id, bool is_replaced, Gtk.ListBox notif_list_box) { 23 | if (is_replaced) { 24 | remove_notification(notification_id, notif_list_box); 25 | } 26 | 27 | var notification = notifd.get_notification(notification_id); 28 | notif_list_box.prepend(new NotificationItem(notification)); 29 | } 30 | 31 | private void remove_notification(uint notification_id, Gtk.ListBox notif_list_box) { 32 | NotificationItem? notif_popup = (NotificationItem)notif_list_box.get_first_child(); 33 | 34 | while (notif_popup != null) { 35 | if (notif_popup.notification.id == notification_id) { 36 | notif_list_box.remove(notif_popup); 37 | break; 38 | } 39 | notif_popup = (NotificationItem)notif_popup.get_next_sibling(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Notifications/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files( 2 | 'NotificationItem.vala', 3 | 'QNotifications.vala', 4 | 'NotificationPopupsCenter.vala' 5 | ) -------------------------------------------------------------------------------- /src/OnScreenDisplay/OnScreenDisplay.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/OnScreenDisplay.ui")] 2 | public class OnScreenDisplay : Astal.Window { 3 | private uint _hide_timeout_id = 0; 4 | 5 | public static OnScreenDisplay instance { get; private set; } 6 | public AstalWp.Endpoint speaker { get; private set; } 7 | public Backlight backlight { get; private set; } 8 | 9 | [GtkChild] 10 | public unowned Gtk.Stack stack_osd; 11 | 12 | construct { 13 | if (instance == null) { 14 | instance = this; 15 | } else { 16 | this.destroy(); 17 | } 18 | speaker = AstalWp.get_default().audio.default_speaker; 19 | backlight = Backlight.get_default(); 20 | } 21 | 22 | private void handle_timeout() { 23 | // Remove the existing timeout if it exists 24 | if (_hide_timeout_id != 0) { 25 | GLib.Source.remove(_hide_timeout_id); 26 | _hide_timeout_id = 0; 27 | } 28 | 29 | // Set a new timeout 30 | _hide_timeout_id = GLib.Timeout.add(3000, () => { 31 | this.visible = false; 32 | _hide_timeout_id = 0; 33 | return false; 34 | }); 35 | } 36 | 37 | public void change_volume() { 38 | this.visible = true; 39 | this.stack_osd.visible_child_name = "volume_osd"; 40 | handle_timeout(); 41 | } 42 | 43 | public void change_brightness() { 44 | this.visible = true; 45 | this.stack_osd.visible_child_name = "brightness_osd"; 46 | handle_timeout(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/OnScreenDisplay/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('OnScreenDisplay.vala') -------------------------------------------------------------------------------- /src/PowerMenu/PowerMenu.vala: -------------------------------------------------------------------------------- 1 | private enum PMOption { 2 | NONE, 3 | SHUTDOWN, 4 | REBOOT, 5 | SUSPEND, 6 | HIBERNATE, 7 | LOGOUT 8 | } 9 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/PowerMenu.ui")] 10 | public class PowerMenu : Astal.Window { 11 | private PMOption _option; 12 | public string uptime { get; set; } 13 | 14 | [GtkChild] 15 | private unowned Gtk.Stack stapel; 16 | 17 | construct { 18 | _option = PMOption.NONE; 19 | 20 | Morghulis.instance.bind_property("uptime", this, "uptime", BindingFlags.SYNC_CREATE); 21 | 22 | this.notify["visible"].connect(() => { 23 | if (!visible) { 24 | stapel.visible_child_name = "actions"; 25 | _option = PMOption.NONE; 26 | } 27 | }); 28 | } 29 | 30 | [GtkCallback] 31 | private void cancel() { 32 | debug("Cancelled"); 33 | stapel.visible_child_name = "actions"; 34 | _option = PMOption.NONE; 35 | } 36 | 37 | [GtkCallback] 38 | private void confirm() { 39 | switch (_option) { 40 | case PMOption.SHUTDOWN: 41 | try { 42 | Process.spawn_command_line_async("systemctl poweroff"); 43 | } catch (SpawnError e) { 44 | warning("Failed to shutdown: %s", e.message); 45 | } 46 | break; 47 | 48 | case PMOption.REBOOT: 49 | try { 50 | Process.spawn_command_line_async("systemctl reboot"); 51 | } catch (SpawnError e) { 52 | warning("Failed to reboot: %s", e.message); 53 | } 54 | break; 55 | 56 | case PMOption.SUSPEND: 57 | try { 58 | Process.spawn_command_line_async("systemctl suspend"); 59 | } catch (SpawnError e) { 60 | warning("Failed to suspend: %s", e.message); 61 | } 62 | break; 63 | 64 | case PMOption.HIBERNATE: 65 | try { 66 | Process.spawn_command_line_async("systemctl hibernate"); 67 | } catch (SpawnError e) { 68 | warning("Failed to hibernate: %s", e.message); 69 | } 70 | break; 71 | 72 | case PMOption.LOGOUT: 73 | try { 74 | Process.spawn_command_line_async(@"loginctl terminate-user $(Morghulis.user_name)"); 75 | } catch (SpawnError e) { 76 | warning("Failed to logout: %s", e.message); 77 | } 78 | break; 79 | 80 | default: 81 | message("Unreachable code reached"); 82 | break; 83 | } 84 | } 85 | 86 | [GtkCallback] 87 | private void set_shutdown() { 88 | debug("Set Shutdown"); 89 | _option = PMOption.SHUTDOWN; 90 | stapel.visible_child_name = "confirmation"; 91 | } 92 | 93 | [GtkCallback] 94 | private void set_reboot() { 95 | debug("Set Reboot"); 96 | _option = PMOption.REBOOT; 97 | stapel.visible_child_name = "confirmation"; 98 | } 99 | 100 | [GtkCallback] 101 | private void set_suspend() { 102 | debug("Set Suspend"); 103 | _option = PMOption.SUSPEND; 104 | stapel.visible_child_name = "confirmation"; 105 | } 106 | 107 | [GtkCallback] 108 | private void set_hibernate() { 109 | debug("Set Hibernate"); 110 | _option = PMOption.HIBERNATE; 111 | stapel.visible_child_name = "confirmation"; 112 | } 113 | 114 | [GtkCallback] 115 | private void set_logout() { 116 | debug("Set Logout"); 117 | _option = PMOption.LOGOUT; 118 | stapel.visible_child_name = "confirmation"; 119 | } 120 | 121 | [GtkCallback] 122 | private void just_lock() { 123 | debug("Set Lock"); 124 | try { 125 | Process.spawn_command_line_async("loginctl lock-session"); 126 | } catch (SpawnError e) { 127 | warning("Failed to lock: %s", e.message); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/PowerMenu/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('PowerMenu.vala') -------------------------------------------------------------------------------- /src/QuickMenu/BatteryBox.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/BatteryBox.ui")] 2 | class BatteryBox : Gtk.Box { 3 | public static Gtk.Stack bb_stack_ref; 4 | public AstalPowerProfiles.PowerProfiles power_profiles { get; private set; } 5 | public AstalBattery.Device battery { get; private set; } 6 | public Backlight backlight { get; private set; } 7 | public Gtk.Adjustment brightness_adj { get; private set; } 8 | public Gtk.Adjustment battery_adj { get; private set; } 9 | 10 | [GtkChild] 11 | private unowned Gtk.Stack bb_stack; 12 | 13 | [GtkChild] 14 | private unowned Gtk.Button ppd_stack_btn; 15 | 16 | [GtkChild] 17 | private unowned Gtk.Button power_saver; 18 | 19 | [GtkChild] 20 | private unowned Gtk.Button balanced; 21 | 22 | [GtkChild] 23 | private unowned Gtk.Button performance; 24 | 25 | construct { 26 | battery = AstalBattery.Device.get_default(); 27 | power_profiles = AstalPowerProfiles.PowerProfiles.get_default(); 28 | backlight = Backlight.get_default(); 29 | if (battery.is_present) { 30 | debug("Setting up Battery module"); 31 | brightness_adj = new Gtk.Adjustment(0, 0, 1, 0, 0, 0); 32 | battery_adj = new Gtk.Adjustment(0, 0, 1, 0, 0, 0); 33 | 34 | backlight.bind_property("percentage", brightness_adj, "value", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 35 | battery.bind_property("percentage", battery_adj, "value", BindingFlags.SYNC_CREATE); 36 | 37 | if (power_profiles != null && power_profiles.version != null && power_profiles.version != "") { 38 | debug("Setting up Power Profiles module"); 39 | sync_ppd(); 40 | power_profiles.notify["active-profile"].connect(sync_ppd); 41 | } else { 42 | ppd_stack_btn.visible = false; 43 | } 44 | bb_stack_ref = bb_stack; 45 | } else { 46 | this.dispose(); 47 | } 48 | } 49 | 50 | [GtkCallback] 51 | private void show_power_profiles() { 52 | if (bb_stack.visible_child_name == "profiles") { 53 | bb_stack.set_visible_child_name("sliders"); 54 | } else { 55 | bb_stack.set_visible_child_name("profiles"); 56 | } 57 | } 58 | 59 | [GtkCallback] 60 | private void set_power_saver() { 61 | power_profiles.active_profile = "power-saver"; 62 | } 63 | 64 | [GtkCallback] 65 | private void set_balanced() { 66 | power_profiles.active_profile = "balanced"; 67 | } 68 | 69 | [GtkCallback] 70 | private void set_performance() { 71 | power_profiles.active_profile = "performance"; 72 | } 73 | 74 | private void sync_ppd() { 75 | switch (power_profiles.active_profile) { 76 | case "power-saver": 77 | power_saver.add_css_class("accent"); 78 | balanced.remove_css_class("accent"); 79 | performance.remove_css_class("accent"); 80 | break; 81 | 82 | case "balanced": 83 | power_saver.remove_css_class("accent"); 84 | balanced.add_css_class("accent"); 85 | performance.remove_css_class("accent"); 86 | break; 87 | 88 | case "performance": 89 | power_saver.remove_css_class("accent"); 90 | balanced.remove_css_class("accent"); 91 | performance.add_css_class("accent"); 92 | break; 93 | 94 | default: 95 | critical("Unreachable code reached"); 96 | break; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/QuickMenu/MprisPlayer.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/MprisPlayer.ui")] 2 | public class MprisPlayer : Gtk.Box { 3 | public AstalMpris.Player player { get; set; } 4 | 5 | [GtkCallback] 6 | public void next() { 7 | this.player.next(); 8 | } 9 | 10 | [GtkCallback] 11 | public void prev() { 12 | this.player.previous(); 13 | } 14 | 15 | [GtkCallback] 16 | public void play_pause() { 17 | this.player.play_pause(); 18 | } 19 | 20 | [GtkCallback] 21 | public string pause_icon(AstalMpris.PlaybackStatus status) { 22 | switch (status) { 23 | case AstalMpris.PlaybackStatus.PLAYING: 24 | return "media-playback-pause-symbolic"; 25 | 26 | case AstalMpris.PlaybackStatus.PAUSED: 27 | case AstalMpris.PlaybackStatus.STOPPED: 28 | default: 29 | return "media-playback-start-symbolic"; 30 | } 31 | } 32 | 33 | [GtkCallback] 34 | public string art_url(string? url) { 35 | if (url != null && url != "") { 36 | return url.substring(7); 37 | } else { 38 | return ""; 39 | } 40 | } 41 | 42 | [GtkCallback] 43 | public string current_pos(double pos) { 44 | int minutes = (int)(pos / 60); 45 | int seconds = (int)(pos % 60); 46 | 47 | if (seconds < 10) { 48 | return @"$minutes:0$seconds"; 49 | } else { 50 | return @"$minutes:$seconds"; 51 | } 52 | } 53 | 54 | [GtkCallback] 55 | public string total_pos(double len) { 56 | int minutes = (int)(len / 60); 57 | int seconds = (int)(len % 60); 58 | 59 | if (seconds < 10) { 60 | return @"$minutes:0$seconds"; 61 | } else { 62 | return @"$minutes:$seconds"; 63 | } 64 | } 65 | 66 | [GtkChild] 67 | public unowned Gtk.Adjustment media_len_adjust; 68 | 69 | public MprisPlayer(AstalMpris.Player player) { 70 | Object(); 71 | this.player = player; 72 | 73 | this.player.bind_property("position", media_len_adjust, "value", 74 | GLib.BindingFlags.BIDIRECTIONAL | GLib.BindingFlags.SYNC_CREATE); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/QuickMenu/PowerBox.vala: -------------------------------------------------------------------------------- 1 | private enum PowerOption { 2 | NONE, 3 | SHUTDOWN, 4 | REBOOT, 5 | LOGOUT 6 | } 7 | 8 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/PowerBox.ui")] 9 | public class PowerBox : Gtk.Box { 10 | private PowerOption _option; 11 | 12 | public string uptime { get; set; } 13 | public string user_name { get; private set; } 14 | public Gdk.Paintable user_image_paintable { get; private set; } 15 | public static Gtk.Stack mstack { get; private set; } 16 | 17 | [GtkChild] 18 | private unowned Gtk.Stack main_stack; 19 | 20 | construct { 21 | _option = PowerOption.NONE; 22 | user_name = Morghulis.user_name; 23 | var user_image_path = Environment.get_home_dir() + "/user.png"; 24 | try { 25 | var pixbuf = new Gdk.Pixbuf.from_file(user_image_path); 26 | if (pixbuf != null) { 27 | user_image_paintable = Gdk.Texture.for_pixbuf(pixbuf); 28 | } 29 | } catch (Error e) { 30 | critical("Error loading paintable: %s\n", e.message); 31 | } 32 | mstack = main_stack; 33 | 34 | Morghulis.instance.bind_property("uptime", this, "uptime", BindingFlags.SYNC_CREATE); 35 | } 36 | 37 | /// I honestly think this can be done better 38 | 39 | [GtkCallback] 40 | private void show_shutdown_confirm() { 41 | _option = PowerOption.SHUTDOWN; 42 | main_stack.visible_child_name = "confirm"; 43 | } 44 | 45 | [GtkCallback] 46 | private void show_logout_confirm() { 47 | _option = PowerOption.LOGOUT; 48 | main_stack.visible_child_name = "confirm"; 49 | } 50 | 51 | [GtkCallback] 52 | private void show_reboot_confirm() { 53 | _option = PowerOption.REBOOT; 54 | main_stack.visible_child_name = "confirm"; 55 | } 56 | 57 | [GtkCallback] 58 | private void cancel_action() { 59 | _option = PowerOption.NONE; 60 | main_stack.visible_child_name = "main"; 61 | } 62 | 63 | [GtkCallback] 64 | private void confirm_action() { 65 | switch (_option) { 66 | case PowerOption.SHUTDOWN: 67 | shutdown(); 68 | break; 69 | 70 | case PowerOption.REBOOT: 71 | reboot(); 72 | break; 73 | 74 | case PowerOption.LOGOUT: 75 | logout(); 76 | break; 77 | 78 | default: 79 | message("Unreachable code reached"); 80 | break; 81 | } 82 | _option = PowerOption.NONE; 83 | main_stack.visible_child_name = "main"; 84 | } 85 | 86 | private void shutdown() { 87 | try { 88 | Process.spawn_command_line_async("systemctl poweroff"); 89 | } catch (SpawnError e) { 90 | warning("Failed to shutdown: %s", e.message); 91 | } 92 | } 93 | 94 | private void reboot() { 95 | try { 96 | Process.spawn_command_line_async("systemctl reboot"); 97 | } catch (SpawnError e) { 98 | warning("Failed to reboot: %s", e.message); 99 | } 100 | } 101 | 102 | public void logout() { 103 | try { 104 | Process.spawn_command_line_async(@"loginctl terminate-user $user_name"); 105 | } catch (SpawnError e) { 106 | warning("Failed to logout: %s", e.message); 107 | } 108 | } 109 | 110 | [GtkCallback] 111 | public void suspend() { 112 | try { 113 | Process.spawn_command_line_async("systemctl suspend"); 114 | } catch (SpawnError e) { 115 | warning("Failed to suspend: %s", e.message); 116 | } 117 | } 118 | 119 | [GtkCallback] 120 | public void hibernate() { 121 | try { 122 | Process.spawn_command_line_async("systemctl hibernate"); 123 | } catch (SpawnError e) { 124 | warning("Failed to hibernate: %s", e.message); 125 | } 126 | } 127 | 128 | [GtkCallback] 129 | public void lock() { 130 | try { 131 | Process.spawn_command_line_async("loginctl lock-session"); 132 | } catch (SpawnError e) { 133 | warning("Failed to lock: %s", e.message); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/QuickMenu/QAudioBox.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QAudioBox.ui")] 2 | public class QAudioBox : Gtk.Box { 3 | public AstalWp.Wp? wp { get; private set; } 4 | public AstalWp.Endpoint speaker { get; private set; } 5 | public AstalWp.Endpoint microphone { get; private set; } 6 | 7 | public Gtk.Adjustment speaker_adj { get; private set; } 8 | public Gtk.Adjustment microphone_adj { get; private set; } 9 | 10 | [GtkChild] 11 | public unowned Gtk.ListBox sources; 12 | 13 | [GtkChild] 14 | public unowned Gtk.ListBox mixers; 15 | 16 | [GtkChild] 17 | public unowned Gtk.ListBox sinks; 18 | 19 | [GtkChild] 20 | private unowned Gtk.Revealer go_down_revealer; 21 | 22 | [GtkChild] 23 | private unowned Gtk.ScrolledWindow scrolled_window; 24 | 25 | construct { 26 | this.wp = AstalWp.get_default(); 27 | if (wp == null) { 28 | critical("Failed to initialize wp"); 29 | } else { 30 | this.speaker = wp.audio.default_speaker; 31 | this.microphone = wp.audio.default_microphone; 32 | } 33 | this.speaker_adj = new Gtk.Adjustment(speaker.volume, 0, 1, 0, 0, 0); 34 | this.microphone_adj = new Gtk.Adjustment(microphone.volume, 0, 1, 0, 0, 0); 35 | 36 | speaker.bind_property( 37 | "volume", 38 | speaker_adj, 39 | "value", 40 | BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL 41 | ); 42 | 43 | microphone.bind_property( 44 | "volume", 45 | microphone_adj, 46 | "value", 47 | BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL 48 | ); 49 | 50 | wp.audio.speakers.@foreach((e) => on_added(e, sinks)); 51 | wp.audio.microphones.@foreach((e) => on_added(e, sources)); 52 | wp.audio.streams.@foreach((e) => on_added(e, mixers)); 53 | 54 | wp.audio.speaker_added.connect((e) => on_added(e, sinks)); 55 | wp.audio.speaker_removed.connect((e) => on_removed(e, sinks)); 56 | 57 | wp.audio.microphone_added.connect((e) => on_added(e, sources)); 58 | wp.audio.microphone_removed.connect((e) => on_removed(e, sources)); 59 | 60 | wp.audio.stream_added.connect((e) => on_added(e, mixers)); 61 | wp.audio.stream_removed.connect((e) => on_removed(e, mixers)); 62 | 63 | scrolled_window.edge_reached.connect(on_edge_reached); 64 | } 65 | 66 | [GtkCallback] 67 | void toggle_speaker() { 68 | speaker.mute = !speaker.mute; 69 | } 70 | 71 | [GtkCallback] 72 | void toggle_microphone() { 73 | microphone.mute = !microphone.mute; 74 | } 75 | 76 | private void on_added(AstalWp.Node e, Gtk.ListBox l) { 77 | l.append(new QAudioItem(e)); 78 | } 79 | 80 | private void on_removed(AstalWp.Node e, Gtk.ListBox l) { 81 | var current = (QAudioItem)l.get_first_child(); 82 | 83 | while (current != null) { 84 | if (current.endpoint == e) { 85 | l.remove(current); 86 | break; 87 | } 88 | current = (QAudioItem)current.get_next_sibling(); 89 | } 90 | } 91 | 92 | private void on_edge_reached(Gtk.PositionType pos) { 93 | if (pos == Gtk.PositionType.TOP) { 94 | go_down_revealer.reveal_child = true; 95 | } else { 96 | go_down_revealer.reveal_child = false; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/QuickMenu/QAudioItem.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QAudioItem.ui")] 2 | public class QAudioItem : Gtk.ListBoxRow { 3 | public AstalWp.Node endpoint { get; construct; } 4 | 5 | [GtkChild] 6 | private unowned Gtk.Adjustment volume_adjust; 7 | 8 | public QAudioItem(AstalWp.Node endpoint) { 9 | Object(endpoint: endpoint); 10 | 11 | this.endpoint.bind_property( 12 | "volume", 13 | volume_adjust, 14 | "value", 15 | BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL 16 | ); 17 | } 18 | 19 | [GtkCallback] 20 | public string fallback_icon(string? icon_name) { 21 | switch (icon_name) { 22 | case "audio-card-analog-pci": 23 | return "audio-card-symbolic"; 24 | 25 | case "audio-headset-bluetooth": 26 | return "audio-headset-symbolic"; 27 | 28 | default: 29 | return icon_name; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/QuickMenu/QBluetooth.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QBluetooth.ui")] 2 | public class QBluetooth : Gtk.Box { 3 | public AstalBluetooth.Bluetooth bluetooth { get; set; } 4 | 5 | [GtkChild] 6 | public unowned Gtk.ListBox blue_list; 7 | 8 | construct { 9 | bluetooth = AstalBluetooth.get_default(); 10 | 11 | bluetooth.devices.@foreach(dev => on_added(dev)); 12 | bluetooth.device_added.connect((_, dev) => on_added(dev)); 13 | bluetooth.device_removed.connect((_, dev) => on_removed(dev)); 14 | this.blue_list.set_sort_func(sfunc); 15 | this.blue_list.invalidate_sort(); 16 | } 17 | 18 | public int sfunc(Gtk.ListBoxRow la, Gtk.ListBoxRow lb) { 19 | QBluetoothItem a = (QBluetoothItem)la; 20 | 21 | return a.device.connected ? -1 : 1; 22 | } 23 | 24 | private void on_added(AstalBluetooth.Device device) { 25 | blue_list.append(new QBluetoothItem(device)); 26 | this.blue_list.invalidate_sort(); 27 | } 28 | 29 | private void on_removed(AstalBluetooth.Device device) { 30 | var current = (QBluetoothItem)blue_list.get_first_child(); 31 | 32 | while (current != null) { 33 | if (current.device == device) { 34 | blue_list.remove(current); 35 | break; 36 | } 37 | 38 | current = (QBluetoothItem)current.get_next_sibling(); 39 | } 40 | this.blue_list.invalidate_sort(); 41 | } 42 | 43 | [GtkCallback] 44 | public void toggle_discover() { 45 | if (bluetooth.adapter.discovering) { 46 | try { 47 | bluetooth.adapter.stop_discovery(); 48 | } catch (Error e) { 49 | critical("Error: %s", e.message); 50 | } 51 | } else { 52 | try { 53 | bluetooth.adapter.start_discovery(); 54 | } catch (Error e) { 55 | critical("Error: %s", e.message); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/QuickMenu/QBluetoothItem.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QBluetoothItem.ui")] 2 | public class QBluetoothItem : Gtk.ListBoxRow { 3 | public AstalBluetooth.Device device { get; construct set; } 4 | 5 | [GtkChild] 6 | public unowned Gtk.Label battery_label; 7 | 8 | public QBluetoothItem(AstalBluetooth.Device? device) { 9 | Object( 10 | device: device 11 | ); 12 | } 13 | 14 | [GtkCallback] 15 | public void switch_connection() { 16 | if (this.device.connected) { 17 | this.device.disconnect_device.begin(); 18 | } else { 19 | this.device.connect_device.begin(); 20 | } 21 | } 22 | 23 | [GtkCallback] 24 | public string device_icon(string? icon) { 25 | if (icon == null) { 26 | return "bluetooth-active"; 27 | } 28 | return icon; 29 | } 30 | 31 | [GtkCallback] 32 | public string battery_percent(double percent) { 33 | return @"$(Math.round(percent * 100))%"; 34 | } 35 | 36 | [GtkCallback] 37 | public bool is_battery_a_real_thing(double percent) { 38 | return percent != -1; 39 | } 40 | 41 | public bool active { 42 | get { 43 | return has_css_class("button_accent_bg"); 44 | } 45 | set { 46 | if (value) { 47 | this.add_css_class("button_accent_bg"); 48 | } else { 49 | this.remove_css_class("button_accent_bg"); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/QuickMenu/QButton.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QButton.ui")] 2 | public class QButton : Gtk.Box { 3 | public signal void clicked(); 4 | public signal void clicked_extras(); 5 | 6 | public string icon { get; set; } 7 | public string identity { get; set; } 8 | public string status { get; set; } 9 | 10 | public bool active { 11 | get { 12 | return this.has_css_class("qbutton"); 13 | } 14 | set { 15 | if (value) { 16 | this.add_css_class("qbutton"); 17 | this.add_css_class("qbutton"); 18 | } else { 19 | this.remove_css_class("qbutton"); 20 | this.remove_css_class("qbutton"); 21 | } 22 | } 23 | } 24 | public bool inactive { 25 | get { 26 | return !this.has_css_class("qbutton"); 27 | } 28 | set { 29 | if (!value) { 30 | this.add_css_class("qbutton"); 31 | this.add_css_class("qbutton"); 32 | } else { 33 | this.remove_css_class("qbutton"); 34 | this.remove_css_class("qbutton"); 35 | } 36 | } 37 | } 38 | 39 | QButton() { 40 | Object( 41 | name: "Button" 42 | ); 43 | } 44 | 45 | [GtkCallback] 46 | public void on_clicked() { 47 | clicked(); 48 | } 49 | 50 | [GtkCallback] 51 | public void on_clicked_extras() { 52 | clicked_extras(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/QuickMenu/QNetwork.vala: -------------------------------------------------------------------------------- 1 | //This file if complete garbage 2 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QNetwork.ui")] 3 | public class QNetwork : Gtk.Box { 4 | public AstalNetwork.Network network { get; set; } 5 | private GLib.ListStore wifi_store; 6 | 7 | [GtkChild] 8 | private unowned Gtk.ListView wifi_list; 9 | 10 | construct { 11 | network = AstalNetwork.get_default(); 12 | wifi_store = new GLib.ListStore(typeof(WifiItem)); 13 | 14 | var factory = setup_factory(); 15 | var selection = new Gtk.NoSelection(wifi_store); 16 | wifi_list.set_factory(factory); 17 | wifi_list.set_model(selection); 18 | 19 | network.wifi.notify["access-points"].connect(refresh_items); 20 | refresh_items(); 21 | } 22 | 23 | private Gtk.SignalListItemFactory setup_factory() { 24 | var factory = new Gtk.SignalListItemFactory(); 25 | 26 | factory.setup.connect((factory, obj) => { 27 | var list_item = (Gtk.ListItem)obj; 28 | var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 5); 29 | box.append(new Gtk.Image()); 30 | box.append(new Gtk.Label(null)); 31 | box.add_css_class("padding_10"); 32 | list_item.child = box; 33 | }); 34 | 35 | factory.bind.connect((factory, obj) => { 36 | var list_item = (Gtk.ListItem)obj; 37 | var box = (Gtk.Box)list_item.get_child(); 38 | var image = (Gtk.Image)box.get_first_child(); 39 | var label = (Gtk.Label)box.get_last_child(); 40 | var item = (WifiItem)list_item.get_item(); 41 | 42 | image.set_from_icon_name(item.icon_name); 43 | image.pixel_size = 25; 44 | label.label = item.ssid; 45 | }); 46 | 47 | return factory; 48 | } 49 | 50 | private void refresh_items() { 51 | wifi_store.remove_all(); 52 | network.wifi.access_points.foreach((ap) => { 53 | wifi_store.append(new WifiItem(ap)); 54 | }); 55 | } 56 | 57 | [GtkCallback] 58 | public void refresh() { 59 | network.wifi.scan(); 60 | } 61 | } 62 | 63 | public class WifiItem : Object { 64 | public string ssid { get; set; } 65 | public string icon_name { get; set; } 66 | public AstalNetwork.AccessPoint access_point { get; set; } 67 | 68 | public WifiItem(AstalNetwork.AccessPoint ap) { 69 | Object(); 70 | this.ssid = ap.ssid; 71 | this.icon_name = ap.icon_name; 72 | this.access_point = ap; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/QuickMenu/QuickMenu.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/QuickMenu.ui")] 2 | public class QuickMenu : Astal.Window { 3 | public AstalWp.Endpoint speaker { get; set; } 4 | public static QuickMenu instance { get; private set; } 5 | 6 | construct { 7 | if (instance == null) { 8 | instance = this; 9 | } else { 10 | this.destroy(); 11 | } 12 | 13 | this.notify["visible"].connect(() => { 14 | if (!this.visible) { 15 | Settings.settings_navigation.pop(); 16 | PowerBox.mstack.set_visible_child_name("main"); 17 | BatteryBox.bb_stack_ref?.set_visible_child_name("sliders"); 18 | } 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/QuickMenu/Settings.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/Settings.ui")] 2 | public class Settings : Adw.Bin { 3 | private AstalMpris.Mpris _mpris; 4 | 5 | public AstalNetwork.Network network { get; private set; } 6 | public AstalBluetooth.Bluetooth bluetooth { get; private set; } 7 | public AstalNotifd.Notifd notifd { get; private set; } 8 | public AstalWp.Wp? wp { get; private set; } 9 | public Gdk.Paintable no_media_players { get; private set; } 10 | public static Adw.NavigationView settings_navigation { get; private set; } 11 | 12 | [GtkChild] 13 | public unowned Adw.NavigationView quick_settings_navigation_view; 14 | 15 | construct { 16 | network = AstalNetwork.get_default(); 17 | bluetooth = AstalBluetooth.get_default(); 18 | wp = AstalWp.get_default(); 19 | notifd = AstalNotifd.get_default(); 20 | 21 | setup_empty_notif(); 22 | 23 | _mpris = AstalMpris.get_default(); 24 | _mpris.players.@foreach((p) => on_player_added(p)); 25 | _mpris.player_added.connect((p) => on_player_added(p)); 26 | _mpris.player_closed.connect((p) => on_player_removed(p)); 27 | 28 | settings_navigation = quick_settings_navigation_view; 29 | } 30 | 31 | [GtkCallback] 32 | public string notif_status(bool dnd) { 33 | return dnd 34 | ? "Don't disturb" 35 | : "Enabled"; 36 | } 37 | 38 | [GtkCallback] 39 | public string notif_icon(bool dnd) { 40 | return dnd 41 | ? "notifications-disabled-symbolic" 42 | : "preferences-system-notifications-symbolic"; 43 | } 44 | 45 | [GtkCallback] 46 | public void network_clicked() { 47 | this.network.wifi.enabled = !this.network.wifi.enabled; 48 | } 49 | 50 | [GtkCallback] 51 | public void network_clicked_extras() { 52 | quick_settings_navigation_view.push_by_tag("network"); 53 | } 54 | 55 | [GtkCallback] 56 | public string conn_status(bool connected) { 57 | return connected 58 | ? "Connected" 59 | : "Off"; 60 | } 61 | 62 | [GtkCallback] 63 | public string network_identity(string? identity) { 64 | if (identity != null && identity != "") { 65 | return identity; 66 | } else { 67 | return "Wifi"; 68 | } 69 | } 70 | 71 | [GtkCallback] 72 | public void bluetooth_clicked() { 73 | this.bluetooth.adapter.powered = !this.bluetooth.adapter.powered; 74 | } 75 | 76 | [GtkCallback] 77 | public void bluetooth_clicked_extras() { 78 | quick_settings_navigation_view.push_by_tag("bluetooth"); 79 | } 80 | 81 | [GtkCallback] 82 | public string bluetooth_icon_name(bool connected) { 83 | return connected 84 | ? "bluetooth-active-symbolic" 85 | : "bluetooth-disabled-symbolic"; 86 | } 87 | 88 | [GtkCallback] 89 | public string bluetooth_identity(string? identity) { 90 | if (identity != null && identity != "") { 91 | return identity; 92 | } else { 93 | return "Bluetooth"; 94 | } 95 | } 96 | 97 | [GtkCallback] 98 | public void audio_clicked() { 99 | wp.audio.default_speaker.mute = !wp.audio.default_speaker.mute; 100 | } 101 | 102 | [GtkCallback] 103 | public void audio_clicked_extras() { 104 | quick_settings_navigation_view.push_by_tag("audio"); 105 | } 106 | 107 | [GtkCallback] 108 | public string audio_status(bool muted) { 109 | return muted 110 | ? "Muted" 111 | : "Unmuted"; 112 | } 113 | 114 | [GtkCallback] 115 | public void notifications_clicked() { 116 | notifd.dont_disturb = !notifd.dont_disturb; 117 | } 118 | 119 | [GtkCallback] 120 | public void notifications_clicked_extras() { 121 | quick_settings_navigation_view.push_by_tag("notifications"); 122 | } 123 | 124 | public void TODO() { 125 | message("TODO!"); 126 | } 127 | 128 | [GtkChild] 129 | private unowned Adw.Carousel players; 130 | 131 | [GtkCallback] 132 | public string mpris_stack(uint n_pages) { 133 | return (n_pages > 0) ? "mpris" : "no_mpris"; 134 | } 135 | 136 | private void on_player_added(AstalMpris.Player player) { 137 | var mpris_widget = new MprisPlayer(player); 138 | 139 | this.players.append(mpris_widget); 140 | } 141 | 142 | private void on_player_removed(AstalMpris.Player player) { 143 | MprisPlayer current = (MprisPlayer)this.players.get_first_child(); 144 | 145 | while (current != null) { 146 | if (current.player == player) { 147 | this.players.remove(current); 148 | break; 149 | } 150 | current = (MprisPlayer)current.get_next_sibling(); 151 | } 152 | } 153 | 154 | private void setup_empty_notif() { 155 | try { 156 | var pixbuf = new Gdk.Pixbuf.from_resource("/com/github/ARKye03/morghulis/assets/wyvern-svgrepo-com.svg"); 157 | if (pixbuf != null) { 158 | no_media_players = Gdk.Texture.for_pixbuf(pixbuf); 159 | } 160 | } catch (Error e) { 161 | warning("Failed to load image: %s", e.message); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/QuickMenu/SysInfo.vala: -------------------------------------------------------------------------------- 1 | using GTop; 2 | 3 | public class SysInfo : Gtk.Box { 4 | private AstalWp.Endpoint speaker { get; set; } 5 | 6 | construct { 7 | speaker = AstalWp.get_default().audio.default_speaker; 8 | 9 | var cpu_bar = new CpuMonitorBar(); 10 | this.append(cpu_bar); 11 | 12 | var mem_bar = new MemMonitorBar(); 13 | this.append(mem_bar); 14 | 15 | // Start the periodic update 16 | GLib.Timeout.add_seconds(3, () => { 17 | cpu_bar.update_cpu(); 18 | mem_bar.update_mem(); 19 | return true; 20 | }); 21 | } 22 | } 23 | 24 | private class CpuMonitorBar : Gtk.Box { 25 | private GTop.Cpu? cpu; 26 | private CircularProgressBar cpu_bar; 27 | private Gtk.Label cpu_label; 28 | 29 | private uint64 last_used; 30 | private uint64 last_total; 31 | private float load; 32 | 33 | construct { 34 | cpu_bar = new CircularProgressBar(); 35 | cpu_label = new Gtk.Label("CPU"); 36 | last_used = 0; 37 | last_total = 0; 38 | 39 | this.orientation = Gtk.Orientation.VERTICAL; 40 | 41 | cpu_bar.line_width = 10; 42 | cpu_bar.line_cap = Gsk.LineCap.ROUND; 43 | cpu_bar.percentage = 0; 44 | cpu_bar.hexpand = true; 45 | cpu_bar.vexpand = true; 46 | 47 | this.append(cpu_bar); 48 | this.append(cpu_label); 49 | } 50 | 51 | public void update_cpu() { 52 | // Get new CPU stats 53 | GTop.get_cpu(out cpu); 54 | 55 | // Calculate deltas using unsigned 64-bit arithmetic 56 | uint64 used = cpu.user + cpu.sys + cpu.nice + cpu.irq + cpu.softirq; 57 | uint64 total = used + cpu.idle + cpu.iowait; 58 | 59 | uint64 diff_used = used - last_used; 60 | uint64 diff_total = total - last_total; 61 | 62 | // Avoid division by zero and calculate load 63 | if (diff_total > 0) { 64 | load = (float)diff_used / (float)diff_total; 65 | } else { 66 | load = 0.0f; 67 | } 68 | 69 | // Update last values 70 | last_used = used; 71 | last_total = total; 72 | 73 | // Update the progress bar (clamp between 0-100%) 74 | cpu_bar.percentage = double.min(1.0, load); 75 | } 76 | } 77 | 78 | private class MemMonitorBar : Gtk.Box { 79 | private GTop.Memory? mem; 80 | private CircularProgressBar mem_bar; 81 | private Gtk.Label mem_label; 82 | 83 | private uint64 available = 0; 84 | private uint64 real_used = 0; 85 | 86 | construct { 87 | mem_bar = new CircularProgressBar(); 88 | mem_label = new Gtk.Label("Memory"); 89 | 90 | this.orientation = Gtk.Orientation.VERTICAL; 91 | 92 | mem_bar.line_width = 10; 93 | mem_bar.line_cap = Gsk.LineCap.ROUND; 94 | mem_bar.percentage = 0; 95 | mem_bar.hexpand = true; 96 | mem_bar.vexpand = true; 97 | 98 | this.append(mem_bar); 99 | this.append(mem_label); 100 | } 101 | 102 | public void update_mem() { 103 | GTop.get_mem(out mem); 104 | 105 | available = mem.free + mem.buffer + mem.cached; 106 | real_used = mem.total - available; 107 | mem_bar.percentage = (double)real_used / mem.total; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/QuickMenu/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files( 2 | 'QuickMenu.vala', 3 | 'QNetwork.vala', 4 | 'QBluetooth.vala', 5 | 'QBluetoothItem.vala', 6 | 'BatteryBox.vala', 7 | 'MprisPlayer.vala', 8 | 'QButton.vala', 9 | 'PowerBox.vala', 10 | 'Settings.vala', 11 | 'QAudioBox.vala', 12 | 'QAudioItem.vala', 13 | 'SysInfo.vala', 14 | ) 15 | 16 | if astal_battery_dep.found() 17 | project_args += ['--define=battery'] 18 | endif 19 | 20 | if astal_power_profiles_dep.found() 21 | project_args += ['--define=power-profiles'] 22 | endif -------------------------------------------------------------------------------- /src/Runner/Runner.vala: -------------------------------------------------------------------------------- 1 | [CCode(cname = "mpars_evaluate")] 2 | public extern double mpars_evaluate(string expression, out string? error); 3 | 4 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/Runner.ui")] 5 | public class Runner : Astal.Window { 6 | public static Runner instance { get; private set; } 7 | public AstalApps.Apps apps { get; construct set; } 8 | 9 | [GtkChild] 10 | private unowned Gtk.Entry entry; 11 | 12 | [GtkChild] 13 | private unowned Adw.Bin math_bin; 14 | 15 | [GtkChild] 16 | private unowned Gtk.Label math_label; 17 | 18 | [GtkChild] 19 | private unowned Gtk.ListBox app_list; 20 | 21 | private int sort_func(Gtk.ListBoxRow la, Gtk.ListBoxRow lb) { 22 | RunnerButton a = (RunnerButton)la; 23 | RunnerButton b = (RunnerButton)lb; 24 | 25 | if (a.score == b.score) { 26 | return b.app.frequency - a.app.frequency; 27 | } 28 | return (a.score > b.score) ? -1 : 1; 29 | } 30 | 31 | private bool filter_func(Gtk.ListBoxRow row) { 32 | RunnerButton app = (RunnerButton)row; 33 | 34 | return app.score >= 0; 35 | } 36 | 37 | private bool looks_like_math(string text) { 38 | return text[0] != ':' && 39 | (text.contains("+") || 40 | text.contains("-") || 41 | text.contains("*") || 42 | text.contains("/") || 43 | text.contains("^")); 44 | } 45 | 46 | [GtkCallback] 47 | public void update_list() { 48 | string input = this.entry.text.strip(); 49 | 50 | // Handle math expressions 51 | if (looks_like_math(input)) { 52 | string error; 53 | double result = mpars_evaluate(input, out error); 54 | 55 | if (error == null) { 56 | math_label.set_text(result.to_string()); 57 | math_bin.visible = true; 58 | return; 59 | } else { 60 | math_bin.visible = false; 61 | } 62 | app_list.visible = false; 63 | return; 64 | } else { 65 | app_list.visible = true; 66 | math_bin.visible = false; 67 | } 68 | 69 | // Update app filtering 70 | var child = this.app_list.get_first_child(); 71 | while (child != null) { 72 | if (child is RunnerButton) { 73 | var app = (RunnerButton)child; 74 | app.score = apps.fuzzy_score(input, app.app); 75 | } 76 | child = child.get_next_sibling(); 77 | } 78 | 79 | this.app_list.invalidate_sort(); 80 | this.app_list.invalidate_filter(); 81 | } 82 | 83 | [GtkCallback] 84 | public void launch_first_runner_button() { 85 | RunnerButton selected_button = (RunnerButton)this.app_list.get_first_child(); 86 | 87 | if (selected_button != null && app_list.visible) { 88 | selected_button.activate(); 89 | this.visible = false; 90 | } 91 | } 92 | 93 | [GtkCallback] 94 | public void key_released(uint keyval) { 95 | if (keyval == Gdk.Key.Escape) { 96 | this.visible = false; 97 | } 98 | } 99 | 100 | construct { 101 | if (instance == null) { 102 | instance = this; 103 | } else { 104 | this.destroy(); 105 | } 106 | 107 | this.apps = new AstalApps.Apps(); 108 | 109 | this.app_list.set_sort_func(sort_func); 110 | this.app_list.set_filter_func(filter_func); 111 | 112 | this.apps.list.@foreach(app => { 113 | this.app_list.append(new RunnerButton(app)); 114 | }); 115 | 116 | this.notify["visible"].connect(() => { 117 | if (!this.visible) { 118 | this.entry.text = ""; 119 | } else { 120 | this.entry.grab_focus(); 121 | } 122 | }); 123 | this.margin_top = Morghulis.primary_monitor.get_geometry().height / 4; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Runner/RunnerButton.vala: -------------------------------------------------------------------------------- 1 | [GtkTemplate(ui = "/com/github/ARKye03/morghulis/ui/RunnerButton.ui")] 2 | public class RunnerButton : Gtk.ListBoxRow { 3 | public AstalApps.Application app { get; construct; } 4 | public double score { get; set; } 5 | 6 | [GtkCallback] 7 | public void clicked() { 8 | app.launch(); 9 | } 10 | 11 | [GtkCallback] 12 | public void activated() { 13 | app.launch(); 14 | } 15 | 16 | public RunnerButton(AstalApps.Application app) { 17 | Object(app: app); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Runner/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('Runner.vala', 'RunnerButton.vala') -------------------------------------------------------------------------------- /src/Tray/Tray.vala: -------------------------------------------------------------------------------- 1 | public class Tray : Gtk.Widget { 2 | private HashTable items; 3 | private Gtk.FlowBox flow_box; 4 | 5 | public AstalTray.Tray tray { get; private set; } 6 | 7 | construct { 8 | this.tray = AstalTray.get_default(); 9 | this.items = new HashTable(str_hash, str_equal); 10 | this.layout_manager = new Gtk.BinLayout(); 11 | this.visible = false; 12 | this.flow_box = new Gtk.FlowBox() { 13 | max_children_per_line = 4, 14 | homogeneous = true, 15 | column_spacing = row_spacing = 1, 16 | selection_mode = Gtk.SelectionMode.NONE, 17 | }; 18 | 19 | this.tray.item_added.connect(on_added); 20 | this.tray.item_removed.connect(on_removed); 21 | flow_box.set_parent(this); 22 | } 23 | 24 | private void on_added(AstalTray.Tray tray, string item_id) { 25 | if (this.items.contains(item_id)) { 26 | return; 27 | } 28 | var tray_item = this.tray.get_item(item_id); 29 | if (tray_item.id != null && tray_item.id != "") { 30 | var item = create_tray_item(tray_item); 31 | this.items.insert(item_id, item); 32 | flow_box.append(item); 33 | this.visible = true; 34 | } 35 | } 36 | 37 | private void on_removed(AstalTray.Tray tray, string item_id) { 38 | if (!this.items.contains(item_id)) { 39 | return; 40 | } 41 | flow_box.remove(this.items.take(item_id)); 42 | this.visible = items.size() > 0; 43 | } 44 | 45 | private Gtk.FlowBoxChild create_tray_item(AstalTray.TrayItem item) { 46 | var button = new Gtk.MenuButton() { 47 | direction = Gtk.ArrowType.UP, 48 | }; 49 | 50 | item.notify["action_group"].connect(() => { 51 | button.insert_action_group("dbusmenu", item.action_group); 52 | }); 53 | button.insert_action_group("dbusmenu", item.action_group); 54 | item.bind_property("menu-model", button, "menu-model", BindingFlags.SYNC_CREATE); 55 | var icon = new Gtk.Image(); 56 | item.bind_property("gicon", icon, "gicon", BindingFlags.SYNC_CREATE); 57 | button.child = icon; 58 | 59 | return new Gtk.FlowBoxChild() { 60 | child = button 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Tray/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += files('Tray.vala') -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | project_resources += ['Main.vala', 'App.vala'] 2 | project_args = [] 3 | 4 | vapidir = meson.current_source_dir() + '/vapi/' 5 | 6 | # Optional dependencies 7 | astal_hyprland_dep = dependency('astal-hyprland-0.1', required: false) 8 | astal_river_dep = dependency('astal-river-0.1', required: false) 9 | astal_battery_dep = dependency('astal-battery-0.1', required: false) 10 | astal_power_profiles_dep = dependency('astal-power-profiles-0.1', required: false) 11 | 12 | deps = [ 13 | dependency('gtk4'), 14 | dependency('gtk4-layer-shell-0'), 15 | dependency('gsound'), 16 | dependency('libnm'), 17 | dependency('astal-4-4.0'), 18 | dependency('astal-io-0.1'), 19 | dependency('astal-wireplumber-0.1'), 20 | dependency('astal-mpris-0.1'), 21 | dependency('astal-tray-0.1'), 22 | dependency('astal-notifd-0.1'), 23 | dependency('astal-network-0.1'), 24 | dependency('astal-bluetooth-0.1'), 25 | dependency('astal-apps-0.1'), 26 | dependency('gio-unix-2.0'), 27 | dependency('libadwaita-1', version: '>= 1.6.0'), 28 | dependency('libgtop-2.0'), 29 | ] 30 | 31 | # Add optional dependencies if found 32 | if astal_hyprland_dep.found() 33 | deps += astal_hyprland_dep 34 | endif 35 | 36 | if astal_river_dep.found() 37 | deps += astal_river_dep 38 | endif 39 | 40 | if astal_battery_dep.found() 41 | deps += astal_battery_dep 42 | endif 43 | 44 | if astal_power_profiles_dep.found() 45 | deps += astal_power_profiles_dep 46 | endif 47 | 48 | subdir('Modules') 49 | subdir('NavBar') 50 | subdir('PowerMenu') 51 | subdir('Tray') 52 | subdir('Notifications') 53 | subdir('OnScreenDisplay') 54 | subdir('QuickMenu') 55 | subdir('Runner') 56 | 57 | executable( 58 | project_name, 59 | project_resources, 60 | vala_args: [ 61 | '--gresourcesdir=data/', 62 | '--vapidir=' + vapidir, 63 | ] 64 | + project_args, 65 | dependencies: deps, 66 | link_args: ['-lm'], # Link math library 67 | install: true, 68 | ) -------------------------------------------------------------------------------- /uncrustify.cfg: -------------------------------------------------------------------------------- 1 | # ARKye03 "Uncrustihell" config 2 | newlines = LF 3 | input_tab_size = 8 # original tab size 4 | output_tab_size = 4 # new tab size 5 | 6 | # Indentation 7 | indent_with_tabs = 2 # 1=indent to level only, 2=indent with tabs 8 | indent_columns = output_tab_size 9 | indent_align_string = FALSE # align broken strings 10 | indent_brace = 0 11 | indent_class = TRUE 12 | indent_namespace = FALSE 13 | indent_member = indent_columns 14 | indent_switch_case = indent_columns 15 | indent_switch_break_with_case = TRUE 16 | indent_paren_close = 2 17 | 18 | 19 | # Newlines 20 | nl_start_of_file = remove 21 | nl_end_of_file = force 22 | nl_end_of_file_min = 1 23 | nl_max = 2 24 | nl_before_block_comment = 2 25 | nl_after_func_body = 2 26 | nl_after_func_proto_group = 2 27 | nl_assign_brace = remove # "= {" vs "= \n {" 28 | nl_enum_brace = remove # "enum {" vs "enum \n {" 29 | nl_union_brace = remove # "union {" vs "union \n {" 30 | nl_struct_brace = remove # "struct {" vs "struct \n {" 31 | nl_do_brace = remove # "do {" vs "do \n {" 32 | nl_if_brace = remove # "if () {" vs "if () \n {" 33 | nl_for_brace = remove # "for () {" vs "for () \n {" 34 | nl_else_brace = remove # "else {" vs "else \n {" 35 | nl_while_brace = remove # "while () {" vs "while () \n {" 36 | nl_switch_brace = remove # "switch () {" vs "switch () \n {" 37 | nl_var_def_blk_end_func_top = 1 38 | nl_before_case = 1 39 | nl_fcall_brace = remove # "foo() {" vs "foo()\n{" 40 | nl_fdef_brace = remove # "int foo() {" vs "int foo()\n{" 41 | nl_after_return = TRUE 42 | nl_brace_while = remove 43 | nl_brace_else = remove # "} else" vs "} \n else" - cuddled else 44 | nl_squeeze_ifdef = TRUE 45 | 46 | # Positioning 47 | pos_bool = trail # BOOL ops on trailing end 48 | # The position of conditional operators, as in the '?' and ':' of 49 | # 'expr ? stmt : stmt', in wrapped expressions. 50 | pos_conditional = break # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force 51 | 52 | # Eat blanks 53 | eat_blanks_before_close_brace = TRUE 54 | eat_blanks_after_open_brace = TRUE 55 | 56 | # Modifiers 57 | mod_paren_on_return = false # "return 1;" vs "return (1);" 58 | mod_full_brace_if = force # "if (a) a--;" vs "if (a) { a--; }" 59 | mod_full_brace_for = force # "for () a--;" vs "for () { a--; }" 60 | mod_full_brace_do = force # "do a--; while ();" vs "do { a--; } while ();" 61 | mod_full_brace_while = force # "while (a) a--;" vs "while (a) { a--; }" 62 | 63 | # Spacing 64 | sp_try_brace = force 65 | sp_brace_catch = force # ignore/add/remove/force 66 | sp_brace_else = force 67 | sp_else_brace = force 68 | sp_before_byref = remove 69 | sp_before_semi = remove 70 | sp_paren_paren = remove # space between (( and )) 71 | sp_return_paren = remove # "return (1);" vs "return(1);" 72 | sp_sizeof_paren = remove # "sizeof (int)" vs "sizeof(int)" 73 | sp_before_sparen = force # "if (" vs "if(" 74 | sp_after_sparen = force # "if () {" vs "if (){" 75 | sp_after_cast = remove # "(int) a" vs "(int)a" 76 | sp_inside_braces = force # "{ 1 }" vs "{1}" 77 | sp_inside_braces_struct = force # "{ 1 }" vs "{1}" 78 | sp_inside_braces_enum = force # "{ 1 }" vs "{1}" 79 | sp_inside_paren = remove 80 | sp_inside_fparen = remove 81 | sp_inside_sparen = remove 82 | sp_inside_for = remove 83 | sp_inside_square = remove 84 | sp_assign = force 85 | sp_arith = force 86 | sp_bool = force 87 | sp_compare = force 88 | sp_assign = force 89 | sp_after_comma = force 90 | sp_func_def_paren = remove # "int foo (){" vs "int foo(){" 91 | sp_func_call_paren = remove # "foo (" vs "foo(" 92 | sp_func_proto_paren = remove # "int foo ();" vs "int foo();" 93 | sp_func_class_paren = remove 94 | sp_before_angle = remove 95 | sp_angle_word = force 96 | sp_angle_paren = remove 97 | sp_sparen_brace = add 98 | sp_fparen_brace = add 99 | sp_after_ptr_star = force 100 | sp_before_ptr_star = remove 101 | sp_between_ptr_star = remove 102 | 103 | # Alignment 104 | align_with_tabs = TRUE 105 | 106 | # Preprocessor 107 | pp_indent = remove # ignore/add/remove/force 108 | 109 | # Comment 110 | cmt_star_cont = TRUE 111 | -------------------------------------------------------------------------------- /vala-lint.conf: -------------------------------------------------------------------------------- 1 | [Checks] 2 | block-opening-brace-space-before=error 3 | double-semicolon=error 4 | double-spaces=error 5 | ellipsis=error 6 | line-length=warn 7 | naming-convention=error 8 | no-space=error 9 | note=warn 10 | space-before-paren=off 11 | use-of-tabs=off 12 | trailing-newlines=error 13 | trailing-whitespace=error 14 | unnecessary-string-template=error 15 | 16 | [Disabler] 17 | disable-by-inline-comments=true 18 | 19 | [line-length] 20 | max-line-length=120 21 | ignore-comments=true 22 | 23 | [naming-convention] 24 | exceptions=UUID, 25 | 26 | [note] 27 | keywords=TODO,FIXME, 28 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | v0.10.0 2 | --------------------------------------------------------------------------------