Compare commits

...

36 Commits

Author SHA1 Message Date
457f9d71e6 Weather module unit tests passing 2025-07-27 21:39:08 -07:00
1e2851103f Added unit tests for weather module 2025-07-27 14:54:05 -07:00
1834ddac1b Used Claude Sonnet 3.5 agent to add license and documentation to all
files in the project
2025-07-26 13:31:50 -07:00
a8591da49b Better error handling
[With input from AI in VSCode]
2025-07-26 13:14:05 -07:00
0e027005a1 Better error handling of icon in weather module 2025-07-19 20:21:20 -07:00
7245914da8 Added mechanism to skip updates in a module
* This is useful in modules where HTTP API calls are made. Updating them
  every second is expensive, network intensive and can cause rate limits
  to be hit
* Added get_name() function to modules for easier debug statements
2025-06-14 17:33:24 -07:00
e8ab11eff9 More null handling in weather API responses 2025-06-14 16:55:26 -07:00
91b02c24fb Error handling to keep unibar from dying
* Handled the case where network goes down and Curl errors out fetching
  weather data
2025-06-08 19:40:02 -07:00
c33360fb7b Time zone name instead of offset in time display 2025-06-08 09:59:59 -07:00
be96618f76 General clean up 2025-06-07 21:53:32 -07:00
aeefa114e6 README update 2025-06-07 21:27:12 -07:00
e25a8df19e Reintroduced weather module removed by mistake 2025-06-07 21:26:52 -07:00
79e1a5956e Clear data before new bar display iteration
This makes sure that data displayed is always fresh
2025-06-07 21:19:02 -07:00
4452cdbaa0 Added date and time modules 2025-06-07 20:42:23 -07:00
0b7ff61da0 Added forever-loop to update bar contents at interval
* New argument --interval to specify update interval
2025-06-07 20:14:34 -07:00
51253240b3 README update 2025-06-07 16:29:03 -07:00
00079662df Added power module 2025-06-07 16:15:56 -07:00
c2eee03ac2 Trimmed progress display 2025-06-07 16:15:34 -07:00
eba6f977ec Code clean up 2025-06-07 16:15:17 -07:00
17ce090ef8 General clean up
* Option changed: --debug-icons -> --debug-modules
2025-06-06 18:10:43 -07:00
750a2813d3 Changed debug option names
* --debug-json -> --debug
* --debug-modules -> --debug-icons
2025-06-06 17:57:14 -07:00
c84e9698d7 Added CPU usage module 2025-06-06 17:50:16 -07:00
e6719dc6f5 Use available mem instead of free mem 2025-06-06 17:48:35 -07:00
8bad155f15 Added memory module 2025-06-03 17:26:30 -07:00
9b79fcd904 Reordered modules 2025-05-23 23:49:09 -07:00
b62c9cd484 Network icons based on wifi/ethernet
Some refactoring
2025-05-23 23:48:23 -07:00
2be0d56655 New network module 2025-05-22 17:49:30 -07:00
a2d9c768f8 new() returns Self 2025-05-11 20:42:11 -07:00
a0d19b870c Added example output in README 2025-05-11 20:30:34 -07:00
b8b0afd945 Added icon to music module 2025-05-11 20:22:13 -07:00
b7f83b4810 Refactoring of bar modules
* Moved debug functions to separate trait
* Broke up module actions and moved assembling of icon and content
  in main instead of module itself. This will enable disabling of
  icon or content via options in the future
2025-05-11 16:02:14 -07:00
ade570b8af Added weather symbols 2025-05-10 20:39:43 -07:00
710399628b Update usage 2025-05-05 05:15:52 +00:00
e1f2518239 Implementation of modules in separate structs 2025-05-04 22:13:41 -07:00
a20e1d8336 Initial verison of implementation 2025-05-04 18:50:21 -07:00
480ba07c45 Updated README with initial information 2025-05-04 18:49:54 -07:00
13 changed files with 2189 additions and 2 deletions

18
Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "unibar"
version = "0.1.0"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1.0.196"
serde_json = "1.0.113"
curl = "0.4.46"
regex = "1.11.1"
chrono = "0.4.41"
chrono-tz = "0.10.3"
iana-time-zone = "0.1.63"
clap = { version = "4.5.1", features = ["derive"] }
thiserror = "1.0"

View File

@@ -1,3 +1,78 @@
# unibar
# Unibar
A tool that returns variety of components in a string suitable to use in a status bar
Simple bar written in Rust
# Description
* A tool that returns variety of components in a string suitable to use in a
status bar
* Currently implemented modules:
- date
- memory
- music
- network
- power
- time
- weather
- cpu
# Usage
* Use the `--help` option for usage information:
```sh
A tool that returns variety of components in a string
suitable to use in a status bar
Usage: unibar [OPTIONS]
Options:
-i, --interval <INTERVAL>
Update interval in seconds
[default: 5]
-s, --weather-station <WEATHER_STATION>
Name of the weather station
[default: khio]
-m, --weather-metric
Use Metric units in weather data. Imperial units otherwise
-p, --music-progress
Show music progess
-D, --debug
Show verbose debug information during run
-M, --debug-modules
Show module debug information after all modules are evaluated but before output is printed
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
```
# Example output
* Default
```shell
$ target/release/unibar
status ⏸ Dire Straits - Tunnel of Love 💾 24% 💻 2% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:24PM -07:00
status ⏸ Dire Straits - Tunnel of Love 💾 24% 💻 2% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:24PM -07:00
status ⏸ Dire Straits - Tunnel of Love 💾 24% 💻 2% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:25PM -07:00
^C
```
* With music progress
```shell
$ target/release/unibar --music-progress
status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:25PM -07:00
status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:25PM -07:00
^C
```

View File

