use crate::{draw::Color, PenroseError, Result};
use std::{
io::{Read, Write},
process::{Command, Stdio},
};
#[derive(Debug, Clone)]
pub enum MenuMatch {
Line(usize, String),
UserInput(String),
NoMatch,
}
#[derive(Debug, Clone)]
pub struct DMenuConfig {
pub show_line_numbers: bool,
pub password_input: bool,
pub bg_color: Color,
pub fg_color: Color,
pub selected_color: Color,
pub n_lines: usize,
}
impl Default for DMenuConfig {
fn default() -> Self {
Self {
show_line_numbers: false,
password_input: false,
bg_color: 0x282828ff.into(),
fg_color: 0xebdbb2ff.into(),
selected_color: 0x458588ff.into(),
n_lines: 10,
}
}
}
impl DMenuConfig {
fn flags(&self, prompt: &str, screen_index: usize) -> Vec<String> {
let mut s = format!(
"-l {} -nb {} -nf {} -sb {} -m {}",
self.n_lines,
self.bg_color.as_rgb_hex_string(),
self.fg_color.as_rgb_hex_string(),
self.selected_color.as_rgb_hex_string(),
screen_index,
);
if self.password_input {
s.push_str(" -P");
}
let mut flags = s
.split_whitespace()
.map(|s| s.into())
.collect::<Vec<String>>();
if !prompt.is_empty() {
flags.append(&mut vec!["-p".into(), prompt.into()]);
}
flags
}
}
#[derive(Debug, Clone)]
pub struct DMenu {
config: DMenuConfig,
prompt: String,
choices: Vec<String>,
}
impl DMenu {
pub fn new(
prompt: impl Into<String>,
choices: Vec<impl Into<String>>,
config: DMenuConfig,
) -> Self {
Self {
prompt: prompt.into(),
choices: choices.into_iter().map(|s| s.into()).collect(),
config,
}
}
pub fn run(&self, screen_index: usize) -> Result<MenuMatch> {
let args = self.config.flags(&self.prompt, screen_index);
let choices = if self.config.show_line_numbers {
self.choices
.iter()
.enumerate()
.map(|(i, s)| format!("{:<3} {}", i, s))
.collect::<Vec<String>>()
.join("\n")
} else {
self.choices.join("\n")
};
let mut proc = Command::new("dmenu")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.args(args)
.spawn()?;
{
let mut stdin = proc
.stdin
.take()
.ok_or_else(|| perror!("unable to open stdin"))?;
stdin.write_all(choices.as_bytes())?;
}
let mut raw = String::new();
proc.stdout
.ok_or_else(|| PenroseError::SpawnProc("failed to spawn dmenu".into()))?
.read_to_string(&mut raw)?;
let choice = raw.trim();
if choice.is_empty() {
return Ok(MenuMatch::NoMatch);
}
Ok(self
.choices
.iter()
.enumerate()
.find(|(i, s)| {
if self.config.show_line_numbers {
format!("{:<3} {}", i, s) == choice
} else {
*s == choice
}
})
.map_or_else(
|| MenuMatch::UserInput(choice.to_string()),
|(i, _)| MenuMatch::Line(i, self.choices[i].to_string()),
))
}
}