use crate::{Color, Error, Result};
use std::{
io::{Read, Write},
process::{Command, Stdio},
};
#[derive(Debug, Clone)]
pub enum MenuMatch {
Line(usize, String),
UserInput(String),
NoMatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DMenuKind {
Suckless,
Rust,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct DMenuConfig {
pub show_line_numbers: bool,
pub show_on_bottom: bool,
pub password_input: bool,
pub ignore_case: bool,
pub bg_color: Color,
pub fg_color: Color,
pub selected_color: Color,
pub n_lines: u8,
pub custom_font: Option<String>,
pub kind: DMenuKind,
pub custom_prompt: Option<String>,
}
impl Default for DMenuConfig {
fn default() -> Self {
Self {
show_line_numbers: false,
show_on_bottom: false,
password_input: false,
ignore_case: false,
bg_color: 0x282828ff.into(),
fg_color: 0xebdbb2ff.into(),
selected_color: 0x458588ff.into(),
n_lines: 10,
custom_font: None,
kind: DMenuKind::Suckless,
custom_prompt: None,
}
}
}
impl DMenuConfig {
pub fn with_prompt(prompt: &str) -> Self {
Self {
custom_prompt: Some(prompt.to_string()),
..Default::default()
}
}
fn flags(&self, screen_index: usize) -> Vec<String> {
let Self {
show_on_bottom,
password_input,
ignore_case,
bg_color,
fg_color,
selected_color,
n_lines,
custom_font,
kind,
custom_prompt,
..
} = self;
let prefix = match kind {
DMenuKind::Suckless => "-",
DMenuKind::Rust => "--",
};
let mut flags = vec!["-m".to_owned(), screen_index.to_string()];
flags.push(format!("{prefix}nb"));
flags.push(bg_color.as_rgb_hex_string());
flags.push(format!("{prefix}nf"));
flags.push(fg_color.as_rgb_hex_string());
flags.push(format!("{prefix}sb"));
flags.push(selected_color.as_rgb_hex_string());
if *n_lines > 0 {
flags.push("-l".to_owned());
flags.push(n_lines.to_string());
}
if *show_on_bottom {
flags.push("-b".to_owned());
}
if *password_input {
flags.push("-P".to_owned());
}
if *ignore_case {
flags.push("-i".to_owned());
}
if let Some(font) = custom_font {
flags.push(format!("{prefix}fn"));
flags.push(font.to_owned());
}
if let Some(prompt) = custom_prompt {
flags.push("-p".to_owned());
flags.push(prompt.to_owned());
}
flags
}
}
#[derive(Debug, Clone)]
pub struct DMenu {
config: DMenuConfig,
screen_index: usize,
}
impl DMenu {
pub fn new(config: &DMenuConfig, screen_index: usize) -> Self {
Self {
config: config.to_owned(),
screen_index,
}
}
pub fn run(&self) -> Result<()> {
let args = self.config.flags(self.screen_index);
let spawned_process = Command::new("dmenu_run").args(args).spawn();
match spawned_process {
Ok(mut process) => match process.wait() {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
},
Err(e) => Err(e.into()),
}
}
pub fn build_menu(&self, param_choices: Vec<impl Into<String>>) -> Result<MenuMatch> {
let choices: Vec<String> = param_choices
.into_iter()
.map(std::convert::Into::into)
.collect();
let raw = self.raw_user_choice_from_dmenu(&choices)?;
let choice = raw.trim();
if choice.is_empty() {
return Ok(MenuMatch::NoMatch);
}
let res = choices
.iter()
.enumerate()
.find(|(i, s)| {
if self.config.show_line_numbers {
format!("{i:<3} {s}") == choice
} else {
*s == choice
}
})
.map_or_else(
|| MenuMatch::UserInput(choice.to_owned()),
|(i, _)| {
MenuMatch::Line(
i,
choices.get(i).expect("Indexing choices panicked").clone(),
)
},
);
Ok(res)
}
fn choices_as_input_bytes(&self, choices: &[String]) -> Vec<u8> {
if self.config.show_line_numbers {
choices
.iter()
.enumerate()
.map(|(i, s)| format!("{i:<3} {s}"))
.collect::<Vec<String>>()
.join("\n")
.as_bytes()
.to_vec()
} else {
choices.join("\n").as_bytes().to_vec()
}
}
fn raw_user_choice_from_dmenu(&self, choices: &[String]) -> Result<String> {
let args = self.config.flags(self.screen_index);
let mut proc = Command::new("dmenu")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.args(args)
.spawn()?;
{
let mut stdin = proc
.stdin
.take()
.ok_or_else(|| Error::Custom("unable to open stdin".to_owned()))?;
stdin.write_all(&self.choices_as_input_bytes(choices))?;
}
let mut raw = String::new();
proc.stdout
.ok_or_else(|| Error::Custom("failed to spawn dmenu".to_owned()))?
.read_to_string(&mut raw)?;
Ok(raw)
}
}
#[cfg(test)]
mod tests {
use super::{DMenuConfig, DMenuKind};
#[test]
fn dmenu_suckless_config_test() {
let dc = DMenuConfig {
custom_font: Some("mono".to_owned()),
..DMenuConfig::default()
};
assert_eq!(dc.kind, DMenuKind::Suckless);
let flags = dc.flags(0);
for (i, flag) in flags.into_iter().enumerate() {
if i == 2 {
assert_eq!(flag, "-nb".to_owned());
}
if i == 4 {
assert_eq!(flag, "-nf".to_owned());
}
if i == 6 {
assert_eq!(flag, "-sb".to_owned());
}
if i == 10 {
assert_eq!(flag, "-fn".to_owned());
}
}
}
#[test]
fn dmenu_rs_config_test() {
let dc = DMenuConfig {
custom_font: Some("mono".to_owned()),
kind: DMenuKind::Rust,
..DMenuConfig::default()
};
assert_eq!(dc.kind, DMenuKind::Rust);
let flags = dc.flags(0);
for (i, flag) in flags.into_iter().enumerate() {
if i == 2 {
assert_eq!(flag, "--nb".to_owned());
}
if i == 4 {
assert_eq!(flag, "--nf".to_owned());
}
if i == 6 {
assert_eq!(flag, "--sb".to_owned());
}
if i == 10 {
assert_eq!(flag, "--fn".to_owned());
}
}
}
}