Full Code of AMNatty/wleave for AI

development 19de1116be22 cached
31 files
99.2 KB
25.4k tokens
121 symbols
1 requests
Download .txt
Repository: AMNatty/wleave
Branch: development
Commit: 19de1116be22
Files: 31
Total size: 99.2 KB

Directory structure:
gitextract_wwkaal9a/

├── .editorconfig
├── .gitignore
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── completions/
│   ├── _wleave
│   ├── wleave.bash
│   └── wleave.fish
├── completions_gen/
│   ├── .gitignore
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── icons/
│   └── icon-attribution.md
├── layout.json
├── man/
│   ├── wleave.1.scd
│   ├── wleave.5.scd
│   └── wleave.json.5.scd
├── sh.natty.Wleave.desktop
├── src/
│   ├── app/
│   │   ├── mod.rs
│   │   └── window.rs
│   ├── button.rs
│   ├── cli_opt.rs
│   ├── config.rs
│   ├── error.rs
│   ├── exec.rs
│   ├── layout.rs
│   ├── lib.rs
│   ├── main.rs
│   ├── paintable.rs
│   └── units.rs
└── style.css

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4

[*.rs]
max_line_length = 120

[*.json]
indent_size = 4

[*.scd]
indent_style = tab


================================================
FILE: .gitignore
================================================
/target
/.idea
/.vscode

/layout.dev


================================================
FILE: Cargo.toml
================================================
[package]
name = "wleave"
version = "0.7.2"
edition = "2024"
license = "MIT"
description = "A Wayland layer-shell logout prompt written in GTK"
repository = "https://github.com/AMNatty/wleave"

[workspace]
members = [".", "completions_gen"]

[dependencies]
clap = { version = "4.1", features = ["derive"] }

thiserror = "2"
miette = { version = "7", features = ["derive", "fancy"] }

cfg-if = "1"

convert_case = "0.10"

dirs = "6.0"
cairo-rs = "0.21"
glib = "0.21"
glib-macros = "0.21"
libadwaita = { version = "0.8", features = ["v1_7"] }
librsvg = "2.61"
gdk4 = { version = "0.10", features = ["v4_18"] }
gtk4 = { version = "0.10", features = ["gnome_45"] }
gtk4-layer-shell = "0.7"
which = "8"

