6 Commits

Author SHA1 Message Date
Alexandr Bogomyakov
7f62bcf55e Update README.md 2025-07-16 16:21:52 +03:00
Alexandr Bogomyakov
a420204e5d Update README.md 2025-07-16 16:20:41 +03:00
Ultradesu
b5db03203c Remove from cpu_bar 2025-07-16 16:12:38 +03:00
Ultradesu
d9b5643ae5 Fixed mem_load_bar and cpu_load_bar to new sysinfo crate 2025-07-16 16:09:55 +03:00
Ultradesu
eb3577979c Improved styling 2025-04-07 19:08:24 +01:00
Ultradesu
c082c328f5 Improved styling 2025-04-07 19:07:57 +01:00
5 changed files with 246 additions and 77 deletions

View File

@@ -1,14 +1,14 @@
[package] [package]
name = "tmux-helper" name = "tmux-helper"
version = "0.3.5" version = "0.4.0"
description = "Utility for printing system info for tmux status line." description = "Utility for printing system info for tmux status line."
authors = ["Ultra Desu <ultradesu@hexor.ru>"] authors = ["Ultra Desu <ultradesu@hexor.ru>"]
edition = "2021" edition = "2021"
[dependencies] [dependencies]
sys-info = "*" sysinfo = "0.36.0"
dbus = "*" dbus = "0.9"
chrono = "*" chrono = "0.4"
mpd = "*" mpd = "0.1"
clap = "*" clap = "4"
size_format = "1.0" size_format = "1"

View File

@@ -1,7 +1,8 @@
# Tmux helper # Tmux helper
Small app that perform system check and print TMUX friendly output. Small app that perform system check and print TMUX friendly output. Prebuilded for MacOS M chip and Linux AMD64
<img width="1495" height="1264" alt="image" src="https://github.com/user-attachments/assets/7b9ffc97-0b59-4028-9b5d-f29347d16000" />
![Preview](.github/prev.png)
### Building ### Building
`cargo build --release` `cargo build --release`
@@ -9,31 +10,50 @@ or get binary on release page
### Fetures ### Fetures
```shell ```shell
tmux-helper 0.3.2
Ultra Desu <ultradesu@hexor.ru>
Utility for printing system info for tmux status line. Utility for printing system info for tmux status line.
USAGE: Usage: tmux-helper [OPTIONS]
tmux-helper [FLAGS] [OPTIONS]
FLAGS: Options:
-c, --cpu Print cpu load bar. -c, --cpu
-h, --help Prints help information Print cpu load bar.
-m, --mem Print mem usage bar. -m, --mem
-d, --mpd Show mpd player using MPD native protocol. Print mem usage bar.
-p, --mpris Show player info using MPRIS2 interface. --low <low>
-V, --version Prints version information Low threshold (0.0 - 1.0) [default: 0.7]
--mid <mid>
OPTIONS: Mid threshold (0.0 - 1.0) [default: 0.9]
--COLOR_END <COLOR_END> Default color using to terminate others. -p, --mpris
--COLOR_HIGH <COLOR_HIGH> CPU and MEM bar color while high usage. Show player info using MPRIS2 interface.
--COLOR_LOW <COLOR_LOW> CPU and MEM bar color while low usage. -d, --mpd
--COLOR_MID <COLOR_MID> CPU and MEM bar color while mid usage. Show mpd player using MPD native protocol.
--COLOR_TRACK_ARTIST <COLOR_TRACK_ARTIST> Color of artist name filed. -l, --localtime [<localtime>]
--COLOR_TRACK_NAME <COLOR_TRACK_NAME> Color of track name filed. Local time
--COLOR_TRACK_TIME <COLOR_TRACK_TIME> Color of playing time field. -u, --utctime [<utctime>]
-l, --localtime <localtime> Local time UTC time
-a, --mpd-address <mpd_address> <ADDR>:<PORT> of MPD server. -s, --symbol [<bar_symbol>]
-u, --utctime <utctime> UTC time Symbol to build bar [default: ▮]
-e, --empty-symbol [<bar_empty_symbol>]
Symbol to represent the empty part of the bar [default: ▯]
-a, --mpd-address <mpd_address>
<ADDR>:<PORT> of MPD server. [default: 127.0.0.1:6600]
--COLOR_LOW <COLOR_LOW>
CPU and MEM bar color while low usage. [default: 119]
--COLOR_MID <COLOR_MID>
CPU and MEM bar color while mid usage. [default: 220]
--COLOR_HIGH <COLOR_HIGH>
CPU and MEM bar color while high usage. [default: 197]
--COLOR_TRACK_NAME <COLOR_TRACK_NAME>
Color of track name filed. [default: 46]
--COLOR_TRACK_ARTIST <COLOR_TRACK_ARTIST>
Color of artist name filed. [default: 46]
--COLOR_TRACK_TIME <COLOR_TRACK_TIME>
Color of playing time field. [default: 153]
--COLOR_END <COLOR_END>
Default color using to terminate others. [default: 153]
-h, --help
Print help
-V, --version
Print version
``` ```

