Compare commits

...

28 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
13 changed files with 1735 additions and 97 deletions

View File

@@ -2,6 +2,7 @@
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
@@ -10,4 +11,8 @@ 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

@@ -6,15 +6,32 @@ Simple bar written in Rust
* 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
@@ -26,8 +43,11 @@ Options:
-p, --music-progress
Show music progess
-D, --debug-json
Show JSON data returned by query
-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')
@@ -42,12 +62,17 @@ Options:
```shell
$ target/release/unibar
🌧️ 55.94°F 𝄞 Dire Straits - Tunnel of Love
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
🌧️ 55.94°F 𝄞 Dire Straits - Tunnel of Love [ 22% 14:23]
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

@@ -1,30 +1,76 @@
// 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,
opts: common::AppOptions,
current_stdout: String,
progress_stdout: String,
state_stdout: String,
}
impl UnibarModuleMusic {
// --------------------
pub fn new(o :common::AppOptions) -> UnibarModuleMusic {
pub fn new(o :common::AppOptions) -> Self {
UnibarModuleMusic {
opts: o,
current_stdout: "".to_string(),
progress_stdout: "".to_string(),
state_stdout: "stopped".to_string(),
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:
@@ -66,7 +112,7 @@ impl bar_modules::BarModuleActions for UnibarModuleMusic {
parts.push(format!("{}", self.current_stdout));
if self.opts.music_progress {
parts.push(format!("[{}]", self.progress_stdout));
parts.push(format!("[{}]", self.progress_stdout.trim()));
}
return format!("{}", parts.join(" "));

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

@@ -1,37 +1,113 @@
// 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) -> UnibarModuleWeather {
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) -> f32 {
let deg_c :f32 = self.weather_info["features"][0]["properties"]["temperature"]["value"]
.to_string().parse().unwrap();
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()?
}
};
match self.opts.weather_units {
common::TemperatureUnits::Metric => return deg_c,
common::TemperatureUnits::Imperial => return (deg_c * 9.0 / 5.0) + 32.0,
}
Ok(match self.opts.weather_units {
common::TemperatureUnits::Metric => deg_c,
common::TemperatureUnits::Imperial => Self::celsius_to_fahrenheit(deg_c),
})
}
// --------------------
@@ -88,7 +164,8 @@ impl UnibarModuleWeather {
let mut url_string = Vec::new();
let mut curl_ret = Vec::new();
url_string.push("https://api.weather.gov/icons".to_owned());
url_string.push(WEATHER_API_BASE.to_owned());
url_string.push("/icons".to_owned());
curl.url(url_string.concat().as_str()).unwrap();
{
@@ -150,56 +227,111 @@ impl UnibarModuleWeather {
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("https://api.weather.gov/stations/".to_owned());
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();
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();
transfer.perform().unwrap();
}
if self.opts.debug_json {
println!("-----> curl_data - [{}]", std::str::from_utf8(&curl_ret).unwrap());
}
self.weather_info = serde_json::from_str(str::from_utf8(&curl_ret).unwrap()).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 :f32 = self.get_current_temperature();
let temperature_unit :String = self.get_temperature_unit();
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!("{:.2}{}", temperature_value, temperature_unit);
return format!("{:.0}{}", temperature_value, temperature_unit);
}
// --------------------
fn get_icon(&self) -> 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();
let caps = re.captures(&json_val).unwrap();
// println!("{}", caps.get(1).unwrap().as_str());
return self.get_unicode_symbol(caps.get(1).unwrap().as_str());
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());
}
}
}
}
}
}
@@ -211,3 +343,403 @@ impl bar_modules::BarModuleDebug for UnibarModuleWeather {
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);
}
}
}

View File

@@ -1,21 +1,72 @@
// --------------------
/// All Bar modules must implement the actions
// 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 {
/// Do necessary processing to generate data for this bar module
/// 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);
/// Return String content to be displayed in the bar
/// Returns the formatted content to display in the bar
fn get_content(&self) -> String;
/// Return a Unicode icon to display before content in the bar.
/// This icon may differ based on content of the data
/// 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 {
/// Print debug information at the end
/// 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;

View File

@@ -12,20 +12,28 @@ pub enum TemperatureUnits {
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"),
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 weather_units: TemperatureUnits,
pub weather_station: String,
pub music_progress: bool,
pub debug_json: bool,
pub debug_modules: bool,
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,
}

View File

@@ -1,19 +1,71 @@
// 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/types
/// Common utilities and types used across modules
mod common;
// Bar Modules
/// Status bar component modules
mod bar_modules;
// Commandline parsing
/// Command-line arguments for configuring Unibar behavior
#[derive(Parser, Debug)]
#[command(name = "unibar")]
#[command(version = "1.0")]
#[command(about = "Get string of info for a status bar")]
#[command(about, long_about = "A tool that returns variety of components in a string
suitable to use in a status bar")]
#[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,
@@ -26,70 +78,129 @@ struct CommandlineArgs {
#[arg(short = 'p', long)]
music_progress: bool,
/// Show JSON data returned by query
/// Show verbose debug information during run
#[arg(short = 'D', long)]
debug_json: bool,
debug: bool,
/// Show ICON debug information
#[arg(short = 'I', long)]
/// 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)]
//#[derive(Clone)]
/// Main application structure that manages all status bar modules
/// and coordinates their updates.
struct Unibar {
// acts :UnibarActions
/// 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 {
// --------------------
fn run(&self) {
if self.opts.debug_json {
/// 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
let bar_modules_enabled: Vec<Box<dyn bar_modules::BarModuleActions>> = 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())),
];
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 mut md in bar_modules_enabled {
for md in &mut self.bar_modules_enabled {
let mut mod_parts = Vec::new();
// Each bar module implements following 3 steps:
// 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
md.generate_data();
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(" "));
}
// Show module debug information if enabled
if self.opts.debug_modules {
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())),
];
for md in bar_modules_debugged {
md.post_debug();
}
}
self.show_module_debug();
// Print parts provided by each module
println!("{}", parts.join(" "));
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;
}
}
}
// --------------------
@@ -108,20 +219,33 @@ impl Unibar {
}
}
//
// Entry point
//
/// 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 app = Unibar {
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_json: cmd_args.debug_json,
debug_modules: cmd_args.debug_modules
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();