cssparser = "0.36"

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Natty

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
./target/release/wleave: $(wildcard src/**.rs)
	cargo build --frozen --release --all-features

.PHONY: wleave
wleave: ./target/release/wleave

.PHONY: completions
completions: wleave
	mkdir -p completions
	OUT_DIR=completions cargo run --package wleave_completions --bin wleave_completions

.PHONY: all
all: wleave

.PHONY: clean
clean:
	rm -rf ./target ./completions_generated


================================================
FILE: README.md
================================================
# wleave

![AUR version](https://img.shields.io/aur/version/wleave-git)
![GitHub](https://img.shields.io/github/license/AMNatty/wleave)

A Wayland layer-shell logout prompt!

Originally a fork of [wlogout](https://github.com/ArtsyMacaw/wlogout), `wleave` is now natively GTK4 & Libadwaita and
has a bunch of added quality-of-life features.

![The default Wleave menu look](/example.png)

## Installation

### Arch Linux

**wleave** can be installed from the **AUR**:

```shell
paru -S wleave-git
```

### Building from sources

Dependencies:

- gtk4-layer-shell
- gtk4
- librsvg (for SVG images)
- libadwaita
- a stable version of the Rust toolchain

You can run the application using `cargo run --release` or GNU make:

```shell
make
./target/release/wleave
```

## Usage

The command line options are backwards-compatible with **wlogout**.
See `--help` for a list of options.

### Help, how do I close the menu?

The `<Esc>` key closes the menu, an option to change this may be added eventually.

## Configuration

**wleave** is backwards-compatible with **wlogout** configuration files.

Since **version 0.6.0**, *full JSON configuration* can be used in place of the `wlogout`-based
configuration. The default configuration file can be copied from `/etc/wleave/layout.json`.
The new configuration system is more flexible as it removes the need for extra command-line
arguments.

From `man 5 wleave.json`, the allowed top-level options are:

* `"buttons"` **(array)** - a list of buttons
* `"css"` **(string)** - Specify a custom CSS file instead of the default one
* `"service": false` **(boolean)**
  Run the application as a service, with all instances of wleave opening this one. Allows faster startup at the
  cost of running in the background
* `"button-layout": "grid"` Specify the way buttons should be laid out.
  See [dynamic layouts](#dynamic-layouts-supsince-070sup) for more details.
* `"buttons-per-row": "3"` **(string)** Set the number of buttons per row, or use a fraction to specify the number
  of rows to be used (e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows)
* `"column-spacing": "8px"` **(number / "#px" / "#%")** Set space between buttons columns
* `"row-spacing": "8px"` **(number / "#px" / "#%")** Set space between buttons rows
* `"margin": "20%"` **(number / "#px" / "#%")** Set margin on all sides
* `"margin-left"` **(number / "#px" / "#%")** Set margin for left of buttons. Falls back to the value set by *margin*
* `"margin-right"` **(number / "#px" / "#%")** Set margin for right of buttons. Falls back to the value set by
  *margin*
* `"margin-top"` **(number / "#px" / "#%")** Set margin for top of buttons. Falls back to the value set by *margin*
* `"margin-bottom"` **(number / "#px" / "#%")** Set margin for bottom of buttons. Falls back to the value set by
  *margin*
* `"button-aspect-ratio"` **(string or number)** Set the aspect ratio of the buttons, either as a float (as a number or
  string) or a ratio (e.g. "5/4"). If unspecified, the buttons fill all available space between the margins.
* `"close-on-lost-focus": false` **(boolean)** Closes the menu if focus is lost
* `"show-keybinds": false`: **(boolean)** Show the associated key binds for each button
* `"protocol": "layer-shell"` (**"layer-shell"**/**"xdg"**/**"none"**) Backend to use for full-screening the menu
* `"no-version-info": false` **(boolean)** Hides the version label.
* `"delay-command-ms": 100` **(number)** The number of milliseconds to wait after an action before the associated
  command is executed

The command-line option counterparts of these options take precedence over the configuration file.

*Example configuration* with one button that executes `swaylock` on click:

```json
{
    "margin": 200,
    "buttons-per-row": "1/1",
    "delay-command-ms": 100,
    "close-on-lost-focus": true,
    "show-keybinds": true,
    "buttons": [
        {
            "label": "lock",
            "action": "swaylock",
            "text": "Lock",
            "keybind": "l",
            "icon": "/usr/share/wleave/icons/lock.svg"
        }
    ]
}
```

Layout files may also be read from *stdin* with `--layout -`.
For example, with `jq`, buttons can be picked out:

```shell
$ jq '.buttons[] |= select([.label] | inside(["lock", "logout"]))' layout.json | wleave --layout -
```

### Running as a service <sup>since 0.7.0</sup>

Add the flag `--service` to run the application as a service, waiting until it is activated by another
call to `wleave`. This allows faster startup at the cost of running in the background.

For example, to run `wleave` in the background with the [Niri](https://github.com/YaLTeR/niri) compositor,
spawn the application on startup:

```kdl
spawn-at-startup "wleave" "--service"
```

Currently, the configuration is determined by the background instance, and a restart is necessary
to apply configuration changes.

### Dynamic layouts <sup>since 0.7.0</sup>

Choose one of the available layouts, using the `--button-layout` option.

#### Grid

Name: `"grid"` <br />
Since: 0.7

The default mode, with buttons being simply laid out in a grid.

### Conditional actions <sup>since 0.7.0</sup>

The action field can be an object with exactly one of `shell` or `executable` properties set. The `shell` action
executes the given command in the system shell, while the `executable` action executes the specified binary,
assuming it is in `$PATH`. `executable` actions get filtered if the specified executable does not exist,
allowing multiple possible options.

Any action that is **a plain string is interpreted as a shell command** with no conditions set.

Any extra properties in the `action` objects are interpreted as filters. Currently, only environment variable filtering
is implemented, where the values of fields prefixed with `$` are matched as conditions for the given action.

**The action field can also be an array where the first matching command is picked.**

For example, in the following example, the `loginctl lock-session` command is executed on GNOME, while other desktop
environments will try `gtklock` and then `swaylock`.

```json
{
    "action": [
        {
            "$DESKTOP_SESSION": "gnome",
            "shell": "loginctl lock-session"
        },
        {
            "executable": "gtklock"
        },
        "swaylock"
    ]
}
```

## Styling

By default, `wleave` follows `libadwaita` colors and uses CSS variables.
This allows following the system light/dark theme preference from GNOME settings.
In other desktop environments, this may be changed with
`gsettings set org.gnome.desktop.interface color-scheme "'prefer-dark'"` or
`gsettings set org.gnome.desktop.interface color-scheme "'prefer-light'"` correspondingly.

The stylesheet in `/etc/wleave/style.css` is fully customizable and can be edited.

### Colorized icons

SVG icons are dynamically recolored if possible. <small>(since 0.6.2)</small>

Each button has an identifier set in the layout file, which allows custom-styling each button
one-by-one. Icon colors may be changed by modifying the CSS variable `--view-fg-color`,
or by setting a custom `color` property entirely.

### Example recipe

Example stylesheet that makes the selected icon colored with the `libadwaita` accent color:

```css
window {
    background-color: rgba(12, 12, 12, 0.8);
}

button {
    color: var(--view-fg-color);
    background-color: var(--view-bg-color);
    border: none;
    padding: 10px;
}

button label.action-name {
    font-size: 24px;
}

button label.keybind {
    font-size: 20px;
    font-family: monospace;
}

button:hover label.keybind, button:focus label.keybind {
    opacity: 1;
}

button:hover,
button:focus {
    color: var(--accent-color);
    background-color: var(--window-bg-color);
}

button:active {
    color: var(--accent-fg-color);
    background-color: var(--accent-bg-color);
}

```

## Keybinds reference

See <https://gitlab.gnome.org/GNOME/gtk/-/blob/4.18.0/gdk/keynames.txt> for a list of valid keybinds.

## Enhancements

- A desktop file <sup>since 0.7</sup>
- Conditionally pick different actions on different desktop environments <sup>since 0.7</sup>
- SVG icons can be colorized via CSS `color` <sup>since 0.6</sup>
- Libadwaita accent colors <sup>since 0.6</sup>
- Automatic light theme by default <sup>since 0.6</sup>
- Natively GTK4 <sup>since 0.5</sup>
- New pretty icons by [@earth-walker](https://github.com/earth-walker)
- Autoclose when window focus is lost (the `-f/--close-on-lost-focus` flag)
- Mnemonic labels (the `-k/--show-keybinds` flag)
- Pretty gaps by default
- Less error-prone
- Keybinds accept modifier keys and Unicode characters
- Easier to extend


================================================
FILE: completions/_wleave
================================================
#compdef wleave

autoload -U is-at-least

_wleave() {
    typeset -A opt_args
    typeset -a _arguments_options
    local ret=1

    if is-at-least 5.2; then
        _arguments_options=(-s -S -C)
    else
        _arguments_options=(-s -C)
    fi

    local context curcontext="$curcontext" state line
    _arguments "${_arguments_options[@]}" : \
'-s+[Run the application in service mode - it will stay in the background until triggered]' \
'--service=[Run the application in service mode - it will stay in the background until triggered]' \
'-l+[Specify a layout file, specifying - will read the layout config from stdin]:LAYOUT:_files' \
'--layout=[Specify a layout file, specifying - will read the layout config from stdin]:LAYOUT:_files' \
'--button-layout=[Specify the way the buttons should be laid out in the available space]:BUTTON_LAYOUT:(grid)' \
'-C+[Specify a custom CSS file]:CSS:_files' \
'--css=[Specify a custom CSS file]:CSS:_files' \
'-b+[Set the number of buttons per row, or use a fraction to specify the number of rows to be used (e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows)]:BUTTONS_PER_ROW:_default' \
'--buttons-per-row=[Set the number of buttons per row, or use a fraction to specify the number of rows to be used (e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows)]:BUTTONS_PER_ROW:_default' \
'-c+[Set space between buttons columns]:COLUMN_SPACING:_default' \
'--column-spacing=[Set space between buttons columns]:COLUMN_SPACING:_default' \
'-r+[Set space between buttons rows]:ROW_SPACING:_default' \
'--row-spacing=[Set space between buttons rows]:ROW_SPACING:_default' \
'-m+[Set the margin around buttons]:MARGIN:_default' \
'--margin=[Set the margin around buttons]:MARGIN:_default' \
'-L+[Set margin for the left of buttons]:MARGIN_LEFT:_default' \
'--margin-left=[Set margin for the left of buttons]:MARGIN_LEFT:_default' \
'-R+[Set margin for the right of buttons]:MARGIN_RIGHT:_default' \
'--margin-right=[Set margin for the right of buttons]:MARGIN_RIGHT:_default' \
'-T+[Set margin for the top of buttons]:MARGIN_TOP:_default' \
'--margin-top=[Set margin for the top of buttons]:MARGIN_TOP:_default' \
'-B+[Set the margin for the bottom of buttons]:MARGIN_BOTTOM:_default' \
'--margin-bottom=[Set the margin for the bottom of buttons]:MARGIN_BOTTOM:_default' \
'-A+[Set the aspect ratio of the buttons]:BUTTON_ASPECT_RATIO:_default' \
'--button-aspect-ratio=[Set the aspect ratio of the buttons]:BUTTON_ASPECT_RATIO:_default' \
'-d+[The delay (in milliseconds) between the window closing and executing the selected option]:DELAY_COMMAND_MS:_default' \
'--delay-command-ms=[The delay (in milliseconds) between the window closing and executing the selected option]:DELAY_COMMAND_MS:_default' \
'-f+[Close the menu on lost focus]' \
'--close-on-lost-focus=[Close the menu on lost focus]' \
'-k+[Show the associated key binds]' \
'--show-keybinds=[Show the associated key binds]' \
'-p+[Use layer-shell or xdg protocol]:PROTOCOL:(layer-shell none xdg)' \
'--protocol=[Use layer-shell or xdg protocol]:PROTOCOL:(layer-shell none xdg)' \
'-x+[Hide version information]' \
'--no-version-info=[Hide version information]' \
'-v[]' \
'--version[]' \
'-h[Print help]' \
'--help[Print help]' \
&& ret=0
}

(( $+functions[_wleave_commands] )) ||
_wleave_commands() {
    local commands; commands=()
    _describe -t commands 'wleave commands' commands "$@"
}

if [ "$funcstack[1]" = "_wleave" ]; then
    _wleave "$@"
else
    compdef _wleave wleave
fi


================================================
FILE: completions/wleave.bash
================================================
_wleave() {
    local i cur prev opts cmd
    COMPREPLY=()
    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
        cur="$2"
    else
        cur="${COMP_WORDS[COMP_CWORD]}"
    fi
    prev="$3"
    cmd=""
    opts=""

    for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
    do
        case "${cmd},${i}" in
            ",$1")
                cmd="wleave"
                ;;
            *)
                ;;
        esac
    done

    case "${cmd}" in
        wleave)
            opts="-v -s -l -C -b -c -r -m -L -R -T -B -A -d -f -k -p -x -h --version --service --layout --button-layout --css --buttons-per-row --column-spacing --row-spacing --margin --margin-left --margin-right --margin-top --margin-bottom --button-aspect-ratio --delay-command-ms --close-on-lost-focus --show-keybinds --protocol --no-version-info --help"
            if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
                return 0
            fi
            case "${prev}" in
                --service)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                -s)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                --layout)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -l)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --button-layout)
                    COMPREPLY=($(compgen -W "grid" -- "${cur}"))
                    return 0
                    ;;
                --css)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -C)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --buttons-per-row)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -b)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --column-spacing)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -c)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --row-spacing)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -r)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --margin)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -m)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --margin-left)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -L)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --margin-right)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -R)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --margin-top)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -T)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --margin-bottom)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -B)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --button-aspect-ratio)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -A)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --delay-command-ms)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                -d)
                    COMPREPLY=($(compgen -f "${cur}"))
                    return 0
                    ;;
                --close-on-lost-focus)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                -f)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                --show-keybinds)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                -k)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                --protocol)
                    COMPREPLY=($(compgen -W "layer-shell none xdg" -- "${cur}"))
                    return 0
                    ;;
                -p)
                    COMPREPLY=($(compgen -W "layer-shell none xdg" -- "${cur}"))
                    return 0
                    ;;
                --no-version-info)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                -x)
                    COMPREPLY=($(compgen -W "true false" -- "${cur}"))
                    return 0
                    ;;
                *)
                    COMPREPLY=()
                    ;;
            esac
            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
            return 0
            ;;
    esac
}

if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
    complete -F _wleave -o nosort -o bashdefault -o default wleave
else
    complete -F _wleave -o bashdefault -o default wleave
fi


================================================
FILE: completions/wleave.fish
================================================
complete -c wleave -s s -l service -d 'Run the application in service mode - it will stay in the background until triggered' -r -f -a "true\t''
false\t''"
complete -c wleave -s l -l layout -d 'Specify a layout file, specifying - will read the layout config from stdin' -r -F
complete -c wleave -l button-layout -d 'Specify the way the buttons should be laid out in the available space' -r -f -a "grid\t''"
complete -c wleave -s C -l css -d 'Specify a custom CSS file' -r -F
complete -c wleave -s b -l buttons-per-row -d 'Set the number of buttons per row, or use a fraction to specify the number of rows to be used (e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows)' -r
complete -c wleave -s c -l column-spacing -d 'Set space between buttons columns' -r
complete -c wleave -s r -l row-spacing -d 'Set space between buttons rows' -r
complete -c wleave -s m -l margin -d 'Set the margin around buttons' -r
complete -c wleave -s L -l margin-left -d 'Set margin for the left of buttons' -r
complete -c wleave -s R -l margin-right -d 'Set margin for the right of buttons' -r
complete -c wleave -s T -l margin-top -d 'Set margin for the top of buttons' -r
complete -c wleave -s B -l margin-bottom -d 'Set the margin for the bottom of buttons' -r
complete -c wleave -s A -l button-aspect-ratio -d 'Set the aspect ratio of the buttons' -r
complete -c wleave -s d -l delay-command-ms -d 'The delay (in milliseconds) between the window closing and executing the selected option' -r
complete -c wleave -s f -l close-on-lost-focus -d 'Close the menu on lost focus' -r -f -a "true\t''
false\t''"
complete -c wleave -s k -l show-keybinds -d 'Show the associated key binds' -r -f -a "true\t''
false\t''"
complete -c wleave -s p -l protocol -d 'Use layer-shell or xdg protocol' -r -f -a "layer-shell\t''
none\t''
xdg\t''"
complete -c wleave -s x -l no-version-info -d 'Hide version information' -r -f -a "true\t''
false\t''"
complete -c wleave -s v -l version
complete -c wleave -s h -l help -d 'Print help'


================================================
FILE: completions_gen/.gitignore
================================================
/target
/.idea
/.vscode

================================================
FILE: completions_gen/Cargo.toml
================================================
[package]
name = "wleave_completions"
version = "0.1.0"
edition = "2021"

[dependencies]
clap_complete = "4.1"
clap = "4.1"
wleave = { path = ".." }

================================================
FILE: completions_gen/src/main.rs
================================================
use clap::CommandFactory;
use clap_complete::{generate_to, shells};
use std::env;
use std::io::Error;

use wleave::cli_opt::Args;

fn main() -> Result<(), Error> {
    let outdir = match env::var_os("OUT_DIR") {
        None => return Ok(()),
        Some(outdir) => outdir,
    };

    let mut cmd = Args::command();

    println!(
        "Bash completions generated: {:?}",
        generate_to(shells::Bash, &mut cmd, "wleave", &outdir)?
    );

    println!(
        "Zsh completions generated: {:?}",
        generate_to(shells::Zsh, &mut cmd, "wleave", &outdir)?
    );

    println!(
        "Fish completions generated: {:?}",
        generate_to(shells::Fish, &mut cmd, "wleave", &outdir)?
    );

    Ok(())
}


================================================
FILE: icons/icon-attribution.md
================================================
# Icon Attribution

All icons are licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) by Earth Walker 2023

Modified in version 0.6.0 by Natty to support colorization.


================================================
FILE: layout.json
================================================
{
    "buttons": [
        {
            "label": "lock",
            "action": [
                {
                    "$DESKTOP_SESSION": "gnome",
                    "shell": "loginctl lock-session"
                },
                {
                    "executable": "gtklock"
                },
                {
                    "executable": "swaylock"
                }
            ],
            "text": "Lock",
            "keybind": "l",
            "icon": "/usr/share/wleave/icons/lock.svg"
        },
        {
            "label": "hibernate",
            "action": "systemctl hibernate",
            "text": "Hibernate",
            "keybind": "h",
            "icon": "/usr/share/wleave/icons/hibernate.svg"
        },
        {
            "label": "logout",
            "action": [
                {
                    "$DESKTOP_SESSION": "niri",
                    "shell": "niri msg action quit --skip-confirmation"
                },
                {
                    "$DESKTOP_SESSION": "sway",
                    "shell": "swaymsg exit"
                },
                {
                    "$DESKTOP_SESSION": "plasma",
                    "shell": "qdbus org.kde.KWin /Session org.kde.KWin.Session.quit"
                },
                {
                    "$DESKTOP_SESSION": "gnome",
                    "shell": "gnome-session-quit --no-prompt"
                },
                "loginctl terminate-user $USER"
            ],
            "text": "Logout",
            "keybind": "e",
            "icon": "/usr/share/wleave/icons/logout.svg"
        },
        {
            "label": "shutdown",
            "action": "systemctl poweroff",
            "text": "Shutdown",
            "keybind": "s",
            "icon": "/usr/share/wleave/icons/shutdown.svg"
        },
        {
            "label": "suspend",
            "action": "systemctl suspend",
            "text": "Suspend",
            "keybind": "u",
            "icon": "/usr/share/wleave/icons/suspend.svg"
        },
        {
            "label": "reboot",
            "action": "systemctl reboot",
            "text": "Reboot",
            "keybind": "r",
            "icon": "/usr/share/wleave/icons/reboot.svg"
        }
    ]
}


================================================
FILE: man/wleave.1.scd
================================================
wleave(1)

# NAME

wleave - A Wayland logout menu

# SYNOPSIS

*wleave* [options] [command]

# OPTIONS

*-h, --help*
	Show help message and stop

*-l, --layout* <layout>
	Specify a custom layout file, specify - to read the config from stdin

*--button-layout* "grid"
	Specify the way buttons should be laid out, "grid" being the default. Currently, only "grid" is supported.

*-S, --service*
	Run the application as a service, with all instances of wleave opening this one. Allows faster startup at the cost of running in the background.

*-v, --version*
	Show version number and stop

*-C, -css* <css>
	Specify a custom css file

*-b, --buttons-per-row* <num>
	Set the number of buttons per row, or use a fraction to specify the number of rows to be used (e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows)

*-c, --column-spacing* <space>
	Set space between buttons columns

*-r, --row-spacing* <space>
	Set space between buttons rows

*-m, --margin* <padding>
	Set margin on all sides

*-L, --margin-left* <padding>
	Set margin for left of buttons

*-R, --margin-right* <padding>
	Set margin for right of buttons

*-T, --margin-top* <padding>
	Set margin for top of buttons

*-B, --margin-bottom* <padding>
	Set margin for bottom of buttons

*-A, --button-aspect-ratio* <ratio>
	Set the aspect ratio of the buttons, either as a float (as a number or string) or a ratio (e.g. "5/4").
	If unspecified, the buttons fill all available space between the margins.

*-f, --close-on-lost-focus*
	Closes the menu if focus is lost

*-k, --show-keybinds*
	Show the associated key binds for each button

*-p, --protocol* <protocol>
	Takes layer-shell, xdg, or none.
	The "layer-shell" allows transparency effects; however, only a few compositors correctly support it.
	The "xdg" protocol will work on almost all compositors, but does not allow for transparency.
	The "none" option shows the window as is, without full-screening it

*-d, --delay-command-ms* <number>
	The number of milliseconds to wait after an action before the associated command is executed

*-x, --no-version-info*
	Hide the version label

# DESCRIPTION

wleave is a Wayland-native logout script. It is a modern rewrite of Wlogout and a drop-in replacement.

# CONFIGURATION

wleave searches for a layout and style.css file in the following locations, in this order:

. $XDG_CONFIG_HOME/wleave/
. $XDG_CONFIG_HOME/wlogout/
. /etc/wleave/
. /etc/wlogout/
. /usr/local/etc/wleave
. /usr/local/etc/wlogout

If unset, $XDG_CONFIG_HOME defaults to *~/.config/*.

An error is raised when no layout file is found; However, the style.css file is optional. If you would like to customise either it is recommended that you copy the defaults from */etc/wleave/* into  *~/.config* and make any changes there.

# AUTHORS

Based on Wlogout by Haden Collins <collinshaden@gmail.com>. For more information about wlogout, see <https://github.com/ArtsyMacaw/wlogout>.
Rewrite by Natty <natty.sh.git@gmail.com>, see <https://github.com/AMNatty/wleave>.

# SEE ALSO

*wleave*(5)


================================================
FILE: man/wleave.5.scd
================================================
wleave(5)

# NAME

wleave - layout file and options

# LAYOUT

wleave buttons can have the following properties
- label 
- action
- text
- keybind
- icon
- height \*
- width \* 
- circular \*

\* Optional values

Label is the css selector by which the buttons may be referred to in a *style.css* file, action is the shell command to be executed when the button is clicked, text is the description displayed on the button, keybind is the key mapped to the button (note escape is reserved for exiting the application), height and width are values between 0.0 and 1.0 that control the location of where *text* is displayed the default width 0.5, height 0.9, and circular is a boolean value that makes a button round. 

# FILE

The buttons values are specified in a JSON-like formatted file, wherein the values are used as keys and one button corresponds to one JSON object for example:
```
{
    "label" : "foo",
    "action" : "echo 'hello world'",
    "text" : "bar",
    "keybind" : "f",
    "height" : 1, 
    "width" : 1,
    "circular" : true
}
```
Would create a round button that has a css label of *foo*, prints "hello world" upon being clicked, displays "bar" on the button, be bound to the key 'f', and "bar" would be shown at the bottom right corner. To create multiple buttons simply create another JSON object.

# AUTHORS

Based on Wlogout by Haden Collins <collinshaden@gmail.com>. For more information about wlogout, see <https://github.com/ArtsyMacaw/wlogout>.
Rewrite by Natty <natty.sh.git@gmail.com>, see <https://github.com/AMNatty/wleave>.

# SEE ALSO

*wleave.json*(5)
*wleave*(1)


================================================
FILE: man/wleave.json.5.scd
================================================
wleave.json(5)

# NAME

wleave.json - layout JSON file and configuration options

# TOP-LEVEL-OPTIONS

The top-level options directly correspond to their long-version command-line option counter parts.
Button layout is specified in the extra *"button"* field.

*"buttons"*: <array of buttons>
	See section *LAYOUT* for structure

*"button-layout"*: <*"grid"*>
	Specify the way buttons should be laid out, "grid" being the default. Currently, only "grid" is supported.

*"css"*: <string>
	Specify a custom CSS file instead of the default one

*"service"*: <boolean>
	Run the application as a service, with all instances of wleave opening this one. Allows faster startup at the
	cost of running in the background. ++
*Default*: false

*"buttons-per-row"*: <string>
	Set the number of buttons per row, or use a fraction to specify the number of rows to be used
	(e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows) ++
*Default*: "3"

*"column-spacing"*: <number / "<number>px" / "<number>%">
	Set space between button columns ++
*Default*: "8px"

*"row-spacing"*: <number / "<number>px" / "<number>%">
	Set space between button rows ++
*Default*: "8px"

*"margin"*: <number / "<number>px" / "<number>%">
	Set margin on all sides ++
*Default*: "20%" (of window size)

*"margin-left"* <number / "<number>px" / "<number>%">
	Set margin for left of buttons ++
Falls back to the value set by *margin*

*"margin-right"* <number / "<number>px" / "<number>%">
	Set margin for right of buttons ++
Falls back to the value set by *margin*

*"margin-top"* <number / "<number>px" / "<number>%">
	Set margin for top of buttons ++
Falls back to the value set by *margin*

*"margin-bottom"* <number / "<number>px" / "<number>%">
	Set margin for bottom of buttons ++
Falls back to the value set by *margin*

*"button-aspect-ratio"* <string or number>
	Set the aspect ratio of the buttons, either as a float (as a number or string) or a ratio (e.g. "5/4"). ++
If unspecified, the buttons fill all available space between the margins.

*"close-on-lost-focus"*: <*true*/*false*>
	Closes the menu if focus is lost ++
*Default*: false

*"show-keybinds"*: <*true*/*false*>
	Show the associated key binds for each button ++
*Default*: false

*"protocol"*: <*"layer-shell"*/*"xdg"*/*"none"*>
	The layer-shell protocol is the intended protocol to be used with wleave, however it requires compositor support ++
*Default*: "layer-shell"

*"no-version-info"*: <*true*/*false*>
	Hides the version label ++
*Default*: false

*"delay-command-ms"*: <number>
	The number of milliseconds to wait after an action before the associated command is executed ++
*Default*: 100

# LAYOUT

wleave buttons may have the following properties:

- *"label"* (string) - internal name of the button that can be used in CSS as an identifier
- *"action"* (command string or object or list thereof) - the action that will be executed via shell upon pressing the button
- *"text"* (string) - the visible label text of the button
- *"keybind"* (string) - a GTK key name associated with the button
- *"icon"* (path string) - filename to load an icon image from
- *"height"* (*optional*, number) - value in the range 0.0 to 1.0 to override the placement of the label in the button
- *"width"* (*optional*, number) - value in the range 0.0 to 1.0 to override the placement of the label in the button
- *"circular"* (*optional*, boolean, default false)

The action field can be an object with exactly one of `shell` or `executable` properties set. The `shell` action executes
the given command in the system shell, while the `executable` action executes the specified binary, assuming it is
in `$PATH`. `executable` actions get filtered if the specified executable does not exist, allowing multiple possible options.

Any extra properties in the `action` objects are interpreted as filters. Currently, only environment variable filtering
is implemented, where the values fields prefixed with `$` are matched as conditions for the given action.

The action field can also be an array where the first matching command is picked.

For example, in the following example, the `loginctl lock-session` command is executed on GNOME, while other desktop
environments will try either `gtklock` or `swaylock` in that order.

```
"action": [
    {
        "$DESKTOP_SESSION": "gnome",
        "shell": "loginctl lock-session"
    },
    {
        "executable": "gtklock"
    },
    {
        "executable": "swaylock"
    }
]
```

# FILE

Example file:

```
{
    "close-on-lost-focus": true,
    "show-keybinds": true,
    "buttons-per-row": "1/1",
    "buttons": [
        {
            "label": "lock",
            "action": "swaylock",
            "text": "Lock",
            "keybind": "l",
            "icon": "/usr/share/wleave/icons/lock.svg"
        }
    ]
}

```

# AUTHORS

Based on Wlogout by Haden Collins <collinshaden@gmail.com>. For more information about wlogout, see <https://github.com/ArtsyMacaw/wlogout>.
Rewrite by Natty <natty.sh.git@gmail.com>, see <https://github.com/AMNatty/wleave>.

# SEE ALSO

*wleave*(5)
*wleave*(1)


================================================
FILE: sh.natty.Wleave.desktop
================================================
[Desktop Entry]
# The version of the Desktop Entry spec, not of the application:
Version=1.0
Type=Application
Name=Wleave
Comment=Open a system log-out menu
Icon=wleave
Exec=wleave
Terminal=false
StartupNotify=false
Categories=System;


================================================
FILE: src/app/mod.rs
================================================
pub mod window;

use crate::app::window::WleaveWindow;
use crate::button::{WButton, WButtonActionList, WButtonJustify};
use crate::config::AppConfig;
use crate::exec::run_command;
use crate::layout::MenuLayout;
use crate::paintable::svg_picture_colorized;
use glib::object::Cast;
use glib::timeout_add_local_once;
use glib_macros::{clone, closure};
use gtk4::prelude::{BoxExt, ButtonExt, GObjectPropertyExpressionExt, GtkWindowExt, WidgetExt};
use gtk4::{EventControllerKey, GestureClick, PropagationPhase};
use gtk4_layer_shell::{KeyboardMode, LayerShell};
use libadwaita::prelude::AdwApplicationWindowExt;
use std::sync::Arc;
use std::time::Duration;
use wleave::cli_opt::{ButtonLayout, Protocol};
use wleave::units::{AspectRatio, LengthArgs, LengthDimension};

fn do_exit(window: &WleaveWindow, _service_mode: bool) {
    window.close();
}

fn on_option(
    command_list: &WButtonActionList,
    delay_ms: u32,
    service_mode: bool,
    window: WleaveWindow,
) {
    let Some(command) = command_list.enumerate().find(|w| w.is_applicable()) else {
        return;
    };

    let command = command.clone();

    window.connect_hide(clone!(
        #[strong]
        command,
        move |window| {
            timeout_add_local_once(
                Duration::from_millis(delay_ms.into()),
                clone!(
                    #[strong]
                    command,
                    #[weak_allow_none]
                    window,
                    move || {
                        run_command(command);
                        window.inspect(move |w| do_exit(w, service_mode));
                    }
                ),
            );
        }
    ));

    window.set_visible(false);
}

fn handle_key(
    config: &Arc<AppConfig>,
    window: &WleaveWindow,
    key: &gtk4::gdk::Key,
) -> glib::Propagation {
    if let &gtk4::gdk::Key::Escape = key {
        do_exit(window, config.service);
        return glib::Propagation::Proceed;
    }

    let key = key
        .to_unicode()
        .map(|c| c.to_string())
        .or_else(|| key.name().map(|s| s.to_string()));

    if let Some(ref key_name) = key {
        let button = config.buttons.iter().find(|b| b.keybind == *key_name);

        if let Some(WButton { action, .. }) = button {
            on_option(
                action,
                config.delay_command_ms,
                config.service,
                window.clone(),
            );
        }
    }

    glib::Propagation::Proceed
}

pub fn create_app(config: &Arc<AppConfig>, app: &libadwaita::Application) -> WleaveWindow {
    let service_mode = config.service;

    let container_box = gtk4::CenterBox::builder()
        .valign(gtk4::Align::Fill)
        .halign(gtk4::Align::Fill)
        .orientation(gtk4::Orientation::Vertical)
        .build();

    let window = WleaveWindow::new(app);
    window.set_content(Some(&container_box));

    let margin_left = config
        .margin_left
        .clone()
        .unwrap_or_else(|| config.margin.clone());
    let margin_top = config
        .margin_top
        .clone()
        .unwrap_or_else(|| config.margin.clone());
    let margin_right = config
        .margin_right
        .clone()
        .unwrap_or_else(|| config.margin.clone());
    let margin_bottom = config
        .margin_top
        .clone()
        .unwrap_or_else(|| config.margin.clone());

    window.connect_window_width_notify(clone!(
        #[weak_allow_none]
        container_box,
        move |w| {
            let Some(container_box) = container_box else {
                return;
            };

            let arg = LengthArgs {
                viewport: (w.width() as f32, w.height() as f32),
                dimension: LengthDimension::Horizontal,
            };

            container_box.set_margin_start(margin_left.0.for_args(&arg) as i32);
            container_box.set_margin_end(margin_right.0.for_args(&arg) as i32);
        }
    ));

    window.connect_window_height_notify(clone!(
        #[weak_allow_none]
        container_box,
        move |w| {
            let Some(container_box) = container_box else {
                return;
            };

            let arg = LengthArgs {
                viewport: (w.width() as f32, w.height() as f32),
                dimension: LengthDimension::Vertical,
            };

            container_box.set_margin_top(margin_top.0.for_args(&arg) as i32);
            container_box.set_margin_bottom(margin_bottom.0.for_args(&arg) as i32);
        }
    ));

    match config.protocol {
        Protocol::LayerShell => {
            window.init_layer_shell();
            window.set_layer(gtk4_layer_shell::Layer::Overlay);
            window.set_namespace(Some("wleave"));
            window.set_exclusive_zone(-1);
            window.set_keyboard_mode(KeyboardMode::Exclusive);

            window.set_anchor(gtk4_layer_shell::Edge::Left, true);
            window.set_anchor(gtk4_layer_shell::Edge::Right, true);
            window.set_anchor(gtk4_layer_shell::Edge::Top, true);
            window.set_anchor(gtk4_layer_shell::Edge::Bottom, true);
        }
        Protocol::Xdg => {
            window.fullscreen();
        }
        Protocol::None => {}
    }

    if config.close_on_lost_focus {
        window.connect_is_active_notify(move |window| {
            if window.is_visible() && !window.is_active() && !service_mode {
                do_exit(window, service_mode);
            }
        });
    }

    let click_away_controller = GestureClick::builder()
        .propagation_phase(PropagationPhase::Bubble)
        .button(gtk4::gdk::BUTTON_PRIMARY)
        .n_points(1)
        .build();
    click_away_controller.connect_released(clone!(
        #[weak]
        window,
        #[upgrade_or_panic]
        move |_, _, _, _| {
            do_exit(&window, service_mode);
        }
    ));
    window.add_controller(click_away_controller);

    let key_controller = EventControllerKey::new();
    key_controller.connect_key_pressed(clone!(
        #[strong]
        config,
        #[weak]
        window,
        #[upgrade_or_panic]
        move |_, key, _, _| handle_key(&config, &window, &key)
    ));
    window.add_controller(key_controller);

    let btn_count = config.buttons.len() as u32;
    let buttons_per_row = match config.buttons_per_row {
        ButtonLayout::Auto => None,
        ButtonLayout::PerRow(n) => Some(n),
        ButtonLayout::RowRatio(n, d) => Some(btn_count * n / d.min(btn_count * n)),
    };

    let conf_column_spacing = config.column_spacing.clone();
    let column_spacing = window
        .property_expression_weak("window-width")
        .chain_closure::<f32>(closure!(move |w: Option<WleaveWindow>, width: i32| {
            conf_column_spacing.for_args(&LengthArgs {
                viewport: (
                    width as f32,
                    w.as_ref()
                        .map(WleaveWindow::window_width)
                        .unwrap_or_default() as f32,
                ),
                dimension: LengthDimension::Horizontal,
            })
        }))
        .upcast();

    let conf_row_spacing = config.column_spacing.clone();
    let row_spacing = window
        .property_expression_weak("window-height")
        .chain_closure::<f32>(closure!(move |w: Option<WleaveWindow>, height: i32| {
            conf_row_spacing.for_args(&LengthArgs {
                viewport: (
                    w.as_ref()
                        .map(WleaveWindow::window_width)
                        .unwrap_or_default() as f32,
                    height as f32,
                ),
                dimension: LengthDimension::Horizontal,
            })
        }))
        .upcast();

    let buttons_container = gtk4::Box::builder()
        .valign(gtk4::Align::Fill)
        .halign(gtk4::Align::Fill)
        .layout_manager(&MenuLayout::new(
            config.button_layout,
            config.button_aspect_ratio.map(AspectRatio::as_float),
            column_spacing,
            row_spacing,
            buttons_per_row,
        ))
        .build();

    for bttn in config.buttons.iter() {
        let justify = match bttn.justify {
            WButtonJustify::Center => gtk4::Justification::Center,
            WButtonJustify::Fill => gtk4::Justification::Fill,
            WButtonJustify::Left => gtk4::Justification::Left,
            WButtonJustify::Right => gtk4::Justification::Right,
        };

        let button = gtk4::Button::builder()
            .name(&bttn.label)
            .hexpand(true)
            .vexpand(true)
            .cursor(&gdk4::Cursor::from_name("pointer", None).expect("pointer cursor not found"))
            .build();

        let overlay = gtk4::Overlay::builder().vexpand(true).hexpand(true).build();

        if config.show_keybinds {
            let key_label = gtk4::Label::builder()
                .label(format!("[{}]", bttn.keybind))
                .halign(gtk4::Align::Start)
                .valign(gtk4::Align::Start)
                .css_classes(["dimmed", "keybind"])
                .build();

            overlay.add_overlay(&key_label);
        }

        let inner = gtk4::Box::builder()
            .orientation(gtk4::Orientation::Vertical)
            .valign(gtk4::Align::Center)
            .build();

        let picture = if let Some(icon) = &bttn.icon {
            let picture = if icon.ends_with(".svg") {
                svg_picture_colorized(icon).upcast()
            } else {
                gtk4::Picture::for_filename(icon)
            };

            picture.set_content_fit(gtk4::ContentFit::ScaleDown);
            picture.add_css_class("icon-dropshadow");

            inner.append(&picture);
            Some(picture)
        } else {
            None
        };

        let label = gtk4::Label::builder()
            .label(&bttn.text)
            .css_classes(["action-name"])
            .use_markup(true)
            .justify(justify)
            .build();

        // Picture being none means the old system to configure buttons is used
        if bttn.width.is_some() || bttn.height.is_some() || picture.is_none() {
            label.set_xalign(bttn.width.unwrap_or(0.5));
            label.set_yalign(bttn.height.unwrap_or(0.9));
            overlay.add_overlay(&label);
        } else {
            inner.insert_child_after(&label, picture.as_ref());
        }

        overlay.set_child(Some(&inner));

        if bttn.circular {
            button.add_css_class("circular");
        }

        button.connect_clicked(clone!(
            #[weak]
            window,
            #[to_owned(rename_to = action)]
            &bttn.action,
            #[to_owned(rename_to = delay_ms)]
            &config.delay_command_ms,
            #[upgrade_or_panic]
            move |_| on_option(&action, delay_ms, service_mode, window)
        ));

        button.set_child(Some(&overlay));

        buttons_container.append(&button);
    }

    container_box.set_shrink_center_last(false);
    container_box.set_center_widget(Some(&buttons_container));

    if !config.no_version_info {
        let version_info = gtk4::Label::builder()
        .label(format!(
            "Wleave {}. <a href=\"https://github.com/AMNatty/wleave/releases/tag/0.6.0\">Missing or broken icons?</a>",
            env!("CARGO_PKG_VERSION")
        ))
        .use_markup(true)
        .can_focus(false)
        .css_classes(["dimmed", "version-info"])
        .margin_top(12)
        .build();
        container_box.set_end_widget(Some(&version_info));
    }

    window
}


================================================
FILE: src/app/window.rs
================================================
mod window_impl {
    use glib::object::ObjectExt;
    use glib::subclass::object::DerivedObjectProperties;
    use glib::subclass::object::{ObjectImpl, ObjectImplExt};
    use glib::subclass::types::{ObjectSubclass, ObjectSubclassExt};
    use glib_macros::Properties;
    use gtk4::prelude::WidgetExt;
    use gtk4::subclass::application_window::ApplicationWindowImpl;
    use gtk4::subclass::widget::{WidgetImpl, WidgetImplExt};
    use gtk4::subclass::window::WindowImpl;
    use libadwaita::subclass::application_window::AdwApplicationWindowImpl;
    use std::cell::Cell;

    #[derive(Properties, Default)]
    #[properties(wrapper_type = super::WleaveWindow)]
    pub struct WleaveWindowImpl {
        #[property(name = "window-width", get, set)]
        pub window_width: Cell<i32>,
        #[property(name = "window-height", get, set)]
        pub window_height: Cell<i32>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for WleaveWindowImpl {
        const NAME: &'static str = "WleaveWindow";

        type Type = super::WleaveWindow;

        type ParentType = libadwaita::ApplicationWindow;

        type Interfaces = ();
    }

    #[glib::derived_properties]
    impl ObjectImpl for WleaveWindowImpl {
        fn constructed(&self) {
            self.parent_constructed();

            let obj = self.obj();
            obj.set_window_width(obj.width());
            obj.set_window_height(obj.height());
        }
    }

    impl WidgetImpl for WleaveWindowImpl {
        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
            self.parent_size_allocate(width, height, baseline);

            let obj = self.obj();
            obj.set_window_width(obj.width());
            obj.set_window_height(obj.height());
        }
    }
    impl WindowImpl for WleaveWindowImpl {}
    impl ApplicationWindowImpl for WleaveWindowImpl {}
    impl AdwApplicationWindowImpl for WleaveWindowImpl {}
}

glib::wrapper! {
    pub struct WleaveWindow(ObjectSubclass<window_impl::WleaveWindowImpl>)
        @extends libadwaita::ApplicationWindow, gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
        @implements gtk4::gio::ActionGroup, gtk4::gio::ActionMap, gtk4::Accessible,
                    gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root,
                    gtk4::ShortcutManager;
}

impl WleaveWindow {
    pub fn new(app: &libadwaita::Application) -> Self {
        glib::Object::builder()
            .property("application", app)
            .property("title", "wleave")
            .property("window-width", 400)
            .property("window-height", 400)
            .build()
    }
}


================================================
FILE: src/button.rs
================================================
use serde::de::{IntoDeserializer, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer};
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::str::FromStr;
use tracing::warn;

#[derive(Debug, Clone, Deserialize)]
pub enum WButtonActionHandler {
    #[serde(rename = "shell")]
    Shell(String),
    #[serde(rename = "executable")]
    Executable(String),
}

#[derive(Debug, Clone, Deserialize)]
pub struct WButtonAction {
    #[serde(flatten)]
    pub handler: WButtonActionHandler,
    #[serde(flatten)]
    pub conditions: BTreeMap<String, String>,
}

impl WButtonAction {
    pub fn is_applicable(&self) -> bool {
        if let WButtonActionHandler::Executable(exe) = &self.handler
            && let Err(e) = which::which(exe)
        {
            warn!("Executable {} not available, skipping: {}", exe, e);
            return false;
        }

        for (key, value) in self.conditions.iter() {
            if let Some(env_var) = key.strip_prefix("$") {
                let Ok(var) = std::env::var(env_var) else {
                    return false;
                };

                if var != *value {
                    return false;
                }
            }
        }

        true
    }
}

impl FromStr for WButtonAction {
    type Err = Infallible;

    fn from_str(action: &str) -> Result<Self, Self::Err> {
        Ok(WButtonAction {
            handler: WButtonActionHandler::Shell(action.to_string()),
            conditions: Default::default(),
        })
    }
}

struct WButtonActionVisitor;

impl<'de> Visitor<'de> for WButtonActionVisitor {
    type Value = WButtonAction;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("string or map")
    }

    fn visit_str<E>(self, value: &str) -> Result<WButtonAction, E>
    where
        E: serde::de::Error,
    {
        Ok(FromStr::from_str(value).expect("shell script always valid"))
    }

    fn visit_string<E>(self, val: String) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        self.visit_str(&val)
    }

    fn visit_map<M>(self, map: M) -> Result<WButtonAction, M::Error>
    where
        M: serde::de::MapAccess<'de>,
    {
        Deserialize::deserialize(serde::de::value::MapAccessDeserializer::new(map))
    }
}

