├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── rust-toolchain └── src ├── color.rs ├── config.rs ├── config └── params.rs ├── desktop.rs ├── desktop └── locale.rs ├── draw.rs ├── draw ├── background.rs ├── input_text.rs └── list_view.rs ├── exec.rs ├── font.rs ├── font └── fdue.rs ├── icon.rs ├── input_parser.rs ├── lib.rs ├── main.rs ├── mode.rs ├── mode ├── apps.rs ├── bins.rs └── dialog.rs ├── state.rs ├── state └── filtered_lines.rs ├── style.rs ├── usage_cache.rs ├── window.rs └── window ├── compositor.rs ├── keyboard.rs ├── layer_shell.rs ├── output.rs ├── pointer.rs ├── registry.rs ├── seat.rs ├── shm.rs └── xdg_window.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Install sysdeps 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get -y install libxkbcommon-dev 23 | - uses: actions-rs/toolchain@v1 24 | with: 25 | components: rustfmt, clippy 26 | - name: Build 27 | run: | 28 | cargo build --verbose 29 | - name: Check format 30 | run: cargo fmt -- --check 31 | - name: Lint 32 | run: cargo clippy -- -D warnings 33 | - name: Run tests 34 | run: cargo test --verbose 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | bin-build: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-20.04, ubuntu-22.04] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install sysdeps 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get -y install libxkbcommon-dev 22 | - name: Build 23 | run: cargo build --profile release-lto 24 | - name: Upload binary 25 | uses: svenstaro/upload-release-action@v2 26 | with: 27 | repo_token: ${{ secrets.GITHUB_TOKEN }} 28 | file: target/release-lto/yofi 29 | asset_name: yofi-${{ matrix.os }} 30 | tag: ${{ github.ref }} 31 | - uses: katyo/publish-crates@v1 32 | with: 33 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 34 | no-verify: true 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result* 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased changes 2 | 3 | ## Features 4 | 5 | - Support some actions with pointer (e.g mouse). 6 | 7 | ## Changes 8 | 9 | - Log to stderr instead of stdout. 10 | 11 | ## Fixes 12 | 13 | # 0.2.2 - 2024-03-10 14 | 15 | ## Fixes 16 | 17 | - Invalid scale handling 18 | 19 | # 0.2.1 - 2024-03-10 20 | 21 | ## Features 22 | 23 | - Launch multiple apps from one yofi launch based on fork (#24). 24 | - Nix build (#98). 25 | - Corner roundings config param. 26 | - Background border. 27 | - Font overflow hint for long text items. 28 | 29 | ## Changes 30 | 31 | - Dropped support for font-kit backend. 32 | - Use native fontconfig instead of rust-fontconfig. 33 | - More errors are handled over panic. 34 | 35 | ## Fixes 36 | 37 | - Don't reload font on each ListView redraw. 38 | - Normalize indexed pngs. 39 | - Icon themes suport multiple inheritance. 40 | 41 | # 0.2.0 - 2022-10-12 42 | 43 | ## Features 44 | 45 | - Support prompt message 46 | - Input masking for password 47 | - Better icon support 48 | - Migrate from fzyr to sublime_fuzzy matcher 49 | 50 | ## Changes 51 | 52 | - Use syslog instead of journald that could be disabled 53 | 54 | ## Fixes 55 | 56 | - Don't panic on malformed icons 57 | - Skip desktop files without appropriate file extension 58 | - Reduce allocations for icon loading 59 | - Skip folders listing for binapps mode 60 | - Handle panic gracefully 61 | - Wrong font being selected for list_items 62 | - Account font kerning for font-kit backend 63 | 64 | # 0.1.5 - 2022-01-30 65 | 66 | ## Features 67 | 68 | - Font loading by path without fs scans (#79) 69 | - Render desktop actions for apps mode (#78) 70 | - Fontdue backend supoorted and used by default (#63, #67, #69) 71 | - Support of blacklisting entries (#62) 72 | - Support grayscale/indexed png icons (#61) 73 | - Redirect logs by default to systemd (#58) and stdout (#75) 74 | - More hotkeys for naviation (#35, #49) 75 | - Specify colors in css-like hex (#47) 76 | - Fallback to input at dialog overflow (#43) 77 | - Support environments without layer-shell protocol (#42) 78 | 79 | ## Bug fixes 80 | 81 | - Prioritize the local desktop files over global 82 | - Handle missing glyphs (#40) 83 | 84 | # 0.1.4 - 2021-01-10 85 | 86 | ## Features 87 | 88 | - Support localization (#33) 89 | - Search by keywords in apps mode (#20) 90 | - Magic separators support: `!!` for args, `#` for envs and `~` for workdir (#19) 91 | - Display full path for ambiguous binapps (0b47575) 92 | - ctrl+backspace is alias for ctrl+w (b3fca99) 93 | 94 | ## Bug fixes 95 | 96 | - Update HiDPI scale on each draw (#20) 97 | - Deduplicate binapps entries with the same path (c6b73f2) 98 | - With highligting enabled search may crash sometimes (b990057) 99 | 100 | # 0.1.3 - 2020-12-26 101 | 102 | ## Features 103 | 104 | - HiDPI scaling support (79cb8dd) 105 | - Matched chars highlighting (9d36ab0) 106 | - Intuitive scroll (7958fce) 107 | - Better fuzzy search, thanks for fzyr lib (73b002f) 108 | - ctrl+w hotkey removes last word (3524df6) 109 | - Launch binaries (cf16596) 110 | - Configure font size (1a34eb2) 111 | 112 | # 0.1.2 - 2020-12-19 113 | 114 | ## Features 115 | 116 | - Pixmap icons support #12 117 | - Configurable layout #11 118 | 119 | ## Bug fixes 120 | 121 | - Support absolute paths in Icon desktop entries (7474a12) 122 | - Search for scalable folder for icons as well (125363a) 123 | - Skip placeholders in Exec desktop entries (c548191) 124 | 125 | # 0.1.1 - 2020-12-10 126 | 127 | ## Features 128 | 129 | - Basic icon support #10 130 | - Startup ordering based on usage statistic (d7d40d4) 131 | 132 | # 0.1.0 - 2020-12-06 133 | 134 | ## Features 135 | 136 | - Dialog aka dmenu mode (abbd722) 137 | - CLI arguments for log/config parameters (f4befb1) 138 | 139 | ## Bug fixes 140 | 141 | - Show output even on empty input buffer (6790c4a) 142 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.8.11" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 16 | dependencies = [ 17 | "cfg-if", 18 | "once_cell", 19 | "version_check", 20 | "zerocopy", 21 | ] 22 | 23 | [[package]] 24 | name = "aho-corasick" 25 | version = "1.1.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 28 | dependencies = [ 29 | "memchr", 30 | ] 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.86" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 43 | 44 | [[package]] 45 | name = "argh" 46 | version = "0.1.12" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" 49 | dependencies = [ 50 | "argh_derive", 51 | "argh_shared", 52 | ] 53 | 54 | [[package]] 55 | name = "argh_derive" 56 | version = "0.1.12" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" 59 | dependencies = [ 60 | "argh_shared", 61 | "proc-macro2", 62 | "quote", 63 | "syn 2.0.76", 64 | ] 65 | 66 | [[package]] 67 | name = "argh_shared" 68 | version = "0.1.12" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" 71 | dependencies = [ 72 | "serde", 73 | ] 74 | 75 | [[package]] 76 | name = "arrayref" 77 | version = "0.3.8" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" 80 | 81 | [[package]] 82 | name = "arrayvec" 83 | version = "0.7.6" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 86 | 87 | [[package]] 88 | name = "autocfg" 89 | version = "1.3.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 92 | 93 | [[package]] 94 | name = "base64" 95 | version = "0.21.7" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 98 | 99 | [[package]] 100 | name = "bitflags" 101 | version = "1.3.2" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 104 | 105 | [[package]] 106 | name = "bitflags" 107 | version = "2.6.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 110 | 111 | [[package]] 112 | name = "bytemuck" 113 | version = "1.17.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" 116 | dependencies = [ 117 | "bytemuck_derive", 118 | ] 119 | 120 | [[package]] 121 | name = "bytemuck_derive" 122 | version = "1.7.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "syn 2.0.76", 129 | ] 130 | 131 | [[package]] 132 | name = "calloop" 133 | version = "0.12.4" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" 136 | dependencies = [ 137 | "bitflags 2.6.0", 138 | "log", 139 | "polling", 140 | "rustix", 141 | "slab", 142 | "thiserror", 143 | ] 144 | 145 | [[package]] 146 | name = "calloop-wayland-source" 147 | version = "0.2.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" 150 | dependencies = [ 151 | "calloop", 152 | "rustix", 153 | "wayland-backend", 154 | "wayland-client", 155 | ] 156 | 157 | [[package]] 158 | name = "cc" 159 | version = "1.1.15" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" 162 | dependencies = [ 163 | "shlex", 164 | ] 165 | 166 | [[package]] 167 | name = "cfg-if" 168 | version = "1.0.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 171 | 172 | [[package]] 173 | name = "cfg_aliases" 174 | version = "0.1.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 177 | 178 | [[package]] 179 | name = "concurrent-queue" 180 | version = "2.5.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 183 | dependencies = [ 184 | "crossbeam-utils", 185 | ] 186 | 187 | [[package]] 188 | name = "crc32fast" 189 | version = "1.4.2" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 192 | dependencies = [ 193 | "cfg-if", 194 | ] 195 | 196 | [[package]] 197 | name = "crossbeam-utils" 198 | version = "0.8.20" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 201 | 202 | [[package]] 203 | name = "cstr" 204 | version = "0.2.12" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" 207 | dependencies = [ 208 | "proc-macro2", 209 | "quote", 210 | ] 211 | 212 | [[package]] 213 | name = "cursor-icon" 214 | version = "1.1.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" 217 | 218 | [[package]] 219 | name = "data-url" 220 | version = "0.3.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" 223 | 224 | [[package]] 225 | name = "defaults" 226 | version = "0.2.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "40e9553d64f91aa22cb23a558a19108bce0f6854ef99a123b8c0a9b95a9ba267" 229 | dependencies = [ 230 | "proc-macro2", 231 | "quote", 232 | "syn 1.0.109", 233 | ] 234 | 235 | [[package]] 236 | name = "deranged" 237 | version = "0.3.11" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 240 | dependencies = [ 241 | "powerfmt", 242 | ] 243 | 244 | [[package]] 245 | name = "dlib" 246 | version = "0.5.2" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 249 | dependencies = [ 250 | "libloading", 251 | ] 252 | 253 | [[package]] 254 | name = "downcast-rs" 255 | version = "1.2.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 258 | 259 | [[package]] 260 | name = "either" 261 | version = "1.13.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 264 | 265 | [[package]] 266 | name = "equivalent" 267 | version = "1.0.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 270 | 271 | [[package]] 272 | name = "errno" 273 | version = "0.3.9" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 276 | dependencies = [ 277 | "libc", 278 | "windows-sys 0.52.0", 279 | ] 280 | 281 | [[package]] 282 | name = "error-chain" 283 | version = "0.12.4" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" 286 | dependencies = [ 287 | "version_check", 288 | ] 289 | 290 | [[package]] 291 | name = "euclid" 292 | version = "0.22.11" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" 295 | dependencies = [ 296 | "num-traits", 297 | ] 298 | 299 | [[package]] 300 | name = "fdeflate" 301 | version = "0.3.4" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 304 | dependencies = [ 305 | "simd-adler32", 306 | ] 307 | 308 | [[package]] 309 | name = "fern" 310 | version = "0.6.2" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" 313 | dependencies = [ 314 | "log", 315 | "syslog", 316 | ] 317 | 318 | [[package]] 319 | name = "flate2" 320 | version = "1.0.31" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" 323 | dependencies = [ 324 | "crc32fast", 325 | "miniz_oxide", 326 | ] 327 | 328 | [[package]] 329 | name = "float-cmp" 330 | version = "0.9.0" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 333 | 334 | [[package]] 335 | name = "fontconfig" 336 | version = "0.8.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "c9b79b619a4ae048ea79e927376b1d10294979bda195b0c052fc958be96c62d9" 339 | dependencies = [ 340 | "yeslogic-fontconfig-sys", 341 | ] 342 | 343 | [[package]] 344 | name = "fontdue" 345 | version = "0.8.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "9099a2f86b8e674b75d03ff154b3fe4c5208ed249ced8d69cc313a9fa40bb488" 348 | dependencies = [ 349 | "hashbrown", 350 | "ttf-parser", 351 | ] 352 | 353 | [[package]] 354 | name = "freedesktop-icon-lookup" 355 | version = "0.1.3" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "f3da575afdeef56733a55325c607492bb9871e098d008a37297cedcac356bf33" 358 | dependencies = [ 359 | "either", 360 | "thiserror", 361 | "tini", 362 | ] 363 | 364 | [[package]] 365 | name = "freedesktop_entry_parser" 366 | version = "1.3.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" 369 | dependencies = [ 370 | "nom", 371 | "thiserror", 372 | ] 373 | 374 | [[package]] 375 | name = "getrandom" 376 | version = "0.2.15" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 379 | dependencies = [ 380 | "cfg-if", 381 | "libc", 382 | "wasi", 383 | ] 384 | 385 | [[package]] 386 | name = "hashbrown" 387 | version = "0.14.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 390 | dependencies = [ 391 | "ahash", 392 | "allocator-api2", 393 | ] 394 | 395 | [[package]] 396 | name = "hermit-abi" 397 | version = "0.4.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 400 | 401 | [[package]] 402 | name = "hostname" 403 | version = "0.3.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" 406 | dependencies = [ 407 | "libc", 408 | "match_cfg", 409 | "winapi", 410 | ] 411 | 412 | [[package]] 413 | name = "humantime" 414 | version = "2.1.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 417 | 418 | [[package]] 419 | name = "imagesize" 420 | version = "0.12.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" 423 | 424 | [[package]] 425 | name = "indexmap" 426 | version = "2.4.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" 429 | dependencies = [ 430 | "equivalent", 431 | "hashbrown", 432 | ] 433 | 434 | [[package]] 435 | name = "itertools" 436 | version = "0.12.1" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 439 | dependencies = [ 440 | "either", 441 | ] 442 | 443 | [[package]] 444 | name = "itoa" 445 | version = "1.0.11" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 448 | 449 | [[package]] 450 | name = "kurbo" 451 | version = "0.9.5" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" 454 | dependencies = [ 455 | "arrayvec", 456 | ] 457 | 458 | [[package]] 459 | name = "kurbo" 460 | version = "0.10.4" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" 463 | dependencies = [ 464 | "arrayvec", 465 | "smallvec", 466 | ] 467 | 468 | [[package]] 469 | name = "levenshtein" 470 | version = "1.0.5" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" 473 | 474 | [[package]] 475 | name = "libc" 476 | version = "0.2.158" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 479 | 480 | [[package]] 481 | name = "libloading" 482 | version = "0.8.5" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" 485 | dependencies = [ 486 | "cfg-if", 487 | "windows-targets", 488 | ] 489 | 490 | [[package]] 491 | name = "libm" 492 | version = "0.2.8" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 495 | 496 | [[package]] 497 | name = "linux-raw-sys" 498 | version = "0.4.14" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 501 | 502 | [[package]] 503 | name = "log" 504 | version = "0.4.22" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 507 | 508 | [[package]] 509 | name = "lyon_geom" 510 | version = "1.0.5" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "edecfb8d234a2b0be031ab02ebcdd9f3b9ee418fb35e265f7a540a48d197bff9" 513 | dependencies = [ 514 | "arrayvec", 515 | "euclid", 516 | "num-traits", 517 | ] 518 | 519 | [[package]] 520 | name = "match_cfg" 521 | version = "0.1.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" 524 | 525 | [[package]] 526 | name = "memchr" 527 | version = "2.7.4" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 530 | 531 | [[package]] 532 | name = "memmap2" 533 | version = "0.8.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" 536 | dependencies = [ 537 | "libc", 538 | ] 539 | 540 | [[package]] 541 | name = "memmap2" 542 | version = "0.9.4" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" 545 | dependencies = [ 546 | "libc", 547 | ] 548 | 549 | [[package]] 550 | name = "minimal-lexical" 551 | version = "0.2.1" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 554 | 555 | [[package]] 556 | name = "miniz_oxide" 557 | version = "0.7.4" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 560 | dependencies = [ 561 | "adler", 562 | "simd-adler32", 563 | ] 564 | 565 | [[package]] 566 | name = "nix" 567 | version = "0.28.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 570 | dependencies = [ 571 | "bitflags 2.6.0", 572 | "cfg-if", 573 | "cfg_aliases", 574 | "libc", 575 | ] 576 | 577 | [[package]] 578 | name = "nom" 579 | version = "7.1.3" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 582 | dependencies = [ 583 | "memchr", 584 | "minimal-lexical", 585 | ] 586 | 587 | [[package]] 588 | name = "num-conv" 589 | version = "0.1.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 592 | 593 | [[package]] 594 | name = "num-traits" 595 | version = "0.2.19" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 598 | dependencies = [ 599 | "autocfg", 600 | "libm", 601 | ] 602 | 603 | [[package]] 604 | name = "num_threads" 605 | version = "0.1.7" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 608 | dependencies = [ 609 | "libc", 610 | ] 611 | 612 | [[package]] 613 | name = "once_cell" 614 | version = "1.19.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 617 | 618 | [[package]] 619 | name = "oneshot" 620 | version = "0.1.8" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29" 623 | 624 | [[package]] 625 | name = "pico-args" 626 | version = "0.5.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 629 | 630 | [[package]] 631 | name = "pin-project-lite" 632 | version = "0.2.14" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 635 | 636 | [[package]] 637 | name = "pkg-config" 638 | version = "0.3.30" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 641 | 642 | [[package]] 643 | name = "png" 644 | version = "0.17.13" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" 647 | dependencies = [ 648 | "bitflags 1.3.2", 649 | "crc32fast", 650 | "fdeflate", 651 | "flate2", 652 | "miniz_oxide", 653 | ] 654 | 655 | [[package]] 656 | name = "polling" 657 | version = "3.7.3" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" 660 | dependencies = [ 661 | "cfg-if", 662 | "concurrent-queue", 663 | "hermit-abi", 664 | "pin-project-lite", 665 | "rustix", 666 | "tracing", 667 | "windows-sys 0.59.0", 668 | ] 669 | 670 | [[package]] 671 | name = "powerfmt" 672 | version = "0.2.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 675 | 676 | [[package]] 677 | name = "proc-macro2" 678 | version = "1.0.86" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 681 | dependencies = [ 682 | "unicode-ident", 683 | ] 684 | 685 | [[package]] 686 | name = "quick-xml" 687 | version = "0.34.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" 690 | dependencies = [ 691 | "memchr", 692 | ] 693 | 694 | [[package]] 695 | name = "quickcheck" 696 | version = "1.0.3" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" 699 | dependencies = [ 700 | "rand", 701 | ] 702 | 703 | [[package]] 704 | name = "quickcheck_macros" 705 | version = "1.0.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" 708 | dependencies = [ 709 | "proc-macro2", 710 | "quote", 711 | "syn 1.0.109", 712 | ] 713 | 714 | [[package]] 715 | name = "quote" 716 | version = "1.0.37" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 719 | dependencies = [ 720 | "proc-macro2", 721 | ] 722 | 723 | [[package]] 724 | name = "rand" 725 | version = "0.8.5" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 728 | dependencies = [ 729 | "rand_core", 730 | ] 731 | 732 | [[package]] 733 | name = "rand_core" 734 | version = "0.6.4" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 737 | dependencies = [ 738 | "getrandom", 739 | ] 740 | 741 | [[package]] 742 | name = "raqote" 743 | version = "0.8.4" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "5c3061d5dcf59093c811d645c517be6eb7c26a0110d146730418950139496f84" 746 | dependencies = [ 747 | "euclid", 748 | "lyon_geom", 749 | "sw-composite", 750 | "typed-arena", 751 | ] 752 | 753 | [[package]] 754 | name = "regex" 755 | version = "1.10.6" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 758 | dependencies = [ 759 | "aho-corasick", 760 | "memchr", 761 | "regex-automata", 762 | "regex-syntax", 763 | ] 764 | 765 | [[package]] 766 | name = "regex-automata" 767 | version = "0.4.7" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 770 | dependencies = [ 771 | "aho-corasick", 772 | "memchr", 773 | "regex-syntax", 774 | ] 775 | 776 | [[package]] 777 | name = "regex-syntax" 778 | version = "0.8.4" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 781 | 782 | [[package]] 783 | name = "resvg" 784 | version = "0.40.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "024e40e1ba7313fc315b1720298988c0cd6f8bfe3754b52838aafecebd11355a" 787 | dependencies = [ 788 | "log", 789 | "pico-args", 790 | "rgb", 791 | "svgtypes", 792 | "tiny-skia", 793 | "usvg", 794 | ] 795 | 796 | [[package]] 797 | name = "rgb" 798 | version = "0.8.48" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" 801 | dependencies = [ 802 | "bytemuck", 803 | ] 804 | 805 | [[package]] 806 | name = "roxmltree" 807 | version = "0.19.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" 810 | 811 | [[package]] 812 | name = "rustix" 813 | version = "0.38.35" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" 816 | dependencies = [ 817 | "bitflags 2.6.0", 818 | "errno", 819 | "libc", 820 | "linux-raw-sys", 821 | "windows-sys 0.52.0", 822 | ] 823 | 824 | [[package]] 825 | name = "scoped-tls" 826 | version = "1.0.1" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 829 | 830 | [[package]] 831 | name = "serde" 832 | version = "1.0.209" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" 835 | dependencies = [ 836 | "serde_derive", 837 | ] 838 | 839 | [[package]] 840 | name = "serde_derive" 841 | version = "1.0.209" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" 844 | dependencies = [ 845 | "proc-macro2", 846 | "quote", 847 | "syn 2.0.76", 848 | ] 849 | 850 | [[package]] 851 | name = "serde_spanned" 852 | version = "0.6.7" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 855 | dependencies = [ 856 | "serde", 857 | ] 858 | 859 | [[package]] 860 | name = "shlex" 861 | version = "1.3.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 864 | 865 | [[package]] 866 | name = "simd-adler32" 867 | version = "0.3.7" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 870 | 871 | [[package]] 872 | name = "simplecss" 873 | version = "0.2.1" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" 876 | dependencies = [ 877 | "log", 878 | ] 879 | 880 | [[package]] 881 | name = "siphasher" 882 | version = "0.3.11" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 885 | 886 | [[package]] 887 | name = "slab" 888 | version = "0.4.9" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 891 | dependencies = [ 892 | "autocfg", 893 | ] 894 | 895 | [[package]] 896 | name = "smallvec" 897 | version = "1.13.2" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 900 | 901 | [[package]] 902 | name = "smithay-client-toolkit" 903 | version = "0.18.1" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" 906 | dependencies = [ 907 | "bitflags 2.6.0", 908 | "bytemuck", 909 | "calloop", 910 | "calloop-wayland-source", 911 | "cursor-icon", 912 | "libc", 913 | "log", 914 | "memmap2 0.9.4", 915 | "pkg-config", 916 | "rustix", 917 | "thiserror", 918 | "wayland-backend", 919 | "wayland-client", 920 | "wayland-csd-frame", 921 | "wayland-cursor", 922 | "wayland-protocols", 923 | "wayland-protocols-wlr", 924 | "wayland-scanner", 925 | "xkbcommon", 926 | "xkeysym", 927 | ] 928 | 929 | [[package]] 930 | name = "strict-num" 931 | version = "0.1.1" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 934 | dependencies = [ 935 | "float-cmp", 936 | ] 937 | 938 | [[package]] 939 | name = "sublime_fuzzy" 940 | version = "0.7.0" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "fa7986063f7c0ab374407e586d7048a3d5aac94f103f751088bf398e07cd5400" 943 | 944 | [[package]] 945 | name = "svgtypes" 946 | version = "0.14.0" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "59d7618f12b51be8171a7cfdda1e7a93f79cbc57c4e7adf89a749cf671125241" 949 | dependencies = [ 950 | "kurbo 0.10.4", 951 | "siphasher", 952 | ] 953 | 954 | [[package]] 955 | name = "sw-composite" 956 | version = "0.7.16" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "9ac8fb7895b4afa060ad731a32860db8755da3449a47e796d5ecf758db2671d4" 959 | 960 | [[package]] 961 | name = "syn" 962 | version = "1.0.109" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 965 | dependencies = [ 966 | "proc-macro2", 967 | "quote", 968 | "unicode-ident", 969 | ] 970 | 971 | [[package]] 972 | name = "syn" 973 | version = "2.0.76" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" 976 | dependencies = [ 977 | "proc-macro2", 978 | "quote", 979 | "unicode-ident", 980 | ] 981 | 982 | [[package]] 983 | name = "syslog" 984 | version = "6.1.1" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "dfc7e95b5b795122fafe6519e27629b5ab4232c73ebb2428f568e82b1a457ad3" 987 | dependencies = [ 988 | "error-chain", 989 | "hostname", 990 | "libc", 991 | "log", 992 | "time", 993 | ] 994 | 995 | [[package]] 996 | name = "test-case" 997 | version = "3.3.1" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" 1000 | dependencies = [ 1001 | "test-case-macros", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "test-case-core" 1006 | version = "3.3.1" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" 1009 | dependencies = [ 1010 | "cfg-if", 1011 | "proc-macro2", 1012 | "quote", 1013 | "syn 2.0.76", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "test-case-macros" 1018 | version = "3.3.1" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" 1021 | dependencies = [ 1022 | "proc-macro2", 1023 | "quote", 1024 | "syn 2.0.76", 1025 | "test-case-core", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "thiserror" 1030 | version = "1.0.63" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 1033 | dependencies = [ 1034 | "thiserror-impl", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "thiserror-impl" 1039 | version = "1.0.63" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 1042 | dependencies = [ 1043 | "proc-macro2", 1044 | "quote", 1045 | "syn 2.0.76", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "time" 1050 | version = "0.3.36" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1053 | dependencies = [ 1054 | "deranged", 1055 | "itoa", 1056 | "libc", 1057 | "num-conv", 1058 | "num_threads", 1059 | "powerfmt", 1060 | "serde", 1061 | "time-core", 1062 | "time-macros", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "time-core" 1067 | version = "0.1.2" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1070 | 1071 | [[package]] 1072 | name = "time-macros" 1073 | version = "0.2.18" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1076 | dependencies = [ 1077 | "num-conv", 1078 | "time-core", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "tini" 1083 | version = "1.3.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "e004df4c5f0805eb5f55883204a514cfa43a6d924741be29e871753a53d5565a" 1086 | 1087 | [[package]] 1088 | name = "tiny-skia" 1089 | version = "0.11.4" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" 1092 | dependencies = [ 1093 | "arrayref", 1094 | "arrayvec", 1095 | "bytemuck", 1096 | "cfg-if", 1097 | "log", 1098 | "png", 1099 | "tiny-skia-path", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "tiny-skia-path" 1104 | version = "0.11.4" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" 1107 | dependencies = [ 1108 | "arrayref", 1109 | "bytemuck", 1110 | "strict-num", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "toml" 1115 | version = "0.8.19" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1118 | dependencies = [ 1119 | "serde", 1120 | "serde_spanned", 1121 | "toml_datetime", 1122 | "toml_edit", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "toml_datetime" 1127 | version = "0.6.8" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1130 | dependencies = [ 1131 | "serde", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "toml_edit" 1136 | version = "0.22.20" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 1139 | dependencies = [ 1140 | "indexmap", 1141 | "serde", 1142 | "serde_spanned", 1143 | "toml_datetime", 1144 | "winnow", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "tracing" 1149 | version = "0.1.40" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1152 | dependencies = [ 1153 | "pin-project-lite", 1154 | "tracing-core", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "tracing-core" 1159 | version = "0.1.32" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1162 | 1163 | [[package]] 1164 | name = "ttf-parser" 1165 | version = "0.20.0" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" 1168 | 1169 | [[package]] 1170 | name = "typed-arena" 1171 | version = "2.0.2" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 1174 | 1175 | [[package]] 1176 | name = "unicode-ident" 1177 | version = "1.0.12" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1180 | 1181 | [[package]] 1182 | name = "unicode-segmentation" 1183 | version = "1.11.0" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1186 | 1187 | [[package]] 1188 | name = "usvg" 1189 | version = "0.40.0" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "c04150a94f0bfc3b2c15d4e151524d14cd06765fc6641d8b1c59a248360d4474" 1192 | dependencies = [ 1193 | "base64", 1194 | "data-url", 1195 | "flate2", 1196 | "imagesize", 1197 | "kurbo 0.9.5", 1198 | "log", 1199 | "pico-args", 1200 | "roxmltree", 1201 | "simplecss", 1202 | "siphasher", 1203 | "strict-num", 1204 | "svgtypes", 1205 | "tiny-skia-path", 1206 | "xmlwriter", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "version_check" 1211 | version = "0.9.5" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1214 | 1215 | [[package]] 1216 | name = "wasi" 1217 | version = "0.11.0+wasi-snapshot-preview1" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1220 | 1221 | [[package]] 1222 | name = "wayland-backend" 1223 | version = "0.3.6" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" 1226 | dependencies = [ 1227 | "cc", 1228 | "downcast-rs", 1229 | "rustix", 1230 | "scoped-tls", 1231 | "smallvec", 1232 | "wayland-sys", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "wayland-client" 1237 | version = "0.31.5" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" 1240 | dependencies = [ 1241 | "bitflags 2.6.0", 1242 | "rustix", 1243 | "wayland-backend", 1244 | "wayland-scanner", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "wayland-csd-frame" 1249 | version = "0.3.0" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" 1252 | dependencies = [ 1253 | "bitflags 2.6.0", 1254 | "cursor-icon", 1255 | "wayland-backend", 1256 | ] 1257 | 1258 | [[package]] 1259 | name = "wayland-cursor" 1260 | version = "0.31.5" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" 1263 | dependencies = [ 1264 | "rustix", 1265 | "wayland-client", 1266 | "xcursor", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "wayland-protocols" 1271 | version = "0.31.2" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" 1274 | dependencies = [ 1275 | "bitflags 2.6.0", 1276 | "wayland-backend", 1277 | "wayland-client", 1278 | "wayland-scanner", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "wayland-protocols-wlr" 1283 | version = "0.2.0" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" 1286 | dependencies = [ 1287 | "bitflags 2.6.0", 1288 | "wayland-backend", 1289 | "wayland-client", 1290 | "wayland-protocols", 1291 | "wayland-scanner", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "wayland-scanner" 1296 | version = "0.31.4" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" 1299 | dependencies = [ 1300 | "proc-macro2", 1301 | "quick-xml", 1302 | "quote", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "wayland-sys" 1307 | version = "0.31.4" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" 1310 | dependencies = [ 1311 | "dlib", 1312 | "log", 1313 | "pkg-config", 1314 | ] 1315 | 1316 | [[package]] 1317 | name = "winapi" 1318 | version = "0.3.9" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1321 | dependencies = [ 1322 | "winapi-i686-pc-windows-gnu", 1323 | "winapi-x86_64-pc-windows-gnu", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "winapi-i686-pc-windows-gnu" 1328 | version = "0.4.0" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1331 | 1332 | [[package]] 1333 | name = "winapi-x86_64-pc-windows-gnu" 1334 | version = "0.4.0" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1337 | 1338 | [[package]] 1339 | name = "windows-sys" 1340 | version = "0.52.0" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1343 | dependencies = [ 1344 | "windows-targets", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "windows-sys" 1349 | version = "0.59.0" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1352 | dependencies = [ 1353 | "windows-targets", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "windows-targets" 1358 | version = "0.52.6" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1361 | dependencies = [ 1362 | "windows_aarch64_gnullvm", 1363 | "windows_aarch64_msvc", 1364 | "windows_i686_gnu", 1365 | "windows_i686_gnullvm", 1366 | "windows_i686_msvc", 1367 | "windows_x86_64_gnu", 1368 | "windows_x86_64_gnullvm", 1369 | "windows_x86_64_msvc", 1370 | ] 1371 | 1372 | [[package]] 1373 | name = "windows_aarch64_gnullvm" 1374 | version = "0.52.6" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1377 | 1378 | [[package]] 1379 | name = "windows_aarch64_msvc" 1380 | version = "0.52.6" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1383 | 1384 | [[package]] 1385 | name = "windows_i686_gnu" 1386 | version = "0.52.6" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1389 | 1390 | [[package]] 1391 | name = "windows_i686_gnullvm" 1392 | version = "0.52.6" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1395 | 1396 | [[package]] 1397 | name = "windows_i686_msvc" 1398 | version = "0.52.6" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1401 | 1402 | [[package]] 1403 | name = "windows_x86_64_gnu" 1404 | version = "0.52.6" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1407 | 1408 | [[package]] 1409 | name = "windows_x86_64_gnullvm" 1410 | version = "0.52.6" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1413 | 1414 | [[package]] 1415 | name = "windows_x86_64_msvc" 1416 | version = "0.52.6" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1419 | 1420 | [[package]] 1421 | name = "winnow" 1422 | version = "0.6.18" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 1425 | dependencies = [ 1426 | "memchr", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "xcursor" 1431 | version = "0.3.8" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" 1434 | 1435 | [[package]] 1436 | name = "xdg" 1437 | version = "2.5.2" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 1440 | 1441 | [[package]] 1442 | name = "xkbcommon" 1443 | version = "0.7.0" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" 1446 | dependencies = [ 1447 | "libc", 1448 | "memmap2 0.8.0", 1449 | "xkeysym", 1450 | ] 1451 | 1452 | [[package]] 1453 | name = "xkeysym" 1454 | version = "0.2.1" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" 1457 | dependencies = [ 1458 | "bytemuck", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "xmlwriter" 1463 | version = "0.1.0" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" 1466 | 1467 | [[package]] 1468 | name = "yeslogic-fontconfig-sys" 1469 | version = "5.0.0" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" 1472 | dependencies = [ 1473 | "cstr", 1474 | "dlib", 1475 | "once_cell", 1476 | "pkg-config", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "yofi" 1481 | version = "0.2.2" 1482 | dependencies = [ 1483 | "anyhow", 1484 | "argh", 1485 | "defaults", 1486 | "either", 1487 | "fern", 1488 | "fontconfig", 1489 | "fontdue", 1490 | "freedesktop-icon-lookup", 1491 | "freedesktop_entry_parser", 1492 | "humantime", 1493 | "itertools", 1494 | "levenshtein", 1495 | "libc", 1496 | "log", 1497 | "nix", 1498 | "nom", 1499 | "once_cell", 1500 | "oneshot", 1501 | "png", 1502 | "quickcheck", 1503 | "quickcheck_macros", 1504 | "raqote", 1505 | "regex", 1506 | "resvg", 1507 | "serde", 1508 | "shlex", 1509 | "smithay-client-toolkit", 1510 | "sublime_fuzzy", 1511 | "syslog", 1512 | "test-case", 1513 | "toml", 1514 | "unicode-segmentation", 1515 | "xdg", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "zerocopy" 1520 | version = "0.7.35" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1523 | dependencies = [ 1524 | "zerocopy-derive", 1525 | ] 1526 | 1527 | [[package]] 1528 | name = "zerocopy-derive" 1529 | version = "0.7.35" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1532 | dependencies = [ 1533 | "proc-macro2", 1534 | "quote", 1535 | "syn 2.0.76", 1536 | ] 1537 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yofi" 3 | version = "0.2.2" 4 | authors = ["Kitsu "] 5 | edition = "2021" 6 | 7 | description = "minimalistic menu for wayland" 8 | repository = "https://github.com/l4l/yofi" 9 | license = "MIT" 10 | keywords = ["application-launcher", "menu", "wayland", "wlroots-based-menu", "dmenu-replacement"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [profile.release-lto] 15 | lto = true 16 | inherits = "release" 17 | 18 | [dependencies] 19 | anyhow = "1.0.80" 20 | argh = "0.1.12" 21 | defaults = "0.2.0" 22 | either = "1.10.0" 23 | fep = { version = "1.3.0", package = "freedesktop_entry_parser" } 24 | fern = { version = "0.6.2", features = ["syslog-6"] } 25 | fontconfig = { version = "0.8.0", features = ["dlopen"] } 26 | fontdue = "0.8.0" 27 | freedesktop-icon-lookup = "0.1.3" 28 | humantime = "2.1.0" 29 | itertools = "0.12.1" 30 | levenshtein = "1.0.5" 31 | libc = "0.2.153" 32 | log = "0.4.21" 33 | nix = { version = "0.28.0", features = ["fs", "process"] } 34 | nom = { version = "7.1.3", default-features = false, features = ["std"] } 35 | once_cell = "1.19.0" 36 | oneshot = { version = "0.1.6", default-features = false, features = ["std"] } 37 | png = { version = "0.17.13", default-features = false } 38 | raqote = { version = "0.8.3", default-features = false } 39 | regex = { version = "1.10.3", default-features = false, features = ["std", "perf-inline"] } 40 | resvg = { version = "0.40.0", default-features = false } 41 | sctk = { version = "0.18.1", package = "smithay-client-toolkit", features = ["calloop"] } 42 | serde = { version = "1.0.197", features = ["derive"] } 43 | shlex = "1.3.0" 44 | sublime_fuzzy = "0.7.0" 45 | syslog = "6.1.0" 46 | toml = { version = "0.8.10", default-features = false, features = ["parse"] } 47 | unicode-segmentation = "1.11.0" 48 | xdg = "2.5.2" 49 | 50 | [dev-dependencies] 51 | quickcheck = { version = "1.0.3", default-features = false } 52 | quickcheck_macros = "1.0.0" 53 | test-case = "3.3.1" 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 kitsu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yofi 2 | 3 | ![ci_badge](https://github.com/l4l/yofi/workflows/CI/badge.svg?branch=master) 4 | 5 | .. is a minimalistic menu for Wayland-based compositors. 6 | 7 | ## Installation 8 | 9 | Make sure you setup a wayland environment, in particularly `WAYLAND_DISPLAY` 10 | env var must be set. `wlr_layer_shell` protocol is not necessary but preferred. 11 | There are several installation options: 12 | 13 | - Pre-built release binaries are published at the [Release page](https://github.com/l4l/yofi/releases). 14 | Although these are built in Ubuntu environment it should also work for other Linux distributions. 15 | - \[for Archlinux\] there are [yofi-bin](https://aur.archlinux.org/packages/yofi-bin/) and 16 | [yofi-git](https://aur.archlinux.org/packages/yofi-git/) AUR packages for binary and from-source builds. 17 | - Build last release version from crates.io with `cargo install yofi`. 18 | - Build with [nix](https://nixos.org): `nix profile install github:l4l/yofi`. 19 | - Or you can manually [build from sources](#building). 20 | 21 | ## User documentation 22 | 23 | User documentation is located at [Wiki pages](https://github.com/l4l/yofi/wiki). 24 | Feel free to [open an issue](https://github.com/l4l/yofi/issues/new) if something 25 | is unclear, missing or outdated. 26 | 27 | ## Building 28 | 29 | ### Cargo 30 | 31 | For building the project you need rust compiler and cargo package manager 32 | (usually distributed via [rustup](https://rustup.rs/)). Once installed you 33 | may build & run the project with the following command: 34 | 35 | ```bash 36 | cargo run --release 37 | ``` 38 | 39 | ### Nix 40 | 41 | You can build project using [nix](https://nixos.org): 42 | 43 | ```bash 44 | nix build 45 | ``` 46 | 47 | ## Contributing 48 | 49 | Contributions are welcome, but make sure that: 50 | 51 | - \[If that's a new feature or it changes the existing behavior\] you've discussed it in the issue page before the implementation. 52 | - Your patch is not a refactoring. 53 | - rustfmt and clippy are checked. 54 | - \[optionally\] Added docs if necessary and an entry in CHANGELOG.md. 55 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1724748588, 24 | "narHash": "sha256-NlpGA4+AIf1dKNq76ps90rxowlFXUsV9x7vK/mN37JM=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "a6292e34000dc93d43bccf78338770c1c5ec8a99", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | flake-utils.url = "github:numtide/flake-utils"; 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | }; 6 | 7 | outputs = { self, flake-utils, nixpkgs }: 8 | flake-utils.lib.eachDefaultSystem (system: 9 | let 10 | inherit (nixpkgs) lib; 11 | 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | rpath = lib.makeLibraryPath (with pkgs; [ 14 | fontconfig 15 | libxkbcommon 16 | wayland 17 | ]); 18 | in 19 | { 20 | packages.default = pkgs.rustPlatform.buildRustPackage { 21 | pname = "yofi"; 22 | inherit ((lib.importTOML (self + "/Cargo.toml")).package) version; 23 | 24 | src = self; 25 | 26 | cargoLock.lockFile = self + "/Cargo.lock"; 27 | 28 | nativeBuildInputs = with pkgs; [ 29 | pkg-config 30 | ]; 31 | 32 | buildInputs = with pkgs; [ 33 | libxkbcommon 34 | ]; 35 | 36 | postFixup = '' 37 | patchelf $out/bin/yofi --add-rpath ${rpath} 38 | ''; 39 | }; 40 | 41 | devShells.default = pkgs.mkShell { 42 | nativeBuildInputs = with pkgs; [ 43 | rustc 44 | cargo 45 | pkg-config 46 | libxkbcommon 47 | ]; 48 | 49 | LD_LIBRARY_PATH = rpath; 50 | }; 51 | } 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.76.0 2 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use raqote::SolidSource; 3 | use serde::Deserialize; 4 | 5 | #[derive(Deserialize, Clone, Copy)] 6 | #[cfg_attr(test, derive(Debug, PartialEq))] 7 | #[serde(from = "ColorDeser")] 8 | pub struct Color(u32); 9 | 10 | impl Color { 11 | pub const fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { 12 | Self(u32::from_be_bytes([r, g, b, a])) 13 | } 14 | 15 | pub const fn to_rgba(self) -> [u8; 4] { 16 | self.0.to_be_bytes() 17 | } 18 | 19 | pub fn as_source(self) -> SolidSource { 20 | let [r, g, b, a] = self.to_rgba(); 21 | SolidSource::from_unpremultiplied_argb(a, r, g, b) 22 | } 23 | } 24 | 25 | impl std::ops::Deref for Color { 26 | type Target = u32; 27 | 28 | fn deref(&self) -> &Self::Target { 29 | &self.0 30 | } 31 | } 32 | 33 | impl From for Color { 34 | fn from(value: ColorDeser) -> Self { 35 | match value { 36 | ColorDeser::Int(x) => Self(x), 37 | ColorDeser::String(ColorDeserString(c)) => c, 38 | } 39 | } 40 | } 41 | 42 | #[derive(serde::Deserialize)] 43 | #[serde(untagged)] 44 | enum ColorDeser { 45 | Int(u32), 46 | String(ColorDeserString), 47 | } 48 | 49 | #[derive(serde::Deserialize)] 50 | #[serde(try_from = "String")] 51 | struct ColorDeserString(Color); 52 | 53 | impl TryFrom for ColorDeserString { 54 | type Error = anyhow::Error; 55 | 56 | fn try_from(value: String) -> Result { 57 | let part = match value.chars().next() { 58 | None => anyhow::bail!("color cannot be empty"), 59 | Some('#') => value.split_at(1).1, 60 | Some(_) => { 61 | anyhow::bail!("color can be either decimal or hex number prefixed with '#'") 62 | } 63 | }; 64 | 65 | let decoded = u32::from_str_radix(part, 16).context("parse hex number"); 66 | Ok(Self(match (decoded, part.len()) { 67 | (Ok(d), 3) => { 68 | let (r, g, b) = ( 69 | ((d & 0xf00) >> 8) as u8, 70 | ((d & 0xf0) >> 4) as u8, 71 | (d & 0xf) as u8, 72 | ); 73 | Color::from_rgba(r << 4 | r, g << 4 | g, b << 4 | b, 0xff) 74 | } 75 | (Ok(d), 6) => Color(d << 8 | 0xff), 76 | (Ok(d), 8) => Color(d), 77 | (e, _) => anyhow::bail!( 78 | "hex color can only be specified in #RGB, #RRGGBB, or #RRGGBBAA format, {e:?}" 79 | ), 80 | })) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | use test_case::test_case; 88 | 89 | #[test_case(r##"x = 1234"##, Color(1234); "decimal number")] 90 | #[test_case(r##"x = 0x5432"##, Color(0x5432); "hex number")] 91 | #[test_case(r##"x = "#123""##, Color::from_rgba(0x11, 0x22, 0x33, 0xff); "3-sym css")] 92 | #[test_case(r##"x = "#123456""##, Color::from_rgba(0x12, 0x34, 0x56, 0xff); "6-sym css")] 93 | #[test_case(r##"x = "#12345678""##, Color::from_rgba(0x12, 0x34, 0x56, 0x78); "8-sym css")] 94 | fn deser_color(s: &str, expected: Color) { 95 | #[derive(Deserialize)] 96 | struct T { 97 | x: Color, 98 | } 99 | let c = toml::from_str::(s).unwrap().x; 100 | assert_eq!(c, expected); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::path::PathBuf; 3 | 4 | use anyhow::{Context, Result}; 5 | use defaults::Defaults; 6 | use serde::Deserialize; 7 | 8 | use crate::style::{Margin, Padding, Radius}; 9 | use crate::Color; 10 | 11 | const DEFAULT_CONFIG_NAME: &str = concat!(crate::prog_name!(), ".config"); 12 | 13 | const DEFAULT_ICON_SIZE: u16 = 16; 14 | const DEFAULT_FONT_SIZE: u16 = 24; 15 | 16 | const DEFAULT_FONT_COLOR: Color = Color::from_rgba(0xf8, 0xf8, 0xf2, 0xff); 17 | const DEFAULT_BG_COLOR: Color = Color::from_rgba(0x27, 0x28, 0x22, 0xee); 18 | const DEFAULT_INPUT_BG_COLOR: Color = Color::from_rgba(0x75, 0x71, 0x5e, 0xc0); 19 | const DEFAULT_SELECTED_FONT_COLOR: Color = Color::from_rgba(0xa6, 0xe2, 0x2e, 0xff); 20 | 21 | const DEFAULT_BG_BORDER_COLOR: Color = Color::from_rgba(0x13, 0x14, 0x11, 0xff); 22 | const DEFAULT_BG_BORDER_WIDTH: f32 = 2.0; 23 | 24 | mod params; 25 | 26 | #[derive(Defaults, Deserialize)] 27 | #[serde(default)] 28 | pub struct Config { 29 | #[def = "400"] 30 | width: u32, 31 | #[def = "512"] 32 | height: u32, 33 | #[def = "false"] 34 | force_window: bool, 35 | window_offsets: Option<(i32, i32)>, 36 | scale: Option, 37 | term: Option, 38 | font: Option, 39 | font_size: Option, 40 | bg_color: Option, 41 | bg_border_color: Option, 42 | bg_border_width: Option, 43 | font_color: Option, 44 | #[def = "Radius::all(0.0)"] 45 | corner_radius: Radius, 46 | 47 | icon: Option, 48 | 49 | input_text: InputText, 50 | list_items: ListItems, 51 | mouse: Mouse, 52 | } 53 | 54 | impl Config { 55 | pub fn disable_icons(&mut self) { 56 | self.icon = None; 57 | } 58 | 59 | pub fn override_prompt(&mut self, prompt: String) { 60 | self.input_text.prompt = Some(prompt); 61 | } 62 | 63 | pub fn override_password(&mut self) { 64 | self.input_text.password = true; 65 | } 66 | } 67 | 68 | #[derive(Defaults, Deserialize)] 69 | #[serde(default)] 70 | struct InputText { 71 | font: Option, 72 | font_size: Option, 73 | bg_color: Option, 74 | font_color: Option, 75 | prompt_color: Option, 76 | prompt: Option, 77 | password: bool, 78 | #[def = "Margin::all(5.0)"] 79 | margin: Margin, 80 | #[def = "Padding::from_pair(1.7, -4.0)"] 81 | padding: Padding, 82 | #[def = "Radius::all(f32::MAX)"] 83 | corner_radius: Radius, 84 | } 85 | 86 | #[derive(Defaults, Deserialize)] 87 | #[serde(default)] 88 | struct ListItems { 89 | font: Option, 90 | font_size: Option, 91 | font_color: Option, 92 | selected_font_color: Option, 93 | match_color: Option, 94 | #[def = "Margin { top: 10.0, ..Margin::from_pair(5.0, 15.0) }"] 95 | margin: Margin, 96 | #[def = "false"] 97 | hide_actions: bool, 98 | #[def = "60.0"] 99 | action_left_margin: f32, 100 | #[def = "2.0"] 101 | item_spacing: f32, 102 | #[def = "10.0"] 103 | icon_spacing: f32, 104 | } 105 | 106 | #[derive(Defaults, Deserialize)] 107 | #[serde(default)] 108 | struct Icon { 109 | #[def = "DEFAULT_ICON_SIZE"] 110 | size: u16, 111 | theme: Option, 112 | fallback_icon_path: Option, 113 | } 114 | 115 | #[derive(Defaults, Deserialize)] 116 | #[serde(default)] 117 | struct Mouse { 118 | launch_on_middle: bool, 119 | wheel_scroll_multiplier: f64, 120 | } 121 | 122 | fn default_config_path() -> Result> { 123 | let file = xdg::BaseDirectories::with_prefix(crate::prog_name!()) 124 | .context("failed to get xdg dirs")? 125 | .get_config_file(DEFAULT_CONFIG_NAME); 126 | if file 127 | .try_exists() 128 | .with_context(|| format!("reading default config at {}", file.display()))? 129 | { 130 | Ok(Some(file)) 131 | } else { 132 | Ok(None) 133 | } 134 | } 135 | 136 | impl Config { 137 | pub fn load(path: Option) -> Result { 138 | let path = match path { 139 | Some(p) => p, 140 | None => match default_config_path()? { 141 | Some(path) => path, 142 | None => return Ok(Config::default()), 143 | }, 144 | }; 145 | match std::fs::read_to_string(&path) { 146 | Ok(c) => toml::from_str(&c).context("invalid config"), 147 | Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => Ok(Config::default()), 148 | Err(e) => { 149 | Err(anyhow::Error::new(e).context(format!("config read at {}", path.display()))) 150 | } 151 | } 152 | } 153 | 154 | pub fn param<'a, T>(&'a self) -> T 155 | where 156 | T: From<&'a Self>, 157 | { 158 | self.into() 159 | } 160 | 161 | pub fn terminal_command(&self) -> Vec { 162 | if let Some(cmd) = self.term.as_ref() { 163 | shlex::split(cmd) 164 | .unwrap() 165 | .into_iter() 166 | .map(|s| CString::new(s).unwrap()) 167 | .collect::>() 168 | } else { 169 | vec![] 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/config/params.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::path::Path; 4 | use std::rc::Rc; 5 | 6 | use once_cell::unsync::Lazy; 7 | 8 | use super::*; 9 | use crate::desktop::IconConfig; 10 | use crate::draw::{BgParams, InputTextParams, ListParams}; 11 | use crate::font::{Font, FontBackend, InnerFont}; 12 | use crate::icon::Icon; 13 | use crate::window::{Params as WindowParams, PointerParams}; 14 | 15 | macro_rules! select_conf { 16 | ($config:ident, $inner:ident, $field:ident) => { 17 | $config 18 | .$inner 19 | .$field 20 | .as_ref() 21 | .or_else(|| $config.$field.as_ref()) 22 | }; 23 | } 24 | 25 | impl<'a> From<&'a Config> for InputTextParams<'a> { 26 | fn from(config: &'a Config) -> InputTextParams<'a> { 27 | let font_color = select_conf!(config, input_text, font_color) 28 | .copied() 29 | .unwrap_or(DEFAULT_FONT_COLOR); 30 | 31 | InputTextParams { 32 | font: select_conf!(config, input_text, font) 33 | .map(font_by_name) 34 | .unwrap_or_else(default_font), 35 | font_size: select_conf!(config, input_text, font_size) 36 | .copied() 37 | .unwrap_or(DEFAULT_FONT_SIZE), 38 | bg_color: select_conf!(config, input_text, bg_color) 39 | .copied() 40 | .unwrap_or(DEFAULT_INPUT_BG_COLOR), 41 | font_color, 42 | prompt_color: config.input_text.prompt_color.unwrap_or_else(|| { 43 | let [r, g, b, a] = font_color.to_rgba(); 44 | Color::from_rgba(r, g, b, (a / 4).wrapping_mul(3)) 45 | }), 46 | prompt: config.input_text.prompt.as_deref(), 47 | password: config.input_text.password, 48 | margin: config.input_text.margin.clone(), 49 | padding: config.input_text.padding.clone(), 50 | radius: config.input_text.corner_radius.clone(), 51 | } 52 | } 53 | } 54 | 55 | impl<'a> From<&'a Config> for ListParams { 56 | fn from(config: &'a Config) -> ListParams { 57 | ListParams { 58 | font: select_conf!(config, list_items, font) 59 | .map(font_by_name) 60 | .unwrap_or_else(default_font), 61 | font_size: select_conf!(config, list_items, font_size) 62 | .copied() 63 | .unwrap_or(DEFAULT_FONT_SIZE), 64 | font_color: select_conf!(config, list_items, font_color) 65 | .copied() 66 | .unwrap_or(DEFAULT_FONT_COLOR), 67 | selected_font_color: config 68 | .list_items 69 | .selected_font_color 70 | .unwrap_or(DEFAULT_SELECTED_FONT_COLOR), 71 | match_color: config.list_items.match_color, 72 | icon_size: config.icon.as_ref().map(|c| c.size), 73 | fallback_icon: config 74 | .icon 75 | .as_ref() 76 | .and_then(|i| i.fallback_icon_path.as_ref()) 77 | .map(Icon::new), 78 | margin: config.list_items.margin.clone(), 79 | hide_actions: config.list_items.hide_actions, 80 | action_left_margin: config.list_items.action_left_margin, 81 | item_spacing: config.list_items.item_spacing, 82 | icon_spacing: config.list_items.icon_spacing, 83 | } 84 | } 85 | } 86 | 87 | impl<'a> From<&'a Config> for BgParams { 88 | fn from(config: &'a Config) -> BgParams { 89 | let border = match (config.bg_border_color, config.bg_border_width) { 90 | (None, None) => None, 91 | (Some(c), Some(w)) => Some((c, w)), 92 | (Some(c), None) => Some((c, DEFAULT_BG_BORDER_WIDTH)), 93 | (None, Some(w)) => Some((DEFAULT_BG_BORDER_COLOR, w)), 94 | }; 95 | BgParams { 96 | width: config.width, 97 | height: config.height, 98 | radius: config.corner_radius.clone(), 99 | color: config.bg_color.unwrap_or(DEFAULT_BG_COLOR), 100 | border, 101 | } 102 | } 103 | } 104 | 105 | impl<'a> From<&'a Config> for WindowParams { 106 | fn from(config: &'a Config) -> WindowParams { 107 | WindowParams { 108 | width: config.width, 109 | height: config.height, 110 | force_window: config.force_window, 111 | window_offsets: config.window_offsets, 112 | scale: config.scale, 113 | } 114 | } 115 | } 116 | 117 | impl<'a> From<&'a Config> for Option { 118 | fn from(config: &'a Config) -> Option { 119 | config.icon.as_ref().map(|c| IconConfig { 120 | icon_size: c.size, 121 | theme: c.theme.clone(), 122 | }) 123 | } 124 | } 125 | 126 | impl<'a> From<&'a Config> for PointerParams { 127 | fn from(config: &'a Config) -> Self { 128 | Self { 129 | launch_on_middle: config.mouse.launch_on_middle, 130 | wheel_scroll_multiplier: config.mouse.wheel_scroll_multiplier, 131 | } 132 | } 133 | } 134 | 135 | fn default_font() -> Font { 136 | std::thread_local! { 137 | static DEFAULT_FONT: Lazy = Lazy::new(|| Rc::new(InnerFont::default())); 138 | } 139 | DEFAULT_FONT.with(|f| Rc::clone(f)) 140 | } 141 | 142 | fn font_by_name(name: impl AsRef) -> Font { 143 | std::thread_local! { 144 | static LOADED_FONTS: RefCell> = RefCell::new(HashMap::new()); 145 | } 146 | 147 | let name = name.as_ref(); 148 | 149 | if let Some(font) = LOADED_FONTS.with(|fonts| fonts.borrow().get(name).cloned()) { 150 | return font; 151 | } 152 | 153 | let path = Path::new(name); 154 | let font = if path.is_absolute() && path.exists() { 155 | InnerFont::font_by_path(path) 156 | } else { 157 | InnerFont::font_by_name(name) 158 | }; 159 | let font = Rc::new(font.unwrap_or_else(|e| panic!("cannot find font {}: {}", name, e))); 160 | LOADED_FONTS.with(|fonts| fonts.borrow_mut().insert(name.to_owned(), font.clone())); 161 | font 162 | } 163 | -------------------------------------------------------------------------------- /src/desktop.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fs::{self, DirEntry}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use freedesktop_icon_lookup::{Cache, LookupParam}; 6 | use once_cell::sync::OnceCell; 7 | use xdg::BaseDirectories; 8 | 9 | use crate::icon::Icon; 10 | 11 | mod locale; 12 | 13 | pub static XDG_DIRS: OnceCell = OnceCell::new(); 14 | 15 | pub struct ExecEntry { 16 | pub name: String, 17 | pub exec: String, 18 | pub icon: Option, 19 | } 20 | 21 | pub struct Entry { 22 | pub entry: ExecEntry, 23 | pub actions: Vec, 24 | pub desktop_fname: String, 25 | pub path: PathBuf, 26 | pub name: String, 27 | pub is_terminal: bool, 28 | } 29 | 30 | impl Entry { 31 | pub fn subname(&self, action: usize) -> Option<&str> { 32 | self.actions 33 | .get(action.checked_sub(1)?) 34 | .map(|a| a.name.as_ref()) 35 | } 36 | 37 | pub fn icon(&self, action: usize) -> Option<&Icon> { 38 | if action == 0 { 39 | self.entry.icon.as_ref() 40 | } else { 41 | self.actions[action - 1] 42 | .icon 43 | .as_ref() 44 | .or(self.entry.icon.as_ref()) 45 | } 46 | } 47 | } 48 | 49 | pub fn xdg_dirs<'a>() -> &'a BaseDirectories { 50 | XDG_DIRS.get_or_init(|| BaseDirectories::new().expect("failed to get xdg dirs")) 51 | } 52 | 53 | pub struct IconConfig { 54 | pub icon_size: u16, 55 | pub theme: Option, 56 | } 57 | 58 | pub struct Traverser { 59 | icon_config: Option<(IconConfig, Cache)>, 60 | filter: F, 61 | } 62 | 63 | impl Traverser { 64 | pub fn new(icon_config: Option, filter: F) -> anyhow::Result { 65 | Ok(Self { 66 | icon_config: icon_config 67 | .map(|icon_config| -> anyhow::Result<_> { 68 | let mut lookup = Cache::new()?; 69 | if let Some(theme) = &icon_config.theme { 70 | lookup.load(theme) 71 | } else { 72 | lookup.load_default() 73 | }?; 74 | Ok((icon_config, lookup)) 75 | }) 76 | .transpose()?, 77 | filter, 78 | }) 79 | } 80 | 81 | fn find_icon(&self, name: &str) -> Option { 82 | let (config, lookup) = self.icon_config.as_ref()?; 83 | 84 | let icon_path = Path::new(name); 85 | let path: Option; 86 | 87 | let path = if icon_path.is_absolute() { 88 | Some(icon_path) 89 | } else { 90 | let lookup_icon = |name: &str, icon_size: u16| { 91 | lookup.lookup_param( 92 | LookupParam::new(name) 93 | .with_size(icon_size) 94 | .with_theme(config.theme.as_deref()), 95 | ) 96 | }; 97 | 98 | path = lookup_icon(name, config.icon_size) 99 | .or_else(|| lookup_icon(name, config.icon_size + 8)) 100 | .or_else(|| lookup_icon(name, config.icon_size + 16)) 101 | .or_else(|| lookup_icon(name, 512)) 102 | .or_else(|| lookup_icon(name, config.icon_size - 8)) 103 | .or_else(|| { 104 | let name = format!("{name}-symbolic"); 105 | lookup_icon(&name, config.icon_size) 106 | }); 107 | 108 | path.as_deref() 109 | }; 110 | 111 | path.map(Icon::new) 112 | } 113 | 114 | fn parse_entry(&self, dir_entry: &DirEntry, dir_entry_path: PathBuf) -> Option { 115 | let entry = match fep::parse_entry(&dir_entry_path) { 116 | Ok(e) => e, 117 | Err(err) => { 118 | log::warn!("cannot parse {:?}: {}, skipping", dir_entry, err); 119 | return None; 120 | } 121 | }; 122 | 123 | let main_section = entry.section("Desktop Entry"); 124 | let locale = locale::Locale::current(); 125 | 126 | if main_section.attr("NoDisplay") == Some("true") { 127 | log::trace!("Skipping NoDisplay entry {:?}", dir_entry); 128 | return None; 129 | } 130 | 131 | let localized_entry = |attr_name: &str| { 132 | locale 133 | .keys() 134 | .filter_map(|key| main_section.attr_with_param(attr_name, key)) 135 | .next() 136 | .or_else(|| main_section.attr(attr_name)) 137 | }; 138 | 139 | match (localized_entry("Name"), main_section.attr("Exec")) { 140 | (Some(n), Some(e)) => { 141 | let filename = dir_entry_path.file_name().unwrap(); 142 | let desktop_fname = if let Some(f) = filename.to_str() { 143 | f.to_owned() 144 | } else { 145 | log::error!("found non-UTF8 desktop file: {:?}, skipping", filename); 146 | return None; 147 | }; 148 | 149 | let actions = entry 150 | .sections() 151 | .filter_map(|s| { 152 | if !s.name().starts_with("Desktop Action ") { 153 | return None; 154 | } 155 | let name = s.attr("Name")?.to_owned(); 156 | let exec = s.attr("Exec")?.to_owned(); 157 | Some(ExecEntry { 158 | name, 159 | exec, 160 | icon: localized_entry("Icon").and_then(|name| self.find_icon(name)), 161 | }) 162 | }) 163 | .collect::>(); 164 | 165 | let entry = ExecEntry { 166 | name: n.to_owned(), 167 | exec: e.to_owned(), 168 | icon: localized_entry("Icon").and_then(|name| self.find_icon(name)), 169 | }; 170 | 171 | return Some(Entry { 172 | entry, 173 | actions, 174 | desktop_fname, 175 | path: dir_entry_path, 176 | name: n.to_owned(), 177 | is_terminal: main_section 178 | .attr("Terminal") 179 | .map(|s| s == "true") 180 | .unwrap_or(false), 181 | }); 182 | } 183 | (n, e) => { 184 | if n.is_none() && e.is_none() { 185 | log::debug!( 186 | r#"entry {:?} has no "Name" nor "Exec" attribute"#, 187 | dir_entry_path 188 | ); 189 | } else if n.is_none() { 190 | log::debug!(r#"entry {:?} has no "Name" attribute"#, dir_entry_path); 191 | } else if e.is_none() { 192 | log::debug!(r#"entry {:?} has no "Exec" attribute"#, dir_entry_path); 193 | } 194 | None 195 | } 196 | } 197 | } 198 | } 199 | 200 | impl Traverser 201 | where 202 | F: Fn(&OsStr) -> bool, 203 | { 204 | pub fn find_entries(&self) -> Vec { 205 | let xdg_dirs = xdg_dirs(); 206 | 207 | let dirs = std::iter::once(xdg_dirs.get_data_home()); 208 | let dirs = dirs.chain(xdg_dirs.get_data_dirs()); 209 | let mut entries = self.traverse_dirs(dirs); 210 | entries.sort_by(|x, y| x.entry.name.cmp(&y.entry.name)); 211 | entries.dedup_by(|x, y| x.entry.name == y.entry.name); 212 | entries 213 | } 214 | 215 | fn traverse_dirs(&self, paths: impl IntoIterator) -> Vec { 216 | let mut entries = vec![]; 217 | for path in paths.into_iter() { 218 | let apps_dir = path.join("applications"); 219 | if !apps_dir.exists() { 220 | continue; 221 | } 222 | 223 | for dir_entry in read_dir(&apps_dir).filter(|e| (self.filter)(&e.file_name())) { 224 | self.traverse_dir_entry(&mut entries, dir_entry); 225 | } 226 | } 227 | entries 228 | } 229 | 230 | fn traverse_dir_entry(&self, entries: &mut Vec, dir_entry: DirEntry) { 231 | let dir_entry_path = dir_entry.path(); 232 | 233 | if dir_entry_path.extension().and_then(|s| s.to_str()) != Some("desktop") { 234 | return; 235 | } 236 | 237 | match dir_entry.file_type() { 238 | Err(err) => log::warn!("failed to get `{:?}` file type: {}", dir_entry_path, err), 239 | Ok(tp) if tp.is_dir() => { 240 | for dir_entry in read_dir(&dir_entry_path).filter(|e| (self.filter)(&e.file_name())) 241 | { 242 | self.traverse_dir_entry(entries, dir_entry); 243 | } 244 | 245 | return; 246 | } 247 | _ => {} 248 | } 249 | 250 | if let Some(entry) = self.parse_entry(&dir_entry, dir_entry_path) { 251 | entries.push(entry); 252 | } 253 | } 254 | } 255 | 256 | fn read_dir(path: &Path) -> impl Iterator { 257 | fs::read_dir(path) 258 | .map_err(|e| log::debug!("cannot read {:?} folder: {}, skipping", path, e)) 259 | .into_iter() 260 | .flatten() 261 | .filter_map(|e| { 262 | if let Err(err) = &e { 263 | log::warn!("failed to read file: {}", err); 264 | } 265 | 266 | e.ok() 267 | }) 268 | } 269 | -------------------------------------------------------------------------------- /src/desktop/locale.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | 3 | use once_cell::sync::OnceCell; 4 | use regex::Regex; 5 | 6 | #[cfg_attr(test, derive(Debug, PartialEq, Eq))] 7 | pub struct Locale<'a> { 8 | lang: Option<&'a str>, 9 | country: Option<&'a str>, 10 | modifier: Option<&'a str>, 11 | } 12 | 13 | #[allow(clippy::needless_raw_string_hashes)] 14 | const LOCALE_REGEX: &str = r#"(?x) 15 | ^ 16 | ([[:alpha:]]+) # lang 17 | (?:_([[:alpha:]]+))? # country 18 | (?:\.[^@]*)? # encoding 19 | (?:@(.*))? # modifier 20 | $"#; 21 | 22 | impl<'a> Locale<'a> { 23 | fn from_caputres(s: &'a str, captures: regex::Captures<'_>) -> Self { 24 | Self { 25 | lang: captures.get(1).map(|m| &s[m.range()]), 26 | country: captures.get(2).map(|m| &s[m.range()]), 27 | modifier: captures.get(3).map(|m| &s[m.range()]), 28 | } 29 | } 30 | } 31 | 32 | impl Locale<'static> { 33 | pub fn current<'a>() -> &'a Self { 34 | static LOCALE: OnceCell>> = OnceCell::new(); 35 | LOCALE 36 | .get_or_init(|| { 37 | let s = unsafe { 38 | let ptr = libc::setlocale(libc::LC_MESSAGES, b"\0".as_ptr().cast()); 39 | if ptr.is_null() { 40 | return None; 41 | } 42 | CStr::from_ptr(ptr) 43 | } 44 | .to_str() 45 | .ok()?; 46 | 47 | let re = Regex::new(LOCALE_REGEX).unwrap(); 48 | 49 | let c = re.captures(s)?; 50 | 51 | Some(Self::from_caputres(s, c)) 52 | }) 53 | .as_ref() 54 | .unwrap_or(&Self { 55 | lang: None, 56 | country: None, 57 | modifier: None, 58 | }) 59 | } 60 | 61 | pub fn keys(&self) -> impl Iterator> + '_ { 62 | static LOCALE_ITERS: OnceCell> = OnceCell::new(); 63 | LOCALE_ITERS 64 | .get_or_init(|| { 65 | let mut v = vec![]; 66 | if let Some(((l, c), m)) = self.lang.zip(self.country).zip(self.modifier) { 67 | v.push(format!("{}_{}@{}", l, c, m)); 68 | } 69 | if let Some((l, c)) = self.lang.zip(self.country) { 70 | v.push(format!("{}_{}", l, c)); 71 | } 72 | if let Some((l, m)) = self.lang.zip(self.modifier) { 73 | v.push(format!("{}@{}", l, m)); 74 | } 75 | if let Some(l) = self.lang { 76 | v.push(l.to_string()); 77 | } 78 | 79 | v 80 | }) 81 | .clone() 82 | .into_iter() 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | 90 | use test_case::test_case; 91 | 92 | #[test] 93 | fn regex_compiles() { 94 | let _ = Regex::new(LOCALE_REGEX).unwrap(); 95 | } 96 | 97 | #[test] 98 | fn regex_doesnt_match_empty() { 99 | let re = Regex::new(LOCALE_REGEX).unwrap(); 100 | assert!(re.captures("").is_none()); 101 | } 102 | 103 | impl Locale<'static> { 104 | fn new( 105 | lang: impl Into>, 106 | country: impl Into>, 107 | modifier: impl Into>, 108 | ) -> Self { 109 | Self { 110 | lang: lang.into(), 111 | country: country.into(), 112 | modifier: modifier.into(), 113 | } 114 | } 115 | } 116 | 117 | #[test_case("qw", Locale::new("qw", None, None); "lang")] 118 | #[test_case("qw_ER", Locale::new("qw", "ER", None); "lang, country")] 119 | #[test_case("qw_ER.ty", Locale::new("qw", "ER", None); "lang, country, encoding")] 120 | #[test_case( 121 | "qw_ER.ty@ui", 122 | Locale::new("qw", "ER", "ui"); 123 | "lang, country, encoding, modifier" 124 | )] 125 | #[test_case("qw@ui", Locale::new("qw", None, "ui"); "lang, modifier")] 126 | fn regex_matches(s: &str, x: Locale<'static>) { 127 | let re = Regex::new(LOCALE_REGEX).unwrap(); 128 | let c = re.captures(s).unwrap(); 129 | 130 | let m = c.get(0).unwrap(); 131 | assert_eq!(m.start(), 0); 132 | assert_eq!(m.end(), s.len()); 133 | 134 | assert_eq!(Locale::from_caputres(s, c), x); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts; 2 | 3 | use oneshot::Sender; 4 | pub use raqote::Point; 5 | use raqote::{DrawOptions, Path, PathBuilder, Source, StrokeStyle}; 6 | 7 | pub use background::Params as BgParams; 8 | pub use input_text::Params as InputTextParams; 9 | pub use list_view::{ListItem, Params as ListParams}; 10 | 11 | use crate::{style::Radius, Color}; 12 | 13 | pub type DrawTarget<'a> = raqote::DrawTarget<&'a mut [u32]>; 14 | 15 | mod background; 16 | mod input_text; 17 | mod list_view; 18 | 19 | #[derive(Clone, Copy)] 20 | pub struct Space { 21 | pub width: f32, 22 | pub height: f32, 23 | } 24 | 25 | pub trait Drawable { 26 | // Draws object to `dt` starting at `start_point` point with available `space` 27 | // returns used space of that object. 28 | fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, start_point: Point) -> Space; 29 | } 30 | 31 | pub enum Widget<'a, It = std::iter::Empty>> { 32 | InputText(Box>), 33 | ListView(list_view::ListView<'a, It>), 34 | Background(background::Background), 35 | } 36 | 37 | pub struct Drawables<'a> { 38 | counter: u32, 39 | tx: Option>, 40 | rx: Option>, 41 | state: &'a mut crate::state::State, 42 | background_config: BgParams, 43 | input_config: InputTextParams<'a>, 44 | list_config: ListParams, 45 | } 46 | 47 | impl<'a> Drawables<'a> { 48 | pub fn borrowed_next(&mut self) -> Option { 49 | self.counter += 1; 50 | Some(match self.counter { 51 | 1 => Widget::background(&self.background_config), 52 | 2 => Widget::input_text(self.state.raw_input(), &self.input_config), 53 | 3 => Widget::list_view( 54 | self.state.processed_entries(), 55 | self.state.skip_offset(), 56 | self.state.selected_item(), 57 | self.tx.take().unwrap(), 58 | &self.list_config, 59 | ), 60 | 4 => { 61 | self.state 62 | .update_skip_offset(self.rx.take().unwrap().recv().unwrap()); 63 | return None; 64 | } 65 | _ => return None, 66 | }) 67 | } 68 | } 69 | 70 | pub fn make_drawables<'c: 'it, 's: 'it, 'it>( 71 | config: &'c crate::config::Config, 72 | state: &'s mut crate::state::State, 73 | ) -> Drawables<'it> { 74 | let background_config = config.param(); 75 | let input_config = config.param(); 76 | let list_config = config.param(); 77 | 78 | state.process_entries(); 79 | 80 | let (tx, rx) = oneshot::channel(); 81 | Drawables { 82 | counter: 0, 83 | tx: Some(tx), 84 | rx: Some(rx), 85 | state, 86 | 87 | background_config, 88 | input_config, 89 | list_config, 90 | } 91 | } 92 | 93 | impl<'a, It> Widget<'a, It> { 94 | pub fn input_text(text: &'a str, params: &'a InputTextParams<'a>) -> Self { 95 | Self::InputText(Box::new(input_text::InputText::new(text, params))) 96 | } 97 | 98 | pub fn list_view( 99 | items: It, 100 | skip_offset: usize, 101 | selected_item: usize, 102 | tx: Sender, 103 | params: &'a ListParams, 104 | ) -> Self { 105 | Self::ListView(list_view::ListView::new( 106 | items, 107 | skip_offset, 108 | selected_item, 109 | tx, 110 | params, 111 | )) 112 | } 113 | 114 | pub fn background(params: &'a BgParams) -> Self { 115 | Self::Background(background::Background::new(params)) 116 | } 117 | } 118 | 119 | impl<'a, It> Drawable for Widget<'a, It> 120 | where 121 | It: Iterator>, 122 | { 123 | fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, start_point: Point) -> Space { 124 | match self { 125 | Self::InputText(w) => w.draw(dt, scale, space, start_point), 126 | Self::ListView(w) => w.draw(dt, scale, space, start_point), 127 | Self::Background(w) => w.draw(dt, scale, space, start_point), 128 | } 129 | } 130 | } 131 | 132 | pub struct RoundedRect { 133 | radius: Radius, 134 | color: Color, 135 | border: Option, 136 | } 137 | 138 | impl RoundedRect { 139 | fn new(radius: Radius, color: Color) -> Self { 140 | Self { 141 | radius, 142 | color, 143 | border: None, 144 | } 145 | } 146 | 147 | fn with_border(self, border: Option) -> Self { 148 | Self { border, ..self } 149 | } 150 | } 151 | 152 | impl Drawable for RoundedRect { 153 | fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, start_point: Point) -> Space { 154 | let Point { x, y, _unit } = start_point; 155 | let Space { width, height } = space; 156 | 157 | // We don't want the corner curves to overlap and thus cap the radius 158 | // to at most 50% of the smaller side 159 | let max_radius = width.min(height) / 2.0; 160 | let radius = &self.radius * f32::from(scale); 161 | let Radius { 162 | top_left, 163 | top_right, 164 | bottom_left, 165 | bottom_right, 166 | } = radius.min(Radius::all(max_radius)); 167 | 168 | let mut pb = PathBuilder::new(); 169 | pb.move_to(x, y + top_left); 170 | 171 | pb.arc( 172 | x + top_left, 173 | y + top_left, 174 | top_left, 175 | consts::PI, 176 | consts::FRAC_PI_2, 177 | ); 178 | pb.arc( 179 | x + width - top_right, 180 | y + top_right, 181 | top_right, 182 | 3.0 * consts::FRAC_PI_2, 183 | consts::FRAC_PI_2, 184 | ); 185 | pb.arc( 186 | x + width - bottom_right, 187 | y + height - bottom_right, 188 | bottom_right, 189 | 2.0 * consts::PI, 190 | consts::FRAC_PI_2, 191 | ); 192 | pb.arc( 193 | x + bottom_left, 194 | y + height - bottom_left, 195 | bottom_left, 196 | consts::FRAC_PI_2, 197 | consts::FRAC_PI_2, 198 | ); 199 | pb.line_to(x, y + top_left); 200 | let path = pb.finish(); 201 | 202 | dt.fill( 203 | &path, 204 | &Source::Solid(self.color.as_source()), 205 | &DrawOptions::new(), 206 | ); 207 | 208 | if let Some(b) = self.border { 209 | b.add_stroke(dt, &path); 210 | } 211 | 212 | space 213 | } 214 | } 215 | 216 | pub struct Border { 217 | border_color: Color, 218 | border_width: f32, 219 | } 220 | 221 | impl Border { 222 | pub fn new(border_color: Color, border_width: f32) -> Self { 223 | Self { 224 | border_color, 225 | border_width, 226 | } 227 | } 228 | 229 | fn add_stroke(self, dt: &mut DrawTarget<'_>, path: &Path) { 230 | dt.stroke( 231 | path, 232 | &Source::Solid(self.border_color.as_source()), 233 | &StrokeStyle { 234 | width: self.border_width, 235 | ..Default::default() 236 | }, 237 | &DrawOptions::new(), 238 | ); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/draw/background.rs: -------------------------------------------------------------------------------- 1 | use raqote::{Point, SolidSource}; 2 | 3 | use super::{Border, DrawTarget, Drawable, RoundedRect, Space}; 4 | use crate::{style::Radius, Color}; 5 | 6 | pub struct Params { 7 | pub width: u32, 8 | pub height: u32, 9 | pub color: Color, 10 | pub radius: Radius, 11 | pub border: Option<(Color, f32)>, 12 | } 13 | 14 | pub struct Background { 15 | rect: RoundedRect, 16 | } 17 | 18 | impl Background { 19 | pub fn new(params: &Params) -> Self { 20 | let color = params.color; 21 | let radius = params.radius.clone(); 22 | let border = if let Some((c, w)) = params.border { 23 | Some(Border::new(c, w)) 24 | } else { 25 | None 26 | }; 27 | let rect = RoundedRect::new(radius, color).with_border(border); 28 | 29 | Self { rect } 30 | } 31 | } 32 | 33 | impl Drawable for Background { 34 | fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, start_point: Point) -> Space { 35 | // Clear the draw target to avoid artefacts for scales > 1 in the corners 36 | dt.clear(SolidSource { 37 | r: 0, 38 | g: 0, 39 | b: 0, 40 | a: 0, 41 | }); 42 | 43 | self.rect.draw(dt, scale, space, start_point); 44 | 45 | Space { 46 | width: 0., 47 | height: 0., 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/draw/input_text.rs: -------------------------------------------------------------------------------- 1 | use raqote::{DrawOptions, Point}; 2 | 3 | use super::{DrawTarget, Drawable, RoundedRect, Space}; 4 | use crate::font::{Font, FontBackend, FontColor}; 5 | use crate::style::{Margin, Padding, Radius}; 6 | use crate::Color; 7 | 8 | pub struct Params<'a> { 9 | pub font: Font, 10 | pub font_size: u16, 11 | pub bg_color: Color, 12 | pub font_color: Color, 13 | pub prompt_color: Color, 14 | pub prompt: Option<&'a str>, 15 | pub password: bool, 16 | pub margin: Margin, 17 | pub padding: Padding, 18 | pub radius: Radius, 19 | } 20 | 21 | pub struct InputText<'a> { 22 | text: &'a str, 23 | params: &'a Params<'a>, 24 | rect: RoundedRect, 25 | } 26 | 27 | impl<'a> InputText<'a> { 28 | pub fn new(text: &'a str, params: &'a Params<'a>) -> Self { 29 | let color = params.bg_color; 30 | let radius = params.radius.clone(); 31 | 32 | Self { 33 | text, 34 | params, 35 | rect: RoundedRect::new(radius, color), 36 | } 37 | } 38 | } 39 | 40 | impl<'a> Drawable for InputText<'a> { 41 | fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, point: Point) -> Space { 42 | let font_size = f32::from(self.params.font_size * scale); 43 | 44 | let mut padding = &self.params.padding * f32::from(scale); 45 | const PADDING_TOP: f32 = 2.0; 46 | const PADDING_BOTTOM: f32 = 5.0; 47 | padding.top += PADDING_TOP; 48 | padding.bottom += PADDING_BOTTOM; 49 | let margin = &self.params.margin * f32::from(scale); 50 | 51 | let rect_width = space.width - margin.left - margin.right; 52 | let rect_height = padding.top + font_size + padding.bottom; 53 | let rect_space = Space { 54 | width: rect_width, 55 | height: rect_height, 56 | }; 57 | let rect_point = Point::new(point.x + margin.left, point.y + margin.top); 58 | 59 | self.rect.draw(dt, scale, rect_space, rect_point); 60 | 61 | padding.left += (rect_height / 2.0) 62 | .min(self.params.radius.top_left) 63 | .min(self.params.radius.top_right); 64 | 65 | let pos = Point::new(rect_point.x + padding.left, rect_point.y + padding.top); 66 | let end_pos = Point::new( 67 | dt.width() as f32 - self.params.padding.right - self.params.margin.right, 68 | pos.y, 69 | ); 70 | 71 | let password_text = if self.params.password { 72 | Some("*".repeat(self.text.chars().count())) 73 | } else { 74 | None 75 | }; 76 | 77 | let (color, text) = if self.text.is_empty() { 78 | ( 79 | self.params.prompt_color, 80 | self.params.prompt.unwrap_or_default(), 81 | ) 82 | } else { 83 | let text = if let Some(password_text) = password_text.as_ref() { 84 | password_text.as_str() 85 | } else { 86 | self.text 87 | }; 88 | (self.params.font_color, text) 89 | }; 90 | 91 | self.params.font.draw( 92 | dt, 93 | text, 94 | font_size, 95 | pos, 96 | end_pos, 97 | FontColor::Single(color.as_source()), 98 | &DrawOptions::new(), 99 | ); 100 | 101 | // TODO: use padding.right for text wrapping/clipping 102 | 103 | Space { 104 | width: space.width, 105 | height: margin.top + rect_height + margin.bottom, 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/draw/list_view.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use oneshot::Sender; 4 | use raqote::{AntialiasMode, DrawOptions, Image, Point}; 5 | 6 | use super::{DrawTarget, Drawable, Space}; 7 | use crate::font::{Font, FontBackend, FontColor}; 8 | use crate::state::ContinuousMatch; 9 | use crate::style::Margin; 10 | use crate::Color; 11 | use unicode_segmentation::UnicodeSegmentation; 12 | 13 | pub struct Params { 14 | pub font: Font, 15 | pub font_size: u16, 16 | pub font_color: Color, 17 | pub selected_font_color: Color, 18 | pub match_color: Option, 19 | pub icon_size: Option, 20 | pub fallback_icon: Option, 21 | pub margin: Margin, 22 | pub hide_actions: bool, 23 | pub action_left_margin: f32, 24 | pub item_spacing: f32, 25 | pub icon_spacing: f32, 26 | } 27 | 28 | pub struct ListItem<'a> { 29 | pub name: &'a str, 30 | pub subname: Option<&'a str>, 31 | pub icon: Option>, 32 | pub match_mask: Option>, 33 | } 34 | 35 | pub struct ListView<'a, It> { 36 | items: It, 37 | skip_offset: usize, 38 | selected_item: usize, 39 | new_skip: Sender, 40 | params: &'a Params, 41 | _tparam: PhantomData<&'a ()>, 42 | } 43 | 44 | impl<'a, It> ListView<'a, It> { 45 | pub fn new( 46 | items: It, 47 | skip_offset: usize, 48 | selected_item: usize, 49 | new_skip: Sender, 50 | params: &'a Params, 51 | ) -> Self { 52 | Self { 53 | items, 54 | skip_offset, 55 | selected_item, 56 | new_skip, 57 | params, 58 | _tparam: PhantomData, 59 | } 60 | } 61 | } 62 | 63 | impl<'a, It> Drawable for ListView<'a, It> 64 | where 65 | It: Iterator>, 66 | { 67 | fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, point: Point) -> Space { 68 | let margin = &self.params.margin * f32::from(scale); 69 | let item_spacing = self.params.item_spacing * f32::from(scale); 70 | let icon_size = self.params.icon_size.unwrap_or(0) * scale; 71 | let icon_spacing = self.params.icon_spacing * f32::from(scale); 72 | 73 | let icon_size_f32 = f32::from(icon_size); 74 | let font_size = f32::from(self.params.font_size * scale); 75 | let top_offset = point.y + margin.top + (icon_size_f32 - font_size).max(0.) / 2.; 76 | let entry_height = font_size.max(icon_size_f32); 77 | 78 | let mut iter = self.items.peekable(); 79 | 80 | let hide_actions = self.params.hide_actions; 81 | // For now either all items has subname or none. 82 | let has_subname = iter 83 | .peek() 84 | .map(|e| e.subname.is_some() && !hide_actions) 85 | .unwrap_or(false); 86 | 87 | let displayed_items = ((space.height - margin.top - margin.bottom + item_spacing) 88 | / (entry_height + item_spacing)) as usize 89 | - has_subname as usize; 90 | 91 | let max_offset = self.skip_offset + displayed_items; 92 | let (selected_item, skip_offset) = if self.selected_item < self.skip_offset { 93 | (0, self.selected_item) 94 | } else if max_offset <= self.selected_item { 95 | ( 96 | displayed_items - 1, 97 | self.skip_offset + (self.selected_item - max_offset) + 1, 98 | ) 99 | } else { 100 | (self.selected_item - self.skip_offset, self.skip_offset) 101 | }; 102 | 103 | self.new_skip.send(skip_offset).unwrap(); 104 | 105 | for (i, item) in iter.skip(skip_offset).enumerate().take(displayed_items) { 106 | let relative_offset = (i as f32 + (i > selected_item && has_subname) as i32 as f32) 107 | * (entry_height + item_spacing); 108 | let x_offset = point.x + margin.left; 109 | let y_offset = top_offset + relative_offset; 110 | 111 | let fallback_icon = self 112 | .params 113 | .fallback_icon 114 | .as_ref() 115 | .and_then(|i| i.as_image()); 116 | if let Some(icon) = item.icon.as_ref().or(fallback_icon.as_ref()) { 117 | if icon.width == icon.height && icon.height == i32::from(icon_size) { 118 | dt.draw_image_at( 119 | x_offset, 120 | y_offset + (font_size - icon_size_f32) / 2., 121 | icon, 122 | &DrawOptions::default(), 123 | ); 124 | } else { 125 | dt.draw_image_with_size_at( 126 | icon_size_f32, 127 | icon_size_f32, 128 | x_offset, 129 | y_offset + (font_size - icon_size_f32) / 2., 130 | icon, 131 | &DrawOptions::default(), 132 | ); 133 | } 134 | } 135 | 136 | let pos = Point::new(x_offset + icon_size_f32 + icon_spacing, y_offset); 137 | let end_pos = Point::new(dt.width() as f32 - self.params.margin.right, y_offset); 138 | 139 | let color = if i == selected_item { 140 | self.params.selected_font_color 141 | } else { 142 | self.params.font_color 143 | } 144 | .as_source(); 145 | 146 | let antialias = AntialiasMode::Gray; 147 | let draw_opts = DrawOptions { 148 | antialias, 149 | ..DrawOptions::new() 150 | }; 151 | 152 | let color = if let (Some(match_color), Some(match_mask)) = 153 | (self.params.match_color, item.match_mask) 154 | { 155 | let mut special_color = 156 | vec![color; UnicodeSegmentation::graphemes(item.name, true).count()]; 157 | 158 | let match_color = match_color.as_source(); 159 | let special_len = special_color.len(); 160 | let mut last_idx = 0; // exclusive 161 | 162 | match_mask.for_each(|m| { 163 | let unmatch_range = last_idx..m.start(); 164 | if !unmatch_range.is_empty() { 165 | special_color[unmatch_range].fill(color); 166 | } 167 | 168 | let match_range = m.start()..(m.start() + m.len()).min(special_len); 169 | last_idx = match_range.end; 170 | if !match_range.is_empty() { 171 | special_color[match_range].fill(match_color); 172 | } 173 | }); 174 | 175 | FontColor::Multiple(special_color) 176 | } else { 177 | FontColor::Single(color) 178 | }; 179 | 180 | let font = &self.params.font; 181 | font.draw(dt, item.name, font_size, pos, end_pos, color, &draw_opts); 182 | 183 | if i == selected_item && has_subname { 184 | if let Some(subname) = item.subname { 185 | font.draw( 186 | dt, 187 | subname, 188 | font_size, 189 | Point::new( 190 | pos.x + self.params.action_left_margin, 191 | pos.y + entry_height + item_spacing, 192 | ), 193 | Point::new(end_pos.x, pos.y + entry_height + item_spacing), 194 | FontColor::Single(self.params.font_color.as_source()), 195 | &draw_opts, 196 | ); 197 | } 198 | } 199 | } 200 | 201 | space 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/exec.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | use crate::input_parser::InputValue; 6 | 7 | pub fn exec( 8 | term: Option>, 9 | command_string: impl IntoIterator>, 10 | input_value: &InputValue, 11 | ) -> Result { 12 | let InputValue { 13 | source: _, 14 | search_string: _, 15 | args, 16 | env_vars, 17 | working_dir, 18 | } = input_value; 19 | 20 | if let Some(working_dir) = &working_dir { 21 | nix::unistd::chdir(*working_dir) 22 | .with_context(|| format!("chdir to {working_dir} failed"))?; 23 | } 24 | 25 | let command_iter = command_string.into_iter().map(Into::into); 26 | 27 | let command: Vec<_> = if let Some(mut term) = term { 28 | let mut command = command_iter.fold(Vec::new(), |mut v, item| { 29 | v.extend(item.into_bytes()); 30 | v 31 | }); 32 | if let Some(args) = args { 33 | command.push(b' '); 34 | command.extend(args.as_bytes()); 35 | } 36 | 37 | term.push(CString::new(command).expect("invalid command")); 38 | term 39 | } else { 40 | let args_iter = args.iter().flat_map(|args| { 41 | shlex::split(args) 42 | .expect("invalid arguments") 43 | .into_iter() 44 | .map(|s| CString::new(s).expect("invalid arguments")) 45 | }); 46 | command_iter.chain(args_iter).collect() 47 | }; 48 | 49 | if let Some(env_vars) = env_vars { 50 | let env_vars = std::env::vars() 51 | .map(|(k, v)| format!("{}={}", k, v)) 52 | .chain(shlex::split(env_vars).expect("invalid envs")) 53 | .map(|s| CString::new(s).expect("invalid envs")) 54 | .collect::>(); 55 | 56 | let (prog, args) = (&command[0], &command[0..]); 57 | log::debug!("execvpe: {:?} {:?} (envs: {:?})", prog, args, env_vars); 58 | nix::unistd::execvpe(prog, args, &env_vars).context("execvpe failed") 59 | } else { 60 | let (prog, args) = (&command[0], &command[0..]); 61 | log::debug!("execvp: {:?} {:?}", prog, args); 62 | nix::unistd::execvp(prog, args).context("execvp failed") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/font.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use raqote::{DrawOptions, Point, SolidSource}; 5 | 6 | use crate::DrawTarget; 7 | 8 | mod fdue; 9 | pub type InnerFont = fdue::Font; 10 | pub type Font = std::rc::Rc; 11 | 12 | pub enum FontColor { 13 | Multiple(Vec), 14 | Single(SolidSource), 15 | } 16 | 17 | pub trait FontBackend: Sized { 18 | fn default() -> Self { 19 | const DEFAULT_FONT: &str = "DejaVu Sans Mono"; 20 | Self::font_by_name(DEFAULT_FONT) 21 | .unwrap_or_else(|e| panic!("cannot read the font `{}`: {}", DEFAULT_FONT, e)) 22 | } 23 | 24 | fn font_by_name(name: &str) -> Result; 25 | 26 | fn font_by_path(path: &Path) -> Result; 27 | 28 | #[allow(clippy::too_many_arguments)] 29 | fn draw( 30 | &self, 31 | dt: &mut DrawTarget, 32 | text: &str, 33 | font_size: f32, 34 | start_pos: Point, 35 | end_pos: Point, 36 | color: FontColor, 37 | opts: &DrawOptions, 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/font/fdue.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::BinaryHeap; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use anyhow::Context; 6 | use fontconfig::{Fontconfig, Pattern}; 7 | use fontdue::layout::{ 8 | CoordinateSystem, Layout, LayoutSettings, TextStyle, VerticalAlign, WrapStyle, 9 | }; 10 | use levenshtein::levenshtein; 11 | use once_cell::sync::Lazy; 12 | use raqote::{DrawOptions, Point, SolidSource}; 13 | 14 | use super::{DrawTarget, FontBackend, FontColor, Result}; 15 | 16 | static FONTCONFIG_CACHE: Lazy = 17 | Lazy::new(|| Fontconfig::new().expect("failed to initialize fontconfig")); 18 | const BUF_SIZE: usize = 256 * 256; 19 | 20 | pub struct Font { 21 | inner: fontdue::Font, 22 | // Layout in fontdue uses allocations, so we're reusing it for reduce memory allocations 23 | layout: RefCell, 24 | // Move buffer to heap, because it is very big for stack; only one allocation happens 25 | buffer: RefCell>, 26 | } 27 | 28 | impl Font { 29 | fn with_font(inner: fontdue::Font) -> Self { 30 | Self { 31 | inner, 32 | layout: RefCell::new(Layout::new(CoordinateSystem::PositiveYDown)), 33 | buffer: RefCell::new(vec![0; BUF_SIZE]), 34 | } 35 | } 36 | } 37 | 38 | #[derive(Eq)] 39 | struct FuzzyResult { 40 | text: String, 41 | distance: usize, 42 | } 43 | 44 | impl PartialEq for FuzzyResult { 45 | fn eq(&self, other: &Self) -> bool { 46 | self.distance == other.distance 47 | } 48 | } 49 | 50 | impl Ord for FuzzyResult { 51 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 52 | self.distance.cmp(&other.distance) 53 | } 54 | } 55 | 56 | impl PartialOrd for FuzzyResult { 57 | fn partial_cmp(&self, other: &Self) -> Option { 58 | Some(self.cmp(other)) 59 | } 60 | } 61 | 62 | impl Font { 63 | fn from_path(path: &Path, index: Option) -> Result { 64 | let bytes = std::fs::read(path).context("font read")?; 65 | let inner = fontdue::Font::from_bytes( 66 | bytes, 67 | index 68 | .map(|collection_index| fontdue::FontSettings { 69 | collection_index, 70 | ..Default::default() 71 | }) 72 | .unwrap_or_default(), 73 | ) 74 | .map_err(|e| anyhow::anyhow!("{}", e))?; 75 | 76 | Ok(Font::with_font(inner)) 77 | } 78 | 79 | fn try_find_best_font(name: &str) -> Vec { 80 | const COUNT_MATCHES: usize = 5; 81 | 82 | let pat = Pattern::new(&FONTCONFIG_CACHE); 83 | fontconfig::list_fonts(&pat, None) 84 | .iter() 85 | .filter_map(|pat| { 86 | let text = pat.name()?.to_string(); 87 | Some(FuzzyResult { 88 | distance: levenshtein(name, &text), 89 | text, 90 | }) 91 | }) 92 | .collect::>() 93 | .into_sorted_vec() 94 | .into_iter() 95 | .take(COUNT_MATCHES) 96 | .map(|r| r.text) 97 | .collect() 98 | } 99 | } 100 | 101 | fn index_to_u32(idx: i32) -> Option { 102 | if idx < 0 { 103 | None 104 | } else { 105 | Some(idx as u32) 106 | } 107 | } 108 | 109 | impl FontBackend for Font { 110 | fn default() -> Self { 111 | let pat = Pattern::new(&FONTCONFIG_CACHE); 112 | fontconfig::list_fonts(&pat, None) 113 | .iter() 114 | .find_map(|pat| { 115 | let path = std::path::Path::new(pat.filename()?); 116 | if !path.exists() { 117 | return None; 118 | } 119 | let index = pat.face_index().and_then(index_to_u32); 120 | Font::from_path(path, index) 121 | .map_err(|e| log::debug!("cannot load default font at {}: {e}", path.display())) 122 | .ok() 123 | }) 124 | .expect("cannot find any font") 125 | } 126 | 127 | fn font_by_name(name: &str) -> Result { 128 | let cache = &*FONTCONFIG_CACHE; 129 | 130 | // TODO: use Font.find after https://github.com/yeslogic/fontconfig-rs/pull/27 131 | fn find( 132 | fc: &Fontconfig, 133 | family: &str, 134 | style: Option<&str>, 135 | ) -> Option<(PathBuf, Option)> { 136 | let mut pat = Pattern::new(fc); 137 | let family = std::ffi::CString::new(family).ok()?; 138 | pat.add_string(fontconfig::FC_FAMILY, &family); 139 | 140 | if let Some(style) = style { 141 | let style = std::ffi::CString::new(style).ok()?; 142 | pat.add_string(fontconfig::FC_STYLE, &style); 143 | } 144 | 145 | let font_match = pat.font_match(); 146 | 147 | font_match 148 | .filename() 149 | .map(|filename| (PathBuf::from(filename), font_match.face_index())) 150 | } 151 | 152 | let (path, index) = find(cache, name, None) 153 | .or_else(|| { 154 | let (name, style) = name.rsplit_once(' ')?; 155 | find(cache, name, Some(style)) 156 | }) 157 | .ok_or_else(|| { 158 | let matching = Font::try_find_best_font(name); 159 | log::info!("The font {} could not be found.", name); 160 | if !matching.is_empty() { 161 | use itertools::Itertools; 162 | log::info!("Best matches:\n\t{}\n", matching.into_iter().format("\n\t")); 163 | } 164 | anyhow::anyhow!("cannot find font") 165 | })?; 166 | 167 | Font::from_path(&path, index.and_then(index_to_u32)) 168 | } 169 | 170 | fn font_by_path(path: &Path) -> Result { 171 | Font::from_path(path, None) 172 | } 173 | 174 | fn draw( 175 | &self, 176 | dt: &mut DrawTarget, 177 | mut text: &str, 178 | font_size: f32, 179 | start_pos: Point, 180 | end_pos: Point, 181 | color: FontColor, 182 | opts: &DrawOptions, 183 | ) { 184 | let mut buf = self.buffer.borrow_mut(); 185 | let mut layout = self.layout.borrow_mut(); 186 | 187 | layout.reset(&LayoutSettings { 188 | x: start_pos.x, 189 | y: start_pos.y, 190 | max_height: Some(font_size), 191 | max_width: Some(end_pos.x - start_pos.x), 192 | vertical_align: VerticalAlign::Middle, 193 | wrap_style: WrapStyle::Letter, 194 | ..LayoutSettings::default() 195 | }); 196 | 197 | layout.append(&[&self.inner], &TextStyle::new(text, font_size, 0)); 198 | 199 | let take_glyphs = match layout.lines() { 200 | Some(vec) => { 201 | // If layout return miltiple lines then we have text overflow, cut the text 202 | // and layout again 203 | match vec.get(1) { 204 | Some(second_line) => second_line.glyph_start, 205 | None => layout.glyphs().len(), 206 | } 207 | } 208 | None => layout.glyphs().len(), 209 | }; 210 | 211 | if take_glyphs != layout.glyphs().len() { 212 | let overflow_text = "..."; 213 | 214 | // Try place ... in end of cutted text. Check strange case if width of window is too small 215 | // even for overflow_text. No panic at all 216 | let glyph_offset = if take_glyphs > overflow_text.len() { 217 | take_glyphs - overflow_text.len() 218 | } else { 219 | take_glyphs 220 | }; 221 | 222 | text = &text[0..layout.glyphs().get(glyph_offset).unwrap().byte_offset]; 223 | layout.clear(); 224 | layout.append(&[&self.inner], &TextStyle::new(text, font_size, 0)); 225 | 226 | if glyph_offset != take_glyphs { 227 | layout.append(&[&self.inner], &TextStyle::new(overflow_text, font_size, 0)); 228 | } 229 | } 230 | 231 | for (n, g) in layout.glyphs().iter().take(take_glyphs).enumerate() { 232 | let (_, b) = self.inner.rasterize_config(g.key); 233 | 234 | assert!(g.width * g.height <= BUF_SIZE); 235 | let width = g.width as i32; 236 | let height = g.height as i32; 237 | 238 | let color = match color { 239 | FontColor::Single(color) => color, 240 | FontColor::Multiple(ref colors) => colors[n], 241 | }; 242 | 243 | for (i, x) in b.into_iter().enumerate() { 244 | let src = SolidSource::from_unpremultiplied_argb( 245 | (u32::from(x) * u32::from(color.a) / 255) as u8, 246 | color.r, 247 | color.g, 248 | color.b, 249 | ); 250 | buf[i] = (u32::from(src.a) << 24) 251 | | (u32::from(src.r) << 16) 252 | | (u32::from(src.g) << 8) 253 | | u32::from(src.b); 254 | } 255 | 256 | let img = raqote::Image { 257 | width, 258 | height, 259 | data: &buf[..], 260 | }; 261 | 262 | dt.draw_image_with_size_at(g.width as f32, g.height as f32, g.x, g.y, &img, opts); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/icon.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufReader; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use anyhow::{anyhow, ensure, Context, Result}; 5 | use once_cell::unsync::Lazy; 6 | 7 | pub struct Loaded { 8 | width: u32, 9 | height: u32, 10 | data: Vec, 11 | } 12 | 13 | pub enum IconInner { 14 | Pending(PathBuf), 15 | Failed, 16 | Loaded(Loaded), 17 | } 18 | 19 | pub struct Icon { 20 | inner: Lazy, Box Option>>, 21 | } 22 | 23 | impl Icon { 24 | pub fn new(path: impl Into) -> Self { 25 | let path = path.into(); 26 | Self { 27 | inner: Lazy::new(Box::new(move || { 28 | let mut inner = IconInner::new(path); 29 | inner.load().map(|()| inner) 30 | })), 31 | } 32 | } 33 | 34 | pub fn as_image(&self) -> Option { 35 | Lazy::force(&self.inner) 36 | .as_ref()? 37 | .loaded() 38 | .map(|l| l.as_image()) 39 | } 40 | } 41 | 42 | impl Default for IconInner { 43 | fn default() -> Self { 44 | Self::Failed 45 | } 46 | } 47 | 48 | impl IconInner { 49 | pub fn new(path: impl Into) -> Self { 50 | Self::Pending(path.into()) 51 | } 52 | 53 | fn loaded(&self) -> Option<&Loaded> { 54 | if let Self::Loaded(l) = self { 55 | Some(l) 56 | } else { 57 | unreachable!() 58 | } 59 | } 60 | 61 | fn load(&mut self) -> Option<()> { 62 | let loaded = match self { 63 | Self::Pending(path) => Loaded::load(path), 64 | Self::Failed => return None, 65 | Self::Loaded(_) => return Some(()), 66 | }; 67 | 68 | if let Some(loaded) = loaded { 69 | *self = Self::Loaded(loaded); 70 | Some(()) 71 | } else { 72 | *self = Self::Failed; 73 | None 74 | } 75 | } 76 | } 77 | 78 | impl Loaded { 79 | pub fn load(path: impl AsRef) -> Option { 80 | let path = path.as_ref(); 81 | let failed_to_load = |e| log::info!("failed to load icon at path `{:?}`: {}", path, e); 82 | match path.extension()?.to_str()? { 83 | "png" => Self::from_png_path(path).map_err(failed_to_load).ok(), 84 | "svg" => Self::from_svg_path(path).map_err(failed_to_load).ok(), 85 | ext => { 86 | log::info!("unsupported icon extension: {:?}", ext); 87 | None 88 | } 89 | } 90 | } 91 | 92 | fn from_png_path(path: impl AsRef) -> Result { 93 | let mut decoder = png::Decoder::new(BufReader::new(std::fs::File::open(path.as_ref())?)); 94 | decoder.set_transformations(png::Transformations::normalize_to_color8()); 95 | let mut reader = decoder 96 | .read_info() 97 | .map_err(|e| anyhow!("failed to read png info: {}", e))?; 98 | let mut buf = vec![0; reader.output_buffer_size()]; 99 | let info = reader 100 | .next_frame(&mut buf) 101 | .map_err(|e| anyhow!("failed to read png frame: {}", e))?; 102 | let buf = &buf[..info.buffer_size()]; 103 | 104 | let data = match info.color_type { 105 | png::ColorType::Rgb => { 106 | ensure!(buf.len() % 3 == 0, "corrupted icon file"); 107 | 108 | buf.chunks(3) 109 | .map(|chunk| u32::from_be_bytes([0xff, chunk[0], chunk[1], chunk[2]])) 110 | .collect() 111 | } 112 | png::ColorType::Rgba => rgba_to_argb(buf)?, 113 | png::ColorType::GrayscaleAlpha => { 114 | ensure!(buf.len() % 2 == 0, "corrupted icon file"); 115 | 116 | buf.chunks(2) 117 | .map(|chunk| u32::from_be_bytes([chunk[1], chunk[0], chunk[0], chunk[0]])) 118 | .collect() 119 | } 120 | png::ColorType::Grayscale => buf 121 | .iter() 122 | .copied() 123 | .map(|pix| u32::from_be_bytes([0xff, pix, pix, pix])) 124 | .collect(), 125 | png::ColorType::Indexed => unreachable!("image shall be converted"), 126 | }; 127 | 128 | Ok(Self { 129 | width: info.width, 130 | height: info.height, 131 | data, 132 | }) 133 | } 134 | 135 | fn from_svg_path(path: impl AsRef) -> Result { 136 | let opt = resvg::usvg::Options::default(); 137 | let data = std::fs::read(path.as_ref()) 138 | .with_context(|| format!("failed to open svg file: {:?}", path.as_ref()))?; 139 | let tree = resvg::usvg::Tree::from_data(&data, &opt) 140 | .map_err(|e| anyhow!("svg open error: {}", e))?; 141 | 142 | let pixmap_size = tree.size().to_int_size(); 143 | let width = pixmap_size.width(); 144 | let height = pixmap_size.height(); 145 | let mut buf = 146 | resvg::tiny_skia::Pixmap::new(width, height).context("invalid pixmap size")?; 147 | resvg::render( 148 | &tree, 149 | resvg::tiny_skia::Transform::default(), 150 | &mut buf.as_mut(), 151 | ); 152 | 153 | Ok(Self { 154 | width, 155 | height, 156 | data: rgba_to_argb(buf.data())?, 157 | }) 158 | } 159 | 160 | fn as_image(&self) -> raqote::Image { 161 | raqote::Image { 162 | width: self.width as i32, 163 | height: self.height as i32, 164 | data: self.data.as_slice(), 165 | } 166 | } 167 | } 168 | 169 | fn rgba_to_argb(buf: &[u8]) -> Result> { 170 | ensure!(buf.len() % 4 == 0, "corrupted icon file"); 171 | 172 | let data = buf 173 | .chunks(4) 174 | .map(|chunk| { 175 | let src = raqote::SolidSource::from_unpremultiplied_argb( 176 | chunk[3], chunk[0], chunk[1], chunk[2], 177 | ); 178 | 179 | let a = u32::from(src.a) << 24; 180 | let r = u32::from(src.r) << 16; 181 | let g = u32::from(src.g) << 8; 182 | let b = u32::from(src.b); 183 | 184 | a | r | g | b 185 | }) 186 | .collect(); 187 | 188 | Ok(data) 189 | } 190 | -------------------------------------------------------------------------------- /src/input_parser.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use regex::Regex; 3 | 4 | #[derive(Debug)] 5 | #[cfg_attr(test, derive(PartialEq, Eq))] 6 | pub struct InputValue<'a> { 7 | pub source: &'a str, 8 | pub search_string: &'a str, 9 | pub args: Option<&'a str>, 10 | pub env_vars: Option<&'a str>, 11 | pub working_dir: Option<&'a str>, 12 | } 13 | 14 | impl InputValue<'static> { 15 | pub fn empty() -> Self { 16 | InputValue { 17 | source: "", 18 | search_string: "", 19 | args: None, 20 | env_vars: None, 21 | working_dir: None, 22 | } 23 | } 24 | } 25 | 26 | enum NextValueKind { 27 | Args, 28 | EnvVars, 29 | WorkingDir, 30 | } 31 | 32 | static SEPARATOR_REGEX: OnceCell = OnceCell::new(); 33 | 34 | fn parse_command_part(input: &str) -> (&str, (&str, Option)) { 35 | let re = SEPARATOR_REGEX 36 | .get_or_init(|| Regex::new(r"(!!|#|~)").unwrap()) 37 | .clone(); 38 | let Some(cap) = re.captures(input) else { 39 | return ("", (input, None)); 40 | }; 41 | let m = cap.get(0).unwrap(); 42 | let bang = cap.get(1).unwrap().as_str(); 43 | let kind = match bang { 44 | "!!" => Some(NextValueKind::Args), 45 | "#" => Some(NextValueKind::EnvVars), 46 | "~" => Some(NextValueKind::WorkingDir), 47 | s => panic!("regex bug: unexpected bang match {}", s), 48 | }; 49 | 50 | (&input[m.end()..], (&input[..m.start()], kind)) 51 | } 52 | 53 | pub fn parse(source: &str) -> InputValue<'_> { 54 | let (mut input, (search_string, mut next_kind)) = parse_command_part(source); 55 | let mut command = InputValue { 56 | source, 57 | search_string, 58 | args: None, 59 | env_vars: None, 60 | working_dir: None, 61 | }; 62 | 63 | while let Some(kind) = next_kind.take() { 64 | let (left, (cmd, new_kind)) = parse_command_part(input); 65 | 66 | match kind { 67 | NextValueKind::Args => command.args = Some(cmd), 68 | NextValueKind::EnvVars => command.env_vars = Some(cmd), 69 | NextValueKind::WorkingDir => command.working_dir = Some(cmd), 70 | } 71 | 72 | input = left; 73 | next_kind = new_kind; 74 | } 75 | 76 | debug_assert!(input.is_empty(), "trailing input: {}", input); 77 | 78 | command 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::{parse, InputValue}; 84 | 85 | use quickcheck_macros::quickcheck; 86 | use test_case::test_case; 87 | 88 | #[test_case("", InputValue::empty(); "empty string")] 89 | #[test_case("qwdqwd asd asd", InputValue { 90 | search_string: "qwdqwd asd asd", 91 | ..InputValue::empty() 92 | }; "search string")] 93 | #[test_case("qwdqwd asd asd", InputValue { 94 | search_string: "qwdqwd asd asd", 95 | ..InputValue::empty() 96 | }; "search string with exact prefix")] 97 | #[test_case("qwdqwd!!asd#dsa", InputValue { 98 | search_string: "qwdqwd", 99 | args: Some("asd"), 100 | env_vars: Some("dsa"), 101 | ..InputValue::empty() 102 | }; "search string with args then env")] 103 | #[test_case("qwdqwd#dsa!!asd", InputValue { 104 | search_string: "qwdqwd", 105 | args: Some("asd"), 106 | env_vars: Some("dsa"), 107 | ..InputValue::empty() 108 | }; "search string with env then args")] 109 | #[test_case("qwdqwd~zx,c#qwe !!asd", InputValue { 110 | search_string: "qwdqwd", 111 | args: Some("asd"), 112 | env_vars: Some("qwe "), 113 | working_dir: Some("zx,c"), 114 | ..InputValue::empty() 115 | }; "search string with working dir then env then args")] 116 | #[test_case("#qwe~zx,c!!asd", InputValue { 117 | search_string: "", 118 | args: Some("asd"), 119 | env_vars: Some("qwe"), 120 | working_dir: Some("zx,c"), 121 | ..InputValue::empty() 122 | }; "all but search string")] 123 | #[test_case("ffx!!--new-instance#MOZ_ENABLE_WAYLAND=1~/run/user/1000", InputValue { 124 | search_string: "ffx", 125 | args: Some("--new-instance"), 126 | env_vars: Some("MOZ_ENABLE_WAYLAND=1"), 127 | working_dir: Some("/run/user/1000"), 128 | ..InputValue::empty() 129 | }; "with all params")] 130 | fn test_parse(input: &str, input_value: InputValue) { 131 | assert_eq!( 132 | parse(input), 133 | InputValue { 134 | source: input, 135 | ..input_value 136 | } 137 | ); 138 | } 139 | 140 | #[quickcheck] 141 | fn test_parse_all(input: String) { 142 | parse(&input); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use color::Color; 2 | pub(crate) use desktop::Entry as DesktopEntry; 3 | pub(crate) use draw::DrawTarget; 4 | 5 | mod color; 6 | mod draw; 7 | mod exec; 8 | mod font; 9 | mod icon; 10 | mod input_parser; 11 | mod style; 12 | mod usage_cache; 13 | 14 | pub mod config; 15 | pub mod desktop; 16 | pub mod mode; 17 | pub mod state; 18 | pub mod window; 19 | 20 | #[macro_export] 21 | macro_rules! prog_name { 22 | () => { 23 | "yofi" 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::path::PathBuf; 3 | 4 | use anyhow::{Context, Result}; 5 | use log::LevelFilter; 6 | 7 | use yofi::{config, desktop, mode, prog_name, state, window}; 8 | 9 | fn setup_logger(level: LevelFilter, args: &Args) -> Result<()> { 10 | let dispatcher = fern::Dispatch::new() 11 | .format(|out, message, record| { 12 | out.finish(format_args!( 13 | "{}[{}][{}] {}", 14 | humantime::format_rfc3339(std::time::SystemTime::now()), 15 | record.target(), 16 | record.level(), 17 | message 18 | )) 19 | }) 20 | .level(level) 21 | .chain(std::io::stderr()); 22 | 23 | let dispatcher = if let Some(log_file) = &args.log_file { 24 | dispatcher.chain(fern::log_file(log_file)?) 25 | } else { 26 | dispatcher 27 | }; 28 | 29 | let dispatcher = if !args.disable_syslog_logger { 30 | let formatter = syslog::Formatter3164 { 31 | facility: syslog::Facility::LOG_USER, 32 | hostname: None, 33 | process: prog_name!().into(), 34 | pid: 0, 35 | }; 36 | 37 | match syslog::unix(formatter) { 38 | Err(e) => { 39 | eprintln!("cann't connect to syslog: {:?}", e); 40 | dispatcher 41 | } 42 | Ok(writer) => dispatcher.chain(writer), 43 | } 44 | } else { 45 | dispatcher 46 | }; 47 | 48 | dispatcher.apply()?; 49 | Ok(()) 50 | } 51 | 52 | use argh::FromArgs; 53 | 54 | /// Minimalistic menu launcher 55 | #[derive(FromArgs)] 56 | struct Args { 57 | /// increases log verbosity 58 | #[argh(switch, short = 'v')] 59 | verbose: bool, 60 | /// reduces log verbosity 61 | #[argh(switch, short = 'q')] 62 | quiet: bool, 63 | /// prompt to be displayed as a hint 64 | #[argh(option, short = 'p')] 65 | prompt: Option, 66 | /// password mode, i.e all characters displayed as `*` 67 | #[argh(switch)] 68 | password: bool, 69 | /// path to log file 70 | #[argh(option)] 71 | log_file: Option, 72 | /// disable syslog 73 | #[argh(switch, short = 'd')] 74 | disable_syslog_logger: bool, 75 | /// path to config file 76 | #[argh(option)] 77 | config_file: Option, 78 | /// mode to operate 79 | #[argh(subcommand)] 80 | mode: Option, 81 | } 82 | 83 | #[derive(FromArgs)] 84 | #[argh(subcommand)] 85 | enum ModeArg { 86 | Apps(AppsMode), 87 | Binapps(BinappsMode), 88 | Dialog(DialogMode), 89 | } 90 | 91 | /// Desktop apps mode 92 | #[derive(FromArgs)] 93 | #[argh(subcommand, name = "apps")] 94 | struct AppsMode { 95 | /// optional path to ignored desktop files. 96 | #[argh(option)] 97 | blacklist: Option, 98 | /// flag for listing desktop files for entries names. 99 | #[argh(switch, short = 'l')] 100 | list: bool, 101 | } 102 | 103 | /// Binaries mode 104 | #[derive(FromArgs)] 105 | #[argh(subcommand, name = "binapps")] 106 | struct BinappsMode {} 107 | 108 | /// Dialog mode 109 | #[derive(FromArgs)] 110 | #[argh(subcommand, name = "dialog")] 111 | struct DialogMode {} 112 | 113 | impl ModeArg { 114 | fn try_default() -> Result { 115 | let blacklist = xdg::BaseDirectories::with_prefix(prog_name!()) 116 | .context("failed to get xdg dirs")? 117 | .place_config_file("blacklist") 118 | .map_err(|e| log::error!("failed to create default blacklist file: {}", e)) 119 | .ok(); 120 | Ok(ModeArg::Apps(AppsMode { 121 | blacklist, 122 | list: false, 123 | })) 124 | } 125 | } 126 | 127 | fn main_inner() -> Result<()> { 128 | let mut args: Args = argh::from_env(); 129 | 130 | let mut config = config::Config::load(args.config_file.take())?; 131 | 132 | let log_level = match (args.verbose, args.quiet) { 133 | (true, true) => panic!("either verbose or quiet could be specified, not both"), 134 | (true, _) => LevelFilter::Debug, 135 | (_, true) => LevelFilter::Warn, 136 | (false, false) => LevelFilter::Info, 137 | }; 138 | 139 | setup_logger(log_level, &args)?; 140 | 141 | if let Some(prompt) = args.prompt.take() { 142 | config.override_prompt(prompt); 143 | } 144 | 145 | if args.password { 146 | config.override_password(); 147 | } 148 | 149 | let default_mode_arg; 150 | let mode_arg = match args.mode.as_ref() { 151 | Some(m) => m, 152 | None => { 153 | default_mode_arg = ModeArg::try_default()?; 154 | &default_mode_arg 155 | } 156 | }; 157 | let cmd = match mode_arg { 158 | ModeArg::Apps(AppsMode { blacklist, list }) => { 159 | let blacklist_filter = blacklist 160 | .as_ref() 161 | .and_then(|file| { 162 | let entries = std::fs::read_to_string(file) 163 | .map_err(|e| log::debug!("cannot read blacklist file {:?}: {}", file, e)) 164 | .ok()? 165 | .lines() 166 | .map(std::ffi::OsString::from) 167 | .collect::>(); 168 | 169 | Some(Box::new(move |e: &_| !entries.contains(e)) as Box bool>) 170 | }) 171 | .unwrap_or_else(|| Box::new(|_| true)); 172 | 173 | let entries = desktop::Traverser::new(config.param(), blacklist_filter) 174 | .context("cannot load desktop file traverser")? 175 | .find_entries(); 176 | 177 | if *list { 178 | for e in entries { 179 | println!("{}: {}", e.entry.name, e.desktop_fname); 180 | } 181 | return Ok(()); 182 | } 183 | 184 | mode::Mode::apps(entries, config.terminal_command()) 185 | } 186 | ModeArg::Binapps(BinappsMode {}) => { 187 | config.disable_icons(); 188 | mode::Mode::bins(config.terminal_command()) 189 | } 190 | ModeArg::Dialog(DialogMode {}) => mode::Mode::dialog()?, 191 | }; 192 | 193 | let state = state::State::new(cmd); 194 | let (mut window, mut event_loop) = 195 | window::Window::new(config, state).context("unable create a window")?; 196 | 197 | while !window.asked_exit() { 198 | event_loop.dispatch(None, &mut window)?; 199 | if let Some(err) = window.take_error() { 200 | return Err(err); 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | fn main() -> Result<()> { 208 | let res = std::panic::catch_unwind(main_inner); 209 | 210 | match res { 211 | Ok(res) => res, 212 | Err(err) => { 213 | let msg = if let Some(msg) = err.downcast_ref::() { 214 | msg.clone() 215 | } else if let Some(msg) = err.downcast_ref::<&str>() { 216 | msg.to_string() 217 | } else { 218 | "unknown panic".into() 219 | }; 220 | 221 | let _ = std::process::Command::new("timeout") 222 | .args([ 223 | "1s", 224 | "notify-send", 225 | concat!("--app-name=", prog_name!()), 226 | concat!(prog_name!(), " has panicked!"), 227 | &msg, 228 | ]) 229 | .status(); 230 | 231 | log::error!("panic: {}", msg); 232 | 233 | Err(anyhow::Error::msg(msg)) 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/mode.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::iter::ExactSizeIterator; 3 | 4 | use anyhow::{Context, Result}; 5 | use either::Either; 6 | use raqote::Image; 7 | 8 | use crate::input_parser::InputValue; 9 | use crate::DesktopEntry; 10 | 11 | mod apps; 12 | mod bins; 13 | mod dialog; 14 | 15 | macro_rules! delegate { 16 | (pub fn $name:ident ( &mut self ) -> $ret:ty $(, wrap_with ($wrap:path))?) => { 17 | delegate!(pub fn $name ( & [mut] self, ) -> $ret $(, wrap_with ($wrap))?); 18 | }; 19 | (pub fn $name:ident ( &mut self, $($ident:ident : $tp:ty),* ) -> $ret:ty $(, wrap_with ($wrap:path))?) => { 20 | delegate!(pub fn $name ( & [mut] self, $($ident : $tp),* ) -> $ret $(, wrap_with ($wrap))?); 21 | }; 22 | (pub fn $name:ident ( & $([$m:ident])? self ) -> $ret:ty $(, wrap_with ($wrap:path))?) => { 23 | delegate!(pub fn $name ( & $([$m])? self, ) -> $ret $(, wrap_with ($wrap))?); 24 | }; 25 | (pub fn $name:ident ( & $([$m:ident])? self, $($ident:ident : $tp:ty),* ) -> $ret:ty $(, wrap_with ($wrap:path))?) => { 26 | pub fn $name ( & $($m)? self, $($ident : $tp),* ) -> $ret { 27 | match self { 28 | Mode::Apps(mode) => $($wrap)?(mode.$name($($ident),*)), 29 | Mode::BinApps(mode) => $($wrap)?(mode.$name($($ident),*)), 30 | Mode::Dialog(mode) => $($wrap)?(mode.$name($($ident),*)), 31 | } 32 | } 33 | } 34 | } 35 | 36 | pub struct EvalInfo<'a> { 37 | pub index: Option, 38 | pub subindex: usize, 39 | pub input_value: &'a InputValue<'a>, 40 | } 41 | 42 | impl<'a> std::ops::Deref for EvalInfo<'a> { 43 | type Target = InputValue<'a>; 44 | 45 | fn deref(&self) -> &Self::Target { 46 | self.input_value 47 | } 48 | } 49 | 50 | pub enum Mode { 51 | Apps(apps::AppsMode), 52 | BinApps(bins::BinsMode), 53 | Dialog(dialog::DialogMode), 54 | } 55 | 56 | pub struct Entry<'a> { 57 | pub name: &'a str, 58 | pub subname: Option<&'a str>, 59 | pub icon: Option>, 60 | } 61 | 62 | impl Mode { 63 | pub fn apps(entries: Vec, term: Vec) -> Self { 64 | Self::Apps(apps::AppsMode::new(entries, term)) 65 | } 66 | 67 | pub fn bins(term: Vec) -> Self { 68 | Self::BinApps(bins::BinsMode::new(term)) 69 | } 70 | 71 | pub fn dialog() -> Result { 72 | dialog::DialogMode::new().map(Self::Dialog) 73 | } 74 | 75 | pub fn fork_eval(&mut self, info: EvalInfo<'_>) -> Result<()> { 76 | // Safety: 77 | // - no need for signal-safety as we single-thread everywhere; 78 | // - all file descriptors are closed; 79 | let pid = unsafe { nix::unistd::fork() }.context("fork() error")?; 80 | 81 | if pid.is_child() { 82 | use std::os::fd::AsRawFd; 83 | // Just in case, not sure it will break anything. 84 | let _ = nix::unistd::close(std::io::stdin().as_raw_fd()); 85 | let _ = nix::unistd::close(std::io::stdout().as_raw_fd()); 86 | let _ = nix::unistd::close(std::io::stderr().as_raw_fd()); 87 | 88 | self.eval(info)?; 89 | } 90 | 91 | Ok(()) 92 | } 93 | 94 | delegate!(pub fn eval(&mut self, info: EvalInfo<'_>) -> Result); 95 | delegate!(pub fn entries_len(&self) -> usize); 96 | delegate!(pub fn subentries_len(&self, idx: usize) -> usize); 97 | delegate!(pub fn entry(&self, idx: usize, subidx: usize) -> Entry<'_>); 98 | 99 | pub fn text_entries(&self) -> impl ExactSizeIterator + '_ { 100 | match self { 101 | Mode::Apps(mode) => Either::Left(Either::Right(mode.text_entries())), 102 | Mode::BinApps(mode) => Either::Left(Either::Left(mode.text_entries())), 103 | Mode::Dialog(mode) => Either::Right(mode.text_entries()), 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/mode/apps.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Reverse; 2 | use std::ffi::CString; 3 | 4 | use anyhow::{Context, Result}; 5 | 6 | use super::{Entry, EvalInfo}; 7 | use crate::usage_cache::Usage; 8 | use crate::DesktopEntry; 9 | 10 | const CACHE_PATH: &str = concat!(crate::prog_name!(), ".cache"); 11 | 12 | pub struct AppsMode { 13 | entries: Vec, 14 | term: Vec, 15 | usage: Usage, 16 | } 17 | 18 | impl AppsMode { 19 | pub fn new(mut entries: Vec, term: Vec) -> Self { 20 | let usage = Usage::from_path(CACHE_PATH); 21 | 22 | entries.sort_by_key(|e| Reverse(usage.entry_count(&e.desktop_fname))); 23 | 24 | Self { 25 | entries, 26 | term, 27 | usage, 28 | } 29 | } 30 | 31 | pub fn eval(&mut self, info: EvalInfo<'_>) -> Result { 32 | let idx = info.index.context("no app remain to launch")?; 33 | let entry = &self.entries[idx]; 34 | let exec = if info.subindex == 0 { 35 | &entry.entry.exec 36 | } else { 37 | &entry.actions[info.subindex - 1].exec 38 | }; 39 | 40 | let args = shlex::split(exec) 41 | .with_context(|| format!("invalid app command line: {exec}"))? 42 | .into_iter() 43 | .filter(|s| !s.starts_with('%')) // TODO: use placeholders somehow 44 | .map(|s| CString::new(s).expect("invalid argument")); 45 | 46 | self.usage 47 | .increment_entry_usage(entry.desktop_fname.clone()); 48 | self.usage.try_update_cache(CACHE_PATH); 49 | 50 | let term = if entry.is_terminal { 51 | Some(std::mem::take(&mut self.term)) 52 | } else { 53 | None 54 | }; 55 | 56 | crate::exec::exec(term, args, info.input_value) 57 | } 58 | 59 | pub fn entries_len(&self) -> usize { 60 | self.entries.len() 61 | } 62 | 63 | pub fn subentries_len(&self, idx: usize) -> usize { 64 | self.entries.get(idx).map(|e| e.actions.len()).unwrap_or(0) 65 | } 66 | 67 | pub fn entry(&self, idx: usize, subidx: usize) -> Entry<'_> { 68 | let entry = &self.entries[idx]; 69 | 70 | Entry { 71 | name: entry.entry.name.as_ref(), 72 | subname: Some(entry.subname(subidx).unwrap_or("Default Action")), 73 | icon: entry.icon(subidx).and_then(|i| i.as_image()), 74 | } 75 | } 76 | 77 | pub fn text_entries(&self) -> impl super::ExactSizeIterator { 78 | self.entries.iter().map(|e| e.name.as_str()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/mode/bins.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Reverse; 2 | use std::collections::HashMap; 3 | use std::ffi::CString; 4 | use std::os::unix::fs::PermissionsExt; 5 | 6 | use anyhow::{Context, Result}; 7 | 8 | use super::{Entry, EvalInfo}; 9 | use crate::usage_cache::Usage; 10 | 11 | const CACHE_PATH: &str = concat!(crate::prog_name!(), ".bincache"); 12 | 13 | #[derive(PartialEq, Eq, Hash)] 14 | struct Binary { 15 | path: String, 16 | fname: String, 17 | } 18 | 19 | pub struct BinsMode { 20 | bins: Vec, 21 | entry_name_cache: HashMap, 22 | term: Vec, 23 | usage: Usage, 24 | } 25 | 26 | impl BinsMode { 27 | pub fn new(term: Vec) -> Self { 28 | let usage = Usage::from_path(CACHE_PATH); 29 | 30 | let paths = std::env::var("PATH") 31 | .map(|paths| paths.split(':').map(|s| s.to_owned()).collect()) 32 | .unwrap_or_else(|_| vec!["/usr/bin".into()]); 33 | let mut bins: Vec<_> = paths 34 | .into_iter() 35 | .flat_map(|p| std::fs::read_dir(&p).map_err(|e| log::warn!("failed to read {p}: {e}"))) 36 | .flatten() 37 | .flatten() 38 | .filter_map(|f| { 39 | let meta = f.metadata().ok()?; 40 | if f.path().is_file() && meta.permissions().mode() & 0o001 > 0 { 41 | let p = f.path(); 42 | Some(Binary { 43 | path: p.to_str()?.to_owned(), 44 | fname: p.file_name()?.to_str()?.to_owned(), 45 | }) 46 | } else { 47 | None 48 | } 49 | }) 50 | .collect(); 51 | 52 | bins.sort_by(|x, y| { 53 | let x_usage_count = usage.entry_count(&x.path); 54 | let y_usage_count = usage.entry_count(&y.path); 55 | 56 | Reverse(x_usage_count) 57 | .cmp(&Reverse(y_usage_count)) 58 | .then_with(|| x.path.cmp(&y.path)) 59 | }); 60 | bins.dedup(); 61 | 62 | let mut fname_counts = HashMap::<_, u8>::new(); 63 | 64 | for b in &bins { 65 | let count = fname_counts.entry(b.fname.clone()).or_default(); 66 | *count = count.saturating_add(1); 67 | } 68 | 69 | let mut entry_name_cache = HashMap::new(); 70 | 71 | for bin in &bins { 72 | if fname_counts 73 | .get(&bin.fname) 74 | .filter(|&&cnt| cnt > 1) 75 | .is_none() 76 | { 77 | log::warn!("file name {} found multiple times, skipping", bin.fname); 78 | continue; 79 | } 80 | 81 | entry_name_cache.insert(bin.path.clone(), format!("{} ({})", bin.fname, bin.path)); 82 | } 83 | 84 | Self { 85 | bins, 86 | entry_name_cache, 87 | term, 88 | usage, 89 | } 90 | } 91 | 92 | pub fn eval(&mut self, info: EvalInfo<'_>) -> Result { 93 | let binary = if let Some(idx) = info.index { 94 | self.bins[idx].path.as_str() 95 | } else { 96 | info.search_string 97 | }; 98 | 99 | self.usage.increment_entry_usage(binary.to_string()); 100 | self.usage.try_update_cache(CACHE_PATH); 101 | 102 | crate::exec::exec( 103 | Some(std::mem::take(&mut self.term)), 104 | std::iter::once(CString::new(binary).context("invalid binary name")?), 105 | info.input_value, 106 | ) 107 | } 108 | 109 | pub fn entries_len(&self) -> usize { 110 | self.bins.len() 111 | } 112 | 113 | pub fn subentries_len(&self, _: usize) -> usize { 114 | 0 115 | } 116 | 117 | pub fn entry(&self, idx: usize, _: usize) -> Entry<'_> { 118 | let bin = &self.bins[idx]; 119 | 120 | let name = if let Some(name) = self.entry_name_cache.get(&bin.path) { 121 | name.as_str() 122 | } else { 123 | bin.fname.as_str() 124 | }; 125 | 126 | Entry { 127 | name, 128 | subname: None, 129 | icon: None, 130 | } 131 | } 132 | 133 | pub fn text_entries(&self) -> impl super::ExactSizeIterator { 134 | self.bins.iter().map(|e| e.fname.as_str()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/mode/dialog.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | use super::{Entry, EvalInfo}; 4 | 5 | pub struct DialogMode { 6 | lines: Vec, 7 | } 8 | 9 | impl DialogMode { 10 | pub fn new() -> Result { 11 | std::io::stdin() 12 | .lines() 13 | .collect::>() 14 | .context("failed to read stdin") 15 | .map(|lines| Self { lines }) 16 | } 17 | 18 | pub fn eval(&mut self, info: EvalInfo<'_>) -> Result { 19 | let value = info 20 | .index 21 | .and_then(|idx| Some(self.lines.get(idx)?.as_str())) 22 | .unwrap_or(info.input_value.source); 23 | println!("{value}"); 24 | std::process::exit(0); 25 | } 26 | 27 | pub fn entries_len(&self) -> usize { 28 | self.lines.len() 29 | } 30 | 31 | pub fn subentries_len(&self, _: usize) -> usize { 32 | 0 33 | } 34 | 35 | pub fn entry(&self, idx: usize, _: usize) -> Entry<'_> { 36 | Entry { 37 | name: self.lines[idx].as_ref(), 38 | subname: None, 39 | icon: None, 40 | } 41 | } 42 | 43 | pub fn text_entries(&self) -> impl super::ExactSizeIterator { 44 | self.lines.iter().map(|e| e.as_str()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::draw::ListItem; 2 | use crate::input_parser::InputValue; 3 | use crate::mode::{EvalInfo, Mode}; 4 | pub use filtered_lines::ContinuousMatch; 5 | use filtered_lines::FilteredLines; 6 | 7 | mod filtered_lines; 8 | 9 | struct InputBuffer { 10 | raw_input: String, 11 | parsed_input: InputValue<'static>, 12 | } 13 | 14 | impl InputBuffer { 15 | pub fn new() -> Self { 16 | Self { 17 | raw_input: String::new(), 18 | parsed_input: InputValue::empty(), 19 | } 20 | } 21 | 22 | pub fn update_input(&mut self, f: impl FnOnce(&mut String)) { 23 | f(&mut self.raw_input); 24 | 25 | let parsed = crate::input_parser::parse(&self.raw_input); 26 | 27 | // This transmute is needed for extending `raw_input` lifetime 28 | // to a static one thus making it possible to cache parsed result. 29 | // Safety: this is safe, because it's internal structure invariant 30 | // that `parsed_input` never outlives `raw_input`, nor used after 31 | // its update. 32 | self.parsed_input = unsafe { std::mem::transmute(parsed) }; 33 | } 34 | 35 | pub fn raw_input(&self) -> &str { 36 | self.raw_input.as_str() 37 | } 38 | 39 | pub fn parsed_input<'a>(&self) -> &InputValue<'a> { 40 | &self.parsed_input 41 | } 42 | 43 | pub fn search_string(&self) -> &str { 44 | self.parsed_input.search_string 45 | } 46 | } 47 | 48 | pub struct State { 49 | input_buffer: InputBuffer, 50 | skip_offset: usize, 51 | selected_item: usize, 52 | selected_subitem: usize, 53 | filtered_lines: FilteredLines, 54 | inner: Mode, 55 | } 56 | 57 | impl State { 58 | pub fn new(inner: Mode) -> Self { 59 | Self { 60 | input_buffer: InputBuffer::new(), 61 | skip_offset: 0, 62 | selected_item: 0, 63 | selected_subitem: 0, 64 | filtered_lines: FilteredLines::unfiltred(inner.entries_len()), 65 | inner, 66 | } 67 | } 68 | 69 | pub fn remove_input_char(&mut self) { 70 | self.input_buffer.update_input(|input| { 71 | input.pop(); 72 | }) 73 | } 74 | 75 | pub fn remove_input_word(&mut self) { 76 | self.input_buffer.update_input(|input| { 77 | if let Some(pos) = input.rfind(|x: char| !x.is_alphanumeric()) { 78 | input.truncate(pos); 79 | } else { 80 | input.clear(); 81 | } 82 | }) 83 | } 84 | 85 | pub fn append_to_input(&mut self, s: &str) { 86 | self.input_buffer.update_input(|input| input.push_str(s)) 87 | } 88 | 89 | pub fn clear_input(&mut self) { 90 | self.input_buffer.update_input(|input| input.clear()) 91 | } 92 | 93 | pub fn eval_input(&mut self, with_fork: bool) -> anyhow::Result<()> { 94 | let info = EvalInfo { 95 | index: self.filtered_lines.index(self.selected_item), 96 | subindex: self.selected_subitem, 97 | input_value: self.input_buffer.parsed_input(), 98 | }; 99 | if with_fork { 100 | self.inner.fork_eval(info) 101 | } else { 102 | self.inner 103 | .eval(info) 104 | .map(|_: std::convert::Infallible| unreachable!()) 105 | } 106 | } 107 | 108 | pub fn next_item(&mut self) { 109 | self.selected_subitem = 0; 110 | self.selected_item = self 111 | .inner 112 | .entries_len() 113 | .saturating_sub(1) 114 | .min(self.selected_item + 1); 115 | } 116 | 117 | pub fn prev_item(&mut self) { 118 | self.selected_subitem = 0; 119 | self.selected_item = self.selected_item.saturating_sub(1); 120 | } 121 | 122 | pub fn next_subitem(&mut self) { 123 | self.selected_subitem = self 124 | .inner 125 | .subentries_len(self.filtered_lines.index(self.selected_item).unwrap_or(0)) 126 | .min(self.selected_subitem + 1) 127 | } 128 | 129 | pub fn prev_subitem(&mut self) { 130 | self.selected_subitem = self.selected_subitem.saturating_sub(1) 131 | } 132 | 133 | pub fn raw_input(&self) -> &str { 134 | self.input_buffer.raw_input() 135 | } 136 | 137 | pub fn skip_offset(&self) -> usize { 138 | self.skip_offset 139 | } 140 | 141 | pub fn update_skip_offset(&mut self, x: usize) { 142 | self.skip_offset = x; 143 | } 144 | 145 | pub fn selected_item(&self) -> usize { 146 | self.selected_item 147 | } 148 | 149 | pub fn processed_entries(&self) -> impl Iterator> { 150 | self.filtered_lines 151 | .list_items(&self.inner, self.selected_item, self.selected_subitem) 152 | } 153 | 154 | pub fn process_entries(&mut self) { 155 | self.filtered_lines = if self.input_buffer.search_string().is_empty() { 156 | FilteredLines::unfiltred(self.inner.entries_len()) 157 | } else { 158 | FilteredLines::searched(self.inner.text_entries(), self.input_buffer.search_string()) 159 | }; 160 | 161 | self.selected_item = self 162 | .filtered_lines 163 | .len() 164 | .saturating_sub(1) 165 | .min(self.selected_item); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/state/filtered_lines.rs: -------------------------------------------------------------------------------- 1 | use either::Either; 2 | use sublime_fuzzy::Match; 3 | 4 | use crate::draw::ListItem; 5 | use crate::mode::Mode; 6 | 7 | pub type ContinuousMatch<'a> = sublime_fuzzy::ContinuousMatches<'a>; 8 | 9 | pub struct FilteredLines(Either, usize>); 10 | 11 | impl FilteredLines { 12 | pub fn searched<'a>(entries: impl Iterator, search_string: &str) -> Self { 13 | let mut v = entries 14 | .enumerate() 15 | .filter_map(|(i, e)| Some((i, sublime_fuzzy::best_match(search_string, e)?))) 16 | .collect::>(); 17 | v.sort_by_key(|(_, m)| std::cmp::Reverse(m.score())); 18 | Self(Either::Left(v)) 19 | } 20 | 21 | pub fn unfiltred(len: usize) -> Self { 22 | Self(Either::Right(len)) 23 | } 24 | 25 | pub fn len(&self) -> usize { 26 | match self { 27 | Self(Either::Left(x)) => x.len(), 28 | Self(Either::Right(x)) => *x, 29 | } 30 | } 31 | 32 | pub fn index(&self, selected_item: usize) -> Option { 33 | if self.len() == 0 { 34 | return None; 35 | } 36 | 37 | if selected_item >= self.len() { 38 | panic!("Internal error: selected_item overflow"); 39 | } 40 | 41 | Some(match self { 42 | Self(Either::Left(x)) => x[selected_item].0, 43 | Self(Either::Right(_)) => selected_item, 44 | }) 45 | } 46 | 47 | pub fn list_items<'s, 'm: 's>( 48 | &'s self, 49 | mode: &'m Mode, 50 | item: usize, 51 | subitem: usize, 52 | ) -> impl Iterator> + '_ { 53 | match self { 54 | Self(Either::Left(x)) => { 55 | Either::Left(x.iter().enumerate().map(move |(idx, (item_idx, s_match))| { 56 | let e = mode.entry(*item_idx, if idx == item { subitem } else { 0 }); 57 | ListItem { 58 | name: e.name, 59 | subname: e.subname, 60 | icon: e.icon, 61 | match_mask: Some(s_match.continuous_matches()), 62 | } 63 | })) 64 | } 65 | Self(Either::Right(x)) => Either::Right((0..*x).enumerate().map(move |(idx, i)| { 66 | let e = mode.entry(i, if idx == item { subitem } else { 0 }); 67 | ListItem { 68 | name: e.name, 69 | subname: e.subname, 70 | icon: e.icon, 71 | match_mask: None, 72 | } 73 | })), 74 | } 75 | .into_iter() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::marker::PhantomData; 3 | use std::ops::Mul; 4 | use std::str::FromStr; 5 | 6 | use serde::de::{Deserializer, Visitor}; 7 | use serde::Deserialize; 8 | 9 | #[derive(Clone, Default)] 10 | #[cfg_attr(test, derive(Debug, PartialEq))] 11 | pub struct Padding { 12 | pub top: f32, 13 | pub bottom: f32, 14 | pub left: f32, 15 | pub right: f32, 16 | } 17 | 18 | #[derive(Clone, Default)] 19 | #[cfg_attr(test, derive(Debug, PartialEq))] 20 | pub struct Margin { 21 | pub top: f32, 22 | pub bottom: f32, 23 | pub left: f32, 24 | pub right: f32, 25 | } 26 | 27 | #[derive(Clone, Default)] 28 | #[cfg_attr(test, derive(Debug, PartialEq))] 29 | pub struct Radius { 30 | pub top_left: f32, 31 | pub top_right: f32, 32 | pub bottom_left: f32, 33 | pub bottom_right: f32, 34 | } 35 | 36 | impl Padding { 37 | pub const fn all(val: f32) -> Self { 38 | Self { 39 | top: val, 40 | bottom: val, 41 | left: val, 42 | right: val, 43 | } 44 | } 45 | 46 | pub const fn from_pair(vertical: f32, horizontal: f32) -> Self { 47 | Self { 48 | top: vertical, 49 | bottom: vertical, 50 | left: horizontal, 51 | right: horizontal, 52 | } 53 | } 54 | 55 | pub const fn from_four(top: f32, right: f32, bottom: f32, left: f32) -> Self { 56 | Self { 57 | top, 58 | bottom, 59 | left, 60 | right, 61 | } 62 | } 63 | } 64 | 65 | impl Mul for &Padding { 66 | type Output = Padding; 67 | 68 | fn mul(self, rhs: f32) -> Padding { 69 | Padding { 70 | top: self.top * rhs, 71 | bottom: self.bottom * rhs, 72 | left: self.left * rhs, 73 | right: self.right * rhs, 74 | } 75 | } 76 | } 77 | 78 | impl Margin { 79 | pub const fn all(val: f32) -> Self { 80 | Self { 81 | top: val, 82 | bottom: val, 83 | left: val, 84 | right: val, 85 | } 86 | } 87 | 88 | pub const fn from_pair(vertical: f32, horizontal: f32) -> Self { 89 | Self { 90 | top: vertical, 91 | bottom: vertical, 92 | left: horizontal, 93 | right: horizontal, 94 | } 95 | } 96 | 97 | pub const fn from_four(top: f32, right: f32, bottom: f32, left: f32) -> Self { 98 | Self { 99 | top, 100 | bottom, 101 | left, 102 | right, 103 | } 104 | } 105 | } 106 | 107 | impl Mul for &Radius { 108 | type Output = Radius; 109 | 110 | fn mul(self, rhs: f32) -> Radius { 111 | Radius { 112 | top_left: self.top_left * rhs, 113 | top_right: self.top_right * rhs, 114 | bottom_left: self.bottom_left * rhs, 115 | bottom_right: self.bottom_right * rhs, 116 | } 117 | } 118 | } 119 | 120 | impl Radius { 121 | pub const fn all(val: f32) -> Self { 122 | Self { 123 | top_left: val, 124 | top_right: val, 125 | bottom_left: val, 126 | bottom_right: val, 127 | } 128 | } 129 | 130 | pub const fn from_pair(first: f32, second: f32) -> Self { 131 | Self { 132 | top_left: first, 133 | top_right: second, 134 | bottom_left: second, 135 | bottom_right: first, 136 | } 137 | } 138 | 139 | pub(crate) fn min(&self, other: Radius) -> Radius { 140 | Self { 141 | top_left: self.top_left.min(other.top_left), 142 | top_right: self.top_right.min(other.top_right), 143 | bottom_left: self.bottom_left.min(other.bottom_left), 144 | bottom_right: self.bottom_right.min(other.bottom_right), 145 | } 146 | } 147 | 148 | pub const fn from_four( 149 | top_left: f32, 150 | top_right: f32, 151 | bottom_right: f32, 152 | bottom_left: f32, 153 | ) -> Self { 154 | Self { 155 | top_left, 156 | top_right, 157 | bottom_left, 158 | bottom_right, 159 | } 160 | } 161 | } 162 | 163 | impl Mul for &Margin { 164 | type Output = Margin; 165 | 166 | fn mul(self, rhs: f32) -> Margin { 167 | Margin { 168 | top: self.top * rhs, 169 | bottom: self.bottom * rhs, 170 | left: self.left * rhs, 171 | right: self.right * rhs, 172 | } 173 | } 174 | } 175 | 176 | macro_rules! impl_traits { 177 | ($t:ty) => { 178 | impl<'de> Deserialize<'de> for $t { 179 | fn deserialize(d: D) -> Result 180 | where 181 | D: Deserializer<'de>, 182 | { 183 | d.deserialize_str(StringVisitor(PhantomData)) 184 | } 185 | } 186 | 187 | impl FromStr for $t { 188 | type Err = String; 189 | 190 | fn from_str(s: &str) -> Result { 191 | let values = FiniteFloatVec::from_str(s)?.values; 192 | 193 | match values.len() { 194 | 1 => Ok(Self::all(values[0])), 195 | 2 => Ok(Self::from_pair(values[0], values[1])), 196 | 4 => Ok(Self::from_four(values[0], values[1], values[2], values[3])), 197 | _ => Err(concat!( 198 | stringify!($t), 199 | " should consists of either 1, 2 or 4 floats" 200 | ) 201 | .into()), 202 | } 203 | } 204 | } 205 | }; 206 | } 207 | 208 | impl_traits!(Padding); 209 | impl_traits!(Margin); 210 | impl_traits!(Radius); 211 | 212 | struct StringVisitor(PhantomData); 213 | 214 | impl<'de, T> Visitor<'de> for StringVisitor 215 | where 216 | T: FromStr, 217 | ::Err: Display, 218 | { 219 | type Value = T; 220 | 221 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 222 | formatter.write_str("unexpected value") 223 | } 224 | 225 | fn visit_str(self, value: &str) -> Result 226 | where 227 | E: serde::de::Error, 228 | { 229 | FromStr::from_str(value).map_err(serde::de::Error::custom) 230 | } 231 | } 232 | 233 | struct FiniteFloatVec { 234 | pub values: Vec, 235 | } 236 | 237 | impl FromStr for FiniteFloatVec { 238 | type Err = String; 239 | 240 | fn from_str(s: &str) -> Result { 241 | let values = s 242 | .split(' ') 243 | .map(|s| s.trim()) 244 | .filter(|s| !s.is_empty()) 245 | .map(|s| { 246 | s.parse::() 247 | .map_err(|_| format!("invalid float value: {:?}", s)) 248 | .and_then(|f| { 249 | f.is_finite() 250 | .then_some(f) 251 | .ok_or(format!("non-finite float value: {:?}", f)) 252 | }) 253 | }) 254 | .collect::, _>>()?; 255 | 256 | Ok(Self { values }) 257 | } 258 | } 259 | 260 | #[cfg(test)] 261 | mod tests { 262 | use super::*; 263 | 264 | #[test] 265 | fn from_str_margin() { 266 | let parse = |s: &str| s.parse::().unwrap(); 267 | 268 | assert_eq!(Margin::all(1.2), parse("1.2")); 269 | assert_eq!(Margin::from_pair(1.2, 3.4), parse("1.2 3.4")); 270 | assert_eq!( 271 | Margin::from_four(1.2, 3.4, 5.6, 7.8), 272 | parse("1.2 3.4 5.6 7.8") 273 | ); 274 | } 275 | 276 | #[test] 277 | fn from_str_padding() { 278 | let parse = |s: &str| s.parse::().unwrap(); 279 | 280 | assert_eq!(Padding::all(1.2), parse("1.2")); 281 | assert_eq!(Padding::from_pair(1.2, 3.4), parse("1.2 3.4")); 282 | assert_eq!( 283 | Padding::from_four(1.2, 3.4, 5.6, 7.8), 284 | parse("1.2 3.4 5.6 7.8") 285 | ); 286 | } 287 | 288 | #[test] 289 | fn from_str_radius() { 290 | let parse = |s: &str| s.parse::().unwrap(); 291 | 292 | assert_eq!(Radius::all(1.2), parse("1.2")); 293 | assert_eq!(Radius::from_pair(1.2, 3.4), parse("1.2 3.4")); 294 | assert_eq!( 295 | Radius::from_four(1.2, 3.4, 5.6, 7.8), 296 | parse("1.2 3.4 5.6 7.8") 297 | ); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/usage_cache.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::hash::Hash; 5 | use std::io::{BufRead, BufReader, Write}; 6 | use std::path::Path; 7 | 8 | pub struct Usage(HashMap); 9 | 10 | impl Usage { 11 | pub fn from_path(path: impl AsRef) -> Self { 12 | let usage = crate::desktop::xdg_dirs() 13 | .place_cache_file(path) 14 | .and_then(File::open) 15 | .map_err(|e| { 16 | if e.kind() != std::io::ErrorKind::NotFound { 17 | log::error!("cannot open cache file: {}", e) 18 | } 19 | }) 20 | .map(BufReader::new) 21 | .into_iter() 22 | .flat_map(|rdr| { 23 | rdr.lines() 24 | .filter(|l| l.as_ref().map(|l| !l.is_empty()).unwrap_or(true)) 25 | .map(|l| { 26 | let line = l.map_err(|e| { 27 | log::error!("unable to read the line from cache: {}", e) 28 | })?; 29 | let mut iter = line.split(' '); 30 | let (count, entry) = (iter.next().ok_or(())?, iter.next().ok_or(())?); 31 | 32 | let count = count.parse().map_err(|e| { 33 | log::error!( 34 | "invalid cache file, unable to parse count (\"{}\"): {}", 35 | count, 36 | e 37 | ) 38 | })?; 39 | 40 | Ok((entry.to_string(), count)) 41 | }) 42 | }) 43 | .collect::>() 44 | .unwrap_or_default(); 45 | 46 | Self(usage) 47 | } 48 | 49 | pub fn entry_count(&self, entry: &Q) -> usize 50 | where 51 | String: Borrow, 52 | Q: Hash + Eq, 53 | { 54 | self.0.get(entry).copied().unwrap_or(0) 55 | } 56 | 57 | pub fn increment_entry_usage(&mut self, entry: String) { 58 | *self.0.entry(entry).or_default() += 1; 59 | } 60 | 61 | pub fn try_update_cache(&self, path: impl AsRef) { 62 | if let Err(e) = crate::desktop::xdg_dirs() 63 | .place_cache_file(path) 64 | .and_then(File::create) 65 | .and_then(|mut f| { 66 | let mut buf = vec![]; 67 | 68 | for (entry, count) in &self.0 { 69 | let s = format!("{} ", count); 70 | buf.extend(s.as_bytes()); 71 | buf.extend(entry.as_bytes()); 72 | buf.push(b'\n'); 73 | } 74 | 75 | f.write_all(&buf) 76 | }) 77 | { 78 | log::error!("failed to update cache: {}", e); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use sctk::{ 3 | delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer, 4 | delegate_registry, delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, 5 | output::OutputState, 6 | reexports::client::{ 7 | protocol::{wl_keyboard::WlKeyboard, wl_pointer::WlPointer, wl_surface::WlSurface}, 8 | *, 9 | }, 10 | reexports::{ 11 | calloop::{EventLoop, LoopHandle}, 12 | calloop_wayland_source::WaylandSource, 13 | }, 14 | registry::RegistryState, 15 | seat::SeatState, 16 | shell::{ 17 | wlr_layer, 18 | xdg::{self, window as xdg_win}, 19 | WaylandSurface, 20 | }, 21 | shm::{ 22 | slot::{Buffer, SlotPool}, 23 | Shm, 24 | }, 25 | }; 26 | 27 | use crate::state::State; 28 | pub use pointer::Params as PointerParams; 29 | 30 | mod compositor; 31 | mod keyboard; 32 | mod layer_shell; 33 | mod output; 34 | mod pointer; 35 | mod registry; 36 | mod seat; 37 | mod shm; 38 | mod xdg_window; 39 | 40 | pub struct Params { 41 | pub width: u32, 42 | pub height: u32, 43 | pub force_window: bool, 44 | pub window_offsets: Option<(i32, i32)>, 45 | pub scale: Option, 46 | } 47 | 48 | pub struct Window { 49 | config: crate::config::Config, 50 | state: State, 51 | 52 | registry_state: RegistryState, 53 | seat_state: SeatState, 54 | output_state: OutputState, 55 | 56 | buffer: Option, 57 | pool: SlotPool, 58 | shm: Shm, 59 | surface: RenderSurface, 60 | configured_surface: bool, 61 | width: u32, 62 | height: u32, 63 | scale: u16, 64 | 65 | input: InputSource, 66 | key_modifiers: sctk::seat::keyboard::Modifiers, 67 | wheel_scroll_pending: f64, 68 | 69 | loop_handle: LoopHandle<'static, Window>, 70 | exit: bool, 71 | 72 | error: Option, 73 | } 74 | 75 | struct InputSource { 76 | keyboard: Option, 77 | pointer: Option, 78 | } 79 | 80 | enum RenderSurface { 81 | Xdg(xdg_win::Window), 82 | LayerShell(wlr_layer::LayerSurface), 83 | } 84 | 85 | impl std::ops::Deref for RenderSurface { 86 | type Target = WlSurface; 87 | 88 | fn deref(&self) -> &Self::Target { 89 | match &self { 90 | RenderSurface::LayerShell(s) => s.wl_surface(), 91 | RenderSurface::Xdg(s) => s.wl_surface(), 92 | } 93 | } 94 | } 95 | 96 | impl Window { 97 | pub fn new( 98 | config: crate::config::Config, 99 | state: State, 100 | ) -> anyhow::Result<(Self, EventLoop<'static, Self>)> { 101 | let conn = Connection::connect_to_env()?; 102 | 103 | let (globals, event_queue) = globals::registry_queue_init(&conn)?; 104 | let qh = event_queue.handle(); 105 | let event_loop: EventLoop = 106 | EventLoop::try_new().context("failed to initialize the event loop")?; 107 | let loop_handle = event_loop.handle(); 108 | WaylandSource::new(conn.clone(), event_queue).insert(loop_handle)?; 109 | 110 | let params: Params = config.param(); 111 | let scale = params.scale.unwrap_or(1); 112 | let (width, height) = (params.width, params.height); 113 | let shm = Shm::bind(&globals, &qh).context("wl_shm is not available")?; 114 | let pool = SlotPool::new( 115 | (4 * width * u32::from(scale) * height * u32::from(scale)) as usize, 116 | &shm, 117 | ) 118 | .context("Failed to create a memory pool!")?; 119 | 120 | let compositor = sctk::compositor::CompositorState::bind(&globals, &qh) 121 | .context("wl_compositor is not available")?; 122 | let surface = compositor.create_surface(&qh); 123 | let surface = if let Some(layer_shell) = wlr_layer::LayerShell::bind(&globals, &qh) 124 | .ok() 125 | .filter(|_| !params.force_window) 126 | { 127 | let layer = layer_shell.create_layer_surface( 128 | &qh, 129 | surface, 130 | wlr_layer::Layer::Top, 131 | Some(crate::prog_name!()), 132 | None, 133 | ); 134 | 135 | if let Some((top_offset, left_offset)) = params.window_offsets { 136 | layer.set_anchor(wlr_layer::Anchor::LEFT | wlr_layer::Anchor::TOP); 137 | layer.set_margin(top_offset, 0, 0, left_offset); 138 | } 139 | layer.set_size(width, height); 140 | layer.set_keyboard_interactivity(wlr_layer::KeyboardInteractivity::Exclusive); 141 | 142 | layer.commit(); 143 | 144 | RenderSurface::LayerShell(layer) 145 | } else { 146 | let xdg_shell = 147 | xdg::XdgShell::bind(&globals, &qh).context("xdg shell is not available")?; 148 | let window = xdg_shell.create_window(surface, xdg_win::WindowDecorations::None, &qh); 149 | window.set_title(crate::prog_name!()); 150 | window.set_min_size(Some((width, height))); 151 | window.unset_fullscreen(); 152 | RenderSurface::Xdg(window) 153 | }; 154 | 155 | Ok(( 156 | Self { 157 | config, 158 | state, 159 | registry_state: RegistryState::new(&globals), 160 | seat_state: SeatState::new(&globals, &qh), 161 | output_state: OutputState::new(&globals, &qh), 162 | buffer: None, 163 | pool, 164 | shm, 165 | surface, 166 | configured_surface: false, 167 | width, 168 | height, 169 | scale, 170 | input: InputSource { 171 | keyboard: None, 172 | pointer: None, 173 | }, 174 | key_modifiers: Default::default(), 175 | wheel_scroll_pending: 0.0, 176 | loop_handle: event_loop.handle(), 177 | exit: false, 178 | error: None, 179 | }, 180 | event_loop, 181 | )) 182 | } 183 | 184 | fn width(&self) -> u32 { 185 | self.width * u32::from(self.scale) 186 | } 187 | 188 | fn height(&self) -> u32 { 189 | self.height * u32::from(self.scale) 190 | } 191 | 192 | pub fn draw(&mut self, qh: &QueueHandle) { 193 | let width = self.width().try_into().expect("width overflow"); 194 | let height = self.height().try_into().expect("height overflow"); 195 | let stride = width * 4; 196 | self.surface.set_buffer_scale(self.scale.into()); 197 | 198 | if self 199 | .buffer 200 | .as_ref() 201 | .filter(|b| b.height() != height || b.stride() != stride) 202 | .is_some() 203 | { 204 | self.buffer.take(); 205 | } 206 | 207 | const FORMAT: protocol::wl_shm::Format = protocol::wl_shm::Format::Argb8888; 208 | let mut buffer = self.buffer.take().unwrap_or_else(|| { 209 | self.pool 210 | .create_buffer(width, height, stride, FORMAT) 211 | .expect("create buffer") 212 | .0 213 | }); 214 | 215 | let canvas = match self.pool.canvas(&buffer) { 216 | Some(canvas) => canvas, 217 | None => { 218 | // This should be rare, but if the compositor has not released the previous 219 | // buffer, we need double-buffering. 220 | let (second_buffer, canvas) = self 221 | .pool 222 | .create_buffer(width, height, stride, FORMAT) 223 | .expect("create buffer"); 224 | buffer = second_buffer; 225 | canvas 226 | } 227 | }; 228 | 229 | use crate::draw::*; 230 | let mut dt = { 231 | #[allow(clippy::needless_lifetimes)] 232 | fn transmute_slice<'a>(a: &'a mut [u8]) -> &'a mut [u32] { 233 | assert_eq!(a.as_ptr().align_offset(std::mem::align_of::()), 0); 234 | assert_eq!(a.len() % 4, 0); 235 | // Safety: 236 | // - (asserted) it's well-aligned for *u32 237 | // - canvas is a valid mut slice 238 | // - it does not alias with original reference as it's been shadowed 239 | // - len does not overflow as it reduced from the valid len 240 | // - lifetimes are same 241 | unsafe { 242 | &mut *std::ptr::slice_from_raw_parts_mut(a.as_mut_ptr().cast(), a.len() / 4) 243 | } 244 | } 245 | let canvas = transmute_slice(canvas); 246 | DrawTarget::from_backing(width, height, canvas) 247 | }; 248 | 249 | let mut space_left = Space { 250 | width: width as f32, 251 | height: height as f32, 252 | }; 253 | let mut point = Point::new(0., 0.); 254 | 255 | let mut drawables = crate::draw::make_drawables(&self.config, &mut self.state); 256 | while let Some(d) = drawables.borrowed_next() { 257 | let occupied = d.draw(&mut dt, self.scale, space_left, point); 258 | debug_assert!( 259 | occupied.width <= space_left.width && occupied.height <= space_left.height 260 | ); 261 | 262 | point.y += occupied.height; 263 | space_left.height -= occupied.height; 264 | } 265 | 266 | self.surface.damage_buffer(0, 0, width, height); 267 | self.surface.frame(qh, self.surface.clone()); 268 | buffer.attach_to(&self.surface).expect("buffer attach"); 269 | self.buffer = Some(buffer); 270 | self.surface.commit(); 271 | } 272 | 273 | pub fn asked_exit(&self) -> bool { 274 | self.exit 275 | } 276 | 277 | pub fn take_error(&mut self) -> Option { 278 | self.error.take() 279 | } 280 | } 281 | 282 | delegate_compositor!(Window); 283 | delegate_output!(Window); 284 | delegate_shm!(Window); 285 | delegate_seat!(Window); 286 | delegate_keyboard!(Window); 287 | delegate_xdg_shell!(Window); 288 | delegate_layer!(Window); 289 | delegate_xdg_window!(Window); 290 | delegate_registry!(Window); 291 | delegate_pointer!(Window); 292 | -------------------------------------------------------------------------------- /src/window/compositor.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use sctk::{ 3 | compositor::CompositorHandler, 4 | reexports::client::{ 5 | protocol::{wl_output, wl_surface::WlSurface}, 6 | *, 7 | }, 8 | }; 9 | 10 | use super::Window; 11 | 12 | impl CompositorHandler for Window { 13 | fn scale_factor_changed( 14 | &mut self, 15 | _conn: &Connection, 16 | _qh: &QueueHandle, 17 | _surface: &WlSurface, 18 | new_factor: i32, 19 | ) { 20 | let old_scale = self.scale; 21 | self.scale = new_factor.try_into().expect("invalid surface scale factor"); 22 | if old_scale != self.scale { 23 | let size = (4 * self.width() * self.height()) 24 | .try_into() 25 | .expect("pixel buffer overflow"); 26 | if let Err(err) = self 27 | .pool 28 | .resize(size) 29 | .with_context(|| format!("on pool resize to {size}")) 30 | { 31 | self.error = Some(err); 32 | } 33 | } 34 | } 35 | 36 | fn transform_changed( 37 | &mut self, 38 | _conn: &Connection, 39 | _qh: &QueueHandle, 40 | _surface: &WlSurface, 41 | _new_transform: wl_output::Transform, 42 | ) { 43 | log::warn!("unexpected transform_changed") 44 | } 45 | 46 | fn frame( 47 | &mut self, 48 | _conn: &Connection, 49 | qh: &QueueHandle, 50 | _surface: &WlSurface, 51 | _time: u32, 52 | ) { 53 | self.draw(qh); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/window/keyboard.rs: -------------------------------------------------------------------------------- 1 | use sctk::{ 2 | reexports::client::{ 3 | protocol::{wl_keyboard::WlKeyboard, wl_surface::WlSurface}, 4 | *, 5 | }, 6 | seat::keyboard::{KeyboardHandler, Modifiers}, 7 | }; 8 | 9 | use super::Window; 10 | 11 | impl KeyboardHandler for Window { 12 | fn press_key( 13 | &mut self, 14 | _conn: &Connection, 15 | _qh: &QueueHandle, 16 | _keyboard: &WlKeyboard, 17 | _serial: u32, 18 | event: sctk::seat::keyboard::KeyEvent, 19 | ) { 20 | use sctk::seat::keyboard::Keysym; 21 | type M = Modifiers; 22 | match (event.keysym, self.key_modifiers) { 23 | (Keysym::Escape, _) | (Keysym::c, M { ctrl: true, .. }) => { 24 | self.exit = true; 25 | } 26 | (Keysym::Down, _) 27 | | (Keysym::j, M { ctrl: true, .. }) 28 | | (Keysym::Tab, M { shift: false, .. }) 29 | | (Keysym::ISO_Left_Tab, M { shift: false, .. }) => self.state.next_item(), 30 | (Keysym::Up, _) 31 | | (Keysym::k, M { ctrl: true, .. }) 32 | | (Keysym::Tab, M { shift: true, .. }) 33 | | (Keysym::ISO_Left_Tab, M { shift: true, .. }) => self.state.prev_item(), 34 | (Keysym::Left, _) => self.state.prev_subitem(), 35 | (Keysym::Right, _) => self.state.next_subitem(), 36 | (Keysym::Return, M { ctrl, .. }) | (Keysym::ISO_Enter, M { ctrl, .. }) => { 37 | if let Err(err) = self.state.eval_input(ctrl) { 38 | self.error = Some(err); 39 | } 40 | } 41 | (Keysym::BackSpace, M { ctrl: false, .. }) => self.state.remove_input_char(), 42 | (Keysym::BackSpace, M { ctrl: true, .. }) | (Keysym::w, M { ctrl: true, .. }) => { 43 | self.state.remove_input_word() 44 | } 45 | (Keysym::bracketright, M { ctrl: true, .. }) => self.state.clear_input(), 46 | // XXX: use if-let guards once stabilized 47 | (_, M { ctrl: false, .. }) if event.utf8.is_some() => { 48 | self.state.append_to_input(event.utf8.as_ref().unwrap()) 49 | } 50 | (k, m) => log::debug!( 51 | "unhandled sym: {:?} (ctrl: {}, shift: {})", 52 | k, 53 | m.ctrl, 54 | m.shift 55 | ), 56 | } 57 | } 58 | 59 | fn update_modifiers( 60 | &mut self, 61 | _conn: &Connection, 62 | _qh: &QueueHandle, 63 | _keyboard: &WlKeyboard, 64 | _serial: u32, 65 | modifiers: sctk::seat::keyboard::Modifiers, 66 | ) { 67 | self.key_modifiers = modifiers; 68 | } 69 | 70 | fn release_key( 71 | &mut self, 72 | _conn: &Connection, 73 | _qh: &QueueHandle, 74 | _keyboard: &WlKeyboard, 75 | _serial: u32, 76 | _event: sctk::seat::keyboard::KeyEvent, 77 | ) { 78 | } 79 | 80 | fn enter( 81 | &mut self, 82 | _conn: &Connection, 83 | _qh: &QueueHandle, 84 | _keyboard: &WlKeyboard, 85 | _surface: &WlSurface, 86 | _serial: u32, 87 | _raw: &[u32], 88 | _keysyms: &[sctk::seat::keyboard::Keysym], 89 | ) { 90 | } 91 | 92 | fn leave( 93 | &mut self, 94 | _conn: &Connection, 95 | _qh: &QueueHandle, 96 | _keyboard: &WlKeyboard, 97 | _surface: &WlSurface, 98 | _serial: u32, 99 | ) { 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/window/layer_shell.rs: -------------------------------------------------------------------------------- 1 | use sctk::shell::wlr_layer::{ 2 | KeyboardInteractivity, LayerShellHandler, LayerSurface, LayerSurfaceConfigure, 3 | }; 4 | 5 | use super::Window; 6 | 7 | impl LayerShellHandler for Window { 8 | fn closed( 9 | &mut self, 10 | _conn: &sctk::reexports::client::Connection, 11 | _qh: &sctk::reexports::client::QueueHandle, 12 | _layer: &LayerSurface, 13 | ) { 14 | self.exit = true; 15 | } 16 | 17 | fn configure( 18 | &mut self, 19 | _conn: &sctk::reexports::client::Connection, 20 | qh: &sctk::reexports::client::QueueHandle, 21 | layer: &LayerSurface, 22 | configure: LayerSurfaceConfigure, 23 | _serial: u32, 24 | ) { 25 | let (w, h) = configure.new_size; 26 | self.width = w; 27 | self.height = h; 28 | layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive); 29 | 30 | if !self.configured_surface { 31 | self.configured_surface = true; 32 | self.draw(qh); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/window/output.rs: -------------------------------------------------------------------------------- 1 | use sctk::{ 2 | output::{OutputHandler, OutputState}, 3 | reexports::client::*, 4 | }; 5 | 6 | use super::Window; 7 | 8 | impl OutputHandler for Window { 9 | fn output_state(&mut self) -> &mut OutputState { 10 | &mut self.output_state 11 | } 12 | 13 | fn new_output( 14 | &mut self, 15 | _: &Connection, 16 | _: &QueueHandle, 17 | _: protocol::wl_output::WlOutput, 18 | ) { 19 | } 20 | 21 | fn update_output( 22 | &mut self, 23 | _: &Connection, 24 | _: &QueueHandle, 25 | _: protocol::wl_output::WlOutput, 26 | ) { 27 | } 28 | 29 | fn output_destroyed( 30 | &mut self, 31 | _: &Connection, 32 | _: &QueueHandle, 33 | _: protocol::wl_output::WlOutput, 34 | ) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/window/pointer.rs: -------------------------------------------------------------------------------- 1 | use sctk::reexports::client::protocol::wl_pointer::AxisSource; 2 | use sctk::reexports::client::{protocol, Connection, QueueHandle}; 3 | use sctk::seat::keyboard::Modifiers; 4 | use sctk::seat::pointer::{PointerEvent, PointerEventKind, PointerHandler, *}; 5 | 6 | use super::Window; 7 | 8 | // According to https://wayland.freedesktop.org/libinput/doc/1.19.0/wheel-api.html 9 | // wheel typically has this angle per step. 10 | // This actually should be configured and auto-detected (from udev probably?) but 11 | // for now it should work for most cases and could be tuned via config. 12 | const SCROLL_PER_STEP: f64 = 15.0; 13 | 14 | pub struct Params { 15 | pub launch_on_middle: bool, 16 | pub wheel_scroll_multiplier: f64, 17 | } 18 | 19 | impl PointerHandler for Window { 20 | fn pointer_frame( 21 | &mut self, 22 | _conn: &Connection, 23 | _qh: &QueueHandle, 24 | _pointer: &protocol::wl_pointer::WlPointer, 25 | events: &[PointerEvent], 26 | ) { 27 | let mut changed = false; 28 | let config = self.config.param::(); 29 | 30 | for event in events { 31 | // Ignore events for other surfaces 32 | if event.surface != *self.surface { 33 | continue; 34 | } 35 | 36 | match event.kind { 37 | // TODO: implement precise clicks on items 38 | // PointerEventKind::Release { 39 | // button: BTN_LEFT, .. 40 | // } => .., 41 | PointerEventKind::Release { 42 | button: BTN_MIDDLE, .. 43 | } if config.launch_on_middle => { 44 | let with_fork = matches!(self.key_modifiers, Modifiers { ctrl: true, .. }); 45 | if let Err(err) = self.state.eval_input(with_fork) { 46 | self.error = Some(err); 47 | } 48 | } 49 | PointerEventKind::Release { 50 | button: BTN_RIGHT, .. 51 | } => self.exit = true, 52 | PointerEventKind::Release { 53 | button: BTN_BACK, .. 54 | } => self.state.prev_subitem(), 55 | PointerEventKind::Release { 56 | button: BTN_FORWARD, 57 | .. 58 | } => self.state.next_subitem(), 59 | PointerEventKind::Axis { 60 | vertical: 61 | AxisScroll { 62 | absolute, 63 | discrete: _, 64 | // XXX: handle this one? 65 | stop: _, 66 | }, 67 | source: 68 | Some(AxisSource::Wheel) 69 | | Some(AxisSource::Finger) 70 | | Some(AxisSource::Continuous), 71 | time: _, 72 | horizontal: _, 73 | } => { 74 | self.wheel_scroll_pending += absolute; 75 | } 76 | PointerEventKind::Enter { .. } 77 | | PointerEventKind::Leave { .. } 78 | | PointerEventKind::Motion { .. } 79 | | PointerEventKind::Press { .. } 80 | | PointerEventKind::Release { .. } 81 | | PointerEventKind::Axis { .. } => continue, 82 | } 83 | changed = true; 84 | } 85 | 86 | if changed { 87 | let scroll_per_step = SCROLL_PER_STEP 88 | * if config.wheel_scroll_multiplier > 0.0 { 89 | config.wheel_scroll_multiplier 90 | } else { 91 | 1.0 92 | }; 93 | let wheel_steps = (self.wheel_scroll_pending / scroll_per_step) as i32; 94 | if wheel_steps != 0 { 95 | self.wheel_scroll_pending -= f64::from(wheel_steps) * scroll_per_step; 96 | } 97 | let is_wheel_down = wheel_steps > 0; 98 | for _ in 0..wheel_steps.abs() { 99 | if is_wheel_down { 100 | self.state.next_item(); 101 | } else { 102 | self.state.prev_item(); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/window/registry.rs: -------------------------------------------------------------------------------- 1 | use sctk::{ 2 | output::OutputState, 3 | registry::{ProvidesRegistryState, RegistryState}, 4 | registry_handlers, 5 | seat::SeatState, 6 | }; 7 | 8 | use super::Window; 9 | 10 | impl ProvidesRegistryState for Window { 11 | fn registry(&mut self) -> &mut RegistryState { 12 | &mut self.registry_state 13 | } 14 | 15 | registry_handlers![OutputState, SeatState]; 16 | } 17 | -------------------------------------------------------------------------------- /src/window/seat.rs: -------------------------------------------------------------------------------- 1 | use sctk::{ 2 | reexports::client::{protocol::wl_seat::WlSeat, *}, 3 | seat::{Capability, SeatHandler, SeatState}, 4 | }; 5 | 6 | use super::Window; 7 | 8 | impl SeatHandler for Window { 9 | fn seat_state(&mut self) -> &mut SeatState { 10 | &mut self.seat_state 11 | } 12 | 13 | fn new_capability( 14 | &mut self, 15 | _conn: &Connection, 16 | qh: &QueueHandle, 17 | seat: WlSeat, 18 | capability: Capability, 19 | ) { 20 | match capability { 21 | Capability::Keyboard if self.input.keyboard.is_none() => { 22 | let wl_keyboard = match self.seat_state.get_keyboard_with_repeat( 23 | qh, 24 | &seat, 25 | None, 26 | self.loop_handle.clone(), 27 | Box::new(|_state, _wl_kbd, _event| {}), 28 | ) { 29 | Ok(k) => k, 30 | Err(err) => { 31 | self.error = Some(err.into()); 32 | return; 33 | } 34 | }; 35 | self.input.keyboard = Some(wl_keyboard); 36 | } 37 | Capability::Pointer if self.input.pointer.is_none() => { 38 | if let Ok(p) = self.seat_state.get_pointer(qh, &seat) { 39 | self.input.pointer = Some(p); 40 | } 41 | } 42 | _ => {} 43 | } 44 | } 45 | 46 | fn remove_capability( 47 | &mut self, 48 | _conn: &Connection, 49 | _qh: &QueueHandle, 50 | _seat: WlSeat, 51 | capability: Capability, 52 | ) { 53 | if let Capability::Keyboard = capability { 54 | if let Some(k) = self.input.keyboard.take() { 55 | k.release(); 56 | } 57 | } 58 | } 59 | 60 | fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) {} 61 | 62 | fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle, _seat: WlSeat) {} 63 | } 64 | -------------------------------------------------------------------------------- /src/window/shm.rs: -------------------------------------------------------------------------------- 1 | use sctk::shm::{Shm, ShmHandler}; 2 | 3 | use super::Window; 4 | 5 | impl ShmHandler for Window { 6 | fn shm_state(&mut self) -> &mut Shm { 7 | &mut self.shm 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/window/xdg_window.rs: -------------------------------------------------------------------------------- 1 | use sctk::{ 2 | reexports::client::*, 3 | shell::xdg::window::{self, WindowConfigure, WindowHandler}, 4 | }; 5 | 6 | use super::Window; 7 | 8 | impl WindowHandler for Window { 9 | fn request_close( 10 | &mut self, 11 | _conn: &Connection, 12 | _qh: &QueueHandle, 13 | _window: &window::Window, 14 | ) { 15 | self.exit = true; 16 | } 17 | 18 | fn configure( 19 | &mut self, 20 | _conn: &Connection, 21 | qh: &QueueHandle, 22 | window: &window::Window, 23 | configure: WindowConfigure, 24 | _serial: u32, 25 | ) { 26 | let (w, h) = configure.new_size; 27 | self.width = w.map(|w| w.get()).unwrap_or(self.width); 28 | self.height = h.map(|h| h.get()).unwrap_or(self.height); 29 | 30 | window.set_title(crate::prog_name!().to_owned()); 31 | window.unset_fullscreen(); 32 | 33 | if !self.configured_surface { 34 | self.configured_surface = true; 35 | self.draw(qh); 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------