for Commands {
345 | fn from(command: Layout) -> Self {
346 | Commands::Layout(command)
347 | }
348 | }
349 |
350 | impl From for Commands {
351 | fn from(command: Pre) -> Self {
352 | Commands::Pre(command)
353 | }
354 | }
355 |
356 | impl From for Commands {
357 | fn from(command: SelectPane) -> Self {
358 | Commands::SelectPane(command)
359 | }
360 | }
361 |
362 | impl From for Commands {
363 | fn from(command: SelectWindow) -> Self {
364 | Commands::SelectWindow(command)
365 | }
366 | }
367 |
368 | impl From for Commands {
369 | fn from(command: SendKeys) -> Self {
370 | Commands::SendKeys(command)
371 | }
372 | }
373 |
374 | impl From for Commands {
375 | fn from(command: Session) -> Self {
376 | Commands::Session(command)
377 | }
378 | }
379 |
380 | impl From for Commands {
381 | fn from(command: Split) -> Self {
382 | Commands::Split(command)
383 | }
384 | }
385 |
386 | impl From for Commands {
387 | fn from(command: SwitchClient) -> Self {
388 | Commands::SwitchClient(command)
389 | }
390 | }
391 |
392 | impl From for Commands {
393 | fn from(command: Window) -> Self {
394 | Commands::Window(command)
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/load/src/lib.rs:
--------------------------------------------------------------------------------
1 | extern crate dirs;
2 | extern crate libc;
3 | extern crate yaml_rust;
4 |
5 | extern crate common;
6 |
7 | pub mod command;
8 | pub mod project;
9 | pub mod tmux;
10 |
11 | use args::Args;
12 | use command::Commands;
13 | use common::project_paths::project_paths;
14 | use common::{args, first_run};
15 | use project::parser;
16 | use tmux::config::Config;
17 |
18 | pub fn exec(args: Args) -> Result<(), String> {
19 | let project_paths = project_paths(&args);
20 |
21 | // FIXME: Should have an error message
22 | let yaml = project::read(&args.arg_project, &project_paths).unwrap();
23 | let project_name = &yaml[0]["name"]
24 | .as_str()
25 | .unwrap_or(&args.arg_project)
26 | .to_string();
27 |
28 | let commands: Vec;
29 | match project::session_exists(project_name) {
30 | Some(c) => {
31 | commands = vec![c];
32 | }
33 | None => {
34 | let config = Config::from_string(tmux::get_config());
35 | commands = parser::call(&yaml, project_name, args.flag_d, &config)
36 | .expect("Couldn't parse commands");
37 | }
38 | };
39 |
40 | if args.flag_debug {
41 | println!("{:?}", &commands);
42 | };
43 |
44 | for command in &commands {
45 | command
46 | .as_trait()
47 | .call(args.flag_debug)
48 | .map_err(|e| format!("Had a problem running commands for tmux {}", e))?;
49 | }
50 |
51 | Ok(())
52 | }
53 |
--------------------------------------------------------------------------------
/load/src/project/mod.rs:
--------------------------------------------------------------------------------
1 | //! The project module takes care of muxed related initialization. Locating the
2 | //! users home directory. Finding the desired config files, and reading the
3 | //! configs in.
4 | pub mod parser;
5 |
6 | use command::{Attach, Commands, SwitchClient};
7 | use common::project_paths::ProjectPaths;
8 | use first_run::check_first_run;
9 | use std::env;
10 | use std::fs::File;
11 | use std::io::prelude::*;
12 | use tmux::has_session;
13 | use yaml_rust::{Yaml, YamlLoader};
14 |
15 | static TMUX_ENV_VAR: &str = "TMUX";
16 |
17 | /// Using the provided project name, locate the path to that project file. It
18 | /// should be something similar to: `~/.muxed/my_project.yml`
19 | /// Read in the contents of the config (which should be Yaml), and parse the
20 | /// contents as yaml.
21 | ///
22 | /// `project_name`: The name of the project, corresponding to the project config
23 | /// file.
24 | /// `project_paths`: The struct of paths
25 | ///
26 | /// # Examples
27 | ///
28 | /// Given the project name "compiler" and a project file found at:
29 | /// `~/.muxed/compiler.yml`.
30 | ///
31 | /// ```rust,no_run
32 | /// extern crate common;
33 | /// extern crate load;
34 | /// extern crate yaml_rust;
35 | ///
36 | /// use common::project_paths::ProjectPaths;
37 | /// use load::project::read;
38 | /// use std::path::PathBuf;
39 | /// use yaml_rust::{Yaml, YamlLoader};
40 | ///
41 | /// let paths = ProjectPaths::new(
42 | /// PathBuf::from("/tmp"),
43 | /// PathBuf::from("/tmp/.muxed"),
44 | /// PathBuf::from("/tmp/.muxed/projectname.yml")
45 | /// );
46 | ///
47 | /// let yaml: Result, String> = read("compiler", &paths);
48 | ///
49 | /// assert!(yaml.is_ok());
50 | /// ```
51 | pub fn read(project_name: &str, project_paths: &ProjectPaths) -> Result, String> {
52 | check_first_run(&project_paths.project_directory)?;
53 |
54 | let mut file = File::open(&project_paths.project_file).map_err(|e| format!("No project configuration file was found with the name `{}` in the directory `{}`. Received error: {}", project_name, &project_paths.project_directory.display(), e.to_string()))?;
55 | let mut contents = String::new();
56 |
57 | file.read_to_string(&mut contents)
58 | .map_err(|e| e.to_string())?;
59 |
60 | let parsed_yaml = YamlLoader::load_from_str(&contents).map_err(|e| e.to_string())?;
61 |
62 | Ok(parsed_yaml)
63 | }
64 |
65 | /// Find out if a tmux session is already active with this name. If it is active
66 | /// return `Some` with a command to attach to the session. If a
67 | /// session is not active return None and let the app carry on.
68 | pub fn session_exists(project_name: &str) -> Option {
69 | if has_session(project_name).success() {
70 | Some(open(project_name))
71 | } else {
72 | None
73 | }
74 | }
75 |
76 | /// Check to see how we want to open the project. Do we need to attach to a new
77 | /// tmux session or can we switch the client from a running session.
78 | ///
79 | /// # Examples
80 | ///
81 | /// ```rust
82 | /// extern crate load;
83 | ///
84 | /// use load::command::{Attach, Commands, Command};
85 | /// use load::project::open;
86 | ///
87 | /// let correct_type = match open("muxed") {
88 | /// Commands::Attach(_) => true,
89 | /// _ => false,
90 | /// };
91 | ///
92 | /// assert!(correct_type)
93 | /// ```
94 | pub fn open(project_name: &str) -> Commands {
95 | if env::var_os(TMUX_ENV_VAR).is_some() {
96 | SwitchClient::new(&project_name).into()
97 | } else {
98 | Attach::new(&project_name, None).into()
99 | }
100 | }
101 |
102 | #[cfg(test)]
103 | mod test {
104 | use super::*;
105 | use common::rand_names;
106 | use std::fs;
107 |
108 | #[test]
109 | fn missing_file_returns_err() {
110 | let project_paths = ProjectPaths::from_strs("/tmp", ".muxed", "");
111 | let result = read(&String::from("not_a_file"), &project_paths);
112 | assert!(result.is_err())
113 | }
114 |
115 | #[test]
116 | fn poorly_formatted_file_returns_err() {
117 | let name = rand_names::project_file_name();
118 | let project_paths = ProjectPaths::from_strs("/tmp", ".muxed", &name);
119 |
120 | let _ = fs::create_dir(&project_paths.project_directory);
121 | let mut buffer = File::create(&project_paths.project_file).unwrap();
122 | let _ = buffer.write(b"mix: [1,2,3]: muxed");
123 | let _ = buffer.sync_all();
124 |
125 | let result = read(&name, &project_paths);
126 | let _ = fs::remove_file(&project_paths.project_file);
127 | assert!(result.is_err());
128 | }
129 |
130 | #[test]
131 | fn good_file_returns_ok() {
132 | let name = rand_names::project_file_name();
133 | let project_paths = ProjectPaths::from_strs("/tmp", ".muxed", &name);
134 |
135 | let _ = fs::create_dir(&project_paths.project_directory);
136 | let mut buffer = File::create(&project_paths.project_file).unwrap();
137 | let _ = buffer.write(
138 | b"---
139 | windows: ['cargo', 'vim', 'git']
140 | ",
141 | );
142 | let _ = buffer.sync_all();
143 |
144 | let result = read(&name, &project_paths);
145 | let _ = fs::remove_file(&project_paths.project_file);
146 | assert!(result.is_ok());
147 | }
148 |
149 | #[test]
150 | fn open_returns_attach_in_bare_context() {
151 | let attach_command = match open("muxed") {
152 | Commands::Attach(_) => true,
153 | _ => false,
154 | };
155 |
156 | assert!(attach_command);
157 | }
158 |
159 | #[test]
160 | fn open_returns_switch_client_in_nested_context() {
161 | let _ = env::set_var(TMUX_ENV_VAR, "somestring");
162 | let switch_command = match open("muxed") {
163 | Commands::SwitchClient(_) => true,
164 | _ => false,
165 | };
166 | let _ = env::remove_var(TMUX_ENV_VAR);
167 |
168 | assert!(switch_command);
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/load/src/project/parser.rs:
--------------------------------------------------------------------------------
1 | //! The YAML parser. Here is where we convert the yaml in to commands to be
2 | //! processed later.
3 |
4 | use command::*;
5 | use dirs::home_dir;
6 | use project;
7 | use std::path::{Path, PathBuf};
8 | use std::rc::Rc;
9 | use tmux::config::Config;
10 | use tmux::target::*;
11 | use yaml_rust::Yaml;
12 |
13 | /// Here was pass in the parsed yaml and project name. The purpose of this call
14 | /// loop is to build the stack of commands that are run to setup a users tmux
15 | /// session.
16 | ///
17 | /// `yaml_string`: The parsed yaml from the config file.
18 | /// `project_name`: The name of the project.
19 | pub fn call<'a>(
20 | yaml_string: &'a [Yaml],
21 | project_name: &'a str,
22 | daemonize: bool,
23 | tmux_config: &Config,
24 | ) -> Result, String> {
25 | let mut commands: Vec = vec![];
26 | let project_name = Rc::new(project_name);
27 |
28 | // There should only be one doc but it's a vec so take the first.
29 | let doc = &yaml_string[0];
30 |
31 | let root = expand_path(&doc["root"]);
32 | let pre_window = pre_matcher(&doc["pre_window"]);
33 |
34 | // A clojure used to capture the current local root and pre Options.
35 | // This way we can call the clojure to create common SendKeys command
36 | // like changing the directory or executing a system command from the
37 | // `pre_window` option.
38 | let common_commands = |target: Target| -> Vec {
39 | let mut commands2 = vec![];
40 |
41 | // SendKeys for the Pre option
42 | if let Some(p) = pre_window.clone() {
43 | for v in &p {
44 | if let Some(ref r) = *v {
45 | commands2.push(SendKeys::new(target.clone(), r.clone()).into());
46 | };
47 | }
48 | };
49 |
50 | commands2
51 | };
52 |
53 | let windows = doc["windows"]
54 | .as_vec()
55 | .expect("No Windows have been defined.");
56 |
57 | for window in windows.iter() {
58 | match *window {
59 | Yaml::Hash(ref h) => {
60 | for (k, v) in h {
61 | if v.as_hash().is_some() {
62 | let path = match expand_path(&v["path"]) {
63 | Some(x) => Some(x),
64 | None => root.clone(),
65 | };
66 |
67 | commands.push(
68 | Window::new(
69 | &project_name,
70 | Rc::new(k.as_str().expect("window should have a name").to_string()),
71 | path.clone(),
72 | )
73 | .into(),
74 | );
75 |
76 | let target = WindowTarget::new(
77 | Rc::clone(&project_name),
78 | k.as_str().ok_or_else(|| "no target specified")?,
79 | );
80 | commands.append(&mut pane_matcher(
81 | v,
82 | &target,
83 | &common_commands,
84 | &tmux_config,
85 | path.clone(),
86 | )?);
87 | } else {
88 | commands.push(Window::new(
89 | &project_name,
90 | Rc::new(k.as_str().ok_or_else(|| {
91 | "Windows require being named in your config."
92 | })?
93 | .to_string()),
94 | root.clone()
95 | ).into());
96 |
97 | let target = WindowTarget::new(Rc::clone(&project_name), k.as_str().unwrap());
98 | commands.append(&mut common_commands(Target::WindowTarget(target.clone())));
99 |
100 | // SendKeys for the exec command
101 | if let Some(ex) = v.as_str() {
102 | if !ex.is_empty() {
103 | commands.push(
104 | SendKeys::new(
105 | Target::WindowTarget(target.clone()),
106 | v.as_str().unwrap().to_string(),
107 | )
108 | .into(),
109 | );
110 | };
111 | }
112 | }
113 | }
114 | }
115 | Yaml::String(ref s) => {
116 | commands
117 | .push(Window::new(&project_name, Rc::new(s.to_string()), root.clone()).into());
118 |
119 | let target = WindowTarget::new(Rc::clone(&project_name), &s);
120 | commands.append(&mut common_commands(Target::WindowTarget(target)));
121 | }
122 | Yaml::Integer(ref s) => {
123 | commands.push(
124 | Window::new(&project_name, Rc::new(format!("{}", s)), root.clone()).into(),
125 | );
126 |
127 | let target = WindowTarget::new(Rc::clone(&project_name), &s.to_string());
128 | commands.append(&mut common_commands(Target::WindowTarget(target)));
129 | }
130 | _ => panic!("Muxed config file formatting isn't recognized."),
131 | };
132 | }
133 |
134 | let (first, commands1) = commands.split_first().unwrap();
135 | let mut remains = commands1.to_vec();
136 |
137 | if let Commands::Window(ref w) = &first {
138 | remains.insert(
139 | 0,
140 | Session::new(&project_name, Rc::clone(&w.name), root.clone()).into(),
141 | );
142 |
143 | if let Some(path) = &w.path {
144 | remains.insert(
145 | 1,
146 | SendKeys::new(
147 | Target::WindowTarget(WindowTarget::new(Rc::clone(&project_name), &w.name)),
148 | format!("cd {}", path.display()),
149 | )
150 | .into(),
151 | );
152 | }
153 |
154 | remains.push(SelectWindow::new(WindowTarget::new(Rc::clone(&project_name), &w.name)).into());
155 | remains.push(
156 | SelectPane::new(PaneTarget::new(
157 | &project_name,
158 | &w.name,
159 | tmux_config.base_index,
160 | ))
161 | .into(),
162 | );
163 | };
164 |
165 | // FIXME: Due to inserting the Pre commands into the 0 position in the stack,
166 | // if pre is defined as an array, it is executed in reverse order.
167 | let pre = pre_matcher(&doc["pre"]);
168 | if let Some(ref p) = pre {
169 | for v in p.iter() {
170 | if let Some(ref r) = *v {
171 | remains.insert(0, Pre::new(r.clone()).into());
172 | };
173 | }
174 | };
175 |
176 | if !daemonize {
177 | remains.push(project::open(&project_name).into());
178 | };
179 |
180 | Ok(remains)
181 | }
182 |
183 | /// Pane matcher is for breaking apart the panes. Splitting windows when needed
184 | /// and executing commands as needed.
185 | fn pane_matcher<'a, T>(
186 | window: &'a Yaml,
187 | target: &WindowTarget,
188 | common_commands: T,
189 | tmux_config: &Config,
190 | inherited_path: Option>,
191 | ) -> Result, String>
192 | where
193 | T: Fn(Target) -> Vec,
194 | {
195 | let mut commands = vec![];
196 | let panes = window["panes"]
197 | .as_vec()
198 | .expect("Something is wrong with panes.");
199 |
200 | let path = match expand_path(&window["path"]) {
201 | Some(x) => Some(x),
202 | None => inherited_path,
203 | };
204 |
205 | for (i, pane) in panes.iter().enumerate() {
206 | let pt = PaneTarget::new(
207 | &target.session,
208 | &target.window,
209 | i + tmux_config.pane_base_index,
210 | );
211 | // For every pane, we need one less split.
212 | // ex. An existing window to become 2 panes, needs 1 split.
213 | if i < (panes.len() - 1) {
214 | commands.push(Split::new(pt.clone(), path.clone()).into());
215 | };
216 |
217 | // Call the common_commands clojure to execute `cd` and `pre_window` options in
218 | // pane splits.
219 | commands.append(&mut common_commands(Target::PaneTarget(pt.clone())));
220 |
221 | // Execute given commands in each new pane after all splits are
222 | // complete.
223 | if let Some(p) = pane.as_str() {
224 | if !p.is_empty() {
225 | commands.push(SendKeys::new(Target::PaneTarget(pt.clone()), p.to_string()).into());
226 | };
227 | };
228 | }
229 |
230 | // After all panes are split select the layout for the window
231 | if window["layout"].as_str().is_some() {
232 | let err = format!(
233 | "A problem with the specified layout for the window: {}",
234 | target
235 | );
236 | let layout = window["layout"].as_str().expect(&err);
237 | commands.push(Layout::new(target.clone(), layout.to_string()).into());
238 | };
239 |
240 | Ok(commands)
241 | }
242 |
243 | fn pre_matcher(node: &Yaml) -> Option>> {
244 | match *node {
245 | // See if pre contains an array or a string. If it's an array we
246 | // need to check the values of it again to verify they are strings.
247 | Yaml::String(ref x) => Some(vec![Some(x.to_string())]),
248 | Yaml::Array(ref x) => Some(
249 | x.iter()
250 | .map(|y| match *y {
251 | Yaml::String(ref z) => Some(z.to_string()),
252 | _ => None,
253 | })
254 | .collect(),
255 | ),
256 | _ => None,
257 | }
258 | }
259 |
260 | fn expand_path(node: &Yaml) -> Option> {
261 | match node.as_str() {
262 | Some(string) => Some(if string.contains("~/") {
263 | let home = home_dir().expect("Home dir could not be expanded");
264 | let path = home.join(Path::new(string).strip_prefix("~/").unwrap());
265 | Rc::new(path)
266 | } else {
267 | Rc::new(PathBuf::from(string))
268 | }),
269 | None => None,
270 | }
271 | }
272 |
273 | #[cfg(test)]
274 | mod test {
275 | use super::*;
276 | use yaml_rust::YamlLoader;
277 |
278 | #[test]
279 | pub fn expect_1_session() {
280 | let s = "---
281 | windows: ['cargo', 'vim', 'git']
282 | ";
283 | let yaml = YamlLoader::load_from_str(s).unwrap();
284 | let remains: Vec = call(
285 | &yaml,
286 | "muxed",
287 | false,
288 | &Config {
289 | base_index: 0,
290 | pane_base_index: 0,
291 | },
292 | )
293 | .unwrap()
294 | .into_iter()
295 | .filter(|x| match x {
296 | Commands::Session(_) => true,
297 | _ => false,
298 | })
299 | .collect();
300 |
301 | assert_eq!(remains.len(), 1)
302 | }
303 |
304 | #[test]
305 | pub fn expect_2_windows_from_array() {
306 | let s = "---
307 | windows: ['cargo', 'vim', 'git']
308 | ";
309 | let yaml = YamlLoader::load_from_str(s).unwrap();
310 | let remains: Vec = call(
311 | &yaml,
312 | "muxed",
313 | false,
314 | &Config {
315 | base_index: 0,
316 | pane_base_index: 0,
317 | },
318 | )
319 | .unwrap()
320 | .into_iter()
321 | .filter(|x| match x {
322 | Commands::Window(_) => true,
323 | _ => false,
324 | })
325 | .collect();
326 |
327 | assert_eq!(remains.len(), 2)
328 | }
329 |
330 | #[test]
331 | pub fn expect_1_attach() {
332 | let s = "---
333 | windows: ['cargo', 'vim', 'git']
334 | ";
335 | let yaml = YamlLoader::load_from_str(s).unwrap();
336 | let remains: Vec = call(
337 | &yaml,
338 | "muxed",
339 | false,
340 | &Config {
341 | base_index: 0,
342 | pane_base_index: 0,
343 | },
344 | )
345 | .unwrap()
346 | .into_iter()
347 | .filter(|x| match x {
348 | Commands::Attach(_) => true,
349 | _ => false,
350 | })
351 | .collect();
352 |
353 | assert_eq!(remains.len(), 1)
354 | }
355 |
356 | #[test]
357 | pub fn expect_2_windows_with_mixed_type_names() {
358 | let s = "---
359 | windows: [1, 'vim', 3]
360 | ";
361 | let yaml = YamlLoader::load_from_str(s).unwrap();
362 | let remains: Vec = call(
363 | &yaml,
364 | "muxed",
365 | false,
366 | &Config {
367 | base_index: 0,
368 | pane_base_index: 0,
369 | },
370 | )
371 | .unwrap()
372 | .into_iter()
373 | .filter(|x| match x {
374 | Commands::Window(_) => true,
375 | _ => false,
376 | })
377 | .collect();
378 | assert_eq!(remains.len(), 2)
379 | }
380 |
381 | #[test]
382 | pub fn expect_2_windows_from_list() {
383 | let s = "---
384 | windows:
385 | - cargo: ''
386 | - vim: ''
387 | - git: ''
388 | ";
389 | let yaml = YamlLoader::load_from_str(s).unwrap();
390 | let remains: Vec = call(
391 | &yaml,
392 | "muxed",
393 | false,
394 | &Config {
395 | base_index: 0,
396 | pane_base_index: 0,
397 | },
398 | )
399 | .unwrap()
400 | .into_iter()
401 | .filter(|x| match x {
402 | Commands::Window(_) => true,
403 | _ => false,
404 | })
405 | .collect();
406 | assert_eq!(remains.len(), 2)
407 | }
408 |
409 | #[test]
410 | pub fn expect_ok_with_empty_syscommands() {
411 | let s = "---
412 | windows:
413 | - editor:
414 | ";
415 | let yaml = YamlLoader::load_from_str(s).unwrap();
416 | let result = call(
417 | &yaml,
418 | "muxed",
419 | false,
420 | &Config {
421 | base_index: 0,
422 | pane_base_index: 0,
423 | },
424 | );
425 | assert!(result.is_ok())
426 | }
427 |
428 | #[test]
429 | pub fn expect_no_send_keys_commands() {
430 | let s = "---
431 | windows:
432 | - editor:
433 | ";
434 |
435 | let yaml = YamlLoader::load_from_str(s).unwrap();
436 | let remains: Vec = call(
437 | &yaml,
438 | "muxed",
439 | false,
440 | &Config {
441 | base_index: 0,
442 | pane_base_index: 0,
443 | },
444 | )
445 | .unwrap()
446 | .into_iter()
447 | .filter(|x| match x {
448 | Commands::SendKeys(_) => true,
449 | _ => false,
450 | })
451 | .collect();
452 |
453 | assert_eq!(remains.len(), 0)
454 | }
455 |
456 | #[test]
457 | pub fn expect_err_with_nameless_window() {
458 | let s = "---
459 | windows:
460 | - : ls
461 | ";
462 | let yaml = YamlLoader::load_from_str(s).unwrap();
463 | let result = call(
464 | &yaml,
465 | "muxed",
466 | false,
467 | &Config {
468 | base_index: 0,
469 | pane_base_index: 0,
470 | },
471 | );
472 | assert!(result.is_err())
473 | }
474 |
475 | #[test]
476 | pub fn expect_ok_with_empty_panes_syscommands() {
477 | let s = "---
478 | windows:
479 | - cargo:
480 | layout: 'main-vertical'
481 | panes:
482 | -
483 | ";
484 | let yaml = YamlLoader::load_from_str(s).unwrap();
485 | let result = call(
486 | &yaml,
487 | "muxed",
488 | false,
489 | &Config {
490 | base_index: 0,
491 | pane_base_index: 0,
492 | },
493 | );
494 | assert!(result.is_ok())
495 | }
496 |
497 | #[test]
498 | pub fn expect_no_send_keys_with_empty_panes_syscommands() {
499 | let s = "---
500 | windows:
501 | - editor:
502 | panes:
503 | -
504 | ";
505 |
506 | let yaml = YamlLoader::load_from_str(s).unwrap();
507 | let remains: Vec = call(
508 | &yaml,
509 | "muxed",
510 | false,
511 | &Config {
512 | base_index: 0,
513 | pane_base_index: 0,
514 | },
515 | )
516 | .unwrap()
517 | .into_iter()
518 | .filter(|x| match x {
519 | Commands::SendKeys(_) => true,
520 | _ => false,
521 | })
522 | .collect();
523 |
524 | assert_eq!(remains.len(), 0)
525 | }
526 |
527 | #[test]
528 | pub fn expect_1_split_window() {
529 | let s = "---
530 | windows:
531 | - editor:
532 | layout: 'main-vertical'
533 | panes: ['vim', 'guard']
534 | ";
535 |
536 | let yaml = YamlLoader::load_from_str(s).unwrap();
537 | let remains: Vec = call(
538 | &yaml,
539 | "muxed",
540 | false,
541 | &Config {
542 | base_index: 0,
543 | pane_base_index: 0,
544 | },
545 | )
546 | .unwrap()
547 | .into_iter()
548 | .filter(|x| match x {
549 | Commands::Split(_) => true,
550 | _ => false,
551 | })
552 | .collect();
553 |
554 | assert_eq!(remains.len(), 1)
555 | }
556 |
557 | #[test]
558 | pub fn expect_1_layout() {
559 | let s = "---
560 | windows:
561 | - editor:
562 | layout: 'main-vertical'
563 | panes: ['vim', 'guard']
564 | ";
565 |
566 | let yaml = YamlLoader::load_from_str(s).unwrap();
567 | let remains: Vec = call(
568 | &yaml,
569 | "muxed",
570 | false,
571 | &Config {
572 | base_index: 0,
573 | pane_base_index: 0,
574 | },
575 | )
576 | .unwrap()
577 | .into_iter()
578 | .filter(|x| match x {
579 | Commands::Layout(_) => true,
580 | _ => false,
581 | })
582 | .collect();
583 |
584 | assert_eq!(remains.len(), 1)
585 | }
586 |
587 | #[test]
588 | pub fn expect_1_session_with_panes_array() {
589 | let s = "---
590 | windows:
591 | - editor:
592 | layout: 'main-vertical'
593 | panes: ['vim', 'guard']
594 | ";
595 |
596 | let yaml = YamlLoader::load_from_str(s).unwrap();
597 | let remains: Vec = call(
598 | &yaml,
599 | "muxed",
600 | false,
601 | &Config {
602 | base_index: 0,
603 | pane_base_index: 0,
604 | },
605 | )
606 | .unwrap()
607 | .into_iter()
608 | .filter(|x| match x {
609 | Commands::Session(_) => true,
610 | _ => false,
611 | })
612 | .collect();
613 |
614 | assert_eq!(remains.len(), 1)
615 | }
616 |
617 | #[test]
618 | pub fn expect_no_layout() {
619 | let s = "---
620 | windows:
621 | - editor:
622 | panes: ['vim', 'guard']
623 | ";
624 |
625 | let yaml = YamlLoader::load_from_str(s).unwrap();
626 | let remains: Vec = call(
627 | &yaml,
628 | "muxed",
629 | false,
630 | &Config {
631 | base_index: 0,
632 | pane_base_index: 0,
633 | },
634 | )
635 | .unwrap()
636 | .into_iter()
637 | .filter(|x| match x {
638 | Commands::Layout(_) => true,
639 | _ => false,
640 | })
641 | .collect();
642 |
643 | assert_eq!(remains.len(), 0)
644 | }
645 |
646 | #[test]
647 | pub fn expect_three_send_keys_commands_from_pre_window() {
648 | // pre gets run on all 2 panes and 1 window for a total of 3
649 | let s = "---
650 | pre_window: 'ls'
651 | windows:
652 | - editor:
653 | panes:
654 | -
655 | -
656 | - logs:
657 | ";
658 | let yaml = YamlLoader::load_from_str(s).unwrap();
659 | let remains: Vec = call(
660 | &yaml,
661 | "muxed",
662 | false,
663 | &Config {
664 | base_index: 0,
665 | pane_base_index: 0,
666 | },
667 | )
668 | .unwrap()
669 | .into_iter()
670 | .filter(|x| match x {
671 | Commands::SendKeys(_) => true,
672 | _ => false,
673 | })
674 | .collect();
675 |
676 | assert_eq!(remains.len(), 3)
677 | }
678 |
679 | #[test]
680 | pub fn expect_two_send_keys_commands_from_pre_window() {
681 | let s = "---
682 | pre_window:
683 | - 'ls'
684 | - 'ls'
685 | windows:
686 | - editor:
687 | ";
688 | let yaml = YamlLoader::load_from_str(s).unwrap();
689 | let remains: Vec = call(
690 | &yaml,
691 | "muxed",
692 | false,
693 | &Config {
694 | base_index: 0,
695 | pane_base_index: 0,
696 | },
697 | )
698 | .unwrap()
699 | .into_iter()
700 | .filter(|x| match x {
701 | Commands::SendKeys(_) => true,
702 | _ => false,
703 | })
704 | .collect();
705 |
706 | assert_eq!(remains.len(), 2)
707 | }
708 |
709 | #[test]
710 | pub fn expect_no_send_keys_with_blank_panes() {
711 | let s = "---
712 | windows:
713 | - editor:
714 | panes: ['','','']
715 | ";
716 |
717 | let yaml = YamlLoader::load_from_str(s).unwrap();
718 | let remains: Vec = call(
719 | &yaml,
720 | "muxed",
721 | false,
722 | &Config {
723 | base_index: 0,
724 | pane_base_index: 0,
725 | },
726 | )
727 | .unwrap()
728 | .into_iter()
729 | .filter(|x| match x {
730 | Commands::SendKeys(_) => true,
731 | _ => false,
732 | })
733 | .collect();
734 |
735 | assert_eq!(remains.len(), 0)
736 | }
737 |
738 | #[test]
739 | pub fn expect_no_send_keys_with_blank_window() {
740 | let s = "---
741 | windows:
742 | - editor: ''
743 | ";
744 |
745 | let yaml = YamlLoader::load_from_str(s).unwrap();
746 | let remains: Vec = call(
747 | &yaml,
748 | "muxed",
749 | false,
750 | &Config {
751 | base_index: 0,
752 | pane_base_index: 0,
753 | },
754 | )
755 | .unwrap()
756 | .into_iter()
757 | .filter(|x| match x {
758 | Commands::SendKeys(_) => true,
759 | _ => false,
760 | })
761 | .collect();
762 |
763 | assert_eq!(remains.len(), 0)
764 | }
765 |
766 | #[test]
767 | pub fn expect_full_directory_name() {
768 | let s = "---
769 | root: ~/JustPlainSimple Technologies Inc./financials/ledgers
770 | windows:
771 | - dir: ''
772 | ";
773 |
774 | let yaml = YamlLoader::load_from_str(s).unwrap();
775 | let remains: Commands = call(
776 | &yaml,
777 | "financials",
778 | false,
779 | &Config {
780 | base_index: 0,
781 | pane_base_index: 0,
782 | },
783 | )
784 | .unwrap()
785 | .into_iter()
786 | .find(|x| match x {
787 | Commands::Session(_) => true,
788 | _ => false,
789 | })
790 | .unwrap();
791 |
792 | let root = match remains {
793 | Commands::Session(ref k) => k,
794 | _ => panic!("nope"),
795 | };
796 |
797 | let home = dirs::home_dir().unwrap();
798 | let path = PathBuf::from("JustPlainSimple Technologies Inc./financials/ledgers");
799 |
800 | assert_eq!(root.root_path, Some(Rc::new(home.join(path))))
801 | }
802 |
803 | #[test]
804 | pub fn expect_1_select_window() {
805 | let s = "---
806 | windows: ['cargo', 'vim', 'git']
807 | ";
808 | let yaml = YamlLoader::load_from_str(s).unwrap();
809 | let remains: Vec = call(
810 | &yaml,
811 | "muxed",
812 | false,
813 | &Config {
814 | base_index: 0,
815 | pane_base_index: 0,
816 | },
817 | )
818 | .unwrap()
819 | .into_iter()
820 | .filter(|x| match x {
821 | Commands::SelectWindow(_) => true,
822 | _ => false,
823 | })
824 | .collect();
825 |
826 | assert_eq!(remains.len(), 1)
827 | }
828 |
829 | #[test]
830 | pub fn expect_1_select_pane() {
831 | let s = "---
832 | windows: ['cargo', 'vim', 'git']
833 | ";
834 | let yaml = YamlLoader::load_from_str(s).unwrap();
835 | let remains: Vec = call(
836 | &yaml,
837 | "muxed",
838 | false,
839 | &Config {
840 | base_index: 0,
841 | pane_base_index: 0,
842 | },
843 | )
844 | .unwrap()
845 | .into_iter()
846 | .filter(|x| match x {
847 | Commands::SelectPane(_) => true,
848 | _ => false,
849 | })
850 | .collect();
851 |
852 | assert_eq!(remains.len(), 1)
853 | }
854 |
855 | #[test]
856 | pub fn expect_vec_of_option_string() {
857 | let s = "---
858 | pre: ls -alh
859 | ";
860 | let yaml = YamlLoader::load_from_str(s).unwrap();
861 | let pre = pre_matcher(&yaml[0]["pre"]);
862 | assert_eq!(pre.unwrap(), vec!(Some("ls -alh".to_string())))
863 | }
864 |
865 | #[test]
866 | pub fn expect_vec_of_option_strings() {
867 | let s = "---
868 | pre:
869 | - ls -alh
870 | - tail -f
871 | ";
872 | let yaml = YamlLoader::load_from_str(s).unwrap();
873 | let pre = pre_matcher(&yaml[0]["pre"]);
874 | assert_eq!(
875 | pre.unwrap(),
876 | vec!(Some("ls -alh".to_string()), Some("tail -f".to_string()))
877 | )
878 | }
879 |
880 | #[test]
881 | pub fn expect_some_from_pre_matcher() {
882 | let s = "---
883 | pre: ls -alh
884 | ";
885 | let yaml = YamlLoader::load_from_str(s).unwrap();
886 | let pre = pre_matcher(&yaml[0]["pre"]);
887 | assert!(pre.is_some())
888 | }
889 |
890 | #[test]
891 | pub fn expect_none_from_pre_matcher() {
892 | let s = "---
893 | pre:
894 | ";
895 | let yaml = YamlLoader::load_from_str(s).unwrap();
896 | let pre = pre_matcher(&yaml[0]["pre"]);
897 | assert!(pre.is_none())
898 | }
899 | }
900 |
--------------------------------------------------------------------------------
/load/src/tmux/config.rs:
--------------------------------------------------------------------------------
1 | //! The tmux config data we want
2 | /// This assists in the parsing and accessibility of a users tmux configuration
3 | /// options. Once the data is parsed we move it into a config struct for easy
4 | /// access.
5 | use std::collections::HashMap;
6 | use std::str::FromStr;
7 |
8 | /// A simple struct for accessing parsed config options we want to know about.
9 | #[derive(Debug, Clone)]
10 | pub struct Config {
11 | pub base_index: usize,
12 | pub pane_base_index: usize,
13 | }
14 |
15 | /// The parser of config options. A string of output is passed in. It's simple
16 | /// key value pairs in the format of `key value\n`
17 | impl Config {
18 | pub fn from_string(options: String) -> Config {
19 | let lines = options.lines();
20 | let mut config: HashMap<&str, &str> = HashMap::new();
21 |
22 | for line in lines {
23 | let opt: Vec<&str> = line.split(' ').collect();
24 | config.insert(opt[0], opt[1]);
25 | }
26 |
27 | Config {
28 | base_index: usize::from_str(config.get("base-index").unwrap_or(&"0")).unwrap(),
29 | pane_base_index: usize::from_str(config.get("pane-base-index").unwrap_or(&"0"))
30 | .unwrap(),
31 | }
32 | }
33 | }
34 |
35 | #[test]
36 | fn expect_base_index_0() {
37 | let output = "some-stuff false\nbase-index 0\nother-thing true".to_string();
38 | let config = Config::from_string(output);
39 | assert_eq!(config.base_index, 0)
40 | }
41 |
42 | #[test]
43 | fn expect_base_index_5() {
44 | let output = "some-stuff false\nbase-index 5\nother-thing true".to_string();
45 | let config = Config::from_string(output);
46 | assert_eq!(config.base_index, 5)
47 | }
48 |
49 | #[test]
50 | fn expect_missing_base_index_0() {
51 | let output = "some-stuff false".to_string();
52 | let config = Config::from_string(output);
53 | assert_eq!(config.base_index, 0)
54 | }
55 |
56 | #[test]
57 | fn expect_pane_base_index_0() {
58 | let output = "some-stuff false\npane-base-index 0\nother-thing true".to_string();
59 | let config = Config::from_string(output);
60 | assert_eq!(config.pane_base_index, 0)
61 | }
62 |
63 | #[test]
64 | fn expect_pane_base_index_5() {
65 | let output = "some-stuff false\npane-base-index 5\nother-thing true".to_string();
66 | let config = Config::from_string(output);
67 | assert_eq!(config.pane_base_index, 5)
68 | }
69 |
70 | #[test]
71 | fn expect_missing_pane_base_index_0() {
72 | let output = "some-stuff false".to_string();
73 | let config = Config::from_string(output);
74 | assert_eq!(config.pane_base_index, 0)
75 | }
76 |
--------------------------------------------------------------------------------
/load/src/tmux/mod.rs:
--------------------------------------------------------------------------------
1 | //! The interface for interacting with TMUX sessions. All the commands that are
2 | /// built up during the parsing phase get matched to functions here. The
3 | /// functions in this module all build up strings the get passed to a private
4 | /// call. The private call converts them to `CStrings` and makes an "unsafe" system
5 | /// call. All functions go through this `call` function as a common gateway to
6 | /// system calls and can all be easily logged there.
7 | pub mod config;
8 | pub mod target;
9 |
10 | use libc::system;
11 | use std::ffi::CString;
12 | use std::io;
13 | use std::os::unix::process::ExitStatusExt;
14 | use std::process::{Command, ExitStatus, Output};
15 |
16 | /// The program to call commands on.
17 | static TMUX_NAME: &str = "tmux";
18 |
19 | /// The gateway to calling any functions on tmux. Most public functions in this
20 | /// module will be fed through this `call` function. This safely creates a new
21 | /// thread to execute the command on. We say "Most" public functions will use
22 | /// this as `attach` specificaly does not use it.
23 | ///
24 | /// args: The command we will send to tmux on the host system for execution.
25 | ///
26 | /// # Examples
27 | ///
28 | /// ```rust
29 | /// extern crate load;
30 | /// use load::tmux::call;
31 | ///
32 | /// let _ = call(&["new-window", "-t", "muxed-test", "-c", "~/Projects/muxed/"]);
33 | /// let _ = call(&["kill-session", "-t", "muxed-test"]);
34 | /// ```
35 | pub fn call(args: &[&str]) -> Result