struct WButtonActionWrapper(WButtonAction);

impl<'de> Deserialize<'de> for WButtonActionWrapper {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(WButtonActionWrapper(
            deserializer.deserialize_any(WButtonActionVisitor)?,
        ))
    }
}

#[derive(Debug, Clone)]
pub enum WButtonActionList {
    Single(WButtonAction),
    Multiple(Vec<WButtonAction>),
}

struct WButtonActionListVisitor;

impl<'de> Visitor<'de> for WButtonActionListVisitor {
    type Value = WButtonActionList;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("one or multiple actions")
    }

    fn visit_str<E>(self, value: &str) -> Result<WButtonActionList, E>
    where
        E: serde::de::Error,
    {
        Ok(WButtonActionList::Single(
            WButtonActionWrapper::deserialize(value.into_deserializer())?.0,
        ))
    }

    fn visit_seq<M>(self, mut seq: M) -> Result<Self::Value, M::Error>
    where
        M: SeqAccess<'de>,
    {
        let mut actions = Vec::new();

        while let Some(WButtonActionWrapper(value)) = seq.next_element::<WButtonActionWrapper>()? {
            actions.push(value);
        }

        Ok(WButtonActionList::Multiple(actions))
    }

    fn visit_map<M>(self, map: M) -> Result<WButtonActionList, M::Error>
    where
        M: serde::de::MapAccess<'de>,
    {
        Ok(WButtonActionList::Single(WButtonAction::deserialize(
            serde::de::value::MapAccessDeserializer::new(map),
        )?))
    }
}

