aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs36
-rw-r--r--src/main.rs31
-rw-r--r--src/ui.rs158
3 files changed, 225 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..3e0571d
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,36 @@
+use std::fs;
+use anyhow::Context;
+use serde::{Deserialize, Serialize};
+
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Config {
+ pub name: String,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Config {
+ name: "CHANGEME".to_owned(),
+ }
+ }
+}
+
+const CONFIGFILE : &str = "odr-dabmux-gui-config.toml";
+
+impl Config {
+ pub fn load() -> anyhow::Result<Self> {
+ if std::path::Path::new(CONFIGFILE).exists() {
+ let file_contents = fs::read_to_string(CONFIGFILE)?;
+ toml::from_str(&file_contents).context("parsing config file")
+ }
+ else {
+ Ok(Default::default())
+ }
+ }
+
+ pub fn store(&self) -> anyhow::Result<()> {
+ fs::write(CONFIGFILE, toml::to_string_pretty(&self)?)
+ .context("writing config file")
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e64570a
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,31 @@
+use std::sync::{Arc, Mutex};
+use anyhow::{anyhow, Context};
+use log::{debug, info, warn, error};
+
+mod ui;
+mod config;
+
+struct AppState {
+ conf : config::Config,
+}
+
+type SharedState = Arc<Mutex<AppState>>;
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ simple_logger::SimpleLogger::new()
+ .with_level(log::LevelFilter::Debug)
+ .env()
+ .init().unwrap();
+
+ let conf = config::Config::load().expect("Could not load config");
+
+ let shared_state = Arc::new(Mutex::new(AppState {
+ conf : conf.clone(),
+ }));
+
+ let port = 3000;
+ info!("Setting up listener on port {port}");
+ ui::serve(port, shared_state).await;
+ Ok(())
+}
diff --git a/src/ui.rs b/src/ui.rs
new file mode 100644
index 0000000..529dfa6
--- /dev/null
+++ b/src/ui.rs
@@ -0,0 +1,158 @@
+use std::net::SocketAddr;
+use anyhow::{anyhow, Context};
+use askama::Template;
+use axum::{
+ Form,
+ Json,
+ Router,
+ extract::State,
+ extract::{ws::{Message, WebSocket, WebSocketUpgrade}, ConnectInfo},
+ http::StatusCode,
+ response::IntoResponse,
+ routing::{get, post},
+};
+use serde::Deserialize;
+
+use log::{debug, info, warn, error};
+use tower_http::services::ServeDir;
+
+use crate::config;
+use crate::SharedState;
+
+pub async fn serve(port: u16, shared_state: SharedState) {
+ let app = Router::new()
+ .route("/", get(dashboard))
+ .route("/settings", get(show_settings).post(post_settings))
+ .nest_service("/static", ServeDir::new("static"))
+ /* For an example for timeouts and tracing, have a look at the git history */
+ .with_state(shared_state);
+
+ let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await.unwrap();
+ axum::serve(listener,
+ app.into_make_service_with_connect_info::<SocketAddr>())
+ .await.unwrap()
+}
+
+#[derive(PartialEq)]
+enum ActivePage {
+ Dashboard,
+ Settings,
+ None,
+}
+
+impl ActivePage {
+ // Used by templates/head.html to include the correct js files in <head>
+ fn styles(&self) -> Vec<&'static str> {
+ match self {
+ ActivePage::Dashboard => vec![],
+ ActivePage::Settings => vec![],
+ ActivePage::None => vec![],
+ }
+ }
+}
+
+#[derive(Template)]
+#[template(path = "dashboard.html")]
+struct DashboardTemplate<'a> {
+ title: &'a str,
+ page: ActivePage,
+ conf: config::Config,
+}
+
+async fn dashboard(State(state): State<SharedState>) -> DashboardTemplate<'static> {
+ let conf = {
+ let st = state.lock().unwrap();
+ st.conf.clone()
+ };
+
+ DashboardTemplate {
+ title: "Dashboard",
+ conf,
+ page: ActivePage::Dashboard,
+ }
+}
+
+#[derive(Template)]
+#[template(path = "settings.html")]
+struct SettingsTemplate<'a> {
+ title: &'a str,
+ page: ActivePage,
+ conf: config::Config,
+}
+
+async fn show_settings(State(state): State<SharedState>) -> SettingsTemplate<'static> {
+ SettingsTemplate {
+ title: "Settings",
+ page: ActivePage::Settings,
+ conf: state.lock().unwrap().conf.clone(),
+ }
+}
+
+#[derive(Template)]
+#[template(path = "settings_applied.html")]
+struct SettingsAppliedTemplate<'a> {
+ title: &'a str,
+ page: ActivePage,
+ conf: config::Config,
+ ok: bool,
+ error_message: &'a str,
+ error_reason: String,
+}
+
+#[derive(Deserialize, Debug)]
+struct FormConfig {
+ name: String,
+}
+
+impl TryFrom<FormConfig> for config::Config {
+ type Error = anyhow::Error;
+
+ fn try_from(value: FormConfig) -> Result<Self, Self::Error> {
+ Ok(config::Config {
+ name: value.name,
+ })
+ }
+}
+
+async fn post_settings(
+ State(state): State<SharedState>,
+ Form(input): Form<FormConfig>) -> (StatusCode, SettingsAppliedTemplate<'static>) {
+ match config::Config::try_from(input) {
+ Ok(c) => {
+ match c.store() {
+ Ok(()) => {
+ state.lock().unwrap().conf.clone_from(&c);
+
+ (StatusCode::OK, SettingsAppliedTemplate {
+ title: "Settings",
+ conf: c,
+ page: ActivePage::None,
+ ok: true,
+ error_message: "",
+ error_reason: "".to_owned(),
+ })
+ }
+ Err(e) => {
+ (StatusCode::INTERNAL_SERVER_ERROR, SettingsAppliedTemplate {
+ title: "Settings",
+ conf : c,
+ page: ActivePage::None,
+ ok: false,
+ error_message: "Failed to store config",
+ error_reason: e.to_string(),
+ })
+ },
+ }
+ },
+ Err(e) => {
+ (StatusCode::BAD_REQUEST, SettingsAppliedTemplate {
+ title: "Settings",
+ conf: state.lock().unwrap().conf.clone(),
+ page: ActivePage::None,
+ ok: false,
+ error_message: "Error interpreting POST data",
+ error_reason: e.to_string(),
+ })
+ },
+ }
+}