@@ -0,0 +1,204 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! CPU usage monitoring module for Unibar
//!
//! This module tracks CPU utilization by sampling /proc/stat and calculating
//! the percentage of time spent in different CPU states. It provides real-time
//! CPU usage information for display in the status bar.
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::thread;
use std::time::Duration;
use crate::common;
use crate::bar_modules;
/// Represents CPU time spent in various states
///
/// This struct holds counters for different CPU states as reported by /proc/stat.
/// Each field represents the amount of time the CPU has spent in that particular state
/// since system boot, measured in USER_HZ units (typically 100Hz).
#[derive(Debug, Default, Clone)]
struct CpuTimes {
user: u64,
nice: u64,
system: u64,
idle: u64,
iowait: u64,
irq: u64,
softirq: u64,
steal: u64,
guest: u64,
guest_nice: u64,
}
impl CpuTimes {
pub fn new() -> Self {
CpuTimes {
user: 0,
nice: 0,
system: 0,
idle: 0,
iowait: 0,
irq: 0,
softirq: 0,
steal: 0,
guest: 0,
guest_nice: 0
}
}
fn total(&self) -> u64 {
self.user
+ self.nice
+ self.system
+ self.idle
+ self.iowait
+ self.irq
+ self.softirq
+ self.steal
+ self.guest
+ self.guest_nice
}
fn idle(&self) -> u64 {
self.idle + self.iowait
}
}
#[derive(Clone)]
pub struct UnibarModuleCpu {
opts: common::AppOptions,
cpu_times_sample_1: CpuTimes,
cpu_times_sample_2: CpuTimes,
}
impl UnibarModuleCpu {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleCpu {
opts: o,
cpu_times_sample_1: CpuTimes::new(),
cpu_times_sample_2: CpuTimes::new(),
}
}
// --------------------
// cpu 242109 7 60985 6538626 8344 39138 9647 0 0 0
// * user - time spent in user mode
// * nice - time spent processing nice processes in user mode
// * system - time spent executing kernel code
// * idle - time spent idle
// * iowait - time spent waiting for I/O
// * irq - time spent servicing interrupts
// * softirq - time spent servicing software interrupts
// * steal - time stolen from a virtual machine
// * guest - time spent running a virtual CPU for a guest operating system
// * guest_nice - time spent running a virtual CPU for a “niced” guest operating system
fn read_cpu_times(&self) -> io::Result<CpuTimes> {
let file = File::open("/proc/stat")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.starts_with("cpu ") {
let fields: Vec<u64> = line.split_whitespace().skip(1).map(|s| s.parse().unwrap_or(0)).collect();
if fields.len() >= 8 {
return Ok(CpuTimes {
user: fields[0],
nice: fields[1],
system: fields[2],
idle: fields[3],
iowait: fields[4],
irq: fields[5],
softirq: fields[6],
steal: fields[7],
guest: if fields.len() > 8 { fields[8] } else { 0 },
guest_nice: if fields.len() > 9 { fields[9] } else { 0 },
});
}
}
}
Err(io::Error::new(io::ErrorKind::InvalidData, "Could not find cpu line in /proc/stat"))
}
}
impl bar_modules::BarModuleActions for UnibarModuleCpu {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleCpu".to_string();
}
// --------------------
fn clear(&mut self) {
self.cpu_times_sample_1 = CpuTimes::new();
self.cpu_times_sample_2 = CpuTimes::new();
}
// --------------------
fn generate_data(&mut self) {
self.cpu_times_sample_1 = self.read_cpu_times().expect("Trouble getting CPU times sample 1");
thread::sleep(Duration::from_millis(500));
self.cpu_times_sample_2 = self.read_cpu_times().expect("Trouble getting CPU times sample 2");
if self.opts.debug {
println!("-----> CPU times sample 1 - {:#?}", self.cpu_times_sample_1);
println!("-----> CPU times sample 2 - {:#?}", self.cpu_times_sample_2);
}
}
// --------------------
fn get_content(&self) -> String {
let sample_1_total = self.cpu_times_sample_1.total();
let sample_2_total = self.cpu_times_sample_2.total();
let sample_1_idle = self.cpu_times_sample_1.idle();
let sample_2_idle = self.cpu_times_sample_2.idle();
let total_diff = sample_2_total - sample_1_total;
let idle_diff = sample_2_idle - sample_1_idle;
if total_diff > 0 {
let cpu_usage = (1.0 - (idle_diff as f64 / total_diff as f64)) * 100.0;
return format!("{}%", cpu_usage.ceil() as i32);
} else {
return format!("CPU?");
}
}
// --------------------
fn get_icon(&self) -> String {
return "💻".to_string();
}
}
impl bar_modules::BarModuleDebug for UnibarModuleCpu {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,89 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Date module for Unibar
//!
//! This module displays the current date in a formatted string.
//! It updates automatically to show the current date with month,
//! day, and year information.
//!
//! # Features
//! - Formatted date display
//! - Automatic daily updates
//! - Calendar icon
use chrono::{DateTime, Local};
use crate::common;
use crate::bar_modules;
/// Date display module showing current calendar date
#[derive(Clone)]
pub struct UnibarModuleDate {
opts: common::AppOptions,
date_time: DateTime<Local>,
}
impl UnibarModuleDate {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleDate {
opts: o,
date_time: Local::now(),
}
}
}
impl bar_modules::BarModuleActions for UnibarModuleDate {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleDate".to_string();
}
// --------------------
fn clear(&mut self) {
self.date_time = Local::now();
}
// --------------------
fn generate_data(&mut self) {
if self.opts.debug {
println!("-----> Date dump {:#?}", self.date_time);
}
}
// --------------------
fn get_content(&self) -> String {
return format!("{}", self.date_time.format("%Y %h %d"));
}
// --------------------
fn get_icon(&self) -> String {
return "📅".to_string();
}
}
impl bar_modules::BarModuleDebug for UnibarModuleDate {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,112 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Memory module for Unibar
//!
//! This module monitors system memory usage by reading /proc/meminfo.
//! It calculates and displays current memory utilization as a percentage
//! of total available memory.
//!
//! # Features
//! - Real-time memory usage monitoring
//! - Percentage-based display
//! - Automatic updates at configured intervals
use std::fs::File;
use std::path::Path;
use std::io::prelude::*;
use regex::Regex;
use crate::common;
use crate::bar_modules;
/// Memory monitor that displays current system memory usage
#[derive(Clone)]
pub struct UnibarModuleMemory {
opts: common::AppOptions,
meminfo_str: String,
}
impl UnibarModuleMemory {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleMemory {
opts: o,
meminfo_str: "".to_string(),
}
}
}
impl bar_modules::BarModuleActions for UnibarModuleMemory {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleMemory".to_string();
}
// --------------------
fn clear(&mut self) {
self.meminfo_str = "".to_string();
}
// --------------------
fn generate_data(&mut self) {
let path = Path::new("/proc/meminfo");
// Contents of '/proc/meminfo' has memory information
let mut meminfo_file = match File::open(&path) {
Err(why) => panic!("couldn't open {}: {}", path.display(), why),
Ok(file) => file,
};
match meminfo_file.read_to_string(&mut self.meminfo_str) {
Err(why) => panic!("couldn't read {}: {}", path.display(), why),
Ok(_) => if self.opts.debug {
println!("-----> meminfo - {:#?}", self.meminfo_str);
},
};
self.meminfo_str.pop();
}
// --------------------
// MemTotal: 7822812 kB\nMemFree: 399244 kB\nMemAvailable: 3986504 kB
fn get_content(&self) -> String {
let re = Regex::new(r"MemTotal:\s+(\d+)\s+kB\nMemFree:\s+(\d+)\s+kB\nMemAvailable:\s+(\d+)").unwrap();
let caps = re.captures(self.meminfo_str.as_str()).unwrap();
let total_mem :f32 = caps.get(1).unwrap().as_str().parse::<f32>().unwrap();
let avail_mem :f32 = caps.get(3).unwrap().as_str().parse::<f32>().unwrap();
return format!("{}%", ((total_mem - avail_mem)/total_mem * 100.0).ceil() as i32);
}
// --------------------
fn get_icon(&self) -> String {
return "💾".to_string();
}
}
impl bar_modules::BarModuleDebug for UnibarModuleMemory {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,137 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Music module for Unibar
//!
//! This module displays current music playback information.
//! It shows the currently playing track and playback progress
//! using the system's music player.
//!
//! # Features
//! - Current track information
//! - Playback progress
//! - Playback state (playing/paused)
//! - Music note icon
use std::process::{Stdio, Command};
use crate::common;
use crate::bar_modules;
/// Music playback monitor showing current track and progress
#[derive(Clone)]
pub struct UnibarModuleMusic {
opts: common::AppOptions,
current_stdout: String,
progress_stdout: String,
state_stdout: String,
}
impl UnibarModuleMusic {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleMusic {
opts: o,
current_stdout: "".to_string(),
progress_stdout: "".to_string(),
state_stdout: "stopped".to_string(),
}
}
}
impl bar_modules::BarModuleActions for UnibarModuleMusic {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleMusic".to_string();
}
// --------------------
fn clear(&mut self) {
self.current_stdout = "".to_string();
self.progress_stdout = "".to_string();
self.state_stdout = "stopped".to_string();
}
// --------------------
fn generate_data(&mut self) {
// MPD format options here:
// https://manpages.ubuntu.com/manpages/plucky/man1/mpc.1.html
let current_output = Command::new("mpc")
.arg("--format")
.arg("[%artist% - ][%title%]")
.arg("current")
.stdout(Stdio::piped())
.output()
.unwrap();
self.current_stdout = String::from_utf8(current_output.stdout).unwrap();
self.current_stdout.pop();
if self.opts.music_progress {
let progress_output = Command::new("mpc")
.arg("status")
.arg("%percenttime% %totaltime%")
.stdout(Stdio::piped())
.output()
.unwrap();
self.progress_stdout = String::from_utf8(progress_output.stdout).unwrap();
self.progress_stdout.pop();
}
let icon_output = Command::new("mpc")
.arg("status")
.arg("%state%")
.stdout(Stdio::piped())
.output()
.unwrap();
self.state_stdout = String::from_utf8(icon_output.stdout).unwrap();
self.state_stdout.pop();
}
// --------------------
fn get_content(&self) -> String {
let mut parts = Vec::new();
parts.push(format!("{}", self.current_stdout));
if self.opts.music_progress {
parts.push(format!("[{}]", self.progress_stdout.trim()));
}
return format!("{}", parts.join(" "));
}
// --------------------
fn get_icon(&self) -> String {
// MPD state can be 'playing', 'paused' or 'stopped'
match self.state_stdout.as_str() {
"paused" => return "".to_string(),
"stopped" => return "".to_string(),
_ => return "𝄞".to_string(),
}
}
}
impl bar_modules::BarModuleDebug for UnibarModuleMusic {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,152 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Network module for Unibar
//!
//! This module monitors network interfaces and displays current connection status
//! and IP addresses. It supports both wireless and ethernet interfaces, with
//! different icons for each connection type.
//!
//! # Features
//! - Automatic interface detection
//! - Priority-based interface selection (ethernet over wireless)
//! - IP address display
//! - Connection status monitoring
use serde_json::json;
use serde_json::Value;
use std::process::{Stdio, Command};
use crate::common;
use crate::bar_modules;
/// Network interface monitor that displays connection status and IP addresses
#[derive(Clone)]
pub struct UnibarModuleNetwork {
opts: common::AppOptions,
ip_addr_stdout: String,
network_info: Value,
}
impl UnibarModuleNetwork {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleNetwork {
opts: o,
ip_addr_stdout: "".to_string(),
network_info: json!(serde_json::Value::Null),
}
}
}
impl bar_modules::BarModuleActions for UnibarModuleNetwork {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleNetwork".to_string();
}
// --------------------
fn clear(&mut self) {
self.network_info = json!(serde_json::Value::Null);
}
// --------------------
fn generate_data(&mut self) {
// Output of 'ip -j address' command has network information
match Command::new("ip")
.arg("-j") // Output in json format
.arg("address")
.stdout(Stdio::piped())
.output() {
Err(e) => {
eprintln!("Error getting network information {e}");
self.network_info = json!(serde_json::Value::Null);
}
Ok(ip_addr_output) => {
self.ip_addr_stdout = String::from_utf8(ip_addr_output.stdout).unwrap();
self.ip_addr_stdout.pop();
let network_data: Value = serde_json::from_str::<Value>(self.ip_addr_stdout.as_str()).unwrap();
if let Some(interfaces) = network_data.as_array() {
if self.opts.debug {
println!("-----> network_data - {:#?}", network_data);
}
// Get all interfaces that are up
let mut up_intf :Vec<_> = interfaces.iter()
.filter(|el| el["operstate"].as_str().unwrap().contains("UP"))
.cloned().collect();
// Sort by 'ifname'. This is an unreliable way to proiritize ethernet over wifi.
// Ethenet network interface names normally start with 'e' and wifi interface names
// start with 'w'
up_intf.sort_by(|a, b| a["ifname"].as_str().unwrap().cmp(b["ifname"].as_str().unwrap()));
if up_intf.len() > 0 {
let inet_addr :Vec<_> = up_intf[0]["addr_info"].as_array().unwrap().iter()
.filter(|ai| ai["scope"].as_str().unwrap().contains("global"))
.cloned().collect();
if inet_addr.len() > 0 {
if self.opts.debug {
println!("-----> Inet Addr - {:#?}", inet_addr);
}
self.network_info = inet_addr[0].clone();
self.network_info["ifname"] = up_intf[0]["ifname"].clone();
}
}
}
}
}
if self.opts.debug {
println!("-----> ip_addr - {:#?}", self.network_info);
}
}
// --------------------
fn get_content(&self) -> String {
if self.network_info != serde_json::Value::Null {
// If any interface was up, return the local IP address
return self.network_info["local"].as_str().unwrap().to_string();
}
return "Network down".to_string();
}
// --------------------
fn get_icon(&self) -> String {
if self.network_info != serde_json::Value::Null {
// Select icon based on which interface is up (ethernet or wifi)
if self.network_info["ifname"].as_str().unwrap().to_string().starts_with("w") {
return "📶".to_string();
} else {
return "🌎".to_string();
}
}
return "📡".to_string();
}
}
impl bar_modules::BarModuleDebug for UnibarModuleNetwork {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,184 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Power module for Unibar
//!
//! This module monitors system power status using UPower.
//! It displays battery level and charging status with appropriate icons.
//!
//! # Features
//! - Battery level monitoring
//! - Charging status detection
//! - Different icons for charging/discharging states
//! - Automatic updates
use std::process::{Stdio, Command};
use regex::Regex;
use crate::common;
use crate::bar_modules;
/// Power monitor that displays battery status and level
#[derive(Clone)]
pub struct UnibarModulePower {
opts: common::AppOptions,
power_info: PowerData,
}
#[derive(Debug, Default, Clone)]
struct PowerData {
charging: bool,
level: u64,
}
impl PowerData {
pub fn new() -> Self {
PowerData {
charging: true,
level: 100,
}
}
}
impl UnibarModulePower {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModulePower {
opts: o,
power_info: PowerData::new(),
}
}
}
impl bar_modules::BarModuleActions for UnibarModulePower {
// --------------------
fn get_name(&self) -> String {
return "UnibarModulePower".to_string();
}
// --------------------
fn clear(&mut self) {
self.power_info = PowerData::new();
}
// --------------------
fn generate_data(&mut self) {
// Following command is used to get power data:
// % upower -i /org/freedesktop/UPower/devices/DisplayDevice
//
// In UPower, the "display device" is a special object that represents the combined
// power status that should be shown in desktop environments.
//
// Purpose:
//
// It acts as a central point of information for the power icon displayed in your
// system's notification area or taskbar.
// Instead of desktop environments having to query all individual power sources,
// they can access this single "display device" to get a summary of the overall
// power status.
//
// Key characteristics:
//
// Composite device: It combines information from multiple power sources
// (like batteries, UPS) to provide a unified view.
// Object path: It has a dedicated object path, typically
// /org/freedesktop/UPower/devices/DisplayDevice.
// Properties: It exposes properties like Type, State, Percentage, TimeToEmpty,
// TimeToFull, and IconName to describe the device's status and
// icon representation.
// Dynamic Type: Unlike real devices, the Type of the display device can
// change (e.g., from Battery to UPS) depending on the current power situation.
//
// In essence, the "display device" simplifies power management for desktop environments
// by providing a convenient interface to show the relevant power information to the user.
//
let power_dump = Command::new("upower")
.arg("--dump")
.stdout(Stdio::piped())
.output()
.unwrap();
let power_str = String::from_utf8(power_dump.stdout).unwrap();
let lines: Vec<&str> = power_str.split('\n').map(|s| s.trim()).collect();
let mut found_device: bool = false;
// let devices: HashMap<String, HashMap<String, String>>::new();
// let cur_dev: HashMap<String, String>::new();
for line in lines {
if line.is_empty() { continue };
let dev_re = Regex::new(r"^Device:\s+(.*)").unwrap();
if let Some(caps) = dev_re.captures(line) {
if let Some((last, _els)) = &caps[1].split('/').collect::<Vec<_>>().split_last() {
if self.opts.debug {
println!("-----> Power dump - Device - {:#?}", last);
}
if last.to_string() == "DisplayDevice" {
found_device = true;
}
}
}
if found_device {
let level_re = Regex::new(r"^percentage:\s+(\d+)").unwrap();
if let Some(caps) = level_re.captures(line) {
self.power_info.level = caps[1].parse().unwrap_or(0);
if self.opts.debug {
println!("-----> Power dump - level - {:#?}", self.power_info.level);
}
}
let state_re = Regex::new(r"^state:\s+(\S+)").unwrap();
if let Some(caps) = state_re.captures(line) {
self.power_info.charging = caps[1].to_string() != "discharging";
if self.opts.debug {
println!("-----> Power dump - charging - {:#?}", self.power_info.charging);
}
}
}
// if self.opts.debug {
// println!("-----> Power dump {:#?}", line);
// }
}
}
// --------------------
fn get_content(&self) -> String {
// return format!("{}", parts.join(" "));
return format!("{:.0}%", self.power_info.level);
}
// --------------------
fn get_icon(&self) -> String {
match self.power_info.charging {
true => return "🔌".to_string(),
false => return "🔋".to_string()
}
}
}
impl bar_modules::BarModuleDebug for UnibarModulePower {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,106 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Time module for Unibar
//!
//! This module displays the current time with timezone information.
//! It automatically updates to show the current time in 12-hour format
//! with AM/PM indicator.
//!
//! # Features
//! - 12-hour time format with AM/PM
//! - Timezone display
//! - Automatic updates
//! - Unicode clock icon
use chrono::{DateTime, TimeZone, Local, Utc};
use crate::common;
use crate::bar_modules;
use chrono_tz::{OffsetName, Tz};
/// Time display module showing current time and timezone
#[derive(Clone)]
pub struct UnibarModuleTime {
opts: common::AppOptions,
date_time: DateTime<Local>,
time_zone: String,
}
impl UnibarModuleTime {
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleTime {
opts: o,
date_time: Local::now(),
time_zone: "".to_string(),
}
}
}
impl bar_modules::BarModuleActions for UnibarModuleTime {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleTime".to_string();
}
// --------------------
fn clear(&mut self) {
self.date_time = Local::now();
self.time_zone = "".to_string();
}
// --------------------
fn generate_data(&mut self) {
match iana_time_zone::get_timezone() {
Ok(tz_str) => {
let tz: Tz = tz_str.parse().expect("Trouble parsing timezone string");
let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
self.time_zone = offset.abbreviation()
.expect("Trouble abbreviating timezone")
.to_string();
}
Err(e) => {
eprintln!("Trouble getting timezone: {e}");
}
}
if self.opts.debug {
println!("-----> Time dump {:#?}", self.date_time);
}
}
// --------------------
fn get_content(&self) -> String {
return format!("{} {}", self.date_time.format("%I:%M%p"), self.time_zone);
}
// --------------------
fn get_icon(&self) -> String {
return "🕑".to_string();
}
}
impl bar_modules::BarModuleDebug for UnibarModuleTime {
// --------------------
fn post_debug(&self) {
}
}

