1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 10:41:03 +02:00

Merge pull request #59 from JakeStanger/feat/better-images

Better images
This commit is contained in:
Jake Stanger 2023-01-30 12:13:10 +00:00 committed by GitHub
commit 400ac00d23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1042 additions and 253 deletions

436
Cargo.lock generated
View file

@ -213,6 +213,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -366,6 +372,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@ -603,6 +619,15 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encoding_rs"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-kinds"
version = "0.5.1"
@ -688,6 +713,30 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]]
name = "from_variants"
version = "1.0.0"
@ -1054,6 +1103,25 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "h2"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1090,6 +1158,77 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyprland"
version = "0.3.0"
@ -1141,6 +1280,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indenter"
version = "0.3.3"
@ -1186,6 +1335,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "ironbar"
version = "0.9.0"
@ -1206,6 +1361,7 @@ dependencies = [
"mpris",
"notify",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
@ -1374,6 +1530,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1439,6 +1601,24 @@ dependencies = [
"thiserror",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.23.2"
@ -1575,6 +1755,51 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openssl"
version = "0.10.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [
"proc-macro2",
"quote 1.0.21",
"syn 1.0.105",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "ordered-stream"
version = "0.0.1"
@ -1658,6 +1883,12 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.1"
@ -1906,6 +2137,43 @@ dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
@ -1942,6 +2210,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1954,6 +2231,29 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "security-framework"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c4437699b6d34972de58652c68b98cb5b53a4199ab126db8e20ec8ded29a721"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "0.11.0"
@ -2023,6 +2323,18 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.14"
@ -2365,6 +2677,21 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.23.0"
@ -2396,6 +2723,16 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.11"
@ -2407,6 +2744,20 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]]
name = "toml"
version = "0.5.9"
@ -2450,6 +2801,12 @@ dependencies = [
"toml_datetime",
]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.37"
@ -2533,6 +2890,12 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "typenum"
version = "1.16.0"
@ -2555,12 +2918,27 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicode-bidi"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
@ -2585,6 +2963,17 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68"
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.0"
@ -2597,6 +2986,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
@ -2653,6 +3048,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -2690,6 +3095,18 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
@ -2789,6 +3206,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
@ -2886,6 +3313,15 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "xcursor"
version = "0.3.4"

View file

@ -26,6 +26,7 @@ async_once = "0.2.6"
indexmap = "1.9.1"
futures-util = "0.3.21"
chrono = "0.4.19"
reqwest = {version = "0.11.14" }
regex = { version = "1.6.0", default-features = false, features = ["std"] }
stray = { version = "0.1.2" }
dirs = "4.0.0"

View file

@ -272,6 +272,7 @@ The following table lists each of the top-level bar config options:
| `position` | `top` or `bottom` or `left` or `right` | `bottom` | The bar's position on screen. |
| `anchor_to_edges` | `boolean` | `false` | Whether to anchor the bar to the edges of the screen. Setting to false centres the bar. |
| `height` | `integer` | `42` | The bar's height in pixels. |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |
| `center` | `Module[]` | `[]` | Array of center modules. |
| `end` | `Module[]` | `[]` | Array of right or bottom modules. |

15
docs/Images.md Normal file
View file

@ -0,0 +1,15 @@
Ironbar is capable of loading images from multiple sources.
In any situation where an option takes text or an icon,
you can use a string in any of the following formats, and it will automatically be detected as an image:
| Source | Example |
|-------------------------------|---------------------------------|
| GTK icon theme | `icon:firefox` |
| Local file | `file:///path/to/file.jpg` |
| Remote file (over HTTP/HTTPS) | `https://example.com/image.jpg` |
Remote images are loaded asynchronously to avoid blocking the UI thread.
Be aware this can cause elements to change size upon load if the image is large enough.
Note that mixing text and images is not supported.
Your best option here is to use Nerd Font icons instead.

View file

@ -2,6 +2,7 @@
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Images](images)
- [Styling guide](styling-guide)
# Examples

View file

@ -18,15 +18,17 @@ It is well worth looking at the examples.
### `Widget`
| Name | Type | Default | Description |
|---------------|------------------------------|--------------|---------------------------------------------------------------------------|
| `widget_type` | `box` or `label` or `button` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
| Name | Type | Default | Description |
|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------|
| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. |
| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
### Labels
@ -56,6 +58,8 @@ The following bar commands are supported:
- `popup:open`
- `popup:close`
---
XML is arguably better-suited and easier to read for this sort of markup,
but currently is not supported.
Nonetheless, it may be worth comparing the examples to the below equivalent

View file

@ -12,7 +12,6 @@ Displays the title and/or icon of the currently focused window.
| `show_icon` | `boolean` | `true` | Whether to show the app's icon |
| `show_title` | `boolean` | `true` | Whether to show the app's title |
| `icon_size` | `integer` | `32` | Size of icon in pixels |
| `icon_theme` | `string` | `null` | GTK icon theme to use |
| `truncate` or `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand version if specifying a length. |
| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |

View file

@ -14,7 +14,6 @@ Optionally displays a launchable set of favourites.
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_theme` | `string` | `null` | GTK icon theme to use. |
<details>
<summary>JSON</summary>

