diff --git a/Cargo.lock b/Cargo.lock
index 9891333..6a3b81f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -894,6 +894,20 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "gm-control-panel"
+version = "0.1.0"
+dependencies = [
+ "futures",
+ "gdk4",
+ "gio",
+ "glib",
+ "glib-build-tools",
+ "gtk4",
+ "libadwaita",
+ "tokio",
+]
+
[[package]]
name = "gobject-sys"
version = "0.17.10"
diff --git a/Cargo.toml b/Cargo.toml
index 25f3e94..e6b1e27 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,8 +1,11 @@
[workspace]
members = [
"changeset",
+<<<<<<< HEAD
"config",
"config-derive",
+=======
+>>>>>>> 36f1bb1 (Make the main app window appear, start working on config)
"coordinates",
"cyberpunk-splash",
"dashboard",
@@ -11,6 +14,10 @@ members = [
"fluent-ergonomics",
"kifu/core",
"geo-types",
+<<<<<<< HEAD
+=======
+ "gm-control-panel",
+>>>>>>> 36f1bb1 (Make the main app window appear, start working on config)
"hex-grid",
"ifc",
"memorycache",
diff --git a/build.sh b/build.sh
index 579cc62..f96bd5f 100755
--- a/build.sh
+++ b/build.sh
@@ -14,6 +14,7 @@ RUST_ALL_TARGETS=(
"flow"
"fluent-ergonomics"
"geo-types"
+ "gm-control-panel"
"hex-grid"
"ifc"
"memorycache"
diff --git a/gm-control-panel/Cargo.toml b/gm-control-panel/Cargo.toml
new file mode 100644
index 0000000..cb8375a
--- /dev/null
+++ b/gm-control-panel/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "gm-control-panel"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+adw = { version = "0.4", package = "libadwaita", features = [ "v1_2" ] }
+futures = { version = "0.3" }
+gio = { version = "0.17" }
+glib = { version = "0.17" }
+gdk = { version = "0.6", package = "gdk4" }
+gtk = { version = "0.6", package = "gtk4" }
+tokio = { version = "1", features = ["full"] }
+
+[build-dependencies]
+glib-build-tools = "0.16"
+
diff --git a/gm-control-panel/build.rs b/gm-control-panel/build.rs
new file mode 100644
index 0000000..2f8c7ae
--- /dev/null
+++ b/gm-control-panel/build.rs
@@ -0,0 +1,7 @@
+fn main() {
+ glib_build_tools::compile_resources(
+ "resources",
+ "resources/gresources.xml",
+ "com.luminescent-dreams.dashboard.gresource",
+ );
+}
diff --git a/gm-control-panel/resources/gresources.xml b/gm-control-panel/resources/gresources.xml
new file mode 100644
index 0000000..1447b98
--- /dev/null
+++ b/gm-control-panel/resources/gresources.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/gm-control-panel/src/app_window.rs b/gm-control-panel/src/app_window.rs
new file mode 100644
index 0000000..fb07457
--- /dev/null
+++ b/gm-control-panel/src/app_window.rs
@@ -0,0 +1,12 @@
+#[derive(Clone)]
+pub struct ApplicationWindow {
+ pub window: adw::ApplicationWindow,
+}
+
+impl ApplicationWindow {
+ pub fn new(app: &adw::Application) -> Self {
+ let window = adw::ApplicationWindow::new(app);
+
+ Self { window }
+ }
+}
diff --git a/gm-control-panel/src/config.rs b/gm-control-panel/src/config.rs
new file mode 100644
index 0000000..2d932fb
--- /dev/null
+++ b/gm-control-panel/src/config.rs
@@ -0,0 +1,184 @@
+use crate::types::Player;
+use serde::{Deserialize, Serialize};
+use std::{
+ collections::HashMap,
+ fs::File,
+ io::{ErrorKind, Read},
+ path::PathBuf,
+};
+use thiserror::Error;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+enum OptionNames {
+ Language,
+ MusicPath,
+ PlaylistDatabasePath,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum ConfigOption {
+ Language(Language),
+ MusicPath(MusicPath),
+ PlaylistDatabasePath(PlaylistDatabasePath),
+}
+
+#[derive(Debug, Error)]
+pub enum ConfigReadError {
+ #[error("Cannot read the configuration file: {0}")]
+ CannotRead(std::io::Error),
+ #[error("Cannot open the configuration file for reading: {0}")]
+ CannotOpen(std::io::Error),
+ #[error("Invalid json data found in the configurationfile: {0}")]
+ InvalidJSON(serde_json::Error),
+}
+
+#[derive(Clone, Debug)]
+pub struct Config {
+ config_path: PathBuf,
+ values: HashMap,
+}
+
+impl Config {
+ pub fn new(config_path: PathBuf) -> Self {
+ Self {
+ config_path,
+ values: HashMap::new(),
+ }
+ }
+
+ pub fn from_path(config_path: PathBuf) -> Result {
+ let mut settings = config_path.clone();
+ settings.push("config");
+
+ match File::open(settings) {
+ Ok(mut file) => {
+ let mut buf = String::new();
+ file.read_to_string(&mut buf)
+ .map_err(|err| ConfigReadError::CannotRead(err))?;
+ let values = serde_json::from_str(buf.as_ref())
+ .map_err(|err| ConfigReadError::InvalidJSON(err))?;
+ Ok(Self {
+ config_path,
+ values,
+ })
+ }
+ Err(io_err) => {
+ match io_err.kind() {
+ ErrorKind::NotFound => {
+ /* create the path and an empty file */
+ Ok(Self {
+ config_path,
+ values: HashMap::new(),
+ })
+ }
+ _ => Err(ConfigReadError::CannotOpen(io_err)),
+ }
+ }
+ }
+ }
+
+ pub fn set(&mut self, val: ConfigOption) {
+ let _ = match val {
+ ConfigOption::Language(_) => self.values.insert(OptionNames::Language, val),
+ ConfigOption::MusicPath(_) => self.values.insert(OptionNames::MusicPath, val),
+ ConfigOption::PlaylistDatabasePath(_) => {
+ self.values.insert(OptionNames::PlaylistDatabasePath, val)
+ }
+ };
+ }
+
+ pub fn get<'a, T>(&'a self) -> T
+ where
+ T: From<&'a Self>,
+ {
+ self.into()
+ }
+}
+
+/*
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct DatabasePath(PathBuf);
+
+impl std::ops::Deref for DatabasePath {
+ type Target = PathBuf;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl From<&Config> for DatabasePath {
+ fn from(config: &Config) -> Self {
+ match config.values.get(&OptionNames::DatabasePath) {
+ Some(ConfigOption::DatabasePath(path)) => path.clone(),
+ _ => DatabasePath(config.config_path.clone()),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Me(Player);
+
+impl From<&Config> for Option {
+ fn from(config: &Config) -> Self {
+ config
+ .values
+ .get(&OptionNames::Me)
+ .and_then(|val| match val {
+ ConfigOption::Me(me) => Some(me.clone()),
+ _ => None,
+ })
+ }
+}
+
+impl std::ops::Deref for Me {
+ type Target = Player;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+*/
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::types::Rank;
+ use cool_asserts::assert_matches;
+
+ #[test]
+ fn it_can_set_and_get_options() {
+ let mut config = Config::new(PathBuf::from("."));
+ config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
+ "fixtures/five_games",
+ ))));
+ config.set(ConfigOption::Me(Me(Player {
+ name: "Savanni".to_owned(),
+ rank: Some(Rank::Kyu(10)),
+ })));
+ }
+
+ #[test]
+ fn it_can_serialize_and_deserialize() {
+ let mut config = Config::new(PathBuf::from("."));
+ config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
+ "fixtures/five_games",
+ ))));
+ config.set(ConfigOption::Me(Me(Player {
+ name: "Savanni".to_owned(),
+ rank: Some(Rank::Kyu(10)),
+ })));
+ let s = serde_json::to_string(&config.values).unwrap();
+ println!("{}", s);
+ let values: HashMap = serde_json::from_str(s.as_ref()).unwrap();
+ println!("options: {:?}", values);
+
+ assert_matches!(values.get(&OptionNames::DatabasePath),
+ Some(ConfigOption::DatabasePath(db_path)) =>
+ assert_eq!(*db_path, config.get())
+ );
+
+ assert_matches!(values.get(&OptionNames::Me), Some(ConfigOption::Me(val)) =>
+ assert_eq!(Some(val.clone()), config.get())
+ );
+ }
+}
diff --git a/gm-control-panel/src/main.rs b/gm-control-panel/src/main.rs
new file mode 100644
index 0000000..cb63dc7
--- /dev/null
+++ b/gm-control-panel/src/main.rs
@@ -0,0 +1,48 @@
+use glib::{Continue, Sender};
+use gtk::prelude::*;
+use std::{
+ env,
+ sync::{Arc, RwLock},
+};
+
+mod app_window;
+use app_window::ApplicationWindow;
+
+#[derive(Clone, Debug)]
+pub enum Message {}
+
+#[derive(Clone)]
+pub struct Core {
+ tx: Arc>>>,
+}
+
+pub fn main() {
+ let app = adw::Application::builder()
+ .application_id("com.luminescent-dreams.gm-control-panel")
+ .build();
+
+ let runtime = tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .unwrap();
+
+ let core = Core {
+ tx: Arc::new(RwLock::new(None)),
+ };
+
+ app.connect_activate(move |app| {
+ let (gtk_tx, gtk_rx) =
+ gtk::glib::MainContext::channel::(gtk::glib::PRIORITY_DEFAULT);
+
+ *core.tx.write().unwrap() = Some(gtk_tx);
+
+ let window = ApplicationWindow::new(app);
+ window.window.present();
+
+ gtk_rx.attach(None, move |_msg| Continue(true));
+ });
+
+ let args: Vec = env::args().collect();
+ ApplicationExtManual::run_with_args(&app, &args);
+ runtime.shutdown_background();
+}