diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 53a6fdc..a8ad7e4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,7 +15,7 @@ jobs: run: rustc --version - name: Build run: cargo rustc --lib --verbose -- -D warnings - | cargo rustc --bin main --verbose -- -D warnings + | cargo rustc --bin marcador --verbose -- -D warnings clippy: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 0d4ecbe..2e70646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ reqwest = { version = "0.11", features = ["blocking", "json"]} clap = { version = "4", features = ["derive"]} copypasta = "0.10" tokio = "1.35" -rofi = { git = "ssh://git@github.com/joajfreitas/rofi-rs.git"} +#rofi = { git = "ssh://git@github.com/joajfreitas/rofi-rs.git"} +thiserror = "1.0.19" diff --git a/src/bin/marcador.rs b/src/bin/marcador.rs index 8f8399b..1dacbbf 100644 --- a/src/bin/marcador.rs +++ b/src/bin/marcador.rs @@ -2,6 +2,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use copypasta::{ClipboardContext, ClipboardProvider}; use marcador::models::Bookmarks; +use marcador::rofi; use marcador::server::server; use marcador::{BookmarkProxy, RemoteProxy}; @@ -47,7 +48,6 @@ fn rofi_delete(proxy: &RemoteProxy, index: usize, books: Vec) -> Resu fn rofi_open(url: &str) -> Result<(), String> { open::with(url, "firefox").map_err(|_| "Failed to open url")?; Ok(()) - } fn command_rofi(host: String) -> Result<(), String> { diff --git a/src/lib.rs b/src/lib.rs index 6eabc5a..f48cd86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod models; +pub mod rofi; pub mod schema; pub mod server; diff --git a/src/rofi.rs b/src/rofi.rs new file mode 100644 index 0000000..827c85c --- /dev/null +++ b/src/rofi.rs @@ -0,0 +1,501 @@ +// Copyright 2020 Tibor Schneider +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Rofi ui manager +//! Spawn rofi windows, and parse the result appropriately. +//! +//! ## Simple example +//! +//! ``` +//! use rofi; +//! use std::{fs, env}; +//! +//! let dir_entries = fs::read_dir(env::current_dir().unwrap()) +//! .unwrap() +//! .map(|d| format!("{:?}", d.unwrap().path())) +//! .collect::>(); +//! +//! match rofi::Rofi::new(&dir_entries).run() { +//! Ok(choice) => println!("Choice: {}", choice), +//! Err(rofi::Error::Interrupted) => println!("Interrupted"), +//! Err(e) => println!("Error: {}", e) +//! } +//! ``` +//! +//! ## Example of returning an index +//! `rofi` can also be used to return an index of the selected item: +//! +//! ``` +//! use rofi; +//! use std::{fs, env}; +//! +//! let dir_entries = fs::read_dir(env::current_dir().unwrap()) +//! .unwrap() +//! .map(|d| format!("{:?}", d.unwrap().path())) +//! .collect::>(); +//! +//! match rofi::Rofi::new(&dir_entries).run_index() { +//! Ok(element) => println!("Choice: {}", element), +//! Err(rofi::Error::Interrupted) => println!("Interrupted"), +//! Err(rofi::Error::NotFound) => println!("User input was not found"), +//! Err(e) => println!("Error: {}", e) +//! } +//! ``` +//! +//! ## Example of using pango formatted strings +//! `rofi` can display pango format. Here is a simple example (you have to call +//! the `self..pango` function). +//! +//! ``` +//! use rofi; +//! use rofi::pango::{Pango, FontSize}; +//! use std::{fs, env}; +//! +//! let entries: Vec = vec![ +//! Pango::new("Option 1").size(FontSize::Small).fg_color("#666000").build(), +//! Pango::new("Option 2").size(FontSize::Large).fg_color("#deadbe").build(), +//! ]; +//! +//! match rofi::Rofi::new(&entries).pango().run() { +//! Ok(element) => println!("Choice: {}", element), +//! Err(rofi::Error::Interrupted) => println!("Interrupted"), +//! Err(e) => println!("Error: {}", e) +//! } +//! ``` + +//#![deny(missing_docs, missing_debug_implementations, rust_2018_idioms)] + +pub mod pango; + +use std::io::{Read, Write}; +use std::process::{Child, Command, Stdio}; +use thiserror::Error; + +/// # Rofi Window Builder +/// Rofi struct for displaying user interfaces. This struct is build after the +/// non-consuming builder pattern. You can prepare a window, and draw it +/// multiple times without reconstruction and reallocation. You can choose to +/// return a handle to the child process `RofiChild`, which allows you to kill +/// the process. +#[derive(Debug, Clone)] +pub struct Rofi<'a, T> +where + T: AsRef, +{ + elements: &'a [T], + case_sensitive: bool, + lines: Option, + message: Option, + width: Width, + format: Format, + args: Vec, + sort: bool, +} + +/// Rofi child process. +#[derive(Debug)] +pub struct RofiChild { + num_elements: T, + p: Child, +} + +impl RofiChild { + fn new(p: Child, arg: T) -> Self { + Self { + num_elements: arg, + p, + } + } + /// Kill the Rofi process + pub fn kill(&mut self) -> Result<(), Error> { + Ok(self.p.kill()?) + } +} + +impl RofiChild { + /// Wait for the result and return the output as a String. + fn wait_with_output(&mut self) -> Result<(i32, Option), Error> { + let status = self.p.wait()?; + let code = status.code().unwrap(); + if status.success() || (code >= 10 && code <= 30) { + let mut buffer = String::new(); + if let Some(mut reader) = self.p.stdout.take() { + reader.read_to_string(&mut buffer)?; + } + if buffer.ends_with('\n') { + buffer.pop(); + } + if buffer.is_empty() { + Err(Error::Blank {}) + } else { + Ok((code, Some(buffer))) + } + } else { + Err(Error::Interrupted {}) + } + } +} + +impl RofiChild { + /// Wait for the result and return the output as an usize. + fn wait_with_output(&mut self) -> Result<(i32, Option), Error> { + let status = self.p.wait()?; + let code = status.code().unwrap(); + if status.success() || (code >= 10 && code <= 30) { + let mut buffer = String::new(); + if let Some(mut reader) = self.p.stdout.take() { + reader.read_to_string(&mut buffer)?; + } + if buffer.ends_with('\n') { + buffer.pop(); + } + if buffer.is_empty() { + Err(Error::Blank {}) + } else { + let idx: isize = buffer.parse::()?; + if idx < 0 || idx > self.num_elements as isize { + Ok((code, None)) + } else { + Ok((code, Some(idx as usize))) + } + } + } else { + Err(Error::Interrupted {}) + } + } +} + +impl<'a, T> Rofi<'a, T> +where + T: AsRef, +{ + /// Generate a new, unconfigured Rofi window based on the elements provided. + pub fn new(elements: &'a [T]) -> Self { + Self { + elements, + case_sensitive: false, + lines: None, + width: Width::None, + format: Format::Text, + args: Vec::new(), + sort: false, + message: None, + } + } + + /// Show the window, and return the selected string, including pango + /// formatting if available + pub fn run(&self) -> Result<(i32, Option), Error> { + self.spawn()?.wait_with_output() + } + + /// show the window, and return the index of the selected string This + /// function will overwrite any subsequent calls to `self.format`. + pub fn run_index(&mut self) -> Result<(i32, Option), Error> { + self.spawn_index()?.wait_with_output() + } + + /// Set sort flag + pub fn set_sort(&mut self) -> &mut Self { + self.sort = true; + self + } + + /// enable pango markup + pub fn pango(&mut self) -> &mut Self { + self.args.push("-markup-rows".to_string()); + self + } + + /// enable password mode + pub fn password(&mut self) -> &mut Self { + self.args.push("-password".to_string()); + self + } + + /// enable message dialog mode (-e) + pub fn message_only(&mut self, message: impl Into) -> Result<&mut Self, Error> { + if !self.elements.is_empty() { + return Err(Error::ConfigErrorMessageAndOptions); + } + self.message = Some(message.into()); + Ok(self) + } + + /// Sets the number of lines. + /// If this function is not called, use the number of lines provided in the + /// elements vector. + pub fn lines(&mut self, l: usize) -> &mut Self { + self.lines = Some(l); + self + } + + /// Set the width of the window (overwrite the theme settings) + pub fn width(&mut self, w: Width) -> Result<&mut Self, Error> { + w.check()?; + self.width = w; + Ok(self) + } + + /// Sets the case sensitivity (disabled by default) + pub fn case_sensitive(&mut self, sensitivity: bool) -> &mut Self { + self.case_sensitive = sensitivity; + self + } + + /// Set the prompt of the rofi window + pub fn prompt(&mut self, prompt: impl Into) -> &mut Self { + self.args.push("-p".to_string()); + self.args.push(prompt.into()); + self + } + + /// + pub fn message(&mut self, message: impl Into) -> &mut Self { + self.args.push("-mesg".to_string()); + self.args.push(message.into()); + self + } + + /// Set the rofi theme + /// This will make sure that rofi uses `~/.config/rofi/{theme}.rasi` + pub fn theme(&mut self, theme: Option>) -> &mut Self { + if let Some(t) = theme { + self.args.push("-theme".to_string()); + self.args.push(t.into()); + } + self + } + + /// Set the return format of the rofi call. Default is `Format::Text`. If + /// you call `self.spawn_index` later, the format will be overwritten with + /// `Format::Index`. + pub fn return_format(&mut self, format: Format) -> &mut Self { + self.format = format; + self + } + + /// + pub fn kb_custom(&mut self, id: u32, shortcut: &str) -> &mut Self { + self.args.push(format!("-kb-custom-{}", id)); + self.args.push(shortcut.to_string()); + self + } + + /// Returns a child process with the pre-prepared rofi window + /// The child will produce the exact output as provided in the elements vector. + pub fn spawn(&self) -> Result, std::io::Error> { + Ok(RofiChild::new(self.spawn_child()?, String::new())) + } + + /// Returns a child process with the pre-prepared rofi window. + /// The child will produce the index of the chosen element in the vector. + /// This function will overwrite any subsequent calls to `self.format`. + pub fn spawn_index(&mut self) -> Result, std::io::Error> { + self.format = Format::Index; + Ok(RofiChild::new(self.spawn_child()?, self.elements.len())) + } + + fn spawn_child(&self) -> Result { + let mut child = Command::new("rofi") + .args(match &self.message { + Some(msg) => vec!["-e", msg], + None => vec!["-dmenu"], + }) + .args(&self.args) + .arg("-format") + .arg(self.format.as_arg()) + .arg("-l") + .arg(match self.lines.as_ref() { + Some(s) => format!("{}", s), + None => format!("{}", self.elements.len()), + }) + .arg(match self.case_sensitive { + true => "-case-sensitive", + false => "-i", + }) + .args(match self.width { + Width::None => vec![], + Width::Percentage(x) => vec![ + "-theme-str".to_string(), + format!("window {{width: {}%;}}", x), + ], + Width::Pixels(x) => vec![ + "-theme-str".to_string(), + format!("window {{width: {}px;}}", x), + ], + }) + .arg(match self.sort { + true => "-sort", + false => "", + }) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + if let Some(mut writer) = child.stdin.take() { + for element in self.elements { + writer.write_all(element.as_ref().as_bytes())?; + writer.write_all(b"\n")?; + } + } + + Ok(child) + } +} + +static EMPTY_OPTIONS: Vec = vec![]; + +impl<'a> Rofi<'a, String> { + /// Generate a new, Rofi window in "message only" mode with the given message. + pub fn new_message(message: impl Into) -> Self { + let mut rofi = Self::new(&EMPTY_OPTIONS); + rofi.message_only(message) + .expect("Invariant: provided empty options so it is safe to unwrap message_only"); + rofi + } +} + +/// Width of the rofi window to overwrite the default width from the rogi theme. +#[derive(Debug, Clone, Copy)] +pub enum Width { + /// No width specified, use the default one from the theme + None, + /// Width in percentage of the screen, must be between 0 and 100 + Percentage(usize), + /// Width in pixels, must be greater than 100 + Pixels(usize), +} + +impl Width { + fn check(&self) -> Result<(), Error> { + match self { + Self::Percentage(x) => { + if *x > 100 { + Err(Error::InvalidWidth("Percentage must be between 0 and 100")) + } else { + Ok(()) + } + } + Self::Pixels(x) => { + if *x <= 100 { + Err(Error::InvalidWidth("Pixels must be larger than 100")) + } else { + Ok(()) + } + } + _ => Ok(()), + } + } +} + +/// Different modes, how rofi should return the results +#[derive(Debug, Clone, Copy)] +pub enum Format { + /// Regular text, including markup + #[allow(dead_code)] + Text, + /// Text, where the markup is removed + StrippedText, + /// Text with the exact user input + UserInput, + /// Index of the chosen element + Index, +} + +impl Format { + fn as_arg(&self) -> &'static str { + match self { + Format::Text => "s", + Format::StrippedText => "p", + Format::UserInput => "f", + Format::Index => "i", + } + } +} + +/// Rofi Error Type +#[derive(Error, Debug)] +pub enum Error { + /// IO Error + #[error("IO Error: {0}")] + IoError(#[from] std::io::Error), + /// Parse Int Error, only occurs when getting the index. + #[error("Parse Int Error: {0}")] + ParseIntError(#[from] std::num::ParseIntError), + /// Error returned when the user has interrupted the action + #[error("User interrupted the action")] + Interrupted, + /// Error returned when the user chose a blank option + #[error("User chose a blank line")] + Blank, + /// Error returned the width is invalid, only returned in Rofi::width() + #[error("Invalid width: {0}")] + InvalidWidth(&'static str), + /// Error, when the input of the user is not found. This only occurs when + /// getting the index. + #[error("User input was not found")] + NotFound, + /// Incompatible configuration: cannot specify non-empty options and message_only. + #[error("Can't specify non-empty options and message_only")] + ConfigErrorMessageAndOptions, +} + +#[cfg(test)] +mod rofitest { + use super::*; + #[test] + fn simple_test() { + let options = vec!["a", "b", "c", "d"]; + let empty_options: Vec = Vec::new(); + match Rofi::new(&options).prompt("choose c").run() { + Ok(ret) => assert!(ret == "c"), + _ => assert!(false), + } + match Rofi::new(&options).prompt("chose c").run_index() { + Ok(ret) => assert!(ret == 2), + _ => assert!(false), + } + match Rofi::new(&options) + .prompt("press escape") + .width(Width::Percentage(15)) + .unwrap() + .run_index() + { + Err(Error::Interrupted) => assert!(true), + _ => assert!(false), + } + match Rofi::new(&options) + .prompt("Enter something wrong") + .run_index() + { + Err(Error::NotFound) => assert!(true), + _ => assert!(false), + } + match Rofi::new(&empty_options) + .prompt("Enter password") + .password() + .return_format(Format::UserInput) + .run() + { + Ok(ret) => assert!(ret == "password"), + _ => assert!(false), + } + match Rofi::new_message("A message with no input").run() { + Err(Error::Blank) => (), // ok + _ => assert!(false), + } + } +} diff --git a/src/rofi/pango.rs b/src/rofi/pango.rs new file mode 100644 index 0000000..6d95086 --- /dev/null +++ b/src/rofi/pango.rs @@ -0,0 +1,487 @@ +// Copyright 2020 Tibor Schneider +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Pango markup language support +//! https://developer.gnome.org/pygtk/stable/pango-markup-language.html + +use std::collections::HashMap; +use std::fmt; + +/// Structure for writing Pango markup spans +#[derive(Debug, Clone)] +pub struct Pango<'a> { + content: &'a str, + options: HashMap<&'static str, &'a str>, +} + +impl<'a> Pango<'a> { + /// Generate a new pango class + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test").build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn new(content: &'a str) -> Pango<'_> { + Pango { + content, + options: HashMap::new(), + } + } + + /// Generate a new pango class with options capacity + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::with_capacity("test", 0).build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn with_capacity(content: &'a str, size: usize) -> Pango<'_> { + Pango { + content, + options: HashMap::with_capacity(size), + } + } + + /// Generate the pango string + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .slant_style(pango::SlantStyle::Italic) + /// .size(pango::FontSize::Small) + /// .build(); + /// assert!(t == "test" || + /// t == "test"); + /// ``` + pub fn build(&mut self) -> String { + self.to_string() + } + + /// Generates a pango string based on the options, but with a different + /// content. + /// + /// ``` + /// use rofi::pango; + /// let mut p = pango::Pango::new(""); + /// p.slant_style(pango::SlantStyle::Italic); + /// p.size(pango::FontSize::Small); + /// let t = p.build_content("test"); + /// assert!(t == "test" || + /// t == "test"); + /// ``` + pub fn build_content(&self, content: &str) -> String { + self.to_string_with_content(content) + } + + /// Set the font + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .font_description("Sans Italic 12") + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn font_description(&mut self, font: &'a str) -> &mut Self { + self.options.insert("font_desc", font); + self + } + + /// set the font family + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .font_family(pango::FontFamily::Monospace) + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn font_family(&mut self, family: FontFamily) -> &mut Self { + self.options.insert( + "face", + match family { + FontFamily::Normal => "normal", + FontFamily::Sans => "sans", + FontFamily::Serif => "serif", + FontFamily::Monospace => "monospace", + }, + ); + self + } + + /// Set the size of the font, relative to the configured font size + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .size(pango::FontSize::Huge) + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn size(&mut self, size: FontSize) -> &mut Self { + self.options.insert( + "size", + match size { + FontSize::VeryTiny => "xx-small", + FontSize::Tiny => "x-small", + FontSize::Small => "small", + FontSize::Normal => "medium", + FontSize::Large => "large", + FontSize::Huge => "x-large", + FontSize::VeryHuge => "xx-large", + FontSize::Smaller => "smaller", + FontSize::Larger => "larger", + }, + ); + self + } + + /// Set the slant style (italic / oblique / normal) + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .slant_style(pango::SlantStyle::Oblique) + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn slant_style(&mut self, style: SlantStyle) -> &mut Self { + self.options.insert( + "style", + match style { + SlantStyle::Normal => "normal", + SlantStyle::Oblique => "oblique", + SlantStyle::Italic => "italic", + }, + ); + self + } + + /// Set the font weight + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .weight(pango::Weight::Bold) + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn weight(&mut self, weight: Weight) -> &mut Self { + self.options.insert( + "weight", + match weight { + Weight::Thin => "100", + Weight::UltraLight => "ultralight", + Weight::Light => "light", + Weight::Normal => "normal", + Weight::Medium => "500", + Weight::SemiBold => "600", + Weight::Bold => "bold", + Weight::UltraBold => "ultrabold", + Weight::Heavy => "heavy", + Weight::UltraHeavy => "1000", + }, + ); + self + } + + /// Set the alpha of the text + /// Important: alpha must be fo the form: XX%, where XX is a number between 0 and 100. + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .alpha("50%") + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn alpha(&mut self, alpha: &'a str) -> &mut Self { + self.options.insert("alpha", alpha); + self + } + + /// Use smallcaps + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .small_caps() + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn small_caps(&mut self) -> &mut Self { + self.options.insert("variant", "smallcaps"); + self + } + + /// Set the stretch (expanded or condensed) + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .stretch(pango::FontStretch::Condensed) + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn stretch(&mut self, stretch: FontStretch) -> &mut Self { + self.options.insert( + "stretch", + match stretch { + FontStretch::UltraCondensed => "ultracondensed", + FontStretch::ExtraCondensed => "extracondensed", + FontStretch::Condensed => "condensed", + FontStretch::SemiCondensed => "semicondensed", + FontStretch::Normal => "normal", + FontStretch::SemiExpanded => "semiexpanded", + FontStretch::Expanded => "expanded", + FontStretch::ExtraExpanded => "extraexpanded", + FontStretch::UltraExpanded => "ultraexpanded", + }, + ); + self + } + + /// Set the foreground color + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .fg_color("#00FF00") + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn fg_color(&mut self, color: &'a str) -> &mut Self { + self.options.insert("foreground", color); + self + } + + /// Set the background color + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .bg_color("#00FF00") + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn bg_color(&mut self, color: &'a str) -> &mut Self { + self.options.insert("background", color); + self + } + + /// Set the underline style + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .underline(pango::Underline::Double) + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn underline(&mut self, underline: Underline) -> &mut Self { + self.options.insert( + "underline", + match underline { + Underline::None => "none", + Underline::Single => "single", + Underline::Double => "double", + Underline::Low => "low", + }, + ); + self + } + + /// set the font to strike through + /// + /// # Usage + /// + /// ``` + /// use rofi::pango; + /// let t = pango::Pango::new("test") + /// .strike_through() + /// .build(); + /// assert_eq!(t, "test"); + /// ``` + pub fn strike_through(&mut self) -> &mut Self { + self.options.insert("strikethrough", "true"); + self + } + + fn to_string_with_content(&self, content: &str) -> String { + if self.options.is_empty() { + content.to_string() + } else { + format!( + "{}", + self.options + .iter() + .map(|(k, v)| format!("{}='{}'", k, v)) + .collect::>() + .join(" "), + content + ) + } + } +} + +/// Enumeration over all available font families +#[derive(Debug, Clone, Copy)] +pub enum FontFamily { + /// Normal font + Normal, + /// Sans Serif font + Sans, + /// Font including serif + Serif, + /// Monospaced font + Monospace, +} + +/// Enumeration over all avaliable font sizes +#[derive(Debug, Clone, Copy)] +pub enum FontSize { + /// Very tiny font size, corresponsds to xx-small + VeryTiny, + /// Tiny font size, corresponds to x-small + Tiny, + /// Small font size, corresponds to small + Small, + /// Normal font size (default), corresponds to medium + Normal, + /// Large font size, corresponds to large + Large, + /// Huge font size, corresponds to x-large + Huge, + /// Very huge font size, corresponds to xx-large + VeryHuge, + /// Relative font size, makes content smaller than the parent + Smaller, + /// Relative font size, makes content larger than the parent + Larger, +} + +/// Enumeration over all possible slant styles +#[derive(Debug, Clone, Copy)] +pub enum SlantStyle { + /// No slant + Normal, + /// Oblique, normal font skewed + Oblique, + /// Italic font, (different face) + Italic, +} + +/// Enumeration over all possible weights +#[derive(Debug, Clone, Copy)] +pub enum Weight { + /// Thin weight (=100) + Thin, + /// Ultralight weight (=200) + UltraLight, + /// Light weight (=300) + Light, + /// Normal weight (=400) + Normal, + /// Medium weight (=500) + Medium, + /// SemiBold weight (=600) + SemiBold, + /// Bold weight (=700) + Bold, + /// Ultrabold weight (=800) + UltraBold, + /// Heavy (=900) + Heavy, + /// UltraHeavy weight (=1000) + UltraHeavy, +} + +/// enumeration over all possible font stretch modes +#[derive(Debug, Clone, Copy)] +pub enum FontStretch { + /// UltraCondensed, letters are extremely close together + UltraCondensed, + /// ExtraCondensed, letters are very close together + ExtraCondensed, + /// Condensed, letters are close together + Condensed, + /// SemiCondensed, letters somewhat are close together + SemiCondensed, + /// Normal, normal spacing as defined by the font + Normal, + /// SemiExpanded, letters somewhat are far apart + SemiExpanded, + /// Expanded, letters somewhat far apart + Expanded, + /// ExtraExpanded, letters very far apart + ExtraExpanded, + /// UltraExpanded, letters extremely far apart + UltraExpanded, +} + +/// enumeration over all possible underline modes +#[derive(Debug, Clone, Copy)] +pub enum Underline { + /// No underline mode + None, + /// Single, normal underline + Single, + /// Double + Double, + /// Low, only the lower line of double is drawn + Low, +} + +impl<'a> fmt::Display for Pango<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.options.is_empty() { + write!(f, "{}", self.content) + } else { + write!(f, "{}", self.content) + } + } +}