#![allow(clippy::mutex_atomic)]
use async_std::task;
use dashmap::DashMap;
use horrorshow::{html, owned_html, Raw, Render};
use serde::{de::DeserializeOwned, Deserialize};
use std::{cmp::Ordering, fmt::Display, string::ToString, sync::Mutex, thread};
use tide::{Request, Response};
pub use const_tweaker_attribute::tweak;
#[doc(hidden)]
pub use ctor::ctor;
#[doc(hidden)]
#[derive(Debug)]
pub enum Field {
F32 {
value: f32,
min: f32,
max: f32,
step: f32,
module: String,
file: String,
line: u32,
},
F64 {
value: f64,
min: f64,
max: f64,
step: f64,
module: String,
file: String,
line: u32,
},
I8 {
value: i8,
min: i8,
max: i8,
step: i8,
module: String,
file: String,
line: u32,
},
U8 {
value: u8,
min: u8,
max: u8,
step: u8,
module: String,
file: String,
line: u32,
},
I16 {
value: i16,
min: i16,
max: i16,
step: i16,
module: String,
file: String,
line: u32,
},
U16 {
value: u16,
min: u16,
max: u16,
step: u16,
module: String,
file: String,
line: u32,
},
I32 {
value: i32,
min: i32,
max: i32,
step: i32,
module: String,
file: String,
line: u32,
},
U32 {
value: u32,
min: u32,
max: u32,
step: u32,
module: String,
file: String,
line: u32,
},
I64 {
value: i64,
min: i64,
max: i64,
step: i64,
module: String,
file: String,
line: u32,
},
U64 {
value: u64,
min: u64,
max: u64,
step: u64,
module: String,
file: String,
line: u32,
},
Usize {
value: usize,
min: usize,
max: usize,
step: usize,
module: String,
file: String,
line: u32,
},
Bool {
value: bool,
module: String,
file: String,
line: u32,
},
String {
value: String,
module: String,
file: String,
line: u32,
},
}
impl Field {
pub fn module_path(&self) -> &str {
match self {
Field::F32 { module, .. }
| Field::F64 { module, .. }
| Field::I8 { module, .. }
| Field::U8 { module, .. }
| Field::I16 { module, .. }
| Field::U16 { module, .. }
| Field::I32 { module, .. }
| Field::U32 { module, .. }
| Field::I64 { module, .. }
| Field::U64 { module, .. }
| Field::Usize { module, .. }
| Field::Bool { module, .. }
| Field::String { module, .. } => &*module,
}
}
pub fn file(&self) -> String {
match self {
Field::F32 { file, line, .. }
| Field::F64 { file, line, .. }
| Field::I8 { file, line, .. }
| Field::U8 { file, line, .. }
| Field::I16 { file, line, .. }
| Field::U16 { file, line, .. }
| Field::I32 { file, line, .. }
| Field::U32 { file, line, .. }
| Field::I64 { file, line, .. }
| Field::U64 { file, line, .. }
| Field::Usize { file, line, .. }
| Field::Bool { file, line, .. }
| Field::String { file, line, .. } => format!("{}:{}", file, line),
}
}
pub fn line_number(&self) -> u32 {
match self {
Field::F32 { line, .. }
| Field::F64 { line, .. }
| Field::I8 { line, .. }
| Field::U8 { line, .. }
| Field::I16 { line, .. }
| Field::U16 { line, .. }
| Field::I32 { line, .. }
| Field::U32 { line, .. }
| Field::I64 { line, .. }
| Field::U64 { line, .. }
| Field::Usize { line, .. }
| Field::Bool { line, .. }
| Field::String { line, .. } => *line,
}
}
pub fn to_html_widget(&self, key: &str) -> String {
match self {
Field::F32 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "f32").to_string(),
Field::F64 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "f64").to_string(),
Field::I8 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "i8").to_string(),
Field::U8 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "u8").to_string(),
Field::I16 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "i16").to_string(),
Field::U16 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "u16").to_string(),
Field::I32 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "i32").to_string(),
Field::U32 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "u32").to_string(),
Field::I64 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "i64").to_string(),
Field::U64 {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "u64").to_string(),
Field::Usize {
value,
min,
max,
step,
..
} => Field::render_slider(key, *value, *min, *max, *step, "usize").to_string(),
Field::Bool { value, .. } => Field::render_bool(key, *value).to_string(),
Field::String { value, .. } => Field::render_string(key, value).to_string(),
}
}
fn render_slider<'a, T>(
key: &'a str,
value: T,
min: T,
max: T,
step: T,
http_path: &'a str,
) -> impl Render + ToString + 'a
where
T: Display + 'a,
{
owned_html! {
div (class="column") {
input (type="range",
id=key.to_string(),
min=min.to_string(),
max=max.to_string(),
step=step.to_string(),
defaultValue=value.to_string(),
style="width: 100%",
oninput=send(key, "Number(this.value)", http_path))
{ }
}
div (class="column is-narrow") {
span (id=format!("{}_label", key), class="is-small")
{ : value.to_string() }
}
}
}
fn render_bool<'a>(key: &'a str, value: bool) -> impl Render + ToString + 'a {
owned_html! {
div (class="column") {
input (type="checkbox",
id=key,
value=value.to_string(),
onclick=send(key, "this.checked", "bool"))
{ }
}
div (class="column is-narrow") {
span (id=format!("{}_label", key))
{ : value.to_string() }
}
}
}
fn render_string<'a>(key: &'a str, value: &'a str) -> impl Render + ToString + 'a {
owned_html! {
div (class="column") {
input (type="text",
id=key,
value=value,
style="width: 100%",
onchange=send(key, "this.value", "string"))
{ }
}
div (class="column is-narrow") {
span (id=format!("{}_label", key))
{ : value.to_string() }
}
}
}
}
#[derive(Debug, Deserialize)]
struct PostData<T> {
key: String,
value: T,
}
lazy_static::lazy_static! {
#[doc(hidden)]
pub static ref DATA: DashMap<&'static str, Field> = DashMap::new();
static ref LAST_MAP_SIZE: Mutex<usize> = Mutex::new(0);
}
#[ctor::ctor]
fn run() {
thread::spawn(|| {
task::block_on(async {
let mut app = tide::new();
app.at("/").get(main_site);
app.at("/should_refresh").get(should_refresh);
app.at("/set/f32").post(|r| handle_set_value(r, set_f32));
app.at("/set/f64").post(|r| handle_set_value(r, set_f64));
app.at("/set/bool").post(|r| handle_set_value(r, set_bool));
app.at("/set/string")
.post(|r| handle_set_value(r, set_string));
app.listen("127.0.0.1:9938").await
})
.expect("Running web server failed");
});
}
async fn main_site(_: Request<()>) -> Response {
let mut last_map_size = LAST_MAP_SIZE.lock().unwrap();
*last_map_size = DATA.len();
let body = html! {
style { : include_str!("bulma.css") }
style { : "* { font-family: sans-serif}" }
body {
section (class="hero is-primary") {
div (class="hero-body") {
div (class="container") {
h1 (class="title is-1") { : "Const Tweaker Web Interface" }
}
}
}
: render_widgets();
div (class="container") {
div (class="notification is-danger") {
span(id="status") { }
}
}
}
script { : Raw(include_str!("send.js")) }
};
Response::new(200)
.body_string(format!("{}", body))
.set_header("content-type", "text/html;charset=utf-8")
}
fn render_widgets() -> impl Render {
owned_html! {
@for module in modules().into_iter() {
section (class="section") {
div (class="container box") {
h3 (class="title is-3") { : format!("Module: \"{}\"", module) }
: render_module(&module)
}
div (class="container box") {
h4 (class="title is-4") { : "Changes" }
div (class="columns") {
div (class="column") {
textarea (
class="textarea",
style="font-family: monospace",
id=format!("{}_output", module.replace("::", "_")),
readonly,
placeholder="No changes")
}
div (class="column is-narrow control") {
button (class="button is-link", onclick=format!("copy_text(\"{}\")", module)) {
: "Copy"
}
}
}
}
}
}
}
}
fn render_module<'a>(module: &'a str) -> impl Render + 'a {
let mut data = DATA
.iter()
.filter(|kv| kv.value().module_path() == module)
.collect::<Vec<_>>();
data.sort_by(|a, b| {
a.value()
.line_number()
.partial_cmp(&b.value().line_number())
.unwrap_or(Ordering::Equal)
});
owned_html! {
@for ref_multi in data.iter() {
: render_widget(ref_multi.key(), ref_multi.value())
}
}
}
fn render_widget<'a>(key: &'a str, field: &'a Field) -> impl Render + 'a {
owned_html! {
div (class="columns") {
div (class="column is-narrow") {
span (class="is-small") { : key }
br {}
span (class="tag") { : field.file() }
}
: Raw(field.to_html_widget(key))
}
}
}
fn send(key: &str, look_for: &str, data_type: &str) -> String {
format!(
"send('{}', {}, '{}')",
key.replace("\\", "\\\\"),
look_for,
data_type
)
}
async fn should_refresh(_request: Request<()>) -> Response {
let mut last_map_size = LAST_MAP_SIZE.lock().unwrap();
if *last_map_size == DATA.len() {
Response::new(200)
} else {
*last_map_size = DATA.len();
Response::new(200).body_string("refresh".to_string())
}
}
async fn handle_set_value<T, F>(mut request: Request<()>, set_value: F) -> Response
where
T: DeserializeOwned,
F: Fn(&mut Field, T),
{
let post_data: PostData<T> = request.body_json().await.expect("Could not decode JSON");
set_value(
&mut DATA
.get_mut(&*post_data.key)
.expect("Could not get item from map"),
post_data.value,
);
Response::new(200)
}
fn set_f32(field: &mut Field, new_value: f32) {
match field {
Field::F32 { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn set_f64(field: &mut Field, new_value: f64) {
match field {
Field::F64 { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn set_bool(field: &mut Field, new_value: bool) {
match field {
Field::Bool { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn set_string(field: &mut Field, new_value: String) {
match field {
Field::String { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn modules() -> Vec<String> {
let mut modules: Vec<_> = DATA
.iter()
.map(|kv| kv.value().module_path().to_string())
.collect::<_>();
modules.sort();
modules.dedup();
modules
}