View file

@ -17,11 +17,18 @@ in MPRIS mode, the widget will listen to all players and automatically detect/di
| `format` | `string` | `{icon} {title} / {artist}` | Format string for the widget. More info below. |
| `truncate` or `truncate.mode` | `start` or `middle` or `end` | `null` | The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand version if specifying a length. |
| `truncate.length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `icons.play` | `string` | `` | Icon to show when playing. |
| `icons.pause` | `string` | `` | Icon to show when paused. |
| `icons.volume` | `string` | `墳` | Icon to show under popup volume slider. |
| `host` | `string` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
| `icons.play` | `string/image` | `` | Icon to show when playing. |
| `icons.pause` | `string/image` | `` | Icon to show when paused. |
| `icons.prev` | `string/image` | `玲` | Icon to show on previous button. |
| `icons.next` | `string/image` | `怜` | Icon to show on next button. |
| `icons.volume` | `string/image` | `墳` | Icon to show under popup volume slider. |
| `icons.track` | `string/image` | `` | Icon to show next to track title. |
| `icons.album` | `string/image` | `` | Icon to show next to album name. |
| `icons.artist` | `string/image` | `ﴁ` | Icon to show next to artist name. |
| `host` | `string/image` | `localhost:6600` | [MPD Only] TCP or Unix socket for the MPD server. |
| `music_dir` | `string/image` | `$HOME/Music` | [MPD Only] Path to MPD server's music directory on disc. Required for album art. |
See [here](images) for information on images.
<details>
<summary>JSON</summary>
@ -122,24 +129,25 @@ and will be replaced with values from the currently playing track:
## Styling
| Selector | Description |
|------------------------------------------|------------------------------------------|
| `#music` | Tray widget button |
| `#popup-music` | Popup box |
| `#popup-music #album-art` | Album art image inside popup box |
| `#popup-music #title` | Track title container inside popup box |
| `#popup-music #title .icon` | Track title icon label inside popup box |
| `#popup-music #title .label` | Track title label inside popup box |
| `#popup-music #album` | Track album container inside popup box |
| `#popup-music #album .icon` | Track album icon label inside popup box |
| `#popup-music #album .label` | Track album label inside popup box |
| `#popup-music #artist` | Track artist container inside popup box |
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
| `#popup-music #artist .label` | Track artist label inside popup box |
| `#popup-music #controls` | Controls container inside popup box |
| `#popup-music #controls #btn-prev` | Previous button inside popup box |
| `#popup-music #controls #btn-play-pause` | Play/pause button inside popup box |
| `#popup-music #controls #btn-next` | Next button inside popup box |
| `#popup-music #volume` | Volume container inside popup box |
| `#popup-music #volume #slider` | Volume slider popup box |
| `#popup-music #volume .icon` | Volume icon label inside popup box |
| Selector | Description |
|-------------------------------------|------------------------------------------|
| `#music` | Tray widget button |
| `#popup-music` | Popup box |
| `#popup-music #album-art` | Album art image inside popup box |
| `#popup-music #title` | Track title container inside popup box |
| `#popup-music #title .icon` | Track title icon label inside popup box |
| `#popup-music #title .label` | Track title label inside popup box |
| `#popup-music #album` | Track album container inside popup box |
| `#popup-music #album .icon` | Track album icon label inside popup box |
| `#popup-music #album .label` | Track album label inside popup box |
| `#popup-music #artist` | Track artist container inside popup box |
| `#popup-music #artist .icon` | Track artist icon label inside popup box |
| `#popup-music #artist .label` | Track artist label inside popup box |
| `#popup-music #controls` | Controls container inside popup box |
| `#popup-music #controls #btn-prev` | Previous button inside popup box |
| `#popup-music #controls #btn-play` | Play button inside popup box |
| `#popup-music #controls #btn-pause` | Pause button inside popup box |
| `#popup-music #controls #btn-next` | Next button inside popup box |
| `#popup-music #volume` | Volume container inside popup box |
| `#popup-music #volume #slider` | Volume slider popup box |
| `#popup-music #volume .icon` | Volume icon label inside popup box |

View file