impl<'de> Deserialize<'de> for WButtonActionList {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_any(WButtonActionListVisitor)
    }
}

impl WButtonActionList {
    pub fn enumerate(&self) -> impl Iterator<Item = &WButtonAction> {
        match self {
            WButtonActionList::Single(action) => std::slice::from_ref(action).iter(),
            WButtonActionList::Multiple(actions) => actions.iter(),
        }
    }
}

#[derive(Debug, Default)]
pub enum WButtonJustify {
    #[default]
    Center,
    Fill,
    Left,
    Right,
}

impl<'de> Deserialize<'de> for WButtonJustify {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(FromStr::from_str(<&str>::deserialize(deserializer)?).expect("never fails"))
    }
}

impl FromStr for WButtonJustify {
    type Err = Infallible;

    fn from_str(val: &str) -> Result<Self, Self::Err> {
        Ok(match val {
            "center" => WButtonJustify::Center,
            "fill" => WButtonJustify::Fill,
            "left" => WButtonJustify::Left,
            "right" => WButtonJustify::Right,
            _ => WButtonJustify::Center,
        })
    }
}

#[derive(Debug, Deserialize)]
pub struct WButton {
    pub label: String,
    pub action: WButtonActionList,
    pub text: String,
    pub keybind: String,
    #[serde(default)]
    pub justify: WButtonJustify,
    pub width: Option<f32>,
    pub height: Option<f32>,
    #[serde(default = "default_circular")]
    pub circular: bool,
    #[serde(default)]
    pub icon: Option<String>,
}

fn default_circular() -> bool {
    false
}


================================================
FILE: src/cli_opt.rs
================================================
use crate::units::{AspectRatio, LengthValue, Margin};
use clap::{ArgAction, Parser, ValueEnum};
use serde::{Deserialize, Deserializer};
use std::{
    error::Error,
    fmt::{Debug, Display},
    num::NonZeroU32,
    path::PathBuf,
    str::FromStr,
};

#[derive(Debug, Copy, Clone, Default, ValueEnum, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Protocol {
    #[default]
    LayerShell,
    None,
    Xdg,
}

#[derive(Debug, Copy, Clone, Default, ValueEnum, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MenuLayoutStrategy {
    #[default]
    Grid,
}

#[derive(Parser, Debug)]
#[command(author, version, disable_version_flag = true, about, long_about = None)]
pub struct Args {
    #[arg(short = 'v', long, action = ArgAction::Version)]
    pub version: Option<bool>,

    /// Run the application in service mode - it will stay in the background until triggered
    #[arg(short = 's', long, num_args = 0..=1, require_equals = true, default_missing_value = "true"
    )]
    pub service: Option<bool>,

    /// Specify a layout file, specifying - will read the layout config from stdin
    #[arg(short = 'l', long)]
    pub layout: Option<PathBuf>,

    /// Specify the way the buttons should be laid out in the available space
    #[arg(long)]
    pub button_layout: Option<MenuLayoutStrategy>,

    /// Specify a custom CSS file
    #[arg(short = 'C', long)]
    pub css: Option<PathBuf>,

    /// Set the number of buttons per row, or use a fraction to specify the number of rows to be
    /// used (e.g. "1/1" for all buttons in a single row, "1/5" to distribute the buttons over 5 rows)
    #[arg(short = 'b', long, value_parser = clap::value_parser!(ButtonLayout))]
    pub buttons_per_row: Option<ButtonLayout>,

    /// Set space between buttons columns
    #[arg(short = 'c', long)]
    pub column_spacing: Option<LengthValue>,

    /// Set space between buttons rows
    #[arg(short = 'r', long)]
    pub row_spacing: Option<LengthValue>,

    /// Set the margin around buttons
    #[arg(short = 'm', long)]
    pub margin: Option<Margin>,

    /// Set margin for the left of buttons
    #[arg(short = 'L', long)]
    pub margin_left: Option<Margin>,

    /// Set margin for the right of buttons
    #[arg(short = 'R', long)]
    pub margin_right: Option<Margin>,

    /// Set margin for the top of buttons
    #[arg(short = 'T', long)]
    pub margin_top: Option<Margin>,

    /// Set the margin for the bottom of buttons
    #[arg(short = 'B', long)]
    pub margin_bottom: Option<Margin>,

    /// Set the aspect ratio of the buttons.
    #[arg(short = 'A', long)]
    pub button_aspect_ratio: Option<AspectRatio>,

    /// The delay (in milliseconds) between the window closing and executing the selected option
    #[arg(short = 'd', long)]
    pub delay_command_ms: Option<u32>,

    /// Close the menu on lost focus
    #[arg(short = 'f', long, num_args = 0..=1, require_equals = true, default_missing_value = "true"
    )]
    pub close_on_lost_focus: Option<bool>,

    /// Show the associated key binds
    #[arg(short = 'k', long, num_args = 0..=1, require_equals = true, default_missing_value = "true"
    )]
    pub show_keybinds: Option<bool>,

    /// Use layer-shell or xdg protocol
    #[arg(short = 'p', long, value_enum)]
    pub protocol: Option<Protocol>,

    /// Hide version information
    #[arg(short = 'x', long, num_args = 0..=1, require_equals = true, default_missing_value = "true"
    )]
    pub no_version_info: Option<bool>,
}

#[derive(Clone, Copy, Debug, Default)]
pub enum ButtonLayout {
    #[default]
    Auto,
    PerRow(u32),
    RowRatio(u32, u32),
}

impl<'de> Deserialize<'de> for ButtonLayout {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        FromStr::from_str(&s).map_err(serde::de::Error::custom)
    }
}

impl FromStr for ButtonLayout {
    type Err = Box<dyn Error + Send + Sync + 'static>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s == "auto" {
            return Ok(ButtonLayout::Auto);
        }

        if let Ok(per_row) = s.parse::<NonZeroU32>() {
            return Ok(ButtonLayout::PerRow(per_row.into()));
        }

        if let Some((n, d)) = s.split_once("/")
            && let (Ok(n), Ok(d)) = (n.parse::<NonZeroU32>(), d.parse::<NonZeroU32>())
        {
            return Ok(ButtonLayout::RowRatio(n.into(), d.into()));
        }

        Err("Value neither a number (1, 2, 3) nor a ratio (1/1, 2/3, ...)".into())
    }
}

