1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-16 22:31:03 +02:00

feat(sysinfo): overhaul to add aggregate/unit/formatting support

This completely reworks the sysinfo module to add support for aggregate functions, better support for working with individual devices, the ability to specify units, and some string formatting support.

Several new tokens have also been added, and performance should be marginally improved.

BREAKING CHANGE: Use of the `sys_info` module in your config will need to be updated to use the new token format. See the wiki page for more info.
This commit is contained in:
Jake Stanger 2025-01-04 23:08:01 +00:00
parent 49ab7e0c7b
commit 01de0ac6f5
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
14 changed files with 1633 additions and 589 deletions

105
Cargo.lock generated
View file

@ -472,9 +472,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.4" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
@ -1392,15 +1392,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -1565,7 +1556,7 @@ dependencies = [
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"windows-core", "windows-core 0.52.0",
] ]
[[package]] [[package]]
@ -1664,7 +1655,7 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [ dependencies = [
"hermit-abi 0.3.9", "hermit-abi",
"libc", "libc",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1964,7 +1955,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
dependencies = [ dependencies = [
"hermit-abi 0.3.9", "hermit-abi",
"libc", "libc",
"log", "log",
"wasi", "wasi",
@ -2168,16 +2159,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "num_cpus"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi 0.2.6",
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.30.3" version = "0.30.3"
@ -2519,9 +2500,9 @@ dependencies = [
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.7.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [ dependencies = [
"either", "either",
"rayon-core", "rayon-core",
@ -2529,14 +2510,12 @@ dependencies = [
[[package]] [[package]]
name = "rayon-core" name = "rayon-core"
version = "1.11.0" version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [ dependencies = [
"crossbeam-channel",
"crossbeam-deque", "crossbeam-deque",
"crossbeam-utils", "crossbeam-utils",
"num_cpus",
] ]
[[package]] [[package]]
@ -3162,17 +3141,16 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.29.11" version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
dependencies = [ dependencies = [
"cfg-if",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"memchr",
"ntapi", "ntapi",
"once_cell",
"rayon", "rayon",
"winapi", "windows",
] ]
[[package]] [[package]]
@ -3965,6 +3943,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"
@ -3974,17 +3962,60 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote 1.0.38",
"syn 2.0.92",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote 1.0.38",
"syn 2.0.92",
]
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [ dependencies = [
"windows-result", "windows-result 0.2.0",
"windows-strings", "windows-strings",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.2.0" version = "0.2.0"
@ -4000,7 +4031,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [ dependencies = [
"windows-result", "windows-result 0.2.0",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]

View file

@ -63,7 +63,7 @@ keyboard = ["dep:input", "dep:evdev-rs", "dep:libc", "dep:nix"]
launcher = [] launcher = []
music = ["regex"] music = ["dep:regex"]
"music+all" = ["music", "music+mpris", "music+mpd"] "music+all" = ["music", "music+mpris", "music+mpd"]
"music+mpris" = ["music", "mpris"] "music+mpris" = ["music", "mpris"]
"music+mpd" = ["music", "mpd-utils"] "music+mpd" = ["music", "mpd-utils"]
@ -72,7 +72,7 @@ network_manager = ["futures-lite", "futures-signals", "zbus"]
notifications = ["zbus"] notifications = ["zbus"]
sys_info = ["sysinfo", "regex"] sys_info = ["dep:sysinfo"]
tray = ["system-tray"] tray = ["system-tray"]
@ -148,12 +148,15 @@ libc = { version = "0.2.164", optional = true }
# music # music
mpd-utils = { version = "0.2.1", optional = true } mpd-utils = { version = "0.2.1", optional = true }
mpris = { version = "2.0.1", optional = true } mpris = { version = "2.0.1", optional = true }
regex = { version = "1.11.1", default-features = false, features = [
"std",
], optional = true }
# network_manager # network_manager
futures-signals = { version = "0.3.34", optional = true } futures-signals = { version = "0.3.34", optional = true }
# sys_info # sys_info
sysinfo = { version = "0.29.11", optional = true } sysinfo = { version = "0.33.1", optional = true }
# tray # tray
system-tray = { version = "0.7.0", features = ["dbusmenu-gtk3"], optional = true } system-tray = { version = "0.7.0", features = ["dbusmenu-gtk3"], optional = true }
@ -164,9 +167,6 @@ libpulse-binding = { version = "2.28.2", optional = true }
# shared # shared
futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces
nix = { version = "0.29.0", optional = true, features = ["event", "fs", "poll"] } # clipboard, input nix = { version = "0.29.0", optional = true, features = ["event", "fs", "poll"] } # clipboard, input
regex = { version = "1.11.1", default-features = false, features = [
"std",
], optional = true } # music, sys_info
zbus = { version = "5.5.0", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower zbus = { version = "5.5.0", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower
swayipc-async = { version = "2.0.1", optional = true } # workspaces, keyboard swayipc-async = { version = "2.0.1", optional = true } # workspaces, keyboard
hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true } # workspaces, keyboard hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true } # workspaces, keyboard

View file

@ -3,6 +3,8 @@ Displays one or more labels containing system information.
Separating information across several labels allows for styling each one independently. Separating information across several labels allows for styling each one independently.
Pango markup is supported. Pango markup is supported.
Options can be provided in a token to specify operations, units and formatting.
![Screenshot showing sys-info module with widgets for all of the types of formatting tokens](https://user-images.githubusercontent.com/5057870/196059090-4056d083-69f0-4e6f-9673-9e35dc29d9f0.png) ![Screenshot showing sys-info module with widgets for all of the types of formatting tokens](https://user-images.githubusercontent.com/5057870/196059090-4056d083-69f0-4e6f-9673-9e35dc29d9f0.png)
@ -10,17 +12,17 @@ Pango markup is supported.
> Type: `sys_info` > Type: `sys_info`
| Name | Type | Default | Description | | Name | Type | Default | Description |
|--------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------------------| |--------------------|------------------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------|
| `format` | `string[]` | `null` | Array of strings including formatting tokens. For available tokens see below. | | `format` | `string[]` | `null` | Array of strings including formatting tokens. For available tokens see below. |
| `interval` | `integer` or `Map` | `5` | Seconds between refreshing. Can be a single value for all data or a map of individual refresh values for different data types. | | `interval` | `integer` or `Map` | `5` | Seconds between refreshing. Can be a single value for all data or a map of individual refresh values for different data types. |
| `interval.memory` | `integer` | `5` | Seconds between refreshing memory data | | `interval.memory` | `integer` | `5` | Seconds between refreshing memory data. |
| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data | | `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data. |
| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data | | `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data. |
| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data | | `interval.disks` | `integer` | `5` | Seconds between refreshing disk data. |
| `interval.network` | `integer` | `5` | Seconds between refreshing network data | | `interval.network` | `integer` | `5` | Seconds between refreshing network data. |
| `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the labels. | | `orientation` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | Orientation of the labels. |
| `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | How the labels are laid out (not the rotation of an individual label). | | `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | `'horizontal'` | How the labels are laid out (not the rotation of an individual label). |
<details> <details>
<summary>JSON</summary> <summary>JSON</summary>
@ -30,12 +32,11 @@ Pango markup is supported.
"end": [ "end": [
{ {
"format": [ "format": [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)", " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)", "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", "󰖡 {load_average_1} | {load_average_5} | {load_average_15}",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}",
"󰥔 {uptime}" "󰥔 {uptime}"
], ],
"interval": { "interval": {
@ -60,13 +61,12 @@ Pango markup is supported.
[[end]] [[end]]
type = 'sys_info' type = 'sys_info'
format = [ format = [
' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C', " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
' {memory_used} / {memory_total} GB ({memory_percent}%)', " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
'| {swap_used} / {swap_total} GB ({swap_percent}%)', "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
'󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)', "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
'󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps', "󰖡 {load_average_1} | {load_average_5} | {load_average_15}",
'󰖡 {load_average:1} | {load_average:5} | {load_average:15}', "󰥔 {uptime}"
'󰥔 {uptime}',
] ]
[end.interval] [end.interval]
@ -87,13 +87,12 @@ temps = 5
```yaml ```yaml
end: end:
- format: - format:
- ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C' - " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C"
- ' {memory_used} / {memory_total} GB ({memory_percent}%)' - " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)"
- '| {swap_used} / {swap_total} GB ({swap_percent}%)' - "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s"
- '󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)' - "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps"
- '󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps' - "󰖡 {load_average_1} | {load_average_5} | {load_average_15}"
- '󰖡 {load_average:1} | {load_average:5} | {load_average:15}' - "󰥔 {uptime}"
- '󰥔 {uptime}'
interval: interval:
cpu: 1 cpu: 1
disks: 300 disks: 300
@ -121,12 +120,11 @@ end:
interval.networks = 3 interval.networks = 3
format = [ format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C" " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)" " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)" "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)" "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps"
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps" "󰖡 {load_average_1} | {load_average_5} | {load_average_15}"
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}"
"󰥔 {uptime}" "󰥔 {uptime}"
] ]
} }
@ -138,39 +136,179 @@ end:
### Formatting Tokens ### Formatting Tokens
The following tokens can be used in the `format` configuration option: The below table lists the tokens which can be used in the `format` configuration option.
More information about each of these and the additional options can be found further below.
| Token | Description | | Token | Default Function | Default Unit | Default Formatting |
|--------------------------|------------------------------------------------------------------------------------| |--------------------------|------------------|--------------|--------------------|
| **CPU** | | | **CPU** | | | |
| `{cpu_percent}` | Total CPU utilisation percentage | | `{cpu_frequency[#core]}` | `mean` | MHz | `.2` |
| **Memory** | | | `{cpu_percent[#core]}` | `mean` | % | `0<2` |
| `{memory_free}` | Memory free in GB. | | **Memory** | | | |
| `{memory_used}` | Memory used in GB. | | `{memory_free}` | N/A | GB | `0<4.1` |
| `{memory_total}` | Memory total in GB. | | `{memory_available}` | N/A | GB | `0<4.1` |
| `{memory_percent}` | Memory utilisation percentage. | | `{memory_used}` | N/A | GB | `0<4.1` |
| `{swap_free}` | Swap free in GB. | | `{memory_total}` | N/A | GB | `0<4.1` |
| `{swap_used}` | Swap used in GB. | | `{memory_percent}` | N/A | GB | `0<4.1` |
| `{swap_total}` | Swap total in GB. | | `{swap_free}` | N/A | GB | `0<4.1` |
| `{swap_percent}` | Swap utilisation percentage. | | `{swap_used}` | N/A | GB | `0<4.1` |
| **Temperature** | | | `{swap_total}` | N/A | GB | `0<4.1` |
| `{temp_c:[sensor]}` | Temperature in degrees C. Replace `[sensor]` with the sensor label. | | `{swap_percent}` | N/A | GB | `0<4.1` |
| `{temp_f:[sensor]}` | Temperature in degrees F. Replace `[sensor]` with the sensor label. | | **Temperature** | | | |
| **Disk** | | | `{temp_c[#sensor]}` | `max` | °C | |
| `{disk_free:[mount]}` | Disk free space in GB. Replace `[mount]` with the disk mountpoint. | | `{temp_f[#sensor]}` | `max` | °F | |
| `{disk_used:[mount]}` | Disk used space in GB. Replace `[mount]` with the disk mountpoint. | | **Disk** | | | |
| `{disk_total:[mount]}` | Disk total space in GB. Replace `[mount]` with the disk mountpoint. | | `{disk_free[#mount]}` | `sum` | GB | |
| `{disk_percent:[mount]}` | Disk utilisation percentage. Replace `[mount]` with the disk mountpoint. | | `{disk_used[#mount]}` | `sum` | GB | |
| **Network** | | | `{disk_total[#mount]}` | `sum` | GB | |
| `{net_down:[adapter]}` | Average network download speed in Mbps. Replace `[adapter]` with the adapter name. | | `{disk_percent[#mount]}` | `sum` | % | |
| `{net_up:[adapter]}` | Average network upload speed in Mbps. Replace `[adapter]` with the adapter name. | | `{disk_read[#mount]}` | `sum` | MB/s | |
| **System** | | | `{disk_write[#mount]}` | `sum` | MB/s | |
| `{load_average:1}` | 1-minute load average. | | **Network** | | | |
| `{load_average:5}` | 5-minute load average. | | `{net_down[#adapter]}` | `sum` | Mb/s | |
| `{load_average:15}` | 15-minute load average. | | `{net_up[#adapter]}` | `sum` | Mb/s | |
| `{uptime}` | System uptime formatted as `HH:mm`. | | **System** | | | |
| `{load_average_1}` | N/A | - | `.2` |
| `{load_average_5}` | N/A | - | `.2` |
| `{load_average_15}` | N/A | - | `.2` |
| `{uptime}` | N/A | ??? | ??? |
For Intel CPUs, you can typically use `coretemp-Package-id-0` for the temperature sensor. For AMD, you can use `k10temp-Tccd1`. #### Functions and names
Many of the tokens operate on a value set, as opposed to an individual value:
- CPU tokens operate on each physical thread.
- Temperature tokens operate on each sensor.
- Disk tokens operate on each mount.
- Network tokens operate on each adapter.
By default, these will apply a function to the full set to reduce them down to a single value.
The list of available functions is shown below:
| Function | Description |
|----------|-----------------------------------------|
| `sum` | Adds each value in the set. |
| `min` | Gets the smallest value in the set. |
| `max` | Gets the largest value in the set. |
| `mean` | Gets the mean average value of the set. |
It is also possible to get only a single value from the set by specifying a name instead of a function.
| Token category | Valid name |
|----------------|-------------------------------------------------------------------------|
| CPU | A CPU thread, eg `cpu0`, `cpu1`, ... |
| Temperature | A sensor name, eg `CPUTIN`. These line up with the output of `sensors`. |
| Disk | A disk mountpoint, eg `/`, `/home`, ... |
| Network | An adapter name, eg `eth0` or `enp30s0`. |
To specify a name or function, use a `@`. For example, to show disk percent for `/home`:
```json
"{disk_percent@/home}%"
```
To show total CPU utilization where each core represents 100% (like `htop` etc):
```json
"{cpu_percent@sum}%"
```
#### Prefixes and units
For tokens which return an appropriate unit, you can specify the SI prefix (or unit in some special cases).
The following options can be supplied:
| Name | Value |
|---------|-------|
| Kilo | `k` |
| Mega | `M` |
| Giga | `G` |
| Tera | `T` |
| Peta | `P` |
| | |
| Kibi | `ki` |
| Mebi | `Mi` |
| Gibi | `Gi` |
| Tebi | `Ti` |
| Pebi | `Pi` |
| | |
| Kilobit | `kb` |
| Megabit | `Mb` |
| Gigabit | `Gb` |
To specify a prefix or unit, use a `#`. For example, to show free total disk space in terabytes:
```json
"{disk_free#T} TB"
```
#### Formatting
To control the formatting of the resultant number,
a subset of Rust's string formatting is implemented. This includes:
- Width
- Fill/Alignment
- Precision
Formatting is specified with a `:` and MUST be the last part of a token.
##### Width
The width controls the minimum string length of the value.
Specifying just a width will left-pad the value with `0` until the value reaches the target length.
The width can be any value from `1-9`. Larger values are not supported.
For example, to render CPU usage as `045%`:
```json
"{cpu_usage:3}%"
```
##### Fill/Alignment
These options can be used to control the `width` property.
To specify the fill and alignment, prefix the width with a character and a direction.
Fill characters can be any single UTF-8 character EXCEPT 1-9. Alignment must be one of:
- `<` - Left fill
- `^` - Center fill
- `>` - Right fill
For example, to render CPU usage as ` 45%`:
```json
"{cpu_usage: <3}%"
```
##### Precision
The number of decimal places a value is shown to can be controlled using precision.
Any value is supported.
To specify precision, include a `.` followed by the value. If other options are supplied, this MUST come after.
For example, to render used disk space to 2dp:
```json
"{disk_used:.2} GB"
```
---
#### Combining Options
Each of the token options can be combined to create more complex solutions.
Putting it all together, you could show the free disk space on your `/home` partition in terabytes,
left-padded with spaces to a min width of 5, and shown to 2dp as follows:
```json
"{disk_used@/home#T: <5.2} TB"
```
## Styling ## Styling

View file

@ -55,13 +55,12 @@ let {
interval.networks = 3 interval.networks = 3
format = [ format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C" " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C"
" {memory_used} / {memory_total} GB ({memory_percent}%)" " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)"
"| {swap_used} / {swap_total} GB ({swap_percent}%)" "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s"
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)" "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps"
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps" "󰖡 {load_average1} | {load_average5} | {load_average15}"
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}" "󰥔 {uptime}"
"󰥔 {uptime}"
] ]
} }

View file

@ -1,5 +1,4 @@
{ {
"$schema": "https://f.jstanger.dev/github/ironbar/schema.json",
"anchor_to_edges": true, "anchor_to_edges": true,
"position": "bottom", "position": "bottom",
"icon_theme": "Paper", "icon_theme": "Paper",
@ -64,12 +63,11 @@
"networks": 3 "networks": 3
}, },
"format": [ "format": [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)", " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)", "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", "󰖡 {load_average1} | {load_average5} | {load_average15}",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}",
"󰥔 {uptime}" "󰥔 {uptime}"
] ]
}, },

View file

@ -53,12 +53,11 @@ interval = 500
[[end]] [[end]]
type = "sys_info" type = "sys_info"
format = [ format = [
" {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C",
" {memory_used} / {memory_total} GB ({memory_percent}%)", " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)",
"| {swap_used} / {swap_total} GB ({swap_percent}%)", "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s",
"󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps",
"󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", "󰖡 {load_average1} | {load_average5} | {load_average15}",
"󰖡 {load_average:1} | {load_average:5} | {load_average:15}",
"󰥔 {uptime}", "󰥔 {uptime}",
] ]

View file

@ -1,4 +1,3 @@
$schema: https://f.jstanger.dev/github/ironbar/schema.json
anchor_to_edges: true anchor_to_edges: true
position: bottom position: bottom
icon_theme: Paper icon_theme: Paper
@ -44,12 +43,11 @@ end:
disks: 300 disks: 300
networks: 3 networks: 3
format: format:
-  {cpu_percent}% | {temp_c:k10temp-Tccd1}°C -  {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C
-  {memory_used} / {memory_total} GB ({memory_percent}%) -  {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)
- '| {swap_used} / {swap_total} GB ({swap_percent}%)' - 󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s
- 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%) - 󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps
- 󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps - 󰖡 {load_average1} | {load_average5} | {load_average15}
- 󰖡 {load_average:1} | {load_average:5} | {load_average:15}
- 󰥔 {uptime} - 󰥔 {uptime}
- type: volume - type: volume
format: '{icon} {percentage}%' format: '{icon} {percentage}%'

View file

@ -21,6 +21,8 @@ pub mod networkmanager;
pub mod sway; pub mod sway;
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
pub mod swaync; pub mod swaync;
#[cfg(feature = "sys_info")]
pub mod sysinfo;
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
pub mod tray; pub mod tray;
#[cfg(feature = "upower")] #[cfg(feature = "upower")]
@ -54,6 +56,8 @@ pub struct Clients {
network_manager: Option<Arc<networkmanager::Client>>, network_manager: Option<Arc<networkmanager::Client>>,
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
notifications: Option<Arc<swaync::Client>>, notifications: Option<Arc<swaync::Client>>,
#[cfg(feature = "sys_info")]
sys_info: Option<Arc<sysinfo::Client>>,
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
tray: Option<Arc<tray::Client>>, tray: Option<Arc<tray::Client>>,
#[cfg(feature = "upower")] #[cfg(feature = "upower")]
@ -185,6 +189,13 @@ impl Clients {
Ok(client) Ok(client)
} }
#[cfg(feature = "sys_info")]
pub fn sys_info(&mut self) -> Arc<sysinfo::Client> {
self.sys_info
.get_or_insert_with(|| Arc::new(sysinfo::Client::new()))
.clone()
}
#[cfg(feature = "tray")] #[cfg(feature = "tray")]
pub fn tray(&mut self) -> ClientResult<tray::Client> { pub fn tray(&mut self) -> ClientResult<tray::Client> {
let client = if let Some(client) = &self.tray { let client = if let Some(client) = &self.tray {

390
src/clients/sysinfo.rs Normal file
View file

@ -0,0 +1,390 @@
use crate::modules::sysinfo::Interval;
use crate::{lock, register_client};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Mutex;
use sysinfo::{Components, Disks, LoadAvg, Networks, RefreshKind, System};
#[repr(u64)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum Prefix {
#[default]
None = 1,
Kilo = 1000,
Mega = Prefix::Kilo as u64 * 1000,
Giga = Prefix::Mega as u64 * 1000,
Tera = Prefix::Giga as u64 * 1000,
Peta = Prefix::Tera as u64 * 1000,
Kibi = 1024,
Mebi = Prefix::Kibi as u64 * 1024,
Gibi = Prefix::Mebi as u64 * 1024,
Tebi = Prefix::Gibi as u64 * 1024,
Pebi = Prefix::Tebi as u64 * 1024,
// # Units
// These are special cases
// where you'd actually want to do slightly more than a prefix alone.
// Included as part of the prefix system for simplicity.
KiloBit = 128,
MegaBit = Prefix::KiloBit as u64 * 1024,
GigaBit = Prefix::MegaBit as u64 * 1024,
}
#[derive(Debug, Clone)]
pub enum Function {
None,
Sum,
Min,
Max,
Mean,
Name(String),
}
#[derive(Debug)]
pub struct ValueSet {
values: HashMap<Box<str>, Value>,
}
impl FromIterator<(Box<str>, Value)> for ValueSet {
fn from_iter<T: IntoIterator<Item = (Box<str>, Value)>>(iter: T) -> Self {
Self {
values: iter.into_iter().collect(),
}
}
}
impl ValueSet {
fn values(&self, prefix: Prefix) -> impl Iterator<Item = f64> + use<'_> {
self.values
.values()
.map(move |v| v.get(prefix))
.filter(|v| !v.is_nan())
}
pub fn apply(&self, function: &Function, prefix: Prefix) -> f64 {
match function {
Function::None => 0.0,
Function::Sum => self.sum(prefix),
Function::Min => self.min(prefix),
Function::Max => self.max(prefix),
Function::Mean => self.mean(prefix),
Function::Name(name) => self
.values
.get(&Box::from(name.as_str()))
.map(|v| v.get(prefix))
.unwrap_or_default(),
}
}
fn sum(&self, prefix: Prefix) -> f64 {
self.values(prefix).sum()
}
fn min(&self, prefix: Prefix) -> f64 {
self.values(prefix)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.unwrap_or_default()
}
fn max(&self, prefix: Prefix) -> f64 {
self.values(prefix)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
.unwrap_or_default()
}
fn mean(&self, prefix: Prefix) -> f64 {
self.sum(prefix) / self.values.len() as f64
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Value {
value: f64,
prefix: Prefix,
}
impl Value {
pub fn new(value: f64) -> Self {
Self::new_with_prefix(value, Prefix::None)
}
pub fn new_with_prefix(value: f64, prefix: Prefix) -> Self {
Self { value, prefix }
}
pub fn get(self, prefix: Prefix) -> f64 {
if prefix == self.prefix {
self.value
} else {
let scale = self.prefix as u64 as f64 / prefix as u64 as f64;
self.value * scale
}
}
}
#[derive(Debug)]
pub struct Client {
system: Mutex<System>,
disks: Mutex<Disks>,
components: Mutex<Components>,
networks: Mutex<Networks>,
load_average: Mutex<LoadAvg>,
}
impl Client {
pub fn new() -> Self {
let refresh_kind = RefreshKind::everything().without_processes();
let system = System::new_with_specifics(refresh_kind);
let disks = Disks::new_with_refreshed_list();
let components = Components::new_with_refreshed_list();
let networks = Networks::new_with_refreshed_list();
let load_average = System::load_average();
Self {
system: Mutex::new(system),
disks: Mutex::new(disks),
components: Mutex::new(components),
networks: Mutex::new(networks),
load_average: Mutex::new(load_average),
}
}
pub fn refresh_cpu(&self) {
lock!(self.system).refresh_cpu_all();
}
pub fn refresh_memory(&self) {
lock!(self.system).refresh_memory();
}
pub fn refresh_network(&self) {
lock!(self.networks).refresh(true);
}
pub fn refresh_temps(&self) {
lock!(self.components).refresh(true);
}
pub fn refresh_disks(&self) {
lock!(self.disks).refresh(true);
}
pub fn refresh_load_average(&self) {
*lock!(self.load_average) = System::load_average();
}
pub fn cpu_frequency(&self) -> ValueSet {
lock!(self.system)
.cpus()
.iter()
.map(|cpu| {
(
cpu.name().into(),
Value::new_with_prefix(cpu.frequency() as f64, Prefix::Mega),
)
})
.collect()
}
pub fn cpu_percent(&self) -> ValueSet {
lock!(self.system)
.cpus()
.iter()
.map(|cpu| (cpu.name().into(), Value::new(cpu.cpu_usage() as f64)))
.collect()
}
pub fn memory_free(&self) -> Value {
Value::new(lock!(self.system).free_memory() as f64)
}
pub fn memory_available(&self) -> Value {
Value::new(lock!(self.system).available_memory() as f64)
}
pub fn memory_total(&self) -> Value {
Value::new(lock!(self.system).total_memory() as f64)
}
pub fn memory_used(&self) -> Value {
Value::new(lock!(self.system).used_memory() as f64)
}
pub fn memory_percent(&self) -> Value {
let total = lock!(self.system).total_memory() as f64;
let used = lock!(self.system).used_memory() as f64;
Value::new(used / total * 100.0)
}
pub fn swap_free(&self) -> Value {
Value::new(lock!(self.system).free_swap() as f64)
}
pub fn swap_total(&self) -> Value {
Value::new(lock!(self.system).total_swap() as f64)
}
pub fn swap_used(&self) -> Value {
Value::new(lock!(self.system).used_swap() as f64)
}
pub fn swap_percent(&self) -> Value {
let total = lock!(self.system).total_swap() as f64;
let used = lock!(self.system).used_swap() as f64;
Value::new(used / total * 100.0)
}
pub fn temp_c(&self) -> ValueSet {
lock!(self.components)
.iter()
.map(|comp| {
(
comp.label().into(),
Value::new(comp.temperature().unwrap_or_default() as f64),
)
})
.collect()
}
pub fn temp_f(&self) -> ValueSet {
lock!(self.components)
.iter()
.map(|comp| {
(
comp.label().into(),
Value::new(c_to_f(comp.temperature().unwrap_or_default() as f64)),
)
})
.collect()
}
pub fn disk_free(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.available_space() as f64),
)
})
.collect()
}
pub fn disk_total(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.total_space() as f64),
)
})
.collect()
}
pub fn disk_used(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new((disk.total_space() - disk.available_space()) as f64),
)
})
.collect()
}
pub fn disk_percent(&self) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(
(disk.total_space() - disk.available_space()) as f64
/ disk.total_space() as f64
* 100.0,
),
)
})
.collect()
}
pub fn disk_read(&self, interval: Interval) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.usage().read_bytes as f64 / interval.disks() as f64),
)
})
.collect()
}
pub fn disk_write(&self, interval: Interval) -> ValueSet {
lock!(self.disks)
.iter()
.map(|disk| {
(
disk.mount_point().to_string_lossy().into(),
Value::new(disk.usage().written_bytes as f64 / interval.disks() as f64),
)
})
.collect()
}
pub fn net_down(&self, interval: Interval) -> ValueSet {
lock!(self.networks)
.iter()
.map(|(name, net)| {
(
name.as_str().into(),
Value::new(net.received() as f64 / interval.networks() as f64),
)
})
.collect()
}
pub fn net_up(&self, interval: Interval) -> ValueSet {
lock!(self.networks)
.iter()
.map(|(name, net)| {
(
name.as_str().into(),
Value::new(net.transmitted() as f64 / interval.networks() as f64),
)
})
.collect()
}
pub fn load_average_1(&self) -> Value {
Value::new(lock!(self.load_average).one)
}
pub fn load_average_5(&self) -> Value {
Value::new(lock!(self.load_average).five)
}
pub fn load_average_15(&self) -> Value {
Value::new(lock!(self.load_average).fifteen)
}
/// Gets system uptime formatted as `HH:mm`.
pub fn uptime(&self) -> String {
let uptime = System::uptime();
let hours = uptime / 3600;
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60)
}
}
register_client!(Client, sys_info);
const fn c_to_f(c: f64) -> f64 {
c / 5.0 * 9.0 + 32.0
}

