use crate::commands::Type;
use regex::{Regex, RegexBuilder};
use std::io::prelude::Write;
use std::process::Command;
use std::str::FromStr;
use std::str::Lines;
use std::{error::Error, fmt};
use term::StdoutTerminal;
#[derive(Debug, PartialEq)]
pub enum EntityType {
Sink,
Module,
Source,
Input,
SinkInput,
Client,
Card,
Unknown,
}
impl FromStr for EntityType {
type Err = PulseError;
fn from_str(input: &str) -> Result<EntityType, Self::Err> {
match input {
"Sink" => Ok(EntityType::Sink),
"Module" => Ok(EntityType::Module),
"Source" => Ok(EntityType::Source),
"Input" => Ok(EntityType::Input),
"Sink Input" => Ok(EntityType::SinkInput),
"Client" => Ok(EntityType::Client),
"Card" => Ok(EntityType::Card),
_ => Ok(EntityType::Unknown),
}
}
}
struct Entity {
id: String,
state: String,
name: String,
description: String,
driver: String,
mute: String,
volume: String,
}
#[derive(Debug)]
pub struct PulseError {
message: String,
}
impl Error for PulseError {}
impl fmt::Display for PulseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Your pulse request failed: {}", &self.message)
}
}
fn list_sinks() -> String {
let output = Command::new(Type::Pactl.to_string())
.arg("list")
.output()
.unwrap();
if !output.status.success() {
println!("error");
}
String::from_utf8(output.stdout).unwrap()
}
fn get_search_pattern(search_value: String, case_sensitive: bool) -> Result<Regex, regex::Error> {
RegexBuilder::new(&format!(".*{}.*", search_value))
.case_insensitive(!case_sensitive)
.build()
}
pub fn search(
search_key: String,
search_value: String,
case_sensitive: bool,
) -> Result<String, PulseError> {
let pattern = get_search_pattern(search_value, case_sensitive)
.expect("Something went wrong building the regex");
for group in list_sinks().split_terminator("\n\n") {
match find(group, search_key.to_owned(), pattern.to_owned()) {
Ok(id) => return Ok(id),
_ => continue,
}
}
Err(PulseError {
message: "Search failed".to_owned(),
})
}
fn find(group: &str, search_key: String, pattern: Regex) -> Result<String, PulseError> {
let mut lines = group.lines();
let mut first_line = lines.next().unwrap().split(" #");
let group_type = EntityType::from_str(first_line.next().unwrap())?;
let id = String::from(first_line.next().unwrap());
if group_type != EntityType::Sink {
return Err(PulseError {
message: "Not a Sink".to_owned(),
});
}
for line in lines {
let mut split_line = line.split(": ");
let key = split_line.next().unwrap().trim();
let value = split_line.next().unwrap_or("");
if key == search_key && pattern.is_match(value) {
return Ok(id);
}
}
Err(PulseError {
message: "Not matched".to_owned(),
})
}
pub fn list() -> Result<(), PulseError> {
let mut t = term::stdout().unwrap();
writeln!(t).unwrap();
for group in list_sinks().split_terminator("\n\n") {
let mut lines = group.lines();
let mut first_line = lines.next().unwrap().split(" #");
let group_type = EntityType::from_str(first_line.next().unwrap())?;
let id = String::from(first_line.next().unwrap());
if group_type != EntityType::Sink {
continue;
}
let sink = Entity {
id,
state: pull_data(&mut lines, "State".to_string())?,
name: pull_data(&mut lines, "Name".to_string())?,
description: pull_data(&mut lines, "Description".to_string())?,
driver: pull_data(&mut lines, "Driver".to_string())?,
mute: pull_data(&mut lines, "Mute".to_string())?,
volume: pull_data(&mut lines, "Volume".to_string())?,
};
print_attribute(&mut t, " ID", &sink.id);
print_attribute(&mut t, "Description", &sink.description);
print_attribute(&mut t, " Name", &sink.name);
print_attribute(&mut t, " State", &sink.state);
print_attribute(&mut t, " Driver", &sink.driver);
print_attribute(&mut t, " Mute", &sink.mute);
print_attribute(&mut t, " Volume", &sink.volume);
writeln!(t).unwrap();
}
Ok(())
}
fn pull_data(lines: &mut Lines, search_key: String) -> Result<String, PulseError> {
for line in lines {
let mut split_line = line.split(": ");
let key = split_line.next().unwrap().trim();
let value = split_line.next().unwrap_or("");
if key == search_key {
return Ok(value.to_string());
}
}
Err(PulseError {
message: "Could not find data from pactl".to_owned(),
})
}
fn print_attribute(t: &mut Box<StdoutTerminal>, key: &str, value: &str) {
t.attr(term::Attr::Bold).unwrap();
write!(t, "{}: ", key).unwrap();
t.reset().unwrap();
writeln!(t, "{}", value).unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_find_by_description() {
let search_value = "Fiio".to_string();
let pattern = Regex::new(&format!(".*{}.*", search_value).to_owned()).unwrap();
let contents = fs::read_to_string("src/test/data/pactl-fiio.txt")
.expect("Something went wrong reading the file");
assert_eq!(
find(&contents, "Description".to_string(), pattern).unwrap(),
"43"
)
}
#[test]
fn test_get_search_pattern_case_sensitive() {
assert!(get_search_pattern("test".to_string(), true)
.unwrap()
.is_match("test"))
}
#[test]
fn test_get_search_pattern_case_sensitive_with_capitals() {
assert!(!get_search_pattern("Test".to_string(), true)
.unwrap()
.is_match("test"))
}
#[test]
fn test_get_search_pattern_case_insensitive() {
assert!(get_search_pattern("test".to_string(), false)
.unwrap()
.is_match("Test"))
}
#[test]
fn test_get_search_pattern_case_insensitive_with_capitals() {
assert!(get_search_pattern("Test".to_string(), false)
.unwrap()
.is_match("test"))
}
}