umberbar/
lib.rs

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
32/*
33macro_rules! i_source {
34    ($x:expr, $y:expr) =>  {
35    Source {
36      unit: Some($x.to_string()),
37      get: |_| (Value::I($y(System::new()) as u8), SourceData::Nothing),
38      data: SourceData::Nothing,
39    }
40  }
41}
42
43macro_rules! s_source {
44    ($x:expr, $y:expr) =>  {
45    Source {
46      unit: Some($x.to_string()),
47      get: |_| (Value::S($y), SourceData::Nothing),
48      data: SourceData::Nothing,
49    }
50  }
51}
52*/
53
54pub 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}