View File

@@ -0,0 +1,745 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Weather module for Unibar
//!
//! Fetches weather data from the National Weather Service API and displays
//! current temperature and conditions using Unicode weather symbols.
//!
//! # Features
//! - Temperature display in Celsius or Fahrenheit
//! - Automatic updates at configurable intervals
//! - Weather condition icons using Unicode symbols
//! - Robust error handling for API interactions
use std::str;
use curl::easy::{Easy, List};
use serde_json::json;
use serde_json::Value;
use regex::Regex;
use thiserror::Error;
use std::num::ParseFloatError;
use crate::common;
use crate::common::UpdateResult;
use crate::bar_modules;
/// Number of cycles between weather updates
const UPDATE_INTERVAL: u32 = 300;
/// Base URL for the National Weather Service API
const WEATHER_API_BASE: &str = "https://api.weather.gov";
#[derive(Clone)]
pub struct UnibarModuleWeather {
opts: common::AppOptions,
weather_info: Value,
update_cnt: u32,
}
#[derive(Debug, Error)]
pub enum WeatherError {
#[error("Invalid temperature format: {0}")]
ParseError(String),
#[error("API response missing temperature field: {0}")]
ApiResponseStrErr(String),
}
impl From<ParseFloatError> for WeatherError {
fn from(err: ParseFloatError) -> Self {
WeatherError::ParseError(err.to_string())
}
}
impl UnibarModuleWeather {
const FAHRENHEIT_MULTIPLIER: f64 = 9.0 / 5.0;
const FAHRENHEIT_OFFSET: f64 = 32.0;
fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * Self::FAHRENHEIT_MULTIPLIER + Self::FAHRENHEIT_OFFSET
}
// --------------------
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleWeather {
opts: o,
weather_info: json!(serde_json::Value::Null),
update_cnt: 0,
}
}
// --------------------
fn get_current_temperature(&self) -> Result<f64, WeatherError> {
let deg_c = match &self.weather_info {
serde_json::Value::Null => 0.0,
_ => {
// Safely navigate the JSON structure
self.weather_info
.get("features")
.and_then(|f| f.get(0))
.and_then(|f| f.get("properties"))
.and_then(|p| p.get("temperature"))
.and_then(|t| t.get("value"))
.and_then(|v| v.as_f64())
.ok_or(WeatherError::ApiResponseStrErr("as_str likely failed".to_string()))?
// .parse()?
}
};
Ok(match self.opts.weather_units {
common::TemperatureUnits::Metric => deg_c,
common::TemperatureUnits::Imperial => Self::celsius_to_fahrenheit(deg_c),
})
}
// --------------------
fn get_temperature_unit(&self) -> String{
return if self.opts.weather_units == common::TemperatureUnits::Metric { "°C".to_string() }
else { "°F".to_string() };
}
// --------------------
fn get_unicode_symbol(&self, condition: &str) -> String {
match condition {
"skc" => { return "☀️".to_string(); } // Fair/clear
"few" => { return "🌥️".to_string(); } // A few clouds
"sct" => { return "🌥️".to_string(); } // Partly cloudy
"bkn" => { return "🌥️".to_string(); } // Mostly cloudy
"ovc" => { return "☁️".to_string(); } // Overcast
"wind_skc" => { return "☀️".to_string(); } // Fair/clear and windy
"wind_few" => { return "🌥️".to_string(); } // A few clouds and windy
"wind_sct" => { return "🌥️".to_string(); } // Partly cloudy and windy
"wind_bkn" => { return "🌥️".to_string(); } // Mostly cloudy and windy
"wind_ovc" => { return "☁️".to_string(); } // Overcast and windy
"snow" => { return "❄️".to_string(); } // Snow
"rain_snow" => { return "🌨️".to_string(); } // Rain/snow
"rain_sleet" => { return "🌨️".to_string(); } // Rain/sleet
"snow_sleet" => { return "🌨️".to_string(); } // Snow/sleet
"fzra" => { return "🌨️".to_string(); } // Freezing rain
"rain_fzra" => { return "🌨️".to_string(); } // Rain/freezing rain
"snow_fzra" => { return "🌨️".to_string(); } // Freezing rain/snow
"sleet" => { return "🌨️".to_string(); } // Sleet
"rain" => { return "🌧️".to_string(); } // Rain
"rain_showers" => { return "🌧️".to_string(); } // Rain showers (high cloud cover)
"rain_showers_hi" => { return "🌧️".to_string(); } // Rain showers (low cloud cover)
"tsra" => { return "⛈️".to_string(); } // Thunderstorm (high cloud cover)
"tsra_sct" => { return "⛈️".to_string(); } // Thunderstorm (medium cloud cover)
"tsra_hi" => { return "⛈️".to_string(); } // Thunderstorm (low cloud cover)
"tornado" => { return "🌪️".to_string(); } // Tornado
"hurricane" => { return "🌬️".to_string(); } // Hurricane conditions
"tropical_storm" => { return "🌬️".to_string(); } // Tropical storm conditions
"dust" => { return "🌫️".to_string(); } // Dust
"smoke" => { return "🌫️".to_string(); } // Smoke
"haze" => { return "🌫️".to_string(); } // Haze
"hot" => { return "🥵".to_string(); } // Hot
"cold" => { return "🧣".to_string(); } // Cold
"blizzard" => { return "🥶".to_string(); } // Blizzard
"fog" => { return "🌫️".to_string(); } // Fog/mist
_ => { return "🌤️".to_string(); } // It is always sunny here
}
}
// --------------------
fn get_icons(&self) {
// Print a web page onto stdout
let mut curl = Easy::new();
let mut url_string = Vec::new();
let mut curl_ret = Vec::new();
url_string.push(WEATHER_API_BASE.to_owned());
url_string.push("/icons".to_owned());
curl.url(url_string.concat().as_str()).unwrap();
{
let mut list = List::new();
list.append("User-Agent: Bar Weather (mahesh@heshapps.com)").unwrap();
curl.http_headers(list).unwrap();
let mut transfer = curl.transfer();
transfer.write_function(|data| {
curl_ret.extend_from_slice(data);
Ok(data.len())
}).unwrap();
transfer.perform().unwrap();
}
println!("-----> curl_data - [{}]", std::str::from_utf8(&curl_ret).unwrap());
}
// --------------------
fn show_icons(&self) {
println!("{} skc Fair/clear", self.get_unicode_symbol("skc"));
println!("{} few A few clouds", self.get_unicode_symbol("few"));
println!("{} sct Partly cloudy", self.get_unicode_symbol("sct"));
println!("{} bkn Mostly cloudy", self.get_unicode_symbol("bkn"));
println!("{} ovc Overcast", self.get_unicode_symbol("ovc"));
println!("{} wind_skc Fair/clear and windy", self.get_unicode_symbol("wind_skc"));
println!("{} wind_few A few clouds and windy", self.get_unicode_symbol("wind_few"));
println!("{} wind_sct Partly cloudy and windy", self.get_unicode_symbol("wind_sct"));
println!("{} wind_bkn Mostly cloudy and windy", self.get_unicode_symbol("wind_bkn"));
println!("{} wind_ovc Overcast and windy", self.get_unicode_symbol("wind_ovc"));
println!("{} snow Snow", self.get_unicode_symbol("snow"));
println!("{} rain_snow Rain/snow", self.get_unicode_symbol("rain_snow"));
println!("{} rain_sleet Rain/sleet", self.get_unicode_symbol("rain_sleet"));
println!("{} snow_sleet Snow/sleet", self.get_unicode_symbol("snow_sleet"));
println!("{} fzra Freezing rain", self.get_unicode_symbol("fzra"));
println!("{} rain_fzra Rain/freezing rain", self.get_unicode_symbol("rain_fzra"));
println!("{} snow_fzra Freezing rain/snow", self.get_unicode_symbol("snow_fzra"));
println!("{} sleet Sleet", self.get_unicode_symbol("sleet"));
println!("{} rain Rain", self.get_unicode_symbol("rain"));
println!("{} rain_showers Rain showers (high cloud cover)", self.get_unicode_symbol("rain_showers"));
println!("{} rain_showers_hi Rain showers (low cloud cover)", self.get_unicode_symbol("rain_showers_hi"));
println!("{} tsra Thunderstorm (high cloud cover)", self.get_unicode_symbol("tsra"));
println!("{} tsra_sct Thunderstorm (medium cloud cover)", self.get_unicode_symbol("tsra_sct"));
println!("{} tsra_hi Thunderstorm (low cloud cover)", self.get_unicode_symbol("tsra_hi"));
println!("{} tornado Tornado", self.get_unicode_symbol("tornado"));
println!("{} hurricane Hurricane conditions", self.get_unicode_symbol("hurricane"));
println!("{} tropical_storm Tropical storm conditions", self.get_unicode_symbol("tropical_storm"));
println!("{} dust Dust", self.get_unicode_symbol("dust"));
println!("{} smoke Smoke", self.get_unicode_symbol("smoke"));
println!("{} haze Haze", self.get_unicode_symbol("haze"));
println!("{} hot Hot", self.get_unicode_symbol("hot"));
println!("{} cold Cold", self.get_unicode_symbol("cold"));
println!("{} blizzard Blizzard", self.get_unicode_symbol("blizzard"));
println!("{} fog Fog/mist", self.get_unicode_symbol("fog"));
println!("{} _ It is always sunny here", self.get_unicode_symbol("_"));
}
}
impl bar_modules::BarModuleActions for UnibarModuleWeather {
// --------------------
fn get_name(&self) -> String {
return "UnibarModuleWeather".to_string();
}
// --------------------
// Weather update every 60 cycles
fn should_update(&mut self) -> UpdateResult {
if self.update_cnt == 0 {
self.update_cnt = UPDATE_INTERVAL;
return UpdateResult::Update;
} else {
self.update_cnt -= 1;
return UpdateResult::Skip;
}
}
// --------------------
fn clear(&mut self) {
self.weather_info = json!(serde_json::Value::Null);
}
// --------------------
fn generate_data(&mut self) {
// Print a web page onto stdout
let mut curl = Easy::new();
let mut url_string = Vec::new();
let mut curl_ret = Vec::new();
let mut curl_err = false;
url_string.push(WEATHER_API_BASE.to_owned());
url_string.push("/stations/".to_owned());
url_string.push(self.opts.weather_station.to_owned());
url_string.push("/observations?limit=1".to_owned());
curl.url(url_string.concat().as_str()).unwrap();
let mut list = List::new();
list.append("User-Agent: Bar Weather (mahesh@heshapps.com)").unwrap();
curl.http_headers(list).unwrap();
// Scoped block to handle the data transfer (Credit: Gemini)
{
let mut transfer = curl.transfer();
// Configure callback to write received response
transfer.write_function(|data| {
curl_ret.extend_from_slice(data);
Ok(data.len())
}).unwrap();
// Perform the request once configured
if let Err(e) = transfer.perform() {
eprintln!("Curl error: {e}");
curl_err = true;
}
} // transfer goes out of scope and cleans up here
if !curl_err {
let curl_reg_str = String::from_utf8(curl_ret).unwrap_or_else(|_| {
"".to_string()
});
self.weather_info = serde_json::from_str(curl_reg_str.as_str()).unwrap();
if self.weather_info["features"][0]["properties"]["temperature"]["value"] == serde_json::Value::Null {
self.weather_info = serde_json::Value::Null;
}
}
}
// --------------------
fn get_content(&self) -> String {
let temperature_value :f64 = self.get_current_temperature().expect("Temperature query");
let temperature_unit :String = self.get_temperature_unit();
// let temperature_icon :String = self.get_icon(v.clone());
return format!("{:.0}{}", temperature_value, temperature_unit);
}
// --------------------
fn get_icon(&self) -> String {
match self.weather_info {
serde_json::Value::Null => {
return "".to_string();
}
_ => {
// "icon": "https://api.weather.gov/icons/land/night/ovc?size=medium",
let re = Regex::new(r"(\w+)\?size").unwrap();
let json_val = self.weather_info["features"][0]["properties"]["icon"].to_string();
if self.opts.debug {
println!("-----> weather_data - {:#?}", self.weather_info["features"][0]["properties"]);
}
match self.weather_info["features"][0]["properties"]["icon"] {
serde_json::Value::Null => {
return "".to_string();
}
_ => {
let caps = re.captures(&json_val).unwrap();
return self.get_unicode_symbol(caps.get(1).unwrap().as_str());
}
}
}
}
}
}
impl bar_modules::BarModuleDebug for UnibarModuleWeather {
// --------------------
fn post_debug(&self) {
self.get_icons();
self.show_icons();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bar_modules::BarModuleActions; // Add this line
use crate::common::AppOptions;
use crate::common::TemperatureUnits;
/// Creates a default AppOptions instance for testing
fn create_test_options(metric: bool) -> AppOptions {
AppOptions {
interval: 1,
weather_units: if metric { TemperatureUnits::Metric } else { TemperatureUnits::Imperial },
weather_station: "test_station".to_string(),
music_progress: false,
debug: false,
debug_modules: false,
debug_update: false,
}
}
#[test]
/// Test get_current_temperature returns correct value for metric and imperial units
fn test_get_current_temperature_units() {
let mut metric_module = UnibarModuleWeather::new(create_test_options(true));
let mut imperial_module = UnibarModuleWeather::new(create_test_options(false));
metric_module.weather_info = json!({
"features": [{
"properties": {
"temperature": {
"value": 25.0
}
}
}]
});
imperial_module.weather_info = metric_module.weather_info.clone();
assert_eq!(metric_module.get_current_temperature().unwrap(), 25.0);
assert!((imperial_module.get_current_temperature().unwrap() - 77.0).abs() < 0.01);
}
#[test]
/// Test get_unicode_symbol returns default for unknown condition
fn test_get_unicode_symbol_default() {
let module = UnibarModuleWeather::new(create_test_options(true));
assert_eq!(module.get_unicode_symbol("not_a_real_condition"), "🌤️");
}
#[test]
/// Test clear resets weather_info to Null
fn test_clear_resets_weather_info() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
module.weather_info = json!({"features": []});
module.clear();
assert_eq!(module.weather_info, json!(serde_json::Value::Null));
}
#[test]
/// Test get_content returns formatted temperature string
fn test_get_content_format() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
module.weather_info = json!({
"features": [{
"properties": {
"temperature": {
"value": 18.7
}
}
}]
});
let content = module.get_content();
assert!(content.starts_with("19°C"));
}
#[test]
/// Test get_icon returns fallback when icon is missing
fn test_get_icon_missing_icon() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
module.weather_info = json!({
"features": [{
"properties": {
"icon": serde_json::Value::Null
}
}]
});
assert_eq!(module.get_icon(), "");
}
#[test]
/// Test get_icon returns fallback when weather_info is Null
fn test_get_icon_null_weather_info() {
let module = UnibarModuleWeather::new(create_test_options(true));
assert_eq!(module.get_icon(), "");
}
#[test]
/// Test weather module initialization
fn test_module_initialization() {
let module = UnibarModuleWeather::new(create_test_options(true));
assert_eq!(module.update_cnt, 0);
assert_eq!(module.weather_info, json!(serde_json::Value::Null));
}
#[test]
/// Test temperature unit display
fn test_temperature_unit_display() {
let metric_module = UnibarModuleWeather::new(create_test_options(true));
let imperial_module = UnibarModuleWeather::new(create_test_options(false));
assert_eq!(metric_module.get_temperature_unit(), "°C");
assert_eq!(imperial_module.get_temperature_unit(), "°F");
}
#[test]
/// Test weather condition to Unicode symbol mapping
fn test_unicode_symbol_mapping() {
let module = UnibarModuleWeather::new(create_test_options(true));
assert_eq!(module.get_unicode_symbol("snow"), "❄️");
assert_eq!(module.get_unicode_symbol("ovc"), "☁️");
assert_eq!(module.get_unicode_symbol("rain"), "🌧️");
assert_eq!(module.get_unicode_symbol("unknown"), "🌤️");
}
#[test]
/// Test error handling for invalid temperature data
fn test_error_handling() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
// Test null weather info
assert!(module.get_current_temperature().is_ok());
assert_eq!(module.get_current_temperature().unwrap(), 0.0);
// Test invalid JSON structure
module.weather_info = json!({
"features": [{
"properties": {
"temperature": {
"value": "invalid"
}
}
}]
});
assert!(module.get_current_temperature().is_err());
}
#[test]
/// Test update counter behavior. Update only when update_cnt==0
fn test_update_counter() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
assert_eq!(module.update_cnt, 0);
assert_eq!(module.should_update(), UpdateResult::Update);
module.update_cnt = UPDATE_INTERVAL - 1;
assert_eq!(module.should_update(), UpdateResult::Skip);
module.update_cnt = UPDATE_INTERVAL;
assert_eq!(module.should_update(), UpdateResult::Skip);
}
#[test]
/// Test JSON parsing with valid weather data
fn test_valid_weather_data_parsing() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
// Simulate valid weather data
module.weather_info = json!({
"features": [{
"properties": {
"temperature": {
"value": 20.5
}
}
}]
});
// println!("{:#?}", module.weather_info);
let result = module.get_current_temperature();
// println!("{:#?}", result);
match result {
Ok(20.5) => {
// This is fine - we expect 20.5 for valid weather_info
assert_eq!(module.weather_info["features"][0]["properties"]["temperature"]["value"], 20.5);
},
Err(WeatherError::ApiResponseStrErr(_)) => {
assert!(false, "Expected valid temperature but got ApiResponseStrErr");
},
other => {
panic!("Unexpected result: {:?}", other);
}
}
}
#[test]
/// Test icon extraction from weather API URL
fn test_icon_extraction() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
// Simulate weather data with icon URL
module.weather_info = json!({
"features": [{
"properties": {
"icon": "https://api.weather.gov/icons/land/night/ovc?size=medium"
}
}]
});
let icon = module.get_icon();
// println!("Extracted icon: {}", icon);
// Check if the icon is correctly extracted
assert_eq!(icon, "☁️");
}
#[test]
/// Test handling of malformed/incomplete JSON structures
fn test_missing_json_structure() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
// Test cases with increasingly incomplete JSON structures
let test_cases = vec![
// Missing value field
json!({
"features": [{
"properties": {
"temperature": {}
}
}]
}),
// Missing temperature object
json!({
"features": [{
"properties": {}
}]
}),
// Missing properties
json!({
"features": [{}]
}),
// Empty features array
json!({
"features": []
}),
// Missing features
json!({}),
// Completely different structure
json!({
"unexpected": "structure"
}),
// Empty object
json!({}),
// Null value
json!(null),
// Array instead of object
json!([1, 2, 3]),
// Wrong data types
json!({
"features": [{
"properties": {
"temperature": {
"value": true // boolean instead of number
}
}
}]
}),
];
for (i, test_case) in test_cases.into_iter().enumerate() {
module.weather_info = test_case;
let result = module.get_current_temperature();
match result {
Ok(0.0) => {
// This is fine - we expect 0.0 for null weather_info
assert_eq!(module.weather_info, json!(null),
"Case {}: Should return 0.0 only for null weather_info", i);
},
Err(WeatherError::ApiResponseStrErr(_)) => {
// This is also fine - we expect ApiResponseStrErr for malformed data
},
other => {
panic!("Case {}: Unexpected result: {:?}", i, other);
}
}
}
}
#[test]
/// Test handling of malformed temperature values
fn test_invalid_temperature_values() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
let test_cases = vec![
// Invalid temperature values
json!({
"features": [{
"properties": {
"temperature": {
"value": "not a number"
}
}
}]
}),
json!({
"features": [{
"properties": {
"temperature": {
"value": ""
}
}
}]
}),
json!({
"features": [{
"properties": {
"temperature": {
"value": null
}
}
}]
}),
json!({
"features": [{
"properties": {
"temperature": {
"value": {}
}
}
}]
}),
json!({
"features": [{
"properties": {
"temperature": {
"value": []
}
}
}]
})
];
for (i, test_case) in test_cases.into_iter().enumerate() {
module.weather_info = test_case;
let result = module.get_current_temperature();
assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))),
"Case {}: Expected ApiResponseStrErr for invalid temperature value", i);
}
}
#[test]
/// Test that deep nested access is safe
fn test_deep_nested_access() {
let mut module = UnibarModuleWeather::new(create_test_options(true));
// Test accessing nested fields that don't exist
let test_cases = vec![
// Deeper nesting than expected
json!({
"features": [{
"properties": {
"temperature": {
"value": {
"nested": "too deep"
}
}
}
}]
}),
// Missing intermediate fields
json!({
"features": [{
"properties": {
"temperature": {
"missing_value": 20.5
}
}
}]
}),
// Wrong nesting structure
json!({
"features": { // Object instead of array
"properties": {
"temperature": {
"value": 20.5
}
}
}
})
];
for (i, test_case) in test_cases.into_iter().enumerate() {
module.weather_info = test_case;
let result = module.get_current_temperature();
assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))),
"Case {}: Expected ApiResponseStrErr for invalid nested structure", i);
}
}
}

