Compare commits

..

2 commits

Author SHA1 Message Date
e1d70e2c4f
update config file format 2019-12-24 00:05:56 -05:00
9f4577f0ed
actually name yaml file correctly 2019-12-23 22:59:12 -05:00
6 changed files with 116 additions and 53 deletions

View file

@ -1,20 +0,0 @@
# The location which your server is listening on and binds to. You must restart
# bunbun for changes to take effect for this config option.
bind_address: "127.0.0.1:8080"
# The root location where people can access your instance of bunbun
public_address: "localhost:8080"
# A default route, if no route is was matched. If none were matched, the entire
# query is used as the query for the default route. This field is optional.
default_route: "g"
routes:
# Meta
ls: "/ls"
help: "/ls"
list: "/ls"
# Google
g: "https://google.com/search?q={{query}}"
yt: "https://www.youtube.com/results?search_query={{query}}"
r: "https://reddit.com/r/{{query}}"

35
bunbun.default.yaml Normal file
View file

@ -0,0 +1,35 @@
# The location which your server is listening on and binds to. You must restart
# bunbun for changes to take effect for this config option.
bind_address: "127.0.0.1:8080"
# The root location where people can access your instance of bunbun
public_address: "localhost:8080"
# A default route, if no route is was matched. If none were matched, the entire
# query is used as the query for the default route. This field is optional, but
# highly recommended for ease-of-use.
default_route: "g"
# A list containing route groups. Each route group must have a name and a
# mapping of routes, with an optional description field. Each route mapping may
# contain "{{query}}", which will be populated by the user's search query. This
# input is percent-escaped. If multiple routes are defined, then the later
# defined route is used.
groups:
-
name: "Meta commands"
description: "Commands for bunbun"
routes:
ls: &ls "/ls"
help: *ls
list: *ls
-
name: "Google"
routes:
g: "https://google.com/search?q={{query}}"
yt: "https://www.youtube.com/results?search_query={{query}}"
-
name: "Uncategorized routes"
description: "One-off routes with no specific grouping"
routes:
r: "https://reddit.com/r/{{query}}"

View file

@ -21,5 +21,5 @@ args:
- config: - config:
short: "c" short: "c"
long: "config" long: "config"
default_value: "/etc/bunbun.toml" default_value: "/etc/bunbun.yaml"
help: Specify the location of the config file to read from. Needs read/write permissions. help: Specify the location of the config file to read from. Needs read/write permissions.

View file