impl Display for ButtonLayout {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Auto => f.write_str("auto"),
            Self::PerRow(r) => write!(f, "{r}"),
            Self::RowRatio(n, d) => write!(f, "{n}/{d}"),
        }
    }
}


================================================
FILE: src/config.rs
================================================
use crate::button::WButton;
use crate::error::WError;
use convert_case::ccase;
use gdk4::gio;
use gtk4::CssProvider;
use miette::{NamedSource, Report, SourceOffset};
use serde::Deserialize;
use std::borrow::Cow;
use std::io::Read;
use std::path::{Path, PathBuf};
use tracing::{Level, debug, enabled, error, info, warn};
use wleave::cli_opt::{Args, ButtonLayout, MenuLayoutStrategy, Protocol};
use wleave::units::{AspectRatio, LengthValue, Margin};

#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppConfig {
    #[serde(default)]
    pub service: bool,
    #[serde(default)]
    pub button_layout: MenuLayoutStrategy,
    pub margin_left: Option<Margin>,
    pub margin_right: Option<Margin>,
    pub margin_top: Option<Margin>,
    pub margin_bottom: Option<Margin>,
    #[serde(default = "default_margin")]
    pub margin: Margin,
    #[serde(default = "default_spacing")]
    pub column_spacing: LengthValue,
    #[serde(default = "default_spacing")]
    pub row_spacing: LengthValue,
    pub button_aspect_ratio: Option<AspectRatio>,
    #[serde(default = "default_delay")]
    pub delay_command_ms: u32,
    #[serde(default)]
    pub protocol: Protocol,
    #[serde(default)]
    pub buttons_per_row: ButtonLayout,
    #[serde(default)]
    pub close_on_lost_focus: bool,
    pub buttons: Vec<WButton>,
    #[serde(default)]
    pub show_keybinds: bool,
    #[serde(default)]
    pub no_version_info: bool,
    pub css: Option<PathBuf>,
}

impl Default for AppConfig {
    fn default() -> Self {
        AppConfig {
            service: false,
            button_layout: MenuLayoutStrategy::Grid,
            margin_left: None,
            margin_right: None,
            margin_top: None,
            margin_bottom: None,
            margin: default_margin(),
            column_spacing: default_spacing(),
            row_spacing: default_spacing(),
            button_aspect_ratio: None,
            delay_command_ms: default_delay(),
            protocol: Default::default(),
            buttons_per_row: Default::default(),
            close_on_lost_focus: false,
            buttons: vec![],
            show_keybinds: false,
            no_version_info: false,
            css: None,
        }
    }
}

fn default_margin() -> Margin {
    Margin(LengthValue::Percentage(0.2))
}

fn default_spacing() -> LengthValue {
    LengthValue::Px(8.0)
}

fn default_delay() -> u32 {
    100
}

fn file_search_given(given_file: impl AsRef<Path>) -> Result<PathBuf, WError> {
    let file = given_file.as_ref();
    if !file.is_file() {
        return Err(WError::SpecifiedPathNotAFile(file.to_owned()));
    }

    Ok(file.to_owned())
}

pub fn file_search_path(file_name: impl AsRef<Path>) -> Result<PathBuf, WError> {
    let file_name = file_name.as_ref();
    let user_config_dir = dirs::config_dir()
        .or_else(|| dirs::home_dir().map(|p| p.join(".config")))
        .unwrap_or_else(|| Path::new("~/.config").to_owned());

    for path in &[
        &user_config_dir.join("wleave"),
        &user_config_dir.join("wlogout"),
        Path::new("/etc/wleave"),
        Path::new("/etc/wlogout"),
        Path::new("/usr/local/etc/wleave"),
        Path::new("/usr/local/etc/wlogout"),
    ] {
        let full_path = path.join(file_name);
        if full_path.is_file() {
            debug!("File found in: {}", full_path.display());
            return Ok(full_path);
        } else {
            debug!("No file found in: {}", full_path.display());
        }
    }

    Err(WError::FileNotInSearchPath(file_name.to_owned()))
}

fn parse_config(input: impl Read, source_path: Cow<Path>) -> Result<AppConfig, WError> {
    let path = source_path.into_owned();
    let path_name = path.display().to_string();
    info!("Reading options from: {}", path_name);
    let config = std::io::read_to_string(input).map_err(|e| WError::IoError(path, e))?;

    let new = serde_json::de::from_str::<AppConfig>(&config).map_err(|e| {
        WError::FileParseFailed(
            NamedSource::new(path_name.clone(), config.to_owned()),
            SourceOffset::from_location(&config, e.line(), e.column()),
            e,
        )
    });

    let legacy = serde_json::Deserializer::from_str(&config)
        .into_iter::<WButton>()
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| {
            WError::FileParseFailed(
                NamedSource::new(path_name, config.to_owned()),
                SourceOffset::from_location(&config, e.line(), e.column()),
                e,
            )
        })
        .map(|buttons| AppConfig {
            buttons,
            ..Default::default()
        });

    match (new, legacy) {
        (Ok(conf), _) => {
            info!("Using the JSON layout format.");
            Ok(conf)
        }
        (Err(e), Ok(legacy)) => {
            debug!("The JSON format could not be parsed: {:?}", Report::from(e));
            info!("Using the backwards-compatible layout format.");
            if !enabled!(Level::DEBUG) {
                warn!(
                    "If this is not intended, run the application with RUST_LOG=debug to show the JSON parse error."
                );
            }

            Ok(legacy)
        }
        (Err(e), Err(_)) => {
            error!("{:?}", e);

            Err(e)
        }
    }
}

pub fn load_config(file: Option<&impl AsRef<Path>>) -> Result<AppConfig, WError> {
    if let Some("-") = file.map(AsRef::as_ref).and_then(Path::to_str) {
        return parse_config(std::io::stdin(), Path::new("<stdin>").into());
    }

    let file_path = file.map(file_search_given).unwrap_or_else(|| {
        file_search_path("layout.json").or_else(|_| file_search_path("layout"))
    })?;

    let input =
        std::fs::File::open(&file_path).map_err(|e| WError::IoError(file_path.clone(), e))?;
    parse_config(input, file_path.into())
}

pub fn load_css(file: Option<impl AsRef<Path>>) -> Result<CssProvider, WError> {
    let path = file
        .map(file_search_given)
        .unwrap_or_else(|| file_search_path("style.css"))?;

    let provider = CssProvider::new();
    provider.connect_parsing_error(|_provider, _section, error| {
        warn!("CSS Parse error: {:?}", error);
    });
    provider.load_from_file(&gio::File::for_path(&path));

    Ok(provider)
}

macro_rules! merge_option {
    ($conf:ident, $args:ident, $name:ident => $val:expr) => {
        if let Some($name) = $args.$name {
            info!(
                "\"{}\" specified from args: {:?}",
                ccase!(snake -> kebab, stringify!($name)),
                $name
            );
            $conf.$name = $val;
        } else {
            info!(
                "\"{}\" specified from config: {:?}",
                ccase!(snake -> kebab, stringify!($name)),
                $conf.$name
            );
        }
    };
    ($conf:ident, $args:ident, $name:ident) => {
        merge_option!($conf, $args, $name => $name)
    };
}

pub fn merge_with_args(config: &mut AppConfig, args: Args) {
    merge_option!(config, args, service);
    merge_option!(config, args, button_layout);
    merge_option!(config, args, margin_top => Some(margin_top));
    merge_option!(config, args, margin_bottom => Some(margin_bottom));
    merge_option!(config, args, margin_left => Some(margin_left));
    merge_option!(config, args, margin_right => Some(margin_right));
    merge_option!(config, args, margin);
    merge_option!(config, args, protocol);
    merge_option!(config, args, column_spacing);
    merge_option!(config, args, row_spacing);
    merge_option!(config, args, button_aspect_ratio => Some(button_aspect_ratio));
    merge_option!(config, args, show_keybinds);
    merge_option!(config, args, close_on_lost_focus);
    merge_option!(config, args, buttons_per_row);
    merge_option!(config, args, no_version_info);
    merge_option!(config, args, delay_command_ms);

    if let Some(css) = args.css.clone() {
        info!(
            "\"css\" file specified from args: {:?}",
            Path::display(&css)
        );
        config.css = Some(css);
    } else {
        info!(
            "\"css\" file specified from config: {:?}",
            config.css.as_deref().map(Path::display)
        );
    }
}


================================================
FILE: src/error.rs
================================================
use miette::{Diagnostic, NamedSource, SourceOffset};
use std::path::PathBuf;
use thiserror::Error;

#[derive(Error, Diagnostic, Debug)]
pub enum WError {
    #[error("Failed to load the specified configuration file {0} as it does not exist")]
    SpecifiedPathNotAFile(PathBuf),
    #[error("Failed to find the configuration file {0} in the search path")]
    FileNotInSearchPath(PathBuf),
    #[error("An error has occurred while reading file {0}: {1}")]
    IoError(PathBuf, std::io::Error),
    #[error("JSON parsing failed")]
    #[diagnostic(code(wleave::parse_failed))]
    FileParseFailed(
        #[source_code] NamedSource<String>,
        #[label("The parser failed here")] SourceOffset,
        #[source] serde_json::Error,
    ),
}


================================================
FILE: src/exec.rs
================================================
use crate::button::{WButtonAction, WButtonActionHandler};
use std::process::Command;
use tracing::error;

pub fn run_command(command: WButtonAction) {
    match command.handler {
        WButtonActionHandler::Executable(exe) => {
            if let Err(e) = Command::new(exe).spawn() {
                error!("Execution error: {e}");
            }
        }
        WButtonActionHandler::Shell(script) => {
            if let Err(e) = Command::new("sh").args(["-c", &script]).spawn() {
                error!("Execution error: {e}");
            }
        }
    }
}


================================================
FILE: src/layout.rs
================================================
use crate::layout::menu_layout::LayoutMenuImpl;
use crate::layout::menu_layout_child::MenuLayoutChildImpl;
use glib::object::Cast;
use glib::subclass::types::ObjectSubclassIsExt;
use gtk4::prelude::{LayoutManagerExt, WidgetExt};
use libadwaita::gtk;
use wleave::cli_opt::MenuLayoutStrategy;

mod menu_layout_child {
    use gdk4::prelude::ObjectExt;
    use glib::subclass::object::{DerivedObjectProperties, ObjectImpl};
    use glib::subclass::types::ObjectSubclass;
    use glib_macros::Properties;
    use gtk4::subclass::layout_child::LayoutChildImpl;
    use std::cell::Cell;

    #[derive(Properties, Default)]
    #[properties(wrapper_type = super::MenuLayoutChild)]
    pub struct MenuLayoutChildImpl {
        #[property(
            name = "placement",
            get,
            set,
            builder(super::MenuLayoutChildPlacement::default())
        )]
        pub placement: Cell<super::MenuLayoutChildPlacement>,
        #[property(name = "grid-x", get, set)]
        pub grid_x: Cell<u32>,
        #[property(name = "grid-y", get, set)]
        pub grid_y: Cell<u32>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for MenuLayoutChildImpl {
        const NAME: &'static str = "MenuLayoutChild";

        type Type = super::MenuLayoutChild;

        type ParentType = gtk4::LayoutChild;

        type Interfaces = ();
    }

    #[glib::derived_properties]
    impl ObjectImpl for MenuLayoutChildImpl {}

    impl LayoutChildImpl for MenuLayoutChildImpl {}
}

glib::wrapper! {
    pub struct MenuLayoutChild(ObjectSubclass<MenuLayoutChildImpl>)
        @extends gtk4::LayoutChild;
}

impl MenuLayoutChild {
    fn new(
        manager: &MenuLayout,
        child: &gtk4::Widget,
        placement: MenuLayoutChildPlacement,
    ) -> MenuLayoutChild {
        glib::Object::builder()
            .property("layout-manager", manager)
            .property("child-widget", child)
            .property("placement", placement)
            .build()
    }
}

#[derive(Default, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, glib::Enum)]
#[enum_type(name = "MenuLayoutChildPlacement")]
pub enum MenuLayoutChildPlacement {
    #[default]
    Unknown,
    Grid,
}

mod menu_layout {
    use crate::layout::{MenuLayoutChild, MenuLayoutChildPlacement, MenuLayoutProvider};
    use gdk4::prelude::ObjectExt;
    use gdk4::subclass::prelude::DerivedObjectProperties;
    use glib::object::Cast;
    use glib::subclass::object::ObjectImpl;
    use glib::subclass::prelude::ObjectSubclassExt;
    use glib::subclass::types::ObjectSubclass;
    use glib::types::StaticType;
    use glib_macros::Properties;
    use gtk4::prelude::WidgetExt;
    use gtk4::subclass::layout_manager::LayoutManagerImpl;
    use std::cell::{Cell, RefCell};
    use tracing::instrument;

    #[derive(Properties, Default)]
    #[properties(wrapper_type = super::MenuLayout)]
    pub struct LayoutMenuImpl {
        #[property(name = "aspect-ratio", get, set)]
        aspect_ratio: Cell<f64>,
        #[property(name = "buttons-per-row", get, set)]
        buttons_per_row: Cell<u32>,
        #[property(name = "aspect-ratio-set", get, set)]
        aspect_ratio_set: Cell<bool>,
        #[property(name = "column-spacing", get, set)]
        column_spacing: Cell<f64>,
        #[property(name = "row-spacing", get, set)]
        row_spacing: Cell<f64>,
        pub(super) layout_strategy: RefCell<MenuLayoutProvider>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for LayoutMenuImpl {
        const NAME: &'static str = "MenuLayout";

        type Type = super::MenuLayout;

        type ParentType = gtk4::LayoutManager;

        type Interfaces = ();
    }

    #[glib::derived_properties]
    impl ObjectImpl for LayoutMenuImpl {}