72
src/bar_modules/mod.rs Normal file
View File

@@ -0,0 +1,72 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Bar modules for the Unibar status bar
//!
//! This module contains implementations of various status bar components that
//! display system information like weather, CPU usage, memory, etc.
//!
//! Each module implements the `BarModuleActions` trait which defines the core
//! functionality required for status bar components.
use crate::common::UpdateResult;
/// Core trait that must be implemented by all bar modules.
///
/// This trait defines the interface that every status bar component must implement
/// to provide its functionality to the Unibar system.
pub trait BarModuleActions {
/// Returns the name of the module
fn get_name(&self) -> String;
/// Cleans up any state before refreshing data
fn clear(&mut self);
/// Fetches and processes new data for the module
fn generate_data(&mut self);
/// Returns the formatted content to display in the bar
fn get_content(&self) -> String;
/// Returns a Unicode icon representing the module's current state
fn get_icon(&self) -> String;
/// Determines if the module should update its data
/// Default implementation always returns Update
fn should_update(&mut self) -> UpdateResult {
return UpdateResult::Update;
}
}
pub trait BarModuleDebug {
/// Prints debug information about the module
fn post_debug(&self);
}
// Module declarations
pub mod bar_module_weather;
pub mod bar_module_music;
pub mod bar_module_network;
pub mod bar_module_memory;
pub mod bar_module_cpu;
pub mod bar_module_power;
pub mod bar_module_date;
pub mod bar_module_time;

