1use std::env;
2use std::time::Duration;
3use std::collections::HashMap;
4use std::process::Command;
5use tokio::time::sleep;
6use systemstat::{System, Platform, saturating_sub_bytes, DelayedMeasurement, CPULoad};
7use std::io::Write;
8use std::fs::File;
9use regex::Regex;
10
11type Logo = fn(&Value) -> String;
12
13pub enum Value {
14 S(String),
15 I(u8),
16}
17
18pub enum SourceData {
19 U(usize),
20 CPU(DelayedMeasurement<CPULoad>),
21 Nothing
22}
23
24pub trait Source {
25 fn unit(&self) -> Option<String>;
26 fn get(&mut self) -> Value;
27}
28
29pub struct Sources {
30}
31
32pub struct BatterySource {
55}
56
57impl Source for BatterySource {
58 fn unit(&self) -> Option<String> {
59 Some("%".to_string())
60 }
61
62 fn get(&mut self) -> Value {
63 let s = System::new();
64 Value::I(s.battery_life().map_or(0.0, |x| (
65 if x.remaining_capacity > 1.0 { 100.0 } else { x.remaining_capacity * 100.0})) as u8)
66 }
67}
68
69pub struct CpuSource {
70 delayed_measurement_opt: Option<DelayedMeasurement<CPULoad>>,
71}
72
73impl Source for CpuSource {
74
75 fn unit(&self) -> Option<String> {
76 Some("%".to_string())
77 }
78
79 fn get(&mut self) -> Value {
80 let res = match &self.delayed_measurement_opt {
81 Some(cpu) => {
82 let cpu = cpu.done().unwrap();
83 let cpu = cpu.system + cpu.user;
84 Value::I((cpu * 100.0) as u8)
85 },
86 _ => Value::I(0),
87 };
88 self.delayed_measurement_opt = match System::new().cpu_load_aggregate() {
89 Ok(r) => Some(r),
90 Err(_) => None
91 };
92 res
93 }
94}
95
96pub struct CpuTempSource {
97}
98
99impl Source for CpuTempSource {
100 fn unit(&self) -> Option<String> {
101 Some("°C".to_string())
102 }
103
104 fn get(&mut self) -> Value {
105 Value::I(System::new().cpu_temp().unwrap_or(0.0) as u8)
106 }
107}
108
109pub struct MemorySource {
110}
111
112impl Source for MemorySource {
113 fn unit(&self) -> Option<String> {
114 Some("%".to_string())
115 }
116
117 fn get(&mut self) -> Value {
118 Value::I(System::new().memory().map_or(0.0, |mem| (saturating_sub_bytes(mem.total, mem.free).as_u64() * 100 / mem.total.as_u64()) as f32) as u8)
119 }
120}
121
122pub struct DateSource {
123}
124
125impl Source for DateSource {
126 fn unit(&self) -> Option<String> {
127 Some("".to_string())
128 }
129
130 fn get(&mut self) -> Value {
131 Value::S({ let mut s = Command::new("sh")
132 .arg("-c")
133 .arg("date | sed -E 's/:[0-9]{2} .*//'").output().map_or("".to_string(), |o| String::from_utf8(o.stdout).unwrap_or("".to_string())); s.pop(); s})
134 }
135}
136
137pub struct WindowSource {
138 max_chars: usize
139}
140
141impl Source for WindowSource {
142 fn unit(&self) -> Option<String> {
143 Some("".to_string())
144 }
145
146 fn get(&mut self) -> Value {
147 let s = Command::new("sh")
148 .arg("-c")
149 .arg("xdotool getwindowfocus getwindowpid getwindowname 2>/dev/null").output().map_or("".to_string(),
150 |o| String::from_utf8(o.stdout).unwrap_or("".to_string()));
151 let lines : Vec<&str> = s.split("\n").collect();
152 if lines.len() >= 2 {
153 let mut comm = std::fs::read_to_string(format!("/proc/{}/comm", lines[0])).unwrap_or("".to_string());
154 comm.pop();
155 let s = format!("{} - {}", comm, lines[1]);
156 Value::S(match s.char_indices().nth(self.max_chars) {
157 None => s,
158 Some((idx, _)) => (&s[..idx]).to_string(),
159 })
160 }
161 else {
162 Value::S("".to_string())
163 }
164 }
165}
166
167
168impl Sources {
169
170 pub fn battery() -> Box<BatterySource> {
171 Box::new(BatterySource { })
172 }
173
174 pub fn cpu() -> Box<CpuSource> {
175 Box::new(CpuSource{ delayed_measurement_opt: None })
176 }
177 pub fn cpu_temp() -> Box<CpuTempSource> {
178 Box::new(CpuTempSource { })
179 }
180
181 pub fn memory() -> Box<MemorySource> {
182 Box::new(MemorySource { })
183 }
184
185 pub fn date() -> Box<DateSource> {
186 Box::new(DateSource { })
187 }
188
189 pub fn window(max_chars: usize) -> Box<WindowSource> {
190 Box::new(WindowSource { max_chars: max_chars })
191 }
192}
193
194pub struct Logos {
195}
196
197macro_rules! i_logo {
198 ($x:expr) => {
199 |v| match v {
200 Value::S(_) => "",
201 Value::I(i) => $x(i)
202 }.to_string()
203 }
204}
205
206impl Logos {
207
208 pub fn battery() -> Logo {
209 i_logo!(|i| ["", "", "", "", "", "", "", "", "", "", ""].get((i/10) as usize).unwrap_or(&""))
210 }
211
212 pub fn cpu() -> Logo {
213 |_| " ".to_string()
214 }
215
216 pub fn cpu_temp() -> Logo {
217 |_| " ".to_string()
218 }
219
220 pub fn memory() -> Logo {
221 |_| " ".to_string()
222 }
223
224 pub fn date() -> Logo {
225 |_| " ".to_string()
226 }
227
228 fn website(s: &str) -> String {
229 let browsers = "^(chrom|firefox|qutebrowser)";
230 format!("{}.* {}.*", browsers, s)
231 }
232
233 fn terminals() -> String {
234 "^(alacritty|termite|xterm)".to_string()
235 }
236
237 fn terminal(s: &str) -> String {
238 format!("{}.* {}.*", Logos::terminals(), s)
239 }
240
241 pub fn window() -> Logo {
242 |value| {
243 match value {
244 Value::S(value) => {
245 let mut matches : HashMap<String, Regex> = HashMap::new();
246 macro_rules! nm {
247 ($x:expr, $y:expr) => {
248 matches.insert($x.to_string(), Regex::new($y).unwrap());
249 }
250 }
251 nm!(" ", &Logos::website("Stack Overflow"));
252 nm!(" ", &Logos::website("Facebook"));
253 nm!("暑", &Logos::website("Twitter"));
254 nm!(" ", &Logos::website("YouTube"));
255 nm!(" ", &Logos::website("reddit"));
256 nm!(" ", &Logos::website("Wikipedia"));
257 nm!(" ", &Logos::website("GitHub"));
258 nm!(" ", &Logos::website("WhatsApp"));
259 nm!(" ", "signal-desktop .*");
260 nm!(" ", &Logos::terminal("n?vim"));
261 nm!(" ", "^(mpv|mplayer).*");
262 nm!(" ", &(Logos::terminals() + ".*"));
263 nm!(" ", "^firefox.*");
264 nm!(" ", "^chrom.*");
265 nm!(" ", "^gimp.*");
266 matches.insert(" ".to_string(), Regex::new("^firefox.*").unwrap());
267 for (logo, reg) in &matches {
268 if reg.is_match(value) {
269 return logo.to_string();
270 }
271 }
272 },
273 Value::I(_) => {},
274 }
275 " ".to_string()
276 }
277 }
278
279}
280
281type Color = u32;
282
283#[derive(Clone)]
284pub enum ColoredStringItem {
285 S(String),
286 BgColor(Color),
287 FgColor(Color),
288 StopFg,
289 StopBg,
290}
291
292#[derive(Clone)]
293pub struct ColoredString {
294 string: Vec<ColoredStringItem>,
295}
296
297impl ColoredString {
298
299 pub fn new() -> ColoredString {
300 ColoredString {
301 string: vec![],
302 }
303 }
304
305 pub fn bg(&mut self, color: Color) -> &mut ColoredString {
306 self.string.push(ColoredStringItem::BgColor(color));
307 self
308 }
309
310 pub fn fg(&mut self, color: Color) -> &mut ColoredString {
311 self.string.push(ColoredStringItem::FgColor(color));
312 self
313 }
314
315 pub fn fg_bg(&mut self, colors: &(Color, Color)) -> &mut ColoredString {
316 self.fg(colors.0).bg(colors.1)
317 }
318
319
320 pub fn s(&mut self, s: &str) -> &mut ColoredString {
321 self.string.push(ColoredStringItem::S(s.to_string()));
322 self
323 }
324
325 pub fn ebg(&mut self) -> &mut ColoredString {
326 self.string.push(ColoredStringItem::StopBg);
327 self
328 }
329
330 pub fn efg(&mut self) -> &mut ColoredString {
331 self.string.push(ColoredStringItem::StopFg);
332 self
333 }
334
335 fn htc(color: Color) -> String {
336 let b = color & 0xff;
337 let g = (color >> 8) & 0xff;
338 let r = (color >> 16) & 0xff;
339 format!("{};{};{}", r, g, b)
340 }
341
342 pub fn to_string(&self) -> String {
343 self.string.clone().into_iter().map ( |item|
344 match item {
345 ColoredStringItem::S(s) => s,
346 ColoredStringItem::BgColor(c) => format!("\x1b[48;2;{}m", ColoredString::htc(c)),
347 ColoredStringItem::FgColor(c) => format!("\x1b[38;2;{}m", ColoredString::htc(c)),
348 ColoredStringItem::StopBg => "\x1b[49m".to_string(),
349 ColoredStringItem::StopFg => "\x1b[39m".to_string(),
350 }
351 ).collect::<Vec<String>>().join("")
352 }
353
354 pub fn len(&self) -> usize {
355 self.string.clone().into_iter().map ( |item|
356 match item {
357 ColoredStringItem::S(s) => s.chars().count(),
358 _ => 0,
359 }
360 ).fold(0, |a, b| a + b)
361 }
362
363}
364
365pub type FgColorAndBgColor = (Color, Color);
366
367pub struct Palette {
368 _source: Option<String>,
369 colors: Vec<FgColorAndBgColor>,
370}
371
372impl Palette {
373
374 pub fn black() -> Palette {
375 Palette {
376 _source: None,
377 colors: vec![(0xfffff, 0)]
378 }
379 }
380
381 pub fn grey_blue_cold_winter() -> Palette {
382 Palette {
383 _source: Some("https://colorhunt.co/palette/252807".to_string()),
384 colors: vec![
385 (0,0xf6f5f5),
386 (0,0xd3e0ea),
387 (0xf6f5f5,0x1687a7),
388 (0xd3e0ea,0x276678),
389 ]
390 }
391 }
392
393 pub fn black_grey_turquoise_dark() -> Palette {
394 Palette {
395 _source: Some("https://colorhunt.co/palette/2763".to_string()),
396 colors: vec![
397 (0xeeeeee,0x222831),
398 (0xeeeeee,0x393e46),
399 (0,0x00adb5),
400 (0,0xeeeeee),
401 ]
402 }
403 }
404
405 pub fn red_pink_turquoise_spring() -> Palette {
406 Palette {
407 _source: Some("https://colorhunt.co/palette/2257091".to_string()),
408 colors: vec![
409 (0,0xef4f4f),
410 (0,0xee9595),
411 (0,0xffcda3),
412 (0,0x74c7b8),
413 ]
414 }
415 }
416
417 pub fn get(&self, i: usize) -> &FgColorAndBgColor {
418 self.colors.get(i % self.colors.len()).unwrap_or(&(0,0xff))
419 }
420
421}
422
423pub struct ThemedWidgets {
424}
425
426impl ThemedWidgets {
427
428 pub fn simple(widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
429 (widget_position,
430 sources_logos.into_iter().enumerate().map( |(i, source_logo)| {
431 let fg_bg = palette.get(i);
432 Widget {
433 source: source_logo.0,
434 prefix: ColoredString::new().fg_bg(fg_bg).s(" ").clone(),
435 suffix: ColoredString::new().s(" ").ebg().efg().s(" ").clone(),
436 logo: source_logo.1,
437 }}).collect())
438 }
439
440 pub fn detached(left_separator: &str, right_separator: &str, widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
441 (widget_position,
442 sources_logos.into_iter().enumerate().map( |(i, source_logo)| {
443 let fg_bg = palette.get(i);
444 Widget {
445 source: source_logo.0,
446 prefix: ColoredString::new().fg(fg_bg.1).s(left_separator).fg_bg(fg_bg).s(" ").clone(),
447 suffix: ColoredString::new().s(" ").ebg().fg(fg_bg.1).s(right_separator).efg().s(" ").clone(),
448 logo: source_logo.1,
449 }}).collect())
450 }
451
452 pub fn slash(widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
453 ThemedWidgets::detached(" ", "", widget_position, sources_logos, palette)
454 }
455
456 pub fn tab(widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
457 ThemedWidgets::detached(" ", " ", widget_position, sources_logos, palette)
458 }
459
460 pub fn attached(left_separator: &str, right_separator: &str, widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
461 let sources_logos_len = sources_logos.len();
462 let left = widget_position == WidgetPosition::Left;
463 (widget_position,
464 sources_logos.into_iter().enumerate().map( |(i, source_logo)| {
465 let fg_bg = palette.get(i);
466 let n_fg_bg = palette.get(i + 1);
467 let prefix = if left {
468 ColoredString::new().fg_bg(fg_bg).s(" ").clone()
469 } else {
470 let mut s = ColoredString::new();
471 if i + 1 < sources_logos_len {
472 s.bg(n_fg_bg.1);
473 }
474 s.fg(fg_bg.1).s(right_separator).fg_bg(fg_bg).s(" ").clone()
475 };
476 let suffix = if left {
477 let mut s = ColoredString::new();
478 s.s(" ").ebg().fg(fg_bg.1);
479 if i + 1 < sources_logos_len { s.bg(n_fg_bg.1); };
480 s.s(left_separator).ebg().efg().clone()
481 } else {
482 ColoredString::new().s(" ").ebg().efg().clone()
483 };
484 Widget {
485 source: source_logo.0,
486 prefix: prefix,
487 suffix: suffix,
488 logo: source_logo.1,
489 }}).collect())
490 }
491
492 pub fn powerline(widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
493 ThemedWidgets::attached("", "", widget_position, sources_logos, palette)
494
495 }
496
497 pub fn flames(widget_position: WidgetPosition, sources_logos: Vec<(Box<dyn Source>, Logo)>, palette: &Palette) -> (WidgetPosition, Vec<Widget>) {
498 ThemedWidgets::attached(" ", " ", widget_position, sources_logos, palette)
499 }
500}
501
502pub struct Widget {
503 pub source: Box<dyn Source>,
504 pub prefix: ColoredString,
505 pub suffix: ColoredString,
506 pub logo: Logo,
507}
508
509struct Ansi {
510}
511
512impl Ansi {
513
514 fn hide_cursor() {
515 print!("\x1b[?25l");
516 }
517
518 fn move_back(n: usize) {
519 print!("\x1b[{}D", n);
520 }
521
522 fn move_to(line: usize, col: usize) {
523 print!("\x1b[{};{}H", line, col);
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, std::cmp::Eq, Hash)]
528pub enum WidgetPosition {
529 Left,
530 Right
531}
532
533impl Widget {
534 pub async fn draw(&mut self, widget_position: &WidgetPosition) {
535 let value = (*self.source).get();
536 let unit = (*self.source).unit();
537 let logo = (self.logo)(&value);
538 let value_s = match value {
539 Value::S(s) => s,
540 Value::I(i) => i.to_string()
541 };
542 let s = format!("{}{} {}{}{}", self.prefix.to_string(), logo, value_s, unit.clone().unwrap_or(String::from("")), self.suffix.to_string());
543 if widget_position == &WidgetPosition::Left {
544 print!("{}", s);
545 } else {
546 let len = format!("{} {}{}", logo, value_s, unit.clone().unwrap_or(String::from("")), ).chars().count() + self.prefix.len() + self.suffix.len();
547 Ansi::move_back(len);
548 print!("{}", s);
549 Ansi::move_back(len);
550 }
551 }
552}
553
554pub struct Conf {
555 pub font: String,
556 pub font_size: u8,
557 pub terminal_width: u16,
558 pub refresh_time: Duration,
559 pub widgets: HashMap<WidgetPosition, Vec<Widget>>,
560}
561
562pub struct UmberBar {
563 pub conf: Conf
564}
565
566impl UmberBar {
567
568 pub async fn draw_at(widget_position: &WidgetPosition, widgets: &mut Vec<Widget>) {
569 for widget in widgets {
570 widget.draw(&widget_position).await
571 }
572 }
573
574 pub async fn draw(&mut self) {
575 let left = WidgetPosition::Left;
576 let right = WidgetPosition::Right;
577 Ansi::move_to(0, 0);
578 print!("{}", " ".repeat(self.conf.terminal_width as usize));
579 Ansi::move_to(0, 0);
580 UmberBar::draw_at(&left, self.conf.widgets.get_mut(&left).unwrap_or(&mut vec![])).await;
581 Ansi::move_to(0, self.conf.terminal_width as usize);
582 UmberBar::draw_at(&right, self.conf.widgets.get_mut(&right).unwrap_or(&mut vec![])).await;
583 let _ = std::io::stdout().flush();
584 }
585
586 fn is_child_process() -> bool {
587 match env::var("within_umberbar") {
588 Ok(_) => true,
589 Err(_) => false,
590 }
591
592 }
593
594 async fn run_inside_terminal(&mut self) {
595 Ansi::hide_cursor();
596 loop {
597 self.draw().await;
598 sleep(self.conf.refresh_time).await;
599 }
600 }
601
602 fn run_terminal(&mut self) {
603 let output = format!("font:\n family: {}\n size: {}\nbackground_opacity: 0\nwindow:\n position:\n x: 0\n y: 0\n dimensions:\n columns: {}\n lines: 1\n", self.conf.font, self.conf.font_size, self.conf.terminal_width);
604 let alacritty_conf_path = "/tmp/alacritty-umberbar-rs.yml";
605 let mut ofile = File::create(alacritty_conf_path)
606 .expect("unable to create file");
607 ofile.write_all(output.as_bytes())
608 .expect("unable to write");
609 match std::env::current_exe() {
610 Ok(cmd) => {
611 let _ = Command::new("alacritty")
612 .env("within_umberbar", "true")
613 .arg("--config-file")
614 .arg(alacritty_conf_path)
615 .arg("--class")
616 .arg("xscreensaver")
617 .arg("--command")
618 .arg(cmd)
619 .status();
620 }
621 Err(e) => { print!("{}", e); }
622 }
623 }
624
625 pub async fn run(&mut self) {
626 if UmberBar::is_child_process() {
627 self.run_inside_terminal().await
628 } else {
629 self.run_terminal()
630 }
631 }
632}
633
634pub fn umberbar(conf: Conf) -> UmberBar {
635 UmberBar {
636 conf: conf
637 }
638}