├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── cnx-bin ├── Cargo.toml └── src │ └── main.rs ├── cnx-contrib ├── Cargo.toml └── src │ ├── lib.rs │ └── widgets │ ├── battery.rs │ ├── battery │ ├── battery_bsd.rs │ └── battery_linux.rs │ ├── command.rs │ ├── cpu.rs │ ├── disk_usage.rs │ ├── leftwm.rs │ ├── mod.rs │ ├── sensors.rs │ ├── sensors │ ├── sensors_bsd.rs │ └── sensors_linux.rs │ ├── volume.rs │ ├── volume │ ├── volume_bsd.rs │ └── volume_linux.rs │ ├── weather.rs │ └── wireless.rs ├── cnx ├── Cargo.toml └── src │ ├── bar.rs │ ├── lib.rs │ ├── text.rs │ ├── widgets │ ├── active_window_title.rs │ ├── clock.rs │ ├── mod.rs │ └── pager.rs │ └── xcb.rs ├── rust-toolchain ├── screenshot.png └── shell.nix /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | install_stable: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: 1.67.0 20 | override: true 21 | - uses: Swatinem/rust-cache@v1 22 | - name: Test 23 | run: | 24 | set -e 25 | sudo apt install libx11-xcb-dev libxcb-ewmh-dev libasound2-dev \ 26 | libpango1.0-dev libcairo2-dev libiw-dev 27 | cargo build 28 | make setup 29 | make check 30 | shell: bash 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .envrc 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | * Add the leftwm widget to cnx-contrib 4 | * Add ability to specify bar offset and width 5 | 6 | # v0.3.1 7 | 8 | * Add ability to define colors for widgets' attributes with RGB values or Hex color codes. 9 | * Extend pager widget to mark hidden desktops that contain windows. 10 | * Fix calculation of cpu usage. 11 | * Add widget for commands. Shows the output of configured cli commands. 12 | 13 | # v0.3.0 14 | 15 | * Update to tokio 1.2.0 (from 0.2) 16 | * Use tokio-stream package for streams. 17 | * Add widget for cpu which shows cpu consumption (Code directly ported from xmobar) 18 | * Add widget for wireless card. Shows your WiFi strength. 19 | * Modify existing widgets to support custom template 20 | * Use Pango's markup syntax to colorize and make it pretty 21 | * Add widget for weather. Uses weathernoaa package to achieve the same. 22 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "alsa" 16 | version = "0.5.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" 19 | dependencies = [ 20 | "alsa-sys", 21 | "bitflags", 22 | "libc", 23 | "nix 0.20.0", 24 | ] 25 | 26 | [[package]] 27 | name = "alsa-sys" 28 | version = "0.3.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 31 | dependencies = [ 32 | "libc", 33 | "pkg-config", 34 | ] 35 | 36 | [[package]] 37 | name = "ansi_term" 38 | version = "0.11.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 41 | dependencies = [ 42 | "winapi", 43 | ] 44 | 45 | [[package]] 46 | name = "anyhow" 47 | version = "1.0.41" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" 50 | 51 | [[package]] 52 | name = "arrayvec" 53 | version = "0.5.2" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 56 | 57 | [[package]] 58 | name = "async-stream" 59 | version = "0.3.3" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" 62 | dependencies = [ 63 | "async-stream-impl", 64 | "futures-core", 65 | ] 66 | 67 | [[package]] 68 | name = "async-stream-impl" 69 | version = "0.3.3" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" 72 | dependencies = [ 73 | "proc-macro2", 74 | "quote", 75 | "syn", 76 | ] 77 | 78 | [[package]] 79 | name = "async-trait" 80 | version = "0.1.64" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" 83 | dependencies = [ 84 | "proc-macro2", 85 | "quote", 86 | "syn", 87 | ] 88 | 89 | [[package]] 90 | name = "atty" 91 | version = "0.2.14" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 94 | dependencies = [ 95 | "hermit-abi", 96 | "libc", 97 | "winapi", 98 | ] 99 | 100 | [[package]] 101 | name = "autocfg" 102 | version = "1.0.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 105 | 106 | [[package]] 107 | name = "base64" 108 | version = "0.13.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 111 | 112 | [[package]] 113 | name = "bindgen" 114 | version = "0.53.3" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" 117 | dependencies = [ 118 | "bitflags", 119 | "cexpr", 120 | "cfg-if 0.1.10", 121 | "clang-sys", 122 | "clap", 123 | "env_logger", 124 | "lazy_static", 125 | "lazycell", 126 | "log", 127 | "peeking_take_while", 128 | "proc-macro2", 129 | "quote", 130 | "regex", 131 | "rustc-hash", 132 | "shlex", 133 | "which", 134 | ] 135 | 136 | [[package]] 137 | name = "bitflags" 138 | version = "1.2.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 141 | 142 | [[package]] 143 | name = "bitvec" 144 | version = "0.19.5" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" 147 | dependencies = [ 148 | "funty", 149 | "radium", 150 | "tap", 151 | "wyz", 152 | ] 153 | 154 | [[package]] 155 | name = "bumpalo" 156 | version = "3.6.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" 159 | 160 | [[package]] 161 | name = "byte-unit" 162 | version = "4.0.12" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "063197e6eb4b775b64160dedde7a0986bb2836cce140e9492e9e96f28e18bcd8" 165 | dependencies = [ 166 | "utf8-width", 167 | ] 168 | 169 | [[package]] 170 | name = "bytes" 171 | version = "1.0.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" 174 | 175 | [[package]] 176 | name = "cairo-rs" 177 | version = "0.16.7" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" 180 | dependencies = [ 181 | "bitflags", 182 | "cairo-sys-rs", 183 | "glib", 184 | "libc", 185 | "once_cell", 186 | "thiserror", 187 | ] 188 | 189 | [[package]] 190 | name = "cairo-sys-rs" 191 | version = "0.16.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" 194 | dependencies = [ 195 | "glib-sys", 196 | "libc", 197 | "system-deps", 198 | ] 199 | 200 | [[package]] 201 | name = "cc" 202 | version = "1.0.66" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" 205 | 206 | [[package]] 207 | name = "cexpr" 208 | version = "0.4.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" 211 | dependencies = [ 212 | "nom 5.1.2", 213 | ] 214 | 215 | [[package]] 216 | name = "cfg-expr" 217 | version = "0.11.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" 220 | dependencies = [ 221 | "smallvec", 222 | ] 223 | 224 | [[package]] 225 | name = "cfg-if" 226 | version = "0.1.10" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 229 | 230 | [[package]] 231 | name = "cfg-if" 232 | version = "1.0.0" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 235 | 236 | [[package]] 237 | name = "chrono" 238 | version = "0.4.19" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 241 | dependencies = [ 242 | "libc", 243 | "num-integer", 244 | "num-traits", 245 | "time", 246 | "winapi", 247 | ] 248 | 249 | [[package]] 250 | name = "clang-sys" 251 | version = "0.29.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" 254 | dependencies = [ 255 | "glob", 256 | "libc", 257 | "libloading", 258 | ] 259 | 260 | [[package]] 261 | name = "clap" 262 | version = "2.33.3" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 265 | dependencies = [ 266 | "ansi_term", 267 | "atty", 268 | "bitflags", 269 | "strsim", 270 | "textwrap", 271 | "unicode-width", 272 | "vec_map", 273 | ] 274 | 275 | [[package]] 276 | name = "cnx" 277 | version = "0.3.0" 278 | dependencies = [ 279 | "anyhow", 280 | "async-stream", 281 | "cairo-rs", 282 | "cairo-sys-rs", 283 | "chrono", 284 | "colors-transform", 285 | "futures", 286 | "lazy_static", 287 | "ordered-float", 288 | "pango", 289 | "pangocairo", 290 | "tokio", 291 | "tokio-stream", 292 | "xcb", 293 | "xcb-util", 294 | ] 295 | 296 | [[package]] 297 | name = "cnx-bin" 298 | version = "0.1.0" 299 | dependencies = [ 300 | "anyhow", 301 | "byte-unit", 302 | "cnx", 303 | "cnx-contrib", 304 | "weathernoaa", 305 | ] 306 | 307 | [[package]] 308 | name = "cnx-contrib" 309 | version = "0.1.0" 310 | dependencies = [ 311 | "alsa", 312 | "anyhow", 313 | "async-stream", 314 | "byte-unit", 315 | "cnx", 316 | "iwlib", 317 | "nix 0.20.0", 318 | "openssl", 319 | "process-stream", 320 | "regex", 321 | "reqwest", 322 | "serde", 323 | "serde_derive", 324 | "serde_json", 325 | "sioctl", 326 | "tokio", 327 | "tokio-stream", 328 | "weathernoaa", 329 | ] 330 | 331 | [[package]] 332 | name = "colors-transform" 333 | version = "0.2.11" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178" 336 | 337 | [[package]] 338 | name = "core-foundation" 339 | version = "0.9.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" 342 | dependencies = [ 343 | "core-foundation-sys", 344 | "libc", 345 | ] 346 | 347 | [[package]] 348 | name = "core-foundation-sys" 349 | version = "0.8.2" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" 352 | 353 | [[package]] 354 | name = "encoding_rs" 355 | version = "0.8.28" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" 358 | dependencies = [ 359 | "cfg-if 1.0.0", 360 | ] 361 | 362 | [[package]] 363 | name = "env_logger" 364 | version = "0.7.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 367 | dependencies = [ 368 | "atty", 369 | "humantime", 370 | "log", 371 | "regex", 372 | "termcolor", 373 | ] 374 | 375 | [[package]] 376 | name = "fnv" 377 | version = "1.0.7" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 380 | 381 | [[package]] 382 | name = "foreign-types" 383 | version = "0.3.2" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 386 | dependencies = [ 387 | "foreign-types-shared", 388 | ] 389 | 390 | [[package]] 391 | name = "foreign-types-shared" 392 | version = "0.1.1" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 395 | 396 | [[package]] 397 | name = "form_urlencoded" 398 | version = "1.0.1" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 401 | dependencies = [ 402 | "matches", 403 | "percent-encoding", 404 | ] 405 | 406 | [[package]] 407 | name = "funty" 408 | version = "1.1.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" 411 | 412 | [[package]] 413 | name = "futures" 414 | version = "0.3.26" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" 417 | dependencies = [ 418 | "futures-channel", 419 | "futures-core", 420 | "futures-executor", 421 | "futures-io", 422 | "futures-sink", 423 | "futures-task", 424 | "futures-util", 425 | ] 426 | 427 | [[package]] 428 | name = "futures-channel" 429 | version = "0.3.26" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" 432 | dependencies = [ 433 | "futures-core", 434 | "futures-sink", 435 | ] 436 | 437 | [[package]] 438 | name = "futures-core" 439 | version = "0.3.26" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" 442 | 443 | [[package]] 444 | name = "futures-executor" 445 | version = "0.3.26" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" 448 | dependencies = [ 449 | "futures-core", 450 | "futures-task", 451 | "futures-util", 452 | ] 453 | 454 | [[package]] 455 | name = "futures-io" 456 | version = "0.3.26" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" 459 | 460 | [[package]] 461 | name = "futures-macro" 462 | version = "0.3.26" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" 465 | dependencies = [ 466 | "proc-macro2", 467 | "quote", 468 | "syn", 469 | ] 470 | 471 | [[package]] 472 | name = "futures-sink" 473 | version = "0.3.26" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" 476 | 477 | [[package]] 478 | name = "futures-task" 479 | version = "0.3.26" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" 482 | 483 | [[package]] 484 | name = "futures-util" 485 | version = "0.3.26" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" 488 | dependencies = [ 489 | "futures-channel", 490 | "futures-core", 491 | "futures-io", 492 | "futures-macro", 493 | "futures-sink", 494 | "futures-task", 495 | "memchr", 496 | "pin-project-lite", 497 | "pin-utils", 498 | "slab", 499 | ] 500 | 501 | [[package]] 502 | name = "getrandom" 503 | version = "0.2.2" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" 506 | dependencies = [ 507 | "cfg-if 1.0.0", 508 | "libc", 509 | "wasi 0.10.0+wasi-snapshot-preview1", 510 | ] 511 | 512 | [[package]] 513 | name = "gio" 514 | version = "0.16.7" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" 517 | dependencies = [ 518 | "bitflags", 519 | "futures-channel", 520 | "futures-core", 521 | "futures-io", 522 | "futures-util", 523 | "gio-sys", 524 | "glib", 525 | "libc", 526 | "once_cell", 527 | "pin-project-lite", 528 | "smallvec", 529 | "thiserror", 530 | ] 531 | 532 | [[package]] 533 | name = "gio-sys" 534 | version = "0.16.3" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" 537 | dependencies = [ 538 | "glib-sys", 539 | "gobject-sys", 540 | "libc", 541 | "system-deps", 542 | "winapi", 543 | ] 544 | 545 | [[package]] 546 | name = "glib" 547 | version = "0.16.7" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f" 550 | dependencies = [ 551 | "bitflags", 552 | "futures-channel", 553 | "futures-core", 554 | "futures-executor", 555 | "futures-task", 556 | "futures-util", 557 | "gio-sys", 558 | "glib-macros", 559 | "glib-sys", 560 | "gobject-sys", 561 | "libc", 562 | "once_cell", 563 | "smallvec", 564 | "thiserror", 565 | ] 566 | 567 | [[package]] 568 | name = "glib-macros" 569 | version = "0.16.3" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" 572 | dependencies = [ 573 | "anyhow", 574 | "heck", 575 | "proc-macro-crate", 576 | "proc-macro-error", 577 | "proc-macro2", 578 | "quote", 579 | "syn", 580 | ] 581 | 582 | [[package]] 583 | name = "glib-sys" 584 | version = "0.16.3" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" 587 | dependencies = [ 588 | "libc", 589 | "system-deps", 590 | ] 591 | 592 | [[package]] 593 | name = "glob" 594 | version = "0.3.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 597 | 598 | [[package]] 599 | name = "gobject-sys" 600 | version = "0.16.3" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" 603 | dependencies = [ 604 | "glib-sys", 605 | "libc", 606 | "system-deps", 607 | ] 608 | 609 | [[package]] 610 | name = "h2" 611 | version = "0.3.1" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "d832b01df74254fe364568d6ddc294443f61cbec82816b60904303af87efae78" 614 | dependencies = [ 615 | "bytes", 616 | "fnv", 617 | "futures-core", 618 | "futures-sink", 619 | "futures-util", 620 | "http", 621 | "indexmap", 622 | "slab", 623 | "tokio", 624 | "tokio-util", 625 | "tracing", 626 | ] 627 | 628 | [[package]] 629 | name = "hashbrown" 630 | version = "0.9.1" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 633 | 634 | [[package]] 635 | name = "heck" 636 | version = "0.4.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 639 | 640 | [[package]] 641 | name = "hermit-abi" 642 | version = "0.1.18" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 645 | dependencies = [ 646 | "libc", 647 | ] 648 | 649 | [[package]] 650 | name = "http" 651 | version = "0.2.3" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" 654 | dependencies = [ 655 | "bytes", 656 | "fnv", 657 | "itoa 0.4.7", 658 | ] 659 | 660 | [[package]] 661 | name = "http-body" 662 | version = "0.4.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" 665 | dependencies = [ 666 | "bytes", 667 | "http", 668 | ] 669 | 670 | [[package]] 671 | name = "httparse" 672 | version = "1.3.5" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" 675 | 676 | [[package]] 677 | name = "httpdate" 678 | version = "0.3.2" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" 681 | 682 | [[package]] 683 | name = "humantime" 684 | version = "1.3.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 687 | dependencies = [ 688 | "quick-error", 689 | ] 690 | 691 | [[package]] 692 | name = "hyper" 693 | version = "0.14.4" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "e8e946c2b1349055e0b72ae281b238baf1a3ea7307c7e9f9d64673bdd9c26ac7" 696 | dependencies = [ 697 | "bytes", 698 | "futures-channel", 699 | "futures-core", 700 | "futures-util", 701 | "h2", 702 | "http", 703 | "http-body", 704 | "httparse", 705 | "httpdate", 706 | "itoa 0.4.7", 707 | "pin-project", 708 | "socket2 0.3.19", 709 | "tokio", 710 | "tower-service", 711 | "tracing", 712 | "want", 713 | ] 714 | 715 | [[package]] 716 | name = "hyper-tls" 717 | version = "0.5.0" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 720 | dependencies = [ 721 | "bytes", 722 | "hyper", 723 | "native-tls", 724 | "tokio", 725 | "tokio-native-tls", 726 | ] 727 | 728 | [[package]] 729 | name = "idna" 730 | version = "0.2.2" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" 733 | dependencies = [ 734 | "matches", 735 | "unicode-bidi", 736 | "unicode-normalization", 737 | ] 738 | 739 | [[package]] 740 | name = "indexmap" 741 | version = "1.6.2" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" 744 | dependencies = [ 745 | "autocfg", 746 | "hashbrown", 747 | ] 748 | 749 | [[package]] 750 | name = "ipnet" 751 | version = "2.3.0" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" 754 | 755 | [[package]] 756 | name = "itoa" 757 | version = "0.4.7" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 760 | 761 | [[package]] 762 | name = "itoa" 763 | version = "1.0.5" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 766 | 767 | [[package]] 768 | name = "iwlib" 769 | version = "0.1.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "7fb07046a0d1f1c1b957abe598afc42e3a2d4e6f723580d56bbe9aaf142505fd" 772 | dependencies = [ 773 | "bindgen", 774 | "iwlib_sys", 775 | "libc", 776 | ] 777 | 778 | [[package]] 779 | name = "iwlib_sys" 780 | version = "0.1.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "c672ccb01a169fda0c38cad1865d47c07ed0ce62339ecb245cc9ab425093f9c1" 783 | dependencies = [ 784 | "bindgen", 785 | ] 786 | 787 | [[package]] 788 | name = "js-sys" 789 | version = "0.3.48" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78" 792 | dependencies = [ 793 | "wasm-bindgen", 794 | ] 795 | 796 | [[package]] 797 | name = "lazy_static" 798 | version = "1.4.0" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 801 | 802 | [[package]] 803 | name = "lazycell" 804 | version = "1.3.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 807 | 808 | [[package]] 809 | name = "lexical-core" 810 | version = "0.7.6" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" 813 | dependencies = [ 814 | "arrayvec", 815 | "bitflags", 816 | "cfg-if 1.0.0", 817 | "ryu", 818 | "static_assertions", 819 | ] 820 | 821 | [[package]] 822 | name = "libc" 823 | version = "0.2.139" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 826 | 827 | [[package]] 828 | name = "libloading" 829 | version = "0.5.2" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" 832 | dependencies = [ 833 | "cc", 834 | "winapi", 835 | ] 836 | 837 | [[package]] 838 | name = "lock_api" 839 | version = "0.4.6" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" 842 | dependencies = [ 843 | "scopeguard", 844 | ] 845 | 846 | [[package]] 847 | name = "log" 848 | version = "0.4.14" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 851 | dependencies = [ 852 | "cfg-if 1.0.0", 853 | ] 854 | 855 | [[package]] 856 | name = "matches" 857 | version = "0.1.8" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 860 | 861 | [[package]] 862 | name = "memchr" 863 | version = "2.4.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 866 | 867 | [[package]] 868 | name = "mime" 869 | version = "0.3.16" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 872 | 873 | [[package]] 874 | name = "mio" 875 | version = "0.8.5" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 878 | dependencies = [ 879 | "libc", 880 | "log", 881 | "wasi 0.11.0+wasi-snapshot-preview1", 882 | "windows-sys 0.42.0", 883 | ] 884 | 885 | [[package]] 886 | name = "native-tls" 887 | version = "0.2.7" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" 890 | dependencies = [ 891 | "lazy_static", 892 | "libc", 893 | "log", 894 | "openssl", 895 | "openssl-probe", 896 | "openssl-sys", 897 | "schannel", 898 | "security-framework", 899 | "security-framework-sys", 900 | "tempfile", 901 | ] 902 | 903 | [[package]] 904 | name = "nix" 905 | version = "0.17.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" 908 | dependencies = [ 909 | "bitflags", 910 | "cc", 911 | "cfg-if 0.1.10", 912 | "libc", 913 | "void", 914 | ] 915 | 916 | [[package]] 917 | name = "nix" 918 | version = "0.20.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" 921 | dependencies = [ 922 | "bitflags", 923 | "cc", 924 | "cfg-if 1.0.0", 925 | "libc", 926 | ] 927 | 928 | [[package]] 929 | name = "nom" 930 | version = "5.1.2" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" 933 | dependencies = [ 934 | "memchr", 935 | "version_check", 936 | ] 937 | 938 | [[package]] 939 | name = "nom" 940 | version = "6.1.2" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" 943 | dependencies = [ 944 | "bitvec", 945 | "funty", 946 | "lexical-core", 947 | "memchr", 948 | "version_check", 949 | ] 950 | 951 | [[package]] 952 | name = "num-integer" 953 | version = "0.1.44" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 956 | dependencies = [ 957 | "autocfg", 958 | "num-traits", 959 | ] 960 | 961 | [[package]] 962 | name = "num-traits" 963 | version = "0.2.14" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 966 | dependencies = [ 967 | "autocfg", 968 | ] 969 | 970 | [[package]] 971 | name = "num_cpus" 972 | version = "1.13.0" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 975 | dependencies = [ 976 | "hermit-abi", 977 | "libc", 978 | ] 979 | 980 | [[package]] 981 | name = "once_cell" 982 | version = "1.17.0" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 985 | 986 | [[package]] 987 | name = "openssl" 988 | version = "0.10.32" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" 991 | dependencies = [ 992 | "bitflags", 993 | "cfg-if 1.0.0", 994 | "foreign-types", 995 | "lazy_static", 996 | "libc", 997 | "openssl-sys", 998 | ] 999 | 1000 | [[package]] 1001 | name = "openssl-probe" 1002 | version = "0.1.2" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 1005 | 1006 | [[package]] 1007 | name = "openssl-src" 1008 | version = "111.22.0+1.1.1q" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853" 1011 | dependencies = [ 1012 | "cc", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "openssl-sys" 1017 | version = "0.9.60" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" 1020 | dependencies = [ 1021 | "autocfg", 1022 | "cc", 1023 | "libc", 1024 | "openssl-src", 1025 | "pkg-config", 1026 | "vcpkg", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "ordered-float" 1031 | version = "1.1.1" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" 1034 | dependencies = [ 1035 | "num-traits", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "pango" 1040 | version = "0.16.5" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" 1043 | dependencies = [ 1044 | "bitflags", 1045 | "gio", 1046 | "glib", 1047 | "libc", 1048 | "once_cell", 1049 | "pango-sys", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "pango-sys" 1054 | version = "0.16.3" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" 1057 | dependencies = [ 1058 | "glib-sys", 1059 | "gobject-sys", 1060 | "libc", 1061 | "system-deps", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "pangocairo" 1066 | version = "0.16.3" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "16ad2ec87789371b551fd2367c10aa37060412ffd3e60abd99491b21b93a3f9b" 1069 | dependencies = [ 1070 | "bitflags", 1071 | "cairo-rs", 1072 | "glib", 1073 | "libc", 1074 | "pango", 1075 | "pangocairo-sys", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "pangocairo-sys" 1080 | version = "0.16.3" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "848d2df9b7f1a8c7a19d994de443bcbe5d4382610ccb8e64247f932be74fcf76" 1083 | dependencies = [ 1084 | "cairo-sys-rs", 1085 | "glib-sys", 1086 | "libc", 1087 | "pango-sys", 1088 | "system-deps", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "parking_lot" 1093 | version = "0.12.1" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1096 | dependencies = [ 1097 | "lock_api", 1098 | "parking_lot_core", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "parking_lot_core" 1103 | version = "0.9.7" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 1106 | dependencies = [ 1107 | "cfg-if 1.0.0", 1108 | "libc", 1109 | "redox_syscall", 1110 | "smallvec", 1111 | "windows-sys 0.45.0", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "peeking_take_while" 1116 | version = "0.1.2" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 1119 | 1120 | [[package]] 1121 | name = "percent-encoding" 1122 | version = "2.1.0" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 1125 | 1126 | [[package]] 1127 | name = "pin-project" 1128 | version = "1.0.5" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" 1131 | dependencies = [ 1132 | "pin-project-internal", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "pin-project-internal" 1137 | version = "1.0.5" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" 1140 | dependencies = [ 1141 | "proc-macro2", 1142 | "quote", 1143 | "syn", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "pin-project-lite" 1148 | version = "0.2.9" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 1151 | 1152 | [[package]] 1153 | name = "pin-utils" 1154 | version = "0.1.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1157 | 1158 | [[package]] 1159 | name = "pkg-config" 1160 | version = "0.3.19" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 1163 | 1164 | [[package]] 1165 | name = "ppv-lite86" 1166 | version = "0.2.10" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 1169 | 1170 | [[package]] 1171 | name = "proc-macro-crate" 1172 | version = "1.2.1" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" 1175 | dependencies = [ 1176 | "once_cell", 1177 | "thiserror", 1178 | "toml", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "proc-macro-error" 1183 | version = "1.0.4" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1186 | dependencies = [ 1187 | "proc-macro-error-attr", 1188 | "proc-macro2", 1189 | "quote", 1190 | "syn", 1191 | "version_check", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "proc-macro-error-attr" 1196 | version = "1.0.4" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1199 | dependencies = [ 1200 | "proc-macro2", 1201 | "quote", 1202 | "version_check", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "proc-macro2" 1207 | version = "1.0.50" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" 1210 | dependencies = [ 1211 | "unicode-ident", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "process-stream" 1216 | version = "0.4.1" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "3393bf8403a4ca728aafcc4b7fd899c1db520c47d9273f2ab7d69e86d67c3390" 1219 | dependencies = [ 1220 | "async-stream", 1221 | "async-trait", 1222 | "futures", 1223 | "tap", 1224 | "tokio", 1225 | "tokio-stream", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "quick-error" 1230 | version = "1.2.3" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1233 | 1234 | [[package]] 1235 | name = "quote" 1236 | version = "1.0.8" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" 1239 | dependencies = [ 1240 | "proc-macro2", 1241 | ] 1242 | 1243 | [[package]] 1244 | name = "radium" 1245 | version = "0.5.3" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" 1248 | 1249 | [[package]] 1250 | name = "rand" 1251 | version = "0.8.3" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" 1254 | dependencies = [ 1255 | "libc", 1256 | "rand_chacha", 1257 | "rand_core", 1258 | "rand_hc", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "rand_chacha" 1263 | version = "0.3.0" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" 1266 | dependencies = [ 1267 | "ppv-lite86", 1268 | "rand_core", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "rand_core" 1273 | version = "0.6.2" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" 1276 | dependencies = [ 1277 | "getrandom", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "rand_hc" 1282 | version = "0.3.0" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 1285 | dependencies = [ 1286 | "rand_core", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "redox_syscall" 1291 | version = "0.2.16" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1294 | dependencies = [ 1295 | "bitflags", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "regex" 1300 | version = "1.5.4" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 1303 | dependencies = [ 1304 | "aho-corasick", 1305 | "memchr", 1306 | "regex-syntax", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "regex-syntax" 1311 | version = "0.6.25" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 1314 | 1315 | [[package]] 1316 | name = "remove_dir_all" 1317 | version = "0.5.3" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1320 | dependencies = [ 1321 | "winapi", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "reqwest" 1326 | version = "0.11.2" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "bf12057f289428dbf5c591c74bf10392e4a8003f993405a902f20117019022d4" 1329 | dependencies = [ 1330 | "base64", 1331 | "bytes", 1332 | "encoding_rs", 1333 | "futures-core", 1334 | "futures-util", 1335 | "http", 1336 | "http-body", 1337 | "hyper", 1338 | "hyper-tls", 1339 | "ipnet", 1340 | "js-sys", 1341 | "lazy_static", 1342 | "log", 1343 | "mime", 1344 | "native-tls", 1345 | "percent-encoding", 1346 | "pin-project-lite", 1347 | "serde", 1348 | "serde_urlencoded", 1349 | "tokio", 1350 | "tokio-native-tls", 1351 | "url", 1352 | "wasm-bindgen", 1353 | "wasm-bindgen-futures", 1354 | "web-sys", 1355 | "winreg", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "rustc-hash" 1360 | version = "1.1.0" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1363 | 1364 | [[package]] 1365 | name = "ryu" 1366 | version = "1.0.5" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1369 | 1370 | [[package]] 1371 | name = "schannel" 1372 | version = "0.1.19" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 1375 | dependencies = [ 1376 | "lazy_static", 1377 | "winapi", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "scopeguard" 1382 | version = "1.1.0" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1385 | 1386 | [[package]] 1387 | name = "security-framework" 1388 | version = "2.1.2" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" 1391 | dependencies = [ 1392 | "bitflags", 1393 | "core-foundation", 1394 | "core-foundation-sys", 1395 | "libc", 1396 | "security-framework-sys", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "security-framework-sys" 1401 | version = "2.1.1" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d" 1404 | dependencies = [ 1405 | "core-foundation-sys", 1406 | "libc", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "serde" 1411 | version = "1.0.152" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 1414 | 1415 | [[package]] 1416 | name = "serde_derive" 1417 | version = "1.0.152" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 1420 | dependencies = [ 1421 | "proc-macro2", 1422 | "quote", 1423 | "syn", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "serde_json" 1428 | version = "1.0.91" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 1431 | dependencies = [ 1432 | "itoa 1.0.5", 1433 | "ryu", 1434 | "serde", 1435 | ] 1436 | 1437 | [[package]] 1438 | name = "serde_urlencoded" 1439 | version = "0.7.0" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 1442 | dependencies = [ 1443 | "form_urlencoded", 1444 | "itoa 0.4.7", 1445 | "ryu", 1446 | "serde", 1447 | ] 1448 | 1449 | [[package]] 1450 | name = "shlex" 1451 | version = "0.1.1" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" 1454 | 1455 | [[package]] 1456 | name = "signal-hook-registry" 1457 | version = "1.3.0" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" 1460 | dependencies = [ 1461 | "libc", 1462 | ] 1463 | 1464 | [[package]] 1465 | name = "sioctl" 1466 | version = "0.0.1" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "efc3e89296142362fe4967e6a7cd19934d4f7be97a4c3bd95f6c362aba321122" 1469 | dependencies = [ 1470 | "libc", 1471 | "nix 0.17.0", 1472 | "sndio-sys", 1473 | ] 1474 | 1475 | [[package]] 1476 | name = "slab" 1477 | version = "0.4.2" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1480 | 1481 | [[package]] 1482 | name = "smallvec" 1483 | version = "1.10.0" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1486 | 1487 | [[package]] 1488 | name = "sndio-sys" 1489 | version = "0.0.1" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "0005ce55e70c5a0680a8f2f03d39f6185f7b17206bc784867657ebe938636f90" 1492 | dependencies = [ 1493 | "bindgen", 1494 | "libc", 1495 | ] 1496 | 1497 | [[package]] 1498 | name = "socket2" 1499 | version = "0.3.19" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 1502 | dependencies = [ 1503 | "cfg-if 1.0.0", 1504 | "libc", 1505 | "winapi", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "socket2" 1510 | version = "0.4.7" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1513 | dependencies = [ 1514 | "libc", 1515 | "winapi", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "static_assertions" 1520 | version = "1.1.0" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1523 | 1524 | [[package]] 1525 | name = "strsim" 1526 | version = "0.8.0" 1527 | source = "registry+https://github.com/rust-lang/crates.io-index" 1528 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1529 | 1530 | [[package]] 1531 | name = "syn" 1532 | version = "1.0.107" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 1535 | dependencies = [ 1536 | "proc-macro2", 1537 | "quote", 1538 | "unicode-ident", 1539 | ] 1540 | 1541 | [[package]] 1542 | name = "system-deps" 1543 | version = "6.0.3" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" 1546 | dependencies = [ 1547 | "cfg-expr", 1548 | "heck", 1549 | "pkg-config", 1550 | "toml", 1551 | "version-compare", 1552 | ] 1553 | 1554 | [[package]] 1555 | name = "tap" 1556 | version = "1.0.1" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1559 | 1560 | [[package]] 1561 | name = "tempfile" 1562 | version = "3.2.0" 1563 | source = "registry+https://github.com/rust-lang/crates.io-index" 1564 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 1565 | dependencies = [ 1566 | "cfg-if 1.0.0", 1567 | "libc", 1568 | "rand", 1569 | "redox_syscall", 1570 | "remove_dir_all", 1571 | "winapi", 1572 | ] 1573 | 1574 | [[package]] 1575 | name = "termcolor" 1576 | version = "1.1.2" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1579 | dependencies = [ 1580 | "winapi-util", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "textwrap" 1585 | version = "0.11.0" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1588 | dependencies = [ 1589 | "unicode-width", 1590 | ] 1591 | 1592 | [[package]] 1593 | name = "thiserror" 1594 | version = "1.0.24" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" 1597 | dependencies = [ 1598 | "thiserror-impl", 1599 | ] 1600 | 1601 | [[package]] 1602 | name = "thiserror-impl" 1603 | version = "1.0.24" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" 1606 | dependencies = [ 1607 | "proc-macro2", 1608 | "quote", 1609 | "syn", 1610 | ] 1611 | 1612 | [[package]] 1613 | name = "time" 1614 | version = "0.1.44" 1615 | source = "registry+https://github.com/rust-lang/crates.io-index" 1616 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 1617 | dependencies = [ 1618 | "libc", 1619 | "wasi 0.10.0+wasi-snapshot-preview1", 1620 | "winapi", 1621 | ] 1622 | 1623 | [[package]] 1624 | name = "tinyvec" 1625 | version = "1.1.1" 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" 1627 | checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" 1628 | dependencies = [ 1629 | "tinyvec_macros", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "tinyvec_macros" 1634 | version = "0.1.0" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1637 | 1638 | [[package]] 1639 | name = "tokio" 1640 | version = "1.19.2" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" 1643 | dependencies = [ 1644 | "bytes", 1645 | "libc", 1646 | "memchr", 1647 | "mio", 1648 | "num_cpus", 1649 | "once_cell", 1650 | "parking_lot", 1651 | "pin-project-lite", 1652 | "signal-hook-registry", 1653 | "socket2 0.4.7", 1654 | "tokio-macros", 1655 | "winapi", 1656 | ] 1657 | 1658 | [[package]] 1659 | name = "tokio-macros" 1660 | version = "1.8.2" 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" 1662 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 1663 | dependencies = [ 1664 | "proc-macro2", 1665 | "quote", 1666 | "syn", 1667 | ] 1668 | 1669 | [[package]] 1670 | name = "tokio-native-tls" 1671 | version = "0.3.0" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1674 | dependencies = [ 1675 | "native-tls", 1676 | "tokio", 1677 | ] 1678 | 1679 | [[package]] 1680 | name = "tokio-stream" 1681 | version = "0.1.11" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" 1684 | dependencies = [ 1685 | "futures-core", 1686 | "pin-project-lite", 1687 | "tokio", 1688 | ] 1689 | 1690 | [[package]] 1691 | name = "tokio-util" 1692 | version = "0.6.4" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "ec31e5cc6b46e653cf57762f36f71d5e6386391d88a72fd6db4508f8f676fb29" 1695 | dependencies = [ 1696 | "bytes", 1697 | "futures-core", 1698 | "futures-sink", 1699 | "log", 1700 | "pin-project-lite", 1701 | "tokio", 1702 | ] 1703 | 1704 | [[package]] 1705 | name = "toml" 1706 | version = "0.5.11" 1707 | source = "registry+https://github.com/rust-lang/crates.io-index" 1708 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 1709 | dependencies = [ 1710 | "serde", 1711 | ] 1712 | 1713 | [[package]] 1714 | name = "tower-service" 1715 | version = "0.3.1" 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" 1717 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 1718 | 1719 | [[package]] 1720 | name = "tracing" 1721 | version = "0.1.25" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" 1724 | dependencies = [ 1725 | "cfg-if 1.0.0", 1726 | "pin-project-lite", 1727 | "tracing-core", 1728 | ] 1729 | 1730 | [[package]] 1731 | name = "tracing-core" 1732 | version = "0.1.17" 1733 | source = "registry+https://github.com/rust-lang/crates.io-index" 1734 | checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" 1735 | dependencies = [ 1736 | "lazy_static", 1737 | ] 1738 | 1739 | [[package]] 1740 | name = "try-lock" 1741 | version = "0.2.3" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1744 | 1745 | [[package]] 1746 | name = "unicode-bidi" 1747 | version = "0.3.4" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 1750 | dependencies = [ 1751 | "matches", 1752 | ] 1753 | 1754 | [[package]] 1755 | name = "unicode-ident" 1756 | version = "1.0.6" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 1759 | 1760 | [[package]] 1761 | name = "unicode-normalization" 1762 | version = "0.1.17" 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" 1764 | checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" 1765 | dependencies = [ 1766 | "tinyvec", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "unicode-width" 1771 | version = "0.1.8" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 1774 | 1775 | [[package]] 1776 | name = "url" 1777 | version = "2.2.1" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" 1780 | dependencies = [ 1781 | "form_urlencoded", 1782 | "idna", 1783 | "matches", 1784 | "percent-encoding", 1785 | ] 1786 | 1787 | [[package]] 1788 | name = "utf8-width" 1789 | version = "0.1.5" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" 1792 | 1793 | [[package]] 1794 | name = "vcpkg" 1795 | version = "0.2.11" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" 1798 | 1799 | [[package]] 1800 | name = "vec_map" 1801 | version = "0.8.2" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1804 | 1805 | [[package]] 1806 | name = "version-compare" 1807 | version = "0.1.1" 1808 | source = "registry+https://github.com/rust-lang/crates.io-index" 1809 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 1810 | 1811 | [[package]] 1812 | name = "version_check" 1813 | version = "0.9.2" 1814 | source = "registry+https://github.com/rust-lang/crates.io-index" 1815 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 1816 | 1817 | [[package]] 1818 | name = "void" 1819 | version = "1.0.2" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 1822 | 1823 | [[package]] 1824 | name = "want" 1825 | version = "0.3.0" 1826 | source = "registry+https://github.com/rust-lang/crates.io-index" 1827 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1828 | dependencies = [ 1829 | "log", 1830 | "try-lock", 1831 | ] 1832 | 1833 | [[package]] 1834 | name = "wasi" 1835 | version = "0.10.0+wasi-snapshot-preview1" 1836 | source = "registry+https://github.com/rust-lang/crates.io-index" 1837 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1838 | 1839 | [[package]] 1840 | name = "wasi" 1841 | version = "0.11.0+wasi-snapshot-preview1" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1844 | 1845 | [[package]] 1846 | name = "wasm-bindgen" 1847 | version = "0.2.71" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7" 1850 | dependencies = [ 1851 | "cfg-if 1.0.0", 1852 | "serde", 1853 | "serde_json", 1854 | "wasm-bindgen-macro", 1855 | ] 1856 | 1857 | [[package]] 1858 | name = "wasm-bindgen-backend" 1859 | version = "0.2.71" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8" 1862 | dependencies = [ 1863 | "bumpalo", 1864 | "lazy_static", 1865 | "log", 1866 | "proc-macro2", 1867 | "quote", 1868 | "syn", 1869 | "wasm-bindgen-shared", 1870 | ] 1871 | 1872 | [[package]] 1873 | name = "wasm-bindgen-futures" 1874 | version = "0.4.21" 1875 | source = "registry+https://github.com/rust-lang/crates.io-index" 1876 | checksum = "8e67a5806118af01f0d9045915676b22aaebecf4178ae7021bc171dab0b897ab" 1877 | dependencies = [ 1878 | "cfg-if 1.0.0", 1879 | "js-sys", 1880 | "wasm-bindgen", 1881 | "web-sys", 1882 | ] 1883 | 1884 | [[package]] 1885 | name = "wasm-bindgen-macro" 1886 | version = "0.2.71" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "e5ac38da8ef716661f0f36c0d8320b89028efe10c7c0afde65baffb496ce0d3b" 1889 | dependencies = [ 1890 | "quote", 1891 | "wasm-bindgen-macro-support", 1892 | ] 1893 | 1894 | [[package]] 1895 | name = "wasm-bindgen-macro-support" 1896 | version = "0.2.71" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e" 1899 | dependencies = [ 1900 | "proc-macro2", 1901 | "quote", 1902 | "syn", 1903 | "wasm-bindgen-backend", 1904 | "wasm-bindgen-shared", 1905 | ] 1906 | 1907 | [[package]] 1908 | name = "wasm-bindgen-shared" 1909 | version = "0.2.71" 1910 | source = "registry+https://github.com/rust-lang/crates.io-index" 1911 | checksum = "7d6f8ec44822dd71f5f221a5847fb34acd9060535c1211b70a05844c0f6383b1" 1912 | 1913 | [[package]] 1914 | name = "weathernoaa" 1915 | version = "0.2.0" 1916 | source = "registry+https://github.com/rust-lang/crates.io-index" 1917 | checksum = "63fef04a6eedbf5b277c458c8e1d89c9e8314ae3384a1c435037674ebaa48a03" 1918 | dependencies = [ 1919 | "anyhow", 1920 | "nom 6.1.2", 1921 | "reqwest", 1922 | "thiserror", 1923 | "tokio", 1924 | ] 1925 | 1926 | [[package]] 1927 | name = "web-sys" 1928 | version = "0.3.48" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "ec600b26223b2948cedfde2a0aa6756dcf1fef616f43d7b3097aaf53a6c4d92b" 1931 | dependencies = [ 1932 | "js-sys", 1933 | "wasm-bindgen", 1934 | ] 1935 | 1936 | [[package]] 1937 | name = "which" 1938 | version = "3.1.1" 1939 | source = "registry+https://github.com/rust-lang/crates.io-index" 1940 | checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" 1941 | dependencies = [ 1942 | "libc", 1943 | ] 1944 | 1945 | [[package]] 1946 | name = "winapi" 1947 | version = "0.3.9" 1948 | source = "registry+https://github.com/rust-lang/crates.io-index" 1949 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1950 | dependencies = [ 1951 | "winapi-i686-pc-windows-gnu", 1952 | "winapi-x86_64-pc-windows-gnu", 1953 | ] 1954 | 1955 | [[package]] 1956 | name = "winapi-i686-pc-windows-gnu" 1957 | version = "0.4.0" 1958 | source = "registry+https://github.com/rust-lang/crates.io-index" 1959 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1960 | 1961 | [[package]] 1962 | name = "winapi-util" 1963 | version = "0.1.5" 1964 | source = "registry+https://github.com/rust-lang/crates.io-index" 1965 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1966 | dependencies = [ 1967 | "winapi", 1968 | ] 1969 | 1970 | [[package]] 1971 | name = "winapi-x86_64-pc-windows-gnu" 1972 | version = "0.4.0" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1975 | 1976 | [[package]] 1977 | name = "windows-sys" 1978 | version = "0.42.0" 1979 | source = "registry+https://github.com/rust-lang/crates.io-index" 1980 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1981 | dependencies = [ 1982 | "windows_aarch64_gnullvm", 1983 | "windows_aarch64_msvc", 1984 | "windows_i686_gnu", 1985 | "windows_i686_msvc", 1986 | "windows_x86_64_gnu", 1987 | "windows_x86_64_gnullvm", 1988 | "windows_x86_64_msvc", 1989 | ] 1990 | 1991 | [[package]] 1992 | name = "windows-sys" 1993 | version = "0.45.0" 1994 | source = "registry+https://github.com/rust-lang/crates.io-index" 1995 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1996 | dependencies = [ 1997 | "windows-targets", 1998 | ] 1999 | 2000 | [[package]] 2001 | name = "windows-targets" 2002 | version = "0.42.1" 2003 | source = "registry+https://github.com/rust-lang/crates.io-index" 2004 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 2005 | dependencies = [ 2006 | "windows_aarch64_gnullvm", 2007 | "windows_aarch64_msvc", 2008 | "windows_i686_gnu", 2009 | "windows_i686_msvc", 2010 | "windows_x86_64_gnu", 2011 | "windows_x86_64_gnullvm", 2012 | "windows_x86_64_msvc", 2013 | ] 2014 | 2015 | [[package]] 2016 | name = "windows_aarch64_gnullvm" 2017 | version = "0.42.1" 2018 | source = "registry+https://github.com/rust-lang/crates.io-index" 2019 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 2020 | 2021 | [[package]] 2022 | name = "windows_aarch64_msvc" 2023 | version = "0.42.1" 2024 | source = "registry+https://github.com/rust-lang/crates.io-index" 2025 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 2026 | 2027 | [[package]] 2028 | name = "windows_i686_gnu" 2029 | version = "0.42.1" 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" 2031 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 2032 | 2033 | [[package]] 2034 | name = "windows_i686_msvc" 2035 | version = "0.42.1" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 2038 | 2039 | [[package]] 2040 | name = "windows_x86_64_gnu" 2041 | version = "0.42.1" 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" 2043 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 2044 | 2045 | [[package]] 2046 | name = "windows_x86_64_gnullvm" 2047 | version = "0.42.1" 2048 | source = "registry+https://github.com/rust-lang/crates.io-index" 2049 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 2050 | 2051 | [[package]] 2052 | name = "windows_x86_64_msvc" 2053 | version = "0.42.1" 2054 | source = "registry+https://github.com/rust-lang/crates.io-index" 2055 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 2056 | 2057 | [[package]] 2058 | name = "winreg" 2059 | version = "0.7.0" 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" 2061 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 2062 | dependencies = [ 2063 | "winapi", 2064 | ] 2065 | 2066 | [[package]] 2067 | name = "wyz" 2068 | version = "0.2.0" 2069 | source = "registry+https://github.com/rust-lang/crates.io-index" 2070 | checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" 2071 | 2072 | [[package]] 2073 | name = "xcb" 2074 | version = "0.9.0" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "62056f63138b39116f82a540c983cc11f1c90cd70b3d492a70c25eaa50bd22a6" 2077 | dependencies = [ 2078 | "libc", 2079 | "log", 2080 | ] 2081 | 2082 | [[package]] 2083 | name = "xcb-util" 2084 | version = "0.3.0" 2085 | source = "registry+https://github.com/rust-lang/crates.io-index" 2086 | checksum = "43893e47f27bf7d81d489feef3a0e34a457e90bc314b7e74ad9bb3980e4c1c48" 2087 | dependencies = [ 2088 | "libc", 2089 | "xcb", 2090 | ] 2091 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cnx", 4 | "cnx-contrib", 5 | "cnx-bin" 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Killough 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL = help 2 | SHELL=bash 3 | 4 | ## Run all the tests 5 | tests: 6 | cargo test 7 | 8 | ## Pre-check before publishing to crate 9 | check: 10 | cargo clean 11 | make check-without-clean 12 | 13 | ## Same as check but without clean 14 | check-without-clean: 15 | make tests 16 | cargo fmt --all -- --check 17 | cargo clippy -- -D warnings 18 | cargo check 19 | 20 | ## Watch and run test 21 | watch-test: 22 | cargo watch -x test 23 | 24 | ## Run the binary 25 | run: 26 | cargo run 27 | 28 | 29 | ## Create release binary 30 | bin-release: 31 | cd cnx-bin && cargo install --path . --root ~ 32 | 33 | ## Setup 34 | setup: 35 | rustup component add rustfmt 36 | rustup component add clippy 37 | 38 | ## Watch and run build 39 | watch-build: 40 | cargo watch -x build 41 | 42 | ## Watch and run binary 43 | watch-run: 44 | cargo watch -x run 45 | 46 | ## Show help screen. 47 | help: 48 | @echo "Please use \`make ' where is one of\n\n" 49 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 50 | helpMessage = match(lastLine, /^## (.*)/); \ 51 | if (helpMessage) { \ 52 | helpCommand = substr($$1, 0, index($$1, ":")); \ 53 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 54 | printf "%-30s %s\n", helpCommand, helpMessage; \ 55 | } \ 56 | } \ 57 | { lastLine = $$0 }' $(MAKEFILE_LIST) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cnx — [![CI](https://github.com/mjkillough/cnx/actions/workflows/ci.yml/badge.svg)](https://github.com/mjkillough/cnx/actions) 2 | 3 | A simple X11 status bar for use with simple WMs. 4 | 5 | Cnx doesn't rely on functionality from any specific WM, instead preferring to 6 | get its data from generic properties defined in EWMH. If your WM implements 7 | enough of EWMH, it should work with Cnx. 8 | 9 | ![screenshot of cnx](/screenshot.png?raw=true) 10 | 11 | ## Features 12 | 13 | Cnx is written to be customisable, simple and fast. 14 | 15 | Where possible, it prefers to asynchronously wait for changes in the underlying 16 | data sources (and uses [`tokio`] to achieve this), rather than periodically 17 | calling out to external programs. 18 | 19 | [`tokio`]: https://tokio.rs/ 20 | 21 | There are currently these widgets available: 22 | 23 | - Active Window Title — Shows the title (EWMH's `_NET_WM_NAME`) for 24 | the currently focused window (EWMH's `_NEW_ACTIVE_WINDOW`). 25 | - Pager — Shows the WM's workspaces/groups, highlighting whichever is 26 | currently active. (Uses EWMH's `_NET_DESKTOP_NAMES`, 27 | `_NET_NUMBER_OF_DESKTOPS` and `_NET_CURRENT_DESKTOP`). 28 | - Clock — Shows the time. 29 | 30 | The cnx-contrib crate contains additional widgets: 31 | 32 | - **Sensors** — Periodically parses and displays the output of the 33 | sensors provided by the system. 34 | - **Volume** - Shows the current volume/mute status of the default output 35 | device. 36 | - **Battery** - Shows the remaining battery and charge status. 37 | - **Wireless** - Shows the wireless strength of your current network. 38 | - **CPU** - Shows the current CPU consumption 39 | - **Weather** - Shows the Weather information of your location 40 | - **Disk Usage** - Show the current usage of your monted filesystem 41 | - **LeftWM** - Shows the monitors and tags from LeftWM 42 | 43 | The [`Sensors`], [`Volume`] and [`Battery`] widgets require platform 44 | support. They currently support Linux (see dependencies below) and OpenBSD. 45 | Support for additional platforms should be possible. 46 | 47 | ## How to use 48 | 49 | Cnx is a library that allows you to make your own status bar. 50 | 51 | In normal usage, you will create a new binary project that relies on the `cnx` 52 | crate, and customize it through options passed to the main `Cnx` object and 53 | its widgets. (It's inspired by [`QTile`] and [`dwm`], in that the configuration 54 | is done entirely in code, allowing greater extensibility without needing complex 55 | configuration handling). 56 | 57 | [`QTile`]: http://www.qtile.org/ 58 | [`dwm`]: http://dwm.suckless.org/ 59 | 60 | An simple example of a binary using Cnx is: 61 | 62 | ```rust 63 | use cnx::text::*; 64 | use cnx::widgets::*; 65 | use cnx::{Cnx, Position}; 66 | 67 | fn main() -> Result<()> { 68 | let attr = Attributes { 69 | font: Font::new("Envy Code R 21"), 70 | fg_color: Color::white(), 71 | bg_color: None, 72 | padding: Padding::new(8.0, 8.0, 0.0, 0.0), 73 | }; 74 | 75 | let mut cnx = Cnx::new(Position::Top); 76 | cnx.add_widget(ActiveWindowTitle::new(attr.clone())); 77 | cnx.add_widget(Clock::new(attr.clone())); 78 | cnx.run()?; 79 | 80 | Ok(()) 81 | } 82 | ``` 83 | 84 | A more complex example is given in [`src/bin/cnx.rs`] alongside the project. 85 | (This is the default `[bin]` target for the crate, so you _could_ use it by 86 | either executing `cargo run` from the crate root, or even running `cargo install 87 | cnx; cnx`. However, neither of these are recommended as options for customizing 88 | Cnx are then limited). 89 | 90 | Before running Cnx, you'll need to make sure your system has the required 91 | [dependencies]. 92 | 93 | [`src/bin/cnx.rs`]: https://github.com/mjkillough/cnx/blob/master/src/bin/cnx.rs 94 | [dependencies]: #dependencies 95 | 96 | ## Dependencies 97 | 98 | In addition to the Rust dependencies in `Cargo.toml`, Cnx also depends on these 99 | system libraries: 100 | - `x11-xcb` 101 | - `xcb-util`: `xcb-ewmh` / `xcb-icccm` / `xcb-keysyms` 102 | - `pango` 103 | - `cairo` 104 | - `pangocairo` 105 | 106 | The following Ubuntu packages should allow your system to meet these 107 | requirements: 108 | 109 | ``` 110 | apt-get install libx11-xcb-dev libxcb-ewmh-dev libpango1.0-dev libcairo2-dev 111 | ``` 112 | 113 | If the `volume` feature is enabled (and it is by default), you will 114 | also need `alsa-lib` on Linux: 115 | 116 | ``` 117 | apt-get install libasound2-dev 118 | ``` 119 | 120 | If the `wireless` feature is enabled (and it is not by default), you will also need `iwlib-dev` 121 | on Linux: 122 | 123 | ``` 124 | apt install libiw-dev 125 | ``` 126 | 127 | ## Tests 128 | 129 | Unfortunately there aren't many. You can run what's here with: 130 | 131 | ``` 132 | cargo test 133 | ``` 134 | 135 | 136 | ## License 137 | 138 | MIT 139 | -------------------------------------------------------------------------------- /cnx-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cnx-bin" 3 | version = "0.1.0" 4 | authors = ["Sibi Prabakaran "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | cnx = { path = "../cnx" } 11 | cnx-contrib = { path = "../cnx-contrib", features = ["wireless", "leftwm"]} 12 | anyhow = "1.0.41" 13 | weathernoaa = "0.2.0" 14 | byte-unit = "4.0.12" 15 | -------------------------------------------------------------------------------- /cnx-bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use byte_unit::ByteUnit; 3 | use cnx::text::*; 4 | use cnx::widgets::*; 5 | use cnx::{Cnx, Position}; 6 | use cnx_contrib::widgets::battery::*; 7 | use cnx_contrib::widgets::disk_usage::*; 8 | use cnx_contrib::widgets::*; 9 | use weathernoaa::weather::WeatherInfo; 10 | 11 | fn pango_markup_render(color: Color, start_text: String, text: String) -> String { 12 | format!( 13 | "[{} {}]", 14 | start_text, color.to_hex(), text 15 | ) 16 | } 17 | 18 | fn pango_markup_single_render(color: Color, start_text: String) -> String { 19 | format!( 20 | "[{}]", 21 | start_text, color.to_hex() 22 | ) 23 | } 24 | 25 | fn weather_sky_condition(condition: String) -> &'static str { 26 | match &condition[..] { 27 | "clear" => "🌣", 28 | "sunny" => "🌣", 29 | "mostly clear" => "🌤", 30 | "mostly sunny" => "🌤", 31 | "partly sunny" => "⛅", 32 | "fair" => "🌑", 33 | "cloudy" => "☁", 34 | "overcast" => "☁", 35 | "partly cloudy" => "⛅", 36 | "mostly cloudy" => "🌧", 37 | "considerable cloudines" => "☔", 38 | _ => "🌑", 39 | } 40 | } 41 | 42 | fn main() -> Result<()> { 43 | let attr = Attributes { 44 | font: Font::new("Ubuntu Mono Bold 14"), 45 | fg_color: Color::white(), 46 | bg_color: None, 47 | padding: Padding::new(0.0, 0.0, 0.0, 0.0), 48 | }; 49 | 50 | let mut cnx = Cnx::new(Position::Bottom); 51 | 52 | // let sensors = Sensors::new(attr.clone(), vec!["Core 0", "Core 1"]); 53 | let battery_render = Box::new(|battery_info: BatteryInfo| { 54 | let percentage = battery_info.capacity; 55 | 56 | let default_text = format!("🔋{percentage:.0}%",); 57 | pango_markup_single_render(Color::white(), default_text) 58 | }); 59 | 60 | let battery = Battery::new(attr.clone(), Color::red(), None, Some(battery_render)); 61 | let render = Box::new(|load| { 62 | let mut color = Color::yellow().to_hex(); 63 | if load < 5 { 64 | color = Color::green().to_hex(); 65 | } 66 | if load > 50 { 67 | color = Color::red().to_hex(); 68 | } 69 | format!( 70 | "[Cpu: {load}%]" 71 | ) 72 | }); 73 | let cpu = cpu::Cpu::new(attr.clone(), Some(render))?; 74 | 75 | let volume = volume::Volume::new(attr.clone()); 76 | 77 | let default_threshold = Threshold::default(); 78 | 79 | let wireless = 80 | wireless::Wireless::new(attr.clone(), "wlp2s0".to_owned(), Some(default_threshold)); 81 | 82 | let disk_render = Box::new(|disk_info: DiskInfo| { 83 | let used = disk_info.used.get_adjusted_unit(ByteUnit::GiB).format(0); 84 | let total = disk_info.total.get_adjusted_unit(ByteUnit::GiB).format(0); 85 | let disk_text = format!("🏠 {used}/{total}"); 86 | pango_markup_single_render(Color::white(), disk_text) 87 | }); 88 | 89 | let disk_usage = disk_usage::DiskUsage::new(attr.clone(), "/home".into(), Some(disk_render)); 90 | 91 | let weather_render = Box::new(|weather: WeatherInfo| { 92 | let sky_condition = weather_sky_condition(weather.sky_condition); 93 | let weather_text = format!("BLR: {sky_condition} :"); 94 | let weather_temp = format!(" {}°C", weather.temperature.celsius); 95 | pango_markup_render(Color::white(), weather_text, weather_temp) 96 | }); 97 | 98 | let weather = weather::Weather::new(attr.clone(), "VOBL".into(), Some(weather_render)); 99 | 100 | let active_attr = Attributes { 101 | font: Font::new("Ubuntu Mono Bold 14"), 102 | fg_color: Color::white(), 103 | bg_color: Some(Color::blue()), 104 | padding: Padding::new(8.0, 8.0, 0.0, 0.0), 105 | }; 106 | let inactive_attr = Attributes { 107 | bg_color: None, 108 | ..active_attr.clone() 109 | }; 110 | let non_empty_attr = Attributes { 111 | fg_color: Color::blue(), 112 | ..inactive_attr.clone() 113 | }; 114 | let pager_attrs = PagerAttributes { 115 | active_attr, 116 | inactive_attr, 117 | non_empty_attr, 118 | }; 119 | let pager = Pager::new(pager_attrs); 120 | 121 | cnx.add_widget(pager); 122 | cnx.add_widget(ActiveWindowTitle::new(attr.clone())); 123 | cnx.add_widget(cpu); 124 | cnx.add_widget(weather); 125 | cnx.add_widget(disk_usage); 126 | cnx.add_widget(wireless); 127 | cnx.add_widget(volume); 128 | 129 | // cnx.add_widget(sensors); 130 | cnx.add_widget(battery); 131 | let time_template = Some("[%d-%m-%Y %a %I:%M %p]".into()); 132 | cnx.add_widget(Clock::new(attr, time_template)); 133 | cnx.run()?; 134 | 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /cnx-contrib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cnx-contrib" 3 | version = "0.1.0" 4 | authors = ["Sibi Prabakaran ", "Michael Killough "] 5 | edition = "2021" 6 | 7 | [package.metadata.docs.rs] 8 | all-features = true 9 | rustdoc-args = ["--cfg", "docsrs"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [features] 14 | default = ["volume"] 15 | volume = ["alsa", "sioctl"] 16 | wireless = ["iwlib"] 17 | leftwm = ["process-stream", "serde", "serde_derive", "serde_json"] 18 | 19 | [dependencies] 20 | cnx = { path = "../cnx" } 21 | anyhow = "1.0.41" 22 | weathernoaa = "0.2.0" 23 | tokio = { version = "1.18.0", features = ["rt", "net", "time", "macros", "rt-multi-thread"] } 24 | tokio-stream = { version = "0.1.8" } 25 | async-stream = "0.3.3" 26 | iwlib = { version = "0.1", optional = true} 27 | alsa = { version = "0.5.0", optional = true} 28 | regex = "1.5" 29 | nix = "0.20.0" 30 | byte-unit = "4.0.12" 31 | reqwest = { version = "0.11" } 32 | process-stream = { version = "0.4.1", optional = true} 33 | serde = { version = "1.0.152", optional = true} 34 | serde_derive = { version = "1.0.152", optional = true} 35 | serde_json = { version = "1.0.91", optional = true} 36 | [target.'cfg(openbsd)'.dependencies] 37 | sioctl = { version = "0.0.1", optional = true} 38 | openssl = { version = "0.10", features = ["vendored"] } 39 | -------------------------------------------------------------------------------- /cnx-contrib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | pub mod widgets; 3 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/battery.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "openbsd")] 2 | mod battery_bsd; 3 | #[cfg(target_os = "linux")] 4 | mod battery_linux; 5 | #[cfg(feature = "openbsd")] 6 | pub use battery_bsd::Battery; 7 | #[cfg(target_os = "linux")] 8 | pub use battery_linux::{Battery, BatteryInfo, Status}; 9 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/battery/battery_bsd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use cnx::cmd::{command_output, from_command_output}; 3 | use cnx::text::{Attributes, Color, Text}; 4 | use cnx::widgets::{Widget, WidgetStream}; 5 | use std::str::FromStr; 6 | use std::time::Duration; 7 | use tokio::time; 8 | use tokio_stream::wrappers::IntervalStream; 9 | use tokio_stream::StreamExt; 10 | 11 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 12 | enum Status { 13 | Charging, 14 | Discharging, 15 | Unknown, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug)] 19 | struct Info { 20 | status: Status, 21 | minutes: Option, 22 | percentage: u8, // 0-100 23 | } 24 | 25 | impl Info { 26 | fn time_remaining(&self) -> Option<(u16, u16)> { 27 | if let Some(minutes) = self.minutes { 28 | let hours = minutes / 60; 29 | let minutes = minutes % 60; 30 | return Some((hours, minutes)); 31 | } 32 | None 33 | } 34 | } 35 | 36 | #[derive(Default)] 37 | struct OpenBsd; 38 | 39 | impl OpenBsd { 40 | fn load_status(&self) -> Result { 41 | let string = command_output("apm", &["-a"])?; 42 | match string.trim() { 43 | "0" => Ok(Status::Discharging), 44 | "1" => Ok(Status::Charging), 45 | _ => Ok(Status::Unknown), 46 | } 47 | } 48 | 49 | fn load_percentage(&self) -> Result { 50 | let percentage = from_command_output("apm", &["-l"]).context("Battery percentage")?; 51 | Ok(percentage) 52 | } 53 | 54 | fn load_time_remaining(&self) -> Result> { 55 | let string = command_output("apm", &["-m"])?; 56 | if string.trim() == "unknown" { 57 | return Ok(None); 58 | } 59 | let minutes = u16::from_str(string.trim()).context("Parsing time remaining")?; 60 | Ok(Some(minutes)) 61 | } 62 | 63 | fn load_info(&self) -> Result { 64 | let status = self.load_status()?; 65 | let percentage = self.load_percentage()?; 66 | let minutes = self.load_time_remaining()?; 67 | Ok(Info { 68 | status, 69 | percentage, 70 | minutes, 71 | }) 72 | } 73 | } 74 | 75 | // TODO: Gate this on platform once we add the Linux impl back. 76 | type BatteryInfo = OpenBsd; 77 | 78 | /// Shows battery charge percentage and (dis)charge time. 79 | /// 80 | /// This widget shows the battery's current charge percentage and the amount of 81 | /// remaining (dis)charge time, depending on whether the battery is charging or 82 | /// discharging. The format of the output is `(PP% HH:MM)`. 83 | /// 84 | /// When the battery has less than 10% charge remaining, the widget's text will 85 | /// change to the specified `warning_color`. 86 | /// 87 | /// On Linux, battery charge information is read from [`/sys/class/power_supply/BAT0/`]. 88 | /// 89 | /// On OpenBSD, battery information is parsed from [`apm`]. 90 | /// 91 | /// [`/sys/class/power_supply/BAT0/`]: https://www.kernel.org/doc/Documentation/power/power_supply_class.txt 92 | /// [`apm`]: https://man.openbsd.org/apm.8 93 | pub struct Battery { 94 | update_interval: Duration, 95 | info: BatteryInfo, 96 | attr: Attributes, 97 | warning_color: Color, 98 | } 99 | 100 | impl Battery { 101 | /// Creates a new Battery widget. 102 | /// 103 | /// The `warning_color` attributes are used when there is less than 10% 104 | /// battery charge remaining. 105 | pub fn new(attr: Attributes, warning_color: Color) -> Self { 106 | Self { 107 | update_interval: Duration::from_secs(60), 108 | info: BatteryInfo::default(), 109 | attr, 110 | warning_color, 111 | } 112 | } 113 | 114 | fn tick(&self) -> Result> { 115 | let info = self.info.load_info()?; 116 | 117 | let mut text = match info.status { 118 | Status::Charging => "(⚡ ".to_owned(), 119 | _ => "(".to_owned(), 120 | }; 121 | text += &format!("{:.0}%", info.percentage); 122 | if let Some((hours, minutes)) = info.time_remaining() { 123 | text += &format!(" - {hours}:{minutes:02})", hours = hours, minutes = minutes); 124 | } else { 125 | text += ")"; 126 | } 127 | 128 | // If we're discharging and have <=10% left, then render with a 129 | // special warning color. 130 | let mut attr = self.attr.clone(); 131 | if info.status == Status::Discharging && info.percentage <= 10 { 132 | attr.fg_color = self.warning_color.clone() 133 | } 134 | 135 | Ok(vec![Text { 136 | attr, 137 | text, 138 | stretch: false, 139 | markup: false, 140 | }]) 141 | } 142 | } 143 | 144 | impl Widget for Battery { 145 | fn into_stream(self: Box) -> Result { 146 | let interval = time::interval(self.update_interval); 147 | let stream = IntervalStream::new(interval).map(move |_| self.tick()); 148 | 149 | Ok(Box::pin(stream)) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/battery/battery_linux.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Error, Result}; 2 | use cnx::text::{Attributes, Color, Text}; 3 | use cnx::widgets::{Widget, WidgetStream}; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::str::FromStr; 7 | use std::time::Duration; 8 | use tokio::time; 9 | use tokio_stream::wrappers::IntervalStream; 10 | use tokio_stream::StreamExt; 11 | 12 | /// Represent Battery's operating status 13 | #[derive(Clone, Debug, Eq, PartialEq)] 14 | pub enum Status { 15 | Full, 16 | Charging, 17 | Discharging, 18 | Unknown, 19 | } 20 | 21 | impl FromStr for Status { 22 | type Err = Error; 23 | 24 | fn from_str(s: &str) -> Result { 25 | match s { 26 | "Full" => Ok(Status::Full), 27 | "Charging" => Ok(Status::Charging), 28 | "Discharging" => Ok(Status::Discharging), 29 | "Unknown" => Ok(Status::Unknown), 30 | _ => Err(anyhow!("Unknown Status: {}", s)), 31 | } 32 | } 33 | } 34 | 35 | /// Shows battery charge percentage 36 | /// 37 | /// This widget shows the battery's current charge percentage. 38 | /// 39 | /// When the battery has less than 10% charge remaining, the widget's text will 40 | /// change to the specified `warning_color`. 41 | /// 42 | /// Battery charge information is read from [`/sys/class/power_supply/BAT0/`]. 43 | /// 44 | /// [`/sys/class/power_supply/BAT0/`]: https://www.kernel.org/doc/Documentation/power/power_supply_class.txt 45 | pub struct Battery { 46 | update_interval: Duration, 47 | battery: String, 48 | attr: Attributes, 49 | warning_color: Color, 50 | render: Option String>>, 51 | } 52 | 53 | /// Represent Battery information 54 | #[derive(Clone, Debug, PartialEq, Eq)] 55 | pub struct BatteryInfo { 56 | /// Battery Status 57 | pub status: Status, 58 | /// Capacity in percentage 59 | pub capacity: u8, 60 | } 61 | 62 | impl Battery { 63 | /// Creates a new Battery widget. 64 | /// 65 | /// Creates a new `Battery` widget, whose text will be displayed with the 66 | /// given [`Attributes`]. The caller can provide use the `warning_color` 67 | /// argument, to control the [`Color`] of the text once the battery has 68 | /// less than 10% charge remaining. 69 | /// 70 | /// The [`cnx::Cnx`] instance is borrowed during construction in order to get 71 | /// access to handles of its event loop. However, it is not borrowed for 72 | /// the lifetime of the widget. See the [`cnx::Cnx::add_widget`] for more 73 | /// discussion about the lifetime of the borrow. 74 | /// 75 | /// # Examples 76 | /// 77 | /// ``` 78 | /// # #[macro_use] 79 | /// # extern crate cnx; 80 | /// # 81 | /// # use cnx::*; 82 | /// # use cnx::text::*; 83 | /// # use cnx::widgets::*; 84 | /// # use cnx_contrib::widgets::battery::*; 85 | /// # use anyhow::Result; 86 | /// # 87 | /// # fn run() -> Result<()> { 88 | /// let attr = Attributes { 89 | /// font: Font::new("SourceCodePro 21"), 90 | /// fg_color: Color::white(), 91 | /// bg_color: None, 92 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 93 | /// }; 94 | /// 95 | /// let mut cnx = Cnx::new(Position::Top); 96 | /// cnx.add_widget(Battery::new(attr.clone(), Color::red(), None, None)); 97 | /// # Ok(()) 98 | /// # } 99 | /// # fn main() { run().unwrap(); } 100 | /// ``` 101 | pub fn new( 102 | attr: Attributes, 103 | warning_color: Color, 104 | battery: Option, 105 | render: Option String>>, 106 | ) -> Battery { 107 | Battery { 108 | update_interval: Duration::from_secs(60), 109 | battery: battery.unwrap_or_else(|| "BAT0".into()), 110 | attr, 111 | warning_color, 112 | render, 113 | } 114 | } 115 | 116 | fn load_value_inner(&self, file: &str) -> Result 117 | where 118 | T: FromStr, 119 | ::Err: Into, 120 | { 121 | let path = format!("/sys/class/power_supply/{}/{}", self.battery, file); 122 | let mut file = File::open(path)?; 123 | let mut contents = String::new(); 124 | file.read_to_string(&mut contents)?; 125 | let s = FromStr::from_str(contents.trim()) 126 | .map_err(|e: ::Err| e.into()) 127 | .context("Failed to parse value")?; 128 | Ok(s) 129 | } 130 | 131 | fn load_value(&self, file: &str) -> Result 132 | where 133 | T: FromStr, 134 | ::Err: Into, 135 | { 136 | let value = self 137 | .load_value_inner(file) 138 | .with_context(|| format!("Could not load value from battery status file: {file}"))?; 139 | Ok(value) 140 | } 141 | 142 | fn get_value(&self) -> Result { 143 | let capacity: u8 = self.load_value("capacity")?; 144 | let status: Status = self.load_value("status")?; 145 | Ok(BatteryInfo { capacity, status }) 146 | } 147 | 148 | fn tick(&self) -> Result> { 149 | let battery_info = self.get_value()?; 150 | 151 | let default_text = format!("({percentage:.0}%)", percentage = battery_info.capacity,); 152 | let text = self 153 | .render 154 | .as_ref() 155 | .map_or(default_text, |x| (x)(battery_info.clone())); 156 | 157 | // If we're discharging and have <=10% left, then render with a 158 | // special warning color. 159 | let mut attr = self.attr.clone(); 160 | if battery_info.status == Status::Discharging && battery_info.capacity <= 10 { 161 | attr.fg_color = self.warning_color.clone() 162 | } 163 | 164 | Ok(vec![Text { 165 | attr, 166 | text, 167 | stretch: false, 168 | markup: self.render.is_some(), 169 | }]) 170 | } 171 | } 172 | 173 | impl Widget for Battery { 174 | fn into_stream(self: Box) -> Result { 175 | let interval = time::interval(self.update_interval); 176 | let stream = IntervalStream::new(interval).map(move |_| self.tick()); 177 | 178 | Ok(Box::pin(stream)) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/command.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cnx::text::{Attributes, Text}; 3 | use cnx::widgets::{Widget, WidgetStream}; 4 | use std::process::Command as Process; 5 | use std::time::Duration; 6 | use tokio::time; 7 | use tokio_stream::wrappers::IntervalStream; 8 | use tokio_stream::StreamExt; 9 | 10 | pub struct Command { 11 | attr: Attributes, 12 | command: String, 13 | update_interval: Duration, 14 | } 15 | 16 | impl Command { 17 | /// Creates a new [`Command`] widget. 18 | /// 19 | /// Arguments 20 | /// 21 | /// * `attr` - Represents `Attributes` which controls properties like 22 | /// `Font`, foreground and background color etc. 23 | /// 24 | /// * `command` - Command to be executed. 25 | /// 26 | /// * `update_interval` - Time interval between updates. 27 | /// 28 | /// # Examples 29 | /// 30 | /// ``` 31 | /// #[macro_use] 32 | /// extern crate cnx; 33 | /// 34 | /// use cnx::*; 35 | /// use cnx::text::*; 36 | /// use cnx_contrib::widgets::command::*; 37 | /// use anyhow::Result; 38 | /// use std::time::Duration; 39 | /// 40 | /// fn run() -> Result<()> { 41 | /// let attr = Attributes { 42 | /// font: Font::new("SourceCodePro 16"), 43 | /// fg_color: Color::white(), 44 | /// bg_color: None, 45 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 46 | /// }; 47 | /// 48 | /// let mut cnx = Cnx::new(Position::Top); 49 | /// cnx.add_widget(Command::new(attr, "echo foo".into(), Duration::from_secs(10))); 50 | /// Ok(()) 51 | /// } 52 | /// fn main() { run().unwrap(); } 53 | /// ``` 54 | pub fn new(attr: Attributes, command: String, update_interval: Duration) -> Self { 55 | Self { 56 | attr, 57 | command, 58 | update_interval, 59 | } 60 | } 61 | 62 | fn tick(&self) -> Vec { 63 | let output = Process::new("sh") 64 | .arg("-c") 65 | .arg(self.command.clone()) 66 | .output() 67 | .expect("failed to execute process"); 68 | 69 | let texts = vec![Text { 70 | attr: self.attr.clone(), 71 | text: String::from_utf8(output.stdout).unwrap_or_else(|_| "error".into()), 72 | stretch: false, 73 | markup: true, 74 | }]; 75 | 76 | texts 77 | } 78 | } 79 | 80 | impl Widget for Command { 81 | fn into_stream(self: Box) -> Result { 82 | let interval = time::interval(self.update_interval); 83 | let stream = IntervalStream::new(interval).map(move |_| Ok(self.tick())); 84 | 85 | Ok(Box::pin(stream)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/cpu.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use cnx::text::{Attributes, Text}; 3 | use cnx::widgets::{Widget, WidgetStream}; 4 | use std::fs::File; 5 | use std::io::BufRead; 6 | use std::io::BufReader; 7 | use std::time::Duration; 8 | use tokio::time; 9 | use tokio_stream::wrappers::IntervalStream; 10 | use tokio_stream::StreamExt; 11 | 12 | /// Represents CPU widget used to show current CPU consumptiong 13 | pub struct Cpu { 14 | attr: Attributes, 15 | cpu_data: CpuData, 16 | render: Option String>>, 17 | } 18 | 19 | impl Cpu { 20 | /// Creates a new [`Cpu`] widget. 21 | /// 22 | /// Arguments 23 | /// 24 | /// * `attr` - Represents `Attributes` which controls properties like 25 | /// `Font`, foreground and background color etc. 26 | /// 27 | /// * `render` - We use the closure to control the way output is 28 | /// displayed in the bar. `u64` represents the current CPU usage 29 | /// in percentage. 30 | /// 31 | /// # Examples 32 | /// 33 | /// ``` 34 | /// # #[macro_use] 35 | /// # extern crate cnx; 36 | /// # 37 | /// # use cnx::*; 38 | /// # use cnx::text::*; 39 | /// # use cnx_contrib::widgets::cpu::*; 40 | /// # use anyhow::Result; 41 | /// # 42 | /// # fn run() -> Result<()> { 43 | /// let attr = Attributes { 44 | /// font: Font::new("SourceCodePro 21"), 45 | /// fg_color: Color::white(), 46 | /// bg_color: None, 47 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 48 | /// }; 49 | /// 50 | /// let mut cnx = Cnx::new(Position::Top); 51 | /// cnx.add_widget(Cpu::new(attr, None)?); 52 | /// # Ok(()) 53 | /// # } 54 | /// # fn main() { run().unwrap(); } 55 | /// ``` 56 | pub fn new(attr: Attributes, render: Option String>>) -> Result { 57 | let cpu_data = CpuData::get_values()?; 58 | Ok(Cpu { 59 | attr, 60 | cpu_data, 61 | render, 62 | }) 63 | } 64 | 65 | fn tick(&mut self) -> Result> { 66 | let cpu_data = CpuData::get_values()?; 67 | 68 | // https://github.com/jaor/xmobar/blob/61d075d3c275366c3344d59c058d7dd0baf21ef2/src/Xmobar/Plugins/Monitors/Cpu.hs#L128 69 | let previous = &self.cpu_data; 70 | let current = cpu_data; 71 | let diff_total = (current.user_time - previous.user_time) 72 | + (current.nice_time - previous.nice_time) 73 | + (current.system_time - previous.system_time) 74 | + (current.idle_time - previous.idle_time) 75 | + (current.iowait_time - previous.iowait_time); 76 | let percentage = match diff_total { 77 | 0 => 0.0, 78 | _ => (current.total_time - previous.total_time) as f64 / diff_total as f64, 79 | }; 80 | 81 | let cpu_usage = (percentage * 100.0) as u64; 82 | let text = self 83 | .render 84 | .as_ref() 85 | .map_or(format!("{cpu_usage} %"), |x| (x)(cpu_usage)); 86 | self.cpu_data = current; 87 | let texts = vec![Text { 88 | attr: self.attr.clone(), 89 | text, 90 | stretch: false, 91 | markup: true, 92 | }]; 93 | Ok(texts) 94 | } 95 | } 96 | 97 | struct CpuData { 98 | user_time: i64, 99 | nice_time: i64, 100 | system_time: i64, 101 | idle_time: i64, 102 | total_time: i64, 103 | iowait_time: i64, 104 | } 105 | 106 | impl CpuData { 107 | fn get_values() -> Result { 108 | // https://www.kernel.org/doc/Documentation/filesystems/proc.txt 109 | let file = File::open("/proc/stat")?; 110 | let mut cpu_line = String::new(); 111 | let mut reader = BufReader::new(file); 112 | reader.read_line(&mut cpu_line)?; 113 | let val: Vec<&str> = cpu_line 114 | .split(' ') 115 | .filter(|item| item != &"cpu" && !item.is_empty()) 116 | .collect(); 117 | let mut cpu_data = CpuData { 118 | user_time: 0, 119 | nice_time: 0, 120 | system_time: 0, 121 | idle_time: 0, 122 | total_time: 0, 123 | iowait_time: 0, 124 | }; 125 | match val[..] { 126 | [user, nice, system, idle, iowait, ..] => { 127 | let user_time = user.parse()?; 128 | let nice_time = nice.parse()?; 129 | let system_time = system.parse()?; 130 | let idle_time = idle.parse()?; 131 | let iowait_time = iowait.parse()?; 132 | cpu_data.user_time = user_time; 133 | cpu_data.nice_time = nice_time; 134 | cpu_data.system_time = system_time; 135 | cpu_data.idle_time = idle_time; 136 | cpu_data.iowait_time = iowait_time; 137 | cpu_data.total_time = user_time + nice_time + system_time; 138 | } 139 | _ => return Err(anyhow!("Missing data in /proc/stat")), 140 | } 141 | Ok(cpu_data) 142 | } 143 | } 144 | 145 | impl Widget for Cpu { 146 | fn into_stream(mut self: Box) -> Result { 147 | let ten_seconds = Duration::from_secs(10); 148 | let interval = time::interval(ten_seconds); 149 | let stream = IntervalStream::new(interval).map(move |_| self.tick()); 150 | Ok(Box::pin(stream)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/disk_usage.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use byte_unit::{Byte, ByteUnit}; 3 | use cnx::text::{Attributes, Text}; 4 | use cnx::widgets::{Widget, WidgetStream}; 5 | use nix::sys::statvfs::statvfs; 6 | use std::time::Duration; 7 | use tokio::time; 8 | use tokio_stream::wrappers::IntervalStream; 9 | use tokio_stream::StreamExt; 10 | 11 | /// Represent Information about the mounted filesystem 12 | #[derive(Debug)] 13 | pub struct DiskInfo { 14 | /// Total size of the filesystem 15 | pub total: Byte, 16 | /// Total used space of the filesystem 17 | pub used: Byte, 18 | /// Total free space of the filesystem 19 | pub free: Byte, 20 | } 21 | 22 | impl DiskInfo { 23 | fn new(path: &str) -> Result { 24 | let stat = statvfs(path)?; 25 | let total_size = stat.blocks() * stat.fragment_size(); 26 | let used = (stat.blocks() - stat.blocks_free()) * stat.fragment_size(); 27 | let available = stat.blocks_available() * stat.fragment_size(); 28 | let total = byte_unit::Byte::from_bytes(total_size as u128); 29 | let used = byte_unit::Byte::from_bytes(used as u128); 30 | let free: Byte = byte_unit::Byte::from_bytes(available as u128); 31 | 32 | let disk_info = DiskInfo { total, used, free }; 33 | Ok(disk_info) 34 | } 35 | } 36 | 37 | /// Disk usage widget to show current usage and remaining free space 38 | /// in the mounted filesystem. 39 | pub struct DiskUsage { 40 | attr: Attributes, 41 | path: String, 42 | render: Option String>>, 43 | } 44 | 45 | impl DiskUsage { 46 | /// Creates a new [`DiskUsage`] widget. 47 | /// 48 | /// Arguments 49 | /// 50 | /// * `attr` - Represents `Attributes` which controls properties like 51 | /// `Font`, foreground and background color etc. 52 | /// 53 | /// * `path` - Pathname of any file within the mounted filesystem. 54 | 55 | /// * `render` - We use the closure to control the way output is 56 | /// displayed in the bar. [`DiskInfo`] represents the details 57 | /// about the mounted filesystem. 58 | /// 59 | /// # Examples 60 | /// 61 | /// ``` 62 | /// # #[macro_use] 63 | /// # extern crate cnx; 64 | /// # 65 | /// # use cnx::*; 66 | /// # use cnx::text::*; 67 | /// # use cnx_contrib::widgets::disk_usage::*; 68 | /// # use anyhow::Result; 69 | /// # 70 | /// # fn run() -> Result<()> { 71 | /// let attr = Attributes { 72 | /// font: Font::new("SourceCodePro 21"), 73 | /// fg_color: Color::white(), 74 | /// bg_color: None, 75 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 76 | /// }; 77 | /// 78 | /// let mut cnx = Cnx::new(Position::Top); 79 | /// cnx.add_widget(DiskUsage::new(attr, "/home".into(), None)); 80 | /// # Ok(()) 81 | /// # } 82 | /// # fn main() { run().unwrap(); } 83 | /// ``` 84 | pub fn new( 85 | attr: Attributes, 86 | path: String, 87 | render: Option String>>, 88 | ) -> Self { 89 | Self { attr, render, path } 90 | } 91 | 92 | fn tick(&self) -> Result> { 93 | let disk_info = DiskInfo::new(self.path.as_ref())?; 94 | let disk_default_str = format!( 95 | "Disk: {}/{}", 96 | disk_info.used.get_adjusted_unit(ByteUnit::GiB).format(0), 97 | disk_info.total.get_adjusted_unit(ByteUnit::GiB).format(0) 98 | ); 99 | 100 | let text: String = self 101 | .render 102 | .as_ref() 103 | .map_or(disk_default_str, |disk| (disk)(disk_info)); 104 | let texts = vec![Text { 105 | attr: self.attr.clone(), 106 | text, 107 | stretch: false, 108 | markup: true, 109 | }]; 110 | Ok(texts) 111 | } 112 | } 113 | 114 | impl Widget for DiskUsage { 115 | fn into_stream(self: Box) -> Result { 116 | let one_hour = Duration::from_secs(3600); 117 | let interval = time::interval(one_hour); 118 | let stream = IntervalStream::new(interval).map(move |_| self.tick()); 119 | 120 | Ok(Box::pin(stream)) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/leftwm.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cnx::text::{Attributes, Text}; 3 | use cnx::widgets::{Widget, WidgetStream}; 4 | use process_stream::{Process, ProcessExt, StreamExt}; 5 | use serde_derive::Deserialize; 6 | 7 | #[derive(Deserialize, Debug)] 8 | struct State { 9 | // _window_title: String, 10 | workspaces: Vec, 11 | } 12 | 13 | #[derive(Deserialize, Debug)] 14 | struct Workspace { 15 | // h: u16, 16 | // w: u16, 17 | // x: i16, 18 | // y: i16, 19 | output: String, 20 | // layout: String, 21 | // index: u16, 22 | tags: Vec, 23 | } 24 | 25 | #[derive(Deserialize, Debug)] 26 | struct Tag { 27 | name: String, 28 | // index: u16, 29 | mine: bool, 30 | visible: bool, 31 | focused: bool, 32 | // TODO: maybe use? 33 | // urgent: bool, 34 | busy: bool, 35 | } 36 | 37 | /// LeftWMAttributes represents the different [`Attributes`] used by the different tag states 38 | #[derive(Clone)] 39 | pub struct LeftWMAttributes { 40 | /// The Attributes of the focused tag 41 | pub focused: Attributes, 42 | /// The Attributes of the visible tags 43 | pub visible: Attributes, 44 | /// The Attributes of the busy tags 45 | pub busy: Attributes, 46 | /// The Attributes of the empty tags 47 | pub empty: Attributes, 48 | } 49 | 50 | /// LeftWM widget that shows information about the worksapces and tags 51 | pub struct LeftWM { 52 | output: String, 53 | attrs: LeftWMAttributes, 54 | } 55 | 56 | impl LeftWM { 57 | /// Creates a new [`LeftWM`] widget. 58 | /// 59 | /// Arguments 60 | /// 61 | /// * `output` - Represents the name of the monitor this widget it attached to 62 | /// 63 | /// * `attr` - Represents the [`LeftWMAttributes`] which controls properties like 64 | /// `Font`, foreground and background color of the different tag's states 65 | /// 66 | /// # Examples 67 | /// 68 | /// ``` 69 | /// # #[macro_use] 70 | /// # extern crate cnx; 71 | /// # 72 | /// # use cnx::*; 73 | /// # use cnx::text::*; 74 | /// # use cnx_contrib::widgets::leftwm::*; 75 | /// # use anyhow::Result; 76 | /// # 77 | /// # fn run() -> Result<()> { 78 | /// let focused = Attributes { 79 | /// font: Font::new("SourceCodePro 14"), 80 | /// fg_color: Color::white(), 81 | /// bg_color: Some(Color::blue()), 82 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 83 | /// }; 84 | /// 85 | /// let empty = Attributes { 86 | /// bg_color: None, 87 | /// ..focused.clone() 88 | /// }; 89 | /// let busy = Attributes { 90 | /// fg_color: Color::blue(), 91 | /// ..empty.clone() 92 | /// }; 93 | /// let visible = Attributes { 94 | /// fg_color: Color::red(), 95 | /// ..empty.clone() 96 | /// }; 97 | /// 98 | /// let mut cnx = Cnx::new(Position::Top); 99 | /// let leftwm_attr = LeftWMAttributes { 100 | /// focused, 101 | /// empty, 102 | /// busy, 103 | /// visible, 104 | /// }; 105 | /// 106 | /// cnx.add_widget(LeftWM::new("eDP1".to_string(), leftwm_attr)); 107 | /// # Ok(()) 108 | /// # } 109 | /// # fn main() { run().unwrap(); } 110 | /// ``` 111 | pub fn new(output: String, attrs: LeftWMAttributes) -> Self { 112 | LeftWM { output, attrs } 113 | } 114 | 115 | fn on_change(&self, content: String) -> Result> { 116 | let state: State = serde_json::from_str(&content)?; 117 | let w = state.workspaces.iter().find(|w| w.output == self.output); 118 | if let Some(w) = w { 119 | let text = w 120 | .tags 121 | .iter() 122 | .map(|t| { 123 | let attr = if t.mine && t.focused { 124 | self.attrs.focused.clone() 125 | } else if t.mine && t.visible { 126 | self.attrs.visible.clone() 127 | } else if t.busy { 128 | self.attrs.busy.clone() 129 | } else { 130 | self.attrs.empty.clone() 131 | }; 132 | Text { 133 | attr, 134 | text: t.name.clone(), 135 | stretch: false, 136 | markup: true, 137 | } 138 | }) 139 | .collect(); 140 | Ok(text) 141 | } else { 142 | Ok(vec![]) 143 | } 144 | } 145 | } 146 | 147 | impl Widget for LeftWM { 148 | fn into_stream(self: Box) -> Result { 149 | let mut state = Process::new("leftwm-state"); 150 | let s = state 151 | .spawn_and_stream()? 152 | .map(move |s| self.on_change(s.to_string())); 153 | // let s = s.map(|s| todo!()); 154 | Ok(Box::pin(s)) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | /// Battery widget to shows the current capacity 2 | pub mod battery; 3 | /// Command widget to show output of a CLI command 4 | pub mod command; 5 | /// CPU widget to show the current CPU consumption 6 | pub mod cpu; 7 | /// Disk usage widget to show current usage and remaining free space 8 | pub mod disk_usage; 9 | /// LeftWM widget that subscribes to leftwm-state and streams the monitors and tags upfate 10 | #[cfg(feature = "leftwm")] 11 | #[cfg_attr(docsrs, doc(cfg(feature = "leftwm")))] 12 | pub mod leftwm; 13 | /// Sensor widget to periodically parses and displays the output of the sensors provided by the system. 14 | pub mod sensors; 15 | /// Volume widget to show the current volume/mute status of the default output device. 16 | pub mod volume; 17 | /// Weather widget to show temperature of your location 18 | pub mod weather; 19 | /// Wireless widget to show wireless strength of your SSID 20 | #[cfg(feature = "wireless")] 21 | #[cfg_attr(docsrs, doc(cfg(feature = "wireless")))] 22 | pub mod wireless; 23 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/sensors.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "openbsd")] 2 | mod sensors_bsd; 3 | #[cfg(target_os = "linux")] 4 | mod sensors_linux; 5 | #[cfg(feature = "openbsd")] 6 | pub use sensors_bsd::Sensors; 7 | #[cfg(target_os = "linux")] 8 | pub use sensors_linux::Sensors; 9 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/sensors/sensors_bsd.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "openbsd")] 2 | use anyhow::{Context, Error, Result}; 3 | use std::collections::HashMap; 4 | use std::process::Command; 5 | use std::time::Duration; 6 | 7 | use regex::Regex; 8 | 9 | use crate::cmd::command_output; 10 | use crate::text::{Attributes, Text}; 11 | use crate::widgets::{Widget, WidgetStream}; 12 | use lazy_static::lazy_static; 13 | // use regex::Regex; 14 | use std::str::FromStr; 15 | use tokio::time; 16 | use tokio_stream::wrappers::IntervalStream; 17 | use tokio_stream::StreamExt; 18 | 19 | #[derive(Debug, PartialEq)] 20 | struct Value { 21 | value: u64, 22 | units: String, 23 | } 24 | 25 | #[derive(Default)] 26 | struct OpenBsd; 27 | 28 | impl OpenBsd { 29 | fn parse_value(&self, value: &str) -> Result { 30 | let value = f64::from_str(value)?; 31 | Ok(value as u64) 32 | } 33 | 34 | fn parse_units(&self, units: &str) -> String { 35 | match units { 36 | "degC" => "°C".to_owned(), 37 | "RPM" => " RPM".to_owned(), 38 | _ => units.to_owned(), 39 | } 40 | } 41 | 42 | fn load_values(&self, sensors: &[String]) -> Result> { 43 | // TODO: Use sysctl C API rather than shelling out. 44 | 45 | lazy_static! { 46 | static ref RE: Regex = 47 | Regex::new(r"(?P[^=]+)=(?P[^ ]+) (?P[^ \n]+).*\n") 48 | .expect("Failed to compile Sensors regex"); 49 | } 50 | 51 | let output = command_output("sysctl", sensors)?; 52 | let values = RE 53 | .captures_iter(&output) 54 | .map(|mat| { 55 | let value = mat 56 | .name("value") 57 | .ok_or_else(|| anyhow!("Missing value in Sensors output"))?; 58 | let units = mat 59 | .name("units") 60 | .ok_or_else(|| anyhow!("Missing units in Sensors output"))?; 61 | 62 | let value = self.parse_value(value.as_str())?; 63 | let units = self.parse_units(units.as_str()); 64 | 65 | Ok(Value { value, units }) 66 | }) 67 | .collect::>()?; 68 | 69 | Ok(values) 70 | } 71 | } 72 | 73 | // TODO: Use config flag when we re-implement Linux version. 74 | type SensorsInfo = OpenBsd; 75 | 76 | /// Shows the value from one or more hardware sensors. 77 | /// 78 | /// On Linux, this shows the temperature reported by one or more sensors from the 79 | /// output of the `sensors` command, which is part of the [`lm_sensors`] 80 | /// package. It expects the `sensors` executable to be available in the `PATH`. 81 | /// 82 | /// On OpenBSD, this shows the values reported by one or more sensors available 83 | /// through [`sysctl`]. 84 | /// 85 | /// [`lm_sensors`]: https://wiki.archlinux.org/index.php/lm_sensors 86 | /// [`sysctl`]: https://man.openbsd.org/sysctl.8 87 | pub struct Sensors { 88 | update_interval: Duration, 89 | attr: Attributes, 90 | sensors: Vec, 91 | info: SensorsInfo, 92 | } 93 | 94 | impl Sensors { 95 | /// Creates a new Sensors widget. 96 | /// 97 | /// A list of sensor names should be passed as the `sensors` argument. 98 | pub fn new>(attr: Attributes, sensors: Vec) -> Sensors { 99 | let sensors = sensors.into_iter().map(Into::into).collect(); 100 | Sensors { 101 | update_interval: Duration::from_secs(60), 102 | attr, 103 | sensors, 104 | info: SensorsInfo::default(), 105 | } 106 | } 107 | 108 | fn tick(&self) -> Result> { 109 | let values = self 110 | .info 111 | .load_values(&self.sensors) 112 | .context("Failed to get sensor information")?; 113 | 114 | let texts = values 115 | .into_iter() 116 | .map(|Value { value, units }| { 117 | let text = format!("{}{}", value, units); 118 | Text { 119 | attr: self.attr.clone(), 120 | text, 121 | stretch: false, 122 | markup: false, 123 | } 124 | }) 125 | .collect(); 126 | 127 | Ok(texts) 128 | } 129 | } 130 | 131 | impl Widget for Sensors { 132 | fn into_stream(self: Box) -> Result { 133 | let interval = time::interval(self.update_interval); 134 | let stream = IntervalStream::new(interval).map(move |_| self.tick()); 135 | 136 | Ok(Box::pin(stream)) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/sensors/sensors_linux.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | #[cfg(target_os = "linux")] 3 | use cnx::text::{Attributes, Text}; 4 | use cnx::widgets::{Widget, WidgetStream}; 5 | use regex::Regex; 6 | use std::collections::HashMap; 7 | use std::process::Command; 8 | use std::time::Duration; 9 | use tokio::time; 10 | use tokio_stream::wrappers::IntervalStream; 11 | use tokio_stream::StreamExt; 12 | 13 | #[derive(Debug, PartialEq)] 14 | struct Value<'a> { 15 | temp: &'a str, 16 | units: &'a str, 17 | } 18 | 19 | /// Parses the output of the `sensors` executable from `lm_sensors`. 20 | fn parse_sensors_output(output: &str) -> Result>> { 21 | let re: Regex = Regex::new( 22 | // Note: we ignore + but capture - 23 | r"\n(?P[\w ]+):\s+\+?(?P-?\d+\.\d+).(?P[C|F])", 24 | ) 25 | .map_err(|_| anyhow!("Failed to compile regex for parsing sensors output"))?; 26 | 27 | let mut map = HashMap::new(); 28 | for mat in re.captures_iter(output) { 29 | // These .unwraps() are harmless. If we have a match, we have these groups. 30 | map.insert( 31 | mat.name("name").unwrap().as_str(), 32 | Value { 33 | temp: mat.name("temp").unwrap().as_str(), 34 | units: mat.name("units").unwrap().as_str(), 35 | }, 36 | ); 37 | } 38 | 39 | Ok(map) 40 | } 41 | 42 | /// Shows the temperature from one or more sensors. 43 | /// 44 | /// This widget shows the temperature reported by one or more sensors from the 45 | /// output of the `sensors` command, which is part of the [`lm_sensors`] 46 | /// package. 47 | /// 48 | /// It expects the `sensors` executable to be available in the `PATH`. 49 | /// 50 | /// [`lm_sensors`]: https://wiki.archlinux.org/index.php/lm_sensors 51 | pub struct Sensors { 52 | update_interval: Duration, 53 | attr: Attributes, 54 | sensors: Vec, 55 | } 56 | 57 | impl Sensors { 58 | /// Creates a new Sensors widget. 59 | /// 60 | /// Creates a new `Sensors` widget, whose text will be displayed with the 61 | /// given [`Attributes`]. 62 | /// 63 | /// A list of sensor names should be passed as the `sensors` argument. (You 64 | /// can discover the names by running the `sensors` utility in a terminal). 65 | /// 66 | /// The [`cnx::Cnx`] instance is borrowed during construction in order to get 67 | /// access to handles of its event loop. However, it is not borrowed for the 68 | /// lifetime of the widget. See the [`cnx::Cnx::add_widget`] for more discussion 69 | /// about the lifetime of the borrow. 70 | /// 71 | /// # Examples 72 | /// 73 | /// ``` 74 | /// # #[macro_use] 75 | /// # extern crate cnx; 76 | /// # 77 | /// # use cnx::*; 78 | /// # use cnx::text::*; 79 | /// # use cnx::widgets::*; 80 | /// # use cnx_contrib::widgets::sensors::*; 81 | /// # use anyhow::Result; 82 | /// # 83 | /// # fn run() -> Result<()> { 84 | /// let attr = Attributes { 85 | /// font: Font::new("SourceCodePro 21"), 86 | /// fg_color: Color::white(), 87 | /// bg_color: None, 88 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 89 | /// }; 90 | /// 91 | /// let mut cnx = Cnx::new(Position::Top); 92 | /// cnx.add_widget( 93 | /// Sensors::new(attr.clone(), vec!["Core 0", "Core 1"]) 94 | /// ); 95 | /// # Ok(()) 96 | /// # } 97 | /// # fn main() { run().unwrap(); } 98 | /// ``` 99 | pub fn new>(attr: Attributes, sensors: Vec) -> Sensors { 100 | Sensors { 101 | update_interval: Duration::from_secs(60), 102 | attr, 103 | sensors: sensors.into_iter().map(Into::into).collect(), 104 | } 105 | } 106 | 107 | fn tick(&self) -> Result> { 108 | let output = Command::new("sensors") 109 | .output() 110 | .context("Failed to run `sensors`")?; 111 | let string = String::from_utf8(output.stdout).context("Invalid UTF-8 in sensors output")?; 112 | let parsed = parse_sensors_output(&string).context("Failed to parse `sensors` output")?; 113 | self.sensors 114 | .iter() 115 | .map(|sensor_name| { 116 | let text = parsed 117 | .get::(sensor_name) 118 | .map_or("Invalid".to_owned(), |&Value { temp, units }| { 119 | format!("{temp}°{units}") 120 | }); 121 | Ok(Text { 122 | attr: self.attr.clone(), 123 | text, 124 | stretch: false, 125 | markup: false, 126 | }) 127 | }) 128 | .collect() 129 | } 130 | } 131 | 132 | impl Widget for Sensors { 133 | fn into_stream(self: Box) -> Result { 134 | let interval = time::interval(self.update_interval); 135 | let stream = IntervalStream::new(interval).map(move |_| self.tick()); 136 | 137 | Ok(Box::pin(stream)) 138 | } 139 | } 140 | 141 | #[cfg(test)] 142 | mod test { 143 | use super::{parse_sensors_output, Value}; 144 | 145 | #[test] 146 | fn works() { 147 | let output = r#"applesmc-isa-0300 148 | Adapter: ISA adapter 149 | Right Side : 0 RPM (min = 2000 RPM, max = 6199 RPM) 150 | Ts1S: -127.0 C 151 | Ts2S: +34.0 F 152 | 153 | coretemp-isa-0000 154 | Adapter: ISA adapter 155 | Package id 0: +58.0 C (high = +105.0 C, crit = +105.0 C) 156 | Core 0: +53.0 C (high = +105.0 C, crit = +105.0 C) 157 | Core 1: +58.0 C (high = +105.0 C, crit = +105.0 C) 158 | "#; 159 | 160 | let parsed = parse_sensors_output(output).unwrap(); 161 | assert_eq!( 162 | parsed.get("Core 0"), 163 | Some(&Value { 164 | temp: "53.0", 165 | units: "C", 166 | }) 167 | ); 168 | assert_eq!( 169 | parsed.get("Core 1"), 170 | Some(&Value { 171 | temp: "58.0", 172 | units: "C", 173 | }) 174 | ); 175 | assert_eq!( 176 | parsed.get("Ts1S"), 177 | Some(&Value { 178 | temp: "-127.0", 179 | units: "C", 180 | }) 181 | ); 182 | assert_eq!( 183 | parsed.get("Ts2S"), 184 | Some(&Value { 185 | temp: "34.0", 186 | units: "F", 187 | }) 188 | ); 189 | 190 | assert_eq!(parsed.len(), 5); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/volume.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "openbsd")] 2 | #[cfg(feature = "volume")] 3 | mod volume_bsd; 4 | #[cfg(target_os = "linux")] 5 | #[cfg(feature = "volume")] 6 | mod volume_linux; 7 | #[cfg(target_os = "openbsd")] 8 | #[cfg(feature = "volume")] 9 | pub use volume_bsd::Volume; 10 | #[cfg(target_os = "linux")] 11 | #[cfg(feature = "volume")] 12 | pub use volume_linux::Volume; 13 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/volume/volume_bsd.rs: -------------------------------------------------------------------------------- 1 | use std::u8; 2 | 3 | use anyhow::Result; 4 | use async_stream::stream; 5 | use sioctl::Sioctl; 6 | use tokio::stream::{self, Stream, StreamExt}; 7 | use tokio::sync::mpsc; 8 | 9 | use crate::text::{Attributes, Text}; 10 | use crate::widgets::{Widget, WidgetStream}; 11 | 12 | #[derive(Copy, Clone, Debug, PartialEq)] 13 | enum State { 14 | Unknown, 15 | Muted, 16 | Unmuted { percentage: f32 }, 17 | } 18 | 19 | struct OpenBsd; 20 | 21 | impl OpenBsd { 22 | fn new() -> Self { 23 | Self 24 | } 25 | 26 | fn stream(self) -> impl Stream { 27 | // Grab initial state before starting to watch for changes. 28 | let sioctl = Sioctl::new(); 29 | let controls = sioctl.controls(); 30 | 31 | let (sender, receiver) = mpsc::unbounded_channel(); 32 | let watcher = sioctl.watch(move |control| { 33 | if let Err(error) = sender.send(control.clone()) { 34 | println!("Error sending sioctl message: {}", error); 35 | } 36 | }); 37 | 38 | let mut stream = stream::iter(controls).chain(receiver); 39 | stream! { 40 | // Move watcher into stream! {} to keep it alive. 41 | let watcher = watcher; 42 | let mut state = State::Unknown; 43 | let mut muted = false; 44 | let mut percentage = 1.0; 45 | 46 | loop { 47 | if let Some(control) = stream.next().await { 48 | let name = control.name.as_ref(); 49 | let func = control.func.as_ref(); 50 | let value = control.value; 51 | 52 | match (name, func, value) { 53 | ("output", "mute", 1) => muted = true, 54 | ("output", "mute", 0) => muted = false, 55 | ("output", "level", _) => percentage = self.percentage(value), 56 | _ => (), 57 | } 58 | 59 | let new = match (muted, percentage) { 60 | (true, _) => State::Muted, 61 | (_, percentage) => State::Unmuted { percentage }, 62 | }; 63 | 64 | if state != new { 65 | state = new; 66 | yield state; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | fn percentage(&self, value: u8) -> f32 { 74 | (f32::from(value) / f32::from(u8::MAX)) * 100.0 75 | } 76 | } 77 | 78 | type VolumeInfo = OpenBsd; 79 | 80 | pub struct Volume { 81 | attr: Attributes, 82 | } 83 | 84 | impl Volume { 85 | /// Creates a new Volume widget. 86 | pub fn new(attr: Attributes) -> Self { 87 | Self { attr } 88 | } 89 | 90 | fn on_change(&self, state: State) -> Result> { 91 | let text = match state { 92 | State::Unknown => "?".to_owned(), 93 | State::Muted => "M".to_owned(), 94 | State::Unmuted { percentage } => format!("{:.0}%", percentage), 95 | }; 96 | 97 | Ok(vec![Text { 98 | attr: self.attr.clone(), 99 | text, 100 | stretch: false, 101 | }]) 102 | } 103 | } 104 | 105 | impl Widget for Volume { 106 | fn into_stream(self: Box) -> Result { 107 | let info = VolumeInfo::new(); 108 | let stream = info.stream().map(move |state| self.on_change(state)); 109 | 110 | Ok(Box::pin(stream)) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/volume/volume_linux.rs: -------------------------------------------------------------------------------- 1 | use alsa::mixer::{SelemChannelId, SelemId}; 2 | use alsa::{self, Mixer, PollDescriptors}; 3 | use anyhow::{anyhow, Context, Result}; 4 | use cnx::text::{Attributes, Text}; 5 | use cnx::widgets::{Widget, WidgetStream}; 6 | use std::os::unix::io::AsRawFd; 7 | use std::os::unix::io::RawFd; 8 | use std::pin::Pin; 9 | use std::task::Poll; 10 | use tokio::io::unix::AsyncFd; 11 | use tokio_stream::{Stream, StreamExt}; 12 | 13 | /// Shows the current volume of the default ALSA output. 14 | /// 15 | /// This widget shows the current volume of the default ALSA output, or '`M`' if 16 | /// the output is muted. 17 | /// 18 | /// The widget uses `alsa-lib` to receive events when the volume changes, 19 | /// avoiding expensive polling. If you do not have `alsa-lib` installed, you 20 | /// can disable the `volume-widget` feature on the `cnx` crate to avoid 21 | /// compiling this widget. 22 | pub struct Volume { 23 | attr: Attributes, 24 | } 25 | 26 | impl Volume { 27 | /// Creates a new Volume widget. 28 | /// 29 | /// Creates a new `Volume` widget, whose text will be displayed 30 | /// with the given [`Attributes`]. 31 | /// 32 | /// The [`Cnx`] instance is borrowed during construction in order to get 33 | /// access to handles of its event loop. However, it is not borrowed for the 34 | /// lifetime of the widget. See the [`cnx_add_widget!()`] for more discussion 35 | /// about the lifetime of the borrow. 36 | /// 37 | /// [`Attributes`]: ../text/struct.Attributes.html 38 | /// [`Cnx`]: ../struct.Cnx.html 39 | /// [`cnx_add_widget!()`]: ../macro.cnx_add_widget.html 40 | /// 41 | /// # Examples 42 | /// 43 | /// ``` 44 | /// # #[macro_use] 45 | /// # extern crate cnx; 46 | /// # 47 | /// # use cnx::*; 48 | /// # use cnx::text::*; 49 | /// # use cnx::widgets::*; 50 | /// # use cnx_contrib::widgets::*; 51 | /// # use anyhow::Result; 52 | /// # 53 | /// # fn run() -> Result<()> { 54 | /// let attr = Attributes { 55 | /// font: Font::new("SourceCodePro 21"), 56 | /// fg_color: Color::white(), 57 | /// bg_color: None, 58 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 59 | /// }; 60 | /// 61 | /// let mut cnx = Cnx::new(Position::Top); 62 | /// cnx.add_widget(volume::Volume::new(attr.clone())); 63 | /// # Ok(()) 64 | /// # } 65 | /// # fn main() { run().unwrap(); } 66 | /// ``` 67 | pub fn new(attr: Attributes) -> Volume { 68 | Volume { attr } 69 | } 70 | } 71 | 72 | // https://github.com/mjkillough/cnx/blob/92c24238be541c75d88181208862505739be33fd/src/widgets/volume.rs 73 | 74 | impl Widget for Volume { 75 | fn into_stream(self: Box) -> Result { 76 | let mixer_name = "default"; 77 | // We don't attempt to use the same mixer to listen for events and to 78 | // recompute the mixer state (in the callback below) as the Mixer seems 79 | // to cache the state from when it was created. It's relatively cheap 80 | // create a new mixer each time we get an event though. 81 | let mixer = Mixer::new(mixer_name, true) 82 | .with_context(|| format!("Failed to open ALSA mixer: {mixer_name}"))?; 83 | let stream = AlsaEventStream::new(mixer)?.map(move |()| { 84 | // FrontLeft has special meaning in ALSA and is the channel 85 | // that's used when the mixer is mono. 86 | let channel = SelemChannelId::FrontLeft; 87 | 88 | let mixer = Mixer::new(mixer_name, true)?; 89 | let master = mixer.find_selem(&SelemId::new("Master", 0)) 90 | .ok_or_else(|| anyhow!("Couldn't open Master channel"))?; 91 | 92 | let mute = master.get_playback_switch(channel)? == 0; 93 | 94 | let text = if !mute { 95 | let volume = master.get_playback_volume(channel)?; 96 | let (min, max) = master.get_playback_volume_range(); 97 | let percentage = (volume as f64 / (max as f64 - min as f64)) * 100.0; 98 | format!("[🔈 {percentage:.0}%]") 99 | } else { 100 | "🔇".to_owned() 101 | }; 102 | 103 | Ok(vec![Text { 104 | attr: self.attr.clone(), 105 | text, 106 | stretch: false, 107 | markup: true, 108 | }]) 109 | }); 110 | 111 | Ok(Box::pin(stream)) 112 | } 113 | } 114 | 115 | struct AlsaEvented(Mixer); 116 | 117 | impl AlsaEvented { 118 | fn mixer(&self) -> &Mixer { 119 | &self.0 120 | } 121 | 122 | fn fds(&self) -> Vec { 123 | self.0.get().map_or(vec![], |vec_poll| { 124 | vec_poll.iter().map(|pollfd| pollfd.fd).collect() 125 | }) 126 | } 127 | } 128 | 129 | struct AlsaEventStream { 130 | poll: AsyncFd, 131 | initial: bool, 132 | } 133 | 134 | impl AsRawFd for AlsaEvented { 135 | fn as_raw_fd(&self) -> RawFd { 136 | self.fds() 137 | .into_iter() 138 | .next() 139 | .expect("volume: as_raw_fd empty") 140 | } 141 | } 142 | 143 | impl AlsaEventStream { 144 | fn new(mixer: Mixer) -> Result { 145 | Ok(AlsaEventStream { 146 | poll: AsyncFd::new(AlsaEvented(mixer))?, 147 | // The first few calls to poll() need to process any existing events. 148 | // We don't know what state the fds are in when we give them to tokio 149 | // and it's edge-triggered. 150 | initial: true, 151 | }) 152 | } 153 | } 154 | 155 | impl Stream for AlsaEventStream { 156 | // We don't bother yielding the events and just yield unit each time we get 157 | // an event. This stream is used only to get woken up when the ALSA state 158 | // changes - the caller is expected to requery all necessary state when 159 | // it receives a new item from the stream. 160 | type Item = (); 161 | 162 | fn poll_next( 163 | mut self: Pin<&mut Self>, 164 | cx: &mut std::task::Context, 165 | ) -> Poll> { 166 | // Always assume we're ready initially, so that we can clear the 167 | // state of the fds. 168 | 169 | // Do a poll with a timeout of 0 to figure out exactly which fds were 170 | // woken up, followed by a call to revents() which clears the pending 171 | // events. We don't actually care what the events are - we're just 172 | // using it as a wake-up so we can check the volume again. 173 | if self.initial { 174 | let mixer = self.poll.get_ref().mixer(); 175 | let _poll_result = alsa::poll::poll_all(&[mixer], 0); 176 | self.initial = false; 177 | return Poll::Ready(Some(())); 178 | } 179 | // All events have been consumed - tell Tokio we're interested in waiting 180 | // for more again. 181 | match self.poll.poll_read_ready(cx) { 182 | Poll::Ready(Ok(mut r)) => { 183 | let mixer = self.poll.get_ref().mixer(); 184 | let _poll_result = alsa::poll::poll_all(&[mixer], 0); 185 | let _result = mixer.handle_events(); 186 | r.clear_ready(); 187 | Poll::Ready(Some(())) 188 | } 189 | Poll::Ready(Err(_)) => Poll::Ready(None), 190 | Poll::Pending => Poll::Pending, 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/weather.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_stream::try_stream; 3 | use cnx::text::{Attributes, Text}; 4 | use cnx::widgets::{Widget, WidgetStream}; 5 | use std::time::Duration; 6 | use weathernoaa::weather::*; 7 | 8 | /// Represents Weather widget used to show current weather information. 9 | pub struct Weather { 10 | attr: Attributes, 11 | station_code: String, 12 | render: Option String>>, 13 | } 14 | 15 | impl Weather { 16 | /// Creates a new [`Weather`] widget. 17 | /// 18 | /// Arguments 19 | /// 20 | /// * `attr` - Represents `Attributes` which controls properties like 21 | /// `Font`, foreground and background color etc. 22 | /// 23 | /// * `station_code` - Represents weather station code from the 24 | /// Federal Climate Complex ISD. You can find your place's station 25 | /// code by getting the information from either [NOAA's 26 | /// archive](https://www1.ncdc.noaa.gov/pub/data/noaa/isd-history.txt) 27 | /// or [Internet Archive's 28 | /// data](https://web.archive.org/web/20210522235412/https://www1.ncdc.noaa.gov/pub/data/noaa/isd-history.txt) 29 | /// of the same link. 30 | /// 31 | /// * `render` - We use the closure to control the way output is 32 | /// displayed in the bar. [`WeatherInfo`] represents the current 33 | /// weather details of the particular station. 34 | /// 35 | /// # Examples 36 | /// 37 | /// ``` 38 | /// # #[macro_use] 39 | /// # extern crate cnx; 40 | /// # 41 | /// # use cnx::*; 42 | /// # use cnx::text::*; 43 | /// # use cnx_contrib::widgets::weather::*; 44 | /// # use anyhow::Result; 45 | /// # 46 | /// # fn run() -> Result<()> { 47 | /// let attr = Attributes { 48 | /// font: Font::new("SourceCodePro 21"), 49 | /// fg_color: Color::white(), 50 | /// bg_color: None, 51 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 52 | /// }; 53 | /// 54 | /// let mut cnx = Cnx::new(Position::Top); 55 | /// cnx.add_widget(Weather::new(attr, "VOBL".into(), None)); 56 | /// # Ok(()) 57 | /// # } 58 | /// # fn main() { run().unwrap(); } 59 | /// ``` 60 | pub fn new( 61 | attr: Attributes, 62 | station_code: String, 63 | render: Option String>>, 64 | ) -> Weather { 65 | Weather { 66 | attr, 67 | station_code, 68 | render, 69 | } 70 | } 71 | } 72 | 73 | impl Widget for Weather { 74 | fn into_stream(self: Box) -> Result { 75 | let stream = try_stream! { 76 | loop { 77 | let weather = get_weather(self.station_code.clone()).await?; 78 | let text = self.render.as_ref().map_or(format!("Temp: {}°C", weather.temperature.celsius), |x| (x)(weather)); 79 | let texts = vec![Text { 80 | attr: self.attr.clone(), 81 | text, 82 | stretch: false, 83 | markup: true, 84 | }]; 85 | yield texts; 86 | 87 | let thirty_minutes = 30 * 60; 88 | let sleep_for = Duration::from_secs(thirty_minutes); 89 | tokio::time::sleep(sleep_for).await; 90 | } 91 | }; 92 | Ok(Box::pin(stream)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cnx-contrib/src/widgets/wireless.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cnx::text::{Attributes, Text, Threshold}; 3 | use cnx::widgets::{Widget, WidgetStream}; 4 | use iwlib::*; 5 | use std::time::Duration; 6 | use tokio::time; 7 | use tokio_stream::wrappers::IntervalStream; 8 | use tokio_stream::StreamExt; 9 | 10 | /// Wireless widget to show wireless information for a particular ESSID 11 | pub struct Wireless { 12 | attr: Attributes, 13 | interface: String, 14 | update_interval: Duration, 15 | threshold: Option, 16 | } 17 | 18 | impl Wireless { 19 | /// Creates a new [`Wireless`] widget. 20 | /// 21 | /// Arguments 22 | /// 23 | /// * `attr` - Represents `Attributes` which controls properties like 24 | /// `Font`, foreground and background color etc. 25 | /// 26 | /// * `interface` - String representing the name name of the network 27 | /// interface for your wireless hardware. In Linux systems, you can 28 | /// find that out using `iw dev` command. 29 | /// 30 | /// * `threshold` - Represents threshold values to determine if 31 | /// the wireless strength is low, normal or high. 32 | /// 33 | /// # Examples 34 | /// 35 | /// ``` 36 | /// # #[macro_use] 37 | /// # extern crate cnx; 38 | /// # 39 | /// # use cnx::*; 40 | /// # use cnx::text::*; 41 | /// # use cnx_contrib::widgets::wireless::*; 42 | /// # use anyhow::Result; 43 | /// # 44 | /// # fn run() -> Result<()> { 45 | /// let attr = Attributes { 46 | /// font: Font::new("SourceCodePro 21"), 47 | /// fg_color: Color::white(), 48 | /// bg_color: None, 49 | /// padding: Padding::new(8.0, 8.0, 0.0, 0.0), 50 | /// }; 51 | /// 52 | /// let mut cnx = Cnx::new(Position::Top); 53 | /// cnx.add_widget(Wireless::new(attr, "wlp2s0".into(), None)); 54 | /// # Ok(()) 55 | /// # } 56 | /// # fn main() { run().unwrap(); } 57 | /// ``` 58 | pub fn new(attr: Attributes, interface: String, threshold: Option) -> Wireless { 59 | Wireless { 60 | update_interval: Duration::from_secs(3600), 61 | interface, 62 | attr, 63 | threshold, 64 | } 65 | } 66 | 67 | fn tick(&self) -> Vec { 68 | let wireless_info = get_wireless_info(self.interface.clone()); 69 | 70 | let text = match wireless_info { 71 | Some(info) => match &self.threshold { 72 | Some(thold) => { 73 | let color = if info.wi_quality <= thold.low.threshold { 74 | &thold.low.color 75 | } else if info.wi_quality <= thold.normal.threshold { 76 | &thold.normal.color 77 | } else { 78 | &thold.high.color 79 | }; 80 | format!( 81 | "[{} {}%]", 82 | info.wi_essid, 83 | color.to_hex(), 84 | info.wi_quality 85 | ) 86 | } 87 | None => format!("{} {}%", info.wi_essid, info.wi_quality), 88 | }, 89 | None => "NA".to_owned(), 90 | }; 91 | vec![Text { 92 | attr: self.attr.clone(), 93 | text, 94 | stretch: false, 95 | markup: self.threshold.is_some(), 96 | }] 97 | } 98 | } 99 | 100 | impl Widget for Wireless { 101 | fn into_stream(self: Box) -> Result { 102 | let interval = time::interval(self.update_interval); 103 | let stream = IntervalStream::new(interval).map(move |_| Ok(self.tick())); 104 | 105 | Ok(Box::pin(stream)) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cnx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cnx" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Michael Killough ", "Sibi Prabakaran ) -> xcb::Visualtype { 13 | for root in conn.get_setup().roots() { 14 | for allowed_depth in root.allowed_depths() { 15 | for visual in allowed_depth.visuals() { 16 | if visual.visual_id() == screen.root_visual() { 17 | return visual; 18 | } 19 | } 20 | } 21 | } 22 | panic!("No visual type found"); 23 | } 24 | 25 | /// Creates a `cairo::Surface` for the XCB window with the given `id`. 26 | fn cairo_surface_for_xcb_window( 27 | conn: &xcb::Connection, 28 | screen: &xcb::Screen<'_>, 29 | id: u32, 30 | width: i32, 31 | height: i32, 32 | ) -> Result { 33 | let cairo_conn = unsafe { 34 | cairo::XCBConnection::from_raw_none(conn.get_raw_conn() as *mut cairo_sys::xcb_connection_t) 35 | }; 36 | let visual = unsafe { 37 | cairo::XCBVisualType::from_raw_none( 38 | &mut get_root_visual_type(conn, screen).base as *mut xcb::ffi::xcb_visualtype_t 39 | as *mut cairo_sys::xcb_visualtype_t, 40 | ) 41 | }; 42 | let drawable = cairo::XCBDrawable(id); 43 | let surface = cairo::XCBSurface::create(&cairo_conn, &drawable, &visual, width, height) 44 | .map_err(|status| anyhow!("XCBSurface::create: {}", status))?; 45 | Ok(surface) 46 | } 47 | 48 | fn create_surface( 49 | conn: &xcb::Connection, 50 | screen_idx: usize, 51 | window_id: u32, 52 | height: u16, 53 | width: Option, 54 | offset: Offset, 55 | ) -> Result<(u16, cairo::XCBSurface)> { 56 | let screen = conn 57 | .get_setup() 58 | .roots() 59 | .nth(screen_idx) 60 | .ok_or_else(|| anyhow!("Invalid screen"))?; 61 | let values = [ 62 | (xcb::CW_BACK_PIXEL, screen.black_pixel()), 63 | (xcb::CW_EVENT_MASK, xcb::EVENT_MASK_EXPOSURE), 64 | ]; 65 | 66 | let width = width.unwrap_or_else(|| screen.width_in_pixels()); 67 | 68 | xcb::create_window( 69 | conn, 70 | xcb::COPY_FROM_PARENT as u8, 71 | window_id, 72 | screen.root(), 73 | offset.x, 74 | offset.y, 75 | width, 76 | height, 77 | 0, 78 | xcb::WINDOW_CLASS_INPUT_OUTPUT as u16, 79 | screen.root_visual(), 80 | &values, 81 | ); 82 | 83 | let surface = cairo_surface_for_xcb_window( 84 | conn, 85 | &screen, 86 | window_id, 87 | i32::from(width), 88 | i32::from(height), 89 | )?; 90 | 91 | Ok((width, surface)) 92 | } 93 | 94 | /// An enum specifying the position of the Cnx bar. 95 | /// 96 | /// Passed to [`Cnx::new()`] when constructing a [`Cnx`] instance. 97 | /// 98 | /// [`Cnx::new()`]: struct.Cnx.html#method.new 99 | /// [`Cnx`]: struct.Cnx.html 100 | /// 101 | /// # Examples 102 | /// 103 | /// ``` 104 | /// # use cnx::{Cnx, Position}; 105 | /// let mut cnx = Cnx::new(Position::Top); 106 | /// ``` 107 | #[derive(Clone, Debug)] 108 | pub enum Position { 109 | /// Position the Cnx bar at the top of the screen. 110 | Top, 111 | /// Position the Cnx bar at the bottom of the screen. 112 | Bottom, 113 | } 114 | 115 | /// A struct specifying the `x` and `y` offset 116 | #[derive(Default, Clone, Copy)] 117 | pub struct Offset { 118 | pub x: i16, 119 | pub y: i16, 120 | } 121 | 122 | pub struct Bar { 123 | position: Position, 124 | 125 | conn: Rc, 126 | screen_idx: usize, 127 | window_id: u32, 128 | 129 | surface: cairo::XCBSurface, 130 | width: u16, 131 | height: u16, 132 | offset: Offset, 133 | 134 | contents: Vec>, 135 | } 136 | 137 | impl Bar { 138 | pub fn new(position: Position, width: Option, offset: Offset) -> Result { 139 | let (conn, screen_idx) = 140 | xcb::Connection::connect(None).context("Failed to connect to X server")?; 141 | let screen_idx = screen_idx as usize; 142 | let window_id = conn.generate_id(); 143 | 144 | // We don't actually care about how tall our initial window is - we'll resize 145 | // our window once we know how big it needs to be. However, it seems to need 146 | // to be bigger than 0px, or either Xcb/Cairo (or maybe QTile?) gets upset. 147 | let height = 1; 148 | let (width, surface) = create_surface(&conn, screen_idx, window_id, height, width, offset)?; 149 | 150 | let ewmh_conn = ewmh::Connection::connect(conn) 151 | .map_err(|(e, _)| e) 152 | .context("Failed to wrap xcb::Connection in ewmh::Connection")?; 153 | 154 | let bar = Bar { 155 | conn: Rc::new(ewmh_conn), 156 | window_id, 157 | screen_idx, 158 | surface, 159 | width, 160 | height, 161 | offset, 162 | position, 163 | contents: Vec::new(), 164 | }; 165 | bar.set_ewmh_properties(); 166 | 167 | // XXX We can't map the window until we've updated the window size, or nothing 168 | // gets rendered. I can't tell if this is something we're doing, something Cairo 169 | // is doing or something QTile is doing. This'll do for now and we'll see what 170 | // it is like with Lanta! 171 | // bar.map_window(); 172 | bar.flush(); 173 | 174 | Ok(bar) 175 | } 176 | 177 | fn flush(&self) { 178 | self.conn.flush(); 179 | } 180 | 181 | fn map_window(&self) { 182 | xcb::map_window(&self.conn, self.window_id); 183 | } 184 | 185 | fn set_ewmh_properties(&self) { 186 | ewmh::set_wm_window_type( 187 | &self.conn, 188 | self.window_id, 189 | &[self.conn.WM_WINDOW_TYPE_DOCK()], 190 | ); 191 | 192 | // TODO: Update _WM_STRUT_PARTIAL if the height/position of the bar changes? 193 | let mut strut_partial = ewmh::StrutPartial { 194 | left: 0, 195 | right: 0, 196 | top: 0, 197 | bottom: 0, 198 | left_start_y: 0, 199 | left_end_y: 0, 200 | right_start_y: 0, 201 | right_end_y: 0, 202 | top_start_x: 0, 203 | top_end_x: 0, 204 | bottom_start_x: 0, 205 | bottom_end_x: 0, 206 | }; 207 | match self.position { 208 | Position::Top => strut_partial.top = u32::from(self.height), 209 | Position::Bottom => strut_partial.bottom = u32::from(self.height), 210 | } 211 | ewmh::set_wm_strut_partial(&self.conn, self.window_id, strut_partial); 212 | } 213 | 214 | fn screen(&self) -> Result> { 215 | let screen = self 216 | .conn 217 | .get_setup() 218 | .roots() 219 | .nth(self.screen_idx) 220 | .ok_or_else(|| anyhow!("Invalid screen"))?; 221 | Ok(screen) 222 | } 223 | 224 | fn update_bar_height(&mut self, height: u16) -> Result<()> { 225 | if self.height != height { 226 | self.height = height; 227 | 228 | // If we're at the bottom of the screen, we'll need to update the 229 | // position of the window. 230 | let y = match self.position { 231 | Position::Top => self.offset.y.max(0) as u16, 232 | Position::Bottom => { 233 | let h = (self.screen()?.height_in_pixels() - self.height) as i32; 234 | h.checked_add(self.offset.y as i32).unwrap_or(h).max(0) as u16 235 | } 236 | }; 237 | 238 | // Update the height/position of the XCB window and the height of the Cairo surface. 239 | let values = [ 240 | (xcb::CONFIG_WINDOW_Y as u16, u32::from(y)), 241 | (xcb::CONFIG_WINDOW_HEIGHT as u16, u32::from(self.height)), 242 | (xcb::CONFIG_WINDOW_STACK_MODE as u16, xcb::STACK_MODE_ABOVE), 243 | ]; 244 | xcb::configure_window(&self.conn, self.window_id, &values); 245 | self.map_window(); 246 | self.surface 247 | .set_size(i32::from(self.width), i32::from(self.height)) 248 | .unwrap(); 249 | 250 | // Update EWMH properties - we might need to reserve more or less space. 251 | self.set_ewmh_properties(); 252 | } 253 | 254 | Ok(()) 255 | } 256 | 257 | // Returns the connection to the X server. 258 | // 259 | // The owner of the `Bar` is responsible for polling this for events, 260 | // passing each to `Bar::process_event()`. 261 | pub fn connection(&self) -> &Rc { 262 | &self.conn 263 | } 264 | 265 | // Process an X event received from the `Bar::connection()`. 266 | pub fn process_event(&mut self, event: xcb::GenericEvent) -> Result<()> { 267 | let expose = event.response_type() & !0x80 == xcb::EXPOSE; 268 | if expose { 269 | println!("Redrawing entire bar - expose event."); 270 | self.redraw_entire_bar()?; 271 | } 272 | Ok(()) 273 | } 274 | 275 | // Add a new widget's content to the `Bar`. 276 | // 277 | // Returns the index of the widget within the bar, so that subsequent 278 | // updates can be made by calling `Bar::update_content()`. 279 | pub fn add_content(&mut self, content: Vec) -> Result { 280 | let idx = self.contents.len(); 281 | self.contents.push(Vec::new()); 282 | self.update_content(idx, content)?; 283 | Ok(idx) 284 | } 285 | 286 | // Updates an existing widget's content in the `Bar`. 287 | pub fn update_content(&mut self, idx: usize, content: Vec) -> Result<()> { 288 | // If the text is the same, don't bother re-computing the text or 289 | // redrawing it. This is a spurious wake-up. 290 | let old = &self.contents[idx]; 291 | if &content == old { 292 | return Ok(()); 293 | } 294 | 295 | let mut new = content 296 | .into_iter() 297 | .map(|text| text.compute(&self.surface)) 298 | .collect::>>()?; 299 | 300 | let error_margin = f64::EPSILON; // Use an epsilon for comparison 301 | 302 | // If there are any new texts or any non-stretch texts changed size, 303 | // we'll redraw all texts. 304 | let redraw_entire_bar = old.len() != new.len() 305 | || old 306 | .iter() 307 | .zip(&new) 308 | .any(|(old, new)| ((old.width - new.width).abs() < error_margin) && !new.stretch); 309 | 310 | // Steal dimenions from old ComputedText. If we need new dimensions, 311 | // they'll be recomputed by redraw_entire_bar(). 312 | for (new, old) in new.iter_mut().zip(old.iter()) { 313 | new.x = old.x; 314 | new.y = old.y; 315 | new.height = old.height; 316 | // Only use width for stretch widgets. 317 | if new.stretch { 318 | new.width = old.width; 319 | } 320 | } 321 | 322 | self.contents[idx] = new; 323 | 324 | if !redraw_entire_bar { 325 | println!("Redrawing one"); 326 | self.redraw_content(idx)?; 327 | } else { 328 | println!("Redrawing entire bar - widget update"); 329 | self.redraw_entire_bar()?; 330 | } 331 | 332 | Ok(()) 333 | } 334 | 335 | fn redraw_content(&mut self, idx: usize) -> Result<()> { 336 | for text in &mut self.contents[idx] { 337 | text.render(&self.surface)?; 338 | } 339 | 340 | self.flush(); 341 | 342 | Ok(()) 343 | } 344 | 345 | pub fn redraw_entire_bar(&mut self) -> Result<()> { 346 | self.recompute_dimensions()?; 347 | 348 | for idx in 0..self.contents.len() { 349 | self.redraw_content(idx)?; 350 | } 351 | Ok(()) 352 | } 353 | 354 | fn recompute_dimensions(&mut self) -> Result<()> { 355 | // Set the height to the max height of any content. 356 | let height = self 357 | .contents 358 | .iter() 359 | .flatten() 360 | .map(|text| text.height) 361 | .max_by_key(|height| OrderedFloat(*height)) 362 | .unwrap_or(0.0); 363 | for text in self.contents.iter_mut().flatten() { 364 | text.height = height; 365 | } 366 | self.update_bar_height(height as u16)?; 367 | 368 | // Sum the width of all non-stretch texts. Subtract from the screen 369 | // width to get width remaining for stretch texts. 370 | let used: f64 = self 371 | .contents 372 | .iter() 373 | .flatten() 374 | .filter(|text| !text.stretch) 375 | .map(|text| text.width) 376 | .sum(); 377 | let remaining = f64::from(self.width) - used; 378 | 379 | // Distribute remaining width evenly between stretch texts. 380 | let stretches_count = self 381 | .contents 382 | .iter() 383 | .flatten() 384 | .filter(|text| text.stretch) 385 | .count(); 386 | let stretch_width = remaining / (stretches_count as f64); 387 | let stretches = self 388 | .contents 389 | .iter_mut() 390 | .flatten() 391 | .filter(|text| text.stretch); 392 | for text in stretches { 393 | text.width = stretch_width; 394 | } 395 | 396 | // Set x based on computed widths. 397 | let mut x = 0.0; 398 | for text in self.contents.iter_mut().flatten() { 399 | text.x = x; 400 | x += text.width; 401 | } 402 | 403 | Ok(()) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /cnx/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A simple X11 status bar for use with simple WMs. 2 | //! 3 | //! Cnx is written to be customisable, simple and fast. Where possible, it 4 | //! prefers to asynchronously wait for changes in the underlying data sources 5 | //! (and uses [`tokio`] to achieve this), rather than periodically 6 | //! calling out to external programs. 7 | //! 8 | //! # How to use 9 | //! 10 | //! Cnx is a library that allows you to make your own status bar. 11 | //! 12 | //! In normal usage, you will create a new binary project that relies on the 13 | //! `cnx` crate, and customize it through options passed to the main [`Cnx`] 14 | //! object and its widgets. (It's inspired by [`QTile`] and [`dwm`], in that the 15 | //! configuration is done entirely in code, allowing greater extensibility 16 | //! without needing complex configuration handling). 17 | //! 18 | //! An simple example of a binary using Cnx is: 19 | //! 20 | //! ```no_run 21 | //! 22 | //! use cnx::text::*; 23 | //! use cnx::widgets::*; 24 | //! use cnx::{Cnx, Position}; 25 | //! use anyhow::Result; 26 | //! 27 | //! fn main() -> Result<()> { 28 | //! let attr = Attributes { 29 | //! font: Font::new("Envy Code R 21"), 30 | //! fg_color: Color::white(), 31 | //! bg_color: None, 32 | //! padding: Padding::new(8.0, 8.0, 0.0, 0.0), 33 | //! }; 34 | //! 35 | //! let mut cnx = Cnx::new(Position::Top); 36 | //! cnx.add_widget(ActiveWindowTitle::new(attr.clone())); 37 | //! cnx.add_widget(Clock::new(attr.clone(), None)); 38 | //! cnx.run()?; 39 | //! 40 | //! Ok(()) 41 | //! } 42 | //! ``` 43 | //! 44 | //! A more complex example is given in [`cnx-bin/src/main.rs`] alongside the project. 45 | //! (This is the default `[bin]` target for the crate, so you _could_ use it by 46 | //! either executing `cargo run` from the crate root, or even running `cargo 47 | //! install cnx; cnx`. However, neither of these are recommended as options for 48 | //! customizing Cnx are then limited). 49 | //! 50 | //! Before running Cnx, you'll need to make sure your system has the required 51 | //! dependencies, which are described in the [`README`][readme-deps]. 52 | //! 53 | //! # Built-in widgets 54 | //! 55 | //! There are currently these widgets available: 56 | //! 57 | //! - [`crate::widgets::ActiveWindowTitle`] — Shows the title ([`EWMH`]'s `_NET_WM_NAME`) for 58 | //! the currently focused window ([`EWMH`]'s `_NEW_ACTIVE_WINDOW`). 59 | //! - [`crate::widgets::Pager`] — Shows the WM's workspaces/groups, highlighting whichever is 60 | //! currently active. (Uses [`EWMH`]'s `_NET_DESKTOP_NAMES`, 61 | //! `_NET_NUMBER_OF_DESKTOPS` and `_NET_CURRENT_DESKTOP`). 62 | //! - [`crate::widgets::Clock`] — Shows the time. 63 | //! 64 | //! The cnx-contrib crate contains additional widgets: 65 | //! 66 | //! - **Sensors** — Periodically parses and displays the output of the 67 | //! sensors provided by the system. 68 | //! - **Volume** - Shows the current volume/mute status of the default output 69 | //! device. 70 | //! - **Battery** - Shows the remaining battery and charge status. 71 | //! - **Wireless** - Shows the wireless strength of your current network. 72 | //! - **CPU** - Shows the current CPU consumption 73 | //! - **Weather** - Shows the Weather information of your location 74 | //! - **Disk Usage** - Show the current usage of your monted filesystem 75 | //! - **LeftWM** - Shows the monitors and tags from LeftWM 76 | //! 77 | //! The Sensors, Volume and Battery widgets require platform 78 | //! support. They currently support Linux (see dependencies below) and OpenBSD. 79 | //! Support for additional platforms should be possible. 80 | //! 81 | //! # Dependencies 82 | //! 83 | //! In addition to the Rust dependencies in `Cargo.toml`, Cnx also depends on 84 | //! these system libraries: 85 | //! 86 | //! - `xcb-util`: `xcb-ewmh` / `xcb-icccm` / `xcb-keysyms` 87 | //! - `x11-xcb` 88 | //! - `pango` 89 | //! - `cairo` 90 | //! - `pangocairo` 91 | //! 92 | //! Some widgets have additional dependencies on Linux: 93 | //! 94 | //! - **Volume** widget relies on `alsa-lib` 95 | //! - **Sensors** widget relies on [`lm_sensors`] being installed. 96 | //! - **Wireless** widget relies on `libiw-dev`. 97 | //! 98 | //! # Creating new widgets 99 | //! 100 | //! Cnx is designed such that thirdparty widgets can be written in 101 | //! external crates and used with the main [`Cnx`] instance. We have 102 | //! [`cnx-contrib`] crate which contains various additional 103 | //! widgets. You can also create new crates or add it to the existing 104 | //! [`cnx-contrib`] crate. 105 | //! 106 | //! The built-in [`widgets`] should give you some examples on which to base 107 | //! your work. 108 | //! 109 | //! [`tokio`]: https://tokio.rs/ 110 | //! [`QTile`]: http://www.qtile.org/ 111 | //! [`dwm`]: http://dwm.suckless.org/ 112 | //! [readme-deps]: https://github.com/mjkillough/cnx/blob/master/README.md#dependencies 113 | //! [`cnx-bin/src/main.rs`]: https://github.com/mjkillough/cnx/blob/master/cnx-bin/src/main.rs 114 | //! [`EWMH`]: https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html 115 | //! [`lm_sensors`]: https://wiki.archlinux.org/index.php/lm_sensors 116 | //! [`cnx-contrib`]: https://github.com/mjkillough/cnx/tree/master/cnx-contrib 117 | 118 | #![recursion_limit = "256"] 119 | 120 | mod bar; 121 | pub mod text; 122 | pub mod widgets; 123 | mod xcb; 124 | 125 | use anyhow::Result; 126 | use tokio::runtime::Runtime; 127 | use tokio::task; 128 | use tokio_stream::{StreamExt, StreamMap}; 129 | 130 | use crate::bar::Bar; 131 | use crate::widgets::Widget; 132 | use crate::xcb::XcbEventStream; 133 | 134 | pub use bar::Offset; 135 | pub use bar::Position; 136 | 137 | /// The main object, used to instantiate an instance of Cnx. 138 | /// 139 | /// Widgets can be added using the [`add_widget()`] method. Once configured, 140 | /// the [`run()`] method will take ownership of the instance and run it until 141 | /// the process is killed or an error occurs. 142 | /// 143 | /// [`add_widget()`]: #method.add_widget 144 | /// [`run()`]: #method.run 145 | pub struct Cnx { 146 | /// The position of the Cnx bar 147 | position: Position, 148 | /// The list of widgets attached to the Cnx bar 149 | widgets: Vec>, 150 | /// The (x,y) offset of the bar 151 | /// It can be used in order to run multiple bars in a multi-monitor setup 152 | offset: Offset, 153 | /// The (optional) width of the bar 154 | /// It can be used in order to run multiple bars in a multi-monitor setup 155 | width: Option, 156 | } 157 | 158 | impl Cnx { 159 | /// Creates a new `Cnx` instance. 160 | /// 161 | /// This creates a new `Cnx` instance at either the top or bottom of the 162 | /// screen, depending on the value of the [`Position`] enum. 163 | /// 164 | /// [`Position`]: enum.Position.html 165 | pub fn new(position: Position) -> Self { 166 | let widgets = Vec::new(); 167 | Self { 168 | position, 169 | widgets, 170 | offset: Offset::default(), 171 | width: None, 172 | } 173 | } 174 | 175 | /// Returns a new instance of `Cnx` with the specified width. 176 | /// 177 | /// This allows to specify the width of the `Cnx` bar, 178 | /// which can be used with the [`with_offset()`] method 179 | /// in order to have a multiple bar setup. 180 | /// 181 | /// [`with_offset()`]: #method.with_offset 182 | pub fn with_width(self, width: Option) -> Self { 183 | Self { width, ..self } 184 | } 185 | 186 | /// Returns a new instance of `Cnx` with the specified offset. 187 | /// 188 | /// This allows to specify the x and y offset of the `Cnx` bar, 189 | /// which can be used with the [`with_width()`] method 190 | /// in order to have a multiple bar setup. 191 | /// 192 | /// [`with_width()`]: #method.with_width 193 | pub fn with_offset(self, x: i16, y: i16) -> Self { 194 | Self { 195 | offset: Offset { x, y }, 196 | ..self 197 | } 198 | } 199 | 200 | /// Adds a widget to the `Cnx` instance. 201 | /// 202 | /// Takes ownership of the [`Widget`] and adds it to the Cnx instance to 203 | /// the right of any existing widgets. 204 | /// 205 | /// [`Widget`]: widgets/trait.Widget.html 206 | pub fn add_widget(&mut self, widget: W) 207 | where 208 | W: Widget + 'static, 209 | { 210 | self.widgets.push(Box::new(widget)); 211 | } 212 | 213 | /// Runs the Cnx instance. 214 | /// 215 | /// This method takes ownership of the Cnx instance and runs it until either 216 | /// the process is terminated, or an internal error is returned. 217 | pub fn run(self) -> Result<()> { 218 | // Use a single-threaded event loop. We aren't interested in 219 | // performance too much, so don't mind if we block the loop 220 | // occasionally. We are using events to get woken up as 221 | // infrequently as possible (to save battery). 222 | let rt = Runtime::new()?; 223 | let local = task::LocalSet::new(); 224 | local.block_on(&rt, self.run_inner())?; 225 | Ok(()) 226 | } 227 | 228 | async fn run_inner(self) -> Result<()> { 229 | let mut bar = Bar::new(self.position, self.width, self.offset)?; 230 | 231 | let mut widgets = StreamMap::with_capacity(self.widgets.len()); 232 | for widget in self.widgets { 233 | let idx = bar.add_content(Vec::new())?; 234 | widgets.insert(idx, widget.into_stream()?); 235 | } 236 | 237 | let mut event_stream = XcbEventStream::new(bar.connection().clone())?; 238 | task::spawn_local(async move { 239 | loop { 240 | tokio::select! { 241 | // Pass each XCB event to the Bar. 242 | Some(event) = event_stream.next() => { 243 | if let Err(err) = bar.process_event(event) { 244 | println!("Error processing XCB event: {err}"); 245 | } 246 | }, 247 | 248 | // Each time a widget yields new values, pass to the bar. 249 | // Ignore (but log) any errors from widgets. 250 | Some((idx, result)) = widgets.next() => { 251 | match result { 252 | Err(err) => println!("Error from widget {idx}: {err}"), 253 | Ok(texts) => { 254 | if let Err(err) = bar.update_content(idx, texts) { 255 | println!("Error updating widget {idx}: {err}"); 256 | } 257 | } 258 | } 259 | } 260 | } 261 | } 262 | }) 263 | .await?; 264 | 265 | Ok(()) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /cnx/src/text.rs: -------------------------------------------------------------------------------- 1 | //! Types to represent text to be displayed by widgets. 2 | //! 3 | //! This module is light on documentation. See the existing widget 4 | //! implementations for inspiration. 5 | 6 | use anyhow::Result; 7 | use cairo::{Context, Surface}; 8 | use colors_transform::{Color as ColorTransform, Rgb}; 9 | use pango::{EllipsizeMode, FontDescription}; 10 | use std::fmt; 11 | 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub struct Color { 14 | red: f64, 15 | green: f64, 16 | blue: f64, 17 | } 18 | 19 | macro_rules! color { 20 | ($name:ident,($r:expr, $g:expr, $b:expr)) => { 21 | #[allow(dead_code)] 22 | pub fn $name() -> Color { 23 | Color { 24 | red: $r, 25 | green: $g, 26 | blue: $b, 27 | } 28 | } 29 | }; 30 | } 31 | 32 | impl Color { 33 | color!(red, (1.0, 0.0, 0.0)); 34 | color!(green, (0.0, 1.0, 0.0)); 35 | color!(blue, (0.0, 0.0, 1.0)); 36 | color!(white, (1.0, 1.0, 1.0)); 37 | color!(black, (0.0, 0.0, 0.0)); 38 | color!(yellow, (1.0, 1.0, 0.0)); 39 | 40 | pub fn apply_to_context(&self, cr: &Context) { 41 | cr.set_source_rgb(self.red, self.green, self.blue); 42 | } 43 | 44 | pub fn from_rgb(r: u8, g: u8, b: u8) -> Self { 45 | Self { 46 | red: r as f64 / 255.0, 47 | green: g as f64 / 255.0, 48 | blue: b as f64 / 255.0, 49 | } 50 | } 51 | 52 | /// Parse string as hex color 53 | /// # Example 54 | /// ``` 55 | /// use cnx::text::Color; 56 | /// 57 | /// assert_eq!(Color::from_hex("#1e1e2e"), Color::from_rgb(30, 30, 46)); 58 | /// assert_eq!(Color::from_hex("not hex"), Color::from_rgb(0, 0, 0)); 59 | /// ``` 60 | pub fn from_hex(hex: &str) -> Self { 61 | let rgb = match Rgb::from_hex_str(hex) { 62 | Ok(rgb) => rgb, 63 | Err(_) => Rgb::from(0.0, 0.0, 0.0), 64 | }; 65 | 66 | Self { 67 | red: rgb.get_red() as f64 / 255.0, 68 | green: rgb.get_green() as f64 / 255.0, 69 | blue: rgb.get_blue() as f64 / 255.0, 70 | } 71 | } 72 | 73 | pub fn to_hex(&self) -> String { 74 | let r = if self.red >= 1.0 { 75 | 255 76 | } else { 77 | (self.red * 255.0) as i32 78 | }; 79 | let g = if self.green >= 1.0 { 80 | 255 81 | } else { 82 | (self.green * 255.0) as i32 83 | }; 84 | let b = if self.blue >= 1.0 { 85 | 255 86 | } else { 87 | (self.blue * 255.0) as i32 88 | }; 89 | format!("#{:0width$X}{:0width$X}{:0width$X}", r, g, b, width = 2) 90 | } 91 | } 92 | 93 | #[derive(Clone, Debug, PartialEq)] 94 | pub struct Padding { 95 | left: f64, 96 | right: f64, 97 | top: f64, 98 | bottom: f64, 99 | } 100 | 101 | impl Padding { 102 | pub fn new(left: f64, right: f64, top: f64, bottom: f64) -> Padding { 103 | Padding { 104 | left, 105 | right, 106 | top, 107 | bottom, 108 | } 109 | } 110 | } 111 | 112 | #[derive(Clone, PartialEq, Eq)] 113 | pub struct Font(FontDescription); 114 | 115 | impl Font { 116 | pub fn new(name: &str) -> Font { 117 | Font(FontDescription::from_string(name)) 118 | } 119 | } 120 | 121 | impl fmt::Debug for Font { 122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 123 | write!(f, "{}", self.0) 124 | } 125 | } 126 | 127 | #[derive(Clone, Debug, PartialEq)] 128 | pub struct Attributes { 129 | pub font: Font, 130 | pub fg_color: Color, 131 | pub bg_color: Option, 132 | pub padding: Padding, 133 | } 134 | 135 | pub struct PagerAttributes { 136 | /// Active attributes are applied to the currently active workspace 137 | pub active_attr: Attributes, 138 | /// Inactive attributes are applied to workspaces that are not active and contain no windows 139 | pub inactive_attr: Attributes, 140 | /// Non empty attributes are applied to workspaces that are not active and contain windows 141 | pub non_empty_attr: Attributes, 142 | } 143 | 144 | fn create_pango_layout(cairo_context: &cairo::Context) -> pango::Layout { 145 | pangocairo::functions::create_layout(cairo_context) 146 | } 147 | 148 | fn show_pango_layout(cairo_context: &cairo::Context, layout: &pango::Layout) { 149 | pangocairo::functions::show_layout(cairo_context, layout); 150 | } 151 | 152 | #[derive(Clone, Debug, PartialEq)] 153 | pub struct Text { 154 | pub attr: Attributes, 155 | pub text: String, 156 | pub stretch: bool, 157 | pub markup: bool, 158 | } 159 | 160 | impl Text { 161 | pub(crate) fn compute(self, surface: &Surface) -> Result { 162 | let (width, height) = { 163 | let context = Context::new(surface)?; 164 | let layout = create_pango_layout(&context); 165 | if self.markup { 166 | layout.set_markup(&self.text); 167 | } else { 168 | layout.set_text(&self.text); 169 | } 170 | layout.set_font_description(Some(&self.attr.font.0)); 171 | 172 | let padding = &self.attr.padding; 173 | let (text_width, text_height) = layout.pixel_size(); 174 | let width = f64::from(text_width) + padding.left + padding.right; 175 | let height = f64::from(text_height) + padding.top + padding.bottom; 176 | (width, height) 177 | }; 178 | 179 | Ok(ComputedText { 180 | attr: self.attr, 181 | text: self.text, 182 | stretch: self.stretch, 183 | x: 0.0, 184 | y: 0.0, 185 | width, 186 | height, 187 | markup: self.markup, 188 | }) 189 | } 190 | } 191 | 192 | // This impl allows us to see whether a widget's text has changed without 193 | // having to call the (relatively) expensive .compute(). 194 | impl PartialEq for Text { 195 | fn eq(&self, other: &ComputedText) -> bool { 196 | self.attr == other.attr && self.text == other.text && self.stretch == other.stretch 197 | } 198 | } 199 | 200 | #[derive(Clone, Debug, PartialEq)] 201 | pub(crate) struct ComputedText { 202 | pub attr: Attributes, 203 | pub text: String, 204 | pub stretch: bool, 205 | 206 | pub x: f64, 207 | pub y: f64, 208 | pub width: f64, 209 | pub height: f64, 210 | pub markup: bool, 211 | } 212 | 213 | impl ComputedText { 214 | pub fn render(&self, surface: &Surface) -> Result<()> { 215 | let context = Context::new(surface)?; 216 | let layout = create_pango_layout(&context); 217 | if self.markup { 218 | layout.set_markup(&self.text); 219 | } else { 220 | layout.set_text(&self.text); 221 | } 222 | layout.set_font_description(Some(&self.attr.font.0)); 223 | 224 | context.translate(self.x, self.y); 225 | 226 | // Set the width/height on the Pango layout so that it word-wraps/ellipises. 227 | let padding = &self.attr.padding; 228 | let text_width = self.width - padding.left - padding.right; 229 | let text_height = self.height - padding.top - padding.bottom; 230 | layout.set_ellipsize(EllipsizeMode::End); 231 | layout.set_width(text_width as i32 * pango::SCALE); 232 | layout.set_height(text_height as i32 * pango::SCALE); 233 | 234 | let bg_color = &self.attr.bg_color.clone().unwrap_or_else(Color::black); 235 | bg_color.apply_to_context(&context); 236 | // FIXME: The use of `height` isnt' right here: we want to do the 237 | // full height of the bar, not the full height of the text. It 238 | // would be useful if we could do Surface.get_height(), but that 239 | // doesn't seem to be available in cairo-rs for some reason? 240 | context.rectangle(0.0, 0.0, self.width, self.height); 241 | context.fill()?; 242 | 243 | self.attr.fg_color.apply_to_context(&context); 244 | context.translate(padding.left, padding.top); 245 | show_pango_layout(&context, &layout); 246 | 247 | Ok(()) 248 | } 249 | } 250 | 251 | #[derive(Clone, Debug, PartialEq)] 252 | pub struct ThresholdValue { 253 | pub threshold: u8, 254 | pub color: Color, 255 | } 256 | 257 | #[derive(Clone, Debug, PartialEq)] 258 | pub struct Threshold { 259 | pub low: ThresholdValue, 260 | pub normal: ThresholdValue, 261 | pub high: ThresholdValue, 262 | } 263 | 264 | impl Default for Threshold { 265 | fn default() -> Self { 266 | Threshold { 267 | low: ThresholdValue { 268 | threshold: 40, 269 | color: Color::red(), 270 | }, 271 | normal: ThresholdValue { 272 | threshold: 60, 273 | color: Color::yellow(), 274 | }, 275 | high: ThresholdValue { 276 | threshold: 100, 277 | color: Color::green(), 278 | }, 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /cnx/src/widgets/active_window_title.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use futures::stream::StreamExt; 3 | use xcb_util::ewmh; 4 | 5 | use crate::text::{Attributes, Text}; 6 | use crate::widgets::{Widget, WidgetStream}; 7 | use crate::xcb::xcb_properties_stream; 8 | 9 | /// Shows the title of the currently focused window. 10 | /// 11 | /// This widget shows the title (`_NET_WM_NAME` [`EWMH`] property) of the 12 | /// currently focused window. It uses the `_NET_ACTIVE_WINDOW` [`EWMH`] property 13 | /// of the root window to determine which window is currently focused. 14 | /// 15 | /// The widgets content stretches to fill all available space. If the title is 16 | /// too large for the available space, it will be truncated. 17 | /// 18 | /// [`EWMH`]: https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html 19 | pub struct ActiveWindowTitle { 20 | attr: Attributes, 21 | } 22 | 23 | impl ActiveWindowTitle { 24 | /// Creates a new Active Window Title widget. 25 | pub fn new(attr: Attributes) -> ActiveWindowTitle { 26 | ActiveWindowTitle { attr } 27 | } 28 | 29 | fn on_change(&self, conn: &ewmh::Connection, screen_idx: i32) -> Vec { 30 | let title = ewmh::get_active_window(conn, screen_idx) 31 | .get_reply() 32 | .and_then(|active_window| { 33 | // xcb_properties_stream() will only register for notifications on the 34 | // root window, so will only receive notifications when the active window 35 | // changes. So, for each active window we see, register for property 36 | // change notifications, so that we can see when the currently active 37 | // window changes title. (We'll continue to receive notifications after 38 | // it is no longer the active window, but this isn't a big deal). 39 | let attributes = [(xcb::CW_EVENT_MASK, xcb::EVENT_MASK_PROPERTY_CHANGE)]; 40 | xcb::change_window_attributes(conn, active_window, &attributes); 41 | conn.flush(); 42 | 43 | ewmh::get_wm_name(conn, active_window).get_reply() 44 | }) 45 | .map(|reply| reply.string().to_owned()) 46 | .unwrap_or_else(|_| "".to_owned()); 47 | 48 | vec![Text { 49 | attr: self.attr.clone(), 50 | text: title, 51 | stretch: true, 52 | markup: false, 53 | }] 54 | } 55 | } 56 | 57 | impl Widget for ActiveWindowTitle { 58 | fn into_stream(self: Box) -> Result { 59 | let properties = &["_NET_ACTIVE_WINDOW", "_NET_WM_NAME"]; 60 | let screen_idx = 0; // XXX assume 61 | let (conn, stream) = 62 | xcb_properties_stream(properties).context("Initialising ActiveWindowtitle")?; 63 | 64 | let stream = stream.map(move |()| Ok(self.on_change(&conn, screen_idx))); 65 | 66 | Ok(Box::pin(stream)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cnx/src/widgets/clock.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::time::Duration; 3 | use tokio::time; 4 | use tokio_stream::wrappers::IntervalStream; 5 | use tokio_stream::StreamExt; 6 | 7 | use crate::text::{Attributes, Text}; 8 | use crate::widgets::{Widget, WidgetStream}; 9 | 10 | /// Shows the current time and date. 11 | /// 12 | /// This widget shows the current time and date, in the form `%Y-%m-%d %a %I:%M 13 | /// %p`, e.g. `2017-09-01 Fri 12:51 PM`. 14 | pub struct Clock { 15 | attr: Attributes, 16 | format_str: Option, 17 | } 18 | 19 | impl Clock { 20 | // Creates a new Clock widget. 21 | pub fn new(attr: Attributes, format_str: Option) -> Self { 22 | Self { attr, format_str } 23 | } 24 | 25 | fn tick(&self) -> Vec { 26 | let now = chrono::Local::now(); 27 | let format_time: String = self 28 | .format_str 29 | .clone() 30 | .map_or("%Y-%m-%d %a %I:%M %p".to_string(), |item| item); 31 | let text = now.format(&format_time).to_string(); 32 | let texts = vec![Text { 33 | attr: self.attr.clone(), 34 | text, 35 | stretch: false, 36 | markup: true, 37 | }]; 38 | texts 39 | } 40 | } 41 | 42 | impl Widget for Clock { 43 | fn into_stream(self: Box) -> Result { 44 | // As we're not showing seconds, we can sleep for however long 45 | // it takes until the minutes changes between updates. 46 | let one_minute = Duration::from_secs(60); 47 | let interval = time::interval(one_minute); 48 | let stream = IntervalStream::new(interval).map(move |_| Ok(self.tick())); 49 | 50 | Ok(Box::pin(stream)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cnx/src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provided widgets and types for creating new widgets. 2 | 3 | mod active_window_title; 4 | 5 | mod clock; 6 | mod pager; 7 | pub use self::active_window_title::ActiveWindowTitle; 8 | pub use self::clock::Clock; 9 | pub use self::pager::Pager; 10 | use crate::text::Text; 11 | use anyhow::Result; 12 | use futures::stream::Stream; 13 | use std::pin::Pin; 14 | 15 | /// The stream of `Vec` returned by each widget. 16 | /// 17 | /// This simple type alias makes referring to this stream a little easier. For 18 | /// more information on the stream (and how widgets are structured), please 19 | /// refer to the documentation on the [`Widget`] trait. 20 | /// 21 | /// Any errors on the stream are logged but do not affect the runtime of the 22 | /// main [`crate::Cnx`] instance. 23 | /// 24 | pub type WidgetStream = Pin>>>>; 25 | 26 | /// The main trait implemented by all widgets. 27 | /// 28 | /// This simple trait defines a widget. A widget is essentially just a 29 | /// [`futures::stream::Stream`] and this trait is the standard way of accessing 30 | /// that stream. 31 | /// 32 | /// See the [`WidgetStream`] type alias for the exact type of stream that 33 | /// should be returned. 34 | /// 35 | pub trait Widget { 36 | fn into_stream(self: Box) -> Result; 37 | } 38 | -------------------------------------------------------------------------------- /cnx/src/widgets/pager.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use futures::stream::StreamExt; 3 | use std::cmp::Ordering; 4 | use xcb_util::ewmh; 5 | 6 | use crate::text::{Attributes, PagerAttributes, Text}; 7 | use crate::widgets::{Widget, WidgetStream}; 8 | use crate::xcb::xcb_properties_stream; 9 | 10 | /// Shows the WM's workspaces/groups. 11 | /// 12 | /// This widget can highlight the currently active, inactive and non empty 13 | /// workspaces/groups differently. 14 | /// 15 | /// This widget shows the WM's workspaces/groups, as determined by the [`EWMH`] 16 | /// `_NET_NUMBER_OF_DESKTOPS`, `_NET_DESKTOP_NAMES`, `_NET_CURRENT_DESKTOP`, 17 | /// `_NET_CLIENT_LIST`, `_NET_WM_WINDOW_TYPE` and `_NET_WM_DESKTOP` properties. 18 | /// 19 | /// [`EWMH`]: https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html 20 | pub struct Pager { 21 | active_attr: Attributes, 22 | inactive_attr: Attributes, 23 | non_empty_attr: Attributes, 24 | } 25 | 26 | impl Pager { 27 | /// Creates a new Pager widget. 28 | pub fn new(pager_attrs: PagerAttributes) -> Self { 29 | Self { 30 | active_attr: pager_attrs.active_attr, 31 | inactive_attr: pager_attrs.inactive_attr, 32 | non_empty_attr: pager_attrs.non_empty_attr, 33 | } 34 | } 35 | 36 | fn on_change(&self, conn: &ewmh::Connection, screen_idx: i32) -> Vec { 37 | let number = ewmh::get_number_of_desktops(conn, screen_idx) 38 | .get_reply() 39 | .unwrap_or(0) as usize; 40 | let current = ewmh::get_current_desktop(conn, screen_idx) 41 | .get_reply() 42 | .unwrap_or(0) as usize; 43 | let names_reply = ewmh::get_desktop_names(conn, screen_idx).get_reply(); 44 | let mut names = match names_reply { 45 | Ok(ref r) => r.strings(), 46 | Err(_) => Vec::new(), 47 | }; 48 | 49 | // EWMH states that `number` may not equal `names.len()`, as there may 50 | // be unnamed desktops, or more desktops than are currently in use. 51 | match names.len().cmp(&number) { 52 | Ordering::Equal => (), 53 | Ordering::Greater => names.truncate(number), 54 | Ordering::Less => { 55 | let num_unnamed = number - names.len(); 56 | names.extend(vec!["?"; num_unnamed]); 57 | } 58 | } 59 | 60 | names 61 | .into_iter() 62 | .enumerate() 63 | .map(|(i, name)| { 64 | let attr = if i == current { 65 | self.active_attr.clone() 66 | } else if non_empty_desktops(conn, screen_idx).contains(&(i as u32)) { 67 | self.non_empty_attr.clone() 68 | } else { 69 | self.inactive_attr.clone() 70 | }; 71 | 72 | Text { 73 | attr, 74 | text: name.to_owned(), 75 | stretch: false, 76 | markup: true, 77 | } 78 | }) 79 | .collect() 80 | } 81 | } 82 | 83 | fn non_empty_desktops(conn: &ewmh::Connection, screen_idx: i32) -> Vec { 84 | let client_list = ewmh::get_client_list(conn, screen_idx).get_reply(); 85 | let windows: &[u32] = match client_list { 86 | Ok(ref cl) => cl.windows(), 87 | Err(_) => &[], 88 | }; 89 | 90 | windows 91 | .iter() 92 | .filter(|&w| match ewmh::get_wm_window_type(conn, *w).get_reply() { 93 | Ok(wt) => wt.atoms().first() == Some(&conn.WM_WINDOW_TYPE_NORMAL()), 94 | Err(_) => false, 95 | }) 96 | .filter_map(|w| ewmh::get_wm_desktop(conn, *w).get_reply().ok()) 97 | .filter( 98 | |&d| match ewmh::get_current_desktop(conn, screen_idx).get_reply() { 99 | Ok(c) => c != d, 100 | Err(_) => true, 101 | }, 102 | ) 103 | .collect() 104 | } 105 | 106 | impl Widget for Pager { 107 | fn into_stream(self: Box) -> Result { 108 | let properties = &[ 109 | "_NET_NUMBER_OF_DESKTOPS", 110 | "_NET_CURRENT_DESKTOP", 111 | "_NET_DESKTOP_NAMES", 112 | ]; 113 | let screen_idx = 0; // XXX assume 114 | let (conn, stream) = xcb_properties_stream(properties).context("Initialising Pager")?; 115 | 116 | let stream = stream.map(move |()| Ok(self.on_change(&conn, screen_idx))); 117 | 118 | Ok(Box::pin(stream)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /cnx/src/xcb.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context as _AnyhowContext, Result}; 2 | use std::os::unix::io::AsRawFd; 3 | use std::os::unix::io::RawFd; 4 | use std::pin::Pin; 5 | use std::rc::Rc; 6 | use std::task::{Context, Poll}; 7 | use tokio::io::unix::AsyncFd; 8 | use tokio_stream::{self as stream, Stream, StreamExt}; 9 | use xcb::xproto::{PropertyNotifyEvent, PROPERTY_NOTIFY}; 10 | use xcb_util::ewmh; 11 | 12 | // A wrapper around `ewhm::Connection` that implements `mio::Evented`. 13 | // 14 | // This is just using `mio::EventedFd`. We have to have a custom wrapper 15 | // because `mio::EventedFd` only borrows its fd and it's difficult to 16 | // make it live long enough. 17 | struct XcbEvented(Rc); 18 | 19 | impl AsRawFd for XcbEvented { 20 | fn as_raw_fd(&self) -> RawFd { 21 | let conn: &xcb::Connection = &self.0; 22 | conn.as_raw_fd() 23 | } 24 | } 25 | 26 | // A `Stream` of `xcb::GenericEvent` for the provided `xcb::Connection`. 27 | pub struct XcbEventStream { 28 | conn: Rc, 29 | poll: AsyncFd, 30 | would_block: bool, 31 | } 32 | 33 | impl XcbEventStream { 34 | pub fn new(conn: Rc) -> Result { 35 | let evented = XcbEvented(conn.clone()); 36 | let poll = AsyncFd::with_interest(evented, tokio::io::Interest::READABLE)?; 37 | 38 | Ok(XcbEventStream { 39 | conn, 40 | poll, 41 | would_block: true, 42 | }) 43 | } 44 | } 45 | 46 | impl Stream for XcbEventStream { 47 | type Item = xcb::GenericEvent; 48 | 49 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 50 | let self_ = &mut *self; 51 | let mut ready = None; 52 | if self_.would_block { 53 | match self_.poll.poll_read_ready(cx) { 54 | Poll::Ready(Ok(r)) => { 55 | ready = Some(r); 56 | self_.would_block = false; 57 | } 58 | Poll::Ready(Err(e)) => { 59 | // Unsure when this would happen: 60 | panic!("Error polling xcb::Connection: {e}"); 61 | } 62 | Poll::Pending => return Poll::Pending, 63 | } 64 | } 65 | match self_.conn.poll_for_event() { 66 | Some(event) => Poll::Ready(Some(event)), 67 | None => { 68 | self_.would_block = true; 69 | match ready { 70 | None => self.poll_next(cx), 71 | Some(mut r) => { 72 | r.clear_ready(); 73 | self.poll_next(cx) 74 | } 75 | } 76 | // self.poll_next(cx) 77 | } 78 | } 79 | } 80 | } 81 | 82 | // A `Stream` that listens to `PROPERTY_CHANGE` notifications. 83 | // 84 | // By default it listens to `PROPERTY_CHANGE` notifications for the provided 85 | // `properties` on the root window. The `ewhm::Connection` is returned so that 86 | // the caller may listen to `PROPERTY_CHANGE` notifications on additional 87 | // windows. 88 | pub fn xcb_properties_stream( 89 | properties: &[&str], 90 | ) -> Result<(Rc, impl Stream)> { 91 | let (xcb_conn, screen_idx) = 92 | xcb::Connection::connect(None).context("Failed to connect to X server")?; 93 | let root_window = xcb_conn 94 | .get_setup() 95 | .roots() 96 | .nth(screen_idx as usize) 97 | .ok_or_else(|| anyhow!("Invalid screen"))? 98 | .root(); 99 | let ewmh_conn = ewmh::Connection::connect(xcb_conn) 100 | .map_err(|(e, _)| e) 101 | .context("Failed to wrap xcb::Connection in ewmh::Connection")?; 102 | let conn = Rc::new(ewmh_conn); 103 | 104 | let only_if_exists = true; 105 | let properties = properties 106 | .iter() 107 | .map(|property| -> Result { 108 | let reply = xcb::intern_atom(&conn, only_if_exists, property).get_reply()?; 109 | Ok(reply.atom()) 110 | }) 111 | .collect::>>() 112 | .context("Failed to intern atoms")?; 113 | 114 | // Register for all PROPERTY_CHANGE events. We'll filter out the ones 115 | // that are interesting below. 116 | let attributes = [(xcb::CW_EVENT_MASK, xcb::EVENT_MASK_PROPERTY_CHANGE)]; 117 | xcb::change_window_attributes(&conn, root_window, &attributes); 118 | conn.flush(); 119 | 120 | let xcb_stream = XcbEventStream::new(conn.clone())?; 121 | let stream = xcb_stream.filter_map(move |event| { 122 | if event.response_type() == PROPERTY_NOTIFY { 123 | let event: &PropertyNotifyEvent = unsafe { xcb::cast_event(&event) }; 124 | if properties.iter().any(|p| *p == event.atom()) { 125 | // We don't actually care about the event, just that 126 | // it occurred. 127 | return Some(()); 128 | } 129 | } 130 | None 131 | }); 132 | 133 | // Pretend there was an initial property change to get the initial 134 | // contents of the widget, then allow our stream of XCB events to 135 | // call the callback for actual changes. 136 | let stream = stream::once(()).chain(stream); 137 | 138 | Ok((conn, stream)) 139 | } 140 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.67.0" 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjkillough/cnx/7845d99baa296901171c083db61588a62f9a8b34/screenshot.png -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import { }; 2 | stdenv.mkDerivation { 3 | name = "cnx"; 4 | buildInputs = [ 5 | alsaLib 6 | cairo 7 | clang 8 | glib 9 | gobject-introspection 10 | libclang 11 | libllvm 12 | llvmPackages.libclang 13 | openssl 14 | pango 15 | pkg-config 16 | python3 17 | rust-bindgen 18 | rustfmt 19 | wirelesstools 20 | xorg.libxcb 21 | xorg.xcbutilwm 22 | ]; 23 | 24 | shellHook = '' 25 | export LIBCLANG_PATH="${llvmPackages.libclang.lib}/lib"; 26 | ''; 27 | } 28 | --------------------------------------------------------------------------------