View file

@ -1,451 +0,0 @@
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, send_async, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
use regex::{Captures, Regex};
use serde::Deserialize;
use std::collections::HashMap;
use std::time::Duration;
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
use tokio::sync::mpsc;
use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SysInfoModule {
/// List of strings including formatting tokens.
/// For available tokens, see [below](#formatting-tokens).
///
/// **Required**
format: Vec<String>,
/// Number of seconds between refresh.
///
/// This can be set as a global interval,
/// or passed as an object to customize the interval per-system.
///
/// **Default**: `5`
#[serde(default = "Interval::default")]
interval: Interval,
/// The orientation of text for the labels.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br>
/// **Default** : `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// The orientation by which the labels are laid out.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br>
/// **Default** : `horizontal`
direction: Option<ModuleOrientation>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Copy, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Intervals {
/// The number of seconds between refreshing memory data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
memory: u64,
/// The number of seconds between refreshing CPU data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
cpu: u64,
/// The number of seconds between refreshing temperature data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
temps: u64,
/// The number of seconds between refreshing disk data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
disks: u64,
/// The number of seconds between refreshing network data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
networks: u64,
/// The number of seconds between refreshing system data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
system: u64,
}
#[derive(Debug, Deserialize, Copy, Clone)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Interval {
All(u64),
Individual(Intervals),
}
impl Default for Interval {
fn default() -> Self {
Self::All(default_interval())
}
}
impl Interval {
const fn memory(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.memory,
}
}
const fn cpu(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.cpu,
}
}
const fn temps(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.temps,
}
}
const fn disks(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.disks,
}
}
const fn networks(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.networks,
}
}
const fn system(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.system,
}
}
}
const fn default_interval() -> u64 {
5
}
#[derive(Debug)]
enum RefreshType {
Memory,
Cpu,
Temps,
Disks,
Network,
System,
}
impl Module<gtk::Box> for SysInfoModule {
type SendMessage = HashMap<String, String>;
type ReceiveMessage = ();
module_impl!("sysinfo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let interval = self.interval;
let refresh_kind = RefreshKind::everything()
.without_processes()
.without_users_list();
let mut sys = System::new_with_specifics(refresh_kind);
sys.refresh_components_list();
sys.refresh_disks_list();
sys.refresh_networks_list();
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
macro_rules! spawn_refresh {
($refresh_type:expr, $func:ident) => {{
let tx = refresh_tx.clone();
spawn(async move {
loop {
send_async!(tx, $refresh_type);
sleep(Duration::from_secs(interval.$func())).await;
}
});
}};
}
spawn_refresh!(RefreshType::Memory, memory);
spawn_refresh!(RefreshType::Cpu, cpu);
spawn_refresh!(RefreshType::Temps, temps);
spawn_refresh!(RefreshType::Disks, disks);
spawn_refresh!(RefreshType::Network, networks);
spawn_refresh!(RefreshType::System, system);
let tx = context.tx.clone();
spawn(async move {
let mut format_info = HashMap::new();
while let Some(refresh) = refresh_rx.recv().await {
match refresh {
RefreshType::Memory => refresh_memory_tokens(&mut format_info, &mut sys),
RefreshType::Cpu => refresh_cpu_tokens(&mut format_info, &mut sys),
RefreshType::Temps => refresh_temp_tokens(&mut format_info, &mut sys),
RefreshType::Disks => refresh_disk_tokens(&mut format_info, &mut sys),
RefreshType::Network => {
refresh_network_tokens(&mut format_info, &mut sys, interval.networks());
}
RefreshType::System => refresh_system_tokens(&mut format_info, &sys),
};
send_async!(tx, ModuleUpdateEvent::Update(format_info.clone()));
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let layout = match self.direction {
Some(orientation) => orientation,
None => self.orientation,
};
let container = gtk::Box::new(layout.into(), 10);
let mut labels = Vec::new();
for format in &self.format {
let label = Label::builder().label(format).use_markup(true).build();
label.add_class("item");
label.set_angle(self.orientation.to_angle());
container.add(&label);
labels.push(label);
}
{
let formats = self.format;
glib_recv!(context.subscribe(), info => {
for (format, label) in formats.iter().zip(labels.clone()) {
let format_compiled = re.replace_all(format, |caps: &Captures| {
info.get(&caps[1])
.unwrap_or(&caps[0].to_string())
.to_string()
});
label.set_label_escaped(format_compiled.as_ref());
}
});
}
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}
fn refresh_memory_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_memory();
let total_memory = sys.total_memory();
let available_memory = sys.available_memory();
let actual_used_memory = total_memory - available_memory;
let memory_percent = actual_used_memory as f64 / total_memory as f64 * 100.0;
format_info.insert(
String::from("memory_free"),
(bytes_to_gigabytes(available_memory)).to_string(),
);
format_info.insert(
String::from("memory_used"),
(bytes_to_gigabytes(actual_used_memory)).to_string(),
);
format_info.insert(
String::from("memory_total"),
(bytes_to_gigabytes(total_memory)).to_string(),
);
format_info.insert(
String::from("memory_percent"),
format!("{memory_percent:0>2.0}"),
);
let used_swap = sys.used_swap();
let total_swap = sys.total_swap();
format_info.insert(
String::from("swap_free"),
(bytes_to_gigabytes(sys.free_swap())).to_string(),
);
format_info.insert(
String::from("swap_used"),
(bytes_to_gigabytes(used_swap)).to_string(),
);
format_info.insert(
String::from("swap_total"),
(bytes_to_gigabytes(total_swap)).to_string(),
);
format_info.insert(
String::from("swap_percent"),
format!("{:0>2.0}", used_swap as f64 / total_swap as f64 * 100.0),
);
}
fn refresh_cpu_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_cpu();
let cpu_info = sys.global_cpu_info();
let cpu_percent = cpu_info.cpu_usage();
format_info.insert(String::from("cpu_percent"), format!("{cpu_percent:0>2.0}"));
}
fn refresh_temp_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_components();
let components = sys.components();
for component in components {
let key = component.label().replace(' ', "-");
let temp = component.temperature();
format_info.insert(format!("temp_c:{key}"), format!("{temp:.0}"));
format_info.insert(format!("temp_f:{key}"), format!("{:.0}", c_to_f(temp)));
}
}
fn refresh_disk_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
sys.refresh_disks();
for disk in sys.disks() {
// replace braces to avoid conflict with regex
let key = disk
.mount_point()
.to_str()
.map(|s| s.replace(['{', '}'], ""));
if let Some(key) = key {
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
format_info.insert(
format!("disk_free:{key}"),
bytes_to_gigabytes(available).to_string(),
);
format_info.insert(
format!("disk_used:{key}"),
bytes_to_gigabytes(used).to_string(),
);
format_info.insert(
format!("disk_total:{key}"),
bytes_to_gigabytes(total).to_string(),
);
format_info.insert(
format!("disk_percent:{key}"),
format!("{:0>2.0}", used as f64 / total as f64 * 100.0),
);
}
}
}
fn refresh_network_tokens(
format_info: &mut HashMap<String, String>,
sys: &mut System,
interval: u64,
) {
sys.refresh_networks();
for (iface, network) in sys.networks() {
format_info.insert(
format!("net_down:{iface}"),
format!("{:0>2.0}", bytes_to_megabits(network.received()) / interval),
);
format_info.insert(
format!("net_up:{iface}"),
format!(
"{:0>2.0}",
bytes_to_megabits(network.transmitted()) / interval
),
);
}
}
fn refresh_system_tokens(format_info: &mut HashMap<String, String>, sys: &System) {
// no refresh required for these tokens
let load_average = sys.load_average();
format_info.insert(
String::from("load_average:1"),
format!("{:.2}", load_average.one),
);
format_info.insert(
String::from("load_average:5"),
format!("{:.2}", load_average.five),
);
format_info.insert(
String::from("load_average:15"),
format!("{:.2}", load_average.fifteen),
);
let uptime = Duration::from_secs(sys.uptime()).as_secs();
let hours = uptime / 3600;
format_info.insert(
String::from("uptime"),
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60),
);
}
/// Converts celsius to fahrenheit.
fn c_to_f(c: f32) -> f32 {
c * 9.0 / 5.0 + 32.0
}
const fn bytes_to_gigabytes(b: u64) -> u64 {
const BYTES_IN_GIGABYTE: u64 = 1_000_000_000;
b / BYTES_IN_GIGABYTE
}
const fn bytes_to_megabits(b: u64) -> u64 {
const BYTES_IN_MEGABIT: u64 = 125_000;
b / BYTES_IN_MEGABIT
}