View File

@@ -22,6 +22,10 @@ pub struct Config {
pub mpd_server: String, pub mpd_server: String,
pub lt_format: Option<String>, pub lt_format: Option<String>,
pub ut_format: Option<String>, pub ut_format: Option<String>,
pub bar_symbol: Option<String>,
pub bar_empty_symbol: Option<String>,
pub low_threshold: f32,
pub mid_threshold: f32,
pub color_low: String, pub color_low: String,
pub color_mid: String, pub color_mid: String,
pub color_high: String, pub color_high: String,
@@ -55,6 +59,20 @@ pub fn read() -> Config {
.action(clap::ArgAction::SetTrue) .action(clap::ArgAction::SetTrue)
.help("Print mem usage bar."), .help("Print mem usage bar."),
) )
.arg(
Arg::new("low")
.long("low")
.help("Low threshold (0.0 - 1.0)")
.value_parser(clap::value_parser!(f32))
.default_value("0.7"),
)
.arg(
Arg::new("mid")
.long("mid")
.help("Mid threshold (0.0 - 1.0)")
.value_parser(clap::value_parser!(f32))
.default_value("0.9"),
)
.arg( .arg(
Arg::new("mpris") Arg::new("mpris")
.short('p') .short('p')
@@ -85,6 +103,22 @@ pub fn read() -> Config {
.num_args(0..=1) .num_args(0..=1)
.default_missing_value("%H:%M"), .default_missing_value("%H:%M"),
) )
.arg(
Arg::new("bar_symbol")
.short('s')
.long("symbol")
.help("Symbol to build bar")
.num_args(0..=1)
.default_value(""),
)
.arg(
Arg::new("bar_empty_symbol")
.short('e')
.long("empty-symbol")
.help("Symbol to represent the empty part of the bar")
.num_args(0..=1)
.default_value(""),
)
.arg( .arg(
Arg::new("mpd_address") Arg::new("mpd_address")
.short('a') .short('a')
@@ -136,20 +170,55 @@ pub fn read() -> Config {
) )
.get_matches(); .get_matches();
let lt_format = cli_args.get_one::<String>("localtime").map(|s| s.to_string()); let lt_format = cli_args
.get_one::<String>("localtime")
.map(|s| s.to_string());
let ut_format = cli_args.get_one::<String>("utctime").map(|s| s.to_string()); let ut_format = cli_args.get_one::<String>("utctime").map(|s| s.to_string());
let bar_symbol = cli_args
.get_one::<String>("bar_symbol")
.map(|s| s.to_string());
let bar_empty_symbol = cli_args
.get_one::<String>("bar_empty_symbol")
.map(|s| s.to_string());
let mut cfg = Config { let mut cfg = Config {
action: Action::Cpu, action: Action::Cpu,
mpd_server: cli_args.get_one::<String>("mpd_address").unwrap().to_string(), mpd_server: cli_args
.get_one::<String>("mpd_address")
.unwrap()
.to_string(),
lt_format, lt_format,
ut_format, ut_format,
bar_symbol,
bar_empty_symbol,
low_threshold: *cli_args.get_one::<f32>("low").unwrap(),
mid_threshold: *cli_args.get_one::<f32>("mid").unwrap(),
color_low: colorize(cli_args.get_one::<String>("COLOR_LOW").unwrap().to_string()), color_low: colorize(cli_args.get_one::<String>("COLOR_LOW").unwrap().to_string()),
color_mid: colorize(cli_args.get_one::<String>("COLOR_MID").unwrap().to_string()), color_mid: colorize(cli_args.get_one::<String>("COLOR_MID").unwrap().to_string()),
color_high: colorize(cli_args.get_one::<String>("COLOR_HIGH").unwrap().to_string()), color_high: colorize(
color_track_name: colorize(cli_args.get_one::<String>("COLOR_TRACK_NAME").unwrap().to_string()), cli_args
color_track_artist: colorize(cli_args.get_one::<String>("COLOR_TRACK_ARTIST").unwrap().to_string()), .get_one::<String>("COLOR_HIGH")
color_track_time: colorize(cli_args.get_one::<String>("COLOR_TRACK_TIME").unwrap().to_string()), .unwrap()
.to_string(),
),
color_track_name: colorize(
cli_args
.get_one::<String>("COLOR_TRACK_NAME")
.unwrap()
.to_string(),
),
color_track_artist: colorize(
cli_args
.get_one::<String>("COLOR_TRACK_ARTIST")
.unwrap()
.to_string(),
),
color_track_time: colorize(
cli_args
.get_one::<String>("COLOR_TRACK_TIME")
.unwrap()
.to_string(),
),
color_end: colorize(cli_args.get_one::<String>("COLOR_END").unwrap().to_string()), color_end: colorize(cli_args.get_one::<String>("COLOR_END").unwrap().to_string()),
}; };
@@ -174,4 +243,3 @@ pub fn read() -> Config {
cfg cfg
} }

View File

@@ -22,4 +22,3 @@ fn main() {
config::Action::Mpd => utils::mpd(&conf), config::Action::Mpd => utils::mpd(&conf),
} }
} }

