[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\nmax_line_length = 120\ntab_width = 4\n\n[*.rs]\nmax_line_length = 120\n\n[*.json]\nindent_size = 4\n\n[*.scd]\nindent_style = tab\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/.idea\n/.vscode\n\n/layout.dev\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"wleave\"\nversion = \"0.7.2\"\nedition = \"2024\"\nlicense = \"MIT\"\ndescription = \"A Wayland layer-shell logout prompt written in GTK\"\nrepository = \"https://github.com/AMNatty/wleave\"\n\n[workspace]\nmembers = [\".\", \"completions_gen\"]\n\n[dependencies]\nclap = { version = \"4.1\", features = [\"derive\"] }\n\nthiserror = \"2\"\nmiette = { version = \"7\", features = [\"derive\", \"fancy\"] }\n\ncfg-if = \"1\"\n\nconvert_case = \"0.10\"\n\ndirs = \"6.0\"\ncairo-rs = \"0.21\"\nglib = \"0.21\"\nglib-macros = \"0.21\"\nlibadwaita = { version = \"0.8\", features = [\"v1_7\"] }\nlibrsvg = \"2.61\"\ngdk4 = { version = \"0.10\", features = [\"v4_18\"] }\ngtk4 = { version = \"0.10\", features = [\"gnome_45\"] }\ngtk4-layer-shell = \"0.7\"\nwhich = \"8\"\n\ncssparser = \"0.36\"\n\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\n\ntracing = { version = \"0.1\" }\ntracing-subscriber = { version = \"0.3\", features = [\"env-filter\"] }\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Natty\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "./target/release/wleave: $(wildcard src/**.rs)\n\tcargo build --frozen --release --all-features\n\n.PHONY: wleave\nwleave: ./target/release/wleave\n\n.PHONY: completions\ncompletions: wleave\n\tmkdir -p completions\n\tOUT_DIR=completions cargo run --package wleave_completions --bin wleave_completions\n\n.PHONY: all\nall: wleave\n\n.PHONY: clean\nclean:\n\trm -rf ./target ./completions_generated\n"
  },
  {
    "path": "README.md",
    "content": "# wleave\n\n![AUR version](https://img.shields.io/aur/version/wleave-git)\n![GitHub](https://img.shields.io/github/license/AMNatty/wleave)\n\nA Wayland layer-shell logout prompt!\n\nOriginally a fork of [wlogout](https://github.com/ArtsyMacaw/wlogout), `wleave` is now natively GTK4 & Libadwaita and\nhas a bunch of added quality-of-life features.\n\n![The default Wleave menu look](/example.png)\n\n## Installation\n\n### Arch Linux\n\n**wleave** can be installed from the **AUR**:\n\n```shell\nparu -S wleave-git\n```\n\n### Building from sources\n\nDependencies:\n\n- gtk4-layer-shell\n- gtk4\n- librsvg (for SVG images)\n- libadwaita\n- a stable version of the Rust toolchain\n\nYou can run the application using `cargo run --release` or GNU make:\n\n```shell\nmake\n./target/release/wleave\n```\n\n## Usage\n\nThe command line options are backwards-compatible with **wlogout**.\nSee `--help` for a list of options.\n\n### Help, how do I close the menu?\n\nThe `<Esc>` key closes the menu, an option to change this may be added eventually.\n\n## Configuration\n\n**wleave** is backwards-compatible with **wlogout** configuration files.\n\nSince **version 0.6.0**, *full JSON configuration* can be used in place of the `wlogout`-based\nconfiguration. The default configuration file can be copied from `/etc/wleave/layout.json`.\nThe new configuration system is more flexible as it removes the need for extra command-line\narguments.\n\nFrom `man 5 wleave.json`, the allowed top-level options are:\n\n* `\"buttons\"` **(array)** - a list of buttons\n* `\"css\"` **(string)** - Specify a custom CSS file instead of the default one\n* `\"service\": false` **(boolean)**\n  Run the application as a service, with all instances of wleave opening this one. Allows faster startup at the\n  cost of running in the background\n* `\"button-layout\": \"grid\"` Specify the way buttons should be laid out.\n  See [dynamic layouts](#dynamic-layouts-supsince-070sup) for more details.\n* `\"buttons-per-row\": \"3\"` **(string)** Set the number of buttons per row, or use a fraction to specify the number\n  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)\n* `\"column-spacing\": \"8px\"` **(number / \"#px\" / \"#%\")** Set space between buttons columns\n* `\"row-spacing\": \"8px\"` **(number / \"#px\" / \"#%\")** Set space between buttons rows\n* `\"margin\": \"20%\"` **(number / \"#px\" / \"#%\")** Set margin on all sides\n* `\"margin-left\"` **(number / \"#px\" / \"#%\")** Set margin for left of buttons. Falls back to the value set by *margin*\n* `\"margin-right\"` **(number / \"#px\" / \"#%\")** Set margin for right of buttons. Falls back to the value set by\n  *margin*\n* `\"margin-top\"` **(number / \"#px\" / \"#%\")** Set margin for top of buttons. Falls back to the value set by *margin*\n* `\"margin-bottom\"` **(number / \"#px\" / \"#%\")** Set margin for bottom of buttons. Falls back to the value set by\n  *margin*\n* `\"button-aspect-ratio\"` **(string or number)** Set the aspect ratio of the buttons, either as a float (as a number or\n  string) or a ratio (e.g. \"5/4\"). If unspecified, the buttons fill all available space between the margins.\n* `\"close-on-lost-focus\": false` **(boolean)** Closes the menu if focus is lost\n* `\"show-keybinds\": false`: **(boolean)** Show the associated key binds for each button\n* `\"protocol\": \"layer-shell\"` (**\"layer-shell\"**/**\"xdg\"**/**\"none\"**) Backend to use for full-screening the menu\n* `\"no-version-info\": false` **(boolean)** Hides the version label.\n* `\"delay-command-ms\": 100` **(number)** The number of milliseconds to wait after an action before the associated\n  command is executed\n\nThe command-line option counterparts of these options take precedence over the configuration file.\n\n*Example configuration* with one button that executes `swaylock` on click:\n\n```json\n{\n    \"margin\": 200,\n    \"buttons-per-row\": \"1/1\",\n    \"delay-command-ms\": 100,\n    \"close-on-lost-focus\": true,\n    \"show-keybinds\": true,\n    \"buttons\": [\n        {\n            \"label\": \"lock\",\n            \"action\": \"swaylock\",\n            \"text\": \"Lock\",\n            \"keybind\": \"l\",\n            \"icon\": \"/usr/share/wleave/icons/lock.svg\"\n        }\n    ]\n}\n```\n\nLayout files may also be read from *stdin* with `--layout -`.\nFor example, with `jq`, buttons can be picked out:\n\n```shell\n$ jq '.buttons[] |= select([.label] | inside([\"lock\", \"logout\"]))' layout.json | wleave --layout -\n```\n\n### Running as a service <sup>since 0.7.0</sup>\n\nAdd the flag `--service` to run the application as a service, waiting until it is activated by another\ncall to `wleave`. This allows faster startup at the cost of running in the background.\n\nFor example, to run `wleave` in the background with the [Niri](https://github.com/YaLTeR/niri) compositor,\nspawn the application on startup:\n\n```kdl\nspawn-at-startup \"wleave\" \"--service\"\n```\n\nCurrently, the configuration is determined by the background instance, and a restart is necessary\nto apply configuration changes.\n\n### Dynamic layouts <sup>since 0.7.0</sup>\n\nChoose one of the available layouts, using the `--button-layout` option.\n\n#### Grid\n\nName: `\"grid\"` <br />\nSince: 0.7\n\nThe default mode, with buttons being simply laid out in a grid.\n\n### Conditional actions <sup>since 0.7.0</sup>\n\nThe action field can be an object with exactly one of `shell` or `executable` properties set. The `shell` action\nexecutes the given command in the system shell, while the `executable` action executes the specified binary,\nassuming it is in `$PATH`. `executable` actions get filtered if the specified executable does not exist,\nallowing multiple possible options.\n\nAny action that is **a plain string is interpreted as a shell command** with no conditions set.\n\nAny extra properties in the `action` objects are interpreted as filters. Currently, only environment variable filtering\nis implemented, where the values of fields prefixed with `$` are matched as conditions for the given action.\n\n**The action field can also be an array where the first matching command is picked.**\n\nFor example, in the following example, the `loginctl lock-session` command is executed on GNOME, while other desktop\nenvironments will try `gtklock` and then `swaylock`.\n\n```json\n{\n    \"action\": [\n        {\n            \"$DESKTOP_SESSION\": \"gnome\",\n            \"shell\": \"loginctl lock-session\"\n        },\n        {\n            \"executable\": \"gtklock\"\n        },\n        \"swaylock\"\n    ]\n}\n```\n\n## Styling\n\nBy default, `wleave` follows `libadwaita` colors and uses CSS variables.\nThis allows following the system light/dark theme preference from GNOME settings.\nIn other desktop environments, this may be changed with\n`gsettings set org.gnome.desktop.interface color-scheme \"'prefer-dark'\"` or\n`gsettings set org.gnome.desktop.interface color-scheme \"'prefer-light'\"` correspondingly.\n\nThe stylesheet in `/etc/wleave/style.css` is fully customizable and can be edited.\n\n### Colorized icons\n\nSVG icons are dynamically recolored if possible. <small>(since 0.6.2)</small>\n\nEach button has an identifier set in the layout file, which allows custom-styling each button\none-by-one. Icon colors may be changed by modifying the CSS variable `--view-fg-color`,\nor by setting a custom `color` property entirely.\n\n### Example recipe\n\nExample stylesheet that makes the selected icon colored with the `libadwaita` accent color:\n\n```css\nwindow {\n    background-color: rgba(12, 12, 12, 0.8);\n}\n\nbutton {\n    color: var(--view-fg-color);\n    background-color: var(--view-bg-color);\n    border: none;\n    padding: 10px;\n}\n\nbutton label.action-name {\n    font-size: 24px;\n}\n\nbutton label.keybind {\n    font-size: 20px;\n    font-family: monospace;\n}\n\nbutton:hover label.keybind, button:focus label.keybind {\n    opacity: 1;\n}\n\nbutton:hover,\nbutton:focus {\n    color: var(--accent-color);\n    background-color: var(--window-bg-color);\n}\n\nbutton:active {\n    color: var(--accent-fg-color);\n    background-color: var(--accent-bg-color);\n}\n\n```\n\n## Keybinds reference\n\nSee <https://gitlab.gnome.org/GNOME/gtk/-/blob/4.18.0/gdk/keynames.txt> for a list of valid keybinds.\n\n## Enhancements\n\n- A desktop file <sup>since 0.7</sup>\n- Conditionally pick different actions on different desktop environments <sup>since 0.7</sup>\n- SVG icons can be colorized via CSS `color` <sup>since 0.6</sup>\n- Libadwaita accent colors <sup>since 0.6</sup>\n- Automatic light theme by default <sup>since 0.6</sup>\n- Natively GTK4 <sup>since 0.5</sup>\n- New pretty icons by [@earth-walker](https://github.com/earth-walker)\n- Autoclose when window focus is lost (the `-f/--close-on-lost-focus` flag)\n- Mnemonic labels (the `-k/--show-keybinds` flag)\n- Pretty gaps by default\n- Less error-prone\n- Keybinds accept modifier keys and Unicode characters\n- Easier to extend\n"
  },
  {
    "path": "completions/_wleave",
    "content": "#compdef wleave\n\nautoload -U is-at-least\n\n_wleave() {\n    typeset -A opt_args\n    typeset -a _arguments_options\n    local ret=1\n\n    if is-at-least 5.2; then\n        _arguments_options=(-s -S -C)\n    else\n        _arguments_options=(-s -C)\n    fi\n\n    local context curcontext=\"$curcontext\" state line\n    _arguments \"${_arguments_options[@]}\" : \\\n'-s+[Run the application in service mode - it will stay in the background until triggered]' \\\n'--service=[Run the application in service mode - it will stay in the background until triggered]' \\\n'-l+[Specify a layout file, specifying - will read the layout config from stdin]:LAYOUT:_files' \\\n'--layout=[Specify a layout file, specifying - will read the layout config from stdin]:LAYOUT:_files' \\\n'--button-layout=[Specify the way the buttons should be laid out in the available space]:BUTTON_LAYOUT:(grid)' \\\n'-C+[Specify a custom CSS file]:CSS:_files' \\\n'--css=[Specify a custom CSS file]:CSS:_files' \\\n'-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' \\\n'--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' \\\n'-c+[Set space between buttons columns]:COLUMN_SPACING:_default' \\\n'--column-spacing=[Set space between buttons columns]:COLUMN_SPACING:_default' \\\n'-r+[Set space between buttons rows]:ROW_SPACING:_default' \\\n'--row-spacing=[Set space between buttons rows]:ROW_SPACING:_default' \\\n'-m+[Set the margin around buttons]:MARGIN:_default' \\\n'--margin=[Set the margin around buttons]:MARGIN:_default' \\\n'-L+[Set margin for the left of buttons]:MARGIN_LEFT:_default' \\\n'--margin-left=[Set margin for the left of buttons]:MARGIN_LEFT:_default' \\\n'-R+[Set margin for the right of buttons]:MARGIN_RIGHT:_default' \\\n'--margin-right=[Set margin for the right of buttons]:MARGIN_RIGHT:_default' \\\n'-T+[Set margin for the top of buttons]:MARGIN_TOP:_default' \\\n'--margin-top=[Set margin for the top of buttons]:MARGIN_TOP:_default' \\\n'-B+[Set the margin for the bottom of buttons]:MARGIN_BOTTOM:_default' \\\n'--margin-bottom=[Set the margin for the bottom of buttons]:MARGIN_BOTTOM:_default' \\\n'-A+[Set the aspect ratio of the buttons]:BUTTON_ASPECT_RATIO:_default' \\\n'--button-aspect-ratio=[Set the aspect ratio of the buttons]:BUTTON_ASPECT_RATIO:_default' \\\n'-d+[The delay (in milliseconds) between the window closing and executing the selected option]:DELAY_COMMAND_MS:_default' \\\n'--delay-command-ms=[The delay (in milliseconds) between the window closing and executing the selected option]:DELAY_COMMAND_MS:_default' \\\n'-f+[Close the menu on lost focus]' \\\n'--close-on-lost-focus=[Close the menu on lost focus]' \\\n'-k+[Show the associated key binds]' \\\n'--show-keybinds=[Show the associated key binds]' \\\n'-p+[Use layer-shell or xdg protocol]:PROTOCOL:(layer-shell none xdg)' \\\n'--protocol=[Use layer-shell or xdg protocol]:PROTOCOL:(layer-shell none xdg)' \\\n'-x+[Hide version information]' \\\n'--no-version-info=[Hide version information]' \\\n'-v[]' \\\n'--version[]' \\\n'-h[Print help]' \\\n'--help[Print help]' \\\n&& ret=0\n}\n\n(( $+functions[_wleave_commands] )) ||\n_wleave_commands() {\n    local commands; commands=()\n    _describe -t commands 'wleave commands' commands \"$@\"\n}\n\nif [ \"$funcstack[1]\" = \"_wleave\" ]; then\n    _wleave \"$@\"\nelse\n    compdef _wleave wleave\nfi\n"
  },
  {
    "path": "completions/wleave.bash",
    "content": "_wleave() {\n    local i cur prev opts cmd\n    COMPREPLY=()\n    if [[ \"${BASH_VERSINFO[0]}\" -ge 4 ]]; then\n        cur=\"$2\"\n    else\n        cur=\"${COMP_WORDS[COMP_CWORD]}\"\n    fi\n    prev=\"$3\"\n    cmd=\"\"\n    opts=\"\"\n\n    for i in \"${COMP_WORDS[@]:0:COMP_CWORD}\"\n    do\n        case \"${cmd},${i}\" in\n            \",$1\")\n                cmd=\"wleave\"\n                ;;\n            *)\n                ;;\n        esac\n    done\n\n    case \"${cmd}\" in\n        wleave)\n            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\"\n            if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then\n                COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n                return 0\n            fi\n            case \"${prev}\" in\n                --service)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -s)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --layout)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -l)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --button-layout)\n                    COMPREPLY=($(compgen -W \"grid\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --css)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -C)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --buttons-per-row)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -b)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --column-spacing)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -c)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --row-spacing)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -r)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --margin)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -m)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --margin-left)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -L)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --margin-right)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -R)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --margin-top)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -T)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --margin-bottom)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -B)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --button-aspect-ratio)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -A)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --delay-command-ms)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -d)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --close-on-lost-focus)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -f)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --show-keybinds)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -k)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --protocol)\n                    COMPREPLY=($(compgen -W \"layer-shell none xdg\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -p)\n                    COMPREPLY=($(compgen -W \"layer-shell none xdg\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --no-version-info)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -x)\n                    COMPREPLY=($(compgen -W \"true false\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                *)\n                    COMPREPLY=()\n                    ;;\n            esac\n            COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n            return 0\n            ;;\n    esac\n}\n\nif [[ \"${BASH_VERSINFO[0]}\" -eq 4 && \"${BASH_VERSINFO[1]}\" -ge 4 || \"${BASH_VERSINFO[0]}\" -gt 4 ]]; then\n    complete -F _wleave -o nosort -o bashdefault -o default wleave\nelse\n    complete -F _wleave -o bashdefault -o default wleave\nfi\n"
  },
  {
    "path": "completions/wleave.fish",
    "content": "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''\nfalse\\t''\"\ncomplete -c wleave -s l -l layout -d 'Specify a layout file, specifying - will read the layout config from stdin' -r -F\ncomplete -c wleave -l button-layout -d 'Specify the way the buttons should be laid out in the available space' -r -f -a \"grid\\t''\"\ncomplete -c wleave -s C -l css -d 'Specify a custom CSS file' -r -F\ncomplete -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\ncomplete -c wleave -s c -l column-spacing -d 'Set space between buttons columns' -r\ncomplete -c wleave -s r -l row-spacing -d 'Set space between buttons rows' -r\ncomplete -c wleave -s m -l margin -d 'Set the margin around buttons' -r\ncomplete -c wleave -s L -l margin-left -d 'Set margin for the left of buttons' -r\ncomplete -c wleave -s R -l margin-right -d 'Set margin for the right of buttons' -r\ncomplete -c wleave -s T -l margin-top -d 'Set margin for the top of buttons' -r\ncomplete -c wleave -s B -l margin-bottom -d 'Set the margin for the bottom of buttons' -r\ncomplete -c wleave -s A -l button-aspect-ratio -d 'Set the aspect ratio of the buttons' -r\ncomplete -c wleave -s d -l delay-command-ms -d 'The delay (in milliseconds) between the window closing and executing the selected option' -r\ncomplete -c wleave -s f -l close-on-lost-focus -d 'Close the menu on lost focus' -r -f -a \"true\\t''\nfalse\\t''\"\ncomplete -c wleave -s k -l show-keybinds -d 'Show the associated key binds' -r -f -a \"true\\t''\nfalse\\t''\"\ncomplete -c wleave -s p -l protocol -d 'Use layer-shell or xdg protocol' -r -f -a \"layer-shell\\t''\nnone\\t''\nxdg\\t''\"\ncomplete -c wleave -s x -l no-version-info -d 'Hide version information' -r -f -a \"true\\t''\nfalse\\t''\"\ncomplete -c wleave -s v -l version\ncomplete -c wleave -s h -l help -d 'Print help'\n"
  },
  {
    "path": "completions_gen/.gitignore",
    "content": "/target\n/.idea\n/.vscode"
  },
  {
    "path": "completions_gen/Cargo.toml",
    "content": "[package]\nname = \"wleave_completions\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nclap_complete = \"4.1\"\nclap = \"4.1\"\nwleave = { path = \"..\" }"
  },
  {
    "path": "completions_gen/src/main.rs",
    "content": "use clap::CommandFactory;\nuse clap_complete::{generate_to, shells};\nuse std::env;\nuse std::io::Error;\n\nuse wleave::cli_opt::Args;\n\nfn main() -> Result<(), Error> {\n    let outdir = match env::var_os(\"OUT_DIR\") {\n        None => return Ok(()),\n        Some(outdir) => outdir,\n    };\n\n    let mut cmd = Args::command();\n\n    println!(\n        \"Bash completions generated: {:?}\",\n        generate_to(shells::Bash, &mut cmd, \"wleave\", &outdir)?\n    );\n\n    println!(\n        \"Zsh completions generated: {:?}\",\n        generate_to(shells::Zsh, &mut cmd, \"wleave\", &outdir)?\n    );\n\n    println!(\n        \"Fish completions generated: {:?}\",\n        generate_to(shells::Fish, &mut cmd, \"wleave\", &outdir)?\n    );\n\n    Ok(())\n}\n"
  },
  {
    "path": "icons/icon-attribution.md",
    "content": "# Icon Attribution\n\nAll icons are licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) by Earth Walker 2023\n\nModified in version 0.6.0 by Natty to support colorization.\n"
  },
  {
    "path": "layout.json",
    "content": "{\n    \"buttons\": [\n        {\n            \"label\": \"lock\",\n            \"action\": [\n                {\n                    \"$DESKTOP_SESSION\": \"gnome\",\n                    \"shell\": \"loginctl lock-session\"\n                },\n                {\n                    \"executable\": \"gtklock\"\n                },\n                {\n                    \"executable\": \"swaylock\"\n                }\n            ],\n            \"text\": \"Lock\",\n            \"keybind\": \"l\",\n            \"icon\": \"/usr/share/wleave/icons/lock.svg\"\n        },\n        {\n            \"label\": \"hibernate\",\n            \"action\": \"systemctl hibernate\",\n            \"text\": \"Hibernate\",\n            \"keybind\": \"h\",\n            \"icon\": \"/usr/share/wleave/icons/hibernate.svg\"\n        },\n        {\n            \"label\": \"logout\",\n            \"action\": [\n                {\n                    \"$DESKTOP_SESSION\": \"niri\",\n                    \"shell\": \"niri msg action quit --skip-confirmation\"\n                },\n                {\n                    \"$DESKTOP_SESSION\": \"sway\",\n                    \"shell\": \"swaymsg exit\"\n                },\n                {\n                    \"$DESKTOP_SESSION\": \"plasma\",\n                    \"shell\": \"qdbus org.kde.KWin /Session org.kde.KWin.Session.quit\"\n                },\n                {\n                    \"$DESKTOP_SESSION\": \"gnome\",\n                    \"shell\": \"gnome-session-quit --no-prompt\"\n                },\n                \"loginctl terminate-user $USER\"\n            ],\n            \"text\": \"Logout\",\n            \"keybind\": \"e\",\n            \"icon\": \"/usr/share/wleave/icons/logout.svg\"\n        },\n        {\n            \"label\": \"shutdown\",\n            \"action\": \"systemctl poweroff\",\n            \"text\": \"Shutdown\",\n            \"keybind\": \"s\",\n            \"icon\": \"/usr/share/wleave/icons/shutdown.svg\"\n        },\n        {\n            \"label\": \"suspend\",\n            \"action\": \"systemctl suspend\",\n            \"text\": \"Suspend\",\n            \"keybind\": \"u\",\n            \"icon\": \"/usr/share/wleave/icons/suspend.svg\"\n        },\n        {\n            \"label\": \"reboot\",\n            \"action\": \"systemctl reboot\",\n            \"text\": \"Reboot\",\n            \"keybind\": \"r\",\n            \"icon\": \"/usr/share/wleave/icons/reboot.svg\"\n        }\n    ]\n}\n"
  },
  {
    "path": "man/wleave.1.scd",
    "content": "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\tShow help message and stop\n\n*-l, --layout* <layout>\n\tSpecify a custom layout file, specify - to read the config from stdin\n\n*--button-layout* \"grid\"\n\tSpecify the way buttons should be laid out, \"grid\" being the default. Currently, only \"grid\" is supported.\n\n*-S, --service*\n\tRun the application as a service, with all instances of wleave opening this one. Allows faster startup at the cost of running in the background.\n\n*-v, --version*\n\tShow version number and stop\n\n*-C, -css* <css>\n\tSpecify a custom css file\n\n*-b, --buttons-per-row* <num>\n\tSet 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)\n\n*-c, --column-spacing* <space>\n\tSet space between buttons columns\n\n*-r, --row-spacing* <space>\n\tSet space between buttons rows\n\n*-m, --margin* <padding>\n\tSet margin on all sides\n\n*-L, --margin-left* <padding>\n\tSet margin for left of buttons\n\n*-R, --margin-right* <padding>\n\tSet margin for right of buttons\n\n*-T, --margin-top* <padding>\n\tSet margin for top of buttons\n\n*-B, --margin-bottom* <padding>\n\tSet margin for bottom of buttons\n\n*-A, --button-aspect-ratio* <ratio>\n\tSet the aspect ratio of the buttons, either as a float (as a number or string) or a ratio (e.g. \"5/4\").\n\tIf unspecified, the buttons fill all available space between the margins.\n\n*-f, --close-on-lost-focus*\n\tCloses the menu if focus is lost\n\n*-k, --show-keybinds*\n\tShow the associated key binds for each button\n\n*-p, --protocol* <protocol>\n\tTakes layer-shell, xdg, or none.\n\tThe \"layer-shell\" allows transparency effects; however, only a few compositors correctly support it.\n\tThe \"xdg\" protocol will work on almost all compositors, but does not allow for transparency.\n\tThe \"none\" option shows the window as is, without full-screening it\n\n*-d, --delay-command-ms* <number>\n\tThe number of milliseconds to wait after an action before the associated command is executed\n\n*-x, --no-version-info*\n\tHide the version label\n\n# DESCRIPTION\n\nwleave is a Wayland-native logout script. It is a modern rewrite of Wlogout and a drop-in replacement.\n\n# CONFIGURATION\n\nwleave searches for a layout and style.css file in the following locations, in this order:\n\n. $XDG_CONFIG_HOME/wleave/\n. $XDG_CONFIG_HOME/wlogout/\n. /etc/wleave/\n. /etc/wlogout/\n. /usr/local/etc/wleave\n. /usr/local/etc/wlogout\n\nIf unset, $XDG_CONFIG_HOME defaults to *~/.config/*.\n\nAn 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.\n\n# AUTHORS\n\nBased on Wlogout by Haden Collins <collinshaden@gmail.com>. For more information about wlogout, see <https://github.com/ArtsyMacaw/wlogout>.\nRewrite by Natty <natty.sh.git@gmail.com>, see <https://github.com/AMNatty/wleave>.\n\n# SEE ALSO\n\n*wleave*(5)\n"
  },
  {
    "path": "man/wleave.5.scd",
    "content": "wleave(5)\n\n# NAME\n\nwleave - layout file and options\n\n# LAYOUT\n\nwleave buttons can have the following properties\n- label \n- action\n- text\n- keybind\n- icon\n- height \\*\n- width \\* \n- circular \\*\n\n\\* Optional values\n\nLabel 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. \n\n# FILE\n\nThe 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:\n```\n{\n    \"label\" : \"foo\",\n    \"action\" : \"echo 'hello world'\",\n    \"text\" : \"bar\",\n    \"keybind\" : \"f\",\n    \"height\" : 1, \n    \"width\" : 1,\n    \"circular\" : true\n}\n```\nWould 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.\n\n# AUTHORS\n\nBased on Wlogout by Haden Collins <collinshaden@gmail.com>. For more information about wlogout, see <https://github.com/ArtsyMacaw/wlogout>.\nRewrite by Natty <natty.sh.git@gmail.com>, see <https://github.com/AMNatty/wleave>.\n\n# SEE ALSO\n\n*wleave.json*(5)\n*wleave*(1)\n"
  },
  {
    "path": "man/wleave.json.5.scd",
    "content": "wleave.json(5)\n\n# NAME\n\nwleave.json - layout JSON file and configuration options\n\n# TOP-LEVEL-OPTIONS\n\nThe top-level options directly correspond to their long-version command-line option counter parts.\nButton layout is specified in the extra *\"button\"* field.\n\n*\"buttons\"*: <array of buttons>\n\tSee section *LAYOUT* for structure\n\n*\"button-layout\"*: <*\"grid\"*>\n\tSpecify the way buttons should be laid out, \"grid\" being the default. Currently, only \"grid\" is supported.\n\n*\"css\"*: <string>\n\tSpecify a custom CSS file instead of the default one\n\n*\"service\"*: <boolean>\n\tRun the application as a service, with all instances of wleave opening this one. Allows faster startup at the\n\tcost of running in the background. ++\n*Default*: false\n\n*\"buttons-per-row\"*: <string>\n\tSet the number of buttons per row, or use a fraction to specify the number of rows to be used\n\t(e.g. \"1/1\" for all buttons in a single row, \"1/5\" to distribute the buttons over 5 rows) ++\n*Default*: \"3\"\n\n*\"column-spacing\"*: <number / \"<number>px\" / \"<number>%\">\n\tSet space between button columns ++\n*Default*: \"8px\"\n\n*\"row-spacing\"*: <number / \"<number>px\" / \"<number>%\">\n\tSet space between button rows ++\n*Default*: \"8px\"\n\n*\"margin\"*: <number / \"<number>px\" / \"<number>%\">\n\tSet margin on all sides ++\n*Default*: \"20%\" (of window size)\n\n*\"margin-left\"* <number / \"<number>px\" / \"<number>%\">\n\tSet margin for left of buttons ++\nFalls back to the value set by *margin*\n\n*\"margin-right\"* <number / \"<number>px\" / \"<number>%\">\n\tSet margin for right of buttons ++\nFalls back to the value set by *margin*\n\n*\"margin-top\"* <number / \"<number>px\" / \"<number>%\">\n\tSet margin for top of buttons ++\nFalls back to the value set by *margin*\n\n*\"margin-bottom\"* <number / \"<number>px\" / \"<number>%\">\n\tSet margin for bottom of buttons ++\nFalls back to the value set by *margin*\n\n*\"button-aspect-ratio\"* <string or number>\n\tSet the aspect ratio of the buttons, either as a float (as a number or string) or a ratio (e.g. \"5/4\"). ++\nIf unspecified, the buttons fill all available space between the margins.\n\n*\"close-on-lost-focus\"*: <*true*/*false*>\n\tCloses the menu if focus is lost ++\n*Default*: false\n\n*\"show-keybinds\"*: <*true*/*false*>\n\tShow the associated key binds for each button ++\n*Default*: false\n\n*\"protocol\"*: <*\"layer-shell\"*/*\"xdg\"*/*\"none\"*>\n\tThe layer-shell protocol is the intended protocol to be used with wleave, however it requires compositor support ++\n*Default*: \"layer-shell\"\n\n*\"no-version-info\"*: <*true*/*false*>\n\tHides the version label ++\n*Default*: false\n\n*\"delay-command-ms\"*: <number>\n\tThe number of milliseconds to wait after an action before the associated command is executed ++\n*Default*: 100\n\n# LAYOUT\n\nwleave buttons may have the following properties:\n\n- *\"label\"* (string) - internal name of the button that can be used in CSS as an identifier\n- *\"action\"* (command string or object or list thereof) - the action that will be executed via shell upon pressing the button\n- *\"text\"* (string) - the visible label text of the button\n- *\"keybind\"* (string) - a GTK key name associated with the button\n- *\"icon\"* (path string) - filename to load an icon image from\n- *\"height\"* (*optional*, number) - value in the range 0.0 to 1.0 to override the placement of the label in the button\n- *\"width\"* (*optional*, number) - value in the range 0.0 to 1.0 to override the placement of the label in the button\n- *\"circular\"* (*optional*, boolean, default false)\n\nThe action field can be an object with exactly one of `shell` or `executable` properties set. The `shell` action executes\nthe given command in the system shell, while the `executable` action executes the specified binary, assuming it is\nin `$PATH`. `executable` actions get filtered if the specified executable does not exist, allowing multiple possible options.\n\nAny extra properties in the `action` objects are interpreted as filters. Currently, only environment variable filtering\nis implemented, where the values fields prefixed with `$` are matched as conditions for the given action.\n\nThe action field can also be an array where the first matching command is picked.\n\nFor example, in the following example, the `loginctl lock-session` command is executed on GNOME, while other desktop\nenvironments will try either `gtklock` or `swaylock` in that order.\n\n```\n\"action\": [\n    {\n        \"$DESKTOP_SESSION\": \"gnome\",\n        \"shell\": \"loginctl lock-session\"\n    },\n    {\n        \"executable\": \"gtklock\"\n    },\n    {\n        \"executable\": \"swaylock\"\n    }\n]\n```\n\n# FILE\n\nExample file:\n\n```\n{\n    \"close-on-lost-focus\": true,\n    \"show-keybinds\": true,\n    \"buttons-per-row\": \"1/1\",\n    \"buttons\": [\n        {\n            \"label\": \"lock\",\n            \"action\": \"swaylock\",\n            \"text\": \"Lock\",\n            \"keybind\": \"l\",\n            \"icon\": \"/usr/share/wleave/icons/lock.svg\"\n        }\n    ]\n}\n\n```\n\n# AUTHORS\n\nBased on Wlogout by Haden Collins <collinshaden@gmail.com>. For more information about wlogout, see <https://github.com/ArtsyMacaw/wlogout>.\nRewrite by Natty <natty.sh.git@gmail.com>, see <https://github.com/AMNatty/wleave>.\n\n# SEE ALSO\n\n*wleave*(5)\n*wleave*(1)\n"
  },
  {
    "path": "sh.natty.Wleave.desktop",
    "content": "[Desktop Entry]\n# The version of the Desktop Entry spec, not of the application:\nVersion=1.0\nType=Application\nName=Wleave\nComment=Open a system log-out menu\nIcon=wleave\nExec=wleave\nTerminal=false\nStartupNotify=false\nCategories=System;\n"
  },
  {
    "path": "src/app/mod.rs",
    "content": "pub mod window;\n\nuse crate::app::window::WleaveWindow;\nuse crate::button::{WButton, WButtonActionList, WButtonJustify};\nuse crate::config::AppConfig;\nuse crate::exec::run_command;\nuse crate::layout::MenuLayout;\nuse crate::paintable::svg_picture_colorized;\nuse glib::object::Cast;\nuse glib::timeout_add_local_once;\nuse glib_macros::{clone, closure};\nuse gtk4::prelude::{BoxExt, ButtonExt, GObjectPropertyExpressionExt, GtkWindowExt, WidgetExt};\nuse gtk4::{EventControllerKey, GestureClick, PropagationPhase};\nuse gtk4_layer_shell::{KeyboardMode, LayerShell};\nuse libadwaita::prelude::AdwApplicationWindowExt;\nuse std::sync::Arc;\nuse std::time::Duration;\nuse wleave::cli_opt::{ButtonLayout, Protocol};\nuse wleave::units::{AspectRatio, LengthArgs, LengthDimension};\n\nfn do_exit(window: &WleaveWindow, _service_mode: bool) {\n    window.close();\n}\n\nfn on_option(\n    command_list: &WButtonActionList,\n    delay_ms: u32,\n    service_mode: bool,\n    window: WleaveWindow,\n) {\n    let Some(command) = command_list.enumerate().find(|w| w.is_applicable()) else {\n        return;\n    };\n\n    let command = command.clone();\n\n    window.connect_hide(clone!(\n        #[strong]\n        command,\n        move |window| {\n            timeout_add_local_once(\n                Duration::from_millis(delay_ms.into()),\n                clone!(\n                    #[strong]\n                    command,\n                    #[weak_allow_none]\n                    window,\n                    move || {\n                        run_command(command);\n                        window.inspect(move |w| do_exit(w, service_mode));\n                    }\n                ),\n            );\n        }\n    ));\n\n    window.set_visible(false);\n}\n\nfn handle_key(\n    config: &Arc<AppConfig>,\n    window: &WleaveWindow,\n    key: &gtk4::gdk::Key,\n) -> glib::Propagation {\n    if let &gtk4::gdk::Key::Escape = key {\n        do_exit(window, config.service);\n        return glib::Propagation::Proceed;\n    }\n\n    let key = key\n        .to_unicode()\n        .map(|c| c.to_string())\n        .or_else(|| key.name().map(|s| s.to_string()));\n\n    if let Some(ref key_name) = key {\n        let button = config.buttons.iter().find(|b| b.keybind == *key_name);\n\n        if let Some(WButton { action, .. }) = button {\n            on_option(\n                action,\n                config.delay_command_ms,\n                config.service,\n                window.clone(),\n            );\n        }\n    }\n\n    glib::Propagation::Proceed\n}\n\npub fn create_app(config: &Arc<AppConfig>, app: &libadwaita::Application) -> WleaveWindow {\n    let service_mode = config.service;\n\n    let container_box = gtk4::CenterBox::builder()\n        .valign(gtk4::Align::Fill)\n        .halign(gtk4::Align::Fill)\n        .orientation(gtk4::Orientation::Vertical)\n        .build();\n\n    let window = WleaveWindow::new(app);\n    window.set_content(Some(&container_box));\n\n    let margin_left = config\n        .margin_left\n        .clone()\n        .unwrap_or_else(|| config.margin.clone());\n    let margin_top = config\n        .margin_top\n        .clone()\n        .unwrap_or_else(|| config.margin.clone());\n    let margin_right = config\n        .margin_right\n        .clone()\n        .unwrap_or_else(|| config.margin.clone());\n    let margin_bottom = config\n        .margin_top\n        .clone()\n        .unwrap_or_else(|| config.margin.clone());\n\n    window.connect_window_width_notify(clone!(\n        #[weak_allow_none]\n        container_box,\n        move |w| {\n            let Some(container_box) = container_box else {\n                return;\n            };\n\n            let arg = LengthArgs {\n                viewport: (w.width() as f32, w.height() as f32),\n                dimension: LengthDimension::Horizontal,\n            };\n\n            container_box.set_margin_start(margin_left.0.for_args(&arg) as i32);\n            container_box.set_margin_end(margin_right.0.for_args(&arg) as i32);\n        }\n    ));\n\n    window.connect_window_height_notify(clone!(\n        #[weak_allow_none]\n        container_box,\n        move |w| {\n            let Some(container_box) = container_box else {\n                return;\n            };\n\n            let arg = LengthArgs {\n                viewport: (w.width() as f32, w.height() as f32),\n                dimension: LengthDimension::Vertical,\n            };\n\n            container_box.set_margin_top(margin_top.0.for_args(&arg) as i32);\n            container_box.set_margin_bottom(margin_bottom.0.for_args(&arg) as i32);\n        }\n    ));\n\n    match config.protocol {\n        Protocol::LayerShell => {\n            window.init_layer_shell();\n            window.set_layer(gtk4_layer_shell::Layer::Overlay);\n            window.set_namespace(Some(\"wleave\"));\n            window.set_exclusive_zone(-1);\n            window.set_keyboard_mode(KeyboardMode::Exclusive);\n\n            window.set_anchor(gtk4_layer_shell::Edge::Left, true);\n            window.set_anchor(gtk4_layer_shell::Edge::Right, true);\n            window.set_anchor(gtk4_layer_shell::Edge::Top, true);\n            window.set_anchor(gtk4_layer_shell::Edge::Bottom, true);\n        }\n        Protocol::Xdg => {\n            window.fullscreen();\n        }\n        Protocol::None => {}\n    }\n\n    if config.close_on_lost_focus {\n        window.connect_is_active_notify(move |window| {\n            if window.is_visible() && !window.is_active() && !service_mode {\n                do_exit(window, service_mode);\n            }\n        });\n    }\n\n    let click_away_controller = GestureClick::builder()\n        .propagation_phase(PropagationPhase::Bubble)\n        .button(gtk4::gdk::BUTTON_PRIMARY)\n        .n_points(1)\n        .build();\n    click_away_controller.connect_released(clone!(\n        #[weak]\n        window,\n        #[upgrade_or_panic]\n        move |_, _, _, _| {\n            do_exit(&window, service_mode);\n        }\n    ));\n    window.add_controller(click_away_controller);\n\n    let key_controller = EventControllerKey::new();\n    key_controller.connect_key_pressed(clone!(\n        #[strong]\n        config,\n        #[weak]\n        window,\n        #[upgrade_or_panic]\n        move |_, key, _, _| handle_key(&config, &window, &key)\n    ));\n    window.add_controller(key_controller);\n\n    let btn_count = config.buttons.len() as u32;\n    let buttons_per_row = match config.buttons_per_row {\n        ButtonLayout::Auto => None,\n        ButtonLayout::PerRow(n) => Some(n),\n        ButtonLayout::RowRatio(n, d) => Some(btn_count * n / d.min(btn_count * n)),\n    };\n\n    let conf_column_spacing = config.column_spacing.clone();\n    let column_spacing = window\n        .property_expression_weak(\"window-width\")\n        .chain_closure::<f32>(closure!(move |w: Option<WleaveWindow>, width: i32| {\n            conf_column_spacing.for_args(&LengthArgs {\n                viewport: (\n                    width as f32,\n                    w.as_ref()\n                        .map(WleaveWindow::window_width)\n                        .unwrap_or_default() as f32,\n                ),\n                dimension: LengthDimension::Horizontal,\n            })\n        }))\n        .upcast();\n\n    let conf_row_spacing = config.column_spacing.clone();\n    let row_spacing = window\n        .property_expression_weak(\"window-height\")\n        .chain_closure::<f32>(closure!(move |w: Option<WleaveWindow>, height: i32| {\n            conf_row_spacing.for_args(&LengthArgs {\n                viewport: (\n                    w.as_ref()\n                        .map(WleaveWindow::window_width)\n                        .unwrap_or_default() as f32,\n                    height as f32,\n                ),\n                dimension: LengthDimension::Horizontal,\n            })\n        }))\n        .upcast();\n\n    let buttons_container = gtk4::Box::builder()\n        .valign(gtk4::Align::Fill)\n        .halign(gtk4::Align::Fill)\n        .layout_manager(&MenuLayout::new(\n            config.button_layout,\n            config.button_aspect_ratio.map(AspectRatio::as_float),\n            column_spacing,\n            row_spacing,\n            buttons_per_row,\n        ))\n        .build();\n\n    for bttn in config.buttons.iter() {\n        let justify = match bttn.justify {\n            WButtonJustify::Center => gtk4::Justification::Center,\n            WButtonJustify::Fill => gtk4::Justification::Fill,\n            WButtonJustify::Left => gtk4::Justification::Left,\n            WButtonJustify::Right => gtk4::Justification::Right,\n        };\n\n        let button = gtk4::Button::builder()\n            .name(&bttn.label)\n            .hexpand(true)\n            .vexpand(true)\n            .cursor(&gdk4::Cursor::from_name(\"pointer\", None).expect(\"pointer cursor not found\"))\n            .build();\n\n        let overlay = gtk4::Overlay::builder().vexpand(true).hexpand(true).build();\n\n        if config.show_keybinds {\n            let key_label = gtk4::Label::builder()\n                .label(format!(\"[{}]\", bttn.keybind))\n                .halign(gtk4::Align::Start)\n                .valign(gtk4::Align::Start)\n                .css_classes([\"dimmed\", \"keybind\"])\n                .build();\n\n            overlay.add_overlay(&key_label);\n        }\n\n        let inner = gtk4::Box::builder()\n            .orientation(gtk4::Orientation::Vertical)\n            .valign(gtk4::Align::Center)\n            .build();\n\n        let picture = if let Some(icon) = &bttn.icon {\n            let picture = if icon.ends_with(\".svg\") {\n                svg_picture_colorized(icon).upcast()\n            } else {\n                gtk4::Picture::for_filename(icon)\n            };\n\n            picture.set_content_fit(gtk4::ContentFit::ScaleDown);\n            picture.add_css_class(\"icon-dropshadow\");\n\n            inner.append(&picture);\n            Some(picture)\n        } else {\n            None\n        };\n\n        let label = gtk4::Label::builder()\n            .label(&bttn.text)\n            .css_classes([\"action-name\"])\n            .use_markup(true)\n            .justify(justify)\n            .build();\n\n        // Picture being none means the old system to configure buttons is used\n        if bttn.width.is_some() || bttn.height.is_some() || picture.is_none() {\n            label.set_xalign(bttn.width.unwrap_or(0.5));\n            label.set_yalign(bttn.height.unwrap_or(0.9));\n            overlay.add_overlay(&label);\n        } else {\n            inner.insert_child_after(&label, picture.as_ref());\n        }\n\n        overlay.set_child(Some(&inner));\n\n        if bttn.circular {\n            button.add_css_class(\"circular\");\n        }\n\n        button.connect_clicked(clone!(\n            #[weak]\n            window,\n            #[to_owned(rename_to = action)]\n            &bttn.action,\n            #[to_owned(rename_to = delay_ms)]\n            &config.delay_command_ms,\n            #[upgrade_or_panic]\n            move |_| on_option(&action, delay_ms, service_mode, window)\n        ));\n\n        button.set_child(Some(&overlay));\n\n        buttons_container.append(&button);\n    }\n\n    container_box.set_shrink_center_last(false);\n    container_box.set_center_widget(Some(&buttons_container));\n\n    if !config.no_version_info {\n        let version_info = gtk4::Label::builder()\n        .label(format!(\n            \"Wleave {}. <a href=\\\"https://github.com/AMNatty/wleave/releases/tag/0.6.0\\\">Missing or broken icons?</a>\",\n            env!(\"CARGO_PKG_VERSION\")\n        ))\n        .use_markup(true)\n        .can_focus(false)\n        .css_classes([\"dimmed\", \"version-info\"])\n        .margin_top(12)\n        .build();\n        container_box.set_end_widget(Some(&version_info));\n    }\n\n    window\n}\n"
  },
  {
    "path": "src/app/window.rs",
    "content": "mod window_impl {\n    use glib::object::ObjectExt;\n    use glib::subclass::object::DerivedObjectProperties;\n    use glib::subclass::object::{ObjectImpl, ObjectImplExt};\n    use glib::subclass::types::{ObjectSubclass, ObjectSubclassExt};\n    use glib_macros::Properties;\n    use gtk4::prelude::WidgetExt;\n    use gtk4::subclass::application_window::ApplicationWindowImpl;\n    use gtk4::subclass::widget::{WidgetImpl, WidgetImplExt};\n    use gtk4::subclass::window::WindowImpl;\n    use libadwaita::subclass::application_window::AdwApplicationWindowImpl;\n    use std::cell::Cell;\n\n    #[derive(Properties, Default)]\n    #[properties(wrapper_type = super::WleaveWindow)]\n    pub struct WleaveWindowImpl {\n        #[property(name = \"window-width\", get, set)]\n        pub window_width: Cell<i32>,\n        #[property(name = \"window-height\", get, set)]\n        pub window_height: Cell<i32>,\n    }\n\n    #[glib::object_subclass]\n    impl ObjectSubclass for WleaveWindowImpl {\n        const NAME: &'static str = \"WleaveWindow\";\n\n        type Type = super::WleaveWindow;\n\n        type ParentType = libadwaita::ApplicationWindow;\n\n        type Interfaces = ();\n    }\n\n    #[glib::derived_properties]\n    impl ObjectImpl for WleaveWindowImpl {\n        fn constructed(&self) {\n            self.parent_constructed();\n\n            let obj = self.obj();\n            obj.set_window_width(obj.width());\n            obj.set_window_height(obj.height());\n        }\n    }\n\n    impl WidgetImpl for WleaveWindowImpl {\n        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {\n            self.parent_size_allocate(width, height, baseline);\n\n            let obj = self.obj();\n            obj.set_window_width(obj.width());\n            obj.set_window_height(obj.height());\n        }\n    }\n    impl WindowImpl for WleaveWindowImpl {}\n    impl ApplicationWindowImpl for WleaveWindowImpl {}\n    impl AdwApplicationWindowImpl for WleaveWindowImpl {}\n}\n\nglib::wrapper! {\n    pub struct WleaveWindow(ObjectSubclass<window_impl::WleaveWindowImpl>)\n        @extends libadwaita::ApplicationWindow, gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,\n        @implements gtk4::gio::ActionGroup, gtk4::gio::ActionMap, gtk4::Accessible,\n                    gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root,\n                    gtk4::ShortcutManager;\n}\n\nimpl WleaveWindow {\n    pub fn new(app: &libadwaita::Application) -> Self {\n        glib::Object::builder()\n            .property(\"application\", app)\n            .property(\"title\", \"wleave\")\n            .property(\"window-width\", 400)\n            .property(\"window-height\", 400)\n            .build()\n    }\n}\n"
  },
  {
    "path": "src/button.rs",
    "content": "use serde::de::{IntoDeserializer, SeqAccess, Visitor};\nuse serde::{Deserialize, Deserializer};\nuse std::collections::BTreeMap;\nuse std::convert::Infallible;\nuse std::str::FromStr;\nuse tracing::warn;\n\n#[derive(Debug, Clone, Deserialize)]\npub enum WButtonActionHandler {\n    #[serde(rename = \"shell\")]\n    Shell(String),\n    #[serde(rename = \"executable\")]\n    Executable(String),\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct WButtonAction {\n    #[serde(flatten)]\n    pub handler: WButtonActionHandler,\n    #[serde(flatten)]\n    pub conditions: BTreeMap<String, String>,\n}\n\nimpl WButtonAction {\n    pub fn is_applicable(&self) -> bool {\n        if let WButtonActionHandler::Executable(exe) = &self.handler\n            && let Err(e) = which::which(exe)\n        {\n            warn!(\"Executable {} not available, skipping: {}\", exe, e);\n            return false;\n        }\n\n        for (key, value) in self.conditions.iter() {\n            if let Some(env_var) = key.strip_prefix(\"$\") {\n                let Ok(var) = std::env::var(env_var) else {\n                    return false;\n                };\n\n                if var != *value {\n                    return false;\n                }\n            }\n        }\n\n        true\n    }\n}\n\nimpl FromStr for WButtonAction {\n    type Err = Infallible;\n\n    fn from_str(action: &str) -> Result<Self, Self::Err> {\n        Ok(WButtonAction {\n            handler: WButtonActionHandler::Shell(action.to_string()),\n            conditions: Default::default(),\n        })\n    }\n}\n\nstruct WButtonActionVisitor;\n\nimpl<'de> Visitor<'de> for WButtonActionVisitor {\n    type Value = WButtonAction;\n\n    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {\n        formatter.write_str(\"string or map\")\n    }\n\n    fn visit_str<E>(self, value: &str) -> Result<WButtonAction, E>\n    where\n        E: serde::de::Error,\n    {\n        Ok(FromStr::from_str(value).expect(\"shell script always valid\"))\n    }\n\n    fn visit_string<E>(self, val: String) -> Result<Self::Value, E>\n    where\n        E: serde::de::Error,\n    {\n        self.visit_str(&val)\n    }\n\n    fn visit_map<M>(self, map: M) -> Result<WButtonAction, M::Error>\n    where\n        M: serde::de::MapAccess<'de>,\n    {\n        Deserialize::deserialize(serde::de::value::MapAccessDeserializer::new(map))\n    }\n}\n\nstruct WButtonActionWrapper(WButtonAction);\n\nimpl<'de> Deserialize<'de> for WButtonActionWrapper {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        Ok(WButtonActionWrapper(\n            deserializer.deserialize_any(WButtonActionVisitor)?,\n        ))\n    }\n}\n\n#[derive(Debug, Clone)]\npub enum WButtonActionList {\n    Single(WButtonAction),\n    Multiple(Vec<WButtonAction>),\n}\n\nstruct WButtonActionListVisitor;\n\nimpl<'de> Visitor<'de> for WButtonActionListVisitor {\n    type Value = WButtonActionList;\n\n    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {\n        formatter.write_str(\"one or multiple actions\")\n    }\n\n    fn visit_str<E>(self, value: &str) -> Result<WButtonActionList, E>\n    where\n        E: serde::de::Error,\n    {\n        Ok(WButtonActionList::Single(\n            WButtonActionWrapper::deserialize(value.into_deserializer())?.0,\n        ))\n    }\n\n    fn visit_seq<M>(self, mut seq: M) -> Result<Self::Value, M::Error>\n    where\n        M: SeqAccess<'de>,\n    {\n        let mut actions = Vec::new();\n\n        while let Some(WButtonActionWrapper(value)) = seq.next_element::<WButtonActionWrapper>()? {\n            actions.push(value);\n        }\n\n        Ok(WButtonActionList::Multiple(actions))\n    }\n\n    fn visit_map<M>(self, map: M) -> Result<WButtonActionList, M::Error>\n    where\n        M: serde::de::MapAccess<'de>,\n    {\n        Ok(WButtonActionList::Single(WButtonAction::deserialize(\n            serde::de::value::MapAccessDeserializer::new(map),\n        )?))\n    }\n}\n\nimpl<'de> Deserialize<'de> for WButtonActionList {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        deserializer.deserialize_any(WButtonActionListVisitor)\n    }\n}\n\nimpl WButtonActionList {\n    pub fn enumerate(&self) -> impl Iterator<Item = &WButtonAction> {\n        match self {\n            WButtonActionList::Single(action) => std::slice::from_ref(action).iter(),\n            WButtonActionList::Multiple(actions) => actions.iter(),\n        }\n    }\n}\n\n#[derive(Debug, Default)]\npub enum WButtonJustify {\n    #[default]\n    Center,\n    Fill,\n    Left,\n    Right,\n}\n\nimpl<'de> Deserialize<'de> for WButtonJustify {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        Ok(FromStr::from_str(<&str>::deserialize(deserializer)?).expect(\"never fails\"))\n    }\n}\n\nimpl FromStr for WButtonJustify {\n    type Err = Infallible;\n\n    fn from_str(val: &str) -> Result<Self, Self::Err> {\n        Ok(match val {\n            \"center\" => WButtonJustify::Center,\n            \"fill\" => WButtonJustify::Fill,\n            \"left\" => WButtonJustify::Left,\n            \"right\" => WButtonJustify::Right,\n            _ => WButtonJustify::Center,\n        })\n    }\n}\n\n#[derive(Debug, Deserialize)]\npub struct WButton {\n    pub label: String,\n    pub action: WButtonActionList,\n    pub text: String,\n    pub keybind: String,\n    #[serde(default)]\n    pub justify: WButtonJustify,\n    pub width: Option<f32>,\n    pub height: Option<f32>,\n    #[serde(default = \"default_circular\")]\n    pub circular: bool,\n    #[serde(default)]\n    pub icon: Option<String>,\n}\n\nfn default_circular() -> bool {\n    false\n}\n"
  },
  {
    "path": "src/cli_opt.rs",
    "content": "use crate::units::{AspectRatio, LengthValue, Margin};\nuse clap::{ArgAction, Parser, ValueEnum};\nuse serde::{Deserialize, Deserializer};\nuse std::{\n    error::Error,\n    fmt::{Debug, Display},\n    num::NonZeroU32,\n    path::PathBuf,\n    str::FromStr,\n};\n\n#[derive(Debug, Copy, Clone, Default, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum Protocol {\n    #[default]\n    LayerShell,\n    None,\n    Xdg,\n}\n\n#[derive(Debug, Copy, Clone, Default, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum MenuLayoutStrategy {\n    #[default]\n    Grid,\n}\n\n#[derive(Parser, Debug)]\n#[command(author, version, disable_version_flag = true, about, long_about = None)]\npub struct Args {\n    #[arg(short = 'v', long, action = ArgAction::Version)]\n    pub version: Option<bool>,\n\n    /// Run the application in service mode - it will stay in the background until triggered\n    #[arg(short = 's', long, num_args = 0..=1, require_equals = true, default_missing_value = \"true\"\n    )]\n    pub service: Option<bool>,\n\n    /// Specify a layout file, specifying - will read the layout config from stdin\n    #[arg(short = 'l', long)]\n    pub layout: Option<PathBuf>,\n\n    /// Specify the way the buttons should be laid out in the available space\n    #[arg(long)]\n    pub button_layout: Option<MenuLayoutStrategy>,\n\n    /// Specify a custom CSS file\n    #[arg(short = 'C', long)]\n    pub css: Option<PathBuf>,\n\n    /// Set the number of buttons per row, or use a fraction to specify the number of rows to be\n    /// used (e.g. \"1/1\" for all buttons in a single row, \"1/5\" to distribute the buttons over 5 rows)\n    #[arg(short = 'b', long, value_parser = clap::value_parser!(ButtonLayout))]\n    pub buttons_per_row: Option<ButtonLayout>,\n\n    /// Set space between buttons columns\n    #[arg(short = 'c', long)]\n    pub column_spacing: Option<LengthValue>,\n\n    /// Set space between buttons rows\n    #[arg(short = 'r', long)]\n    pub row_spacing: Option<LengthValue>,\n\n    /// Set the margin around buttons\n    #[arg(short = 'm', long)]\n    pub margin: Option<Margin>,\n\n    /// Set margin for the left of buttons\n    #[arg(short = 'L', long)]\n    pub margin_left: Option<Margin>,\n\n    /// Set margin for the right of buttons\n    #[arg(short = 'R', long)]\n    pub margin_right: Option<Margin>,\n\n    /// Set margin for the top of buttons\n    #[arg(short = 'T', long)]\n    pub margin_top: Option<Margin>,\n\n    /// Set the margin for the bottom of buttons\n    #[arg(short = 'B', long)]\n    pub margin_bottom: Option<Margin>,\n\n    /// Set the aspect ratio of the buttons.\n    #[arg(short = 'A', long)]\n    pub button_aspect_ratio: Option<AspectRatio>,\n\n    /// The delay (in milliseconds) between the window closing and executing the selected option\n    #[arg(short = 'd', long)]\n    pub delay_command_ms: Option<u32>,\n\n    /// Close the menu on lost focus\n    #[arg(short = 'f', long, num_args = 0..=1, require_equals = true, default_missing_value = \"true\"\n    )]\n    pub close_on_lost_focus: Option<bool>,\n\n    /// Show the associated key binds\n    #[arg(short = 'k', long, num_args = 0..=1, require_equals = true, default_missing_value = \"true\"\n    )]\n    pub show_keybinds: Option<bool>,\n\n    /// Use layer-shell or xdg protocol\n    #[arg(short = 'p', long, value_enum)]\n    pub protocol: Option<Protocol>,\n\n    /// Hide version information\n    #[arg(short = 'x', long, num_args = 0..=1, require_equals = true, default_missing_value = \"true\"\n    )]\n    pub no_version_info: Option<bool>,\n}\n\n#[derive(Clone, Copy, Debug, Default)]\npub enum ButtonLayout {\n    #[default]\n    Auto,\n    PerRow(u32),\n    RowRatio(u32, u32),\n}\n\nimpl<'de> Deserialize<'de> for ButtonLayout {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        let s = String::deserialize(deserializer)?;\n        FromStr::from_str(&s).map_err(serde::de::Error::custom)\n    }\n}\n\nimpl FromStr for ButtonLayout {\n    type Err = Box<dyn Error + Send + Sync + 'static>;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        if s == \"auto\" {\n            return Ok(ButtonLayout::Auto);\n        }\n\n        if let Ok(per_row) = s.parse::<NonZeroU32>() {\n            return Ok(ButtonLayout::PerRow(per_row.into()));\n        }\n\n        if let Some((n, d)) = s.split_once(\"/\")\n            && let (Ok(n), Ok(d)) = (n.parse::<NonZeroU32>(), d.parse::<NonZeroU32>())\n        {\n            return Ok(ButtonLayout::RowRatio(n.into(), d.into()));\n        }\n\n        Err(\"Value neither a number (1, 2, 3) nor a ratio (1/1, 2/3, ...)\".into())\n    }\n}\n\nimpl Display for ButtonLayout {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Auto => f.write_str(\"auto\"),\n            Self::PerRow(r) => write!(f, \"{r}\"),\n            Self::RowRatio(n, d) => write!(f, \"{n}/{d}\"),\n        }\n    }\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use crate::button::WButton;\nuse crate::error::WError;\nuse convert_case::ccase;\nuse gdk4::gio;\nuse gtk4::CssProvider;\nuse miette::{NamedSource, Report, SourceOffset};\nuse serde::Deserialize;\nuse std::borrow::Cow;\nuse std::io::Read;\nuse std::path::{Path, PathBuf};\nuse tracing::{Level, debug, enabled, error, info, warn};\nuse wleave::cli_opt::{Args, ButtonLayout, MenuLayoutStrategy, Protocol};\nuse wleave::units::{AspectRatio, LengthValue, Margin};\n\n#[derive(Debug, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub struct AppConfig {\n    #[serde(default)]\n    pub service: bool,\n    #[serde(default)]\n    pub button_layout: MenuLayoutStrategy,\n    pub margin_left: Option<Margin>,\n    pub margin_right: Option<Margin>,\n    pub margin_top: Option<Margin>,\n    pub margin_bottom: Option<Margin>,\n    #[serde(default = \"default_margin\")]\n    pub margin: Margin,\n    #[serde(default = \"default_spacing\")]\n    pub column_spacing: LengthValue,\n    #[serde(default = \"default_spacing\")]\n    pub row_spacing: LengthValue,\n    pub button_aspect_ratio: Option<AspectRatio>,\n    #[serde(default = \"default_delay\")]\n    pub delay_command_ms: u32,\n    #[serde(default)]\n    pub protocol: Protocol,\n    #[serde(default)]\n    pub buttons_per_row: ButtonLayout,\n    #[serde(default)]\n    pub close_on_lost_focus: bool,\n    pub buttons: Vec<WButton>,\n    #[serde(default)]\n    pub show_keybinds: bool,\n    #[serde(default)]\n    pub no_version_info: bool,\n    pub css: Option<PathBuf>,\n}\n\nimpl Default for AppConfig {\n    fn default() -> Self {\n        AppConfig {\n            service: false,\n            button_layout: MenuLayoutStrategy::Grid,\n            margin_left: None,\n            margin_right: None,\n            margin_top: None,\n            margin_bottom: None,\n            margin: default_margin(),\n            column_spacing: default_spacing(),\n            row_spacing: default_spacing(),\n            button_aspect_ratio: None,\n            delay_command_ms: default_delay(),\n            protocol: Default::default(),\n            buttons_per_row: Default::default(),\n            close_on_lost_focus: false,\n            buttons: vec![],\n            show_keybinds: false,\n            no_version_info: false,\n            css: None,\n        }\n    }\n}\n\nfn default_margin() -> Margin {\n    Margin(LengthValue::Percentage(0.2))\n}\n\nfn default_spacing() -> LengthValue {\n    LengthValue::Px(8.0)\n}\n\nfn default_delay() -> u32 {\n    100\n}\n\nfn file_search_given(given_file: impl AsRef<Path>) -> Result<PathBuf, WError> {\n    let file = given_file.as_ref();\n    if !file.is_file() {\n        return Err(WError::SpecifiedPathNotAFile(file.to_owned()));\n    }\n\n    Ok(file.to_owned())\n}\n\npub fn file_search_path(file_name: impl AsRef<Path>) -> Result<PathBuf, WError> {\n    let file_name = file_name.as_ref();\n    let user_config_dir = dirs::config_dir()\n        .or_else(|| dirs::home_dir().map(|p| p.join(\".config\")))\n        .unwrap_or_else(|| Path::new(\"~/.config\").to_owned());\n\n    for path in &[\n        &user_config_dir.join(\"wleave\"),\n        &user_config_dir.join(\"wlogout\"),\n        Path::new(\"/etc/wleave\"),\n        Path::new(\"/etc/wlogout\"),\n        Path::new(\"/usr/local/etc/wleave\"),\n        Path::new(\"/usr/local/etc/wlogout\"),\n    ] {\n        let full_path = path.join(file_name);\n        if full_path.is_file() {\n            debug!(\"File found in: {}\", full_path.display());\n            return Ok(full_path);\n        } else {\n            debug!(\"No file found in: {}\", full_path.display());\n        }\n    }\n\n    Err(WError::FileNotInSearchPath(file_name.to_owned()))\n}\n\nfn parse_config(input: impl Read, source_path: Cow<Path>) -> Result<AppConfig, WError> {\n    let path = source_path.into_owned();\n    let path_name = path.display().to_string();\n    info!(\"Reading options from: {}\", path_name);\n    let config = std::io::read_to_string(input).map_err(|e| WError::IoError(path, e))?;\n\n    let new = serde_json::de::from_str::<AppConfig>(&config).map_err(|e| {\n        WError::FileParseFailed(\n            NamedSource::new(path_name.clone(), config.to_owned()),\n            SourceOffset::from_location(&config, e.line(), e.column()),\n            e,\n        )\n    });\n\n    let legacy = serde_json::Deserializer::from_str(&config)\n        .into_iter::<WButton>()\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(|e| {\n            WError::FileParseFailed(\n                NamedSource::new(path_name, config.to_owned()),\n                SourceOffset::from_location(&config, e.line(), e.column()),\n                e,\n            )\n        })\n        .map(|buttons| AppConfig {\n            buttons,\n            ..Default::default()\n        });\n\n    match (new, legacy) {\n        (Ok(conf), _) => {\n            info!(\"Using the JSON layout format.\");\n            Ok(conf)\n        }\n        (Err(e), Ok(legacy)) => {\n            debug!(\"The JSON format could not be parsed: {:?}\", Report::from(e));\n            info!(\"Using the backwards-compatible layout format.\");\n            if !enabled!(Level::DEBUG) {\n                warn!(\n                    \"If this is not intended, run the application with RUST_LOG=debug to show the JSON parse error.\"\n                );\n            }\n\n            Ok(legacy)\n        }\n        (Err(e), Err(_)) => {\n            error!(\"{:?}\", e);\n\n            Err(e)\n        }\n    }\n}\n\npub fn load_config(file: Option<&impl AsRef<Path>>) -> Result<AppConfig, WError> {\n    if let Some(\"-\") = file.map(AsRef::as_ref).and_then(Path::to_str) {\n        return parse_config(std::io::stdin(), Path::new(\"<stdin>\").into());\n    }\n\n    let file_path = file.map(file_search_given).unwrap_or_else(|| {\n        file_search_path(\"layout.json\").or_else(|_| file_search_path(\"layout\"))\n    })?;\n\n    let input =\n        std::fs::File::open(&file_path).map_err(|e| WError::IoError(file_path.clone(), e))?;\n    parse_config(input, file_path.into())\n}\n\npub fn load_css(file: Option<impl AsRef<Path>>) -> Result<CssProvider, WError> {\n    let path = file\n        .map(file_search_given)\n        .unwrap_or_else(|| file_search_path(\"style.css\"))?;\n\n    let provider = CssProvider::new();\n    provider.connect_parsing_error(|_provider, _section, error| {\n        warn!(\"CSS Parse error: {:?}\", error);\n    });\n    provider.load_from_file(&gio::File::for_path(&path));\n\n    Ok(provider)\n}\n\nmacro_rules! merge_option {\n    ($conf:ident, $args:ident, $name:ident => $val:expr) => {\n        if let Some($name) = $args.$name {\n            info!(\n                \"\\\"{}\\\" specified from args: {:?}\",\n                ccase!(snake -> kebab, stringify!($name)),\n                $name\n            );\n            $conf.$name = $val;\n        } else {\n            info!(\n                \"\\\"{}\\\" specified from config: {:?}\",\n                ccase!(snake -> kebab, stringify!($name)),\n                $conf.$name\n            );\n        }\n    };\n    ($conf:ident, $args:ident, $name:ident) => {\n        merge_option!($conf, $args, $name => $name)\n    };\n}\n\npub fn merge_with_args(config: &mut AppConfig, args: Args) {\n    merge_option!(config, args, service);\n    merge_option!(config, args, button_layout);\n    merge_option!(config, args, margin_top => Some(margin_top));\n    merge_option!(config, args, margin_bottom => Some(margin_bottom));\n    merge_option!(config, args, margin_left => Some(margin_left));\n    merge_option!(config, args, margin_right => Some(margin_right));\n    merge_option!(config, args, margin);\n    merge_option!(config, args, protocol);\n    merge_option!(config, args, column_spacing);\n    merge_option!(config, args, row_spacing);\n    merge_option!(config, args, button_aspect_ratio => Some(button_aspect_ratio));\n    merge_option!(config, args, show_keybinds);\n    merge_option!(config, args, close_on_lost_focus);\n    merge_option!(config, args, buttons_per_row);\n    merge_option!(config, args, no_version_info);\n    merge_option!(config, args, delay_command_ms);\n\n    if let Some(css) = args.css.clone() {\n        info!(\n            \"\\\"css\\\" file specified from args: {:?}\",\n            Path::display(&css)\n        );\n        config.css = Some(css);\n    } else {\n        info!(\n            \"\\\"css\\\" file specified from config: {:?}\",\n            config.css.as_deref().map(Path::display)\n        );\n    }\n}\n"
  },
  {
    "path": "src/error.rs",
    "content": "use miette::{Diagnostic, NamedSource, SourceOffset};\nuse std::path::PathBuf;\nuse thiserror::Error;\n\n#[derive(Error, Diagnostic, Debug)]\npub enum WError {\n    #[error(\"Failed to load the specified configuration file {0} as it does not exist\")]\n    SpecifiedPathNotAFile(PathBuf),\n    #[error(\"Failed to find the configuration file {0} in the search path\")]\n    FileNotInSearchPath(PathBuf),\n    #[error(\"An error has occurred while reading file {0}: {1}\")]\n    IoError(PathBuf, std::io::Error),\n    #[error(\"JSON parsing failed\")]\n    #[diagnostic(code(wleave::parse_failed))]\n    FileParseFailed(\n        #[source_code] NamedSource<String>,\n        #[label(\"The parser failed here\")] SourceOffset,\n        #[source] serde_json::Error,\n    ),\n}\n"
  },
  {
    "path": "src/exec.rs",
    "content": "use crate::button::{WButtonAction, WButtonActionHandler};\nuse std::process::Command;\nuse tracing::error;\n\npub fn run_command(command: WButtonAction) {\n    match command.handler {\n        WButtonActionHandler::Executable(exe) => {\n            if let Err(e) = Command::new(exe).spawn() {\n                error!(\"Execution error: {e}\");\n            }\n        }\n        WButtonActionHandler::Shell(script) => {\n            if let Err(e) = Command::new(\"sh\").args([\"-c\", &script]).spawn() {\n                error!(\"Execution error: {e}\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/layout.rs",
    "content": "use crate::layout::menu_layout::LayoutMenuImpl;\nuse crate::layout::menu_layout_child::MenuLayoutChildImpl;\nuse glib::object::Cast;\nuse glib::subclass::types::ObjectSubclassIsExt;\nuse gtk4::prelude::{LayoutManagerExt, WidgetExt};\nuse libadwaita::gtk;\nuse wleave::cli_opt::MenuLayoutStrategy;\n\nmod menu_layout_child {\n    use gdk4::prelude::ObjectExt;\n    use glib::subclass::object::{DerivedObjectProperties, ObjectImpl};\n    use glib::subclass::types::ObjectSubclass;\n    use glib_macros::Properties;\n    use gtk4::subclass::layout_child::LayoutChildImpl;\n    use std::cell::Cell;\n\n    #[derive(Properties, Default)]\n    #[properties(wrapper_type = super::MenuLayoutChild)]\n    pub struct MenuLayoutChildImpl {\n        #[property(\n            name = \"placement\",\n            get,\n            set,\n            builder(super::MenuLayoutChildPlacement::default())\n        )]\n        pub placement: Cell<super::MenuLayoutChildPlacement>,\n        #[property(name = \"grid-x\", get, set)]\n        pub grid_x: Cell<u32>,\n        #[property(name = \"grid-y\", get, set)]\n        pub grid_y: Cell<u32>,\n    }\n\n    #[glib::object_subclass]\n    impl ObjectSubclass for MenuLayoutChildImpl {\n        const NAME: &'static str = \"MenuLayoutChild\";\n\n        type Type = super::MenuLayoutChild;\n\n        type ParentType = gtk4::LayoutChild;\n\n        type Interfaces = ();\n    }\n\n    #[glib::derived_properties]\n    impl ObjectImpl for MenuLayoutChildImpl {}\n\n    impl LayoutChildImpl for MenuLayoutChildImpl {}\n}\n\nglib::wrapper! {\n    pub struct MenuLayoutChild(ObjectSubclass<MenuLayoutChildImpl>)\n        @extends gtk4::LayoutChild;\n}\n\nimpl MenuLayoutChild {\n    fn new(\n        manager: &MenuLayout,\n        child: &gtk4::Widget,\n        placement: MenuLayoutChildPlacement,\n    ) -> MenuLayoutChild {\n        glib::Object::builder()\n            .property(\"layout-manager\", manager)\n            .property(\"child-widget\", child)\n            .property(\"placement\", placement)\n            .build()\n    }\n}\n\n#[derive(Default, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, glib::Enum)]\n#[enum_type(name = \"MenuLayoutChildPlacement\")]\npub enum MenuLayoutChildPlacement {\n    #[default]\n    Unknown,\n    Grid,\n}\n\nmod menu_layout {\n    use crate::layout::{MenuLayoutChild, MenuLayoutChildPlacement, MenuLayoutProvider};\n    use gdk4::prelude::ObjectExt;\n    use gdk4::subclass::prelude::DerivedObjectProperties;\n    use glib::object::Cast;\n    use glib::subclass::object::ObjectImpl;\n    use glib::subclass::prelude::ObjectSubclassExt;\n    use glib::subclass::types::ObjectSubclass;\n    use glib::types::StaticType;\n    use glib_macros::Properties;\n    use gtk4::prelude::WidgetExt;\n    use gtk4::subclass::layout_manager::LayoutManagerImpl;\n    use std::cell::{Cell, RefCell};\n    use tracing::instrument;\n\n    #[derive(Properties, Default)]\n    #[properties(wrapper_type = super::MenuLayout)]\n    pub struct LayoutMenuImpl {\n        #[property(name = \"aspect-ratio\", get, set)]\n        aspect_ratio: Cell<f64>,\n        #[property(name = \"buttons-per-row\", get, set)]\n        buttons_per_row: Cell<u32>,\n        #[property(name = \"aspect-ratio-set\", get, set)]\n        aspect_ratio_set: Cell<bool>,\n        #[property(name = \"column-spacing\", get, set)]\n        column_spacing: Cell<f64>,\n        #[property(name = \"row-spacing\", get, set)]\n        row_spacing: Cell<f64>,\n        pub(super) layout_strategy: RefCell<MenuLayoutProvider>,\n    }\n\n    #[glib::object_subclass]\n    impl ObjectSubclass for LayoutMenuImpl {\n        const NAME: &'static str = \"MenuLayout\";\n\n        type Type = super::MenuLayout;\n\n        type ParentType = gtk4::LayoutManager;\n\n        type Interfaces = ();\n    }\n\n    #[glib::derived_properties]\n    impl ObjectImpl for LayoutMenuImpl {}\n\n    impl LayoutManagerImpl for LayoutMenuImpl {\n        #[instrument(skip(self, widget))]\n        fn allocate(&self, widget: &gtk4::Widget, width: i32, height: i32, baseline: i32) {\n            {\n                let mut layout = self.layout_strategy.borrow_mut();\n\n                layout.column_spacing = self.column_spacing.get();\n                layout.row_spacing = self.row_spacing.get();\n                layout.aspect_ratio = self.aspect_ratio_set.get().then(|| self.aspect_ratio.get());\n            }\n\n            let layout = self.layout_strategy.borrow();\n\n            let mut curr = widget.first_child();\n            let children = std::iter::from_fn(|| {\n                let it = curr.take()?;\n                curr = it.next_sibling();\n                Some(it)\n            })\n            .collect::<Vec<_>>();\n\n            layout.allocate(&self.obj(), &children, width, height, baseline);\n        }\n\n        fn create_layout_child(\n            &self,\n            _widget: &gtk4::Widget,\n            for_child: &gtk4::Widget,\n        ) -> gtk4::LayoutChild {\n            MenuLayoutChild::new(\n                &self.obj(),\n                for_child,\n                match self.layout_strategy.borrow().strategy {\n                    super::MenuLayoutStrategy::Grid => MenuLayoutChildPlacement::Grid,\n                },\n            )\n            .upcast()\n        }\n\n        fn layout_child_type() -> Option<glib::Type> {\n            Some(MenuLayoutChild::static_type())\n        }\n\n        fn request_mode(&self, _widget: &gtk4::Widget) -> gtk4::SizeRequestMode {\n            gtk4::SizeRequestMode::HeightForWidth\n        }\n\n        #[instrument(skip(self, widget))]\n        fn measure(\n            &self,\n            widget: &gtk4::Widget,\n            orientation: gtk4::Orientation,\n            for_size: i32,\n        ) -> (i32, i32, i32, i32) {\n            let mut curr = widget.first_child();\n            let children = std::iter::from_fn(|| {\n                let it = curr.take()?;\n                curr = it.next_sibling();\n                Some(it)\n            })\n            .collect::<Vec<_>>();\n\n            let layout = self.layout_strategy.borrow();\n\n            layout.measure(&self.obj(), &children, orientation, for_size)\n        }\n    }\n}\n\nglib::wrapper! {\n    pub struct MenuLayout(ObjectSubclass<LayoutMenuImpl>)\n        @extends gtk4::LayoutManager;\n}\n\nimpl MenuLayout {\n    pub fn new(\n        button_layout: MenuLayoutStrategy,\n        ratio: Option<impl Into<f64>>,\n        column_spacing: gtk4::Expression,\n        row_spacing: gtk4::Expression,\n        buttons_per_row: Option<impl Into<u32>>,\n    ) -> Self {\n        let obj: MenuLayout = glib::Object::builder()\n            .property(\"aspect-ratio-set\", ratio.is_some())\n            .property(\"aspect-ratio\", ratio.map(Into::into).unwrap_or(1.0))\n            .property(\n                \"buttons-per-row\",\n                buttons_per_row.map(Into::into).unwrap_or_default(),\n            )\n            .build();\n\n        column_spacing.bind(&obj, \"column-spacing\", glib::Object::NONE);\n        row_spacing.bind(&obj, \"row-spacing\", glib::Object::NONE);\n\n        let imp = obj.imp();\n        imp.layout_strategy.borrow_mut().strategy = button_layout;\n\n        obj\n    }\n}\n\n#[derive(Default)]\nstruct MenuLayoutProvider {\n    strategy: MenuLayoutStrategy,\n    column_spacing: f64,\n    row_spacing: f64,\n    aspect_ratio: Option<f64>,\n}\n\nimpl MenuLayoutProvider {\n    fn allocate(\n        &self,\n        obj: &MenuLayout,\n        children: &[gtk4::Widget],\n        width: i32,\n        height: i32,\n        baseline: i32,\n    ) {\n        if children.is_empty() {\n            return;\n        }\n\n        match self.strategy {\n            MenuLayoutStrategy::Grid => {\n                let n = children.len();\n                let col_spacing = (self.column_spacing as i32).max(0) as usize;\n                let row_spacing = (self.row_spacing as i32).max(0) as usize;\n\n                let mut rows = 1;\n                let mut cols = 1;\n                let mut b_width = 0.0;\n                let mut b_height = 0.0;\n\n                let u_width = width as usize;\n                let u_height = height as usize;\n\n                let per_row = obj.buttons_per_row() as usize;\n                // We use 0 for \"auto\" placement\n                let cols_range = if per_row != 0 {\n                    // Try layouts where all buttons either fit into one row or exactly \"per_row\"\n                    per_row.min(n)..=per_row\n                } else {\n                    // Try all possible layouts\n                    1..=n\n                };\n\n                // Axis-aligned rectangle packing\n                // We brute-force the best layout, optimizing for max button area\n                for i_rows in 1..=n {\n                    for j_cols in cols_range.clone() {\n                        if (i_rows * j_cols > n + i_rows || i_rows * j_cols > n + j_cols)\n                            && per_row == 0\n                            || i_rows * j_cols < n\n                        {\n                            continue;\n                        }\n\n                        let col_gaps = j_cols - 1;\n                        let row_gaps = i_rows - 1;\n\n                        let (w, h) = match self.aspect_ratio {\n                            Some(aspect @ 1.0..) => {\n                                let mut w = u_width.saturating_sub(col_gaps * col_spacing) as f64\n                                    / j_cols as f64\n                                    * aspect;\n                                let h = (u_height.saturating_sub(row_gaps * row_spacing) as f64\n                                    / i_rows as f64)\n                                    .min(w / aspect);\n\n                                w = h * aspect;\n\n                                (w, h)\n                            }\n                            Some(aspect @ ..1.0) => {\n                                let mut h = u_height.saturating_sub(row_gaps * row_spacing) as f64\n                                    / i_rows as f64;\n                                let w = (u_width.saturating_sub(col_gaps * col_spacing) as f64\n                                    / j_cols as f64\n                                    * aspect)\n                                    .min(h * aspect);\n\n                                h = w / aspect;\n\n                                (w, h)\n                            }\n                            //\n                            Some(..) | None => {\n                                let w = u_width.saturating_sub(col_gaps * col_spacing) as f64\n                                    / j_cols as f64;\n                                let h = u_height.saturating_sub(row_gaps * row_spacing) as f64\n                                    / i_rows as f64;\n\n                                (w, h)\n                            }\n                        };\n\n                        if w * h > b_width * b_height {\n                            rows = i_rows;\n                            cols = j_cols;\n                            b_width = w;\n                            b_height = h;\n                        }\n                    }\n                }\n\n                let base_x =\n                    (width as f64 - (cols - 1) as f64 * (col_spacing as f64 + b_width) - b_width)\n                        / 2.0;\n                let base_y = (height as f64\n                    - (rows - 1) as f64 * (row_spacing as f64 + b_height)\n                    - b_height)\n                    / 2.0;\n\n                for (i, child) in children.iter().enumerate() {\n                    let child_layout = obj\n                        .layout_child(child)\n                        .downcast::<MenuLayoutChild>()\n                        .expect(\"always MenuLayoutChild\");\n\n                    if child.should_layout() {\n                        let ix = i % cols;\n                        let iy = i / cols;\n\n                        child_layout.set_placement(MenuLayoutChildPlacement::Grid);\n                        child_layout.set_grid_x(ix as u32);\n                        child_layout.set_grid_y(iy as u32);\n\n                        let x_grid = ix as f64;\n                        let y_grid = iy as f64;\n\n                        let x = base_x\n                            + x_grid * b_width\n                            + x_grid * self.column_spacing * self.aspect_ratio.unwrap_or(1.0);\n                        let y = base_y + y_grid * b_height + y_grid * self.row_spacing;\n\n                        child.size_allocate(\n                            &gtk4::Allocation::new(\n                                x as i32,\n                                y as i32,\n                                b_width as i32,\n                                b_height as i32,\n                            ),\n                            baseline,\n                        );\n                    }\n                }\n            }\n        }\n    }\n\n    fn measure(\n        &self,\n        _obj: &MenuLayout,\n        children: &[gtk4::Widget],\n        orientation: gtk4::Orientation,\n        for_size: i32,\n    ) -> (i32, i32, i32, i32) {\n        let gaps = match orientation {\n            gtk::Orientation::Vertical => self.row_spacing as i32,\n            gtk::Orientation::Horizontal => self.column_spacing as i32,\n            _ => 0,\n        };\n\n        let (mut min, mut nat) = (0, 0);\n\n        match self.strategy {\n            MenuLayoutStrategy::Grid => {\n                for child in children.iter() {\n                    if !child.should_layout() {\n                        continue;\n                    }\n\n                    let (c_min, c_nat, _, _) = child.measure(orientation, for_size);\n\n                    min = min.max(c_min + gaps);\n                    nat = nat.max(c_nat + gaps);\n                }\n\n                // A bit of a messy heuristic\n                if matches!(orientation, gtk::Orientation::Horizontal) {\n                    min *= children.len() as i32;\n                    nat *= children.len() as i32;\n                } else {\n                    min *= children.len() as i32;\n                    min /= 2;\n                    nat *= children.len() as i32;\n                    nat /= 2;\n                }\n            }\n        }\n\n        (min, nat, -1, -1)\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "pub mod cli_opt;\npub mod units;\n"
  },
  {
    "path": "src/main.rs",
    "content": "mod app;\nmod button;\nmod config;\nmod error;\nmod exec;\nmod layout;\nmod paintable;\n\nuse clap::Parser;\nuse glib::clone;\nuse std::sync::Arc;\nuse tracing::{Level, error};\nuse tracing_subscriber::EnvFilter;\nuse tracing_subscriber::layer::SubscriberExt;\nuse tracing_subscriber::util::SubscriberInitExt;\n\nuse crate::app::create_app;\nuse crate::config::{AppConfig, load_config, load_css, merge_with_args};\nuse gtk4::gdk::Display;\nuse gtk4::prelude::*;\nuse wleave::cli_opt::Args;\n\nfn on_startup(config: &AppConfig) {\n    let display = Display::default().expect(\"Could not connect to a display\");\n\n    match load_css(config.css.as_ref()) {\n        Ok(css) => gtk4::style_context_add_provider_for_display(\n            &display,\n            &css,\n            gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,\n        ),\n        Err(e) => error!(\"Failed to load CSS: {e}\"),\n    };\n}\n\nfn entry_point(config: Arc<AppConfig>) -> miette::Result<()> {\n    let mut flags = gtk4::gio::ApplicationFlags::empty();\n    flags.set(gtk4::gio::ApplicationFlags::IS_SERVICE, config.service);\n\n    let app = libadwaita::Application::builder()\n        .application_id(\"sh.natty.Wleave\")\n        .flags(flags)\n        .build();\n\n    app.connect_startup(clone!(\n        #[strong]\n        config,\n        move |_| on_startup(config.as_ref())\n    ));\n\n    let hold_guard = if config.service {\n        Some(app.hold())\n    } else {\n        None\n    };\n\n    app.connect_activate(move |app| {\n        let _ = &hold_guard;\n\n        let app_window = create_app(&config, app);\n        app_window.present();\n    });\n\n    app.run_with_args(&[] as &[&str]);\n\n    Ok(())\n}\n\nfn main() -> miette::Result<()> {\n    tracing_subscriber::registry()\n        .with(tracing_subscriber::fmt::layer().without_time())\n        .with(\n            EnvFilter::builder()\n                .with_default_directive(Level::INFO.into())\n                .from_env_lossy(),\n        )\n        .init();\n\n    let args = Args::parse();\n\n    let mut config = load_config(args.layout.as_ref())?;\n    merge_with_args(&mut config, args);\n\n    let config = Arc::new(config);\n    entry_point(config)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/paintable.rs",
    "content": "use gdk4::prelude::*;\nuse gdk4::subclass::paintable::PaintableImpl;\nuse glib::subclass::ObjectImplRef;\nuse glib::subclass::prelude::*;\nuse glib::translate::IntoGlib;\nuse glib_macros::Properties;\nuse gtk4::prelude::*;\nuse gtk4::subclass::prelude::SymbolicPaintableImpl;\nuse miette::miette;\nuse std::cell::{Cell, RefCell};\nuse tracing::error;\n\n#[derive(Properties, Default)]\n#[properties(wrapper_type = PicturePaintable)]\npub struct PicturePaintableImpl {\n    #[property(name = \"image-path\", get, set)]\n    image_path: RefCell<String>,\n    #[property(name = \"scale-factor\", get, set)]\n    scale_factor: Cell<i32>,\n    widget: RefCell<Option<glib::WeakRef<gtk4::Widget>>>,\n    handle: RefCell<Option<rsvg::SvgHandle>>,\n    texture: RefCell<Option<gdk4::MemoryTexture>>,\n    _symbolic_updated: Cell<bool>,\n}\n\n#[glib::object_subclass]\nimpl ObjectSubclass for PicturePaintableImpl {\n    const NAME: &'static str = \"WleavePicturePaintable\";\n\n    type Type = PicturePaintable;\n\n    type ParentType = glib::Object;\n\n    type Interfaces = (gdk4::Paintable, gtk4::SymbolicPaintable);\n}\n\nglib::wrapper! {\n    pub struct PicturePaintable(ObjectSubclass<PicturePaintableImpl>)\n        @implements gdk4::Paintable, gtk4::SymbolicPaintable;\n}\n\n#[glib::derived_properties]\nimpl ObjectImpl for PicturePaintableImpl {\n    fn constructed(&self) {\n        self.parent_constructed();\n\n        let obj = self.obj();\n        let impl_ref = ObjectImplRef::new(self).downgrade();\n        obj.connect_notify_local(Some(\"image-path\"), move |pict, _| {\n            let Some(obj) = impl_ref.upgrade() else {\n                return;\n            };\n\n            obj.texture.take();\n\n            *obj.handle.borrow_mut() = match rsvg::Loader::new()\n                .read_path(pict.image_path())\n                .map_err(|e| miette!(\"Failed to read SVG: {}\", e))\n            {\n                Ok(handle) => Some(handle),\n                Err(e) => {\n                    error!(\"{}\", e);\n                    None\n                }\n            };\n        });\n    }\n}\n\nimpl PicturePaintableImpl {\n    fn draw(&self, width: f64, height: f64) {\n        let scale: f64 = self.scale_factor.get().into();\n        let width = width * scale;\n        let height = height * scale;\n\n        let mut tex_borrow = self.texture.borrow_mut();\n        if tex_borrow.is_some() {\n            return;\n        };\n\n        let Some(handle_ref) = &*self.handle.borrow() else {\n            return;\n        };\n\n        let renderer = rsvg::CairoRenderer::new(handle_ref);\n\n        let mut surface =\n            match cairo::ImageSurface::create(cairo::Format::ARgb32, width as i32, height as i32)\n                .map_err(|e| miette!(\"Failed to create a Cairo surface: {}\", e))\n            {\n                Ok(surf) => surf,\n                Err(e) => {\n                    error!(\"{}\", e);\n                    return;\n                }\n            };\n\n        let ctx = match cairo::Context::new(&surface)\n            .map_err(|e| miette!(\"Failed to create a Cairo context: {}\", e))\n        {\n            Ok(ctx) => ctx,\n            Err(e) => {\n                error!(\"{}\", e);\n                return;\n            }\n        };\n\n        if let Err(e) = renderer\n            .render_document(&ctx, &cairo::Rectangle::new(0.0, 0.0, width, height))\n            .map_err(|e| miette!(\"Failed to render SVG: {}\", e))\n        {\n            error!(\"{}\", e);\n            return;\n        }\n\n        drop(ctx);\n\n        let data = match surface\n            .data()\n            .map_err(|e| miette!(\"Failed to take Cairo image data: {}\", e))\n        {\n            Ok(data) => data,\n            Err(e) => {\n                error!(\"{}\", e);\n                return;\n            }\n        };\n\n        let bytes = glib::Bytes::from(data.as_ref());\n\n        cfg_if::cfg_if! {\n            if #[cfg(target_endian = \"little\")] {\n                let format = gdk4::MemoryFormat::B8g8r8a8;\n            } else {\n                let format = gdk4::MemoryFormat::A8r8g8b8;\n            }\n        }\n\n        *tex_borrow = Some(gdk4::MemoryTexture::new(\n            width as i32,\n            height as i32,\n            format,\n            &bytes,\n            size_of::<i32>() * width as usize,\n        ));\n    }\n}\n\nimpl PaintableImpl for PicturePaintableImpl {\n    fn current_image(&self) -> gdk4::Paintable {\n        self.draw(\n            self.intrinsic_width() as f64,\n            self.intrinsic_height() as f64,\n        );\n\n        let Some(tex_ref) = &*self.texture.borrow() else {\n            return gdk4::Paintable::new_empty(self.intrinsic_width(), self.intrinsic_height());\n        };\n\n        gdk4::Paintable::from(tex_ref.clone())\n    }\n\n    fn flags(&self) -> gdk4::PaintableFlags {\n        gdk4::PaintableFlags::empty()\n    }\n\n    fn intrinsic_width(&self) -> i32 {\n        let Some(handle_ref) = &*self.handle.borrow() else {\n            return 256;\n        };\n\n        let renderer = rsvg::CairoRenderer::new(handle_ref);\n        let size = renderer\n            .intrinsic_size_in_pixels()\n            .map(|(w, _)| w.ceil() as i32);\n\n        size.unwrap_or(256)\n    }\n\n    fn intrinsic_height(&self) -> i32 {\n        let Some(handle_ref) = &*self.handle.borrow() else {\n            return 256;\n        };\n\n        let renderer = rsvg::CairoRenderer::new(handle_ref);\n        let size = renderer\n            .intrinsic_size_in_pixels()\n            .map(|(_, h)| h.ceil() as i32);\n\n        size.unwrap_or(256)\n    }\n\n    fn intrinsic_aspect_ratio(&self) -> f64 {\n        let Some(handle_ref) = &*self.handle.borrow() else {\n            return 1.0;\n        };\n\n        let renderer = rsvg::CairoRenderer::new(handle_ref);\n        let size = renderer.intrinsic_size_in_pixels().map(|(w, h)| w / h);\n\n        size.unwrap_or(1.0)\n    }\n\n    fn snapshot(&self, snapshot: &gdk4::Snapshot, width: f64, height: f64) {\n        if !self._symbolic_updated.get() {\n            if let Some(w) = &*self.widget.borrow()\n                && let Some(widget) = w.upgrade()\n            {\n                self.snapshot_symbolic(snapshot, width, height, &[widget.color()]);\n            } else {\n                self.snapshot_symbolic(snapshot, width, height, &[]);\n            }\n        }\n\n        self.draw(width, height);\n\n        let Some(tex_ref) = &*self.texture.borrow() else {\n            return;\n        };\n\n        SnapshotExt::append_texture(\n            snapshot,\n            tex_ref,\n            &gtk4::graphene::Rect::new(0.0, 0.0, width as f32, height as f32),\n        );\n\n        self._symbolic_updated.set(false);\n    }\n}\n\nimpl SymbolicPaintableImpl for PicturePaintableImpl {\n    fn snapshot_symbolic(\n        &self,\n        snapshot: &gdk4::Snapshot,\n        width: f64,\n        height: f64,\n        colors: &[gdk4::RGBA],\n    ) {\n        let mut handle_borrow = self.handle.borrow_mut();\n        let Some(handle_ref) = &mut *handle_borrow else {\n            return;\n        };\n\n        let col_idx = gtk4::SymbolicColor::Foreground.into_glib();\n\n        let col = colors.get(col_idx as usize).unwrap_or(&gdk4::RGBA::BLACK);\n\n        if let Err(e) = handle_ref\n            .set_stylesheet(&format!(\n                r#\"\n                    svg {{\n                        color: {col} !important;\n                    }}\n                \"#\n            ))\n            .map_err(|e| miette!(\"Failed to set stylesheet for SVG while loading: {}\", e))\n        {\n            error!(\"{}\", e);\n            return;\n        }\n\n        drop(handle_borrow);\n\n        self.texture.take();\n        self._symbolic_updated.set(true);\n\n        self.snapshot(snapshot, width, height);\n    }\n}\n\nimpl PicturePaintable {\n    fn for_path(icon_path: impl Into<String>) -> Self {\n        glib::Object::builder()\n            .property(\"image-path\", icon_path.into())\n            .property(\"scale-factor\", 1)\n            .build()\n    }\n}\n\npub fn svg_picture_colorized(icon: &str) -> gtk4::Picture {\n    let paintable = PicturePaintable::for_path(icon);\n\n    let picture = gtk4::Picture::for_paintable(&paintable);\n    paintable.set_scale_factor(picture.scale_factor());\n    picture.connect_scale_factor_notify(move |pict| {\n        let Some(p) = pict.paintable() else {\n            return;\n        };\n\n        let Some(paintable) = p.downcast_ref::<PicturePaintable>() else {\n            return;\n        };\n\n        paintable.set_scale_factor(pict.scale_factor());\n    });\n\n    let internals = paintable.imp();\n    *internals.widget.borrow_mut() = Some(ObjectExt::downgrade(picture.as_ref()));\n\n    picture\n}\n"
  },
  {
    "path": "src/units.rs",
    "content": "use miette::miette;\nuse serde::{Deserialize, Deserializer};\nuse serde_json::Value;\nuse std::error::Error;\nuse std::fmt::Display;\nuse std::num::NonZeroU32;\nuse std::str::FromStr;\n\n#[derive(Clone, Copy, Debug)]\npub enum AspectRatio {\n    Float(f32),\n    Ratio(u32, u32),\n}\n\nimpl Default for AspectRatio {\n    fn default() -> Self {\n        AspectRatio::Float(1.0)\n    }\n}\n\nimpl<'de> Deserialize<'de> for AspectRatio {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        let v = Value::deserialize(deserializer)?;\n        if let Some(f) = v.as_f64() {\n            Ok(AspectRatio::Float(f as f32))\n        } else if let Some(s) = v.as_str() {\n            FromStr::from_str(s).map_err(serde::de::Error::custom)\n        } else {\n            Err(serde::de::Error::custom(\n                \"Aspect ratio neither a positive float nor a ratio (1/1, 2/3, ...)\",\n            ))\n        }\n    }\n}\n\nimpl FromStr for AspectRatio {\n    type Err = Box<dyn Error + Send + Sync + 'static>;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        if let Ok(float) = s.parse::<f32>() {\n            if float < 0.0 {\n                return Err(\"Aspect ratio cannot be negative\".into());\n            }\n            return Ok(AspectRatio::Float(float));\n        }\n\n        if let Some((n, d)) = s.split_once('/')\n            && let (Ok(n), Ok(d)) = (n.parse::<NonZeroU32>(), d.parse::<NonZeroU32>())\n        {\n            return Ok(AspectRatio::Ratio(n.into(), d.into()));\n        }\n\n        Err(\"Aspect ratio neither a float nor a ratio\".into())\n    }\n}\n\nimpl Display for AspectRatio {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Float(r) => write!(f, \"{r}\"),\n            Self::Ratio(n, d) => write!(f, \"{n}/{d}\"),\n        }\n    }\n}\n\nimpl AspectRatio {\n    pub fn as_float(self) -> f32 {\n        match self {\n            Self::Float(f) => f,\n            Self::Ratio(n, d) => (n as f32) / (d as f32),\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub enum LengthValue {\n    Px(f32),\n    Percentage(f32),\n}\n\n#[derive(Debug, Deserialize)]\n#[serde(untagged)]\nenum LengthSerialized {\n    Number(f32),\n    String(String),\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum LengthDimension {\n    Vertical,\n    Horizontal,\n}\n\n#[derive(Debug, Clone)]\npub struct LengthArgs {\n    pub viewport: (f32, f32),\n    pub dimension: LengthDimension,\n}\n\nimpl<'de> Deserialize<'de> for LengthValue {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        match LengthSerialized::deserialize(deserializer)? {\n            LengthSerialized::Number(num) => Ok(LengthValue::Px(num)),\n            LengthSerialized::String(val) => val.parse().map_err(serde::de::Error::custom),\n        }\n    }\n}\n\nimpl LengthValue {\n    pub fn for_args(&self, args: &LengthArgs) -> f32 {\n        match self {\n            LengthValue::Percentage(p) => match args.dimension {\n                LengthDimension::Horizontal => *p * args.viewport.0,\n                LengthDimension::Vertical => *p * args.viewport.1,\n            },\n            LengthValue::Px(val) => *val,\n        }\n    }\n\n    fn parse(val: &'_ str) -> Result<Self, cssparser::BasicParseError<'_>> {\n        let mut input = cssparser::ParserInput::new(val);\n        let mut parser = cssparser::Parser::new(&mut input);\n\n        let token = parser.next()?;\n\n        let value = match token {\n            cssparser::Token::Number { value, .. } => LengthValue::Px(*value),\n            cssparser::Token::Percentage { unit_value, .. } => LengthValue::Percentage(*unit_value),\n            cssparser::Token::Dimension { value, unit, .. } => match unit.as_ref() {\n                \"px\" => LengthValue::Px(*value),\n                _ => {\n                    let tok = token.clone();\n                    return Err(parser\n                        .current_source_location()\n                        .new_basic_unexpected_token_error(tok));\n                }\n            },\n            _ => {\n                let tok = token.clone();\n                return Err(parser\n                    .current_source_location()\n                    .new_basic_unexpected_token_error(tok));\n            }\n        };\n\n        parser.expect_exhausted()?;\n\n        Ok(value)\n    }\n}\n\nimpl FromStr for LengthValue {\n    type Err = miette::Report;\n\n    fn from_str(val: &str) -> Result<Self, Self::Err> {\n        LengthValue::parse(val).map_err(|e| miette!(\"Length parse error: {:?}\", e))\n    }\n}\n\n#[derive(Debug, Clone, Deserialize)]\n#[serde(transparent)]\npub struct Margin(pub LengthValue);\n\nimpl FromStr for Margin {\n    type Err = miette::Report;\n\n    fn from_str(val: &str) -> Result<Self, Self::Err> {\n        LengthValue::from_str(val)\n            .map(Margin)\n            .map_err(|e| miette!(\"Margin parse error: {:?}\", e))\n    }\n}\n"
  },
  {
    "path": "style.css",
    "content": "/**\n * See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html for more CSS variables.\n * Or use your own colors. Both work, but using your own colors may break the automatic light/dark theme function.\n */\n\n\nwindow {\n    background-color: rgba(12, 12, 12, 0.8);\n}\n\nbutton {\n    color: oklab(from var(--view-fg-color) var(--standalone-color-oklab));\n    background-color: var(--view-bg-color);\n    border: none;\n    padding: 10px;\n}\n\nbutton label.action-name {\n    font-size: 24px;\n    font-weight: 400;\n}\n\nbutton label.keybind {\n    font-size: 20px;\n    font-family: monospace;\n}\n\nbutton:hover label.keybind, button:focus label.keybind {\n    opacity: 1;\n}\n\nbutton:focus,\nbutton:hover {\n    background-color: color-mix(in srgb, var(--accent-bg-color), var(--view-bg-color));\n}\n\nbutton:active {\n    color: var(--accent-fg-color);\n    background-color: var(--accent-bg-color);\n}\n\nbutton#shutdown {\n    --view-fg-color: #ff8d8d;\n}\n\nbutton#hibernate {\n    --view-fg-color: #a8c0ff;\n}\n\nbutton#reboot {\n    --view-fg-color: #84ffaa;\n}\n\nbutton#lock {\n    --view-fg-color: #ffe8b6;\n}\n\nbutton#logout {\n    --view-fg-color: #ffcca8;\n}\n\nbutton#suspend {\n    --view-fg-color: #caaff9;\n}\n"
  }
]