    impl LayoutManagerImpl for LayoutMenuImpl {
        #[instrument(skip(self, widget))]
        fn allocate(&self, widget: &gtk4::Widget, width: i32, height: i32, baseline: i32) {
            {
                let mut layout = self.layout_strategy.borrow_mut();

                layout.column_spacing = self.column_spacing.get();
                layout.row_spacing = self.row_spacing.get();
                layout.aspect_ratio = self.aspect_ratio_set.get().then(|| self.aspect_ratio.get());
            }

            let layout = self.layout_strategy.borrow();

            let mut curr = widget.first_child();
            let children = std::iter::from_fn(|| {
                let it = curr.take()?;
                curr = it.next_sibling();
                Some(it)
            })
            .collect::<Vec<_>>();

            layout.allocate(&self.obj(), &children, width, height, baseline);
        }

        fn create_layout_child(
            &self,
            _widget: &gtk4::Widget,
            for_child: &gtk4::Widget,
        ) -> gtk4::LayoutChild {
            MenuLayoutChild::new(
                &self.obj(),
                for_child,
                match self.layout_strategy.borrow().strategy {
                    super::MenuLayoutStrategy::Grid => MenuLayoutChildPlacement::Grid,
                },
            )
            .upcast()
        }

        fn layout_child_type() -> Option<glib::Type> {
            Some(MenuLayoutChild::static_type())
        }

        fn request_mode(&self, _widget: &gtk4::Widget) -> gtk4::SizeRequestMode {
            gtk4::SizeRequestMode::HeightForWidth
        }

        #[instrument(skip(self, widget))]
        fn measure(
            &self,
            widget: &gtk4::Widget,
            orientation: gtk4::Orientation,
            for_size: i32,
        ) -> (i32, i32, i32, i32) {
            let mut curr = widget.first_child();
            let children = std::iter::from_fn(|| {
                let it = curr.take()?;
                curr = it.next_sibling();
                Some(it)
            })
            .collect::<Vec<_>>();

            let layout = self.layout_strategy.borrow();

            layout.measure(&self.obj(), &children, orientation, for_size)
        }
    }
}

glib::wrapper! {
    pub struct MenuLayout(ObjectSubclass<LayoutMenuImpl>)
        @extends gtk4::LayoutManager;
}

impl MenuLayout {
    pub fn new(
        button_layout: MenuLayoutStrategy,
        ratio: Option<impl Into<f64>>,
        column_spacing: gtk4::Expression,
        row_spacing: gtk4::Expression,
        buttons_per_row: Option<impl Into<u32>>,
    ) -> Self {
        let obj: MenuLayout = glib::Object::builder()
            .property("aspect-ratio-set", ratio.is_some())
            .property("aspect-ratio", ratio.map(Into::into).unwrap_or(1.0))
            .property(
                "buttons-per-row",
                buttons_per_row.map(Into::into).unwrap_or_default(),
            )
            .build();

        column_spacing.bind(&obj, "column-spacing", glib::Object::NONE);
        row_spacing.bind(&obj, "row-spacing", glib::Object::NONE);

        let imp = obj.imp();
        imp.layout_strategy.borrow_mut().strategy = button_layout;

        obj
    }
}

#[derive(Default)]
struct MenuLayoutProvider {
    strategy: MenuLayoutStrategy,
    column_spacing: f64,
    row_spacing: f64,
    aspect_ratio: Option<f64>,
}

impl MenuLayoutProvider {
    fn allocate(
        &self,
        obj: &MenuLayout,
        children: &[gtk4::Widget],
        width: i32,
        height: i32,
        baseline: i32,
    ) {
        if children.is_empty() {
            return;
        }

        match self.strategy {
            MenuLayoutStrategy::Grid => {
                let n = children.len();
                let col_spacing = (self.column_spacing as i32).max(0) as usize;
                let row_spacing = (self.row_spacing as i32).max(0) as usize;

                let mut rows = 1;
                let mut cols = 1;
                let mut b_width = 0.0;
                let mut b_height = 0.0;

                let u_width = width as usize;
                let u_height = height as usize;

                let per_row = obj.buttons_per_row() as usize;
                // We use 0 for "auto" placement
                let cols_range = if per_row != 0 {
                    // Try layouts where all buttons either fit into one row or exactly "per_row"
                    per_row.min(n)..=per_row
                } else {
                    // Try all possible layouts
                    1..=n
                };

                // Axis-aligned rectangle packing
                // We brute-force the best layout, optimizing for max button area
                for i_rows in 1..=n {
                    for j_cols in cols_range.clone() {
                        if (i_rows * j_cols > n + i_rows || i_rows * j_cols > n + j_cols)
                            && per_row == 0
                            || i_rows * j_cols < n
                        {
                            continue;
                        }

                        let col_gaps = j_cols - 1;
                        let row_gaps = i_rows - 1;

                        let (w, h) = match self.aspect_ratio {
                            Some(aspect @ 1.0..) => {
                                let mut w = u_width.saturating_sub(col_gaps * col_spacing) as f64
                                    / j_cols as f64
                                    * aspect;
                                let h = (u_height.saturating_sub(row_gaps * row_spacing) as f64
                                    / i_rows as f64)
                                    .min(w / aspect);

                                w = h * aspect;

                                (w, h)
                            }
                            Some(aspect @ ..1.0) => {
                                let mut h = u_height.saturating_sub(row_gaps * row_spacing) as f64
                                    / i_rows as f64;
                                let w = (u_width.saturating_sub(col_gaps * col_spacing) as f64
                                    / j_cols as f64
                                    * aspect)
                                    .min(h * aspect);

                                h = w / aspect;

                                (w, h)
                            }
                            //
                            Some(..) | None => {
                                let w = u_width.saturating_sub(col_gaps * col_spacing) as f64
                                    / j_cols as f64;
                                let h = u_height.saturating_sub(row_gaps * row_spacing) as f64
                                    / i_rows as f64;

                                (w, h)
                            }
                        };

                        if w * h > b_width * b_height {
                            rows = i_rows;
                            cols = j_cols;
                            b_width = w;
                            b_height = h;
                        }
                    }
                }

                let base_x =
                    (width as f64 - (cols - 1) as f64 * (col_spacing as f64 + b_width) - b_width)
                        / 2.0;
                let base_y = (height as f64
                    - (rows - 1) as f64 * (row_spacing as f64 + b_height)
                    - b_height)
                    / 2.0;

                for (i, child) in children.iter().enumerate() {
                    let child_layout = obj
                        .layout_child(child)
                        .downcast::<MenuLayoutChild>()
                        .expect("always MenuLayoutChild");

                    if child.should_layout() {
                        let ix = i % cols;
                        let iy = i / cols;

                        child_layout.set_placement(MenuLayoutChildPlacement::Grid);
                        child_layout.set_grid_x(ix as u32);
                        child_layout.set_grid_y(iy as u32);

                        let x_grid = ix as f64;
                        let y_grid = iy as f64;

                        let x = base_x
                            + x_grid * b_width
                            + x_grid * self.column_spacing * self.aspect_ratio.unwrap_or(1.0);
                        let y = base_y + y_grid * b_height + y_grid * self.row_spacing;

                        child.size_allocate(
                            &gtk4::Allocation::new(
                                x as i32,
                                y as i32,
                                b_width as i32,
                                b_height as i32,
                            ),
                            baseline,
                        );
                    }
                }
            }
        }
    }

    fn measure(
        &self,
        _obj: &MenuLayout,
        children: &[gtk4::Widget],
        orientation: gtk4::Orientation,
        for_size: i32,
    ) -> (i32, i32, i32, i32) {
        let gaps = match orientation {
            gtk::Orientation::Vertical => self.row_spacing as i32,
            gtk::Orientation::Horizontal => self.column_spacing as i32,
            _ => 0,
        };

        let (mut min, mut nat) = (0, 0);

        match self.strategy {
            MenuLayoutStrategy::Grid => {
                for child in children.iter() {
                    if !child.should_layout() {
                        continue;
                    }

                    let (c_min, c_nat, _, _) = child.measure(orientation, for_size);

                    min = min.max(c_min + gaps);
                    nat = nat.max(c_nat + gaps);
                }

                // A bit of a messy heuristic
                if matches!(orientation, gtk::Orientation::Horizontal) {
                    min *= children.len() as i32;
                    nat *= children.len() as i32;
                } else {
                    min *= children.len() as i32;
                    min /= 2;
                    nat *= children.len() as i32;
                    nat /= 2;
                }
            }
        }

        (min, nat, -1, -1)
    }
}


================================================
FILE: src/lib.rs
================================================
pub mod cli_opt;
pub mod units;


================================================
FILE: src/main.rs
================================================
mod app;
mod button;
mod config;
mod error;
mod exec;
mod layout;
mod paintable;

use clap::Parser;
use glib::clone;
use std::sync::Arc;
use tracing::{Level, error};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

use crate::app::create_app;
use crate::config::{AppConfig, load_config, load_css, merge_with_args};
use gtk4::gdk::Display;
use gtk4::prelude::*;
use wleave::cli_opt::Args;

fn on_startup(config: &AppConfig) {
    let display = Display::default().expect("Could not connect to a display");

    match load_css(config.css.as_ref()) {
        Ok(css) => gtk4::style_context_add_provider_for_display(
            &display,
            &css,
            gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
        ),
        Err(e) => error!("Failed to load CSS: {e}"),
    };
}

fn entry_point(config: Arc<AppConfig>) -> miette::Result<()> {
    let mut flags = gtk4::gio::ApplicationFlags::empty();
    flags.set(gtk4::gio::ApplicationFlags::IS_SERVICE, config.service);

    let app = libadwaita::Application::builder()
        .application_id("sh.natty.Wleave")
        .flags(flags)
        .build();

    app.connect_startup(clone!(
        #[strong]
        config,
        move |_| on_startup(config.as_ref())
    ));

    let hold_guard = if config.service {
        Some(app.hold())
    } else {
        None
    };

    app.connect_activate(move |app| {
        let _ = &hold_guard;

        let app_window = create_app(&config, app);
        app_window.present();
    });

    app.run_with_args(&[] as &[&str]);

    Ok(())
}

fn main() -> miette::Result<()> {
    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer().without_time())
        .with(
            EnvFilter::builder()
                .with_default_directive(Level::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let args = Args::parse();

    let mut config = load_config(args.layout.as_ref())?;
    merge_with_args(&mut config, args);

    let config = Arc::new(config);
    entry_point(config)?;

    Ok(())
}


================================================
FILE: src/paintable.rs
================================================
use gdk4::prelude::*;
use gdk4::subclass::paintable::PaintableImpl;
use glib::subclass::ObjectImplRef;
use glib::subclass::prelude::*;
use glib::translate::IntoGlib;
use glib_macros::Properties;
use gtk4::prelude::*;
use gtk4::subclass::prelude::SymbolicPaintableImpl;
use miette::miette;
use std::cell::{Cell, RefCell};
use tracing::error;

#[derive(Properties, Default)]
#[properties(wrapper_type = PicturePaintable)]
pub struct PicturePaintableImpl {
    #[property(name = "image-path", get, set)]
    image_path: RefCell<String>,
    #[property(name = "scale-factor", get, set)]
    scale_factor: Cell<i32>,
    widget: RefCell<Option<glib::WeakRef<gtk4::Widget>>>,
    handle: RefCell<Option<rsvg::SvgHandle>>,
    texture: RefCell<Option<gdk4::MemoryTexture>>,
    _symbolic_updated: Cell<bool>,
}

#[glib::object_subclass]
impl ObjectSubclass for PicturePaintableImpl {
    const NAME: &'static str = "WleavePicturePaintable";

    type Type = PicturePaintable;

    type ParentType = glib::Object;

    type Interfaces = (gdk4::Paintable, gtk4::SymbolicPaintable);
}

glib::wrapper! {
    pub struct PicturePaintable(ObjectSubclass<PicturePaintableImpl>)
        @implements gdk4::Paintable, gtk4::SymbolicPaintable;
}

#[glib::derived_properties]
impl ObjectImpl for PicturePaintableImpl {
    fn constructed(&self) {
        self.parent_constructed();

        let obj = self.obj();
        let impl_ref = ObjectImplRef::new(self).downgrade();
        obj.connect_notify_local(Some("image-path"), move |pict, _| {
            let Some(obj) = impl_ref.upgrade() else {
                return;
            };

            obj.texture.take();

            *obj.handle.borrow_mut() = match rsvg::Loader::new()
                .read_path(pict.image_path())
                .map_err(|e| miette!("Failed to read SVG: {}", e))
            {
                Ok(handle) => Some(handle),
                Err(e) => {
                    error!("{}", e);
                    None
                }
            };
        });
    }
}

impl PicturePaintableImpl {
    fn draw(&self, width: f64, height: f64) {
        let scale: f64 = self.scale_factor.get().into();
        let width = width * scale;
        let height = height * scale;

        let mut tex_borrow = self.texture.borrow_mut();
        if tex_borrow.is_some() {
            return;
        };

        let Some(handle_ref) = &*self.handle.borrow() else {
            return;
        };

        let renderer = rsvg::CairoRenderer::new(handle_ref);

        let mut surface =
            match cairo::ImageSurface::create(cairo::Format::ARgb32, width as i32, height as i32)
                .map_err(|e| miette!("Failed to create a Cairo surface: {}", e))
            {
                Ok(surf) => surf,
                Err(e) => {
                    error!("{}", e);
                    return;
                }
            };

        let ctx = match cairo::Context::new(&surface)
            .map_err(|e| miette!("Failed to create a Cairo context: {}", e))
        {
            Ok(ctx) => ctx,
            Err(e) => {
                error!("{}", e);
                return;
            }
        };

        if let Err(e) = renderer
            .render_document(&ctx, &cairo::Rectangle::new(0.0, 0.0, width, height))
            .map_err(|e| miette!("Failed to render SVG: {}", e))
        {
            error!("{}", e);
            return;
        }

        drop(ctx);

        let data = match surface
            .data()
            .map_err(|e| miette!("Failed to take Cairo image data: {}", e))
        {
            Ok(data) => data,
            Err(e) => {
                error!("{}", e);
                return;
            }
        };

        let bytes = glib::Bytes::from(data.as_ref());

        cfg_if::cfg_if! {
            if #[cfg(target_endian = "little")] {
                let format = gdk4::MemoryFormat::B8g8r8a8;
            } else {
                let format = gdk4::MemoryFormat::A8r8g8b8;
            }
        }

        *tex_borrow = Some(gdk4::MemoryTexture::new(
            width as i32,
            height as i32,
            format,
            &bytes,
            size_of::<i32>() * width as usize,
        ));
    }
}

impl PaintableImpl for PicturePaintableImpl {
    fn current_image(&self) -> gdk4::Paintable {
        self.draw(
            self.intrinsic_width() as f64,
            self.intrinsic_height() as f64,
        );

        let Some(tex_ref) = &*self.texture.borrow() else {
            return gdk4::Paintable::new_empty(self.intrinsic_width(), self.intrinsic_height());
        };

        gdk4::Paintable::from(tex_ref.clone())
    }