@ -5,7 +5,7 @@ use handlebars::Handlebars;
use hotwatch::{Event, Hotwatch}; use hotwatch::{Event, Hotwatch};
use libc::daemon; use libc::daemon;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::cmp::min; use std::cmp::min;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -17,7 +17,7 @@ use std::time::Duration;
mod routes; mod routes;
mod template_args; mod template_args;
const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.toml"); const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml");
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
@ -61,6 +61,8 @@ from_error!(log::SetLoggerError, LoggerInitError);
pub struct State { pub struct State {
public_address: String, public_address: String,
default_route: Option<String>, default_route: Option<String>,
groups: Vec<RouteGroup>,
/// Cached, flattened mapping of all routes and their destinations.
routes: HashMap<String, String>, routes: HashMap<String, String>,
renderer: Handlebars, renderer: Handlebars,
} }
@ -72,30 +74,20 @@ fn main() -> Result<(), BunBunError> {
.author(crate_authors!()) .author(crate_authors!())
.get_matches(); .get_matches();
let log_level = match min(matches.occurrences_of("verbose"), 3) as i8 init_logger(
- min(matches.occurrences_of("quiet"), 2) as i8 matches.occurrences_of("verbose"),
{ matches.occurrences_of("quiet"),
-2 => None, )?;
-1 => Some(log::Level::Error),
0 => Some(log::Level::Warn),
1 => Some(log::Level::Info),
2 => Some(log::Level::Debug),
3 => Some(log::Level::Trace),
_ => unreachable!(),
};
if let Some(level) = log_level { // config has default location provided, unwrapping is fine.
simple_logger::init_with_level(level)?;
}
// config has default location provided
let conf_file_location = String::from(matches.value_of("config").unwrap()); let conf_file_location = String::from(matches.value_of("config").unwrap());
let conf = read_config(&conf_file_location)?; let conf = read_config(&conf_file_location)?;
let renderer = compile_templates(); let renderer = compile_templates();
let state = Arc::from(RwLock::new(State { let state = Arc::from(RwLock::new(State {
public_address: conf.public_address, public_address: conf.public_address,
default_route: conf.default_route, default_route: conf.default_route,
routes: conf.routes, routes: cache_routes(&conf.groups),
groups: conf.groups,
renderer, renderer,
})); }));
@ -110,7 +102,6 @@ fn main() -> Result<(), BunBunError> {
let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?; let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?;
// TODO: keep retry watching in separate thread // TODO: keep retry watching in separate thread
// Closures need their own copy of variables for proper lifecycle management // Closures need their own copy of variables for proper lifecycle management
let state_ref = state.clone(); let state_ref = state.clone();
let conf_file_location_clone = conf_file_location.clone(); let conf_file_location_clone = conf_file_location.clone();
@ -123,7 +114,8 @@ fn main() -> Result<(), BunBunError> {
Ok(conf) => { Ok(conf) => {
state.public_address = conf.public_address; state.public_address = conf.public_address;
state.default_route = conf.default_route; state.default_route = conf.default_route;
state.routes = conf.routes; state.routes = cache_routes(&conf.groups);
state.groups = conf.groups;
info!("Successfully updated active state"); info!("Successfully updated active state");
} }
Err(e) => warn!("Failed to update config file: {}", e), Err(e) => warn!("Failed to update config file: {}", e),
@ -156,11 +148,43 @@ fn main() -> Result<(), BunBunError> {
Ok(()) Ok(())
} }
/// Initializes the logger based on the number of quiet and verbose flags passed
/// in. Usually, these values are mutually exclusive, that is, if the number of
/// verbose flags is non-zero then the quiet flag is zero, and vice versa.
fn init_logger(
num_verbose_flags: u64,
num_quiet_flags: u64,
) -> Result<(), BunBunError> {
let log_level =
match min(num_verbose_flags, 3) as i8 - min(num_quiet_flags, 2) as i8 {
-2 => None,
-1 => Some(log::Level::Error),
0 => Some(log::Level::Warn),
1 => Some(log::Level::Info),
2 => Some(log::Level::Debug),
3 => Some(log::Level::Trace),
_ => unreachable!(), // values are clamped to [0, 3] - [0, 2]
};
if let Some(level) = log_level {
simple_logger::init_with_level(level)?;
}
Ok(())
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct Config { struct Config {
bind_address: String, bind_address: String,
public_address: String, public_address: String,
default_route: Option<String>, default_route: Option<String>,
groups: Vec<RouteGroup>,
}
#[derive(Deserialize, Serialize)]
struct RouteGroup {
name: String,
description: Option<String>,
routes: HashMap<String, String>, routes: HashMap<String, String>,
} }
@ -200,6 +224,21 @@ fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
Ok(serde_yaml::from_str(&config_str)?) Ok(serde_yaml::from_str(&config_str)?)
} }
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, String> {
let mut mapping = HashMap::new();
for group in groups {
for (kw, dest) in &group.routes {
match mapping.insert(kw.clone(), dest.clone()) {
None => trace!("Inserting {} into mapping.", kw),
Some(old_value) => {
debug!("Overriding {} route from {} to {}.", kw, old_value, dest)
}
}
}
}
mapping
}
/// Returns an instance with all pre-generated templates included into the /// Returns an instance with all pre-generated templates included into the
/// binary. This allows for users to have a portable binary without needed the /// binary. This allows for users to have a portable binary without needed the
/// templates at runtime. /// templates at runtime.

View file

@ -23,7 +23,7 @@ const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
#[get("/ls")] #[get("/ls")]
pub fn list(data: Data<Arc<RwLock<State>>>) -> impl Responder { pub fn list(data: Data<Arc<RwLock<State>>>) -> impl Responder {
let data = data.read().unwrap(); let data = data.read().unwrap();
HttpResponse::Ok().body(data.renderer.render("list", &data.routes).unwrap()) HttpResponse::Ok().body(data.renderer.render("list", &data.groups).unwrap())
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View file

@ -14,20 +14,29 @@
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
} }
h1, p { margin: 0; } h1, p { margin: 0; }
table { margin-top: 1em; } table { margin-bottom: 1rem; }
td, th { padding: 0 0.5em; } header { display: flex; flex-wrap: wrap; align-items: baseline; margin-top: 2rem; }
header h2 { margin: 0 1rem 0 0; }
i { color: rgba(255, 255, 255, 0.5); }
td, th { padding: 0 0.5rem; }
.shortcut { text-align: right; } .shortcut { text-align: right; }
.target { text-align: left; width: 100%; }
</style> </style>
</head> </head>
<body> <body>
<h1>Bunbun Command List</h1> <h1>Bunbun Command List</h1>
<p><em>To edit this list, edit your <code>bunbun.toml</code> file.</em></p> <p><i>To edit this list, edit your <code>bunbun.toml</code> file.</i></p>
<main>
{{#each this}} {{!-- Iterate over RouteGroup --}}
<header><h2>{{this.name}}</h2><i>{{this.description}}</i></header>
<table> <table>
<tr> <tr>
<th>Shortcut</th> <th>Shortcut</th>
<th>Target</th> <th class="target">Target</th>
</tr> </tr>
{{#each this}}<tr><td class="shortcut">{{@key}}</td><td class="target">{{this}}</td></tr>{{/each}} {{#each this.routes}}<tr><td class="shortcut">{{@key}}</td><td class="target">{{this}}</td></tr>{{/each}}
</table> </table>
{{/each}}
</main>
</body> </body>
</html> </html>