diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f8f68a1..bb86387 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,20 +6,13 @@ on: - v[0-9]+.[0-9]+.[0-9]+ jobs: - deploy: + release: + name: 'Create Release' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Install build deps - run: ./.github/scripts/ubuntu_setup.sh - - name: Update CHANGELOG id: changelog uses: Requarks/changelog-action@v1 @@ -41,9 +34,54 @@ jobs: uses: stefanzweifel/git-auto-commit-action@v4 with: branch: master - commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]' + commit_message: 'chore: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]' file_pattern: CHANGELOG.md - - uses: katyo/publish-crates@v1 + + publish-crate: + name: 'Publish Crate' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Install build deps + run: ./.github/scripts/ubuntu_setup.sh + + - name: Publish crate + uses: katyo/publish-crates@v1 with: registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + + publish-schema: + name: 'Publish Schema' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: Swatinem/rust-cache@v2 + name: Cache dependencies + + - name: Install build deps + run: ./.github/scripts/ubuntu_setup.sh + + - name: Build schema + run: cargo build --features schema -- --print-schema > target/schema-${{ github.ref_name }}.json + + - name: Copy file via SSH + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SSH_HOST }} + port: ${{ secrets.SSH_PORT }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + source: "target/schema-${{ github.ref_name }}.json" + target: /storage/Public/github/ironbar + strip_components: 1 \ No newline at end of file diff --git a/.github/workflows/schema.yml b/.github/workflows/schema.yml new file mode 100644 index 0000000..be79ca2 --- /dev/null +++ b/.github/workflows/schema.yml @@ -0,0 +1,41 @@ +name: Publish Schema + +on: + workflow_dispatch: + push: + branches: [ "master" ] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: '-Dwarnings' + +jobs: + publish-schema: + name: 'Publish Schema' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: Swatinem/rust-cache@v2 + name: Cache dependencies + + - name: Install build deps + run: ./.github/scripts/ubuntu_setup.sh + + - name: Build + run: cargo build --features schema + + - name: Print schema + run: cargo run --features schema -- --print-schema > target/schema.json + + - name: Copy file via SSH + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SSH_HOST }} + port: ${{ secrets.SSH_PORT }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + source: "target/schema.json" + target: /storage/Public/github/ironbar + strip_components: 1 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f709497..cf81ffb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,9 +437,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -456,9 +456,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -466,9 +466,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -478,9 +478,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -761,6 +761,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.8.1" @@ -891,9 +897,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1446,6 +1452,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1542,9 +1565,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1637,10 +1660,11 @@ dependencies = [ "mlua", "mpd-utils", "mpris", - "nix 0.28.0", + "nix 0.29.0", "notify", "regex", "reqwest", + "schemars", "serde", "serde_json", "smithay-client-toolkit", @@ -1875,9 +1899,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e340c022072f3208a4105458286f4985ba5355bfe243c3073afe45cbe9ecf491" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" dependencies = [ "bstr", "mlua-sys", @@ -1888,9 +1912,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5552e7e4e22ada0463dfdeee6caf6dc057a189fdc83136408a8f950a5e5c5540" +checksum = "a088ed0723df7567f569ba018c5d48c23c501f3878b190b04144dfa5ebfa8abc" dependencies = [ "cc", "cfg-if", @@ -1991,9 +2015,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.4.0", "cfg-if", @@ -2203,9 +2227,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" @@ -2490,9 +2514,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2534,9 +2558,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.0", "bytes", @@ -2547,6 +2571,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -2572,6 +2597,20 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "ron" version = "0.8.1" @@ -2592,9 +2631,9 @@ checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc_version" @@ -2632,6 +2671,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -2648,6 +2700,17 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +[[package]] +name = "rustls-webpki" +version = "0.102.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.13" @@ -2672,6 +2735,30 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote 1.0.35", + "serde_derive_internals", + "syn 2.0.48", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2715,18 +2802,29 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote 1.0.35", + "syn 2.0.48", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote 1.0.35", @@ -2899,6 +2997,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2926,6 +3030,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "subtle" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" + [[package]] name = "swayipc-async" version = "2.0.1" @@ -2986,9 +3096,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "synom" @@ -3127,9 +3237,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -3146,9 +3256,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote 1.0.35", @@ -3165,6 +3275,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.7" @@ -3445,6 +3566,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "upower_dbus" version = "0.3.2" @@ -3458,9 +3585,9 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -4102,6 +4229,12 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zvariant" version = "3.15.0" diff --git a/Cargo.toml b/Cargo.toml index f22cb7f..a29150b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,12 +79,14 @@ workspaces = ["futures-lite"] "workspaces+sway" = ["workspaces", "swayipc-async"] "workspaces+hyprland" = ["workspaces", "hyprland"] +schema = ["dep:schemars"] + [dependencies] # core gtk = "0.18.1" gtk-layer-shell = "0.8.0" glib = "0.18.5" -tokio = { version = "1.37.0", features = [ +tokio = { version = "1.38.0", features = [ "macros", "rt-multi-thread", "time", @@ -99,7 +101,7 @@ tracing-error = { version = "0.2.0" , default-features = false } tracing-appender = "0.2.3" strip-ansi-escapes = "0.2.0" color-eyre = "0.6.3" -serde = { version = "1.0.202", features = ["derive"] } +serde = { version = "1.0.203", features = ["derive"] } indexmap = "2.2.6" dirs = "5.0.1" walkdir = "2.5.0" @@ -109,29 +111,29 @@ wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [ "calloop", ] } -universal-config = { version = "0.5.0", default_features = false } +universal-config = { version = "0.5.0", default-features = false } ctrlc = "3.4.2" cfg-if = "1.0.0" # cli -clap = { version = "4.5.4", optional = true, features = ["derive"] } +clap = { version = "4.5.7", optional = true, features = ["derive"] } # ipc serde_json = { version = "1.0.117", optional = true } # http -reqwest = { version = "0.12.4", default_features = false, features = ["default-tls", "http2"], optional = true } +reqwest = { version = "0.12.5", default-features = false, features = ["default-tls", "http2"], optional = true } # cairo lua-src = { version = "546.0.2", optional = true } -mlua = { version = "0.9.8", optional = true, features = ["luajit"] } +mlua = { version = "0.9.9", optional = true, features = ["luajit"] } cairo-rs = { version = "0.18.5", optional = true, features = ["png"] } # clipboard -nix = { version = "0.28.0", optional = true, features = ["event", "fs"] } +nix = { version = "0.29.0", optional = true, features = ["event", "fs"] } # clock -chrono = { version = "0.4.38", optional = true, default_features = false, features = ["clock", "unstable-locales"] } +chrono = { version = "0.4.38", optional = true, default-features = false, features = ["clock", "unstable-locales"] } # music mpd-utils = { version = "0.2.1", optional = true } @@ -159,7 +161,10 @@ futures-util = { version = "0.3.30", optional = true } # shared futures-lite = { version = "2.3.0", optional = true } # networkmanager, upower, workspaces -regex = { version = "1.10.4", default-features = false, features = [ +regex = { version = "1.10.5", default-features = false, features = [ "std", ], optional = true } # music, sys_info zbus = { version = "3.15.2", default-features = false, features = ["tokio"], optional = true } # networkmanager, notifications, upower + +# schema +schemars = { version = "0.8.21", optional = true } diff --git a/README.md b/README.md index 67809c3..a529e1b 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,15 @@ A flake is included with the repo which can be used with Home Manager. CI builds are automatically cached by Garnix. You can use their binary cache by following the steps [here](https://garnix.io/docs/caching). +### Fedora + +[fedora package](https://copr.fedorainfracloud.org/coprs/victorvintorez/tilingtools/packages/) + +``` sh +dnf copr enable victorvintorez/tilingtools +dnf install ironbar +``` + ### Void Linux [void package](https://github.com/void-linux/void-packages/tree/master/srcpkgs/ironbar) diff --git a/docs/Compiling.md b/docs/Compiling.md index 6b1c9a7..6112885 100644 --- a/docs/Compiling.md +++ b/docs/Compiling.md @@ -101,6 +101,9 @@ cargo build --release --no-default-features \ | workspaces+all | Enables the `workspaces` module with support for all compositors. | | workspaces+sway | Enables the `workspaces` module with support for Sway. | | workspaces+hyprland | Enables the `workspaces` module with support for Hyprland. | +| **Other** | | +| schema | Enables JSON schema support and the CLI `--print-schema` flag. | + ## Speeding up compiling diff --git a/docs/Configuration guide.md b/docs/Configuration guide.md index 5edf0b1..25e1b22 100644 --- a/docs/Configuration guide.md +++ b/docs/Configuration guide.md @@ -20,11 +20,18 @@ Ironbar supports a range of configuration formats, so you can pick your favourit - `config.json` - `config.toml` - `config.yaml` -- `config.corn` (Experimental, includes variable support for re-using blocks. +- `config.corn` (Includes variable support for re-using blocks. See [here](https://github.com/jakestanger/corn) for info) You can also override the default config path using the `IRONBAR_CONFIG` environment variable. +A hosted schema is available for the latest Git version ~~and each versioned release~~. +JSON and YAML both support schema checking by adding the `$schema` key +to the top level of your config. + +- master: `https://f.jstanger.dev/github/ironbar/schema.json` +- ~~release: `https://f.jstanger.dev/github/ironbar/schema-v0.16.0.json`~~ *(Not released yet)* + ## 2. Pick your use-case Ironbar gives you a few ways to configure the bar to suit your needs. @@ -288,23 +295,25 @@ The following table lists each of the top-level bar config options: The following table lists each of the bar-level bar config options: -| Name | Type | Default | Description | -|-------------------|----------------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| `name` | `string` | `bar-` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. | -| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | -| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. | -| `height` | `integer` | `42` | The bar's height in pixels. | -| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. | -| `margin.top` | `integer` | `0` | The margin on the top of the bar | -| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar | -| `margin.left` | `integer` | `0` | The margin on the left of the bar | -| `margin.right` | `integer` | `0` | The margin on the right of the bar | -| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. | -| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. | -| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. | -| `start` | `Module[]` | `[]` | Array of left or top modules. | -| `center` | `Module[]` | `[]` | Array of center modules. | -| `end` | `Module[]` | `[]` | Array of right or bottom modules. | +| Name | Type | Default | Description | +|-------------------|------------------------------------------------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| `name` | `string` | `bar-` | A unique identifier for the bar, used for controlling it over IPC. If not set, uses a generated integer suffix. | +| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. | +| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. | +| `height` | `integer` | `42` | The bar's height in pixels. | +| `margin.top` | `integer` | `0` | The margin on the top of the bar | +| `margin.bottom` | `integer` | `0` | The margin on the bottom of the bar | +| `margin.left` | `integer` | `0` | The margin on the left of the bar | +| `margin.right` | `integer` | `0` | The margin on the right of the bar | +| `layer` | `background` or `bottom` or `top` or `overlay` | `top` | The layer-shell layer to place the bar on. | +| `exclusive_zone` | `boolean` | `true` unless `start_hidden` is enabled. | Whether the bar should reserve an exclusive zone around it. | +| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. | +| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. | +| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. | +| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. | +| `start` | `Module[]` | `[]` | Array of left or top modules. | +| `center` | `Module[]` | `[]` | Array of center modules. | +| `end` | `Module[]` | `[]` | Array of right or bottom modules. | ### 3.2 Module-level options diff --git a/docs/Controlling Ironbar.md b/docs/Controlling Ironbar.md index f8f5eeb..3edfdcc 100644 --- a/docs/Controlling Ironbar.md +++ b/docs/Controlling Ironbar.md @@ -5,28 +5,38 @@ It also includes a command line interface, which can be used for interacting wit # CLI This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`. -You can also view help per-command, for example using `ironbar set --help`. +You can also view help per sub-command or command, for example using `ironbar var --help` or `ironbar var set --help`. -Responses are handled by writing their type to stdout, followed by any value starting on the next line. -Error responses are written to stderr in the same format. +The CLI supports plaintext and JSON output. Plaintext will: + +- Print `ok` for empty success responses +- Print the returned body for success responses +- Print `error` to followed by the error on the next line for error responses. This is printed to `stderr`. Example: ```shell -$ ironbar set subject world +$ ironbar var set subject world ok -$ ironbar get subject -ok +$ ironbar var get subject world + +$ ironbar var get foo +error +Variable not found ``` +All error responses will cause the CLI to exit code 3. + # IPC The server listens on a Unix socket. -This can usually be found at `/run/user/$UID/ironbar-ipc.sock`. +The path is printed on startup, and can usually be found at `/run/user/$UID/ironbar-ipc.sock`. -Commands and responses are sent as JSON objects, denoted by their `type` key. +Commands and responses are sent as JSON objects. + +Commands will have a `command` key, and a `subcommand` key when part of a sub-command. The message buffer is currently limited to `1024` bytes. Particularly large messages will be truncated or cause an error. @@ -47,7 +57,7 @@ Responds with `ok`. ```json { - "type": "ping" + "command": "ping" } ``` @@ -59,7 +69,7 @@ Responds with `ok`. ```json { - "type": "inspect" + "command": "inspect" } ``` @@ -73,48 +83,7 @@ Responds with `ok`. ```json { - "type": "reload" -} -``` - -### `get` - -Gets an [ironvar](ironvars) value. - -Responds with `ok_value` if the value exists, otherwise `error`. - -```json -{ - "type": "get", - "key": "foo" -} -``` - -### `set` - -Sets an [ironvar](ironvars) value. - -Responds with `ok`. - -```json -{ - "type": "set", - "key": "foo", - "value": "bar" -} -``` - -### list - -Gets a list of all [ironvar](ironvars) values. - -Responds with `ok_value`. - -Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `. - -```json -{ - "type": "list" + "command": "reload" } ``` @@ -126,26 +95,113 @@ Responds with `ok` if the stylesheet exists, otherwise `error`. ```json { - "type": "load_css", + "command": "load_css", "path": "/path/to/style.css" } ``` -### `set_visible` +### `var` -Sets a bar's visibility. +Subcommand for controlling Ironvars. + +#### `get` + +Gets an [ironvar](ironvars) value. + +Responds with `ok_value` if the value exists, otherwise `error`. + +```json +{ + "command": "var", + "subcommand": "get", + "key": "foo" +} +``` + +#### `set` + +Sets an [ironvar](ironvars) value. + +Responds with `ok`. + +```json +{ + "command": "var", + "subcommand": "set", + "key": "foo", + "value": "bar" +} +``` + +#### `list` + +Gets a list of all [ironvar](ironvars) values. + +Responds with `ok_value`. + +Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `. + +```json +{ + "command": "var", + "subcommand": "list" +} +``` + +### `bar` + +#### `show` + +Forces a bar to be shown, regardless of the current visibility state. + +```json +{ + "command": "bar", + "subcommand": "show", + "name": "bar-123" +} +``` + +#### `hide` + +Forces a bar to be hidden, regardless of the current visibility state. + +```json +{ + "command": "bar", + "subcommand": "hide", + "name": "bar-123" +} +``` + +#### `set_visible` + +Sets a bar's visibility to one of shown/hidden. Responds with `ok` if the bar exists, otherwise `error`. ```json { - "type": "set_visible", - "bar_name": "bar-123", + "command": "bar", + "subcommand": "set_visible", + "name": "bar-123", "visible": true } ``` -### `get_visible` +#### `toggle_visible` + +Toggles the current visibility state of a bar between shown and hidden. + +```json +{ + "command": "bar", + "subcommand": "toggle_visible", + "name": "bar-123" +} +``` + +#### `get_visible` Gets a bar's visibility. @@ -153,54 +209,98 @@ Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists, ```json { - "type": "get_visible", - "bar_name": "bar-123" + "command": "bar", + "subcommand": "get_visible", + "name": "bar-123" } ``` -### `toggle_popup` - -Toggles the open/closed state for a module's popup. -Since each bar only has a single popup, any open popup on the bar is closed. - -Responds with `ok` if the popup exists, otherwise `error`. - -```json -{ - "type": "toggle_popup", - "bar_name": "bar-123", - "name": "clock" -} -``` - -### `open_popup` +#### `show_popup` Sets a module's popup open, regardless of its current state. Since each bar only has a single popup, any open popup on the bar is closed. -Responds with `ok` if the popup exists, otherwise `error`. +Responds with `ok` if the bar and widget exist, otherwise `error`. ```json { - "type": "open_popup", - "bar_name": "bar-123", - "name": "clock" + "command": "bar", + "subcommand": "show_popup", + "name": "bar-123", + "widget_name": "clock" } ``` -### `close_popup` +#### `hide_popup` Sets the popup on a bar closed, regardless of which module it is open for. -Responds with `ok` if the popup exists, otherwise `error`. +Responds with `ok` if the bar and widget exist, otherwise `error`. ```json { - "type": "close_popup", + "command": "bar", + "subcommand": "hide_popup", "bar_name": "bar-123" } ``` +#### `set_popup_visible` + +Sets a popup's visibility to one of shown/hidden. + +Responds with `ok` if the bar and widget exist, otherwise `error`. + +```json +{ + "command": "bar", + "subcommand": "set_popup_visible", + "name": "bar-123", + "widget_name": "clock", + "visible": true +} +``` + +#### `toggle_popup` + +Toggles the open/closed state for a module's popup. +Since each bar only has a single popup, any open popup on the bar is closed. + +Responds with `ok` if the bar and widget exist, otherwise `error`. + +```json +{ + "command": "bar", + "subcommand": "toggle_popup", + "bar_name": "bar-123", + "widget_name": "clock" +} +``` + +#### `get_popup_visible` + +Gets the popup's current visibility state. + +```json +{ + "command": "bar", + "subcommand": "get_popup_visible", + "bar_name": "bar-123" +} +``` + +#### `set_exclusive` + +Sets whether the bar reserves an exclusive zone. + +```json +{ + "command": "bar", + "subcommand": "set_exclusive", + "exclusive": true +} +``` + ## Responses ### `ok` diff --git a/examples/config.json b/examples/config.json index a54dd12..b85a4ff 100644 --- a/examples/config.json +++ b/examples/config.json @@ -1,4 +1,5 @@ { + "$schema": "https://f.jstanger.dev/github/ironbar/schema.json", "anchor_to_edges": true, "position": "bottom", "icon_theme": "Paper", diff --git a/examples/config.yaml b/examples/config.yaml index e05c4a1..86cb83d 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,3 +1,4 @@ +$schema: https://f.jstanger.dev/github/ironbar/schema.json anchor_to_edges: true position: bottom icon_theme: Paper diff --git a/flake.lock b/flake.lock index a1d9b2d..b304159 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1713979152, - "narHash": "sha256-apdecPuh8SOQnkEET/kW/UcfjCRb8JbV5BKjoH+DcP4=", + "lastModified": 1717025063, + "narHash": "sha256-dIubLa56W9sNNz0e8jGxrX3CAkPXsq7snuFA/Ie6dn8=", "owner": "ipetkov", "repo": "crane", - "rev": "a5eca68a2cf11adb32787fc141cddd29ac8eb79c", + "rev": "480dff0be03dac0e51a8dfc26e882b0d123a450e", "type": "github" }, "original": { @@ -43,11 +43,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1713520724, - "narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=", + "lastModified": 1717067539, + "narHash": "sha256-oIs5EF+6VpHJRvvpVWuqCYJMMVW/6h59aYUv9lABLtY=", "owner": "nix-community", "repo": "naersk", - "rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49", + "rev": "fa19d8c135e776dc97f4dcca08656a0eeb28d5c0", "type": "github" }, "original": { @@ -58,11 +58,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1714314149, - "narHash": "sha256-yNAevSKF4krRWacmLUsLK7D7PlfuY3zF0lYnGYNi9vQ=", + "lastModified": 1717112898, + "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae", + "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", "type": "github" }, "original": { @@ -72,11 +72,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1714253743, - "narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=", + "lastModified": 1716948383, + "narHash": "sha256-SzDKxseEcHR5KzPXLwsemyTR/kaM9whxeiJohbL04rs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994", + "rev": "ad57eef4ef0659193044870c731987a6df5cf56b", "type": "github" }, "original": { @@ -102,11 +102,11 @@ ] }, "locked": { - "lastModified": 1714443211, - "narHash": "sha256-lKTA3XqRo4aVgkyTSCtpcALpGXdmkilHTtN00eRg0QU=", + "lastModified": 1717121863, + "narHash": "sha256-/3sxIe7MZqF/jw1RTQCSmgTjwVod43mmrk84m50MJQ4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "ce35c36f58f82cee6ec959e0d44c587d64281b6f", + "rev": "2a7b53172ed08f856b8382d7dcfd36a4e0cbd866", "type": "github" }, "original": { diff --git a/src/bar.rs b/src/bar.rs index 4363806..2e9e7d2 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -120,27 +120,28 @@ impl Bar { self.name, self.monitor_name ); - self.setup_layer_shell( - &self.window, - true, - config.anchor_to_edges, - config.margin, - monitor, - ); - let start_hidden = config .start_hidden .unwrap_or_else(|| config.autohide.is_some()); + self.setup_layer_shell( + &self.window, + config.exclusive_zone.unwrap_or(!start_hidden), + config.anchor_to_edges, + config.margin, + config.layer, + monitor, + ); + if let Some(autohide) = config.autohide { let hotspot_window = Window::new(WindowType::Toplevel); - Self::setup_autohide(&self.window, &hotspot_window, autohide); self.setup_layer_shell( &hotspot_window, false, config.anchor_to_edges, config.margin, + gtk_layer_shell::Layer::Top, monitor, ); @@ -166,43 +167,46 @@ impl Bar { exclusive_zone: bool, anchor_to_edges: bool, margin: MarginConfig, + layer: gtk_layer_shell::Layer, monitor: &Monitor, ) { + use gtk_layer_shell::Edge; + let position = self.position; win.init_layer_shell(); win.set_monitor(monitor); - win.set_layer(gtk_layer_shell::Layer::Top); + win.set_layer(layer); win.set_namespace(env!("CARGO_PKG_NAME")); if exclusive_zone { win.auto_exclusive_zone_enable(); } - win.set_layer_shell_margin(gtk_layer_shell::Edge::Top, margin.top); - win.set_layer_shell_margin(gtk_layer_shell::Edge::Bottom, margin.bottom); - win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left); - win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right); + win.set_layer_shell_margin(Edge::Top, margin.top); + win.set_layer_shell_margin(Edge::Bottom, margin.bottom); + win.set_layer_shell_margin(Edge::Left, margin.left); + win.set_layer_shell_margin(Edge::Right, margin.right); let bar_orientation = position.orientation(); win.set_anchor( - gtk_layer_shell::Edge::Top, + Edge::Top, position == BarPosition::Top || (bar_orientation == Orientation::Vertical && anchor_to_edges), ); win.set_anchor( - gtk_layer_shell::Edge::Bottom, + Edge::Bottom, position == BarPosition::Bottom || (bar_orientation == Orientation::Vertical && anchor_to_edges), ); win.set_anchor( - gtk_layer_shell::Edge::Left, + Edge::Left, position == BarPosition::Left || (bar_orientation == Orientation::Horizontal && anchor_to_edges), ); win.set_anchor( - gtk_layer_shell::Edge::Right, + Edge::Right, position == BarPosition::Right || (bar_orientation == Orientation::Horizontal && anchor_to_edges), ); @@ -320,6 +324,23 @@ impl Bar { Inner::Loaded { popup } => popup.clone(), } } + + pub fn visible(&self) -> bool { + self.window.is_visible() + } + + /// Sets the window visibility status + pub fn set_visible(&self, visible: bool) { + self.window.set_visible(visible) + } + + pub fn set_exclusive(&self, exclusive: bool) { + if exclusive { + self.window.auto_exclusive_zone_enable(); + } else { + self.window.set_exclusive_zone(0); + } + } } /// Creates a `gtk::Box` container to place widgets inside. diff --git a/src/cli.rs b/src/cli.rs index d0f05f1..769d706 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,9 @@ +use crate::error::ExitCode; use crate::ipc::commands::Command; use crate::ipc::responses::Response; -use clap::Parser; +use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; +use std::process::exit; #[derive(Parser, Debug, Serialize, Deserialize)] #[command(version)] @@ -9,16 +11,50 @@ pub struct Args { #[command(subcommand)] pub command: Option, + /// Prints the config JSON schema to `stdout` + /// and exits. + #[cfg(feature = "schema")] + #[arg(long("print-schema"))] + pub print_schema: bool, + + /// Print debug information to stderr + /// TODO: Make bar follow this too + #[arg(long)] + pub debug: bool, + + /// Format to output the response as. + #[arg(short, long)] + pub format: Option, + /// `bar_id` argument passed by `swaybar_command`. /// Not used. #[arg(short('b'), hide(true))] sway_bar_id: Option, } -pub fn handle_response(response: Response) { - match response { - Response::Ok => println!("ok"), - Response::OkValue { value } => println!("ok\n{value}"), - Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()), +#[derive(Debug, Serialize, Deserialize, Default, ValueEnum, Clone, Copy)] +pub enum Format { + #[default] + Plain, + Json, +} + +pub fn handle_response(response: Response, format: Format) { + let is_err = matches!(response, Response::Err { .. }); + + match format { + Format::Plain => match response { + Response::Ok => println!("ok"), + Response::OkValue { value } => println!("{value}"), + Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()), + }, + Format::Json => println!( + "{}", + serde_json::to_string(&response).expect("to be valid json") + ), + } + + if is_err { + exit(ExitCode::IpcResponseError as i32) } } diff --git a/src/clients/swaync/mod.rs b/src/clients/swaync/mod.rs index 173cf73..495040b 100644 --- a/src/clients/swaync/mod.rs +++ b/src/clients/swaync/mod.rs @@ -14,7 +14,7 @@ pub struct Event { pub count: u32, pub dnd: bool, pub cc_open: bool, - pub inhibited: bool, + // pub inhibited: bool, } type GetSubscribeData = (bool, bool, u32, bool); @@ -22,12 +22,12 @@ type GetSubscribeData = (bool, bool, u32, bool); /// Converts the data returned from /// `get_subscribe_data` into an event for convenience. impl From for Event { - fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self { + fn from((dnd, cc_open, count, _inhibited): (bool, bool, u32, bool)) -> Self { Self { count, dnd, cc_open, - inhibited, + // inhibited, } } } diff --git a/src/clients/wayland/mod.rs b/src/clients/wayland/mod.rs index 8c7fcb8..ca7c367 100644 --- a/src/clients/wayland/mod.rs +++ b/src/clients/wayland/mod.rs @@ -2,8 +2,9 @@ mod macros; mod wl_output; mod wl_seat; -use crate::error::ERR_CHANNEL_RECV; +use crate::error::{ExitCode, ERR_CHANNEL_RECV}; use crate::{arc_mut, lock, register_client, send, spawn, spawn_blocking}; +use std::process::exit; use std::sync::{Arc, Mutex}; use calloop_channel::Event::Msg; @@ -305,6 +306,8 @@ impl Environment { "{:?}", Report::new(err).wrap_err("Failed to dispatch pending wayland events") ); + + exit(ExitCode::WaylandDispatchError as i32) } } } diff --git a/src/clients/wayland/wlr_foreign_toplevel/handle.rs b/src/clients/wayland/wlr_foreign_toplevel/handle.rs index 1cf1a14..91abfea 100644 --- a/src/clients/wayland/wlr_foreign_toplevel/handle.rs +++ b/src/clients/wayland/wlr_foreign_toplevel/handle.rs @@ -49,6 +49,7 @@ impl ToplevelHandleData { #[derive(Debug, Default)] pub struct ToplevelHandleDataInner { initial_done: bool, + closed: bool, output: Option, current_info: Option, @@ -137,14 +138,17 @@ where } Event::OutputEnter { output } => lock!(data.inner).output = Some(output), Event::OutputLeave { output: _ } => lock!(data.inner).output = None, - Event::Closed => state.remove_handle( - conn, - qh, - ToplevelHandle { - handle: handle.clone(), - }, - ), - Event::Done => { + Event::Closed => { + lock!(data.inner).closed = true; + state.remove_handle( + conn, + qh, + ToplevelHandle { + handle: handle.clone(), + }, + ) + } + Event::Done if !lock!(data.inner).closed => { { let pending_info = lock!(data.inner).pending_info.clone(); lock!(data.inner).current_info = Some(pending_info); diff --git a/src/config/common.rs b/src/config/common.rs index d1446e2..f1bc3c7 100644 --- a/src/config/common.rs +++ b/src/config/common.rs @@ -16,6 +16,7 @@ use tracing::trace; /// see [here](script). /// For information on styling, please see the [styling guide](styling-guide). #[derive(Debug, Default, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct CommonConfig { /// Sets the unique widget name, /// allowing you to target it in CSS using `#name`. @@ -160,6 +161,7 @@ pub struct CommonConfig { #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum TransitionType { None, Crossfade, @@ -169,6 +171,7 @@ pub enum TransitionType { #[derive(Debug, Default, Deserialize, Clone, Copy)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum ModuleOrientation { #[default] #[serde(alias = "h")] diff --git a/src/config/impl.rs b/src/config/impl.rs index a5b01b8..3c722c3 100644 --- a/src/config/impl.rs +++ b/src/config/impl.rs @@ -35,6 +35,37 @@ impl<'de> Deserialize<'de> for MonitorConfig { } } +pub fn deserialize_layer<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use gtk_layer_shell::Layer; + + let value = Option::::deserialize(deserializer)?; + value + .map(|v| match v.as_str() { + "background" => Ok(Layer::Background), + "bottom" => Ok(Layer::Bottom), + "top" => Ok(Layer::Top), + "overlay" => Ok(Layer::Overlay), + _ => Err(serde::de::Error::custom("invalid value for orientation")), + }) + .unwrap_or(Ok(Layer::Top)) +} + +#[cfg(feature = "schema")] +pub fn schema_layer(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + use schemars::JsonSchema; + let mut schema: schemars::schema::SchemaObject = ::json_schema(gen).into(); + schema.enum_values = Some(vec![ + "background".into(), + "bottom".into(), + "top".into(), + "overlay".into(), + ]); + schema.into() +} + impl BarPosition { /// Gets the orientation the bar and widgets should use /// based on this position. diff --git a/src/config/mod.rs b/src/config/mod.rs index 852fc7d..12b7164 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -38,11 +38,15 @@ use color_eyre::Result; use serde::Deserialize; use std::collections::HashMap; +#[cfg(feature = "schema")] +use schemars::JsonSchema; + pub use self::common::{CommonConfig, ModuleOrientation, TransitionType}; pub use self::truncate::TruncateMode; #[derive(Debug, Deserialize, Clone)] #[serde(tag = "type", rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(JsonSchema))] pub enum ModuleConfig { #[cfg(feature = "cairo")] Cairo(Box), @@ -123,6 +127,7 @@ impl ModuleConfig { } #[derive(Debug, Clone)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] pub enum MonitorConfig { Single(BarConfig), Multiple(Vec), @@ -130,6 +135,7 @@ pub enum MonitorConfig { #[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(JsonSchema))] pub enum BarPosition { Top, Bottom, @@ -144,6 +150,7 @@ impl Default for BarPosition { } #[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] pub struct MarginConfig { #[serde(default)] pub bottom: i32, @@ -162,6 +169,7 @@ pub struct MarginConfig { /// depending on your [use-case](#2-pick-your-use-case). /// #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] pub struct BarConfig { /// A unique identifier for the bar, used for controlling it over IPC. /// If not set, uses a generated integer suffix. @@ -214,6 +222,36 @@ pub struct BarConfig { #[serde(default)] pub margin: MarginConfig, + /// The layer-shell layer to place the bar on. + /// + /// Taken from the + /// [wlr_layer_shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1#zwlr_layer_shell_v1:enum:layer) definition: + /// + /// > These values indicate which layers a surface can be rendered in. + /// > They are ordered by z depth, bottom-most first. + /// > Traditional shell surfaces will typically be rendered between the bottom and top layers. + /// > Fullscreen shell surfaces are typically rendered at the top layer. + /// > Multiple surfaces can share a single layer, and ordering within a single layer is undefined. + /// + /// **Valid options**: `background`, `bottom`, `top`, `overlay` + ///
+ /// **Default**: `top` + #[serde( + default = "default_layer", + deserialize_with = "r#impl::deserialize_layer" + )] + #[cfg_attr(feature = "schema", schemars(schema_with = "r#impl::schema_layer"))] + pub layer: gtk_layer_shell::Layer, + + /// Whether the bar should reserve an exclusive zone around it. + /// + /// When true, this prevents windows from rendering in the same space + /// as the bar, causing them to shift. + /// + /// **Default**: `true` unless `start_hidden` is set. + #[serde(default)] + pub exclusive_zone: Option, + /// The size of the gap in pixels /// between the bar and the popup window. /// @@ -280,9 +318,11 @@ impl Default for BarConfig { Self { position: BarPosition::default(), - height: default_bar_height(), margin: MarginConfig::default(), name: None, + layer: default_layer(), + exclusive_zone: None, + height: default_bar_height(), start_hidden: None, autohide: None, icon_theme: None, @@ -298,6 +338,7 @@ impl Default for BarConfig { } #[derive(Debug, Deserialize, Clone, Default)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] pub struct Config { /// A map of [ironvar](ironvar) keys and values /// to initialize Ironbar with on startup. @@ -337,6 +378,10 @@ pub struct Config { pub monitors: Option>, } +const fn default_layer() -> gtk_layer_shell::Layer { + gtk_layer_shell::Layer::Top +} + const fn default_bar_height() -> i32 { 42 } diff --git a/src/config/truncate.rs b/src/config/truncate.rs index 87451e4..09966cd 100644 --- a/src/config/truncate.rs +++ b/src/config/truncate.rs @@ -4,6 +4,7 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, Copy)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum EllipsizeMode { Start, Middle, @@ -28,6 +29,7 @@ impl From for GtkEllipsizeMode { /// #[derive(Debug, Deserialize, Clone, Copy)] #[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum TruncateMode { /// Auto mode lets GTK decide when to ellipsize. /// diff --git a/src/dynamic_value/dynamic_bool.rs b/src/dynamic_value/dynamic_bool.rs index df75758..c2162fc 100644 --- a/src/dynamic_value/dynamic_bool.rs +++ b/src/dynamic_value/dynamic_bool.rs @@ -8,6 +8,7 @@ use tokio::sync::mpsc; #[derive(Debug, Deserialize, Clone)] #[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum DynamicBool { /// Either a script or variable, to be determined. Unknown(String), diff --git a/src/error.rs b/src/error.rs index 7ceb6b3..a55fa12 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,8 @@ pub enum ExitCode { GtkDisplay = 1, CreateBars = 2, + IpcResponseError = 3, + WaylandDispatchError = 4, } pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex"; diff --git a/src/ipc/client.rs b/src/ipc/client.rs index b7e1da9..820ace5 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -8,7 +8,7 @@ use tokio::net::UnixStream; impl Ipc { /// Sends a command to the IPC server. /// The server response is returned. - pub async fn send(&self, command: Command) -> Result { + pub async fn send(&self, command: Command, debug: bool) -> Result { let mut stream = match UnixStream::connect(&self.path).await { Ok(stream) => Ok(stream), Err(err) => Err(Report::new(err) @@ -17,6 +17,11 @@ impl Ipc { }?; let write_buffer = serde_json::to_vec(&command)?; + + if debug { + eprintln!("REQUEST JSON: {}", serde_json::to_string(&command)?); + } + stream.write_all(&write_buffer).await?; let mut read_buffer = vec![0; 1024]; diff --git a/src/ipc/commands.rs b/src/ipc/commands.rs index a080ff8..93b183e 100644 --- a/src/ipc/commands.rs +++ b/src/ipc/commands.rs @@ -1,20 +1,39 @@ +use clap::ArgAction; use std::path::PathBuf; -use clap::Subcommand; +use clap::{Args, Subcommand}; use serde::{Deserialize, Serialize}; #[derive(Subcommand, Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] +#[serde(tag = "command", rename_all = "snake_case")] pub enum Command { - /// Return "ok" + /// Pong Ping, - /// Open the GTK inspector + /// Open the GTK inspector. Inspect, - /// Reload the config + /// Reload the config. Reload, + /// Load an additional CSS stylesheet. + /// The sheet is automatically hot-reloaded. + LoadCss { + /// The path to the sheet. + path: PathBuf, + }, + + /// Get and set reactive Ironvar values. + #[command(subcommand)] + Var(IronvarCommand), + + /// Interact with a specific bar. + Bar(BarCommand), +} + +#[derive(Subcommand, Debug, Serialize, Deserialize)] +#[serde(tag = "subcommand", rename_all = "snake_case")] +pub enum IronvarCommand { /// Set an `ironvar` value. /// This creates it if it does not already exist, and updates it if it does. /// Any references to this variable are automatically and immediately updated. @@ -34,49 +53,80 @@ pub enum Command { /// Gets the current value of all `ironvar`s. List, +} - /// Load an additional CSS stylesheet. - /// The sheet is automatically hot-reloaded. - LoadCss { - /// The path to the sheet. - path: PathBuf, - }, +#[derive(Args, Debug, Serialize, Deserialize)] +pub struct BarCommand { + /// The name of the bar. + pub name: String, - /// Set the visibility of the bar with the given name. + #[command(subcommand)] + #[serde(flatten)] + pub subcommand: BarCommandType, +} + +#[derive(Subcommand, Debug, Serialize, Deserialize)] +#[serde(tag = "subcommand", rename_all = "snake_case")] +pub enum BarCommandType { + // == Visibility == \\ + /// Force the bar to be shown, regardless of current visibility state. + Show, + /// Force the bar to be hidden, regardless of current visibility state. + Hide, + /// Set the bar's visibility state via an argument. SetVisible { - ///Bar name to target. - bar_name: String, - /// The visibility status. - #[arg(short, long)] + /// The new visibility state. + #[clap( + num_args(1), + require_equals(true), + action = ArgAction::Set, + )] visible: bool, }, + /// Toggle the current visibility state between shown and hidden. + ToggleVisible, + /// Get the bar's visibility state. + GetVisible, - /// Get the visibility of the bar with the given name. - GetVisible { - /// Bar name to target. - bar_name: String, + // == Popup visibility == \\ + /// Open a popup, regardless of current state. + /// If opening this popup, and a different popup on the same bar is already open, the other is closed. + ShowPopup { + /// The configured name of the widget. + widget_name: String, }, + /// Close a popup, regardless of current state. + HidePopup, + /// Set the popup's visibility state via an argument. + /// If opening this popup, and a different popup on the same bar is already open, the other is closed. + SetPopupVisible { + /// The configured name of the widget. + widget_name: String, + #[clap( + num_args(1), + require_equals(true), + action = ArgAction::Set, + )] + visible: bool, + }, /// Toggle a popup open/closed. /// If opening this popup, and a different popup on the same bar is already open, the other is closed. TogglePopup { - /// The name of the monitor the bar is located on. - bar_name: String, - /// The name of the widget. - name: String, + /// The configured name of the widget. + widget_name: String, }, + /// Get the popup's current visibility state. + GetPopupVisible, - /// Open a popup, regardless of current state. - OpenPopup { - /// The name of the monitor the bar is located on. - bar_name: String, - /// The name of the widget. - name: String, - }, - - /// Close a popup, regardless of current state. - ClosePopup { - /// The name of the monitor the bar is located on. - bar_name: String, + // == Exclusivity == \\ + /// Set whether the bar reserves an exclusive zone. + SetExclusive { + #[clap( + num_args(1), + require_equals(true), + action = ArgAction::Set, + )] + exclusive: bool, }, } diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index efb6ce9..9f58a6b 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -6,7 +6,7 @@ mod server; use std::path::{Path, PathBuf}; use tracing::warn; -pub use commands::Command; +pub use commands::*; pub use responses::Response; #[derive(Debug)] diff --git a/src/ipc/server.rs b/src/ipc/server.rs deleted file mode 100644 index 521545b..0000000 --- a/src/ipc/server.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::fs; -use std::path::Path; -use std::rc::Rc; - -use color_eyre::{Report, Result}; -use gtk::prelude::*; -use gtk::Application; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{UnixListener, UnixStream}; -use tokio::sync::mpsc::{self, Receiver, Sender}; -use tracing::{debug, error, info, warn}; - -use crate::ipc::{Command, Response}; -use crate::modules::PopupButton; -use crate::style::load_css; -use crate::{glib_recv_mpsc, read_lock, send_async, spawn, try_send, write_lock, Ironbar}; - -use super::Ipc; - -impl Ipc { - /// Starts the IPC server on its socket. - /// - /// Once started, the server will begin accepting connections. - pub fn start(&self, application: &Application, ironbar: Rc) { - let (cmd_tx, cmd_rx) = mpsc::channel(32); - let (res_tx, mut res_rx) = mpsc::channel(32); - - let path = self.path.clone(); - - if path.exists() { - warn!("Socket already exists. Did Ironbar exit abruptly?"); - warn!("Attempting IPC shutdown to allow binding to address"); - Self::shutdown(&path); - } - - spawn(async move { - info!("Starting IPC on {}", path.display()); - - let listener = match UnixListener::bind(&path) { - Ok(listener) => listener, - Err(err) => { - error!( - "{:?}", - Report::new(err).wrap_err("Unable to start IPC server") - ); - return; - } - }; - - loop { - match listener.accept().await { - Ok((stream, _addr)) => { - if let Err(err) = - Self::handle_connection(stream, &cmd_tx, &mut res_rx).await - { - error!("{err:?}"); - } - } - Err(err) => { - error!("{err:?}"); - } - } - } - }); - - let application = application.clone(); - glib_recv_mpsc!(cmd_rx, command => { - let res = Self::handle_command(command, &application, &ironbar); - try_send!(res_tx, res); - }); - } - - /// Takes an incoming connections, - /// reads the command message, and sends the response. - /// - /// The connection is closed once the response has been written. - async fn handle_connection( - mut stream: UnixStream, - cmd_tx: &Sender, - res_rx: &mut Receiver, - ) -> Result<()> { - let (mut stream_read, mut stream_write) = stream.split(); - - let mut read_buffer = vec![0; 1024]; - let bytes = stream_read.read(&mut read_buffer).await?; - - let command = serde_json::from_slice::(&read_buffer[..bytes])?; - - debug!("Received command: {command:?}"); - - send_async!(cmd_tx, command); - let res = res_rx - .recv() - .await - .unwrap_or(Response::Err { message: None }); - let res = serde_json::to_vec(&res)?; - - stream_write.write_all(&res).await?; - stream_write.shutdown().await?; - - Ok(()) - } - - /// Takes an input command, runs it and returns with the appropriate response. - /// - /// This runs on the main thread, allowing commands to interact with GTK. - fn handle_command( - command: Command, - application: &Application, - ironbar: &Rc, - ) -> Response { - match command { - Command::Inspect => { - gtk::Window::set_interactive_debugging(true); - Response::Ok - } - Command::Reload => { - info!("Closing existing bars"); - ironbar.bars.borrow_mut().clear(); - - let windows = application.windows(); - for window in windows { - window.close(); - } - - let wl = ironbar.clients.borrow_mut().wayland(); - let outputs = wl.output_info_all(); - - ironbar.reload_config(); - - for output in outputs { - match crate::load_output_bars(ironbar, application, &output) { - Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars), - Err(err) => error!("{err:?}"), - } - } - - Response::Ok - } - Command::Set { key, value } => { - let variable_manager = Ironbar::variable_manager(); - let mut variable_manager = write_lock!(variable_manager); - match variable_manager.set(key, value) { - Ok(()) => Response::Ok, - Err(err) => Response::error(&format!("{err}")), - } - } - Command::Get { key } => { - let variable_manager = Ironbar::variable_manager(); - let value = read_lock!(variable_manager).get(&key); - match value { - Some(value) => Response::OkValue { value }, - None => Response::error("Variable not found"), - } - } - Command::List => { - let variable_manager = Ironbar::variable_manager(); - - let mut values = read_lock!(variable_manager) - .get_all() - .iter() - .map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default())) - .collect::>(); - - values.sort(); - let value = values.join("\n"); - - Response::OkValue { value } - } - Command::LoadCss { path } => { - if path.exists() { - load_css(path); - Response::Ok - } else { - Response::error("File not found") - } - } - Command::TogglePopup { bar_name, name } => { - let bar = ironbar.bar_by_name(&bar_name); - - match bar { - Some(bar) => { - let popup = bar.popup(); - let current_widget = popup.current_widget(); - - popup.hide(); - - let data = popup - .container_cache - .borrow() - .iter() - .find(|(_, value)| value.name == name) - .map(|(id, value)| (*id, value.content.buttons.first().cloned())); - - match data { - Some((id, Some(button))) if current_widget != Some(id) => { - let button_id = button.popup_id(); - - if popup.is_visible() { - popup.hide(); - } else { - popup.show(id, button_id); - } - - Response::Ok - } - Some((_, None)) => Response::error("Module has no popup functionality"), - Some(_) => Response::Ok, - None => Response::error("Invalid module name"), - } - } - None => Response::error("Invalid bar name"), - } - } - Command::OpenPopup { bar_name, name } => { - let bar = ironbar.bar_by_name(&bar_name); - - match bar { - Some(bar) => { - let popup = bar.popup(); - - // only one popup per bar, so hide if open for another widget - popup.hide(); - - let data = popup - .container_cache - .borrow() - .iter() - .find(|(_, value)| value.name == name) - .map(|(id, value)| (*id, value.content.buttons.first().cloned())); - - match data { - Some((id, Some(button))) => { - let button_id = button.popup_id(); - popup.show(id, button_id); - - Response::Ok - } - Some((_, None)) => Response::error("Module has no popup functionality"), - None => Response::error("Invalid module name"), - } - } - None => Response::error("Invalid bar name"), - } - } - Command::ClosePopup { bar_name } => { - let bar = ironbar.bar_by_name(&bar_name); - - match bar { - Some(bar) => { - let popup = bar.popup(); - popup.hide(); - - Response::Ok - } - None => Response::error("Invalid bar name"), - } - } - Command::Ping => Response::Ok, - Command::SetVisible { bar_name, visible } => { - let windows = application.windows(); - let found = windows - .iter() - .find(|window| window.widget_name() == bar_name); - - if let Some(window) = found { - window.set_visible(visible); - Response::Ok - } else { - Response::error("Bar not found") - } - } - Command::GetVisible { bar_name } => { - let windows = application.windows(); - let found = windows - .iter() - .find(|window| window.widget_name() == bar_name); - - if let Some(window) = found { - Response::OkValue { - value: window.is_visible().to_string(), - } - } else { - Response::error("Bar not found") - } - } - } - } - - /// Shuts down the IPC server, - /// removing the socket file in the process. - /// - /// Note this is static as the `Ipc` struct is not `Send`. - pub fn shutdown>(path: P) { - fs::remove_file(&path).ok(); - } -} diff --git a/src/ipc/server/bar.rs b/src/ipc/server/bar.rs new file mode 100644 index 0000000..3bd3b8c --- /dev/null +++ b/src/ipc/server/bar.rs @@ -0,0 +1,89 @@ +use super::Response; +use crate::bar::Bar; +use crate::ipc::{BarCommand, BarCommandType}; +use crate::modules::PopupButton; +use crate::Ironbar; +use std::rc::Rc; + +pub fn handle_command(command: BarCommand, ironbar: &Rc) -> Response { + let bar = ironbar.bar_by_name(&command.name); + let Some(bar) = bar else { + return Response::error("Invalid bar name"); + }; + + use BarCommandType::*; + match command.subcommand { + Show => set_visible(&bar, true), + Hide => set_visible(&bar, false), + SetVisible { visible } => set_visible(&bar, visible), + ToggleVisible => set_visible(&bar, !bar.visible()), + GetVisible => Response::OkValue { + value: bar.visible().to_string(), + }, + + ShowPopup { widget_name } => show_popup(&bar, widget_name), + HidePopup => hide_popup(&bar), + SetPopupVisible { + widget_name, + visible, + } => { + if visible { + show_popup(&bar, widget_name) + } else { + hide_popup(&bar) + } + } + TogglePopup { widget_name } => { + if bar.popup().visible() { + hide_popup(&bar) + } else { + show_popup(&bar, widget_name) + } + } + GetPopupVisible => Response::OkValue { + value: bar.popup().visible().to_string(), + }, + SetExclusive { exclusive } => { + bar.set_exclusive(exclusive); + + Response::Ok + } + } +} + +fn set_visible(bar: &Bar, visible: bool) -> Response { + bar.set_visible(visible); + Response::Ok +} + +fn show_popup(bar: &Bar, widget_name: String) -> Response { + let popup = bar.popup(); + + // only one popup per bar, so hide if open for another widget + popup.hide(); + + let data = popup + .container_cache + .borrow() + .iter() + .find(|(_, value)| value.name == widget_name) + .map(|(id, value)| (*id, value.content.buttons.first().cloned())); + + match data { + Some((id, Some(button))) => { + let button_id = button.popup_id(); + popup.show(id, button_id); + + Response::Ok + } + Some((_, None)) => Response::error("Module has no popup functionality"), + None => Response::error("Invalid module name"), + } +} + +fn hide_popup(bar: &Bar) -> Response { + let popup = bar.popup(); + popup.hide(); + + Response::Ok +} diff --git a/src/ipc/server/ironvar.rs b/src/ipc/server/ironvar.rs new file mode 100644 index 0000000..ced8e77 --- /dev/null +++ b/src/ipc/server/ironvar.rs @@ -0,0 +1,38 @@ +use crate::ipc::commands::IronvarCommand; +use crate::ipc::Response; +use crate::{read_lock, write_lock, Ironbar}; + +pub fn handle_command(command: IronvarCommand) -> Response { + match command { + IronvarCommand::Set { key, value } => { + let variable_manager = Ironbar::variable_manager(); + let mut variable_manager = write_lock!(variable_manager); + match variable_manager.set(key, value) { + Ok(()) => Response::Ok, + Err(err) => Response::error(&format!("{err}")), + } + } + IronvarCommand::Get { key } => { + let variable_manager = Ironbar::variable_manager(); + let value = read_lock!(variable_manager).get(&key); + match value { + Some(value) => Response::OkValue { value }, + None => Response::error("Variable not found"), + } + } + IronvarCommand::List => { + let variable_manager = Ironbar::variable_manager(); + + let mut values = read_lock!(variable_manager) + .get_all() + .iter() + .map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default())) + .collect::>(); + + values.sort(); + let value = values.join("\n"); + + Response::OkValue { value } + } + } +} diff --git a/src/ipc/server/mod.rs b/src/ipc/server/mod.rs new file mode 100644 index 0000000..9ef736a --- /dev/null +++ b/src/ipc/server/mod.rs @@ -0,0 +1,164 @@ +mod bar; +mod ironvar; + +use std::fs; +use std::path::Path; +use std::rc::Rc; + +use color_eyre::{Report, Result}; +use gtk::prelude::*; +use gtk::Application; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::mpsc::{self, Receiver, Sender}; +use tracing::{debug, error, info, warn}; + +use crate::ipc::{Command, Response}; +use crate::style::load_css; +use crate::{glib_recv_mpsc, send_async, spawn, try_send, Ironbar}; + +use super::Ipc; + +impl Ipc { + /// Starts the IPC server on its socket. + /// + /// Once started, the server will begin accepting connections. + pub fn start(&self, application: &Application, ironbar: Rc) { + let (cmd_tx, cmd_rx) = mpsc::channel(32); + let (res_tx, mut res_rx) = mpsc::channel(32); + + let path = self.path.clone(); + + if path.exists() { + warn!("Socket already exists. Did Ironbar exit abruptly?"); + warn!("Attempting IPC shutdown to allow binding to address"); + Self::shutdown(&path); + } + + spawn(async move { + info!("Starting IPC on {}", path.display()); + + let listener = match UnixListener::bind(&path) { + Ok(listener) => listener, + Err(err) => { + error!( + "{:?}", + Report::new(err).wrap_err("Unable to start IPC server") + ); + return; + } + }; + + loop { + match listener.accept().await { + Ok((stream, _addr)) => { + if let Err(err) = + Self::handle_connection(stream, &cmd_tx, &mut res_rx).await + { + error!("{err:?}"); + } + } + Err(err) => { + error!("{err:?}"); + } + } + } + }); + + let application = application.clone(); + glib_recv_mpsc!(cmd_rx, command => { + let res = Self::handle_command(command, &application, &ironbar); + try_send!(res_tx, res); + }); + } + + /// Takes an incoming connections, + /// reads the command message, and sends the response. + /// + /// The connection is closed once the response has been written. + async fn handle_connection( + mut stream: UnixStream, + cmd_tx: &Sender, + res_rx: &mut Receiver, + ) -> Result<()> { + let (mut stream_read, mut stream_write) = stream.split(); + + let mut read_buffer = vec![0; 1024]; + let bytes = stream_read.read(&mut read_buffer).await?; + + // FIXME: Error on invalid command + let command = serde_json::from_slice::(&read_buffer[..bytes])?; + + debug!("Received command: {command:?}"); + + send_async!(cmd_tx, command); + let res = res_rx + .recv() + .await + .unwrap_or(Response::Err { message: None }); + let res = serde_json::to_vec(&res)?; + + stream_write.write_all(&res).await?; + stream_write.shutdown().await?; + + Ok(()) + } + + /// Takes an input command, runs it and returns with the appropriate response. + /// + /// This runs on the main thread, allowing commands to interact with GTK. + fn handle_command( + command: Command, + application: &Application, + ironbar: &Rc, + ) -> Response { + match command { + Command::Ping => Response::Ok, + Command::Inspect => { + gtk::Window::set_interactive_debugging(true); + Response::Ok + } + Command::Reload => { + info!("Closing existing bars"); + ironbar.bars.borrow_mut().clear(); + + let windows = application.windows(); + for window in windows { + window.close(); + } + + let wl = ironbar.clients.borrow_mut().wayland(); + let outputs = wl.output_info_all(); + + ironbar.reload_config(); + + for output in outputs { + match crate::load_output_bars(ironbar, application, &output) { + Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars), + Err(err) => error!("{err:?}"), + } + } + + Response::Ok + } + Command::LoadCss { path } => { + if path.exists() { + load_css(path); + Response::Ok + } else { + Response::error("File not found") + } + } + Command::Var(cmd) => ironvar::handle_command(cmd), + Command::Bar(cmd) => bar::handle_command(cmd, ironbar), + } + } + + /// Shuts down the IPC server, + /// removing the socket file in the process. + /// + /// Note this is static as the `Ipc` struct is not `Send`. + pub fn shutdown>(path: P) { + fs::remove_file(&path).ok(); + } +} diff --git a/src/main.rs b/src/main.rs index 441c0e3..4657574 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,13 +76,30 @@ fn main() { fn run_with_args() { let args = cli::Args::parse(); + #[cfg(feature = "schema")] + if args.print_schema { + let schema = schemars::schema_for!(Config); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); + return; + } + match args.command { Some(command) => { + if args.debug { + eprintln!("REQUEST: {command:?}") + } + let rt = create_runtime(); rt.block_on(async move { let ipc = ipc::Ipc::new(); - match ipc.send(command).await { - Ok(res) => cli::handle_response(res), + match ipc.send(command, args.debug).await { + Ok(res) => { + if args.debug { + eprintln!("RESPONSE: {res:?}") + } + + cli::handle_response(res, args.format.unwrap_or_default()) + } Err(err) => error!("{err:?}"), }; }); @@ -232,6 +249,7 @@ impl Ironbar { /// Gets a `usize` ID value that is unique to the entire Ironbar instance. /// This is just a static `AtomicUsize` that increments every time this function is called. + #[must_use] pub fn unique_id() -> usize { static COUNTER: AtomicUsize = AtomicUsize::new(1); COUNTER.fetch_add(1, Ordering::Relaxed) diff --git a/src/modules/cairo.rs b/src/modules/cairo.rs index 3ce42bb..bb0290b 100644 --- a/src/modules/cairo.rs +++ b/src/modules/cairo.rs @@ -18,6 +18,7 @@ use tokio::time::sleep; use tracing::{debug, error}; #[derive(Debug, Clone, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct CairoModule { /// The path to the Lua script to load. /// This can be absolute, or relative to the working directory. diff --git a/src/modules/clipboard.rs b/src/modules/clipboard.rs index 6365088..751c115 100644 --- a/src/modules/clipboard.rs +++ b/src/modules/clipboard.rs @@ -17,6 +17,7 @@ use tokio::sync::{broadcast, mpsc}; use tracing::{debug, error}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ClipboardModule { /// The icon to show on the bar widget button. /// Supports [image](images) icons. diff --git a/src/modules/clock.rs b/src/modules/clock.rs index a5be924..6af5e88 100644 --- a/src/modules/clock.rs +++ b/src/modules/clock.rs @@ -16,6 +16,7 @@ use crate::modules::{ use crate::{glib_recv, module_impl, send_async, spawn, try_send}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ClockModule { /// The format string to use for the date/time shown on the bar. /// Pango markup is supported. diff --git a/src/modules/custom/box.rs b/src/modules/custom/box.rs index 7e448f4..2c4275f 100644 --- a/src/modules/custom/box.rs +++ b/src/modules/custom/box.rs @@ -6,6 +6,7 @@ use gtk::prelude::*; use serde::Deserialize; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct BoxWidget { /// Widget name. /// diff --git a/src/modules/custom/button.rs b/src/modules/custom/button.rs index 9382da2..98476e8 100644 --- a/src/modules/custom/button.rs +++ b/src/modules/custom/button.rs @@ -10,6 +10,7 @@ use crate::{build, try_send}; use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ButtonWidget { /// Widget name. /// diff --git a/src/modules/custom/image.rs b/src/modules/custom/image.rs index fffac2d..cb4a33f 100644 --- a/src/modules/custom/image.rs +++ b/src/modules/custom/image.rs @@ -9,6 +9,7 @@ use crate::image::ImageProvider; use super::{CustomWidget, CustomWidgetContext}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ImageWidget { /// Widget name. /// diff --git a/src/modules/custom/label.rs b/src/modules/custom/label.rs index 65f303b..55d2507 100644 --- a/src/modules/custom/label.rs +++ b/src/modules/custom/label.rs @@ -9,6 +9,7 @@ use crate::dynamic_value::dynamic_string; use super::{CustomWidget, CustomWidgetContext}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct LabelWidget { /// Widget name. /// diff --git a/src/modules/custom/mod.rs b/src/modules/custom/mod.rs index 5cffb00..9ef32eb 100644 --- a/src/modules/custom/mod.rs +++ b/src/modules/custom/mod.rs @@ -28,6 +28,7 @@ use tokio::sync::{broadcast, mpsc}; use tracing::{debug, error}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct CustomModule { /// Modules and widgets to add to the bar container. /// @@ -45,6 +46,7 @@ pub struct CustomModule { } #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct WidgetConfig { /// One of a custom module native Ironbar module. #[serde(flatten)] @@ -57,6 +59,7 @@ pub struct WidgetConfig { #[derive(Debug, Deserialize, Clone)] #[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum WidgetOrModule { /// A custom-module specific basic widget Widget(Widget), @@ -67,6 +70,7 @@ pub enum WidgetOrModule { #[derive(Debug, Deserialize, Clone)] #[serde(tag = "type", rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum Widget { /// A container to place nested widgets inside. Box(BoxWidget), diff --git a/src/modules/custom/progress.rs b/src/modules/custom/progress.rs index 199ea8c..1d9f40e 100644 --- a/src/modules/custom/progress.rs +++ b/src/modules/custom/progress.rs @@ -13,6 +13,7 @@ use crate::{build, glib_recv_mpsc, spawn, try_send}; use super::{CustomWidget, CustomWidgetContext}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ProgressWidget { /// Widget name. /// diff --git a/src/modules/custom/slider.rs b/src/modules/custom/slider.rs index c533719..bc5ea7f 100644 --- a/src/modules/custom/slider.rs +++ b/src/modules/custom/slider.rs @@ -16,6 +16,7 @@ use crate::{build, glib_recv_mpsc, spawn, try_send}; use super::{CustomWidget, CustomWidgetContext, ExecEvent}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct SliderWidget { /// Widget name. /// diff --git a/src/modules/focused.rs b/src/modules/focused.rs index 9fb43d2..989293f 100644 --- a/src/modules/focused.rs +++ b/src/modules/focused.rs @@ -12,6 +12,7 @@ use tokio::sync::mpsc; use tracing::debug; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct FocusedModule { /// Whether to show icon on the bar. /// diff --git a/src/modules/label.rs b/src/modules/label.rs index 2a8a20c..0004ad9 100644 --- a/src/modules/label.rs +++ b/src/modules/label.rs @@ -9,6 +9,7 @@ use serde::Deserialize; use tokio::sync::mpsc; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct LabelModule { /// The text to show on the label. /// This is a [Dynamic String](dynamic-values#dynamic-string). diff --git a/src/modules/launcher/mod.rs b/src/modules/launcher/mod.rs index bb48cd9..70e4b56 100644 --- a/src/modules/launcher/mod.rs +++ b/src/modules/launcher/mod.rs @@ -19,6 +19,7 @@ use tokio::sync::{broadcast, mpsc}; use tracing::{debug, error, trace}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct LauncherModule { /// List of app IDs (or classes) to always show regardless of open state, /// in the order specified. diff --git a/src/modules/mod.rs b/src/modules/mod.rs index acff983..98e272d 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -384,7 +384,7 @@ impl ModuleFactory for BarModuleFactory { } ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => { debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id); - if popup.is_visible() && popup.current_widget().unwrap_or_default() == id { + if popup.visible() && popup.current_widget().unwrap_or_default() == id { popup.hide(); } else { popup.show(id, button_id); @@ -457,7 +457,7 @@ impl ModuleFactory for PopupModuleFactory { } ModuleUpdateEvent::TogglePopup(_) if !disable_popup => { debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id); - if popup.is_visible() && popup.current_widget().unwrap_or_default() == id { + if popup.visible() && popup.current_widget().unwrap_or_default() == id { popup.hide(); } else { popup.show(id, button_id); diff --git a/src/modules/music/config.rs b/src/modules/music/config.rs index 6d3de79..cfedb8f 100644 --- a/src/modules/music/config.rs +++ b/src/modules/music/config.rs @@ -4,6 +4,7 @@ use serde::Deserialize; use std::path::PathBuf; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Icons { /// Icon to display when playing. /// @@ -71,6 +72,7 @@ impl Default for Icons { #[derive(Debug, Deserialize, Clone, Copy)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum PlayerType { Mpd, Mpris, @@ -83,6 +85,7 @@ impl Default for PlayerType { } #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct MusicModule { /// Type of player to connect to #[serde(default)] diff --git a/src/modules/music/mod.rs b/src/modules/music/mod.rs index a8bb7b0..76e45cd 100644 --- a/src/modules/music/mod.rs +++ b/src/modules/music/mod.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use color_eyre::Result; -use glib::{Propagation, PropertySet}; +use glib::{markup_escape_text, Propagation, PropertySet}; use gtk::prelude::*; use gtk::{Button, IconTheme, Label, Orientation, Scale}; use regex::Regex; @@ -531,6 +531,7 @@ fn get_token_value(song: &Track, token: &str) -> String { "track" => song.track.map(|x| x.to_string()), _ => Some(token.to_string()), } + .map(|str| markup_escape_text(str.as_str()).to_string()) .unwrap_or_default() } diff --git a/src/modules/notifications.rs b/src/modules/notifications.rs index 18e5c2a..116ce03 100644 --- a/src/modules/notifications.rs +++ b/src/modules/notifications.rs @@ -10,6 +10,7 @@ use tokio::sync::mpsc::Receiver; use tracing::error; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct NotificationsModule { /// Whether to show the current notification count. /// @@ -29,6 +30,7 @@ pub struct NotificationsModule { } #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] struct Icons { /// Icon to show when the panel is closed, with no notifications. /// @@ -193,6 +195,7 @@ impl Module for NotificationsModule { if self.show_count { label.add_class("count"); overlay.add_overlay(&label); + overlay.set_overlay_pass_through(&label, true); } let ctx = context.controller_tx.clone(); diff --git a/src/modules/script.rs b/src/modules/script.rs index cd96142..83a0748 100644 --- a/src/modules/script.rs +++ b/src/modules/script.rs @@ -10,6 +10,7 @@ use tokio::sync::mpsc; use tracing::error; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ScriptModule { /// Path to script to execute. /// diff --git a/src/modules/sysinfo.rs b/src/modules/sysinfo.rs index 026cbf2..2568ff2 100644 --- a/src/modules/sysinfo.rs +++ b/src/modules/sysinfo.rs @@ -14,6 +14,7 @@ use tokio::sync::mpsc; use tokio::time::sleep; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct SysInfoModule { /// List of strings including formatting tokens. /// For available tokens, see [below](#formatting-tokens). @@ -51,6 +52,7 @@ pub struct SysInfoModule { } #[derive(Debug, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Intervals { /// The number of seconds between refreshing memory data. /// @@ -91,6 +93,7 @@ pub struct Intervals { #[derive(Debug, Deserialize, Copy, Clone)] #[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum Interval { All(u64), Individual(Intervals), diff --git a/src/modules/tray/mod.rs b/src/modules/tray/mod.rs index 23c4bee..faba589 100644 --- a/src/modules/tray/mod.rs +++ b/src/modules/tray/mod.rs @@ -19,6 +19,7 @@ use tokio::sync::mpsc; use tracing::{debug, error, warn}; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct TrayModule { /// Requests that icons from the theme be used over the item-provided item. /// Most items only provide one or the other so this will have no effect in most circumstances. @@ -38,7 +39,8 @@ pub struct TrayModule { /// **Valid options**: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left` ///
/// **Default**: `left_to_right` if bar is horizontal, `top_to_bottom` if bar is vertical - #[serde(default, deserialize_with = "deserialize_orientation")] + #[serde(default, deserialize_with = "deserialize_pack_direction")] + #[cfg_attr(feature = "schema", schemars(schema_with = "schema_pack_direction"))] direction: Option, /// See [common options](module-level-options#common-options). @@ -50,7 +52,7 @@ const fn default_icon_size() -> u32 { 16 } -fn deserialize_orientation<'de, D>(deserializer: D) -> Result, D::Error> +fn deserialize_pack_direction<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -61,11 +63,24 @@ where "right_to_left" => Ok(PackDirection::Rtl), "top_to_bottom" => Ok(PackDirection::Ttb), "bottom_to_top" => Ok(PackDirection::Btt), - _ => Err(serde::de::Error::custom("invalid value for orientation")), + _ => Err(serde::de::Error::custom("invalid value for direction")), }) .transpose() } +#[cfg(feature = "schema")] +fn schema_pack_direction(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + use schemars::JsonSchema; + let mut schema: schemars::schema::SchemaObject = ::json_schema(gen).into(); + schema.enum_values = Some(vec![ + "top_to_bottom".into(), + "bottom_to_top".into(), + "left_to_right".into(), + "right_to_left".into(), + ]); + schema.into() +} + impl Module for TrayModule { type SendMessage = Event; type ReceiveMessage = ActivateRequest; diff --git a/src/modules/upower.rs b/src/modules/upower.rs index 34baf27..d461cc3 100644 --- a/src/modules/upower.rs +++ b/src/modules/upower.rs @@ -22,6 +22,7 @@ const HOUR: i64 = 60 * 60; const MINUTE: i64 = 60; #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct UpowerModule { /// The format string to use for the widget button label. /// For available tokens, see [below](#formatting-tokens). diff --git a/src/modules/volume.rs b/src/modules/volume.rs index 0f7a812..54c536e 100644 --- a/src/modules/volume.rs +++ b/src/modules/volume.rs @@ -18,6 +18,7 @@ use std::collections::HashMap; use tokio::sync::mpsc; #[derive(Debug, Clone, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct VolumeModule { /// Maximum value to allow volume sliders to reach. /// Pulse supports values > 100 but this may result in distortion. diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs index 91d0cd6..d9f41dd 100644 --- a/src/modules/workspaces.rs +++ b/src/modules/workspaces.rs @@ -15,6 +15,7 @@ use tracing::{debug, trace, warn}; #[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum SortOrder { /// Shows workspaces in the order they're added Added, @@ -31,6 +32,7 @@ impl Default for SortOrder { #[derive(Debug, Deserialize, Clone)] #[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum Favorites { ByMonitor(HashMap>), Global(Vec), @@ -43,6 +45,7 @@ impl Default for Favorites { } #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct WorkspacesModule { /// Map of actual workspace names to custom names. /// diff --git a/src/popup.rs b/src/popup.rs index 0a85a7f..5946e99 100644 --- a/src/popup.rs +++ b/src/popup.rs @@ -229,7 +229,7 @@ impl Popup { } /// Checks if the popup is currently visible - pub fn is_visible(&self) -> bool { + pub fn visible(&self) -> bool { self.window.is_visible() } diff --git a/src/script.rs b/src/script.rs index 71f0dc4..29fe2c4 100644 --- a/src/script.rs +++ b/src/script.rs @@ -14,6 +14,7 @@ use tracing::{debug, error, trace, warn}; #[derive(Debug, Deserialize, Clone)] #[serde(untagged)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum ScriptInput { String(String), Struct(Script), @@ -21,6 +22,7 @@ pub enum ScriptInput { #[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum ScriptMode { Poll, Watch, @@ -75,6 +77,7 @@ impl ScriptMode { } #[derive(Debug, Deserialize, Clone)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Script { #[serde(default = "ScriptMode::default")] pub(crate) mode: ScriptMode,