    fn flags(&self) -> gdk4::PaintableFlags {
        gdk4::PaintableFlags::empty()
    }

    fn intrinsic_width(&self) -> i32 {
        let Some(handle_ref) = &*self.handle.borrow() else {
            return 256;
        };

        let renderer = rsvg::CairoRenderer::new(handle_ref);
        let size = renderer
            .intrinsic_size_in_pixels()
            .map(|(w, _)| w.ceil() as i32);

        size.unwrap_or(256)
    }

    fn intrinsic_height(&self) -> i32 {
        let Some(handle_ref) = &*self.handle.borrow() else {
            return 256;
        };

        let renderer = rsvg::CairoRenderer::new(handle_ref);
        let size = renderer
            .intrinsic_size_in_pixels()
            .map(|(_, h)| h.ceil() as i32);

        size.unwrap_or(256)
    }

    fn intrinsic_aspect_ratio(&self) -> f64 {
        let Some(handle_ref) = &*self.handle.borrow() else {
            return 1.0;
        };

        let renderer = rsvg::CairoRenderer::new(handle_ref);
        let size = renderer.intrinsic_size_in_pixels().map(|(w, h)| w / h);

        size.unwrap_or(1.0)
    }

    fn snapshot(&self, snapshot: &gdk4::Snapshot, width: f64, height: f64) {
        if !self._symbolic_updated.get() {
            if let Some(w) = &*self.widget.borrow()
                && let Some(widget) = w.upgrade()
            {
                self.snapshot_symbolic(snapshot, width, height, &[widget.color()]);
            } else {
                self.snapshot_symbolic(snapshot, width, height, &[]);
            }
        }

        self.draw(width, height);

        let Some(tex_ref) = &*self.texture.borrow() else {
            return;
        };

        SnapshotExt::append_texture(
            snapshot,
            tex_ref,
            &gtk4::graphene::Rect::new(0.0, 0.0, width as f32, height as f32),
        );

        self._symbolic_updated.set(false);
    }
}

impl SymbolicPaintableImpl for PicturePaintableImpl {
    fn snapshot_symbolic(
        &self,
        snapshot: &gdk4::Snapshot,
        width: f64,
        height: f64,
        colors: &[gdk4::RGBA],
    ) {
        let mut handle_borrow = self.handle.borrow_mut();
        let Some(handle_ref) = &mut *handle_borrow else {
            return;
        };

        let col_idx = gtk4::SymbolicColor::Foreground.into_glib();

        let col = colors.get(col_idx as usize).unwrap_or(&gdk4::RGBA::BLACK);

        if let Err(e) = handle_ref
            .set_stylesheet(&format!(
                r#"
                    svg {{
                        color: {col} !important;
                    }}
                "#
            ))
            .map_err(|e| miette!("Failed to set stylesheet for SVG while loading: {}", e))
        {
            error!("{}", e);
            return;
        }

        drop(handle_borrow);

        self.texture.take();
        self._symbolic_updated.set(true);

        self.snapshot(snapshot, width, height);
    }
}

impl PicturePaintable {
    fn for_path(icon_path: impl Into<String>) -> Self {
        glib::Object::builder()
            .property("image-path", icon_path.into())
            .property("scale-factor", 1)
            .build()
    }
}

pub fn svg_picture_colorized(icon: &str) -> gtk4::Picture {
    let paintable = PicturePaintable::for_path(icon);

    let picture = gtk4::Picture::for_paintable(&paintable);
    paintable.set_scale_factor(picture.scale_factor());
    picture.connect_scale_factor_notify(move |pict| {
        let Some(p) = pict.paintable() else {
            return;
        };

        let Some(paintable) = p.downcast_ref::<PicturePaintable>() else {
            return;
        };

        paintable.set_scale_factor(pict.scale_factor());
    });

    let internals = paintable.imp();
    *internals.widget.borrow_mut() = Some(ObjectExt::downgrade(picture.as_ref()));

    picture
}


================================================
FILE: src/units.rs
================================================
use miette::miette;
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::error::Error;
use std::fmt::Display;
use std::num::NonZeroU32;
use std::str::FromStr;

#[derive(Clone, Copy, Debug)]
pub enum AspectRatio {
    Float(f32),
    Ratio(u32, u32),
}

impl Default for AspectRatio {
    fn default() -> Self {
        AspectRatio::Float(1.0)
    }
}

impl<'de> Deserialize<'de> for AspectRatio {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let v = Value::deserialize(deserializer)?;
        if let Some(f) = v.as_f64() {
            Ok(AspectRatio::Float(f as f32))
        } else if let Some(s) = v.as_str() {
            FromStr::from_str(s).map_err(serde::de::Error::custom)
        } else {
            Err(serde::de::Error::custom(
                "Aspect ratio neither a positive float nor a ratio (1/1, 2/3, ...)",
            ))
        }
    }
}

impl FromStr for AspectRatio {
    type Err = Box<dyn Error + Send + Sync + 'static>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Ok(float) = s.parse::<f32>() {
            if float < 0.0 {
                return Err("Aspect ratio cannot be negative".into());
            }
            return Ok(AspectRatio::Float(float));
        }

        if let Some((n, d)) = s.split_once('/')
            && let (Ok(n), Ok(d)) = (n.parse::<NonZeroU32>(), d.parse::<NonZeroU32>())
        {
            return Ok(AspectRatio::Ratio(n.into(), d.into()));
        }

        Err("Aspect ratio neither a float nor a ratio".into())
    }
}

impl Display for AspectRatio {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Float(r) => write!(f, "{r}"),
            Self::Ratio(n, d) => write!(f, "{n}/{d}"),
        }
    }
}

impl AspectRatio {
    pub fn as_float(self) -> f32 {
        match self {
            Self::Float(f) => f,
            Self::Ratio(n, d) => (n as f32) / (d as f32),
        }
    }
}

#[derive(Debug, Clone)]
pub enum LengthValue {
    Px(f32),
    Percentage(f32),
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum LengthSerialized {
    Number(f32),
    String(String),
}

#[derive(Debug, Copy, Clone)]
pub enum LengthDimension {
    Vertical,
    Horizontal,
}

#[derive(Debug, Clone)]
pub struct LengthArgs {
    pub viewport: (f32, f32),
    pub dimension: LengthDimension,
}

impl<'de> Deserialize<'de> for LengthValue {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        match LengthSerialized::deserialize(deserializer)? {
            LengthSerialized::Number(num) => Ok(LengthValue::Px(num)),
            LengthSerialized::String(val) => val.parse().map_err(serde::de::Error::custom),
        }
    }
}

impl LengthValue {
    pub fn for_args(&self, args: &LengthArgs) -> f32 {
        match self {
            LengthValue::Percentage(p) => match args.dimension {
                LengthDimension::Horizontal => *p * args.viewport.0,
                LengthDimension::Vertical => *p * args.viewport.1,
            },
            LengthValue::Px(val) => *val,
        }
    }

    fn parse(val: &'_ str) -> Result<Self, cssparser::BasicParseError<'_>> {
        let mut input = cssparser::ParserInput::new(val);
        let mut parser = cssparser::Parser::new(&mut input);

        let token = parser.next()?;

        let value = match token {
            cssparser::Token::Number { value, .. } => LengthValue::Px(*value),
            cssparser::Token::Percentage { unit_value, .. } => LengthValue::Percentage(*unit_value),
            cssparser::Token::Dimension { value, unit, .. } => match unit.as_ref() {
                "px" => LengthValue::Px(*value),
                _ => {
                    let tok = token.clone();
                    return Err(parser
                        .current_source_location()
                        .new_basic_unexpected_token_error(tok));
                }
            },
            _ => {
                let tok = token.clone();
                return Err(parser
                    .current_source_location()
                    .new_basic_unexpected_token_error(tok));
            }
        };

        parser.expect_exhausted()?;

        Ok(value)
    }
}

impl FromStr for LengthValue {
    type Err = miette::Report;

    fn from_str(val: &str) -> Result<Self, Self::Err> {
        LengthValue::parse(val).map_err(|e| miette!("Length parse error: {:?}", e))
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
pub struct Margin(pub LengthValue);

impl FromStr for Margin {
    type Err = miette::Report;

    fn from_str(val: &str) -> Result<Self, Self::Err> {
        LengthValue::from_str(val)
            .map(Margin)
            .map_err(|e| miette!("Margin parse error: {:?}", e))
    }
}


================================================
FILE: style.css
================================================
/**
 * See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html for more CSS variables.
 * Or use your own colors. Both work, but using your own colors may break the automatic light/dark theme function.
 */


window {
    background-color: rgba(12, 12, 12, 0.8);
}

button {
    color: oklab(from var(--view-fg-color) var(--standalone-color-oklab));
    background-color: var(--view-bg-color);
    border: none;
    padding: 10px;
}

button label.action-name {
    font-size: 24px;
    font-weight: 400;
}

button label.keybind {
    font-size: 20px;
    font-family: monospace;
}

button:hover label.keybind, button:focus label.keybind {
    opacity: 1;
}

button:focus,
button:hover {
    background-color: color-mix(in srgb, var(--accent-bg-color), var(--view-bg-color));
}

button:active {
    color: var(--accent-fg-color);
    background-color: var(--accent-bg-color);
}

button#shutdown {
    --view-fg-color: #ff8d8d;
}

button#hibernate {
    --view-fg-color: #a8c0ff;
}

button#reboot {
    --view-fg-color: #84ffaa;
}

button#lock {
    --view-fg-color: #ffe8b6;
}

button#logout {
    --view-fg-color: #ffcca8;
}

button#suspend {
    --view-fg-color: #caaff9;
}
Download .txt
gitextract_wwkaal9a/

├── .editorconfig
├── .gitignore
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── completions/
│   ├── _wleave
│   ├── wleave.bash
│   └── wleave.fish
├── completions_gen/
│   ├── .gitignore
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── icons/
│   └── icon-attribution.md
├── layout.json
├── man/
│   ├── wleave.1.scd
│   ├── wleave.5.scd
│   └── wleave.json.5.scd
├── sh.natty.Wleave.desktop
├── src/
│   ├── app/
│   │   ├── mod.rs
│   │   └── window.rs
│   ├── button.rs
│   ├── cli_opt.rs
│   ├── config.rs
│   ├── error.rs
│   ├── exec.rs
│   ├── layout.rs
│   ├── lib.rs
│   ├── main.rs
│   ├── paintable.rs
│   └── units.rs
└── style.css
Download .txt
SYMBOL INDEX (121 symbols across 12 files)