@ -8,11 +8,11 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|---------------------------|----------------|----------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string>` | `{}` | A map of actual workspace names to their display labels. Workspaces use their actual name if not present in the map. |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
| Name | Type | Default | Description |
|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string/image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
<details>
<summary>JSON</summary>
@ -72,15 +72,15 @@ end:
```corn
{
end = [
{
type = "workspaces",
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
all_monitors = false
}
]
end = [
{
type = "workspaces",
name_map.1 = ""
name_map.2 = ""
name_map.3 = ""
all_monitors = false
}
]
}
```

12
flake.lock generated
View file

@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1672350804,
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
"lastModified": 1674641431,
"narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
"rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc",
"type": "github"
},
"original": {
@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1672453260,
"narHash": "sha256-ruR2xo30Vn7kY2hAgg2Z2xrCvNePxck6mgR5a8u+zow=",
"lastModified": 1674959389,
"narHash": "sha256-N0kc+CE9Vy7Mj8Z/cLHx59gL+ZnkmylYH0BQEWz3TZU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "176b6fd3dd3d7cea8d22ab1131364a050228d94c",
"rev": "edd082ca16aa055d5504bea39da36b3ee68e4f1d",
"type": "github"
},
"original": {

View file

@ -50,7 +50,7 @@
cargoDeps = rustPlatform.importCargoLock {lockFile = ./Cargo.lock;};
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = with prev; [pkg-config];
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon];
buildInputs = with prev; [gtk3 gdk-pixbuf gtk-layer-shell libxkbcommon openssl];
};
};
packages = genSystems (
@ -74,6 +74,7 @@
gtk3
gtk-layer-shell
pkg-config
openssl
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";

View file

@ -8,7 +8,7 @@ use crate::{await_sync, read_lock, send, write_lock, Config};
use color_eyre::Result;
use gtk::gdk::{EventMask, Monitor, ScrollDirection};
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, EventBox, Orientation, Widget};
use gtk::{Application, ApplicationWindow, EventBox, IconTheme, Orientation, Widget};
use std::sync::{Arc, RwLock};
use tokio::spawn;
use tokio::sync::mpsc;
@ -140,6 +140,11 @@ fn load_modules(
monitor: &Monitor,
output_name: &str,
) -> Result<()> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
macro_rules! info {
($location:expr) => {
ModuleInfo {
@ -148,6 +153,7 @@ fn load_modules(
monitor,
output_name,
location: $location,
icon_theme: &icon_theme,
}
};
}

View file

@ -22,7 +22,7 @@ pub struct Track {
pub disc: Option<u64>,
pub genre: Option<String>,
pub track: Option<u64>,
pub cover_path: Option<PathBuf>,
pub cover_path: Option<String>,
}
#[derive(Clone, Debug)]

View file

@ -121,12 +121,16 @@ impl MpdClient {
fn convert_song(song: &Song, music_dir: &Path) -> Track {
let (track, disc) = song.number();
let cover_path = music_dir.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
);
let cover_path = music_dir
.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
)
.into_os_string()
.into_string()
.ok();
Track {
title: song.title().map(std::string::ToString::to_string),
@ -136,7 +140,7 @@ impl MpdClient {
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
disc: Some(disc),
track: Some(track),
cover_path: Some(cover_path),
cover_path,
}
}
}

View file

@ -5,7 +5,6 @@ use color_eyre::Result;
use lazy_static::lazy_static;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;
@ -260,10 +259,7 @@ impl From<Metadata> for Track {
.and_then(mpris::MetadataValue::as_str_array)
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
track: value.track_number().map(|track| track as u64),
cover_path: value
.art_url()
.map(|path| path.replace("file://", ""))
.map(PathBuf::from),
cover_path: value.art_url().map(|s| s.to_string()),
}
}
}

View file

@ -73,6 +73,9 @@ pub struct Config {
#[serde(default = "default_bar_height")]
pub height: i32,
/// GTK icon theme to use.
pub icon_theme: Option<String>,
pub start: Option<Vec<ModuleConfig>>,
pub center: Option<Vec<ModuleConfig>>,
pub end: Option<Vec<ModuleConfig>>,

View file

@ -1,6 +1,3 @@
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use std::collections::HashMap;
use std::fs::File;
use std::io;
@ -67,7 +64,7 @@ fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
}
/// Attempts to get the icon name from the app's `.desktop` file.
fn get_desktop_icon_name(app_id: &str) -> Option<String> {
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
find_desktop_file(app_id).and_then(|file| {
let map = parse_desktop_file(file);
map.map_or(None, |map| {
@ -75,65 +72,3 @@ fn get_desktop_icon_name(app_id: &str) -> Option<String> {
})
})
}
enum IconLocation {
Theme(String),
File(PathBuf),
}
/// Attempts to get the location of an icon.
///
/// Handles icons that are part of a GTK theme, icons specified as path
/// and icons for steam games.
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
let has_icon = theme
.lookup_icon(app_id, size, IconLookupFlags::empty())
.is_some();
if has_icon {
return Some(IconLocation::Theme(app_id.to_string()));
}
let is_steam_game = app_id.starts_with("steam_app_");
if is_steam_game {
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
return match dirs::data_dir() {
Some(dir) => {
let path = dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
));
return Some(IconLocation::File(path));
}
None => None,
};
}
let icon_name = get_desktop_icon_name(app_id);
if let Some(icon_name) = icon_name {
let is_path = PathBuf::from(&icon_name).exists();
return if is_path {
Some(IconLocation::File(PathBuf::from(icon_name)))
} else {
return Some(IconLocation::Theme(icon_name));
};
}
None
}
/// Gets the icon associated with an app.
pub fn get_icon(theme: &IconTheme, app_id: &str, size: i32) -> Option<Pixbuf> {
let icon_location = get_icon_location(theme, app_id, size);
match icon_location {
Some(IconLocation::Theme(icon_name)) => {
let icon = theme.load_icon(&icon_name, size, IconLookupFlags::FORCE_SIZE);
icon.map_or(None, |icon| icon)
}
Some(IconLocation::File(path)) => Pixbuf::from_file_at_scale(path, size, size, true).ok(),
None => None,
}
}

48
src/image/gtk.rs Normal file
View file

@ -0,0 +1,48 @@
use super::ImageProvider;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
use tracing::error;
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
let button = Button::new();
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
match ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image.clone()))
{
Ok(_) => {
button.set_image(Some(&image));
button.set_always_show_image(true);
}
Err(err) => {
error!("{err:?}");
button.set_label(input);
}
}
} else {
button.set_label(input);
}
button
}
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
let container = gtk::Box::new(Orientation::Horizontal, 0);
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
container.add(&image);
if let Err(err) = ImageProvider::parse(input, icon_theme, size)
.and_then(|provider| provider.load_into_image(image))
{
error!("{err:?}");
}
} else {
let label = Label::new(Some(input));
container.add(&label);
}
container
}

5
src/image/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod gtk;
mod provider;
pub use self::gtk::*;
pub use provider::ImageProvider;

176
src/image/provider.rs Normal file
View file

@ -0,0 +1,176 @@
use crate::desktop_file::get_desktop_icon_name;
use crate::send;
use color_eyre::{Report, Result};
use glib::Bytes;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use reqwest::Url;
use std::path::{Path, PathBuf};
use tokio::spawn;
use tracing::error;
#[derive(Debug)]
enum ImageLocation<'a> {
Icon { name: String, theme: &'a IconTheme },
Local(PathBuf),
Steam(String),
Remote(Url),
}
pub struct ImageProvider<'a> {
location: ImageLocation<'a>,
size: i32,
}
impl<'a> ImageProvider<'a> {
/// Attempts to parse the image input to find its location.
/// Errors if no valid location type can be found.
///
/// Note this checks that icons exist in theme, or files exist on disk
/// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
let location = Self::get_location(input, theme, size)?;
Ok(Self { location, size })
}
/// Returns true if the input starts with a prefix
/// that is supported by the parser
/// (ie the parser would not fallback to checking the input).
pub fn is_definitely_image_input(input: &str) -> bool {
input.starts_with("icon:")
|| input.starts_with("file://")
|| input.starts_with("http://")
|| input.starts_with("https://")
}
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
let (input_type, input_name) = input
.split_once(':')
.map_or((None, input), |(t, n)| (Some(t), n));
match input_type {
Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon {
name: input_name.to_string(),
theme,
}),
Some(input_type) if input_type == "file" => Ok(ImageLocation::Local(PathBuf::from(
input_name[2..].to_string(),
))),
Some(input_type) if input_type == "http" || input_type == "https" => {
Ok(ImageLocation::Remote(input.parse()?))
}
None if input.starts_with("steam_app_") => Ok(ImageLocation::Steam(
input_name.chars().skip("steam_app_".len()).collect(),
)),
None if theme
.lookup_icon(input, size, IconLookupFlags::empty())
.is_some() =>
{
Ok(ImageLocation::Icon {
name: input_name.to_string(),
theme,
})
}
Some(input_type) => Err(Report::msg(format!("Unsupported image type: {input_type}"))),
None if PathBuf::from(input_name).exists() => {
Ok(ImageLocation::Local(PathBuf::from(input_name)))
}
None => get_desktop_icon_name(input_name).map_or_else(
|| Err(Report::msg("Unknown image type")),
|input| Self::get_location(&input, theme, size),
),
}
}
/// Attempts to fetch the image from the location
/// and load it into the provided `GTK::Image` widget.
pub fn load_into_image(&self, image: gtk::Image) -> Result<()> {
// handle remote locations async to avoid blocking UI thread while downloading
if let ImageLocation::Remote(url) = &self.location {
let url = url.clone();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
let bytes = Self::get_bytes_from_http(url).await;
if let Ok(bytes) = bytes {
send!(tx, bytes);
}
});
{
let size = self.size;
rx.attach(None, move |bytes| {
let stream = MemoryInputStream::from_bytes(&bytes);
let pixbuf = Pixbuf::from_stream_at_scale(
&stream,
size,
size,
true,
Some(&Cancellable::new()),
);
match pixbuf {
Ok(pixbuf) => image.set_pixbuf(Some(&pixbuf)),
Err(err) => error!("{err:?}"),
}
Continue(false)
});
}
} else {
let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme),
ImageLocation::Local(path) => self.get_from_file(path),
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id),
ImageLocation::Remote(_) => unreachable!(), // handled above
}?;
image.set_pixbuf(Some(&pixbuf));
};
Ok(())
}
/// Attempts to get a `Pixbuf` from the GTK icon theme.
fn get_from_icon(&self, name: &str, theme: &IconTheme) -> Result<Pixbuf> {
let pixbuf = match theme.lookup_icon(name, self.size, IconLookupFlags::empty()) {
Some(_) => theme.load_icon(name, self.size, IconLookupFlags::FORCE_SIZE),
None => Ok(None),
}?;
pixbuf.map_or_else(
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
Ok,
)
}
/// Attempts to get a `Pixbuf` from a local file.
fn get_from_file(&self, path: &Path) -> Result<Pixbuf> {
let pixbuf = Pixbuf::from_file_at_scale(path, self.size, self.size, true)?;
Ok(pixbuf)
}
/// Attempts to get a `Pixbuf` from a local file,
/// using the Steam game ID to look it up.
fn get_from_steam_id(&self, steam_id: &str) -> Result<Pixbuf> {
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
let path = dirs::data_dir().map_or_else(
|| Err(Report::msg("Missing XDG data dir")),
|dir| {
Ok(dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
)))
},
)?;
self.get_from_file(&path)
}
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
async fn get_bytes_from_http(url: Url) -> Result<Bytes> {
let bytes = reqwest::get(url).await?.bytes().await?;
Ok(Bytes::from_owned(bytes))
}
}

View file

@ -2,9 +2,10 @@ mod bar;
mod bridge_channel;
mod clients;
mod config;
mod desktop_file;
mod dynamic_string;
mod error;
mod icon;
mod image;
mod logging;
mod macros;
mod modules;

View file

@ -82,7 +82,7 @@ impl Module<Button> for ClockModule {
});
}
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: button,
@ -94,6 +94,7 @@ impl Module<Button> for ClockModule {
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
@ -119,6 +120,8 @@ impl Module<Button> for ClockModule {
});
}
container.show_all();
Some(container)
}
}

View file

@ -1,12 +1,13 @@
use crate::config::CommonConfig;
use crate::dynamic_string::DynamicString;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::{ButtonGeometry, Popup};
use crate::script::Script;
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, Label, Orientation};
use gtk::{Button, IconTheme, Label, Orientation};
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
@ -46,6 +47,8 @@ pub struct Widget {
class: Option<String>,
on_click: Option<String>,
orientation: Option<String>,
src: Option<String>,
size: Option<i32>,
}
/// Supported GTK widget types
@ -55,20 +58,33 @@ pub enum WidgetType {
Box,
Label,
Button,
Image,
}
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
fn add_to(
self,
parent: &gtk::Box,
tx: Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &IconTheme,
) {
match self.widget_type {
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)),
WidgetType::Label => parent.add(&self.into_label()),
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
WidgetType::Image => parent.add(&self.into_image(icon_theme)),
}
}
/// Creates a `gtk::Box` from this widget
fn into_box(self, tx: &Sender<ExecEvent>, bar_orientation: Orientation) -> gtk::Box {
fn into_box(
self,
tx: &Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &IconTheme,
) -> gtk::Box {
let mut builder = gtk::Box::builder();
if let Some(name) = self.name {
@ -87,9 +103,9 @@ impl Widget {
}
if let Some(widgets) = self.widgets {
widgets
.into_iter()
.for_each(|widget| widget.add_to(&container, tx.clone(), bar_orientation));
for widget in widgets {
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
}
}
container
@ -157,6 +173,31 @@ impl Widget {
button
}
fn into_image(self, icon_theme: &IconTheme) -> gtk::Image {
let mut builder = gtk::Image::builder();
if let Some(name) = self.name {
builder = builder.name(&name);
}
let gtk_image = builder.build();
if let Some(src) = self.src {
let size = self.size.unwrap_or(32);
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
.and_then(|image| image.load_into_image(gtk_image.clone()))
{
error!("{err:?}");
}
}
if let Some(class) = self.class {
gtk_image.style_context().add_class(&class);
}
gtk_image
}
}
#[derive(Debug)]
@ -217,10 +258,15 @@ impl Module<gtk::Box> for CustomModule {
}
self.bar.clone().into_iter().for_each(|widget| {
widget.add_to(&container, context.controller_tx.clone(), orientation);
widget.add_to(
&container,
context.controller_tx.clone(),
orientation,
info.icon_theme,
);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
@ -232,6 +278,7 @@ impl Module<gtk::Box> for CustomModule {
self,
tx: Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
@ -245,11 +292,18 @@ impl Module<gtk::Box> for CustomModule {
}
if let Some(popup) = self.popup {
popup
.into_iter()
.for_each(|widget| widget.add_to(&container, tx.clone(), Orientation::Horizontal));
for widget in popup {
widget.add_to(
&container,
tx.clone(),
Orientation::Horizontal,
info.icon_theme,
);
}
}
container.show_all();
Some(container)
}
}

View file

@ -1,14 +1,16 @@
use crate::clients::wayland::{self, ToplevelChange};
use crate::config::{CommonConfig, TruncateMode};
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{await_sync, icon, read_lock, send_async};
use crate::{await_sync, read_lock, send_async};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{IconTheme, Image, Label};
use gtk::Label;
use serde::Deserialize;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule {
@ -22,8 +24,6 @@ pub struct FocusedModule {
/// Icon size in pixels.
#[serde(default = "default_icon_size")]
icon_size: i32,
/// GTK icon theme to use.
icon_theme: Option<String>,
truncate: Option<TruncateMode>,
@ -93,15 +93,11 @@ impl Module<gtk::Box> for FocusedModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(&theme));
}
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = Image::builder().name("icon").build();
let icon = gtk::Image::builder().name("icon").build();
let label = Label::builder().name("label").build();
if let Some(truncate) = self.truncate {
@ -112,11 +108,14 @@ impl Module<gtk::Box> for FocusedModule {
container.add(&label);
{
let icon_theme = icon_theme.clone();
context.widget_rx.attach(None, move |(name, id)| {
let pixbuf = icon::get_icon(&icon_theme, &id, self.icon_size);
if self.show_icon {
icon.set_pixbuf(pixbuf.as_ref());
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
.and_then(|image| image.load_into_image(icon.clone()))
{
error!("{err:?}");
}
}
if self.show_title {

View file

@ -1,16 +1,17 @@
use super::open_state::OpenState;
use crate::clients::wayland::ToplevelInfo;
use crate::icon::get_icon;
use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::{read_lock, try_send};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Orientation};
use gtk::{Button, IconTheme, Orientation};
use indexmap::IndexMap;
use std::rc::Rc;
use std::sync::RwLock;
use tokio::sync::mpsc::Sender;
use tracing::error;
#[derive(Debug, Clone)]
pub struct Item {
@ -151,16 +152,24 @@ impl ItemButton {
button = button.label(&item.name);
}
if show_icons {
let icon = get_icon(icon_theme, &item.app_id, 32);
if icon.is_some() {
let image = Image::from_pixbuf(icon.as_ref());
button = button.image(&image).always_show_image(true);
}
}
let button = button.build();
if show_icons {
let gtk_image = gtk::Image::new();
let image = ImageProvider::parse(&item.app_id.clone(), icon_theme, 32);
match image {
Ok(image) => {
button.set_image(Some(&gtk_image));
button.set_always_show_image(true);
if let Err(err) = image.load_into_image(gtk_image) {
error!("{err:?}");
}
}
Err(err) => error!("{err:?}"),
};
}
let style_context = button.style_context();
style_context.add_class("item");

View file

@ -5,13 +5,13 @@ use self::item::{Item, ItemButton, Window};
use self::open_state::OpenState;
use crate::clients::wayland::{self, ToplevelChange};
use crate::config::CommonConfig;
use crate::icon::find_desktop_file;
use crate::desktop_file::find_desktop_file;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{lock, read_lock, try_send, write_lock};
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use gtk::{Button, Orientation};
use indexmap::IndexMap;
use serde::Deserialize;
use std::process::{Command, Stdio};
@ -33,9 +33,6 @@ pub struct LauncherModule {
#[serde(default = "crate::config::default_true")]
show_icons: bool,
/// Name of the GTK icon theme to use.
icon_theme: Option<String>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
@ -309,15 +306,15 @@ impl Module<gtk::Box> for LauncherModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> crate::Result<ModuleWidget<gtk::Box>> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
{
let container = container.clone();
let icon_theme = icon_theme.clone();
let controller_tx = context.controller_tx.clone();
let show_names = self.show_names;
let show_icons = self.show_icons;
@ -325,7 +322,6 @@ impl Module<gtk::Box> for LauncherModule {
let mut buttons = IndexMap::<String, ItemButton>::new();
let controller_tx2 = context.controller_tx.clone();
context.widget_rx.attach(None, move |event| {
match event {
LauncherUpdate::AddItem(item) => {
@ -341,7 +337,7 @@ impl Module<gtk::Box> for LauncherModule {
orientation,
&icon_theme,
&context.tx,
&controller_tx2,
&controller_tx,
);
container.add(&button.button);
@ -400,7 +396,7 @@ impl Module<gtk::Box> for LauncherModule {
});
}
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: container,
popup,
@ -411,6 +407,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
controller_tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
const MAX_WIDTH: i32 = 250;

View file

@ -19,7 +19,7 @@ use crate::popup::ButtonGeometry;
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::Monitor;
use gtk::{Application, Widget};
use gtk::{Application, IconTheme, Widget};
use tokio::sync::mpsc;
#[derive(Clone)]
@ -34,6 +34,7 @@ pub struct ModuleInfo<'a> {
pub bar_position: BarPosition,
pub monitor: &'a Monitor,
pub output_name: &'a str,
pub icon_theme: &'a IconTheme,
}
#[derive(Debug)]
@ -88,6 +89,7 @@ where
self,
_tx: mpsc::Sender<Self::ReceiveMessage>,
_rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,

View file

@ -8,12 +8,34 @@ pub struct Icons {
/// Icon to display when playing.
#[serde(default = "default_icon_play")]
pub(crate) play: String,
/// Icon to display when paused.
#[serde(default = "default_icon_pause")]
pub(crate) pause: String,
/// Icon to display for previous button.
#[serde(default = "default_icon_prev")]
pub(crate) prev: String,
/// Icon to display for next button.
#[serde(default = "default_icon_next")]
pub(crate) next: String,
/// Icon to display under volume slider
#[serde(default = "default_icon_volume")]
pub(crate) volume: String,
/// Icon to display nex to track title
#[serde(default = "default_icon_track")]
pub(crate) track: String,
/// Icon to display nex to album name
#[serde(default = "default_icon_album")]
pub(crate) album: String,
/// Icon to display nex to artist name
#[serde(default = "default_icon_artist")]
pub(crate) artist: String,
}
impl Default for Icons {
@ -21,7 +43,12 @@ impl Default for Icons {
Self {
pause: default_icon_pause(),
play: default_icon_play(),
prev: default_icon_prev(),
next: default_icon_next(),
volume: default_icon_volume(),
track: default_icon_track(),
album: default_icon_album(),
artist: default_icon_artist(),
}
}
}
@ -73,7 +100,7 @@ fn default_socket() -> String {
}
fn default_format() -> String {
String::from("{icon} {title} / {artist}")
String::from("{title} / {artist}")
}
fn default_icon_play() -> String {
@ -84,10 +111,30 @@ fn default_icon_pause() -> String {
String::from("")
}
fn default_icon_prev() -> String {
String::from("\u{f9ad}")
}
fn default_icon_next() -> String {
String::from("\u{f9ac}")
}
fn default_icon_volume() -> String {
String::from("")
}
fn default_icon_track() -> String {
String::from("\u{f886}")
}
fn default_icon_album() -> String {
String::from("\u{f524}")
}
fn default_icon_artist() -> String {
String::from("\u{fd01}")
}
fn default_music_dir() -> PathBuf {
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
}

View file

@ -1,14 +1,14 @@
mod config;
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use color_eyre::Result;
use glib::Continue;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{Button, Image, Label, Orientation, Scale};
use gtk::{Button, IconTheme, Label, Orientation, Scale};
use regex::Regex;
use std::path::PathBuf;
use std::sync::Arc;
@ -18,7 +18,7 @@ use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
pub use self::config::MusicModule;
use self::config::{Icons, PlayerType};
use self::config::PlayerType;
#[derive(Debug)]
pub enum PlayerCommand {
@ -80,7 +80,6 @@ impl Module<Button> for MusicModule {
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let format = self.format.clone();
let icons = self.icons.clone();
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
@ -102,13 +101,8 @@ impl Module<Button> for MusicModule {
match update {
PlayerUpdate::Update(track, status) => match *track {
Some(track) => {
let display_string = replace_tokens(
format.as_str(),
&tokens,
&track,
&status,
&icons,
);
let display_string =
replace_tokens(format.as_str(), &tokens, &track, &status);
let update = SongUpdate {
song: track,
@ -160,15 +154,22 @@ impl Module<Button> for MusicModule {
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
let button = Button::new();
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
button.add(&button_contents);
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, 24);
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, 24);
let label = Label::new(None);
label.set_angle(info.bar_position.get_angle());
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
}
button.add(&label);
button_contents.add(&icon_pause);
button_contents.add(&icon_play);
button_contents.add(&label);
let orientation = info.bar_position.get_orientation();
@ -190,6 +191,21 @@ impl Module<Button> for MusicModule {
context.widget_rx.attach(None, move |mut event| {
if let Some(event) = event.take() {
label.set_label(&event.display_string);
match event.status.state {
PlayerState::Playing => {
icon_play.show();
icon_pause.hide();
}
PlayerState::Paused => {
icon_pause.show();
icon_play.hide();
}
PlayerState::Stopped => {
button.hide();
}
}
button.show();
} else {
button.hide();
@ -200,7 +216,7 @@ impl Module<Button> for MusicModule {
});
};
let popup = self.into_popup(context.controller_tx, context.popup_rx);
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
widget: button,
@ -212,23 +228,28 @@ impl Module<Button> for MusicModule {
self,
tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
info: &ModuleInfo,
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;
let container = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.spacing(10)
.name("popup-music")
.build();
let album_image = Image::builder()
let album_image = gtk::Image::builder()
.width_request(128)
.height_request(128)
.name("album-art")
.build();
let icons = self.icons;
let info_box = gtk::Box::new(Orientation::Vertical, 10);
let title_label = IconLabel::new("\u{f886}", None);
let album_label = IconLabel::new("\u{f524}", None);
let artist_label = IconLabel::new("\u{fd01}", None);
let title_label = IconLabel::new(&icons.track, None, icon_theme);
let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
title_label.container.set_widget_name("title");
album_label.container.set_widget_name("album");
@ -239,12 +260,22 @@ impl Module<Button> for MusicModule {
info_box.add(&artist_label.container);
let controls_box = gtk::Box::builder().name("controls").build();
let btn_prev = Button::builder().label("\u{f9ad}").name("btn-prev").build();
let btn_play_pause = Button::builder().label("").name("btn-play-pause").build();
let btn_next = Button::builder().label("\u{f9ac}").name("btn-next").build();
let btn_prev = new_icon_button(&icons.prev, icon_theme, 24);
btn_prev.set_widget_name("btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, 24);
btn_play.set_widget_name("btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, 24);
btn_pause.set_widget_name("btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, 24);
btn_next.set_widget_name("btn-next");
controls_box.add(&btn_prev);
controls_box.add(&btn_play_pause);
controls_box.add(&btn_play);
controls_box.add(&btn_pause);
controls_box.add(&btn_next);
info_box.add(&controls_box);
@ -259,7 +290,7 @@ impl Module<Button> for MusicModule {
volume_slider.set_inverted(true);
volume_slider.set_widget_name("slider");
let volume_icon = Label::new(Some(&self.icons.volume));
let volume_icon = new_icon_label(&icons.volume, icon_theme, 24);
volume_icon.style_context().add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0);
@ -274,13 +305,14 @@ impl Module<Button> for MusicModule {
try_send!(tx_prev, PlayerCommand::Previous);
});
let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |button| {
if button.style_context().has_class("playing") {
try_send!(tx_toggle, PlayerCommand::Pause);
} else {
try_send!(tx_toggle, PlayerCommand::Play);
}
let tx_play = tx.clone();
btn_play.connect_clicked(move |_| {
try_send!(tx_play, PlayerCommand::Play);
});
let tx_pause = tx.clone();
btn_pause.connect_clicked(move |_| {
try_send!(tx_pause, PlayerCommand::Pause);
});
let tx_next = tx.clone();
@ -297,6 +329,8 @@ impl Module<Button> for MusicModule {
container.show_all();
{
let icon_theme = icon_theme.clone();
let mut prev_cover = None;
rx.attach(None, move |update| {
if let Some(update) = update {
@ -304,16 +338,22 @@ impl Module<Button> for MusicModule {
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover = new_cover.clone();
match new_cover.map(|cover_path| {
Pixbuf::from_file_at_scale(cover_path, 128, 128, true)
}) {
Some(Ok(pixbuf)) => album_image.set_from_pixbuf(Some(&pixbuf)),
let res = match new_cover
.map(|cover_path| ImageProvider::parse(&cover_path, &icon_theme, 128))
{
Some(Ok(image)) => image.load_into_image(album_image.clone()),
Some(Err(err)) => {
error!("{:?}", err);
album_image.set_from_pixbuf(None);
Err(err)
}
None => {
album_image.set_from_pixbuf(None);
Ok(())
}
None => album_image.set_from_pixbuf(None),
};
if let Err(err) = res {
error!("{err:?}");
}
}
title_label
@ -328,23 +368,23 @@ impl Module<Button> for MusicModule {
match update.status.state {
PlayerState::Stopped => {
btn_play_pause.set_sensitive(false);
btn_pause.hide();
btn_play.show();
btn_play.set_sensitive(false);
}
PlayerState::Playing => {
btn_play_pause.set_sensitive(true);
btn_play_pause.set_label(&self.icons.pause);
btn_play.set_sensitive(false);
btn_play.hide();
let style_context = btn_play_pause.style_context();
style_context.add_class("playing");
style_context.remove_class("paused");
btn_pause.set_sensitive(true);
btn_pause.show();
}
PlayerState::Paused => {
btn_play_pause.set_sensitive(true);
btn_play_pause.set_label(&self.icons.play);
btn_pause.set_sensitive(false);
btn_pause.hide();
let style_context = btn_play_pause.style_context();
style_context.add_class("paused");
style_context.remove_class("playing");
btn_play.set_sensitive(true);
btn_play.show();
}
}
@ -374,11 +414,10 @@ fn replace_tokens(
tokens: &Vec<String>,
song: &Track,
status: &Status,
icons: &Icons,
) -> String {
let mut compiled_string = format_string.to_string();
for token in tokens {
let value = get_token_value(song, status, icons, token);
let value = get_token_value(song, status, token);
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
}
compiled_string
@ -386,14 +425,8 @@ fn replace_tokens(
/// Converts a string format token value
/// into its respective value.
fn get_token_value(song: &Track, status: &Status, icons: &Icons, token: &str) -> String {
fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
match token {
"icon" => match status.state {
PlayerState::Stopped => None,
PlayerState::Playing => Some(&icons.play),
PlayerState::Paused => Some(&icons.pause),
}
.map(std::string::ToString::to_string),
"title" => song.title.clone(),
"album" => song.album.clone(),
"artist" => song.artist.clone(),
@ -408,17 +441,17 @@ fn get_token_value(song: &Track, status: &Status, icons: &Icons, token: &str) ->
.unwrap_or_default()
}
#[derive(Clone)]
#[derive(Clone, Debug)]
struct IconLabel {
label: Label,
container: gtk::Box,
}
impl IconLabel {
fn new(icon: &str, label: Option<&str>) -> Self {
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = Label::new(Some(icon));
let icon = new_icon_label(icon_input, icon_theme, 32);
let label = Label::new(label);
icon.style_context().add_class("icon");

View file

@ -1,10 +1,11 @@
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::Button;
use gtk::{Button, IconTheme};
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::HashMap;
@ -49,12 +50,13 @@ fn create_button(
name: &str,
focused: bool,
name_map: &HashMap<String, String>,
icon_theme: &IconTheme,
tx: &Sender<String>,
) -> Button {
let button = Button::builder()
.label(name_map.get(name).map_or(name, String::as_str))
.name(name)
.build();
let label = name_map.get(name).map_or(name, String::as_str);
let button = new_icon_button(label, icon_theme, 32);
button.set_widget_name(name);
let style_context = button.style_context();
style_context.add_class("item");
@ -154,6 +156,7 @@ impl Module<gtk::Box> for WorkspacesModule {
{
let container = container.clone();
let output_name = info.output_name.to_string();
let icon_theme = info.icon_theme.clone();
// keep track of whether init event has fired previously
// since it fires for every workspace subscriber
@ -170,6 +173,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&workspace.name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
container.add(&item);
@ -204,6 +208,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
@ -227,6 +232,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);

View file

@ -133,7 +133,7 @@ impl Popup {
/// Shows the popup
pub fn show(&self, geometry: ButtonGeometry) {
self.window.show_all();
self.window.show();
self.set_pos(geometry);
}