Compare commits
18 Commits
00079662df
...
main
Author | SHA1 | Date | |
---|---|---|---|
28cef49435
|
|||
66923c8f43
|
|||
457f9d71e6 | |||
1e2851103f
|
|||
1834ddac1b
|
|||
a8591da49b
|
|||
0e027005a1
|
|||
7245914da8
|
|||
e8ab11eff9
|
|||
91b02c24fb
|
|||
c33360fb7b
|
|||
be96618f76
|
|||
aeefa114e6
|
|||
e25a8df19e
|
|||
79e1a5956e
|
|||
4452cdbaa0
|
|||
0b7ff61da0
|
|||
51253240b3
|
@@ -2,6 +2,7 @@
|
|||||||
name = "unibar"
|
name = "unibar"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# 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"
|
serde_json = "1.0.113"
|
||||||
curl = "0.4.46"
|
curl = "0.4.46"
|
||||||
regex = "1.11.1"
|
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"] }
|
clap = { version = "4.5.1", features = ["derive"] }
|
||||||
|
thiserror = "1.0"
|
||||||
|
46
README.md
46
README.md
@@ -6,15 +6,32 @@ Simple bar written in Rust
|
|||||||
|
|
||||||
* A tool that returns variety of components in a string suitable to use in a
|
* A tool that returns variety of components in a string suitable to use in a
|
||||||
status bar
|
status bar
|
||||||
|
* Currently implemented modules:
|
||||||
|
- date
|
||||||
|
- memory
|
||||||
|
- music
|
||||||
|
- network
|
||||||
|
- power
|
||||||
|
- time
|
||||||
|
- weather
|
||||||
|
- cpu
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
* Use the `--help` option for usage information:
|
* Use the `--help` option for usage information:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
A tool that returns variety of components in a string
|
||||||
|
suitable to use in a status bar
|
||||||
|
|
||||||
Usage: unibar [OPTIONS]
|
Usage: unibar [OPTIONS]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
-i, --interval <INTERVAL>
|
||||||
|
Update interval in seconds
|
||||||
|
|
||||||
|
[default: 5]
|
||||||
|
|
||||||
-s, --weather-station <WEATHER_STATION>
|
-s, --weather-station <WEATHER_STATION>
|
||||||
Name of the weather station
|
Name of the weather station
|
||||||
|
|
||||||
@@ -26,8 +43,11 @@ Options:
|
|||||||
-p, --music-progress
|
-p, --music-progress
|
||||||
Show music progess
|
Show music progess
|
||||||
|
|
||||||
-D, --debug-json
|
-D, --debug
|
||||||
Show JSON data returned by query
|
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
|
-h, --help
|
||||||
Print help (see a summary with '-h')
|
Print help (see a summary with '-h')
|
||||||
@@ -42,12 +62,30 @@ Options:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ target/release/unibar
|
$ 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
|
* With music progress
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ target/release/unibar --music-progress
|
$ 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
|
||||||
|
```
|
||||||
|
|
||||||
|
# Unit testing
|
||||||
|
|
||||||
|
Use the following command to run unit tests:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo test --package unibar --bin unibar
|
||||||
|
```
|
||||||
|
To run a specific test, use the following:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo test --package unibar --bin unibar -- bar_modules::bar_module_weather::tests::test_get_unicode_symbol_default --exact --show-output
|
||||||
```
|
```
|
||||||
|
6
scripts/vscode
Executable file
6
scripts/vscode
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
/opt/vscode/VSCode-linux-x64/code \
|
||||||
|
--enable-features=UseOzonePlatform,WaylandWindowDecorations \
|
||||||
|
--ozone-platform-hint=auto \
|
||||||
|
--unity-launch %F . & disown %1
|
@@ -1,3 +1,30 @@
|
|||||||
|
// 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::fs::File;
|
||||||
use std::io::{self, BufRead, BufReader};
|
use std::io::{self, BufRead, BufReader};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -6,6 +33,11 @@ use std::time::Duration;
|
|||||||
use crate::common;
|
use crate::common;
|
||||||
use crate::bar_modules;
|
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)]
|
#[derive(Debug, Default, Clone)]
|
||||||
struct CpuTimes {
|
struct CpuTimes {
|
||||||
user: u64,
|
user: u64,
|
||||||
@@ -74,16 +106,16 @@ impl UnibarModuleCpu {
|
|||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
// cpu 242109 7 60985 6538626 8344 39138 9647 0 0 0
|
// cpu 242109 7 60985 6538626 8344 39138 9647 0 0 0
|
||||||
// * user – time spent in user mode
|
// * user - time spent in user mode
|
||||||
// * nice – time spent processing nice processes in user mode
|
// * nice - time spent processing nice processes in user mode
|
||||||
// * system – time spent executing kernel code
|
// * system - time spent executing kernel code
|
||||||
// * idle – time spent idle
|
// * idle - time spent idle
|
||||||
// * iowait – time spent waiting for I/O
|
// * iowait - time spent waiting for I/O
|
||||||
// * irq – time spent servicing interrupts
|
// * irq - time spent servicing interrupts
|
||||||
// * softirq – time spent servicing software interrupts
|
// * softirq - time spent servicing software interrupts
|
||||||
// * steal – time stolen from a virtual machine
|
// * steal - time stolen from a virtual machine
|
||||||
// * guest – time spent running a virtual CPU for a guest operating system
|
// * 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
|
// * guest_nice - time spent running a virtual CPU for a “niced” guest operating system
|
||||||
fn read_cpu_times(&self) -> io::Result<CpuTimes> {
|
fn read_cpu_times(&self) -> io::Result<CpuTimes> {
|
||||||
let file = File::open("/proc/stat")?;
|
let file = File::open("/proc/stat")?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
@@ -116,6 +148,17 @@ impl UnibarModuleCpu {
|
|||||||
|
|
||||||
impl bar_modules::BarModuleActions for UnibarModuleCpu {
|
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) {
|
fn generate_data(&mut self) {
|
||||||
self.cpu_times_sample_1 = self.read_cpu_times().expect("Trouble getting CPU times sample 1");
|
self.cpu_times_sample_1 = self.read_cpu_times().expect("Trouble getting CPU times sample 1");
|
||||||
|
89
src/bar_modules/bar_module_date.rs
Normal file
89
src/bar_modules/bar_module_date.rs
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,35 @@
|
|||||||
|
// 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::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
@@ -6,6 +38,7 @@ use regex::Regex;
|
|||||||
use crate::common;
|
use crate::common;
|
||||||
use crate::bar_modules;
|
use crate::bar_modules;
|
||||||
|
|
||||||
|
/// Memory monitor that displays current system memory usage
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UnibarModuleMemory {
|
pub struct UnibarModuleMemory {
|
||||||
opts: common::AppOptions,
|
opts: common::AppOptions,
|
||||||
@@ -25,6 +58,16 @@ impl UnibarModuleMemory {
|
|||||||
|
|
||||||
impl bar_modules::BarModuleActions for UnibarModuleMemory {
|
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) {
|
fn generate_data(&mut self) {
|
||||||
let path = Path::new("/proc/meminfo");
|
let path = Path::new("/proc/meminfo");
|
||||||
@@ -42,7 +85,6 @@ impl bar_modules::BarModuleActions for UnibarModuleMemory {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
self.meminfo_str.pop();
|
self.meminfo_str.pop();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
|
@@ -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 std::process::{Stdio, Command};
|
||||||
|
|
||||||
use crate::common;
|
use crate::common;
|
||||||
use crate::bar_modules;
|
use crate::bar_modules;
|
||||||
|
|
||||||
|
/// Music playback monitor showing current track and progress
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UnibarModuleMusic {
|
pub struct UnibarModuleMusic {
|
||||||
opts: common::AppOptions,
|
opts: common::AppOptions,
|
||||||
current_stdout :String,
|
current_stdout: String,
|
||||||
progress_stdout :String,
|
progress_stdout: String,
|
||||||
state_stdout: String,
|
state_stdout: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnibarModuleMusic {
|
impl UnibarModuleMusic {
|
||||||
// --------------------
|
// --------------------
|
||||||
pub fn new(o :common::AppOptions) -> Self {
|
pub fn new(o :common::AppOptions) -> Self {
|
||||||
UnibarModuleMusic {
|
UnibarModuleMusic {
|
||||||
opts: o,
|
opts: o,
|
||||||
current_stdout: "".to_string(),
|
current_stdout: "".to_string(),
|
||||||
progress_stdout: "".to_string(),
|
progress_stdout: "".to_string(),
|
||||||
state_stdout: "stopped".to_string(),
|
state_stdout: "stopped".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl bar_modules::BarModuleActions for UnibarModuleMusic {
|
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) {
|
fn generate_data(&mut self) {
|
||||||
// MPD format options here:
|
// MPD format options here:
|
||||||
@@ -89,3 +135,7 @@ impl bar_modules::BarModuleDebug for UnibarModuleMusic {
|
|||||||
fn post_debug(&self) {
|
fn post_debug(&self) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../../unit_tests/bar_modules_music.test.rs"]
|
||||||
|
mod tests;
|
||||||
|
@@ -1,3 +1,36 @@
|
|||||||
|
// 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::json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::process::{Stdio, Command};
|
use std::process::{Stdio, Command};
|
||||||
@@ -5,6 +38,7 @@ use std::process::{Stdio, Command};
|
|||||||
use crate::common;
|
use crate::common;
|
||||||
use crate::bar_modules;
|
use crate::bar_modules;
|
||||||
|
|
||||||
|
/// Network interface monitor that displays connection status and IP addresses
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UnibarModuleNetwork {
|
pub struct UnibarModuleNetwork {
|
||||||
opts: common::AppOptions,
|
opts: common::AppOptions,
|
||||||
@@ -26,43 +60,59 @@ impl UnibarModuleNetwork {
|
|||||||
|
|
||||||
impl bar_modules::BarModuleActions for UnibarModuleNetwork {
|
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) {
|
fn generate_data(&mut self) {
|
||||||
// Output of 'ip -j address' command has network information
|
// Output of 'ip -j address' command has network information
|
||||||
let ip_addr_output = Command::new("ip")
|
match Command::new("ip")
|
||||||
.arg("-j") // Output in json format
|
.arg("-j") // Output in json format
|
||||||
.arg("address")
|
.arg("address")
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.output()
|
.output() {
|
||||||
.unwrap();
|
Err(e) => {
|
||||||
self.ip_addr_stdout = String::from_utf8(ip_addr_output.stdout).unwrap();
|
eprintln!("Error getting network information {e}");
|
||||||
self.ip_addr_stdout.pop();
|
self.network_info = json!(serde_json::Value::Null);
|
||||||
|
|
||||||
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
|
Ok(ip_addr_output) => {
|
||||||
let mut up_intf :Vec<_> = interfaces.iter()
|
self.ip_addr_stdout = String::from_utf8(ip_addr_output.stdout).unwrap();
|
||||||
.filter(|el| el["operstate"].as_str().unwrap().contains("UP"))
|
self.ip_addr_stdout.pop();
|
||||||
.cloned().collect();
|
|
||||||
|
|
||||||
// Sort by 'ifname'. This is an unreliable way to proiritize ethernet over wifi.
|
let network_data: Value = serde_json::from_str::<Value>(self.ip_addr_stdout.as_str()).unwrap();
|
||||||
// Ethenet network interface names normally start with 'e' and wifi interface names
|
|
||||||
// start with 'w'
|
if let Some(interfaces) = network_data.as_array() {
|
||||||
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 {
|
if self.opts.debug {
|
||||||
println!("-----> Inet Addr - {:#?}", inet_addr);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.network_info = inet_addr[0].clone();
|
|
||||||
self.network_info["ifname"] = up_intf[0]["ifname"].clone();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,42 @@
|
|||||||
|
// 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 std::process::{Stdio, Command};
|
||||||
//use std::collections::HashMap;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::common;
|
use crate::common;
|
||||||
use crate::bar_modules;
|
use crate::bar_modules;
|
||||||
|
|
||||||
|
/// Power monitor that displays battery status and level
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UnibarModulePower {
|
pub struct UnibarModulePower {
|
||||||
opts: common::AppOptions,
|
opts: common::AppOptions,
|
||||||
@@ -38,6 +70,16 @@ impl UnibarModulePower {
|
|||||||
|
|
||||||
impl bar_modules::BarModuleActions for UnibarModulePower {
|
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) {
|
fn generate_data(&mut self) {
|
||||||
// Following command is used to get power data:
|
// Following command is used to get power data:
|
||||||
|
106
src/bar_modules/bar_module_time.rs
Normal file
106
src/bar_modules/bar_module_time.rs
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
@@ -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 std::str;
|
||||||
use curl::easy::{Easy, List};
|
use curl::easy::{Easy, List};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use thiserror::Error;
|
||||||
|
use std::num::ParseFloatError;
|
||||||
|
|
||||||
use crate::common;
|
use crate::common;
|
||||||
|
use crate::common::UpdateResult;
|
||||||
use crate::bar_modules;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct UnibarModuleWeather {
|
pub struct UnibarModuleWeather {
|
||||||
opts: common::AppOptions,
|
opts: common::AppOptions,
|
||||||
weather_info: Value,
|
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 {
|
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 {
|
pub fn new(o :common::AppOptions) -> Self {
|
||||||
UnibarModuleWeather {
|
UnibarModuleWeather {
|
||||||
opts: o,
|
opts: o,
|
||||||
weather_info: json!(serde_json::Value::Null),
|
weather_info: json!(serde_json::Value::Null),
|
||||||
|
update_cnt: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
fn get_current_temperature(&self) -> f32 {
|
fn get_current_temperature(&self) -> Result<f64, WeatherError> {
|
||||||
let deg_c :f32 = self.weather_info["features"][0]["properties"]["temperature"]["value"]
|
let deg_c = match &self.weather_info {
|
||||||
.to_string().parse().unwrap();
|
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 {
|
Ok(match self.opts.weather_units {
|
||||||
common::TemperatureUnits::Metric => return deg_c,
|
common::TemperatureUnits::Metric => deg_c,
|
||||||
common::TemperatureUnits::Imperial => return (deg_c * 9.0 / 5.0) + 32.0,
|
common::TemperatureUnits::Imperial => Self::celsius_to_fahrenheit(deg_c),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
@@ -88,7 +164,8 @@ impl UnibarModuleWeather {
|
|||||||
let mut url_string = Vec::new();
|
let mut url_string = Vec::new();
|
||||||
let mut curl_ret = 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();
|
curl.url(url_string.concat().as_str()).unwrap();
|
||||||
{
|
{
|
||||||
@@ -150,42 +227,80 @@ impl UnibarModuleWeather {
|
|||||||
|
|
||||||
impl bar_modules::BarModuleActions for 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) {
|
fn generate_data(&mut self) {
|
||||||
// Print a web page onto stdout
|
// Print a web page onto stdout
|
||||||
let mut curl = Easy::new();
|
let mut curl = Easy::new();
|
||||||
let mut url_string = Vec::new();
|
let mut url_string = Vec::new();
|
||||||
let mut curl_ret = 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(self.opts.weather_station.to_owned());
|
||||||
url_string.push("/observations?limit=1".to_owned());
|
url_string.push("/observations?limit=1".to_owned());
|
||||||
|
|
||||||
curl.url(url_string.concat().as_str()).unwrap();
|
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();
|
let mut transfer = curl.transfer();
|
||||||
|
|
||||||
|
// Configure callback to write received response
|
||||||
transfer.write_function(|data| {
|
transfer.write_function(|data| {
|
||||||
curl_ret.extend_from_slice(data);
|
curl_ret.extend_from_slice(data);
|
||||||
Ok(data.len())
|
Ok(data.len())
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
transfer.perform().unwrap();
|
|
||||||
}
|
|
||||||
if self.opts.debug {
|
|
||||||
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 {
|
fn get_content(&self) -> String {
|
||||||
let temperature_value :f32 = self.get_current_temperature();
|
let temperature_value :f64 = self.get_current_temperature().expect("Temperature query");
|
||||||
let temperature_unit :String = self.get_temperature_unit();
|
let temperature_unit :String = self.get_temperature_unit();
|
||||||
// let temperature_icon :String = self.get_icon(v.clone());
|
// let temperature_icon :String = self.get_icon(v.clone());
|
||||||
|
|
||||||
return format!("{:.0}{}", temperature_value, temperature_unit);
|
return format!("{:.0}{}", temperature_value, temperature_unit);
|
||||||
@@ -193,12 +308,30 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather {
|
|||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
fn get_icon(&self) -> String {
|
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();
|
|
||||||
|
|
||||||
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,3 +343,7 @@ impl bar_modules::BarModuleDebug for UnibarModuleWeather {
|
|||||||
self.show_icons();
|
self.show_icons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../../unit_tests/bar_modules_weather.test.rs"]
|
||||||
|
mod tests;
|
||||||
|
@@ -1,25 +1,72 @@
|
|||||||
// --------------------
|
// MIT License
|
||||||
/// All Bar modules must implement the actions
|
// 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 {
|
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);
|
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;
|
fn get_content(&self) -> String;
|
||||||
|
|
||||||
/// Return a Unicode icon to display before content in the bar.
|
/// Returns a Unicode icon representing the module's current state
|
||||||
/// This icon may differ based on content of the data
|
|
||||||
fn get_icon(&self) -> String;
|
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 {
|
pub trait BarModuleDebug {
|
||||||
/// Print debug information at the end
|
/// Prints debug information about the module
|
||||||
fn post_debug(&self);
|
fn post_debug(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module declarations
|
||||||
pub mod bar_module_weather;
|
pub mod bar_module_weather;
|
||||||
pub mod bar_module_music;
|
pub mod bar_module_music;
|
||||||
pub mod bar_module_network;
|
pub mod bar_module_network;
|
||||||
pub mod bar_module_memory;
|
pub mod bar_module_memory;
|
||||||
pub mod bar_module_cpu;
|
pub mod bar_module_cpu;
|
||||||
pub mod bar_module_power;
|
pub mod bar_module_power;
|
||||||
|
pub mod bar_module_date;
|
||||||
|
pub mod bar_module_time;
|
||||||
|
@@ -12,20 +12,28 @@ pub enum TemperatureUnits {
|
|||||||
impl fmt::Display for TemperatureUnits {
|
impl fmt::Display for TemperatureUnits {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
TemperatureUnits::Metric => write!(f, "Metric"),
|
TemperatureUnits::Metric => write!(f, "Metric"),
|
||||||
TemperatureUnits::Imperial => write!(f, "Imperial"),
|
TemperatureUnits::Imperial => write!(f, "Imperial"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,PartialEq,Eq,Copy,Clone)]
|
||||||
|
pub enum UpdateResult {
|
||||||
|
Update,
|
||||||
|
Skip
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
// Application options
|
// Application options
|
||||||
// --------------------
|
// --------------------
|
||||||
#[derive(Debug,Clone)]
|
#[derive(Debug,Clone)]
|
||||||
pub struct AppOptions {
|
pub struct AppOptions {
|
||||||
pub weather_units: TemperatureUnits,
|
pub interval: u64,
|
||||||
pub weather_station: String,
|
pub weather_units: TemperatureUnits,
|
||||||
pub music_progress: bool,
|
pub weather_station: String,
|
||||||
pub debug: bool,
|
pub music_progress: bool,
|
||||||
pub debug_modules: bool,
|
pub debug: bool,
|
||||||
|
pub debug_modules: bool,
|
||||||
|
pub debug_update: bool,
|
||||||
}
|
}
|
||||||
|
198
src/main.rs
198
src/main.rs
@@ -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::str;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use crate::common::UpdateResult;
|
||||||
|
|
||||||
// Common utilities/types
|
/// Common utilities and types used across modules
|
||||||
mod common;
|
mod common;
|
||||||
// Bar Modules
|
/// Status bar component modules
|
||||||
mod bar_modules;
|
mod bar_modules;
|
||||||
|
/// Command-line arguments for configuring Unibar behavior
|
||||||
// Commandline parsing
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "unibar")]
|
#[command(name = "unibar")]
|
||||||
#[command(version = "1.0")]
|
#[command(version = "1.0")]
|
||||||
#[command(about = "Get string of info for a status bar")]
|
#[command(about = "A versatile status bar information provider")]
|
||||||
#[command(about, long_about = "A tool that returns variety of components in a string
|
#[command(long_about = "Unibar provides system information, weather, and other metrics \
|
||||||
suitable to use in a status bar")]
|
in a format suitable for status bars. It supports various modules including \
|
||||||
|
system monitoring, weather information, and music playback status.")]
|
||||||
struct CommandlineArgs {
|
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
|
/// Name of the weather station
|
||||||
#[arg(short = 's', long, default_value = "khio")]
|
#[arg(short = 's', long, default_value = "khio")]
|
||||||
weather_station: String,
|
weather_station: String,
|
||||||
@@ -34,70 +86,121 @@ struct CommandlineArgs {
|
|||||||
/// but before output is printed
|
/// but before output is printed
|
||||||
#[arg(short = 'M', long)]
|
#[arg(short = 'M', long)]
|
||||||
debug_modules: bool,
|
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)
|
// Application (Unibar)
|
||||||
//
|
//
|
||||||
#[derive(Clone)]
|
//#[derive(Clone)]
|
||||||
|
/// Main application structure that manages all status bar modules
|
||||||
|
/// and coordinates their updates.
|
||||||
struct Unibar {
|
struct Unibar {
|
||||||
|
/// Application-wide configuration options
|
||||||
opts: common::AppOptions,
|
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 {
|
impl Unibar {
|
||||||
// --------------------
|
/// Starts the main application loop, initializing all modules and
|
||||||
fn run(&self) {
|
/// 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 {
|
if self.opts.debug {
|
||||||
self.debug_msg("Debugging ...");
|
self.debug_msg("Debugging ...");
|
||||||
}
|
}
|
||||||
self.check_options();
|
self.check_options();
|
||||||
|
|
||||||
// Set up a list of all modules to be used
|
// Set up a list of all modules to be used
|
||||||
let bar_modules_enabled: Vec<Box<dyn bar_modules::BarModuleActions>> = vec! [
|
self.bar_modules_enabled.push(Box::new(bar_modules::bar_module_music::UnibarModuleMusic::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_memory::UnibarModuleMemory::new(self.opts.clone())));
|
||||||
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())));
|
||||||
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())));
|
||||||
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())));
|
||||||
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())));
|
||||||
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
|
// Get module's part to be displayed in the bar
|
||||||
let mut parts = Vec::new();
|
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();
|
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
|
// * Generate raw data with pertinent information
|
||||||
// * Return a unicode icon to be displayed
|
// * Return a unicode icon to be displayed
|
||||||
// * Return a String content to be displayed after the icon
|
// * Return a String content to be displayed after the icon
|
||||||
//
|
//
|
||||||
// Following generates ICON+CONTENT string for a module to be displayed
|
// Following generates ICON+CONTENT string for a module to be displayed
|
||||||
// in the bar
|
// 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_icon());
|
||||||
mod_parts.push(md.get_content());
|
mod_parts.push(md.get_content());
|
||||||
|
|
||||||
parts.push(mod_parts.join(" "));
|
parts.push(mod_parts.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show module debug information if enabled
|
self.show_module_debug();
|
||||||
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())),
|
|
||||||
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())),
|
|
||||||
];
|
|
||||||
for md in bar_modules_debugged {
|
|
||||||
md.post_debug();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print parts provided by each module
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
@@ -116,20 +219,33 @@ impl Unibar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
/// Main entry point for the Unibar application.
|
||||||
// Entry point
|
///
|
||||||
//
|
/// 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() {
|
fn main() {
|
||||||
let cmd_args = CommandlineArgs::parse();
|
let cmd_args = CommandlineArgs::parse();
|
||||||
let app = Unibar {
|
let mut app = Unibar {
|
||||||
opts: common::AppOptions {
|
opts: common::AppOptions {
|
||||||
|
interval: cmd_args.interval as u64,
|
||||||
weather_units: if cmd_args.weather_metric { common::TemperatureUnits::Metric }
|
weather_units: if cmd_args.weather_metric { common::TemperatureUnits::Metric }
|
||||||
else { common::TemperatureUnits::Imperial },
|
else { common::TemperatureUnits::Imperial },
|
||||||
weather_station: cmd_args.weather_station,
|
weather_station: cmd_args.weather_station,
|
||||||
music_progress: cmd_args.music_progress,
|
music_progress: cmd_args.music_progress,
|
||||||
debug: cmd_args.debug,
|
debug: cmd_args.debug,
|
||||||
debug_modules: cmd_args.debug_modules
|
debug_modules: cmd_args.debug_modules,
|
||||||
|
debug_update: cmd_args.debug_update,
|
||||||
},
|
},
|
||||||
|
bar_modules_enabled: Vec::new(),
|
||||||
|
debug_modules_done: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
app.run();
|
app.run();
|
||||||
|
212
unit_tests/bar_modules_music.test.rs
Normal file
212
unit_tests/bar_modules_music.test.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//! Unit tests for Music module for Unibar
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use std::process::Command;
|
||||||
|
use crate::bar_modules::BarModuleActions;
|
||||||
|
|
||||||
|
/// Helper function to create test options
|
||||||
|
fn create_test_options(with_progress: bool) -> common::AppOptions {
|
||||||
|
common::AppOptions {
|
||||||
|
interval: 1,
|
||||||
|
weather_units: common::TemperatureUnits::Metric,
|
||||||
|
weather_station: "test".to_string(),
|
||||||
|
music_progress: with_progress,
|
||||||
|
debug: false,
|
||||||
|
debug_modules: false,
|
||||||
|
debug_update: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that module handles missing mpc command gracefully.
|
||||||
|
/// Note: This needs ROOT permissions, hence excluding from normal
|
||||||
|
/// testing
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_missing_mpc_command() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Temporarily rename mpc command if it exists
|
||||||
|
let has_mpc = Command::new("which")
|
||||||
|
.arg("mpc")
|
||||||
|
.output()
|
||||||
|
.map(|output| output.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let original_mpc_path = "/usr/bin/mpc";
|
||||||
|
let backup_mpc_path = "/tmp/mpc.bak";
|
||||||
|
|
||||||
|
if has_mpc {
|
||||||
|
// Create a backup
|
||||||
|
std::fs::copy(original_mpc_path, backup_mpc_path).expect("Failed to backup mpc");
|
||||||
|
// Rename the original
|
||||||
|
std::fs::rename(original_mpc_path, "/tmp/mpc_renamed").expect("Failed to rename mpc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate data should not panic
|
||||||
|
module.generate_data();
|
||||||
|
|
||||||
|
// Should show appropriate error state
|
||||||
|
assert_eq!(module.get_icon(), "⏹");
|
||||||
|
assert!(module.get_content().contains("No player"));
|
||||||
|
|
||||||
|
// Restore mpc if it existed
|
||||||
|
if has_mpc {
|
||||||
|
// Remove the renamed file
|
||||||
|
std::fs::remove_file("/tmp/mpc_renamed").expect("Failed to remove renamed mpc");
|
||||||
|
// Restore from backup
|
||||||
|
std::fs::rename(backup_mpc_path, original_mpc_path).expect("Failed to restore mpc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test handling of mpc command failing
|
||||||
|
#[test]
|
||||||
|
fn test_mpc_command_failure() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Mock mpc command to always fail
|
||||||
|
module.current_stdout = "Error: Command not found".to_string();
|
||||||
|
module.progress_stdout = "".to_string();
|
||||||
|
module.state_stdout = "stopped".to_string();
|
||||||
|
|
||||||
|
// Generate data should not panic
|
||||||
|
// module.generate_data();
|
||||||
|
|
||||||
|
// Should show appropriate error state
|
||||||
|
assert_eq!(module.get_icon(), "⏹");
|
||||||
|
assert!(module.get_content().contains("Command not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test handling of various mpc output formats
|
||||||
|
#[test]
|
||||||
|
fn test_unexpected_mpc_output() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test cases for current_stdout
|
||||||
|
let test_cases = vec![
|
||||||
|
"", // Empty output
|
||||||
|
"\n", // Just newline
|
||||||
|
//"invalid utf8 \x80", // Invalid UTF-8
|
||||||
|
"very\nlong\noutput\n", // Multiple lines
|
||||||
|
" ", // Just whitespace
|
||||||
|
"🎵 unicode 🎶", // Unicode characters
|
||||||
|
];
|
||||||
|
|
||||||
|
for test_case in test_cases {
|
||||||
|
module.current_stdout = test_case.to_string();
|
||||||
|
// get_content should not panic
|
||||||
|
let content = module.get_content();
|
||||||
|
assert!(!content.is_empty(), "Content should not be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test state handling and icon selection
|
||||||
|
#[test]
|
||||||
|
fn test_player_states() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(false));
|
||||||
|
|
||||||
|
// Test valid states
|
||||||
|
let states = vec![
|
||||||
|
("playing", "𝄞"),
|
||||||
|
("paused", "⏸"),
|
||||||
|
("stopped", "⏹"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (state, expected_icon) in states {
|
||||||
|
module.state_stdout = state.to_string();
|
||||||
|
assert_eq!(module.get_icon(), expected_icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid states
|
||||||
|
let invalid_states = vec![
|
||||||
|
"",
|
||||||
|
"invalid",
|
||||||
|
"PLAYING",
|
||||||
|
"unknown",
|
||||||
|
"\n",
|
||||||
|
"playing\n",
|
||||||
|
];
|
||||||
|
|
||||||
|
for state in invalid_states {
|
||||||
|
module.state_stdout = state.to_string();
|
||||||
|
assert_eq!(module.get_icon(), "𝄞", "Invalid state should default to playing icon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test progress display formatting
|
||||||
|
#[test]
|
||||||
|
fn test_progress_display() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test various progress formats
|
||||||
|
let test_cases = vec![
|
||||||
|
("50% 3:30", "[50% 3:30]"),
|
||||||
|
("", "[]"),
|
||||||
|
("invalid", "[invalid]"),
|
||||||
|
("100% 0:00", "[100% 0:00]"),
|
||||||
|
(" 50% 3:30 ", "[50% 3:30]"), // Extra whitespace
|
||||||
|
];
|
||||||
|
|
||||||
|
for (progress, expected) in test_cases {
|
||||||
|
module.progress_stdout = progress.to_string();
|
||||||
|
module.current_stdout = "Test Song".to_string();
|
||||||
|
let content = module.get_content();
|
||||||
|
assert!(content.contains(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with progress disabled
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(false));
|
||||||
|
module.progress_stdout = "50% 3:30".to_string();
|
||||||
|
module.current_stdout = "Test Song".to_string();
|
||||||
|
assert_eq!(module.get_content(), "Test Song");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test module initialization
|
||||||
|
#[test]
|
||||||
|
fn test_module_initialization() {
|
||||||
|
let module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
assert_eq!(module.current_stdout, "");
|
||||||
|
assert_eq!(module.progress_stdout, "");
|
||||||
|
assert_eq!(module.state_stdout, "stopped");
|
||||||
|
assert_eq!(module.get_icon(), "⏹");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test clear functionality
|
||||||
|
#[test]
|
||||||
|
fn test_clear() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Set some data
|
||||||
|
module.current_stdout = "Test Song".to_string();
|
||||||
|
module.progress_stdout = "50% 3:30".to_string();
|
||||||
|
module.state_stdout = "playing".to_string();
|
||||||
|
|
||||||
|
// Clear the module
|
||||||
|
module.clear();
|
||||||
|
|
||||||
|
// Verify cleared state
|
||||||
|
assert_eq!(module.current_stdout, "");
|
||||||
|
assert_eq!(module.progress_stdout, "");
|
||||||
|
assert_eq!(module.state_stdout, "stopped");
|
||||||
|
}
|
418
unit_tests/bar_modules_weather.test.rs
Normal file
418
unit_tests/bar_modules_weather.test.rs
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//! Unit tests for Weather module for Unibar
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user