FILE: completions_gen/src/main.rs
  function main (line 8) | fn main() -> Result<(), Error> {

FILE: src/app/mod.rs
  function do_exit (line 21) | fn do_exit(window: &WleaveWindow, _service_mode: bool) {
  function on_option (line 25) | fn on_option(
  function handle_key (line 60) | fn handle_key(
  function create_app (line 91) | pub fn create_app(config: &Arc<AppConfig>, app: &libadwaita::Application...

FILE: src/app/window.rs
  type WleaveWindowImpl (line 16) | pub struct WleaveWindowImpl {
  constant NAME (line 25) | const NAME: &'static str = "WleaveWindow";
  type Type (line 27) | type Type = super::WleaveWindow;
  type ParentType (line 29) | type ParentType = libadwaita::ApplicationWindow;
  type Interfaces (line 31) | type Interfaces = ();
  method constructed (line 36) | fn constructed(&self) {
  method size_allocate (line 46) | fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
  method new (line 68) | pub fn new(app: &libadwaita::Application) -> Self {

FILE: src/button.rs
  type WButtonActionHandler (line 9) | pub enum WButtonActionHandler {
  type WButtonAction (line 17) | pub struct WButtonAction {
    method is_applicable (line 25) | pub fn is_applicable(&self) -> bool {
  type Err (line 50) | type Err = Infallible;
  method from_str (line 52) | fn from_str(action: &str) -> Result<Self, Self::Err> {
  type WButtonActionVisitor (line 60) | struct WButtonActionVisitor;
    type Value (line 63) | type Value = WButtonAction;
    method expecting (line 65) | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::...
    method visit_str (line 69) | fn visit_str<E>(self, value: &str) -> Result<WButtonAction, E>
    method visit_string (line 76) | fn visit_string<E>(self, val: String) -> Result<Self::Value, E>
    method visit_map (line 83) | fn visit_map<M>(self, map: M) -> Result<WButtonAction, M::Error>
  type WButtonActionWrapper (line 91) | struct WButtonActionWrapper(WButtonAction);
    method deserialize (line 94) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  type WButtonActionList (line 105) | pub enum WButtonActionList {
    method deserialize (line 152) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    method enumerate (line 161) | pub fn enumerate(&self) -> impl Iterator<Item = &WButtonAction> {
  type WButtonActionListVisitor (line 110) | struct WButtonActionListVisitor;
    type Value (line 113) | type Value = WButtonActionList;
    method expecting (line 115) | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::...
    method visit_str (line 119) | fn visit_str<E>(self, value: &str) -> Result<WButtonActionList, E>
    method visit_seq (line 128) | fn visit_seq<M>(self, mut seq: M) -> Result<Self::Value, M::Error>
    method visit_map (line 141) | fn visit_map<M>(self, map: M) -> Result<WButtonActionList, M::Error>
  type WButtonJustify (line 170) | pub enum WButtonJustify {
    method deserialize (line 179) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  type Err (line 188) | type Err = Infallible;
  method from_str (line 190) | fn from_str(val: &str) -> Result<Self, Self::Err> {
  type WButton (line 202) | pub struct WButton {
  function default_circular (line 217) | fn default_circular() -> bool {

FILE: src/cli_opt.rs
  type Protocol (line 14) | pub enum Protocol {
  type MenuLayoutStrategy (line 23) | pub enum MenuLayoutStrategy {
  type Args (line 30) | pub struct Args {
  type ButtonLayout (line 113) | pub enum ButtonLayout {
    method deserialize (line 121) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  type Err (line 131) | type Err = Box<dyn Error + Send + Sync + 'static>;
  method from_str (line 133) | fn from_str(s: &str) -> Result<Self, Self::Err> {
  method fmt (line 153) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

FILE: src/config.rs
  type AppConfig (line 17) | pub struct AppConfig {
  method default (line 50) | fn default() -> Self {
  function default_margin (line 74) | fn default_margin() -> Margin {
  function default_spacing (line 78) | fn default_spacing() -> LengthValue {
  function default_delay (line 82) | fn default_delay() -> u32 {
  function file_search_given (line 86) | fn file_search_given(given_file: impl AsRef<Path>) -> Result<PathBuf, WE...
  function file_search_path (line 95) | pub fn file_search_path(file_name: impl AsRef<Path>) -> Result<PathBuf, ...
  function parse_config (line 121) | fn parse_config(input: impl Read, source_path: Cow<Path>) -> Result<AppC...
  function load_config (line 174) | pub fn load_config(file: Option<&impl AsRef<Path>>) -> Result<AppConfig,...
  function load_css (line 188) | pub fn load_css(file: Option<impl AsRef<Path>>) -> Result<CssProvider, W...
  function merge_with_args (line 224) | pub fn merge_with_args(config: &mut AppConfig, args: Args) {

FILE: src/error.rs
  type WError (line 6) | pub enum WError {

FILE: src/exec.rs
  function run_command (line 5) | pub fn run_command(command: WButtonAction) {

FILE: src/layout.rs
  type MenuLayoutChildImpl (line 19) | pub struct MenuLayoutChildImpl {
  constant NAME (line 35) | const NAME: &'static str = "MenuLayoutChild";
  type Type (line 37) | type Type = super::MenuLayoutChild;
  type ParentType (line 39) | type ParentType = gtk4::LayoutChild;
  type Interfaces (line 41) | type Interfaces = ();
  method new (line 56) | fn new(
  type MenuLayoutChildPlacement (line 71) | pub enum MenuLayoutChildPlacement {
  type LayoutMenuImpl (line 94) | pub struct LayoutMenuImpl {
  constant NAME (line 110) | const NAME: &'static str = "MenuLayout";
  type Type (line 112) | type Type = super::MenuLayout;
  type ParentType (line 114) | type ParentType = gtk4::LayoutManager;
  type Interfaces (line 116) | type Interfaces = ();
  method allocate (line 124) | fn allocate(&self, widget: &gtk4::Widget, width: i32, height: i32, basel...
  method create_layout_child (line 146) | fn create_layout_child(
  method layout_child_type (line 161) | fn layout_child_type() -> Option<glib::Type> {
  method request_mode (line 165) | fn request_mode(&self, _widget: &gtk4::Widget) -> gtk4::SizeRequestMode {
  method measure (line 170) | fn measure(
  method new (line 197) | pub fn new(
  type MenuLayoutProvider (line 224) | struct MenuLayoutProvider {
    method allocate (line 232) | fn allocate(
    method measure (line 372) | fn measure(

FILE: src/main.rs
  function on_startup (line 23) | fn on_startup(config: &AppConfig) {
  function entry_point (line 36) | fn entry_point(config: Arc<AppConfig>) -> miette::Result<()> {
  function main (line 69) | fn main() -> miette::Result<()> {

FILE: src/paintable.rs
  type PicturePaintableImpl (line 15) | pub struct PicturePaintableImpl {
    method draw (line 71) | fn draw(&self, width: f64, height: f64) {
  constant NAME (line 28) | const NAME: &'static str = "WleavePicturePaintable";
  type Type (line 30) | type Type = PicturePaintable;
  type ParentType (line 32) | type ParentType = glib::Object;
  type Interfaces (line 34) | type Interfaces = (gdk4::Paintable, gtk4::SymbolicPaintable);
  method constructed (line 44) | fn constructed(&self) {
  method current_image (line 150) | fn current_image(&self) -> gdk4::Paintable {
  method flags (line 163) | fn flags(&self) -> gdk4::PaintableFlags {
  method intrinsic_width (line 167) | fn intrinsic_width(&self) -> i32 {
  method intrinsic_height (line 180) | fn intrinsic_height(&self) -> i32 {
  method intrinsic_aspect_ratio (line 193) | fn intrinsic_aspect_ratio(&self) -> f64 {
  method snapshot (line 204) | fn snapshot(&self, snapshot: &gdk4::Snapshot, width: f64, height: f64) {
  method snapshot_symbolic (line 232) | fn snapshot_symbolic(
  method for_path (line 272) | fn for_path(icon_path: impl Into<String>) -> Self {
  function svg_picture_colorized (line 280) | pub fn svg_picture_colorized(icon: &str) -> gtk4::Picture {

FILE: src/units.rs
  type AspectRatio (line 10) | pub enum AspectRatio {
    method deserialize (line 22) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    method as_float (line 70) | pub fn as_float(self) -> f32 {
  method default (line 16) | fn default() -> Self {
  type Err (line 40) | type Err = Box<dyn Error + Send + Sync + 'static>;
  method from_str (line 42) | fn from_str(s: &str) -> Result<Self, Self::Err> {
  method fmt (line 61) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  type LengthValue (line 79) | pub enum LengthValue {
    method deserialize (line 104) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    method for_args (line 116) | pub fn for_args(&self, args: &LengthArgs) -> f32 {
    method parse (line 126) | fn parse(val: &'_ str) -> Result<Self, cssparser::BasicParseError<'_>> {
  type LengthSerialized (line 86) | enum LengthSerialized {
  type LengthDimension (line 92) | pub enum LengthDimension {
  type LengthArgs (line 98) | pub struct LengthArgs {
  type Err (line 159) | type Err = miette::Report;
  method from_str (line 161) | fn from_str(val: &str) -> Result<Self, Self::Err> {
  type Margin (line 168) | pub struct Margin(pub LengthValue);
  type Err (line 171) | type Err = miette::Report;
  method from_str (line 173) | fn from_str(val: &str) -> Result<Self, Self::Err> {
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (107K chars).
[
  {
    "path": ".editorconfig",
    "chars": 235,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\nmax_l"
  },
  {
    "path": ".gitignore",
    "chars": 37,
    "preview": "/target\n/.idea\n/.vscode\n\n/layout.dev\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 888,
    "preview": "[package]\nname = \"wleave\"\nversion = \"0.7.2\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"A Wayland layer-shell logout"
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2023 Natty\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "Makefile",
    "chars": 378,
    "preview": "./target/release/wleave: $(wildcard src/**.rs)\n\tcargo build --frozen --release --all-features\n\n.PHONY: wleave\nwleave: ./"
  },
  {
    "path": "README.md",
    "chars": 8677,
    "preview": "# wleave\n\n![AUR version](https://img.shields.io/aur/version/wleave-git)\n![GitHub](https://img.shields.io/github/license/"
  },
  {
    "path": "completions/_wleave",
    "chars": 3562,
    "preview": "#compdef wleave\n\nautoload -U is-at-least\n\n_wleave() {\n    typeset -A opt_args\n    typeset -a _arguments_options\n    loca"
  },
  {
    "path": "completions/wleave.bash",
    "chars": 6303,
    "preview": "_wleave() {\n    local i cur prev opts cmd\n    COMPREPLY=()\n    if [[ \"${BASH_VERSINFO[0]}\" -ge 4 ]]; then\n        cur=\"$"
  },
  {
    "path": "completions/wleave.fish",
    "chars": 2031,
    "preview": "complete -c wleave -s s -l service -d 'Run the application in service mode - it will stay in the background until trigge"
  },
  {
    "path": "completions_gen/.gitignore",
    "chars": 23,
    "preview": "/target\n/.idea\n/.vscode"
  },
  {
    "path": "completions_gen/Cargo.toml",
    "chars": 148,
    "preview": "[package]\nname = \"wleave_completions\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nclap_complete = \"4.1\"\nclap = \"4"
  },
  {
    "path": "completions_gen/src/main.rs",
    "chars": 720,
    "preview": "use clap::CommandFactory;\nuse clap_complete::{generate_to, shells};\nuse std::env;\nuse std::io::Error;\n\nuse wleave::cli_o"
  },
  {
    "path": "icons/icon-attribution.md",
    "chars": 201,
    "preview": "# Icon Attribution\n\nAll icons are licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) b"
  },
  {
    "path": "layout.json",
    "chars": 2247,
    "preview": "{\n    \"buttons\": [\n        {\n            \"label\": \"lock\",\n            \"action\": [\n                {\n                    "
  },
  {
    "path": "man/wleave.1.scd",
    "chars": 3057,
    "preview": "wleave(1)\n\n# NAME\n\nwleave - A Wayland logout menu\n\n# SYNOPSIS\n\n*wleave* [options] [command]\n\n# OPTIONS\n\n*-h, --help*\n\tSh"
  },
  {
    "path": "man/wleave.5.scd",
    "chars": 1601,
    "preview": "wleave(5)\n\n# NAME\n\nwleave - layout file and options\n\n# LAYOUT\n\nwleave buttons can have the following properties\n- label "
  },
  {
    "path": "man/wleave.json.5.scd",
    "chars": 5111,
    "preview": "wleave.json(5)\n\n# NAME\n\nwleave.json - layout JSON file and configuration options\n\n# TOP-LEVEL-OPTIONS\n\nThe top-level opt"
  },
  {
    "path": "sh.natty.Wleave.desktop",
    "chars": 235,
    "preview": "[Desktop Entry]\n# The version of the Desktop Entry spec, not of the application:\nVersion=1.0\nType=Application\nName=Wleav"
  },
  {
    "path": "src/app/mod.rs",
    "chars": 11541,
    "preview": "pub mod window;\n\nuse crate::app::window::WleaveWindow;\nuse crate::button::{WButton, WButtonActionList, WButtonJustify};\n"
  },
  {
    "path": "src/app/window.rs",
    "chars": 2654,
    "preview": "mod window_impl {\n    use glib::object::ObjectExt;\n    use glib::subclass::object::DerivedObjectProperties;\n    use glib"
  },
  {
    "path": "src/button.rs",
    "chars": 5625,
    "preview": "use serde::de::{IntoDeserializer, SeqAccess, Visitor};\nuse serde::{Deserialize, Deserializer};\nuse std::collections::BTr"
  },
  {
    "path": "src/cli_opt.rs",
    "chars": 4876,
    "preview": "use crate::units::{AspectRatio, LengthValue, Margin};\nuse clap::{ArgAction, Parser, ValueEnum};\nuse serde::{Deserialize,"
  },
  {
    "path": "src/config.rs",
    "chars": 8231,
    "preview": "use crate::button::WButton;\nuse crate::error::WError;\nuse convert_case::ccase;\nuse gdk4::gio;\nuse gtk4::CssProvider;\nuse"
  },
  {
    "path": "src/error.rs",
    "chars": 744,
    "preview": "use miette::{Diagnostic, NamedSource, SourceOffset};\nuse std::path::PathBuf;\nuse thiserror::Error;\n\n#[derive(Error, Diag"
  },
  {
    "path": "src/exec.rs",
    "chars": 566,
    "preview": "use crate::button::{WButtonAction, WButtonActionHandler};\nuse std::process::Command;\nuse tracing::error;\n\npub fn run_com"
  },
  {
    "path": "src/layout.rs",
    "chars": 14035,
    "preview": "use crate::layout::menu_layout::LayoutMenuImpl;\nuse crate::layout::menu_layout_child::MenuLayoutChildImpl;\nuse glib::obj"
  },
  {
    "path": "src/lib.rs",
    "chars": 32,
    "preview": "pub mod cli_opt;\npub mod units;\n"
  },
  {
    "path": "src/main.rs",
    "chars": 2129,
    "preview": "mod app;\nmod button;\nmod config;\nmod error;\nmod exec;\nmod layout;\nmod paintable;\n\nuse clap::Parser;\nuse glib::clone;\nuse"
  },
  {
    "path": "src/paintable.rs",
    "chars": 8500,
    "preview": "use gdk4::prelude::*;\nuse gdk4::subclass::paintable::PaintableImpl;\nuse glib::subclass::ObjectImplRef;\nuse glib::subclas"
  },
  {
    "path": "src/units.rs",
    "chars": 4892,
    "preview": "use miette::miette;\nuse serde::{Deserialize, Deserializer};\nuse serde_json::Value;\nuse std::error::Error;\nuse std::fmt::"
  },
  {
    "path": "style.css",
    "chars": 1198,
    "preview": "/**\n * See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html for more CSS variables.\n * Or use"
  }
]

About this extraction

This page contains the full source code of the AMNatty/wleave GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (99.2 KB), approximately 25.4k tokens, and a symbol index with 121 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!