1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
mod console;
mod history;
use clouseau_core::Queryable;
use clouseau_pest::{parse_query, Result};
use clouseau_query::Context;
use console::{Cli, CliResult};
use directories::BaseDirs;
use history::History;
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};
use std::{fs::File, path::PathBuf};
pub struct ClouseauConsole {
persist_history: Option<PathBuf>,
max_history_len: usize,
}
impl Default for ClouseauConsole {
fn default() -> Self {
Self {
persist_history: Default::default(),
max_history_len: 100,
}
}
}
impl ClouseauConsole {
pub fn with_persistent_history(mut self, path: Option<PathBuf>) -> ClouseauConsole {
self.persist_history = path.or_else(|| {
if let Some(base_dirs) = BaseDirs::new() {
let mut path = base_dirs.data_dir().to_path_buf();
path.push("clouseau_history");
Some(path)
} else {
eprintln!("Could not determine user home directory");
None
}
});
self
}
pub fn run<D: Queryable>(self, data: &D, ctx: &Context) {
let mut state = CliResult::Ready;
let history = match self.load_history() {
Err(e) => {
eprintln!("Could not load query history: {}", e);
History::default()
}
Ok(history) => history,
};
let mut cli = Cli::new(Some(history));
loop {
state = match state {
CliResult::Ready => cli.run(),
CliResult::Quit => return,
CliResult::Done(query) => {
if let Err(e) = self.save_history(cli.history()) {
eprintln!("Could not save query history: {}", e);
}
match run_query(data, ctx, &query) {
Ok(results) => cli.print_results(results),
Err(e) => cli.print_results(vec![format!("{}", e)]),
}
}
CliResult::Suggestions(query_part) => {
let suggestions = get_suggestions(data, ctx, query_part);
cli.set_suggestions(suggestions)
}
};
}
}
fn load_history(&self) -> std::io::Result<History> {
if let Some(path) = &self.persist_history {
if path.exists() {
let file = File::open(&path)?;
let reader = BufReader::new(file);
let history = reader.lines().collect::<Result<_, _>>()?;
return Ok(History::new(self.max_history_len, history));
}
}
Ok(History::default())
}
fn save_history(&self, history: &History) -> std::io::Result<()> {
if let Some(path) = &self.persist_history {
let mut file = OpenOptions::new().write(true).create(true).open(&path)?;
for query in history.iter() {
file.write_all(query.as_bytes())?;
file.write_all(b"\n")?;
}
}
Ok(())
}
}
fn get_suggestions<D: Queryable>(data: &D, ctx: &Context, query: String) -> Vec<String> {
if query.ends_with("./") {
run_query(data, ctx, &format!("{}.keys()", &query[..query.len() - 2]))
.ok()
.unwrap_or_else(Vec::new)
} else if query.ends_with('/') {
run_query(data, ctx, &format!("{}.keys()", &query[..query.len() - 1]))
.ok()
.unwrap_or_else(Vec::new)
} else if query.ends_with('.') {
ctx.function_names()
.map(|name| format!("{}()", name))
.collect()
} else if query.is_empty() {
vec![String::from("/"), String::from("./"), String::from(".")]
} else {
Vec::new()
}
}
fn run_query<D: Queryable>(data: &D, ctx: &Context, query: &str) -> Result<Vec<String>> {
parse_query(query).map(|query| {
let results = ctx.exec(&query, data as _).peekable();
results.take(100).map(|d| d.to_string()).collect()
})
}