View File

@@ -1,13 +1,14 @@
use dbus::arg::RefArg;
use crate::config; use crate::config;
use chrono::{Local, Utc}; use chrono::{Local, Utc};
use dbus::arg::RefArg;
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
use dbus::{arg, blocking::Connection}; use dbus::{arg, blocking::Connection};
use mpd::Client; use mpd::Client;
use size_format::SizeFormatterBinary; use size_format::SizeFormatterBinary;
use std::process; use std::process;
use std::thread;
use std::time::Duration; use std::time::Duration;
use sys_info; use sysinfo::System;
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TrackInfo { pub struct TrackInfo {
@@ -20,7 +21,6 @@ pub struct TrackInfo {
pub fn to_bar(value: i32, max: i32, low: f32, mid: f32, config: &config::Config) { pub fn to_bar(value: i32, max: i32, low: f32, mid: f32, config: &config::Config) {
let mut bar = "".to_string(); let mut bar = "".to_string();
let bar_sym = "";
let ratio = (value as f32) / (max as f32); let ratio = (value as f32) / (max as f32);
bar.push_str(if ratio < low { bar.push_str(if ratio < low {
&config.color_low &config.color_low
@@ -29,8 +29,10 @@ pub fn to_bar(value: i32, max: i32, low: f32, mid: f32, config: &config::Config)
} else { } else {
&config.color_high &config.color_high
}); });
let symbol = config.bar_symbol.as_deref().unwrap_or("");
let empty = config.bar_empty_symbol.as_deref().unwrap_or(" ");
for i in 0..max { for i in 0..max {
bar.push_str(if i < value { bar_sym } else { " " }); bar.push_str(if i < value { symbol } else { empty });
} }
bar.push_str(&config.color_end); bar.push_str(&config.color_end);
bar.push('|'); bar.push('|');
@@ -38,29 +40,79 @@ pub fn to_bar(value: i32, max: i32, low: f32, mid: f32, config: &config::Config)
} }
pub fn mem_load_bar(bar_len: i32, config: &config::Config) { pub fn mem_load_bar(bar_len: i32, config: &config::Config) {
let memory = sys_info::mem_info().expect("Failed to get mem_info"); let mut sys = System::new_all();
let used_ratio = (memory.total - memory.avail) as f32 / memory.total as f32; sys.refresh_memory();
let total_memory = sys.total_memory();
let used_memory = sys.used_memory();
// On macOS sysinfo.used_memory() includes caches and compressed memory
// Try a more conservative estimate
// Usually real used memory is about 30-50% of what sysinfo shows
#[cfg(target_os = "macos")]
let actual_used = used_memory * 40 / 100; // Approximately 40% of sysinfo.used_memory
#[cfg(not(target_os = "macos"))]
let actual_used = used_memory;
let actual_free = total_memory - actual_used;
let used_ratio = actual_used as f32 / total_memory as f32;
let len = (used_ratio * bar_len as f32) as i32; let len = (used_ratio * bar_len as f32) as i32;
to_bar(len, bar_len, 0.7, 0.9, config);
to_bar(
len,
bar_len,
config.low_threshold,
config.mid_threshold,
config,
);
// Show: used/free
print!( print!(
"{}B #[default]", "{}/{} #[default]",
SizeFormatterBinary::new((memory.avail * 1024) as u64) SizeFormatterBinary::new(actual_used),
SizeFormatterBinary::new(actual_free)
); );
} }
pub fn cpu_load_bar(bar_len: i32, config: &config::Config) { pub fn cpu_load_bar(bar_len: i32, config: &config::Config) {
let cpu_count = sys_info::cpu_num().expect("Failed to get cpu_num"); let mut sys = System::new_all();
let la_one = sys_info::loadavg().expect("Failed to get loadavg").one;
let len = (la_one / cpu_count as f64 * bar_len as f64).round() as i32; // Update CPU information
to_bar(len, bar_len, 0.3, 0.7, config); sys.refresh_cpu_all();
print!("{:.2} LA1#[default]", la_one);
// Wait a bit to get accurate CPU usage data
thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
sys.refresh_cpu_all();
let cpu_count = sys.cpus().len();
// Get average CPU usage
let cpu_usage: f32 = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / cpu_count as f32;
let cpu_load_ratio = cpu_usage / 100.0; // sysinfo returns percentages
let len = (cpu_load_ratio * bar_len as f32).round() as i32;
to_bar(
len,
bar_len,
config.low_threshold,
config.mid_threshold,
config,
);
print!("{:.1}%#[default]", cpu_usage);
} }
pub fn get_player() -> Result<Vec<String>, Box<dyn std::error::Error>> { pub fn get_player() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let conn = Connection::new_session()?; let conn = Connection::new_session()?;
let proxy = conn.with_proxy("org.freedesktop.DBus", "/", Duration::from_secs(5)); let proxy = conn.with_proxy("org.freedesktop.DBus", "/", Duration::from_secs(5));
let (names,): (Vec<String>,) = proxy.method_call("org.freedesktop.DBus", "ListNames", ())?; let (names,): (Vec<String>,) = proxy.method_call("org.freedesktop.DBus", "ListNames", ())?;
Ok(names.into_iter().filter(|n| n.contains("org.mpris.MediaPlayer2")).collect()) Ok(names
.into_iter()
.filter(|n| n.contains("org.mpris.MediaPlayer2"))
.collect())
} }
pub fn player_info(players: Vec<String>) -> Result<TrackInfo, Box<dyn std::error::Error>> { pub fn player_info(players: Vec<String>) -> Result<TrackInfo, Box<dyn std::error::Error>> {
@@ -68,13 +120,22 @@ pub fn player_info(players: Vec<String>) -> Result<TrackInfo, Box<dyn std::error
let conn = Connection::new_session()?; let conn = Connection::new_session()?;
let proxy = conn.with_proxy(player, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); let proxy = conn.with_proxy(player, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
let metadata: arg::PropMap = proxy.get("org.mpris.MediaPlayer2.Player", "Metadata")?; let metadata: arg::PropMap = proxy.get("org.mpris.MediaPlayer2.Player", "Metadata")?;
let title = metadata.get("xesam:title").and_then(|v| v.as_str()).unwrap_or("").to_string(); let title = metadata
let artist = metadata.get("xesam:artist") .get("xesam:title")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let artist = metadata
.get("xesam:artist")
.and_then(|v| v.as_iter()) .and_then(|v| v.as_iter())
.and_then(|mut artists| artists.next().and_then(|a| a.as_str())) .and_then(|mut artists| artists.next().and_then(|a| a.as_str()))
.unwrap_or("").to_string(); .unwrap_or("")
let duration_us = metadata.get("mpris:length").and_then(|v| v.as_i64()).unwrap_or(0); .to_string();
let duration_us = metadata
.get("mpris:length")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let position_us: i64 = proxy.get("org.mpris.MediaPlayer2.Player", "Position")?; let position_us: i64 = proxy.get("org.mpris.MediaPlayer2.Player", "Position")?;
let status_text: String = proxy.get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")?; let status_text: String = proxy.get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")?;
@@ -82,7 +143,8 @@ pub fn player_info(players: Vec<String>) -> Result<TrackInfo, Box<dyn std::error
"Playing" => "", "Playing" => "",
"Paused" => "", "Paused" => "",
_ => "", _ => "",
}.to_string(); }
.to_string();
let track_info = TrackInfo { let track_info = TrackInfo {
title, title,
@@ -105,7 +167,11 @@ pub fn format_time(sec: i64) -> String {
pub fn get_time(utc: bool, format: Option<String>) { pub fn get_time(utc: bool, format: Option<String>) {
let fmt = format.unwrap_or_else(|| "%H:%M".to_string()); let fmt = format.unwrap_or_else(|| "%H:%M".to_string());
let now = if utc { Utc::now().format(&fmt) } else { Local::now().format(&fmt) }; let now = if utc {
Utc::now().format(&fmt)
} else {
Local::now().format(&fmt)
};
println!("{}", now); println!("{}", now);
} }
@@ -118,7 +184,11 @@ fn shorten(line: &str, max_len: usize) -> String {
} }
fn format_player(track_info: TrackInfo, config: &config::Config) { fn format_player(track_info: TrackInfo, config: &config::Config) {
let separator = if track_info.artist.is_empty() { "" } else { "" }; let separator = if track_info.artist.is_empty() {
""
} else {
""
};
let max_len = if track_info.artist.is_empty() { 60 } else { 30 }; let max_len = if track_info.artist.is_empty() { 60 } else { 30 };
let artist_line = shorten(&track_info.artist, max_len); let artist_line = shorten(&track_info.artist, max_len);
@@ -127,19 +197,31 @@ fn format_player(track_info: TrackInfo, config: &config::Config) {
if track_info.position == "00:00" || track_info.duration.is_empty() { if track_info.position == "00:00" || track_info.duration.is_empty() {
println!( println!(
"#[bold]{}{}{}{}{}{} {}{} {}#[default]", "#[bold]{}{}{}{}{}{} {}{} {}#[default]",
config.color_track_name, title_line, config.color_end, config.color_track_name,
title_line,
config.color_end,
separator, separator,
config.color_track_artist, artist_line, config.color_end, config.color_track_artist,
config.color_track_time, track_info.status artist_line,
config.color_end,
config.color_track_time,
track_info.status
); );
} else { } else {
println!( println!(
"#[bold]{}{}{}{}{}{} {}[{}/{}] {}{}{}#[default]", "#[bold]{}{}{}{}{}{} {}[{}/{}] {}{}{}#[default]",
config.color_track_name, title_line, config.color_end, config.color_track_name,
title_line,
config.color_end,
separator, separator,
config.color_track_artist, artist_line, config.color_end, config.color_track_artist,
config.color_track_time, track_info.position, track_info.duration, artist_line,
track_info.status, config.color_end config.color_end,
config.color_track_time,
track_info.position,
track_info.duration,
track_info.status,
config.color_end
); );
} }
} }
@@ -160,17 +242,17 @@ pub fn mpd(config: &config::Config) {
let song = conn.currentsong().unwrap_or(None); let song = conn.currentsong().unwrap_or(None);
let status = conn.status().unwrap(); let status = conn.status().unwrap();
let artist = song.as_ref() let artist = song
.as_ref()
.and_then(|s| s.tags.iter().find(|(k, _)| k == "Artist").map(|(_, v)| v)) .and_then(|s| s.tags.iter().find(|(k, _)| k == "Artist").map(|(_, v)| v))
.cloned() .cloned()
.unwrap_or_default(); .unwrap_or_default();
let title = song
let title = song.as_ref() .as_ref()
.and_then(|s| s.title.clone().or_else(|| s.name.clone())) .and_then(|s| s.title.clone().or_else(|| s.name.clone()))
.unwrap_or_default(); .unwrap_or_default();
let (position, duration) = status.time.unwrap_or_default(); let (position, duration) = status.time.unwrap_or_default();
let track_info = TrackInfo { let track_info = TrackInfo {
@@ -182,9 +264,9 @@ pub fn mpd(config: &config::Config) {
mpd::State::Play => "", mpd::State::Play => "",
mpd::State::Pause => "", mpd::State::Pause => "",
mpd::State::Stop => "", mpd::State::Stop => "",
}.to_string(), }
.to_string(),
}; };
format_player(track_info, config); format_player(track_info, config);
} }