#![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;
#[derive(Debug, Clone)]
pub struct Rofi<'a, T>
where
T: AsRef<str>,
{
elements: &'a [T],
case_sensitive: bool,
lines: Option<usize>,
message: Option<String>,
width: Width,
format: Format,
args: Vec<String>,
sort: bool,
}
#[derive(Debug)]
pub struct RofiChild<T> {
num_elements: T,
p: Child,
}
impl<T> RofiChild<T> {
fn new(p: Child, arg: T) -> Self {
Self {
num_elements: arg,
p,
}
}
pub fn kill(&mut self) -> Result<(), Error> {
Ok(self.p.kill()?)
}
}
impl RofiChild<String> {
fn wait_with_output(&mut self) -> Result<String, Error> {
let status = self.p.wait()?;
let code = status.code().ok_or(Error::IoError(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"Rofi process was interrupted",
)))?;
if status.success() {
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(buffer)
}
} else if (10..=28).contains(&code) {
Err(Error::CustomKeyboardShortcut(code - 9))
} else {
Err(Error::Interrupted {})
}
}
}
impl RofiChild<usize> {
fn wait_with_output(&mut self) -> Result<usize, Error> {
let status = self.p.wait()?;
let code = status.code().ok_or(Error::IoError(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"Rofi process was interrupted",
)))?;
if status.success() {
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::<isize>()?;
if idx < 0 || idx > self.num_elements as isize {
Err(Error::NotFound {})
} else {
Ok(idx as usize)
}
}
} else if (10..=28).contains(&code) {
Err(Error::CustomKeyboardShortcut(code - 9))
} else {
Err(Error::Interrupted {})
}
}
}
impl<'a, T> Rofi<'a, T>
where
T: AsRef<str>,
{
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,
}
}
pub fn run(&self) -> Result<String, Error> {
self.spawn()?.wait_with_output()
}
pub fn run_index(&mut self) -> Result<usize, Error> {
self.spawn_index()?.wait_with_output()
}
pub fn set_sort(&mut self) -> &mut Self {
self.sort = true;
self
}
pub fn pango(&mut self) -> &mut Self {
self.args.push("-markup-rows".to_string());
self
}
pub fn password(&mut self) -> &mut Self {
self.args.push("-password".to_string());
self
}
pub fn message_only(&mut self, message: impl Into<String>) -> Result<&mut Self, Error> {
if !self.elements.is_empty() {
return Err(Error::ConfigErrorMessageAndOptions);
}
self.message = Some(message.into());
Ok(self)
}
pub fn lines(&mut self, l: usize) -> &mut Self {
self.lines = Some(l);
self
}
pub fn width(&mut self, w: Width) -> Result<&mut Self, Error> {
w.check()?;
self.width = w;
Ok(self)
}
pub fn case_sensitive(&mut self, sensitivity: bool) -> &mut Self {
self.case_sensitive = sensitivity;
self
}
pub fn prompt(&mut self, prompt: impl Into<String>) -> &mut Self {
self.args.push("-p".to_string());
self.args.push(prompt.into());
self
}
pub fn message(&mut self, message: impl Into<String>) -> &mut Self {
self.args.push("-mesg".to_string());
self.args.push(message.into());
self
}
pub fn theme(&mut self, theme: Option<impl Into<String>>) -> &mut Self {
if let Some(t) = theme {
self.args.push("-theme".to_string());
self.args.push(t.into());
}
self
}
pub fn return_format(&mut self, format: Format) -> &mut Self {
self.format = format;
self
}
pub fn kb_custom(&mut self, id: u32, shortcut: &str) -> Result<&mut Self, String> {
if !(1..=19).contains(&id) {
return Err(format!("Attempting to set custom keyboard shortcut with invalid id: {}. Valid range is: [1,19]", id));
}
self.args.push(format!("-kb-custom-{}", id));
self.args.push(shortcut.to_string());
Ok(self)
}
pub fn spawn(&self) -> Result<RofiChild<String>, std::io::Error> {
Ok(RofiChild::new(self.spawn_child()?, String::new()))
}
pub fn spawn_index(&mut self) -> Result<RofiChild<usize>, std::io::Error> {
self.format = Format::Index;
Ok(RofiChild::new(self.spawn_child()?, self.elements.len()))
}
fn spawn_child(&self) -> Result<Child, std::io::Error> {
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<String> = vec![];
impl<'a> Rofi<'a, String> {
pub fn new_message(message: impl Into<String>) -> 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
}
}
#[derive(Debug, Clone, Copy)]
pub enum Width {
None,
Percentage(usize),
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(()),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Format {
#[allow(dead_code)]
Text,
StrippedText,
UserInput,
Index,
}
impl Format {
fn as_arg(&self) -> &'static str {
match self {
Format::Text => "s",
Format::StrippedText => "p",
Format::UserInput => "f",
Format::Index => "i",
}
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("IO Error: {0}")]
IoError(#[from] std::io::Error),
#[error("Parse Int Error: {0}")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("User interrupted the action")]
Interrupted,
#[error("User chose a blank line")]
Blank,
#[error("Invalid width: {0}")]
InvalidWidth(&'static str),
#[error("User input was not found")]
NotFound,
#[error("Can't specify non-empty options and message_only")]
ConfigErrorMessageAndOptions,
#[error("User used a custom keyboard shortcut")]
CustomKeyboardShortcut(i32),
}
#[cfg(test)]
mod rofitest {
use super::*;
#[test]
fn simple_test() {
let options = vec!["a", "b", "c", "d"];
let empty_options: Vec<String> = 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) => (), _ => assert!(false),
}
let mut r = Rofi::new(&options);
match r
.message("Press Alt+n")
.kb_custom(1, "Alt+n")
.unwrap()
.run()
{
Err(Error::CustomKeyboardShortcut(exit_code)) => {
assert_eq!(exit_code, 1)
}
_ => assert!(false),
}
}
}