diff --git a/Cargo.lock b/Cargo.lock index 2bf472a..7359df6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -1392,15 +1392,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "hermit-abi" version = "0.3.9" @@ -1565,7 +1556,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1664,7 +1655,7 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "windows-sys 0.48.0", ] @@ -1964,7 +1955,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "log", "wasi", @@ -2168,16 +2159,6 @@ dependencies = [ "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]] name = "object" version = "0.30.3" @@ -2519,9 +2500,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2529,14 +2510,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -3162,17 +3141,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.11" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", "rayon", - "winapi", + "windows", ] [[package]] @@ -3965,6 +3943,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows-core" version = "0.52.0" @@ -3974,17 +3962,60 @@ dependencies = [ "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]] name = "windows-registry" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-strings", "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]] name = "windows-result" version = "0.2.0" @@ -4000,7 +4031,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] diff --git a/Cargo.toml b/Cargo.toml index cc62004..98ec2e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ keyboard = ["dep:input", "dep:evdev-rs", "dep:libc", "dep:nix"] launcher = [] -music = ["regex"] +music = ["dep:regex"] "music+all" = ["music", "music+mpris", "music+mpd"] "music+mpris" = ["music", "mpris"] "music+mpd" = ["music", "mpd-utils"] @@ -72,7 +72,7 @@ network_manager = ["futures-lite", "futures-signals", "zbus"] notifications = ["zbus"] -sys_info = ["sysinfo", "regex"] +sys_info = ["dep:sysinfo"] tray = ["system-tray"] @@ -148,12 +148,15 @@ libc = { version = "0.2.164", optional = true } # music mpd-utils = { version = "0.2.1", optional = true } mpris = { version = "2.0.1", optional = true } +regex = { version = "1.11.1", default-features = false, features = [ + "std", +], optional = true } # network_manager futures-signals = { version = "0.3.34", optional = true } # sys_info -sysinfo = { version = "0.29.11", optional = true } +sysinfo = { version = "0.33.1", optional = true } # tray system-tray = { version = "0.7.0", features = ["dbusmenu-gtk3"], optional = true } @@ -164,9 +167,6 @@ libpulse-binding = { version = "2.28.2", optional = true } # shared 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 -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 swayipc-async = { version = "2.0.1", optional = true } # workspaces, keyboard hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true } # workspaces, keyboard diff --git a/docs/modules/Sys-Info.md b/docs/modules/Sys-Info.md index d2aeddc..d26364d 100644 --- a/docs/modules/Sys-Info.md +++ b/docs/modules/Sys-Info.md @@ -3,6 +3,8 @@ Displays one or more labels containing system information. Separating information across several labels allows for styling each one independently. 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) @@ -10,17 +12,17 @@ Pango markup is supported. > Type: `sys_info` -| Name | Type | Default | Description | -|--------------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------------------| -| `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.memory` | `integer` | `5` | Seconds between refreshing memory data | -| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data | -| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data | -| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data | -| `interval.network` | `integer` | `5` | Seconds between refreshing network data | -| `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). | +| Name | Type | Default | Description | +|--------------------|------------------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------| +| `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.memory` | `integer` | `5` | Seconds between refreshing memory data. | +| `interval.cpu` | `integer` | `5` | Seconds between refreshing cpu data. | +| `interval.temps` | `integer` | `5` | Seconds between refreshing temperature data. | +| `interval.disks` | `integer` | `5` | Seconds between refreshing disk data. | +| `interval.network` | `integer` | `5` | Seconds between refreshing network data. | +| `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). |
JSON @@ -30,12 +32,11 @@ Pango markup is supported. "end": [ { "format": [ - " {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", - " {memory_used} / {memory_total} GB ({memory_percent}%)", - "| {swap_used} / {swap_total} GB ({swap_percent}%)", - "󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", - "󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", - "󰖡 {load_average:1} | {load_average:5} | {load_average:15}", + " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C", + " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)", + "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s", + "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps", + "󰖡 {load_average_1} | {load_average_5} | {load_average_15}", "󰥔 {uptime}" ], "interval": { @@ -60,13 +61,12 @@ Pango markup is supported. [[end]] type = 'sys_info' format = [ - ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C', - ' {memory_used} / {memory_total} GB ({memory_percent}%)', - '| {swap_used} / {swap_total} GB ({swap_percent}%)', - '󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)', - '󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps', - '󰖡 {load_average:1} | {load_average:5} | {load_average:15}', - '󰥔 {uptime}', + " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C", + " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)", + "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s", + "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps", + "󰖡 {load_average_1} | {load_average_5} | {load_average_15}", + "󰥔 {uptime}" ] [end.interval] @@ -87,13 +87,12 @@ temps = 5 ```yaml end: - format: - - ' {cpu_percent}% | {temp_c:k10temp-Tccd1}°C' - - ' {memory_used} / {memory_total} GB ({memory_percent}%)' - - '| {swap_used} / {swap_total} GB ({swap_percent}%)' - - '󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)' - - '󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps' - - '󰖡 {load_average:1} | {load_average:5} | {load_average:15}' - - '󰥔 {uptime}' + - " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C" + - " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)" + - "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s" + - "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps" + - "󰖡 {load_average_1} | {load_average_5} | {load_average_15}" + - "󰥔 {uptime}" interval: cpu: 1 disks: 300 @@ -121,12 +120,11 @@ end: interval.networks = 3 format = [ - " {cpu_percent}% | {temp_c:k10temp-Tccd1}°C" - " {memory_used} / {memory_total} GB ({memory_percent}%)" - "| {swap_used} / {swap_total} GB ({swap_percent}%)" - "󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)" - "󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps" - "󰖡 {load_average:1} | {load_average:5} | {load_average:15}" + " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C" + " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)" + "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s" + "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps" + "󰖡 {load_average_1} | {load_average_5} | {load_average_15}" "󰥔 {uptime}" ] } @@ -138,39 +136,179 @@ end: ### 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 | -|--------------------------|------------------------------------------------------------------------------------| -| **CPU** | | -| `{cpu_percent}` | Total CPU utilisation percentage | -| **Memory** | | -| `{memory_free}` | Memory free in GB. | -| `{memory_used}` | Memory used in GB. | -| `{memory_total}` | Memory total in GB. | -| `{memory_percent}` | Memory utilisation percentage. | -| `{swap_free}` | Swap free in GB. | -| `{swap_used}` | Swap used in GB. | -| `{swap_total}` | Swap total in GB. | -| `{swap_percent}` | Swap utilisation percentage. | -| **Temperature** | | -| `{temp_c:[sensor]}` | Temperature in degrees C. Replace `[sensor]` with the sensor label. | -| `{temp_f:[sensor]}` | Temperature in degrees F. Replace `[sensor]` with the sensor label. | -| **Disk** | | -| `{disk_free:[mount]}` | Disk free space in GB. Replace `[mount]` with the disk mountpoint. | -| `{disk_used:[mount]}` | Disk used space in GB. Replace `[mount]` with the disk mountpoint. | -| `{disk_total:[mount]}` | Disk total space in GB. Replace `[mount]` with the disk mountpoint. | -| `{disk_percent:[mount]}` | Disk utilisation percentage. Replace `[mount]` with the disk mountpoint. | -| **Network** | | -| `{net_down:[adapter]}` | Average network download speed in Mbps. Replace `[adapter]` with the adapter name. | -| `{net_up:[adapter]}` | Average network upload speed in Mbps. Replace `[adapter]` with the adapter name. | -| **System** | | -| `{load_average:1}` | 1-minute load average. | -| `{load_average:5}` | 5-minute load average. | -| `{load_average:15}` | 15-minute load average. | -| `{uptime}` | System uptime formatted as `HH:mm`. | +| Token | Default Function | Default Unit | Default Formatting | +|--------------------------|------------------|--------------|--------------------| +| **CPU** | | | | +| `{cpu_frequency[#core]}` | `mean` | MHz | `.2` | +| `{cpu_percent[#core]}` | `mean` | % | `0<2` | +| **Memory** | | | | +| `{memory_free}` | N/A | GB | `0<4.1` | +| `{memory_available}` | N/A | GB | `0<4.1` | +| `{memory_used}` | N/A | GB | `0<4.1` | +| `{memory_total}` | N/A | GB | `0<4.1` | +| `{memory_percent}` | N/A | GB | `0<4.1` | +| `{swap_free}` | N/A | GB | `0<4.1` | +| `{swap_used}` | N/A | GB | `0<4.1` | +| `{swap_total}` | N/A | GB | `0<4.1` | +| `{swap_percent}` | N/A | GB | `0<4.1` | +| **Temperature** | | | | +| `{temp_c[#sensor]}` | `max` | °C | | +| `{temp_f[#sensor]}` | `max` | °F | | +| **Disk** | | | | +| `{disk_free[#mount]}` | `sum` | GB | | +| `{disk_used[#mount]}` | `sum` | GB | | +| `{disk_total[#mount]}` | `sum` | GB | | +| `{disk_percent[#mount]}` | `sum` | % | | +| `{disk_read[#mount]}` | `sum` | MB/s | | +| `{disk_write[#mount]}` | `sum` | MB/s | | +| **Network** | | | | +| `{net_down[#adapter]}` | `sum` | Mb/s | | +| `{net_up[#adapter]}` | `sum` | Mb/s | | +| **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 diff --git a/examples/config.corn b/examples/config.corn index 03416d2..0af2906 100644 --- a/examples/config.corn +++ b/examples/config.corn @@ -55,13 +55,12 @@ let { interval.networks = 3 format = [ - " {cpu_percent}% | {temp_c:k10temp-Tccd1}°C" - " {memory_used} / {memory_total} GB ({memory_percent}%)" - "| {swap_used} / {swap_total} GB ({swap_percent}%)" - "󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)" - "󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps" - "󰖡 {load_average:1} | {load_average:5} | {load_average:15}" - "󰥔 {uptime}" + " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C" + " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)" + "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s" + "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps" + "󰖡 {load_average1} | {load_average5} | {load_average15}" + "󰥔 {uptime}" ] } diff --git a/examples/config.json b/examples/config.json index b85a4ff..77b1564 100644 --- a/examples/config.json +++ b/examples/config.json @@ -1,5 +1,4 @@ { - "$schema": "https://f.jstanger.dev/github/ironbar/schema.json", "anchor_to_edges": true, "position": "bottom", "icon_theme": "Paper", @@ -64,12 +63,11 @@ "networks": 3 }, "format": [ - " {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", - " {memory_used} / {memory_total} GB ({memory_percent}%)", - "| {swap_used} / {swap_total} GB ({swap_percent}%)", - "󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", - "󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", - "󰖡 {load_average:1} | {load_average:5} | {load_average:15}", + " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C", + " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)", + "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s", + "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps", + "󰖡 {load_average1} | {load_average5} | {load_average15}", "󰥔 {uptime}" ] }, diff --git a/examples/config.toml b/examples/config.toml index 540c053..9af6aaf 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -53,12 +53,11 @@ interval = 500 [[end]] type = "sys_info" format = [ - " {cpu_percent}% | {temp_c:k10temp-Tccd1}°C", - " {memory_used} / {memory_total} GB ({memory_percent}%)", - "| {swap_used} / {swap_total} GB ({swap_percent}%)", - "󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%)", - "󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps", - "󰖡 {load_average:1} | {load_average:5} | {load_average:15}", + " {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C", + " {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%)", + "󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s", + "󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps", + "󰖡 {load_average1} | {load_average5} | {load_average15}", "󰥔 {uptime}", ] diff --git a/examples/config.yaml b/examples/config.yaml index 86cb83d..f7f40cd 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,4 +1,3 @@ -$schema: https://f.jstanger.dev/github/ironbar/schema.json anchor_to_edges: true position: bottom icon_theme: Paper @@ -44,12 +43,11 @@ end: disks: 300 networks: 3 format: - -  {cpu_percent}% | {temp_c:k10temp-Tccd1}°C - -  {memory_used} / {memory_total} GB ({memory_percent}%) - - '| {swap_used} / {swap_total} GB ({swap_percent}%)' - - 󰋊 {disk_used:/} / {disk_total:/} GB ({disk_percent:/}%) - - 󰓢 {net_down:enp39s0} / {net_up:enp39s0} Mbps - - 󰖡 {load_average:1} | {load_average:5} | {load_average:15} + -  {cpu_percent}% | {cpu_frequency} GHz | {temp_c@CPUTIN}°C + -  {memory_used} / {memory_total} GB ({memory_available} | {memory_percent2}%) | {swap_used} / {swap_total} GB ({swap_free} | {swap_percent}%) + - 󰋊 {disk_used#T@/:.1} / {disk_total#T@/:.1} TB ({disk_percent@/}%) | {disk_read} / {disk_write} MB/s + - 󰓢 {net_down@enp39s0} / {net_up@enp39s0} Mbps + - 󰖡 {load_average1} | {load_average5} | {load_average15} - 󰥔 {uptime} - type: volume format: '{icon} {percentage}%' diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 55507a7..1458b8b 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -21,6 +21,8 @@ pub mod networkmanager; pub mod sway; #[cfg(feature = "notifications")] pub mod swaync; +#[cfg(feature = "sys_info")] +pub mod sysinfo; #[cfg(feature = "tray")] pub mod tray; #[cfg(feature = "upower")] @@ -54,6 +56,8 @@ pub struct Clients { network_manager: Option>, #[cfg(feature = "notifications")] notifications: Option>, + #[cfg(feature = "sys_info")] + sys_info: Option>, #[cfg(feature = "tray")] tray: Option>, #[cfg(feature = "upower")] @@ -185,6 +189,13 @@ impl Clients { Ok(client) } + #[cfg(feature = "sys_info")] + pub fn sys_info(&mut self) -> Arc { + self.sys_info + .get_or_insert_with(|| Arc::new(sysinfo::Client::new())) + .clone() + } + #[cfg(feature = "tray")] pub fn tray(&mut self) -> ClientResult { let client = if let Some(client) = &self.tray { diff --git a/src/clients/sysinfo.rs b/src/clients/sysinfo.rs new file mode 100644 index 0000000..fbb0931 --- /dev/null +++ b/src/clients/sysinfo.rs @@ -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, Value>, +} + +impl FromIterator<(Box, Value)> for ValueSet { + fn from_iter, Value)>>(iter: T) -> Self { + Self { + values: iter.into_iter().collect(), + } + } +} + +impl ValueSet { + fn values(&self, prefix: Prefix) -> impl Iterator + 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, + disks: Mutex, + components: Mutex, + networks: Mutex, + load_average: Mutex, +} + +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 +} diff --git a/src/modules/sysinfo.rs b/src/modules/sysinfo.rs deleted file mode 100644 index 35ffb5d..0000000 --- a/src/modules/sysinfo.rs +++ /dev/null @@ -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, - - /// 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` - ///
- /// **Default** : `horizontal` - #[serde(default)] - orientation: ModuleOrientation, - - /// The orientation by which the labels are laid out. - /// - /// **Valid options**: `horizontal`, `vertical`, `h`, `v` - ///
- /// **Default** : `horizontal` - direction: Option, - - /// See [common options](module-level-options#common-options). - #[serde(flatten)] - pub common: Option, -} - -#[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 for SysInfoModule { - type SendMessage = HashMap; - type ReceiveMessage = (); - - module_impl!("sysinfo"); - - fn spawn_controller( - &self, - _info: &ModuleInfo, - context: &WidgetContext, - _rx: mpsc::Receiver, - ) -> 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, - _info: &ModuleInfo, - ) -> Result> { - 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, 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, 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, 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, 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, - 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, 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 -} diff --git a/src/modules/sysinfo/mod.rs b/src/modules/sysinfo/mod.rs new file mode 100644 index 0000000..9e205eb --- /dev/null +++ b/src/modules/sysinfo/mod.rs @@ -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, + + /// 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` + ///
+ /// **Default** : `horizontal` + #[serde(default)] + orientation: ModuleOrientation, + + /// The orientation by which the labels are laid out. + /// + /// **Valid options**: `horizontal`, `vertical`, `h`, `v` + ///
+ /// **Default** : `horizontal` + direction: Option, + + /// See [common options](module-level-options#common-options). + #[serde(flatten)] + pub common: Option, +} + +#[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 for SysInfoModule { + type SendMessage = (usize, String); + type ReceiveMessage = (); + + module_impl!("sysinfo"); + + fn spawn_controller( + &self, + _info: &ModuleInfo, + context: &WidgetContext, + _rx: mpsc::Receiver, + ) -> Result<()> { + let interval = self.interval; + + let client = context.client::(); + + let format_tokens = self + .format + .iter() + .map(|format| parser::parse_input(format.as_str())) + .collect::>>()?; + + 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, + _info: &ModuleInfo, + ) -> Result> { + 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, + }) + } +} diff --git a/src/modules/sysinfo/parser.rs b/src/modules/sysinfo/parser.rs new file mode 100644 index 0000000..af1d918 --- /dev/null +++ b/src/modules/sysinfo/parser.rs @@ -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 { + 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 { + 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 { + 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 for Alignment { + type Error = Report; + + fn try_from(value: char) -> Result { + 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> { + 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) -> 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) -> Result { + 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, mut formatting: Formatting) -> Result { + 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")); + } +} diff --git a/src/modules/sysinfo/renderer.rs b/src/modules/sysinfo/renderer.rs new file mode 100644 index 0000000..f835878 --- /dev/null +++ b/src/modules/sysinfo/renderer.rs @@ -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()), + } + } +} diff --git a/src/modules/sysinfo/token.rs b/src/modules/sysinfo/token.rs new file mode 100644 index 0000000..499662e --- /dev/null +++ b/src/modules/sysinfo/token.rs @@ -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, +}