datman/commands/
ilabel.rs

1/*
2This file is part of Yama.
3
4Yama is free software: you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation, either version 3 of the License, or
7(at your option) any later version.
8
9Yama is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License
15along with Yama.  If not, see <https://www.gnu.org/licenses/>.
16*/
17
18
19use std::io;
20use std::io::{StdinLock, Stdout, Write};
21use std::path::Path;
22
23use arc_interner::ArcIntern;
24use byteorder::ReadBytesExt;
25use termion::input::TermRead;
26use termion::raw::{IntoRawMode, RawTerminal};
27
28use crate::descriptor::{load_descriptor, Descriptor, SourceDescriptor};
29use crate::labelling::State::{Excluded, Labelled, Split};
30use crate::labelling::{
31    load_labelling_rules, save_labelling_rules, GlobRule, Label, LabellingRules, State,
32};
33use crate::tree::{scan, FileTree, FileTree1};
34
35use anyhow::{anyhow, bail};
36
37pub fn interactive_label_node(
38    path: String,
39    current_state: Option<State>,
40    node: &mut FileTree1<Option<State>>,
41    labels: &Vec<Label>,
42    rules: &mut LabellingRules,
43    stdin: &mut StdinLock,
44    stdout: &mut RawTerminal<Stdout>,
45) -> anyhow::Result<()> {
46    let mut next_state = current_state;
47    if let Some(rule_state) = rules.apply(&path) {
48        next_state = Some(rule_state.clone());
49    } else if !next_state
50        .as_ref()
51        .map(|s| s.should_inherit())
52        .unwrap_or(false)
53    {
54        if node.is_dir() {
55            stdout.write_all(format!("\r{}/: _", path).as_bytes())?;
56        } else if node.is_symlink() {
57            stdout.write_all(format!("\r{} (symlink): _", path).as_bytes())?;
58        } else {
59            stdout.write_all(format!("\r{}: _", path).as_bytes())?;
60        }
61        stdout.flush()?;
62        let user_input_state = loop {
63            let next_char = stdin.read_u8()? as char;
64            if next_char >= '1' && next_char <= '9' {
65                let index = next_char as usize - '1' as usize;
66                if let Some(label) = labels.get(index) {
67                    rules
68                        .position_based_rules
69                        .insert(path.clone(), Labelled(label.clone()));
70                    print!("\x08{}\r\n", label.0);
71                    break Some(Labelled(label.clone()));
72                }
73            } else if next_char == 'x' {
74                rules.position_based_rules.insert(path.clone(), Excluded);
75                print!("\x08{}\r\n", next_char);
76                break Some(Excluded);
77            } else if next_char == 's' {
78                if node.is_dir() {
79                    rules.position_based_rules.insert(path.clone(), Split);
80                    print!("\x08{}\r\n", next_char);
81                    break Some(Split);
82                } else {
83                    print!("\x08!");
84                    stdout.flush()?;
85                }
86            } else if next_char == 'p' {
87                print!("\x08p\r\n\tPattern mode. Choose a label or other effect to apply to the pattern matches: _");
88                stdout.flush()?;
89
90                let rule_apply_state = loop {
91                    let next_char = stdin.read_u8()? as char;
92                    if next_char >= '1' && next_char <= '9' {
93                        let index = next_char as usize - '1' as usize;
94                        if let Some(label) = labels.get(index) {
95                            print!("\x08{}\r\n", label.0);
96                            break Labelled(label.clone());
97                        }
98                    } else if next_char == 'x' {
99                        print!("\x08{}\r\n", next_char);
100                        break Excluded;
101                    } else if next_char == 's' {
102                        print!("\x08{}\r\n", next_char);
103                        break Split;
104                    }
105                };
106                stdout.flush()?;
107
108                stdout.suspend_raw_mode()?;
109                print!("\tEnter a glob pattern to match on:\n\t");
110                stdout.flush()?;
111                let (pattern, glob) = loop {
112                    let pattern = stdin
113                        .read_line()?
114                        .ok_or_else(|| anyhow!("EOT? when reading glob pattern"))?;
115
116                    match glob::Pattern::new(&pattern) {
117                        Ok(glob) => {
118                            if !glob.matches(&path) {
119                                println!("Doesn't match the path in question.");
120                                continue;
121                            }
122                            break (pattern, glob);
123                        }
124                        Err(error) => {
125                            println!("Error: {:?}. Try again.", error);
126                        }
127                    }
128                };
129                stdout.activate_raw_mode()?;
130
131                rules.glob_based_rules.push(GlobRule {
132                    pattern,
133                    glob,
134                    outcome: rule_apply_state.clone(),
135                });
136                break Some(rule_apply_state);
137            }
138        };
139        next_state = user_input_state;
140    }
141
142    match node {
143        FileTree::NormalFile { meta, .. } => {
144            *meta = next_state;
145        }
146        FileTree::Directory { meta, children, .. } => {
147            *meta = next_state.clone();
148
149            for (child_name, child) in children.iter_mut() {
150                let child_path = format!("{}/{}", path, child_name);
151                interactive_label_node(
152                    child_path,
153                    next_state.clone(),
154                    child,
155                    labels,
156                    rules,
157                    stdin,
158                    stdout,
159                )?;
160            }
161        }
162        FileTree::SymbolicLink { meta, .. } => {
163            *meta = next_state;
164        }
165        FileTree::Other(_) => {
166            panic!("Other() nodes shouldn't be present here.");
167        }
168    }
169
170    Ok(())
171}
172
173pub fn interactive_labelling_session(path: &Path, source_name: String) -> anyhow::Result<()> {
174    let descriptor: Descriptor = load_descriptor(path)?;
175
176    let source = descriptor
177        .source
178        .get(&source_name)
179        .ok_or_else(|| anyhow!("No source found by that name!"))?;
180
181    if let SourceDescriptor::DirectorySource {
182        hostname: _,
183        directory,
184    } = source
185    {
186        println!("Scanning source; this might take a little while...");
187        let mut dir_scan = scan(directory)?
188            .ok_or_else(|| anyhow!("Empty source."))?
189            .replace_meta(&None);
190
191        let mut rules = load_labelling_rules(path, &source_name)?;
192
193        let labels: Vec<Label> = descriptor
194            .labels
195            .iter()
196            .map(|label| Label(ArcIntern::new(label.clone())))
197            .collect();
198
199        println!("The following label mappings are available:");
200        for (idx, label) in labels.iter().enumerate() {
201            println!("\tFor {:?}, press {}!", label.0.as_ref(), idx + 1);
202        }
203        println!("\tTo split a directory, press 's'!");
204        println!("\tTo exclude an entry, press 'x'!");
205        println!("\tTo apply a pattern, press 'p'...");
206
207        // Set terminal to raw mode to allow reading stdin one key at a time
208        let mut stdout = io::stdout().into_raw_mode().unwrap();
209        let stdin_unlocked = io::stdin();
210        let mut stdin = stdin_unlocked.lock();
211        interactive_label_node(
212            "".to_owned(),
213            None,
214            &mut dir_scan,
215            &labels,
216            &mut rules,
217            &mut stdin,
218            &mut stdout,
219        )?;
220        drop(stdout);
221        drop(stdin);
222
223        println!("\nLabelling completed!");
224
225        // save rules
226        save_labelling_rules(path, &source_name, &rules)?;
227    } else {
228        bail!("Can't do interactive labelling on a non-directory source.");
229    }
230
231    Ok(())
232}