39
src/common/mod.rs Normal file
View File

@@ -0,0 +1,39 @@
use std::fmt;
// --------------------
// Enums
// --------------------
#[derive(Debug,PartialEq,Eq,Copy,Clone)]
pub enum TemperatureUnits {
Metric,
Imperial,
}
impl fmt::Display for TemperatureUnits {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TemperatureUnits::Metric => write!(f, "Metric"),
TemperatureUnits::Imperial => write!(f, "Imperial"),
}
}
}
#[derive(Debug,PartialEq,Eq,Copy,Clone)]
pub enum UpdateResult {
Update,
Skip
}
// --------------------
// Application options
// --------------------
#[derive(Debug,Clone)]
pub struct AppOptions {
pub interval: u64,
pub weather_units: TemperatureUnits,
pub weather_station: String,
pub music_progress: bool,
pub debug: bool,
pub debug_modules: bool,
pub debug_update: bool,
}

254
src/main.rs Normal file
View File

@@ -0,0 +1,254 @@
// MIT License
// Copyright (c) 2025 Mahesh @ HeshApps
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Unibar - A Versatile Status Bar Information Provider
//!
//! Unibar is a customizable status bar information provider that displays various
//! system metrics and information in a format suitable for integration with
//! status bars like i3bar, polybar, or similar tools.
//!
//! # Features
//! - System monitoring (CPU, memory, network)
//! - Power management (battery status, charging)
//! - Weather information
//! - Date and time display
//! - Music playback information
//! - Configurable update intervals
//! - Support for both metric and imperial units
//!
//! # Usage
//! ```bash
//! unibar --interval 1 --weather-station khio --weather-metric
//! ```
//!
//! The output is formatted as a string that can be directly used
//! by status bar applications.
use std::str;
use std::thread;
use std::time::Duration;
use clap::Parser;
use crate::common::UpdateResult;
/// Common utilities and types used across modules
mod common;
/// Status bar component modules
mod bar_modules;
/// Command-line arguments for configuring Unibar behavior
#[derive(Parser, Debug)]
#[command(name = "unibar")]
#[command(version = "1.0")]
#[command(about = "A versatile status bar information provider")]
#[command(long_about = "Unibar provides system information, weather, and other metrics \
in a format suitable for status bars. It supports various modules including \
system monitoring, weather information, and music playback status.")]
struct CommandlineArgs {
/// The frequency at which information is updated, in seconds.
/// Lower values provide more frequent updates but increase system load.
#[arg(short = 'i', long, default_value = "1")]
interval: u64,
/// Name of the weather station
#[arg(short = 's', long, default_value = "khio")]
weather_station: String,
/// Use Metric units in weather data. Imperial units otherwise
#[arg(short = 'm', long)]
weather_metric: bool,
/// Show music progess
#[arg(short = 'p', long)]
music_progress: bool,
/// Show verbose debug information during run
#[arg(short = 'D', long)]
debug: bool,
/// Show module debug information after all modules are evaluated
/// but before output is printed
#[arg(short = 'M', long)]
debug_modules: bool,
/// Show module debug information after all modules are evaluated
/// but before output is printed
#[arg(short = 'U', long)]
debug_update: bool,
}
//
// Application (Unibar)
//
//#[derive(Clone)]
/// Main application structure that manages all status bar modules
/// and coordinates their updates.
struct Unibar {
/// Application-wide configuration options
opts: common::AppOptions,
/// List of active status bar modules
bar_modules_enabled: Vec<Box<dyn bar_modules::BarModuleActions>>,
/// Flag to track if module debugging has been performed
debug_modules_done: bool,
}
impl Unibar {
/// Starts the main application loop, initializing all modules and
/// updating them at the configured interval.
///
/// This function:
/// 1. Initializes all enabled modules
/// 2. Enters an infinite loop for updates
/// 3. Coordinates module updates based on their individual timing requirements
fn run(&mut self) {
if self.opts.debug {
self.debug_msg("Debugging ...");
}
self.check_options();
// Set up a list of all modules to be used
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_music::UnibarModuleMusic::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_memory::UnibarModuleMemory::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_cpu::UnibarModuleCpu::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_power::UnibarModulePower::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_network::UnibarModuleNetwork::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_weather::UnibarModuleWeather::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_date::UnibarModuleDate::new(self.opts.clone())));
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_time::UnibarModuleTime::new(self.opts.clone())));
// Run in a forever-loop ...
loop {
// ... to update the bar contents ...
self.update_bar_contents();
// ... at specfied interval
thread::sleep(Duration::from_millis(self.opts.interval * 1000));
}
}
// --------------------
fn update_bar_contents(&mut self) {
// Get module's part to be displayed in the bar
let mut parts = Vec::new();
for md in &mut self.bar_modules_enabled {
let mut mod_parts = Vec::new();
// Each bar module implements following 4 steps:
// * Clear data from previous iteration
// * Generate raw data with pertinent information
// * Return a unicode icon to be displayed
// * Return a String content to be displayed after the icon
//
// Following generates ICON+CONTENT string for a module to be displayed
// in the bar
match md.should_update() {
UpdateResult::Update => {
md.clear();
md.generate_data();
}
UpdateResult::Skip => {
if self.opts.debug_update {
println!("Skipping module {}", md.get_name());
}
}
}
mod_parts.push(md.get_icon());
mod_parts.push(md.get_content());
parts.push(mod_parts.join(" "));
}
self.show_module_debug();
// Print parts provided by each module
println!("status {}", parts.join(" "));
}
// --------------------
fn show_module_debug(&mut self) {
// Show module debug information if enabled
if self.opts.debug_modules {
if !self.debug_modules_done {
let bar_modules_debugged: Vec<Box<dyn bar_modules::BarModuleDebug>> = vec! [
Box::new(bar_modules::bar_module_weather::UnibarModuleWeather::new(self.opts.clone())),
Box::new(bar_modules::bar_module_music::UnibarModuleMusic::new(self.opts.clone())),
Box::new(bar_modules::bar_module_memory::UnibarModuleMemory::new(self.opts.clone())),
Box::new(bar_modules::bar_module_network::UnibarModuleNetwork::new(self.opts.clone())),
Box::new(bar_modules::bar_module_cpu::UnibarModuleCpu::new(self.opts.clone())),
Box::new(bar_modules::bar_module_power::UnibarModulePower::new(self.opts.clone())),
Box::new(bar_modules::bar_module_date::UnibarModuleDate::new(self.opts.clone())),
Box::new(bar_modules::bar_module_time::UnibarModuleTime::new(self.opts.clone())),
];
for md in bar_modules_debugged {
md.post_debug();
}
self.debug_modules_done = true;
}
}
}
// --------------------
fn check_options(&self) -> bool {
let all_good = true;
// If there are option checks to be added, make all_good a
// mutable var and update its status
return all_good;
}
// --------------------
fn debug_msg(&self, msg: &str) {
println!("[Debug ] -----> {}", msg);
}
}
/// Main entry point for the Unibar application.
///
/// This function:
/// 1. Parses command-line arguments
/// 2. Creates and configures the main application instance
/// 3. Starts the application loop
///
/// # Example
/// ```bash
/// # Run with metric units and 1-second updates
/// unibar --interval 1 --weather-metric
/// ```
fn main() {
let cmd_args = CommandlineArgs::parse();
let mut app = Unibar {
opts: common::AppOptions {
interval: cmd_args.interval as u64,
weather_units: if cmd_args.weather_metric { common::TemperatureUnits::Metric }
else { common::TemperatureUnits::Imperial },
weather_station: cmd_args.weather_station,
music_progress: cmd_args.music_progress,
debug: cmd_args.debug,
debug_modules: cmd_args.debug_modules,
debug_update: cmd_args.debug_update,
},
bar_modules_enabled: Vec::new(),
debug_modules_done: false,
};
app.run();
}
// References:
// https://reintech.io/blog/working-with-json-in-rust