rust_bitbar/
lib.rs

1/// # Example
2///
3/// ```
4/// use rust_bitbar::{Line, Plugin, SubMenu};
5///
6/// let mut pl = Plugin::new();
7/// let mut line = Line::new("first line");
8/// line.set_color("red")
9///     .set_href("http://google.com");
10///
11/// let mut sub_menu = SubMenu::new();
12/// sub_menu.add_line(line);
13///
14/// let status_line = Line::new("🍺🍺🍺");
15/// pl.set_status_line(status_line).set_sub_menu(sub_menu);
16///
17/// pl.render();
18/// ```
19
20/// New returns an empty Bitbar menu without any context
21#[derive(Default)]
22pub struct Plugin {
23    pub status_bar: StatusBar,
24    pub sub_menu: Option<SubMenu>,
25}
26
27/// Line holds the content, styling and behaviour of a line in a Bitbar
28/// menu, both in the menu and submenus
29#[derive(Default, Debug, Clone)]
30pub struct Line {
31    text: String,
32    href: String,
33    color: String,
34    font: String,
35    size: i64,
36    terminal: bool,
37    refresh: bool,
38    drop_down: bool,
39    length: i64,
40    trim: bool,
41    alternate: bool,
42    emojize: bool,
43    ansi: bool,
44    bash: String,
45    params: Vec<String>,
46    template_image: String,
47    image: String,
48    hr: bool,
49}
50
51/// Style wraps options related to text presentation which can be added to a line
52#[derive(Default, Debug)]
53pub struct Style {
54    pub color: String,
55    pub font: String,
56    pub size: i64,
57    pub length: i64,
58    pub trim: bool,
59    pub emojize: bool,
60    pub ansi: bool,
61}
62
63/// Cmd wraps options related to commands which can be added to a line using the
64#[derive(Default, Debug)]
65pub struct Cmd {
66    pub bash: String,
67    pub params: Vec<String>,
68    pub terminal: bool,
69    pub refresh: bool,
70}
71
72/// StatusBar holds one of more Lines of text which are rendered in the status bar.
73/// Multiple Lines will be cycled through over and over
74#[derive(Default, Debug)]
75pub struct StatusBar {
76    pub lines: Vec<Line>,
77}
78
79/// SubMenu contains a slice of SubMenuItems which can be Lines or additional
80/// SubMenus. The Level indicates how nested the submenu is which is used during
81/// render to prepend the correct number of `--` prefixes.
82pub struct SubMenu {
83    pub level: i64,
84    pub lines: Vec<SubMenuItem>,
85}
86
87/// Enum which represent a sub menu item
88pub enum SubMenuItem {
89    Line(Line),
90    SubMenu(Box<SubMenu>),
91}
92
93impl Plugin {
94    /// Function to create empty plugin
95    pub fn new() -> Self {
96        Plugin::default()
97    }
98
99    pub fn set_status_line(&mut self, line: Line) -> &mut Self {
100        self.status_bar.lines.push(line);
101        self
102    }
103
104    pub fn set_sub_menu(&mut self, sub_menu: SubMenu) -> &mut Self {
105        self.sub_menu = Some(sub_menu);
106        self
107    }
108
109    pub fn render(&self) {
110        print!("{}", self.to_string());
111    }
112}
113
114impl std::string::ToString for Plugin {
115    fn to_string(&self) -> String {
116        let mut output = String::from("");
117        for line in self.status_bar.lines.iter().as_ref() {
118            let line_str = line.to_string();
119            output = format!("{}{}\n", output, line_str);
120        }
121        output = format!("{}---\n", output);
122        if self.sub_menu.is_some() {
123            let curr_sm = self.sub_menu.as_ref().unwrap();
124            output = format!("{}{}", output, render_sub_menu(curr_sm));
125        }
126
127        return output;
128    }
129}
130
131impl SubMenu {
132    /// Function to create empty sub menu
133    pub fn new() -> Self {
134        SubMenu {
135            level: 0,
136            lines: vec![],
137        }
138    }
139    /// Line creates a line adding text to the dropdown which will be added after
140    /// the main dropdown delimiter (`---`).
141    pub fn add_line(&mut self, line: Line) -> &mut Self {
142        self.lines.push(SubMenuItem::Line(line));
143        self
144    }
145
146    /// NewSubMenu creates a nested submenu off a submenu.
147    pub fn add_sub_menu(&mut self, sub_menu: SubMenu) -> &mut Self {
148        self.lines.push(SubMenuItem::SubMenu(Box::new(sub_menu)));
149        self
150    }
151
152    /// HR turns a line into a horizontal delimiter, useful for breaking menu items
153    /// into logical groups.
154    pub fn add_hr(&mut self) -> &mut Self {
155        let line = Line {
156            text: "---".to_string(),
157            hr: true,
158            ..Default::default()
159        };
160
161        self.lines.push(SubMenuItem::Line(line));
162        self
163    }
164}
165
166impl Line {
167    /// Function to create empty line
168    pub fn new<T: Into<String>>(text: T) -> Self {
169        Line {
170            text: text.into(),
171            ..Default::default()
172        }
173    }
174    /// Change text of the line
175    pub fn set_text<T: Into<String>>(&mut self, text: T) -> &mut Self {
176        self.text = text.into();
177        self
178    }
179
180    /// Style provides a alternate method for setting the text style related options.
181    pub fn set_style(&mut self, style: Style) -> &mut Self {
182        self.color = style.color;
183        self.font = style.font;
184        self.size = style.size;
185        self.length = style.length;
186        self.trim = style.trim;
187        self.emojize = style.emojize;
188        self.ansi = style.ansi;
189        self
190    }
191
192    /// command provides a alternate method for setting the bash script and
193    /// params along with some related flags via a Cmd struct.
194    pub fn set_command(&mut self, cmd: Cmd) -> &mut Self {
195        self.bash = cmd.bash;
196        self.params = cmd.params;
197        self.terminal = cmd.terminal;
198        self.refresh = cmd.refresh;
199        self
200    }
201
202    /// href adds a URL to the line and makes it clickable.
203    pub fn set_href<T: Into<String>>(&mut self, href: T) -> &mut Self {
204        self.href = href.into();
205        self
206    }
207    /// Color sets the lines font color, can take a name or hex value.
208    pub fn set_color<T: Into<String>>(&mut self, color: T) -> &mut Self {
209        self.color = color.into();
210        self
211    }
212
213    /// Font sets the lines font.
214    pub fn set_font<T: Into<String>>(&mut self, font: T) -> &mut Self {
215        self.font = font.into();
216        self
217    }
218
219    /// Size sets the lines font size.
220    pub fn set_size(&mut self, size: i64) -> &mut Self {
221        self.size = size;
222        self
223    }
224
225    /// Bash makes makes the line clickable and adds a script that will be run on click.
226    pub fn set_bash<T: Into<String>>(&mut self, bash: T) -> &mut Self {
227        self.bash = bash.into();
228        self
229    }
230    /// Params adds arguments which are passed to the script specified by line.bash()
231    pub fn set_params(&mut self, params: Vec<String>) -> &mut Self {
232        self.params = params;
233        self
234    }
235
236    /// Terminal sets a flag which controls whether a Terminal is opened when the bash
237    /// script is run.
238    pub fn set_terminal(&mut self, terminal: bool) -> &mut Self {
239        self.terminal = terminal;
240        self
241    }
242
243    /// Refresh controls whether clicking the line results in the plugin being refreshed.
244    /// If the line has a bash script attached then the plugin is refreshed after the
245    /// script finishes.
246    pub fn set_refresh(&mut self, refresh: bool) -> &mut Self {
247        self.refresh = refresh;
248        self
249    }
250
251    /// DropDown sets a flag which controls whether the line only appears and cycles in the
252    /// status bar but not in the dropdown.
253    pub fn set_drop_down(&mut self, drop_down: bool) -> &mut Self {
254        self.drop_down = drop_down;
255        self
256    }
257
258    /// Length truncates the line after the specified number of characters. An elipsis will
259    /// be added to any truncated strings, as well as a tooltip displaying the full string.
260    pub fn set_length(&mut self, length: i64) -> &mut Self {
261        self.length = length;
262        self
263    }
264
265    /// Trim sets a flag to control whether leading/trailing whitespace is trimmed from the
266    /// title. Defaults to `true`.
267    pub fn set_trim(&mut self, trim: bool) -> &mut Self {
268        self.trim = trim;
269        self
270    }
271
272    /// Alternate sets a flag to mark a line as an alternate to the previous one for when the
273    /// Option key is pressed in the dropdown.
274    pub fn set_alternate(&mut self, alternate: bool) -> &mut Self {
275        self.alternate = alternate;
276        self
277    }
278
279    /// Emojize sets a flag to control parsing of github style :mushroom: into πŸ„.
280    pub fn set_emojize(&mut self, emojize: bool) -> &mut Self {
281        self.emojize = emojize;
282        self
283    }
284
285    /// Ansi sets a flag to control parsing of ANSI codes.
286    pub fn set_ansi(&mut self, ansi: bool) -> &mut Self {
287        self.ansi = ansi;
288        self
289    }
290}
291
292impl std::string::ToString for Line {
293    fn to_string(&self) -> String {
294        let mut res = vec![self.text.to_string()];
295        let mut options: Vec<String> = vec!["|".to_string()];
296        options.append(&mut render_style_options(self));
297        options.append(&mut render_misc_options(self));
298        options.append(&mut render_command_options(self));
299
300        if options.len() > 1 {
301            res.append(&mut options);
302        }
303
304        return res.join(" ");
305    }
306}
307
308impl Style {
309    /// Function to create empty style
310    pub fn new() -> Self {
311        Style::default()
312    }
313}
314
315fn render_sub_menu(sub_menu: &SubMenu) -> String {
316    let mut output = String::new();
317    let mut prefix = String::new();
318
319    if sub_menu.level > 0 {
320        prefix = format!("{} ", "--".repeat(sub_menu.level as usize))
321    }
322    for line in sub_menu.lines.iter().as_ref() {
323        match line {
324            SubMenuItem::Line(current_line) => {
325                if current_line.hr {
326                    output = format!("{}{}{}\n", output, prefix.trim(), current_line.to_string());
327                } else {
328                    output = format!("{}{}{}\n", output, prefix, current_line.to_string());
329                }
330            }
331            SubMenuItem::SubMenu(current_sub_m) => {
332                output = format!("{}{}", output, render_sub_menu(&current_sub_m))
333            }
334        }
335    }
336
337    output
338}
339
340fn render_misc_options(line: &Line) -> Vec<String> {
341    let mut misc_opts = vec![];
342
343    if line.href != "" {
344        misc_opts.push(format!("href='{}'", line.href));
345    }
346    if line.drop_down {
347        misc_opts.push(format!("dropdown='{}'", line.drop_down));
348    }
349    if line.alternate {
350        misc_opts.push(format!("alternate='{}'", line.alternate));
351    }
352
353    misc_opts
354}
355
356fn render_style_options(line: &Line) -> Vec<String> {
357    let mut style_opts = vec![];
358    if line.color != "" {
359        style_opts.push(format!(r#"color="{}""#, line.color));
360    }
361    if line.font != "" {
362        style_opts.push(format!(r#"font="{}""#, line.font));
363    }
364    if line.size > 0 {
365        style_opts.push(format!("size={}", line.size));
366    }
367    if line.length > 0 {
368        style_opts.push(format!("length={}", line.length));
369    }
370    if line.trim {
371        style_opts.push(format!("trim={}", line.trim));
372    }
373    if line.emojize {
374        style_opts.push(format!("emojize={}", line.emojize));
375    }
376    if line.ansi {
377        style_opts.push(format!("ansi={}", line.ansi));
378    }
379
380    style_opts
381}
382
383fn render_command_options(line: &Line) -> Vec<String> {
384    let mut command_opts = vec![];
385    if line.bash != "" {
386        command_opts.push(format!(r#"bash="{}""#, line.bash));
387    }
388
389    if line.params.len() > 0 {
390        for i in 0..line.params.len() {
391            command_opts.push(format!("param{}={}", i, line.params[i]));
392        }
393    }
394    if line.terminal {
395        command_opts.push(format!("terminal={}", line.terminal));
396    }
397    if line.refresh {
398        command_opts.push(format!("refresh={}", line.refresh));
399    }
400
401    command_opts
402}
403
404#[test]
405fn test_render_command_options() {
406    let mut line = Line::new("here is a test".to_string());
407    line.set_bash("echo test".to_string())
408        .set_params(vec!["params1".to_string(), "params2".to_string()])
409        .set_refresh(true);
410    let resp = render_command_options(&line);
411
412    assert_eq!(resp[0], r#"bash="echo test""#.to_string());
413    assert_eq!(resp[1], r#"param0=params1"#.to_string());
414    assert_eq!(resp[2], r#"param1=params2"#.to_string());
415    assert_eq!(resp[3], "refresh=true".to_string());
416}
417
418#[test]
419fn test_line_to_string() {
420    let mut line = Line::new("here is a test".to_string());
421    line.set_bash("echo test".to_string())
422        .set_color("red".to_string())
423        .set_params(vec!["params1".to_string(), "params2".to_string()])
424        .set_refresh(true);
425    let resp = line.to_string();
426
427    assert_eq!(resp, r#"here is a test | color="red" bash="echo test" param0=params1 param1=params2 refresh=true"#.to_string());
428}