314
src/modules/sysinfo/mod.rs Normal file
View file

@ -0,0 +1,314 @@
mod parser;
mod renderer;
mod token;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::modules::sysinfo::token::{Part, TokenType};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{clients, glib_recv, module_impl, send_async, spawn, try_send};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SysInfoModule {
/// List of strings including formatting tokens.
/// For available tokens, see [below](#formatting-tokens).
///
/// **Required**
format: Vec<String>,
/// Number of seconds between refresh.
///
/// This can be set as a global interval,
/// or passed as an object to customize the interval per-system.
///
/// **Default**: `5`
#[serde(default = "Interval::default")]
interval: Interval,
/// The orientation of text for the labels.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br>
/// **Default** : `horizontal`
#[serde(default)]
orientation: ModuleOrientation,
/// The orientation by which the labels are laid out.
///
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
/// <br>
/// **Default** : `horizontal`
direction: Option<ModuleOrientation>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Copy, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Intervals {
/// The number of seconds between refreshing memory data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
memory: u64,
/// The number of seconds between refreshing CPU data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
cpu: u64,
/// The number of seconds between refreshing temperature data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
temps: u64,
/// The number of seconds between refreshing disk data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
disks: u64,
/// The number of seconds between refreshing network data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
networks: u64,
/// The number of seconds between refreshing system data.
///
/// **Default**: `5`
#[serde(default = "default_interval")]
system: u64,
}
#[derive(Debug, Deserialize, Copy, Clone)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Interval {
All(u64),
Individual(Intervals),
}
impl Default for Interval {
fn default() -> Self {
Self::All(default_interval())
}
}
impl Interval {
const fn memory(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.memory,
}
}
const fn cpu(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.cpu,
}
}
const fn temps(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.temps,
}
}
pub const fn disks(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.disks,
}
}
pub const fn networks(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.networks,
}
}
const fn system(self) -> u64 {
match self {
Self::All(n) => n,
Self::Individual(intervals) => intervals.system,
}
}
}
const fn default_interval() -> u64 {
5
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum RefreshType {
Memory,
Cpu,
Temps,
Disks,
Network,
System,
}
impl TokenType {
fn is_affected_by(self, refresh_type: RefreshType) -> bool {
match self {
Self::CpuFrequency | Self::CpuPercent => refresh_type == RefreshType::Cpu,
Self::MemoryFree
| Self::MemoryAvailable
| Self::MemoryTotal
| Self::MemoryUsed
| Self::MemoryPercent
| Self::SwapFree
| Self::SwapTotal
| Self::SwapUsed
| Self::SwapPercent => refresh_type == RefreshType::Memory,
Self::TempC | Self::TempF => refresh_type == RefreshType::Temps,
Self::DiskFree
| Self::DiskTotal
| Self::DiskUsed
| Self::DiskPercent
| Self::DiskRead
| Self::DiskWrite => refresh_type == RefreshType::Disks,
Self::NetDown | Self::NetUp => refresh_type == RefreshType::Network,
Self::LoadAverage1 | Self::LoadAverage5 | Self::LoadAverage15 => {
refresh_type == RefreshType::System
}
Self::Uptime => refresh_type == RefreshType::System,
}
}
}
impl Module<gtk::Box> for SysInfoModule {
type SendMessage = (usize, String);
type ReceiveMessage = ();
module_impl!("sysinfo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let interval = self.interval;
let client = context.client::<clients::sysinfo::Client>();
let format_tokens = self
.format
.iter()
.map(|format| parser::parse_input(format.as_str()))
.collect::<Result<Vec<_>>>()?;
for (i, token_set) in format_tokens.iter().enumerate() {
let rendered = Part::render_all(token_set, &client, interval);
try_send!(context.tx, ModuleUpdateEvent::Update((i, rendered)));
}
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
macro_rules! spawn_refresh {
($refresh_type:expr, $func:ident) => {{
let tx = refresh_tx.clone();
spawn(async move {
loop {
send_async!(tx, $refresh_type);
sleep(Duration::from_secs(interval.$func())).await;
}
});
}};
}
spawn_refresh!(RefreshType::Memory, memory);
spawn_refresh!(RefreshType::Cpu, cpu);
spawn_refresh!(RefreshType::Temps, temps);
spawn_refresh!(RefreshType::Disks, disks);
spawn_refresh!(RefreshType::Network, networks);
spawn_refresh!(RefreshType::System, system);
let tx = context.tx.clone();
spawn(async move {
while let Some(refresh) = refresh_rx.recv().await {
match refresh {
RefreshType::Memory => client.refresh_memory(),
RefreshType::Cpu => client.refresh_cpu(),
RefreshType::Temps => client.refresh_temps(),
RefreshType::Disks => client.refresh_disks(),
RefreshType::Network => client.refresh_network(),
RefreshType::System => client.refresh_load_average(),
};
for (i, token_set) in format_tokens.iter().enumerate() {
let is_affected = token_set
.iter()
.filter_map(|part| {
if let Part::Token(token) = part {
Some(token)
} else {
None
}
})
.any(|t| t.token.is_affected_by(refresh));
if is_affected {
let rendered = Part::render_all(token_set, &client, interval);
send_async!(tx, ModuleUpdateEvent::Update((i, rendered)));
}
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let layout = match self.direction {
Some(orientation) => orientation,
None => self.orientation,
};
let container = gtk::Box::new(layout.into(), 10);
let mut labels = Vec::new();
for _ in &self.format {
let label = Label::builder().use_markup(true).build();
label.add_class("item");
label.set_angle(self.orientation.to_angle());
container.add(&label);
labels.push(label);
}
glib_recv!(context.subscribe(), data => {
let label = &labels[data.0];
label.set_label_escaped(&data.1);
});
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View file

@ -0,0 +1,460 @@
use crate::clients::sysinfo::{Function, Prefix};
use crate::modules::sysinfo::token::{Alignment, Formatting, Part, Token, TokenType};
use color_eyre::{Report, Result};
use std::iter::Peekable;
use std::str::{Chars, FromStr};
impl FromStr for TokenType {
type Err = Report;
fn from_str(s: &str) -> Result<Self> {
match s {
"cpu_frequency" => Ok(Self::CpuFrequency),
"cpu_percent" => Ok(Self::CpuPercent),
"memory_free" => Ok(Self::MemoryFree),
"memory_available" => Ok(Self::MemoryAvailable),
"memory_total" => Ok(Self::MemoryTotal),
"memory_used" => Ok(Self::MemoryUsed),
"memory_percent" => Ok(Self::MemoryPercent),
"swap_free" => Ok(Self::SwapFree),
"swap_total" => Ok(Self::SwapTotal),
"swap_used" => Ok(Self::SwapUsed),
"swap_percent" => Ok(Self::SwapPercent),
"temp_c" => Ok(Self::TempC),
"temp_f" => Ok(Self::TempF),
"disk_free" => Ok(Self::DiskFree),
"disk_total" => Ok(Self::DiskTotal),
"disk_used" => Ok(Self::DiskUsed),
"disk_percent" => Ok(Self::DiskPercent),
"disk_read" => Ok(Self::DiskRead),
"disk_write" => Ok(Self::DiskWrite),
"net_down" => Ok(Self::NetDown),
"net_up" => Ok(Self::NetUp),
"load_average_1" => Ok(Self::LoadAverage1),
"load_average_5" => Ok(Self::LoadAverage5),
"load_average_15" => Ok(Self::LoadAverage15),
"uptime" => Ok(Self::Uptime),
_ => Err(Report::msg(format!("invalid token type: '{s}'"))),
}
}
}
impl FromStr for Function {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"sum" => Ok(Self::Sum),
"min" => Ok(Self::Min),
"max" => Ok(Self::Max),
"mean" => Ok(Self::Mean),
"" => Err(()),
_ => Ok(Self::Name(s.to_string())),
}
}
}
impl Function {
pub(crate) fn default_for(token_type: TokenType) -> Self {
match token_type {
TokenType::CpuFrequency
| TokenType::CpuPercent
| TokenType::TempC
| TokenType::DiskPercent => Self::Mean,
TokenType::DiskFree
| TokenType::DiskTotal
| TokenType::DiskUsed
| TokenType::DiskRead
| TokenType::DiskWrite
| TokenType::NetDown
| TokenType::NetUp => Self::Sum,
_ => Self::None,
}
}
}
impl Prefix {
pub(crate) fn default_for(token_type: TokenType) -> Self {
match token_type {
TokenType::CpuFrequency
| TokenType::MemoryFree
| TokenType::MemoryAvailable
| TokenType::MemoryTotal
| TokenType::MemoryUsed
| TokenType::SwapFree
| TokenType::SwapTotal
| TokenType::SwapUsed
| TokenType::DiskFree
| TokenType::DiskTotal
| TokenType::DiskUsed => Self::Giga,
TokenType::DiskRead | TokenType::DiskWrite => Self::Mega,
TokenType::NetDown | TokenType::NetUp => Self::MegaBit,
_ => Self::None,
}
}
}
impl FromStr for Prefix {
type Err = Report;
fn from_str(s: &str) -> Result<Self> {
match s {
"k" => Ok(Prefix::Kilo),
"M" => Ok(Prefix::Mega),
"G" => Ok(Prefix::Giga),
"T" => Ok(Prefix::Tera),
"P" => Ok(Prefix::Peta),
"ki" => Ok(Prefix::Kibi),
"Mi" => Ok(Prefix::Mebi),
"Gi" => Ok(Prefix::Gibi),
"Ti" => Ok(Prefix::Tebi),
"Pi" => Ok(Prefix::Pebi),
"kb" => Ok(Prefix::KiloBit),
"Mb" => Ok(Prefix::MegaBit),
"Gb" => Ok(Prefix::GigaBit),
_ => Err(Report::msg(format!("invalid prefix: {s}"))),
}
}
}
impl TryFrom<char> for Alignment {
type Error = Report;
fn try_from(value: char) -> Result<Self> {
match value {
'<' => Ok(Self::Left),
'^' => Ok(Self::Center),
'>' => Ok(Self::Right),
_ => Err(Report::msg(format!("Unknown alignment: {value}"))),
}
}
}
impl Formatting {
fn default_for(token_type: TokenType) -> Self {
match token_type {
TokenType::CpuFrequency
| TokenType::LoadAverage1
| TokenType::LoadAverage5
| TokenType::LoadAverage15 => Self {
width: 0,
fill: '0',
align: Alignment::default(),
precision: 2,
},
TokenType::CpuPercent => Self {
width: 2,
fill: '0',
align: Alignment::default(),
precision: 0,
},
TokenType::MemoryFree
| TokenType::MemoryAvailable
| TokenType::MemoryTotal
| TokenType::MemoryUsed
| TokenType::MemoryPercent
| TokenType::SwapFree
| TokenType::SwapTotal
| TokenType::SwapUsed
| TokenType::SwapPercent => Self {
width: 4,
fill: '0',
align: Alignment::default(),
precision: 1,
},
_ => Self {
width: 0,
fill: '0',
align: Alignment::default(),
precision: 0,
},
}
}
}
pub fn parse_input(input: &str) -> Result<Vec<Part>> {
let mut tokens = vec![];
let mut chars = input.chars().peekable();
let mut next_char = chars.peek().copied();
while let Some(char) = next_char {
let token = if char == '{' {
chars.next();
parse_dynamic(&mut chars)?
} else {
parse_static(&mut chars)
};
tokens.push(token);
next_char = chars.peek().copied();
}
Ok(tokens)
}
fn parse_static(chars: &mut Peekable<Chars>) -> Part {
let mut str = String::new();
let mut next_char = chars.next_if(|&c| c != '{');
while let Some(char) = next_char {
if char == '{' {
break;
}
str.push(char);
next_char = chars.next_if(|&c| c != '{');
}
Part::Static(str)
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum DynamicMode {
Token,
Name,
Prefix,
}
fn parse_dynamic(chars: &mut Peekable<Chars>) -> Result<Part> {
let mut mode = DynamicMode::Token;
let mut token_str = String::new();
let mut func_str = String::new();
let mut prefix_str = String::new();
// we don't want to peek here as that would be the same char as the outer loop
let mut next_char = chars.next();
while let Some(char) = next_char {
match char {
'}' | ':' => break,
'@' => mode = DynamicMode::Name,
'#' => mode = DynamicMode::Prefix,
_ => match mode {
DynamicMode::Token => token_str.push(char),
DynamicMode::Name => func_str.push(char),
DynamicMode::Prefix => prefix_str.push(char),
},
}
next_char = chars.next();
}
let token_type = token_str.parse()?;
let mut formatting = Formatting::default_for(token_type);
if next_char == Some(':') {
formatting = parse_formatting(chars, formatting)?;
}
let token = Token {
token: token_type,
function: func_str
.parse()
.unwrap_or_else(|()| Function::default_for(token_type)),
prefix: prefix_str
.parse()
.unwrap_or_else(|_| Prefix::default_for(token_type)),
formatting,
};
Ok(Part::Token(token))
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum FormattingMode {
WidthFillAlign,
Precision,
}
fn parse_formatting(chars: &mut Peekable<Chars>, mut formatting: Formatting) -> Result<Formatting> {
let mut width_string = String::new();
let mut precision_string = String::new();
let mut mode = FormattingMode::WidthFillAlign;
let mut next_char = chars.next();
while let Some(char) = next_char {
match (char, mode) {
('}', _) => break,
('.', _) => mode = FormattingMode::Precision,
(_, FormattingMode::Precision) => precision_string.push(char),
('1'..='9', FormattingMode::WidthFillAlign) => width_string.push(char),
('<' | '^' | '>', FormattingMode::WidthFillAlign) => {
formatting.align = Alignment::try_from(char)?;
}
(_, FormattingMode::WidthFillAlign) => formatting.fill = char,
};
next_char = chars.next();
}
if !width_string.is_empty() {
formatting.width = width_string.parse()?;
}
if !precision_string.is_empty() {
formatting.precision = precision_string.parse()?;
}
Ok(formatting)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_only() {
let tokens = parse_input("hello world").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Static(str) if str == "hello world"));
}
#[test]
fn basic() {
let tokens = parse_input("{cpu_frequency}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
}
#[test]
fn named() {
let tokens = parse_input("{cpu_frequency@cpu0}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert!(matches!(&token.function, Function::Name(n) if n == "cpu0"));
}
#[test]
fn conversion() {
let tokens = parse_input("{cpu_frequency#G}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert_eq!(token.prefix, Prefix::Giga);
}
#[test]
fn formatting_basic() {
let tokens = parse_input("{cpu_frequency:.2}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert_eq!(token.formatting.precision, 2);
}
#[test]
fn formatting_complex() {
let tokens = parse_input("{cpu_frequency:0<5.2}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert_eq!(token.formatting.fill, '0');
assert_eq!(token.formatting.align, Alignment::Left);
assert_eq!(token.formatting.width, 5);
assert_eq!(token.formatting.precision, 2);
}
#[test]
fn complex() {
let tokens = parse_input("{cpu_frequency@cpu0#G:.2}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 1);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert!(matches!(&token.function, Function::Name(n) if n == "cpu0"));
assert_eq!(token.prefix, Prefix::Giga);
assert_eq!(token.formatting.precision, 2);
}
#[test]
fn static_then_token() {
let tokens = parse_input("Freq: {cpu_frequency#G:.2}").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 2);
assert!(matches!(&tokens[0], Part::Static(str) if str == "Freq: "));
assert!(matches!(&tokens[1], Part::Token(_)));
let Part::Token(token) = tokens.get(1).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert_eq!(token.formatting.precision, 2);
}
#[test]
fn token_then_static() {
let tokens = parse_input("{cpu_frequency#G:.2} GHz").unwrap();
println!("{tokens:?}");
assert_eq!(tokens.len(), 2);
assert!(matches!(&tokens[0], Part::Token(_)));
let Part::Token(token) = tokens.get(0).unwrap() else {
return;
};
assert_eq!(token.token, TokenType::CpuFrequency);
assert_eq!(token.formatting.precision, 2);
assert!(matches!(&tokens[1], Part::Static(str) if str == " GHz"));
}
}

View file

@ -0,0 +1,91 @@
use super::token::{Alignment, Part, Token, TokenType};
use super::Interval;
use crate::clients;
use crate::clients::sysinfo::{Value, ValueSet};
pub enum TokenValue {
Number(f64),
String(String),
}
impl Part {
pub fn render_all(
tokens: &[Self],
client: &clients::sysinfo::Client,
interval: Interval,
) -> String {
tokens
.iter()
.map(|part| part.render(client, interval))
.collect()
}
fn render(&self, client: &clients::sysinfo::Client, interval: Interval) -> String {
match self {
Part::Static(str) => str.clone(),
Part::Token(token) => {
match token.get(client, interval) {
TokenValue::Number(value) => {
let fmt = token.formatting;
let mut str = format!("{value:.precision$}", precision = fmt.precision);
// fill/align doesn't support parameterization so we need our own impl
let mut add_to_end = fmt.align == Alignment::Right;
while str.len() < fmt.width {
if add_to_end {
str.push(fmt.fill);
} else {
str.insert(0, fmt.fill);
}
if fmt.align == Alignment::Center {
add_to_end = !add_to_end;
}
}
str
}
TokenValue::String(value) => value,
}
}
}
}
}
impl Token {
pub fn get(&self, client: &clients::sysinfo::Client, interval: Interval) -> TokenValue {
let get = |value: Value| TokenValue::Number(value.get(self.prefix));
let apply = |set: ValueSet| TokenValue::Number(set.apply(&self.function, self.prefix));
match self.token {
// Number tokens
TokenType::CpuFrequency => apply(client.cpu_frequency()),
TokenType::CpuPercent => apply(client.cpu_percent()),
TokenType::MemoryFree => get(client.memory_free()),
TokenType::MemoryAvailable => get(client.memory_available()),
TokenType::MemoryTotal => get(client.memory_total()),
TokenType::MemoryUsed => get(client.memory_used()),
TokenType::MemoryPercent => get(client.memory_percent()),
TokenType::SwapFree => get(client.swap_free()),
TokenType::SwapTotal => get(client.swap_total()),
TokenType::SwapUsed => get(client.swap_used()),
TokenType::SwapPercent => get(client.swap_percent()),
TokenType::TempC => apply(client.temp_c()),
TokenType::TempF => apply(client.temp_f()),
TokenType::DiskFree => apply(client.disk_free()),
TokenType::DiskTotal => apply(client.disk_total()),
TokenType::DiskUsed => apply(client.disk_used()),
TokenType::DiskPercent => apply(client.disk_percent()),
TokenType::DiskRead => apply(client.disk_read(interval)),
TokenType::DiskWrite => apply(client.disk_write(interval)),
TokenType::NetDown => apply(client.net_down(interval)),
TokenType::NetUp => apply(client.net_up(interval)),
TokenType::LoadAverage1 => get(client.load_average_1()),
TokenType::LoadAverage5 => get(client.load_average_5()),
TokenType::LoadAverage15 => get(client.load_average_15()),
// String tokens
TokenType::Uptime => TokenValue::String(client.uptime()),
}
}
}

View file

@ -0,0 +1,66 @@
use crate::clients::sysinfo::{Function, Prefix};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
CpuFrequency,
CpuPercent,
MemoryFree,
MemoryAvailable,
MemoryTotal,
MemoryUsed,
MemoryPercent,
SwapFree,
SwapTotal,
SwapUsed,
SwapPercent,
TempC,
TempF,
DiskFree,
DiskTotal,
DiskUsed,
DiskPercent,
DiskRead,
DiskWrite,
NetDown,
NetUp,
LoadAverage1,
LoadAverage5,
LoadAverage15,
Uptime,
}
#[derive(Debug, Clone)]
pub struct Token {
pub token: TokenType,
pub function: Function,
pub prefix: Prefix,
pub formatting: Formatting,
}
#[derive(Debug, Clone)]
pub enum Part {
Static(String),
Token(Token),
}
#[derive(Debug, Clone, Copy)]
pub struct Formatting {
pub width: usize,
pub fill: char,
pub align: Alignment,
pub precision: usize,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}