use crate::{
configfiles::{self, ConfigFile, ConfigFileImpl},
errors::*,
layouts::Layout as ManagedLayout,
types::*,
};
use i3ipc::I3Connection;
use lazy_static::lazy_static;
use std::{
ffi::{OsStr, OsString},
fs::File,
io::{prelude::*, BufReader},
ops::Deref,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
time::Duration,
};
use tempfile::NamedTempFile;
use wait_timeout::ChildExt;
lazy_static! {
static ref PROJECTS_PREFIX: OsString = OsString::from("projects");
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Project {
configfile: ConfigFileImpl,
pub name: String,
pub path: PathBuf,
config: Option<Config>,
}
impl Deref for Project {
type Target = ConfigFileImpl;
fn deref(&self) -> &ConfigFileImpl {
&self.configfile
}
}
impl Project {
fn from_configfile(configfile: ConfigFileImpl) -> Self {
let name = configfile.name.to_owned();
let path = configfile.path.clone();
Project {
configfile,
name,
path,
config: None,
}
}
fn load(&self) -> Result<Config> {
let mut file = BufReader::new(File::open(&self.path)?);
let mut contents = String::new();
file.read_to_string(&mut contents)?;
toml::from_str::<Config>(&contents).map_err(|e| e.into())
}
pub fn config(&mut self) -> Result<&Config> {
if self.config.is_none() {
self.config = Some(self.load()?);
}
Ok(self.config.as_ref().unwrap())
}
pub fn start(
&mut self,
i3: &mut I3Connection,
working_directory: Option<&OsStr>,
workspace: Option<&str>,
) -> Result<()> {
let config = self.config()?;
let general = &config.general;
let mut tempfile;
let managed_layout_path;
let path: &Path = match general.layout {
Layout::Contents(ref contents) => {
tempfile = NamedTempFile::new()?;
tempfile.write_all(contents.as_bytes())?;
tempfile.flush()?;
tempfile.path()
}
Layout::Managed(ref name) => {
managed_layout_path = ManagedLayout::open(&name)?.path;
&managed_layout_path
}
Layout::Path(ref path) => path,
};
let workspace = workspace
.map(Into::into)
.or_else(|| general.workspace.as_ref().cloned());
if let Some(ref workspace) = workspace {
i3.run_command(&format!("workspace {}", workspace))?;
}
i3.run_command(&format!(
"append_layout {}",
path.to_str()
.ok_or_else(|| ErrorKind::InvalidUtF8Path(path.to_string_lossy().into_owned()))?
))?;
let applications = &config.applications;
for application in applications {
let mut cmd = Command::new(&application.command.program);
cmd.args(&application.command.args);
let working_directory = working_directory
.map(OsStr::to_os_string)
.or_else(|| application.working_directory.as_ref().map(OsString::from))
.or_else(|| general.working_directory.as_ref().map(OsString::from));
if let Some(working_directory) = working_directory {
cmd.current_dir(working_directory);
}
let child = cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
if let Some(ref exec) = application.exec {
exec_commands(&child, exec)?;
}
}
Ok(())
}
}
impl ConfigFile for Project {
fn create<S: AsRef<OsStr> + ?Sized>(name: &S) -> Result<Self> {
let configfile = ConfigFileImpl::create(PROJECTS_PREFIX.as_os_str(), name.as_ref())?;
Ok(Project::from_configfile(configfile))
}
fn create_from_template<S: AsRef<OsStr> + ?Sized>(name: &S, template: &[u8]) -> Result<Self> {
let configfile = ConfigFileImpl::create_from_template(
PROJECTS_PREFIX.as_os_str(),
name.as_ref(),
template,
)?;
Ok(Project::from_configfile(configfile))
}
fn from_path<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self> {
let configfile = ConfigFileImpl::from_path(path)?;
Ok(Project::from_configfile(configfile))
}
fn open<S: AsRef<OsStr> + ?Sized>(name: &S) -> Result<Self> {
let configfile = ConfigFileImpl::open(PROJECTS_PREFIX.as_os_str(), name.as_ref())?;
Ok(Project::from_configfile(configfile))
}
fn copy<S: AsRef<OsStr> + ?Sized>(&self, new_name: &S) -> Result<Self> {
let configfile = self.configfile.copy(new_name)?;
Ok(Project::from_configfile(configfile))
}
fn delete(&self) -> Result<()> {
self.configfile.delete()?;
Ok(())
}
fn rename<S: AsRef<OsStr> + ?Sized>(&self, new_name: &S) -> Result<Self> {
let configfile = self.configfile.rename(new_name)?;
Ok(Project::from_configfile(configfile))
}
fn verify(&self) -> Result<()> {
let config = self.load()?;
let mut paths: Vec<&Path> = vec![];
if let Some(ref p) = config.general.working_directory {
paths.push(p);
}
match config.general.layout {
Layout::Contents(_) => (),
Layout::Managed(ref name) => {
ManagedLayout::open(name)?;
}
Layout::Path(ref path) => paths.push(path),
}
for application in &config.applications {
if let Some(ref p) = application.working_directory {
paths.push(p);
}
}
for path in paths {
if !path.exists() {
return Err(ErrorKind::PathDoesntExist(path.to_string_lossy().into_owned()).into());
}
}
Ok(())
}
fn list() -> Vec<OsString> {
configfiles::list(&*PROJECTS_PREFIX)
}
fn name(&self) -> String {
self.name.to_owned()
}
fn path(&self) -> PathBuf {
self.path.to_owned()
}
fn prefix() -> &'static OsStr {
&*PROJECTS_PREFIX
}
}
pub fn list() -> Vec<OsString> {
configfiles::list(&*PROJECTS_PREFIX)
}
fn exec_text(base_parameters: &[&str], text: &str, timeout: Duration) -> Result<()> {
let args = &[base_parameters, &["type", "--window", "%1", text]].concat();
let mut child = Command::new("xdotool")
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
if child.wait_timeout(timeout)?.is_none() {
child.kill()?;
child.wait()?;
Err(ErrorKind::TextOrKeyInputFailed.into())
} else {
Ok(())
}
}
fn exec_keys<S: AsRef<OsStr>>(
base_parameters: &[&str],
keys: &[S],
timeout: Duration,
) -> Result<()> {
let args = &[base_parameters, &["key", "--window", "%1"]].concat();
let mut child = Command::new("xdotool")
.args(args)
.args(keys)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
if child.wait_timeout(timeout)?.is_none() {
child.kill()?;
child.wait()?;
Err(ErrorKind::TextOrKeyInputFailed.into())
} else {
Ok(())
}
}
fn exec_commands(child: &Child, exec: &Exec) -> Result<()> {
let timeout = exec.timeout;
let pid = child.id().to_string();
let base_parameters = &[
"search",
"--sync",
"--onlyvisible",
"--any",
"--pid",
&pid,
"ignorepattern",
"windowfocus",
"--sync",
"%1",
];
let commands = &exec.commands;
match exec.exec_type {
ExecType::Text => {
for command in commands {
exec_text(base_parameters, command, timeout)?;
exec_keys(base_parameters, &["Return"], timeout)?;
}
}
ExecType::TextNoReturn => {
for command in commands {
exec_text(base_parameters, command, timeout)?;
}
}
ExecType::Keys => exec_keys(base_parameters, commands.as_slice(), timeout)?,
}
Ok(())
}