#![feature(doc_cfg)]
use std::io::{Read, Write};
use std::path::PathBuf;
#[cfg(feature = "config")]
use std::path::Path;
use std::process::{Command, Stdio};
#[cfg(feature = "config")]
mod config;
const NEWLINE: u8 = b'\n';
pub trait Item {
fn key_len(&self) -> usize;
fn line(&self, key_len: usize) -> Vec<u8>;
}
impl<T, U> Item for (T, U)
where
T: AsRef<str>,
U: AsRef<str>,
{
fn key_len(&self) -> usize {
self.0.as_ref().chars().count()
}
fn line(&self, key_len: usize) -> Vec<u8> {
format!(
"{:kwidth$} {}\n",
&self.0.as_ref(),
&self.1.as_ref(),
kwidth = key_len
)
.into_bytes()
}
}
impl Item for &str {
fn key_len(&self) -> usize {
0
}
fn line(&self, _: usize) -> Vec<u8> {
self.as_bytes().to_vec()
}
}
pub struct Dmx {
pub dmenu: PathBuf,
pub font: String,
pub normal_bg: String,
pub normal_fg: String,
pub select_bg: String,
pub select_fg: String,
}
impl std::default::Default for Dmx {
fn default() -> Self {
Dmx {
dmenu: "dmenu".into(),
font: "LiberationMono-12".to_owned(),
normal_bg: "#222".to_owned(),
normal_fg: "#aaa".to_owned(),
select_bg: "#888".to_owned(),
select_fg: "#aff".to_owned(),
}
}
}
impl Dmx {
fn cmd(&self, prompt: &str, n_items: usize) -> Command {
let mut c = Command::new(&self.dmenu);
c.args([
"-l",
&n_items.to_string(),
"-p",
prompt,
"-fn",
&self.font,
"-nb",
&self.normal_bg,
"-nf",
&self.normal_fg,
"-sb",
&self.select_bg,
"-sf",
&self.select_fg,
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
c
}
pub fn select<S, I>(&self, prompt: S, items: &[I]) -> Result<Option<usize>, String>
where
S: AsRef<str>,
I: Item,
{
let klen: usize = items.iter().map(|x| x.key_len()).max().unwrap_or(0);
let output: Vec<Vec<u8>> = items
.iter()
.map(|x| {
let mut v = x.line(klen);
if Some(&NEWLINE) == v.last() {
v
} else {
v.push(NEWLINE);
v
}
})
.collect();
let mut child = self
.cmd(prompt.as_ref(), output.len())
.spawn()
.map_err(|e| format!("Unable to launch dmenu: {}", &e))?;
{
let mut stdin = child.stdin.take().unwrap();
for line in output.iter() {
stdin
.write_all(line)
.map_err(|e| format!("Error writing to dmenu subprocess: {}", &e))?;
}
stdin
.flush()
.map_err(|e| format!("Error writing to dmenu subprocess: {}", &e))?;
}
let mut stdout = child.stdout.take().unwrap();
child
.wait()
.map_err(|e| format!("dmenu subprocess returned error: {}", &e))?;
let mut choice_bytes: Vec<u8> = Vec::new();
let _ = stdout
.read_to_end(&mut choice_bytes)
.map_err(|e| format!("Error reading dmenu output: {}", &e))?;
for (n, line) in output.iter().enumerate() {
if *line == choice_bytes {
return Ok(Some(n));
}
}
Ok(None)
}
#[doc(cfg(feature = "config"))]
#[cfg(feature = "config")]
pub fn from_bytes(bytes: &[u8]) -> Result<Dmx, String> {
let cfgf = config::ConfigFile::from(&bytes)?;
let mut dmx = Dmx::default();
if let Some(dmenu_path) = cfgf.dmenu {
dmx.dmenu = dmenu_path;
}
if let Some(font) = cfgf.font {
dmx.font = font;
}
if let Some(nbg) = cfgf.normal_bg {
dmx.normal_bg = nbg;
}
if let Some(nfg) = cfgf.normal_fg {
dmx.normal_fg = nfg;
}
if let Some(sbg) = cfgf.select_bg {
dmx.select_bg = sbg;
}
if let Some(sfg) = cfgf.select_fg {
dmx.select_fg = sfg;
}
Ok(dmx)
}
#[doc(cfg(feature = "config"))]
#[cfg(feature = "config")]
pub fn from_file<P>(p: P) -> Result<Dmx, String>
where
P: AsRef<Path>,
{
let p = p.as_ref();
let bytes = std::fs::read(p)
.map_err(|e| format!("Error reading from \"{}\": {}", p.display(), &e))?;
Dmx::from_bytes(&bytes)
}
#[doc(cfg(feature = "config"))]
#[cfg(feature = "config")]
pub fn from_slice(b: &[u8]) -> Result<Dmx, String> {
Dmx::from_bytes(b)
}
#[doc(cfg(feature = "config"))]
#[cfg(feature = "config")]
pub fn automagiconf() -> Dmx {
use std::env::var;
if let Ok(path) = var("DMX_CONFIG") {
if let Ok(dmx) = Dmx::from_file(path) {
return dmx;
}
}
if let Ok(config_path) = var("XDG_CONFIG_HOME") {
let mut config_file = PathBuf::from(config_path);
config_file.push("dmx.toml");
if let Ok(dmx) = Dmx::from_file(&config_file) {
return dmx;
}
}
if let Ok(home_dir) = var("HOME") {
let mut config_file = PathBuf::from(home_dir);
config_file.push(".config");
config_file.push("dmx.toml");
if let Ok(dmx) = Dmx::from_file(&config_file) {
return dmx;
}
}
Dmx::default()
}
}
#[cfg(test)]
mod tests;