diff --git a/Cargo.lock b/Cargo.lock
index 4fa384863d98fdaffe027264f6fbd1a632fd7555..883d2ab8b80f374050c8c6298cda0f4be28dc2f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,6 +23,10 @@ name = "accesskit"
 version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a"
+dependencies = [
+ "enumn",
+ "serde",
+]
 
 [[package]]
 name = "accesskit_atspi_common"
@@ -134,6 +138,7 @@ dependencies = [
  "cfg-if",
  "getrandom",
  "once_cell",
+ "serde",
  "version_check",
  "zerocopy",
 ]
@@ -166,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
 dependencies = [
  "android-properties",
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "cc",
  "cesu8",
  "jni 0.21.1",
@@ -229,11 +234,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4"
 dependencies = [
  "clipboard-win",
+ "core-graphics",
+ "image",
  "log",
  "objc2",
  "objc2-app-kit",
  "objc2-foundation",
  "parking_lot",
+ "windows-sys 0.48.0",
  "x11rb",
 ]
 
@@ -578,9 +586,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.6.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "bitstream-io"
@@ -631,7 +642,7 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "353dc0fbd494ab1d066ffdff16f07acbea46ca63f507e093c07fdf2408d84300"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "bluez-generated",
  "dbus",
  "dbus-tokio",
@@ -661,7 +672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b668804e0728a09c83cd9b94c9e1176717ea5522e8a3cb3688c2ac9a5f6e137c"
 dependencies = [
  "async-trait",
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "bluez-async",
  "dashmap 6.1.0",
  "dbus",
@@ -683,9 +694,13 @@ dependencies = [
 
 [[package]]
 name = "built"
-version = "0.7.5"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
+checksum = "73848a43c5d63a1251d17adf6c2bf78aa94830e60a335a95eeea45d6ba9e1e4d"
+dependencies = [
+ "chrono",
+ "git2",
+]
 
 [[package]]
 name = "bumpalo"
@@ -764,7 +779,7 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "log",
  "polling",
  "rustix",
@@ -817,12 +832,6 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
-[[package]]
-name = "cfg_aliases"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
-
 [[package]]
 name = "cfg_aliases"
 version = "0.2.1"
@@ -1145,19 +1154,20 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
 
 [[package]]
 name = "ecolor"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5"
+checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76"
 dependencies = [
  "bytemuck",
  "emath",
+ "serde",
 ]
 
 [[package]]
 name = "eframe"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2f2d9e7ea2d11ec9e98a8683b6eb99f9d7d0448394ef6e0d6d91bd4eb817220"
+checksum = "eba4c50d905804fe9ec4e159fde06b9d38f9440228617ab64a03d7a2091ece63"
 dependencies = [
  "ahash",
  "bytemuck",
@@ -1166,7 +1176,7 @@ dependencies = [
  "egui-wgpu",
  "egui-winit",
  "egui_glow",
- "glow 0.16.0",
+ "glow",
  "glutin",
  "glutin-winit",
  "image",
@@ -1193,24 +1203,26 @@ dependencies = [
 
 [[package]]
 name = "egui"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e"
+checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6"
 dependencies = [
  "accesskit",
  "ahash",
+ "bitflags 2.8.0",
  "emath",
  "epaint",
  "log",
  "nohash-hasher",
  "profiling",
+ "serde",
 ]
 
 [[package]]
 name = "egui-wgpu"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26c1e821d2d8921ef6ce98b258c7e24d9d6aab2ca1f9cdf374eca997e7f67f59"
+checksum = "6d8151704bcef6271bec1806c51544d70e79ef20e8616e5eac01facfd9c8c54a"
 dependencies = [
  "ahash",
  "bytemuck",
@@ -1228,13 +1240,14 @@ dependencies = [
 
 [[package]]
 name = "egui-winit"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e84c2919cd9f3a38a91e8f84ac6a245c19251fd95226ed9fae61d5ea564fce3"
+checksum = "ace791b367c1f63e6044aef2f3834904509d1d1a6912fd23ebf3f6a9af92cd84"
 dependencies = [
  "accesskit_winit",
  "ahash",
  "arboard",
+ "bytemuck",
  "egui",
  "log",
  "profiling",
@@ -1247,9 +1260,9 @@ dependencies = [
 
 [[package]]
 name = "egui_extras"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d7a8198c088b1007108cb2d403bc99a5e370999b200db4f14559610d7330126"
+checksum = "b5b5cf69510eb3d19211fc0c062fb90524f43fe8e2c012967dcf0e2d81cb040f"
 dependencies = [
  "ahash",
  "egui",
@@ -1264,14 +1277,14 @@ dependencies = [
 
 [[package]]
 name = "egui_glow"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eaf6264cc7608e3e69a7d57a6175f438275f1b3889c1a551b418277721c95e6"
+checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0"
 dependencies = [
  "ahash",
  "bytemuck",
  "egui",
- "glow 0.16.0",
+ "glow",
  "log",
  "memoffset",
  "profiling",
@@ -1280,6 +1293,19 @@ dependencies = [
  "winit",
 ]
 
+[[package]]
+name = "egui_tiles"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67756b63b283a65bd0534b0c2a5fb1a12a5768bb6383d422147cc93193d09cfc"
+dependencies = [
+ "ahash",
+ "egui",
+ "itertools 0.13.0",
+ "log",
+ "serde",
+]
+
 [[package]]
 name = "ehttp"
 version = "0.5.0"
@@ -1302,11 +1328,12 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
 
 [[package]]
 name = "emath"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d"
+checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2"
 dependencies = [
  "bytemuck",
+ "serde",
 ]
 
 [[package]]
@@ -1366,11 +1393,22 @@ dependencies = [
  "syn 2.0.95",
 ]
 
+[[package]]
+name = "enumn"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.95",
+]
+
 [[package]]
 name = "epaint"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5666f8d25236293c966fbb3635eac18b04ad1914e3bab55bc7d44b9980cafcac"
+checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915"
 dependencies = [
  "ab_glyph",
  "ahash",
@@ -1382,13 +1420,14 @@ dependencies = [
  "nohash-hasher",
  "parking_lot",
  "profiling",
+ "serde",
 ]
 
 [[package]]
 name = "epaint_default_fonts"
-version = "0.30.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66f6ddac3e6ac6fd4c3d48bb8b1943472f8da0f43a4303bcd8a18aa594401c80"
+checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6"
 
 [[package]]
 name = "equivalent"
@@ -1729,26 +1768,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 
 [[package]]
-name = "gl_generator"
-version = "0.14.0"
+name = "git2"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
 dependencies = [
- "khronos_api",
+ "bitflags 2.8.0",
+ "libc",
+ "libgit2-sys",
  "log",
- "xml-rs",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
 ]
 
 [[package]]
-name = "glow"
-version = "0.14.2"
+name = "gl_generator"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
 dependencies = [
- "js-sys",
- "slotmap",
- "wasm-bindgen",
- "web-sys",
+ "khronos_api",
+ "log",
+ "xml-rs",
 ]
 
 [[package]]
@@ -1769,8 +1811,8 @@ version = "0.32.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848"
 dependencies = [
- "bitflags 2.6.0",
- "cfg_aliases 0.2.1",
+ "bitflags 2.8.0",
+ "cfg_aliases",
  "cgl",
  "core-foundation 0.9.4",
  "dispatch",
@@ -1794,7 +1836,7 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f"
 dependencies = [
- "cfg_aliases 0.2.1",
+ "cfg_aliases",
  "glutin",
  "raw-window-handle",
  "winit",
@@ -1835,7 +1877,7 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "gpu-alloc-types",
 ]
 
@@ -1845,7 +1887,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
 ]
 
 [[package]]
@@ -1854,7 +1896,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "gpu-descriptor-types",
  "hashbrown 0.15.2",
 ]
@@ -1865,7 +1907,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
 ]
 
 [[package]]
@@ -2524,6 +2566,20 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "libgit2-sys"
+version = "0.18.0+1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
 [[package]]
 name = "libloading"
 version = "0.8.6"
@@ -2546,7 +2602,7 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "libc",
  "redox_syscall 0.5.8",
 ]
@@ -2562,6 +2618,32 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "libssh2-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -2710,11 +2792,11 @@ dependencies = [
 
 [[package]]
 name = "metal"
-version = "0.29.0"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21"
+checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block",
  "core-graphics-types",
  "foreign-types",
@@ -2821,22 +2903,23 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
 
 [[package]]
 name = "naga"
-version = "23.1.0"
+version = "24.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
+checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e"
 dependencies = [
  "arrayvec",
  "bit-set",
- "bitflags 2.6.0",
- "cfg_aliases 0.1.1",
+ "bitflags 2.8.0",
+ "cfg_aliases",
  "codespan-reporting",
  "hexf-parse",
  "indexmap",
  "log",
  "rustc-hash",
  "spirv",
+ "strum",
  "termcolor",
- "thiserror 1.0.69",
+ "thiserror 2.0.9",
  "unicode-xid",
 ]
 
@@ -2846,7 +2929,7 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "jni-sys",
  "log",
  "ndk-sys 0.6.0+11769913",
@@ -2902,9 +2985,9 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "cfg-if",
- "cfg_aliases 0.2.1",
+ "cfg_aliases",
  "libc",
  "memoffset",
 ]
@@ -3050,7 +3133,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "libc",
  "objc2",
@@ -3066,7 +3149,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "objc2",
  "objc2-core-location",
@@ -3090,7 +3173,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "objc2",
  "objc2-foundation",
 ]
@@ -3101,7 +3184,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "objc2",
  "objc2-foundation",
@@ -3143,7 +3226,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "dispatch",
  "libc",
@@ -3168,7 +3251,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "objc2",
  "objc2-foundation",
@@ -3180,7 +3263,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "objc2",
  "objc2-foundation",
@@ -3203,7 +3286,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "objc2",
  "objc2-cloud-kit",
@@ -3235,7 +3318,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "objc2",
  "objc2-core-location",
@@ -3257,6 +3340,24 @@ version = "1.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
 
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "orbclient"
 version = "0.3.48"
@@ -3266,6 +3367,15 @@ dependencies = [
  "libredox",
 ]
 
+[[package]]
+name = "ordered-float"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "ordered-stream"
 version = "0.2.0"
@@ -3810,7 +3920,7 @@ version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
 ]
 
 [[package]]
@@ -3967,7 +4077,7 @@ version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "fallible-iterator",
  "fallible-streaming-iterator",
  "hashlink",
@@ -4003,7 +4113,7 @@ version = "0.38.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -4211,7 +4321,7 @@ version = "4.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "779e2977f0cc2ff39708fef48f96f3768ac8ddd8c6caaaab82e83bd240ef99b2"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "cfg-if",
  "core-foundation 0.10.0",
  "core-foundation-sys",
@@ -4340,7 +4450,7 @@ version = "0.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "calloop",
  "calloop-wayland-source",
  "cursor-icon",
@@ -4401,7 +4511,7 @@ version = "0.3.0+sdk-1.3.268.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
 ]
 
 [[package]]
@@ -4442,6 +4552,28 @@ dependencies = [
  "float-cmp",
 ]
 
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.95",
+]
+
 [[package]]
 name = "subtle"
 version = "2.6.1"
@@ -5101,9 +5233,9 @@ dependencies = [
 
 [[package]]
 name = "walkers"
-version = "0.33.0"
+version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13954bb3d2611f8f6ddb35a9461632b307ac51adf89ef8fe39998adf2a2342c"
+checksum = "e15ae9bf81b8cf852ecacf362a5d720ba0b523add2c806ffb3c57731d259c43a"
 dependencies = [
  "egui",
  "egui_extras",
@@ -5226,7 +5358,7 @@ version = "0.31.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "rustix",
  "wayland-backend",
  "wayland-scanner",
@@ -5238,7 +5370,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "cursor-icon",
  "wayland-backend",
 ]
@@ -5260,7 +5392,7 @@ version = "0.32.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "wayland-backend",
  "wayland-client",
  "wayland-scanner",
@@ -5272,7 +5404,7 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "wayland-backend",
  "wayland-client",
  "wayland-protocols",
@@ -5285,7 +5417,7 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "wayland-backend",
  "wayland-client",
  "wayland-protocols",
@@ -5376,12 +5508,13 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
 
 [[package]]
 name = "wgpu"
-version = "23.0.1"
+version = "24.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a"
+checksum = "47f55718f85c2fa756edffa0e7f0e0a60aba463d1362b57e23123c58f035e4b6"
 dependencies = [
  "arrayvec",
- "cfg_aliases 0.1.1",
+ "bitflags 2.8.0",
+ "cfg_aliases",
  "document-features",
  "js-sys",
  "log",
@@ -5401,14 +5534,14 @@ dependencies = [
 
 [[package]]
 name = "wgpu-core"
-version = "23.0.1"
+version = "24.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a"
+checksum = "82a39b8842dc9ffcbe34346e3ab6d496b32a47f6497e119d762c97fcaae3cb37"
 dependencies = [
  "arrayvec",
  "bit-vec",
- "bitflags 2.6.0",
- "cfg_aliases 0.1.1",
+ "bitflags 2.8.0",
+ "cfg_aliases",
  "document-features",
  "indexmap",
  "log",
@@ -5419,26 +5552,26 @@ dependencies = [
  "raw-window-handle",
  "rustc-hash",
  "smallvec",
- "thiserror 1.0.69",
+ "thiserror 2.0.9",
  "wgpu-hal",
  "wgpu-types",
 ]
 
 [[package]]
 name = "wgpu-hal"
-version = "23.0.1"
+version = "24.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821"
+checksum = "5a782e5056b060b0b4010881d1decddd059e44f2ecd01e2db2971b48ad3627e5"
 dependencies = [
  "android_system_properties",
  "arrayvec",
  "ash",
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block",
  "bytemuck",
- "cfg_aliases 0.1.1",
+ "cfg_aliases",
  "core-graphics-types",
- "glow 0.14.2",
+ "glow",
  "glutin_wgl_sys",
  "gpu-alloc",
  "gpu-descriptor",
@@ -5452,13 +5585,14 @@ dependencies = [
  "ndk-sys 0.5.0+25.2.9519653",
  "objc",
  "once_cell",
+ "ordered-float",
  "parking_lot",
  "profiling",
  "raw-window-handle",
  "renderdoc-sys",
  "rustc-hash",
  "smallvec",
- "thiserror 1.0.69",
+ "thiserror 2.0.9",
  "wasm-bindgen",
  "web-sys",
  "wgpu-types",
@@ -5467,12 +5601,13 @@ dependencies = [
 
 [[package]]
 name = "wgpu-types"
-version = "23.0.0"
+version = "24.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068"
+checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "js-sys",
+ "log",
  "web-sys",
 ]
 
@@ -5995,11 +6130,11 @@ dependencies = [
  "ahash",
  "android-activity",
  "atomic-waker",
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "block2",
  "bytemuck",
  "calloop",
- "cfg_aliases 0.2.1",
+ "cfg_aliases",
  "concurrent-queue",
  "core-foundation 0.9.4",
  "core-graphics",
@@ -6123,7 +6258,7 @@ version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
 dependencies = [
- "bitflags 2.6.0",
+ "bitflags 2.8.0",
  "dlib",
  "log",
  "once_cell",
@@ -6159,13 +6294,16 @@ name = "yamm"
 version = "0.1.0"
 dependencies = [
  "bincode",
+ "built",
  "chrono",
  "chrono-humanize",
  "eframe",
  "egui",
  "egui_extras",
+ "egui_tiles",
  "fern",
  "figment",
+ "git2",
  "image",
  "lazy_static",
  "log",
diff --git a/Cargo.toml b/Cargo.toml
index 54b538ce9fcf20b2587f0a1b71ec4b985fa3af66..6e3c3676f0bd1fe431ffceadad5d72edf3497fdf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,8 @@
 name = "yamm"
 version = "0.1.0"
 edition = "2021"
+authors = ["Volkor <me@volkor.me>"]
+build = "build.rs"
 
 [dependencies]
 fern = "0.7.1"
@@ -19,10 +21,15 @@ bincode = "1.3.3"
 chrono-humanize = "0.2.3"
 
 # gui
-eframe = { version = "0.30.0", features = ["wgpu"] }
-egui = "0.30.0"
-walkers = "0.33.0"
-egui_extras = { version = "0.30.0", features = ["all_loaders"] }
+eframe = { version = "0.31.0", features = ["wgpu"] }
+egui = "0.31.0"
+walkers = "0.34.0"
+egui_extras = { version = "0.31.0", features = ["all_loaders"] }
 image = { version = "0.25", features = ["jpeg", "png", "webp"] }
 chrono = "0.4.39"
 longitude = "0.2.1"
+egui_tiles = "0.12.0"
+
+[build-dependencies]
+built = { version = "0.7.6", features = ["git2", "chrono"] }
+git2 = "0.20.0"
\ No newline at end of file
diff --git a/assets/icons/lan-connect.svg b/assets/icons/lan-connect.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4dd9b2d5cc7a613c823832bfde12bab9d6d65bcf
--- /dev/null
+++ b/assets/icons/lan-connect.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M3,13V18L3,20H10V18H5V13H3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M14,15H20V19H14V15Z" /></svg>
\ No newline at end of file
diff --git a/assets/icons/lan-disconnect.svg b/assets/icons/lan-disconnect.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5cbdfbdd4c336d605a2c87be8ed9b72c6cb2b0bb
--- /dev/null
+++ b/assets/icons/lan-disconnect.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3.88,13.46L2.46,14.88L4.59,17L2.46,19.12L3.88,20.54L6,18.41L8.12,20.54L9.54,19.12L7.41,17L9.54,14.88L8.12,13.46L6,15.59L3.88,13.46M14,15H20V19H14V15Z" /></svg>
\ No newline at end of file
diff --git a/assets/icons/lan-pending.svg b/assets/icons/lan-pending.svg
new file mode 100644
index 0000000000000000000000000000000000000000..48946b442033f978daf467818591802bfa19458c
--- /dev/null
+++ b/assets/icons/lan-pending.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M3,12V14H5V12H3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3,15V17H5V15H3M14,15H20V19H14V15M3,18V20H5V18H3M6,18V20H8V18H6M9,18V20H11V18H9Z" /></svg>
\ No newline at end of file
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1723685917a02888c13b3a580d74a837c016e5d9
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+    built::write_built_file().expect("Failed to acquire build-time information");
+}
\ No newline at end of file
diff --git a/docs/gui.md b/docs/gui.md
index 5b2df1f9012187f286a38cb60b7b0b474efbe8d4..a114ee8ab03978c6bedf4eecb793ae7a4002e7de 100644
--- a/docs/gui.md
+++ b/docs/gui.md
@@ -7,4 +7,8 @@
     - https://crates.io/crates/egui-toast ?
 - Nice graphs
     - https://github.com/emilk/egui_plot
-    - use https://github.com/hacknus/serial-monitor-rust they have nice plots, we should steal it
\ No newline at end of file
+    - use https://github.com/hacknus/serial-monitor-rust they have nice plots, we should steal it
+- 'Tiling' UI, make it piss easy to change the UI to each user's liking.
+    - Pop out windows to new ones? Can we also tile them??
+    - List of Presets, with naming?
+    - One single context/alt/top bar for file, edit, whatever.
\ No newline at end of file
diff --git a/src/gui.rs b/src/gui.rs
deleted file mode 100644
index 38824fca44e2028ea274f534be0abce9b8eac3aa..0000000000000000000000000000000000000000
--- a/src/gui.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-use std::str::FromStr;
-
-use chrono::DateTime;
-use egui_extras::{Column, TableBuilder};
-use meshtastic::protobufs::{config::device_config::Role, HardwareModel};
-use tracing::debug;
-use chrono_humanize::{self, HumanTime};
-use longitude::{Distance, DistanceUnit, Location};
-use meshtastic::protobufs::Position;
-
-use crate::{db::Database, CONFIG};
-
-/// Calculates the distance betwen 2 coordinate points.
-fn calc_distance_to_node(away: Position) -> Option<f64> {
-    let home_pos = Database::read_home_node().expect("Failed to find Home Node").position.unwrap();
-
-    let distance = Location {
-        // Home Base
-        latitude: home_pos.latitude_i as f64 * 1e-7,
-        longitude: home_pos.longitude_i as f64 * 1e-7,
-    }.distance(&Location {
-        // Away
-        latitude: away.latitude_i as f64 * 1e-7,
-        longitude: away.longitude_i as f64 * 1e-7,
-    });
-    // convert to the right distance depending on settings
-    match CONFIG.gui.units.as_ref() {
-        "Centimeters" => Some(distance.in_unit(DistanceUnit::Centimeters)),
-        "Meters" => Some(distance.in_unit(DistanceUnit::Meters)),
-        "Kilometers" => Some(distance.in_unit(DistanceUnit::Kilometers)),
-        "Inches" => Some(distance.in_unit(DistanceUnit::Inches)),
-        "Feet" => Some(distance.in_unit(DistanceUnit::Feet)),
-        "Yards" => Some(distance.in_unit(DistanceUnit::Yards)),
-        "Miles" => Some(distance.in_unit(DistanceUnit::Miles)),
-        _ => Some(distance.in_unit(DistanceUnit::Kilometers)), // if you can't type, force km.
-    }
-}
-
-pub(crate) async fn run_gui() {
-    debug!("Running in GUI Mode!");
-    use eframe::{App, egui};
-
-    struct MyApp;
-
-    impl App for MyApp {
-        fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
-            // Table Panel, showing nodes in a list.
-            egui::CentralPanel::default().show(ctx, |ui| {
-                // Get nodes from DB
-                let mut nodes = Database::read_node_list().expect("Failed to read nodes");
-
-                // Sort nodes by `last_heard` in descending order
-                nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
-
-                TableBuilder::new(ui)
-                // TODO for the table:
-                // - Clickable Table Sorting
-                // - Add more info
-                // - Auto hide/show scrollbar on smaller devices
-                // - Add to a panel that we can move around / shitty windowing.
-                    .column(Column::remainder())            // node id
-                    .column(Column::remainder().clip(true)) // node name
-                    .column(Column::remainder())            // snr
-                    .column(Column::remainder())            // dist
-                    .column(Column::remainder())            // hops
-                    .column(Column::remainder().clip(true))            // hw model
-                    .column(Column::remainder())            // role
-                    .column(Column::remainder())            // battery
-                    .column(Column::remainder().clip(true))            // last heard
-                    .striped(true)
-                    .header(15.0, |mut header| {
-                        header.col(|ui| {
-                            ui.heading("Node ID");
-                        });
-                        header.col(|ui| {
-                            ui.heading("Name");
-                        });
-                        header.col(|ui| {
-                            ui.heading("SNR");
-                        });
-                        header.col(|ui| {
-
-                            // Get distance units
-                            let unit = match CONFIG.gui.units.as_ref() {
-                                "Centimeters" => "cm",
-                                "Meters" => "m",
-                                "Kilometers" => "km",
-                                "Inches" => "in",
-                                "Feet" => "ft",
-                                "Yards" => "yd",
-                                "Miles" => "mi",
-                                _ => "km", // Default to km if no match found.
-                            };
-
-                            ui.heading(format!("Distance ({})", unit));
-                        });
-                        header.col(|ui| {
-                            ui.heading("Hops Away");
-                        });
-                        header.col(|ui| {
-                            ui.heading("HW Model");
-                        });
-                        header.col(|ui| {
-                            ui.heading("Role");
-                        });
-                        header.col(|ui| {
-                            ui.heading("Battery");
-                        });
-                        header.col(|ui| {
-                            ui.heading("Last Heard");
-                        });
-                    })
-                    .body(|mut body| {
-                        for node in nodes {
-                            let user = node.user.unwrap();
-                            let device = node.device_metrics.unwrap();
-                            // Convert the last heard to a time, then make it nice to read.
-                            let now = chrono::Local::now().to_utc();
-                            let humanised = if node.last_heard == 0 {
-                                String::from("Unknown")
-                            } else {
-                                let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap();
-                                let duration = last_heard - now;
-                                HumanTime::from(duration).to_string()
-                            };
-
-                            body.row(15.0, |mut row| {
-                                row.col(|ui| {
-                                    ui.label(user.id);
-                                });
-                                row.col(|ui| {
-                                    ui.label(format!("{} ({})", user.long_name, user.short_name));
-                                });
-                                row.col(|ui| {
-                                    ui.label(node.snr.to_string());
-                                });
-                                row.col(|ui| {
-                                    if let Some(distance) = calc_distance_to_node(node.position.unwrap()) {
-                                        if distance.is_nan() {
-                                            ui.label("-");
-                                        } else {
-                                            ui.label(format!("{:.2}", distance));
-                                        }
-                                    } else {
-                                        ui.label("-");
-                                    }
-                                });
-                                row.col(|ui| {
-                                    ui.label(node.hops_away.to_string());
-                                });
-                                row.col(|ui| {
-                                    // Convert the i32 to a HardwareModel object, then convert to string
-                                    ui.label(HardwareModel::from_i32(user.hw_model).unwrap_or_default().as_str_name());
-                                });
-                                row.col(|ui| {
-                                    // Convert the i32 to a Role object, convert to string.
-                                    ui.label(Role::from_i32(user.role).unwrap_or_default().as_str_name());
-                                });
-                                row.col(|ui| {
-                                    ui.label(device.battery_level.to_string());
-                                });
-                                row.col(|ui| {
-                                    ui.label(humanised);
-                                });
-                            });
-                        }
-                    });
-            });
-        }
-    }
-
-    let native_options = eframe::NativeOptions::default();
-    eframe::run_native(
-        "Yet Another Meshtastic Server (GUI)",
-        native_options,
-        Box::new(|_cc| Ok(Box::new(MyApp))),
-    ).unwrap();
-}
\ No newline at end of file
diff --git a/src/gui/context_menu.rs b/src/gui/context_menu.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c064bbe40d070acfc12bf0005fdcbf21bd366bec
--- /dev/null
+++ b/src/gui/context_menu.rs
@@ -0,0 +1,81 @@
+/// This controls the big bar at the top
+/// you know, the one that has File, Edit, View, whatever else?
+/// that one
+
+#[derive(Clone, Default, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+pub struct ContextMenus {}
+
+impl crate::Demo for ContextMenus {
+    fn name(&self) -> &'static str {
+        "☰ Context Menus"
+    }
+
+    fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
+        use crate::View;
+        egui::Window::new(self.name())
+            .vscroll(false)
+            .resizable(false)
+            .open(open)
+            .show(ctx, |ui| self.ui(ui));
+    }
+}
+
+impl crate::View for ContextMenus {
+    fn ui(&mut self, ui: &mut egui::Ui) {
+        ui.horizontal(|ui| {
+            ui.menu_button("Click for menu", Self::nested_menus);
+
+            ui.button("Right-click for menu")
+                .context_menu(Self::nested_menus);
+
+            if ui.ctx().is_context_menu_open() {
+                ui.label("Context menu is open");
+            } else {
+                ui.label("Context menu is closed");
+            }
+        });
+
+        ui.vertical_centered(|ui| {
+            ui.add(crate::egui_github_link_file!());
+        });
+    }
+}
+
+impl ContextMenus {
+    fn nested_menus(ui: &mut egui::Ui) {
+        ui.set_max_width(200.0); // To make sure we wrap long text
+
+        if ui.button("Open…").clicked() {
+            ui.close_menu();
+        }
+        ui.menu_button("SubMenu", |ui| {
+            ui.menu_button("SubMenu", |ui| {
+                if ui.button("Open…").clicked() {
+                    ui.close_menu();
+                }
+                let _ = ui.button("Item");
+            });
+            ui.menu_button("SubMenu", |ui| {
+                if ui.button("Open…").clicked() {
+                    ui.close_menu();
+                }
+                let _ = ui.button("Item");
+            });
+            let _ = ui.button("Item");
+            if ui.button("Open…").clicked() {
+                ui.close_menu();
+            }
+        });
+        ui.menu_button("SubMenu", |ui| {
+            let _ = ui.button("Item1");
+            let _ = ui.button("Item2");
+            let _ = ui.button("Item3");
+            let _ = ui.button("Item4");
+            if ui.button("Open…").clicked() {
+                ui.close_menu();
+            }
+        });
+        let _ = ui.button("Very long text for this item that should be wrapped");
+    }
+}
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..abc0769f328873a3bd40c3843ab0852016cc4be0
--- /dev/null
+++ b/src/gui/mod.rs
@@ -0,0 +1,446 @@
+pub mod widgets;
+use crate::gui::widgets::WidgetType::About;
+use egui::{menu, Color32, FontDefinitions};
+use egui_tiles::{SimplificationOptions, Tile, TileId, Tiles};
+use tracing::debug;
+
+/// Runs the actual GUI.
+/// We contruct the empty tree, headers and basic window stuff here.
+/// Widgets are all opened via this, or loaded from a saved state.
+pub(crate) async fn run_gui() -> Result<(), eframe::Error> {
+    debug!("Running in GUI Mode!");
+    // run actual ui
+    let options = eframe::NativeOptions {
+        viewport: egui::ViewportBuilder::default().with_inner_size([1920.0, 1080.0]),
+        ..Default::default()
+    };
+    eframe::run_native(
+        "Y(et) A(nother) M(eshtastic) S(erver) - v0.1.0",
+        options,
+        Box::new(|_cc| {
+            #[cfg_attr(not(feature = "serde"), allow(unused_mut))]
+            let mut app = YamsApp::default();
+            #[cfg(feature = "serde")]
+            if let Some(storage) = _cc.storage {
+                if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
+                    app = state;
+                }
+            }
+            Ok(Box::new(app))
+        }),
+    )
+}
+
+/// This allows us to add new widgets into the UI.
+impl YamsApp {
+    // Add a new pane to the tree, depending on the widget_type.
+    fn add_about(&mut self, widget_type: widgets::WidgetType,) {
+        // Find the next free id. (maybe?)
+        let next_view_num = self.tree.tiles.next_free_id().0 as usize;
+        // Create a new pane with said view number.
+        let pane = Pane::with_nr(next_view_num, widget_type);
+        // Insert it!
+        let _ = self.tree.tiles.insert_pane(pane);
+    }
+}
+
+// Everything below here I copied directly from the egui_tiles example, I am adapting it. thanks.
+
+/// This is the default state for the egui environment.
+impl Default for YamsApp {
+    fn default() -> Self {
+        // Initialize a variable to keep track of the next view number
+        let mut next_view_nr = 0;
+        // Define a closure that will create a new Pane with a unique view number
+        // and increment the next_view_nr variable for the next time it's called
+        let mut gen_view = || {
+            let view = Pane::with_nr(next_view_nr, widgets::WidgetType::NodeList);
+            next_view_nr += 1;
+            view
+        };
+
+        // Create an empty tiles structure to store a tile (which can contain more tiles)
+        let mut tiles = egui_tiles::Tiles::default();
+
+        // Vector to hold the ids of our tabs
+        let mut tabs = vec![];
+
+        // Insert a tab tile with 7 children into our tiles and push its id onto the tabs vector
+        let tab_tile = {
+            let children = (0..7).map(|_| tiles.insert_pane(gen_view())).collect();
+            tiles.insert_tab_tile(children)
+        };
+        tabs.push(tab_tile);
+
+        // Insert a horizontal tile with 7 children into our tiles and push its id onto the tabs vector
+        tabs.push({
+            let children = (0..7).map(|_| tiles.insert_pane(gen_view())).collect();
+            tiles.insert_horizontal_tile(children)
+        });
+
+        // Insert a vertical tile with 7 children into our tiles and push its id onto the tabs vector
+        tabs.push({
+            let children = (0..7).map(|_| tiles.insert_pane(gen_view())).collect();
+            tiles.insert_vertical_tile(children)
+        });
+
+        // Insert a grid tile with 11 cells into our tiles and push its id onto the tabs vector
+        tabs.push({
+            let cells = (0..11).map(|_| tiles.insert_pane(gen_view())).collect();
+            tiles.insert_grid_tile(cells)
+        });
+
+        // Insert a single pane tile into our tiles and push its id onto the tabs vector
+        tabs.push(tiles.insert_pane(gen_view()));
+
+        // Insert a tab tile with all of our tabs as children into our tiles
+        let root = tiles.insert_tab_tile(tabs);
+
+        // Create an egui_tiles::Tree structure to represent our tree of views, starting at the root id
+        let tree = egui_tiles::Tree::new("my_tree", root, tiles);
+
+        // Return a new YamsApp instance with our tree and default behavior
+        Self {
+            tree,
+            behavior: Default::default(),
+        }
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+/// A struct representing a single pane in the GUI.
+/// Each pane has a unique number associated with it.
+pub struct Pane {
+    /// The unique number of this pane.
+    pub nr: usize,
+    pub widget_type: widgets::WidgetType,
+}
+
+impl std::fmt::Debug for Pane {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        // Write out the debug representation of the Pane struct.
+        f.debug_struct("Pane")
+            .field("nr", &self.nr) // Include the pane's number in the output.
+            .finish()
+    }
+}
+
+impl Pane {
+    /// Creates a new Pane with a unique view number.
+    pub fn with_nr(nr: usize, widget_type: widgets::WidgetType) -> Self {
+        Self { nr, widget_type }
+    }
+    /// Draws the UI for the Pane.
+    /// This is where we actually put stuff into our panes, like text or buttons.
+    /// We also handle dragging of the pane here.
+    pub fn ui(&self, ui: &mut egui::Ui) -> egui_tiles::UiResponse {
+
+        // Pass the widget type and ui object down for rendering of the correct type.
+        widgets::render_widget(&self.widget_type, ui);
+        
+        // We don't drag these boys. (surely I can remove this?)
+        egui_tiles::UiResponse::None
+    }
+}
+
+// The storage for the Tree Behaviour config options
+struct TreeBehavior {
+    simplification_options: egui_tiles::SimplificationOptions,
+    tab_bar_height: f32,
+    gap_width: f32,
+    add_child_to: Option<egui_tiles::TileId>,
+}
+
+// The defaults for how the tree should behave.
+impl Default for TreeBehavior {
+    fn default() -> Self {
+        Self {
+            simplification_options: {
+                SimplificationOptions {
+                    all_panes_must_have_tabs: true,
+                    ..Default::default()
+                }
+            },
+            tab_bar_height: 24.0,
+            gap_width: 2.0,
+            add_child_to: None,
+        }
+    }
+}
+
+
+impl TreeBehavior {
+    /// The UI for changing TreeBehaviour at runtime
+    fn ui(&mut self, ui: &mut egui::Ui) {
+        let Self {
+            simplification_options,
+            tab_bar_height,
+            gap_width,
+            add_child_to: _,
+        } = self;
+
+        // egui::Grid::new("behavior_ui")
+        //     .num_columns(2)
+        //     .show(ui, |ui| {
+        //         ui.label("All panes must have tabs:");
+        //         ui.checkbox(&mut simplification_options.all_panes_must_have_tabs, "");
+        //         ui.end_row();
+
+        //         ui.label("Join nested containers:");
+        //         ui.checkbox(
+        //             &mut simplification_options.join_nested_linear_containers,
+        //             "",
+        //         );
+        //         ui.end_row();
+
+        //         ui.label("Tab bar height:");
+        //         ui.add(
+        //             egui::DragValue::new(tab_bar_height)
+        //                 .range(0.0..=100.0)
+        //                 .speed(1.0),
+        //         );
+        //         ui.end_row();
+
+        //         ui.label("Gap width:");
+        //         ui.add(egui::DragValue::new(gap_width).range(0.0..=20.0).speed(1.0));
+        //         ui.end_row();
+        //     });
+    }
+}
+
+
+impl egui_tiles::Behavior<Pane> for TreeBehavior {
+    fn pane_ui(
+        &mut self,
+        ui: &mut egui::Ui,
+        _tile_id: egui_tiles::TileId,
+        view: &mut Pane,
+    ) -> egui_tiles::UiResponse {
+        view.ui(ui)
+    }
+
+    fn tab_title_for_pane(&mut self, view: &Pane) -> egui::WidgetText {
+        // Use default Display for most names, but some need to have shit added!
+        let mut title = "".to_string();
+        match view.widget_type.clone() {
+            widgets::WidgetType::NodeInfo => {
+                title = format!("Node (SHORTNAME)");
+                // We need to get the nodes name.
+                // name should be "Node (SHORTNAME)"
+                // example: "Node (POCO)"
+            },
+            widgets::WidgetType::Log => {
+                title = format!("{}", widgets::WidgetType::Log)
+            },
+            widgets::WidgetType::MapView => {
+                title = format!("{}", widgets::WidgetType::MapView)
+            },
+            widgets::WidgetType::Settings => {
+                title = format!("{}", widgets::WidgetType::Settings)
+            },
+            widgets::WidgetType::SignalStrengthHistorical => {
+                title = format!("{}", widgets::WidgetType::SignalStrengthHistorical)
+            },
+            widgets::WidgetType::NodeList => {
+                title = format!("{}", widgets::WidgetType::NodeList)
+            },
+            // why is this the only one not bitching about the widgets:: crap?
+            About => {
+                title = format!("{}", widgets::WidgetType::About)
+            },
+        }
+        title.into()
+    }
+
+    fn top_bar_right_ui(
+        &mut self,
+        _tiles: &egui_tiles::Tiles<Pane>,
+        ui: &mut egui::Ui,
+        tile_id: egui_tiles::TileId,
+        _tabs: &egui_tiles::Tabs,
+        _scroll_offset: &mut f32,
+    ) {
+        if ui.button("➕").clicked() {
+            self.add_child_to = Some(tile_id);
+        }
+    }
+
+    // ---
+    // Settings:
+
+    fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
+        self.tab_bar_height
+    }
+
+    fn gap_width(&self, _style: &egui::Style) -> f32 {
+        self.gap_width
+    }
+
+    fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
+        self.simplification_options
+    }
+
+    fn is_tab_closable(&self, _tiles: &Tiles<Pane>, _tile_id: TileId) -> bool {
+        true
+    }
+
+    fn on_tab_close(&mut self, tiles: &mut Tiles<Pane>, tile_id: TileId) -> bool {
+        if let Some(tile) = tiles.get(tile_id) {
+            match tile {
+                Tile::Pane(pane) => {
+                    // Single pane removal
+                    let tab_title = self.tab_title_for_pane(pane);
+                    log::debug!("Closing tab: {}, tile ID: {tile_id:?}", tab_title.text());
+                }
+                Tile::Container(container) => {
+                    // Container removal
+                    log::debug!("Closing container: {:?}", container.kind());
+                    let children_ids = container.children();
+                    for child_id in children_ids {
+                        if let Some(Tile::Pane(pane)) = tiles.get(*child_id) {
+                            let tab_title = self.tab_title_for_pane(pane);
+                            log::debug!("Closing tab: {}, tile ID: {tile_id:?}", tab_title.text());
+                        }
+                    }
+                }
+            }
+        }
+
+        // Proceed to removing the tab
+        true
+    }
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+struct YamsApp {
+    tree: egui_tiles::Tree<Pane>,
+
+    #[cfg_attr(feature = "serde", serde(skip))]
+    behavior: TreeBehavior,
+}
+
+impl eframe::App for YamsApp {
+    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
+        // Initalise Image Loading (for icons)
+        egui_extras::install_image_loaders(ctx);
+
+        // Context/Menu bar at the top
+        egui::TopBottomPanel::top("context_bar").show(ctx, |ui| {
+            // menu bar sits in here
+            menu::bar(ui, |ui| {
+                ui.menu_button("File", |ui| {
+                    if ui.button("Exit").clicked() {
+                        std::process::exit(0);
+                    }
+                });
+
+                // List of widgets here, we can add new widgets into the panel from here!
+                ui.menu_button("Widgets", |ui| {
+                    // Nested menus here
+                    // Add Widget -> Widget Selector
+                });
+
+                // UI Tools (reset, split? layouts?)
+                ui.menu_button("View", |ui| {
+                    if ui.button("Reset Panels").clicked() {
+                        // Nothing yet
+                    }
+                });
+                // Help
+                ui.menu_button("Help", |ui|{
+                    if ui.button("Documentation").clicked() {
+                        // open git to the wiki / docs page
+                    }
+                    if ui.button("About").clicked() {
+                        // Create a new about widget (don't show it in the widgets menu?)
+                        // TODO: THIS DOES NOT WORK!
+                        self.add_about(widgets::WidgetType::About);
+                        
+                    }
+                });
+                // Right aligned, logos for showing general status
+                // Connection Status enum:
+                // ServerNotRunning: mdiLanDisconnect (message: "YAMS is not running")
+                // MeshNoConnection: mdiLanPending (message: "YAMS cannot connect to Meshtastic node")
+                // MeshConnected: mdiLanConnect (message: "YAMS is connected and receiving data")
+                // TODO
+                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                    ui.add(egui::Image::new(egui::include_image!("../../assets/icons/lan-connect.svg"))
+                        .bg_fill(Color32::GREEN)
+                        .alt_text("YAMS is connected and receiving data"))
+                        // outside the image object
+                        .on_hover_text_at_pointer("YAMS is connected and receiving data");
+                    if ui.button("Reset").clicked() {
+                        *self = Default::default();
+                    }
+                    self.behavior.ui(ui);
+                });
+            });
+        });
+
+        egui::CentralPanel::default().show(ctx, |ui| {
+            self.tree.ui(&mut self.behavior, ui);
+        });
+    }
+
+    fn save(&mut self, _storage: &mut dyn eframe::Storage) {
+        #[cfg(feature = "serde")]
+        eframe::set_value(_storage, eframe::APP_KEY, &self);
+    }
+}
+
+fn tree_ui(
+    ui: &mut egui::Ui,
+    behavior: &mut dyn egui_tiles::Behavior<Pane>,
+    tiles: &mut egui_tiles::Tiles<Pane>,
+    tile_id: egui_tiles::TileId,
+) {
+    // Get the name BEFORE we remove the tile below!
+    let text = format!(
+        "{} - {tile_id:?}",
+        behavior.tab_title_for_tile(tiles, tile_id).text()
+    );
+
+    // Temporarily remove the tile to circumvent the borrowchecker
+    let Some(mut tile) = tiles.remove(tile_id) else {
+        log::debug!("Missing tile {tile_id:?}");
+        return;
+    };
+
+    let default_open = true;
+    egui::collapsing_header::CollapsingState::load_with_default_open(
+        ui.ctx(),
+        ui.id().with((tile_id, "tree")),
+        default_open,
+    )
+    .show_header(ui, |ui| {
+        ui.label(text);
+        let mut visible = tiles.is_visible(tile_id);
+        ui.checkbox(&mut visible, "Visible");
+        tiles.set_visible(tile_id, visible);
+    })
+    .body(|ui| match &mut tile {
+        egui_tiles::Tile::Pane(_) => {}
+        egui_tiles::Tile::Container(container) => {
+            let mut kind = container.kind();
+            egui::ComboBox::from_label("Kind")
+                .selected_text(format!("{kind:?}"))
+                .show_ui(ui, |ui| {
+                    for typ in egui_tiles::ContainerKind::ALL {
+                        ui.selectable_value(&mut kind, typ, format!("{typ:?}"))
+                            .clicked();
+                    }
+                });
+            if kind != container.kind() {
+                container.set_kind(kind);
+            }
+
+            for &child in container.children() {
+                tree_ui(ui, behavior, tiles, child);
+            }
+        }
+    });
+
+    // Put the tile back
+    tiles.insert(tile_id, tile);
+}
diff --git a/src/gui/widgets/about.rs b/src/gui/widgets/about.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5e76fd501bc796041e8812f2244e0cce9dae1825
--- /dev/null
+++ b/src/gui/widgets/about.rs
@@ -0,0 +1,71 @@
+use core::f32;
+
+use egui::{text::TextWrapping, Color32, Label, RichText, Separator};
+use egui_tiles::UiResponse;
+use crate::{built_info, gui::Pane};
+
+/// Shows information about the program.
+/// you know, the normal boring stuff
+/// Shows:
+/// - cool ascii logo
+/// - commit number / build hash or something
+/// - link to repo
+/// - contributors
+/// - random shit
+
+pub fn about_ui(ui: &mut egui::Ui) {
+    // Show cool ascii logo
+    // TODO: make this not wrap or truncate with ...
+    // double TODO: replace the logo with a cool ascii art of actual yams (vegetable) with text
+    let mut cool_logo = RichText::new("
+▓██   ██▓ ▄▄▄       ███▄ ▄███▓  ██████ 
+ ▒██  ██▒▒████▄    ▓██▒▀█▀ ██▒▒██    ▒ 
+  ▒██ ██░▒██  ▀█▄  ▓██    ▓██░░ ▓██▄   
+  ░ ▐██▓░░██▄▄▄▄██ ▒██    ▒██   ▒   ██▒
+  ░ ██▒▓░ ▓█   ▓██▒▒██▒   ░██▒▒██████▒▒
+   ██▒▒▒  ▒▒   ▓▒█░░ ▒░   ░  ░▒ ▒▓▒ ▒ ░
+ ▓██ ░▒░   ▒   ▒▒ ░░  ░      ░░ ░▒  ░ ░
+ ▒ ▒ ░░    ░   ▒   ░      ░   ░  ░  ░  
+ ░ ░           ░  ░       ░         ░  
+ ░ ░                                   
+    ")
+        .monospace();
+
+    // Change the colour to green if it's a release build.
+    let color = if built_info::PROFILE == "release" {
+        Color32::DARK_GREEN
+    } else {
+        Color32::DARK_RED
+    };
+
+    // Although we need to disable some stuff to make it not look like crap most of the time...
+    let text_wrapping = TextWrapping {
+        max_width: f32::MAX,
+        overflow_character: None,
+        ..Default::default()
+    };
+    
+    ui.add(Label::new(cool_logo.color(color)));
+
+    ui.separator();
+
+    ui.label(egui::RichText::new("Build Information").heading().strong());
+    
+    // Debug info
+    // TODO: use the text layout more directly, allow for bold midway through the message (the actual info)
+    ui.label(format!("Version {}-{} ({}), built for {} with {}. \nBuilt on {}",
+        built_info::PKG_VERSION,
+        built_info::PROFILE,
+        built_info::GIT_COMMIT_HASH_SHORT.unwrap_or_default(),
+        built_info::TARGET,
+        built_info::RUSTC_VERSION,
+        built_info::BUILT_TIME_UTC
+    ));
+
+
+    // Show link to repo
+    ui.hyperlink("https://git.volkor.me/Volkor/yams");
+
+    // Show authors/contributors
+    ui.label(format!("Contributors: {}", built_info::PKG_AUTHORS));
+}
\ No newline at end of file
diff --git a/src/gui/widgets/debug.rs b/src/gui/widgets/debug.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0f1b51d95e51dffaaced453c3ef3d305d175a1d4
--- /dev/null
+++ b/src/gui/widgets/debug.rs
@@ -0,0 +1 @@
+/// should steal the debug inspector / whatever else from egui examples.
\ No newline at end of file
diff --git a/src/gui/widgets/log.rs b/src/gui/widgets/log.rs
new file mode 100644
index 0000000000000000000000000000000000000000..67d043278fc397c5c6e0c6986cb7a40c2936d996
--- /dev/null
+++ b/src/gui/widgets/log.rs
@@ -0,0 +1,3 @@
+/// This widget shows all log output from the program.
+/// You should be able to open multiple of these, so we can have "Serial Log" and "YAMS Log" as two separate instances of this widget.
+/// Filtering by log level?
\ No newline at end of file
diff --git a/src/gui/widgets/map_view.rs b/src/gui/widgets/map_view.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e45c5bbc8e72c7f857a83bf1fe874c3c48067062
--- /dev/null
+++ b/src/gui/widgets/map_view.rs
@@ -0,0 +1,7 @@
+/// Shows a Leaflet map of all the nodes picked up.
+/// Options for:
+/// - Direct connections - coloured lines depending on signal strength?
+/// - Indirect connections - deducted from traceroutes / other messages / neighbourinfo
+/// - Hiding inactive nodes - set number of hours before greying out, days before hiding completely
+/// - List of nodes - optional filter for ones with no gps, but have connections (use neighbours / traceroutes to figure out general location?)
+/// - Position History - Shows a nice home assistant style history for each nodes position.
\ No newline at end of file
diff --git a/src/gui/widgets/message_list.rs b/src/gui/widgets/message_list.rs
new file mode 100644
index 0000000000000000000000000000000000000000..53c096ad43613331be29524b289b0dccdc5afdbb
--- /dev/null
+++ b/src/gui/widgets/message_list.rs
@@ -0,0 +1,5 @@
+/// Widget displays a list of all messages
+/// Specifically:
+/// - Sub tree of all channels (with >0 messages)
+/// - Filter messages by user (right click user? filters?)
+/// - dedicated filter for unknown encrypted messages?
diff --git a/src/gui/widgets/mod.rs b/src/gui/widgets/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e1d6d30c2c2ddaf0f62b828cafc451c2edf79564
--- /dev/null
+++ b/src/gui/widgets/mod.rs
@@ -0,0 +1,47 @@
+// List of widgets available.
+
+use std::fmt;
+pub mod about;
+pub mod node_list;
+
+#[derive(Clone)]
+pub(crate) enum WidgetType {
+    About,                      // Shows info relevant about YAMS itself.
+    NodeList,                   // Shows nodes in a table
+    NodeInfo,                   // Shows information about a specific node
+    Log,                        // Shows log output from yams (or serial)
+    MapView,                    // Shows Nodes on a map
+    Settings,                   // Shows Settings
+    SignalStrengthHistorical,   // Shows the (average) signal strength of all messages (dropdown to select specific nodes? - will that work with non-direct nodes?)
+}
+
+/// Nice formatting for them, for rendering on the tab headers
+impl fmt::Display for WidgetType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            WidgetType::About => write!(f, "About YAMS"),
+            WidgetType::NodeList => write!(f, "Node List"),
+            WidgetType::Log => write!(f, "Log"),
+            WidgetType::MapView => write!(f, "Map View"),
+            WidgetType::Settings => write!(f, "Settings"),
+            WidgetType::SignalStrengthHistorical => write!(f, "Signal Strength Historical"),
+            WidgetType::NodeInfo => write!(f, "Node"),
+        }
+    }
+}
+
+pub fn render_widget(widget_type: &WidgetType, ui: &mut egui::Ui) {
+    match widget_type {
+        WidgetType::About => {
+            about::about_ui(ui);
+        },
+        WidgetType::NodeList => {
+            node_list::nodelist_ui(ui);
+        },
+        WidgetType::NodeInfo => todo!(),
+        WidgetType::Log => todo!(),
+        WidgetType::MapView => todo!(),
+        WidgetType::Settings => todo!(),
+        WidgetType::SignalStrengthHistorical => todo!(),
+    }
+}
\ No newline at end of file
diff --git a/src/gui/widgets/node_list.rs b/src/gui/widgets/node_list.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6793aff0e4b1a5978f0626283180dbfd4dee1332
--- /dev/null
+++ b/src/gui/widgets/node_list.rs
@@ -0,0 +1,193 @@
+use chrono::DateTime;
+use chrono_humanize::HumanTime;
+use egui_extras::{Column, TableBuilder};
+use longitude::{DistanceUnit, Location};
+use meshtastic::protobufs::{channel::Role, HardwareModel, Position};
+
+use crate::{db::Database, CONFIG};
+
+// The list of nodes
+// This lists all the info about each node.
+// Sortable by any column, allow hiding columns?
+// Nice signal strength bar (also allow clicking to view signal strength history?)
+// Filters:
+// - Direct Connection (direct neighbours / nodes with 0 hops away)
+// - Active (seen in the last `n` minutes)
+// - Inactive (not seen in the last `n` minutes)
+// - Location (has/doesn't have gps location)
+
+/// Calculates the distance betwen 2 coordinate points.
+fn calc_distance_to_node(away: Position) -> Option<f64> {
+    let home_pos = Database::read_home_node().expect("Failed to find Home Node").position.unwrap();
+
+    let distance = Location {
+        // Home Base
+        latitude: home_pos.latitude_i as f64 * 1e-7,
+        longitude: home_pos.longitude_i as f64 * 1e-7,
+    }.distance(&Location {
+        // Away
+        latitude: away.latitude_i as f64 * 1e-7,
+        longitude: away.longitude_i as f64 * 1e-7,
+    });
+    // convert to the right distance depending on settings
+    match CONFIG.gui.units.as_ref() {
+        "Centimeters" => Some(distance.in_unit(DistanceUnit::Centimeters)),
+        "Meters" => Some(distance.in_unit(DistanceUnit::Meters)),
+        "Kilometers" => Some(distance.in_unit(DistanceUnit::Kilometers)),
+        "Inches" => Some(distance.in_unit(DistanceUnit::Inches)),
+        "Feet" => Some(distance.in_unit(DistanceUnit::Feet)),
+        "Yards" => Some(distance.in_unit(DistanceUnit::Yards)),
+        "Miles" => Some(distance.in_unit(DistanceUnit::Miles)),
+        _ => Some(distance.in_unit(DistanceUnit::Kilometers)), // if you can't type, force km.
+    }
+}
+
+/// UI Elements for the Node List
+pub fn nodelist_ui(ui: &mut egui::Ui) {
+    // Get nodes from DB
+    let mut nodes = Database::read_node_list().expect("Failed to read nodes");
+    
+    // Sort nodes by `last_heard` in descending order
+    nodes.sort_by(|a, b| b.last_heard.cmp(&a.last_heard));
+    
+    TableBuilder::new(ui)
+    // TODO for the table:
+    // - Clickable Table Sorting
+    // - Add more info
+    // - Auto hide/show scrollbar on smaller devices
+    // - Add to a panel that we can move around / shitty windowing.
+        .column(Column::remainder())            // node id
+        .column(Column::remainder().clip(true)) // node name
+        .column(Column::remainder())            // snr
+        .column(Column::remainder())            // dist
+        .column(Column::remainder())            // hops
+        .column(Column::remainder().clip(true))            // hw model
+        .column(Column::remainder())            // role
+        .column(Column::remainder())            // battery
+        .column(Column::remainder().clip(true))            // last heard
+        .striped(true)
+        .header(15.0, |mut header| {
+            header.col(|ui| {
+                ui.heading("Node ID");
+            });
+            header.col(|ui| {
+                ui.heading("Name");
+            });
+            header.col(|ui| {
+                ui.heading("SNR");
+            });
+            header.col(|ui| {
+            
+                // Get distance units
+                let unit = match CONFIG.gui.units.as_ref() {
+                    "Centimeters" => "cm",
+                    "Meters" => "m",
+                    "Kilometers" => "km",
+                    "Inches" => "in",
+                    "Feet" => "ft",
+                    "Yards" => "yd",
+                    "Miles" => "mi",
+                    _ => "km", // Default to km if no match found.
+                };
+            
+                ui.heading(format!("Distance ({})", unit));
+            });
+            header.col(|ui| {
+                ui.heading("Hops Away");
+            });
+            header.col(|ui| {
+                ui.heading("HW Model");
+            });
+            header.col(|ui| {
+                ui.heading("Role");
+            });
+            header.col(|ui| {
+                ui.heading("Battery");
+            });
+            header.col(|ui| {
+                ui.heading("Last Heard");
+            });
+        })
+        .body(|mut body| {
+            for node in nodes {
+                let user = node.user.unwrap();
+                let device = node.device_metrics.unwrap();
+                // Convert the last heard to a time, then make it nice to read.
+                let now = chrono::Local::now().to_utc();
+                let humanised = if node.last_heard == 0 {
+                    String::from("Unknown")
+                } else {
+                    let last_heard = DateTime::from_timestamp(node.last_heard.into(), 0).unwrap();
+                    let duration = last_heard - now;
+                    HumanTime::from(duration).to_string()
+                };
+            
+                body.row(15.0, |mut row| {
+                    row.col(|ui| {
+                        ui.label(user.id);
+                    });
+                    row.col(|ui| {
+                        ui.label(format!("{} ({})", user.long_name, user.short_name));
+                    });
+                    row.col(|ui| {
+                        ui.label(node.snr.to_string());
+                    });
+                    row.col(|ui| {
+                        if let Some(distance) = calc_distance_to_node(node.position.unwrap()) {
+                            if distance.is_nan() {
+                                ui.label("-");
+                            } else {
+                                ui.label(format!("{:.2}", distance));
+                            }
+                        } else {
+                            ui.label("-");
+                        }
+                    });
+                    row.col(|ui| {
+                        ui.label(node.hops_away.to_string());
+                    });
+                    row.col(|ui| {
+                        // Convert the i32 to a HardwareModel object, then convert to string
+                        ui.label(HardwareModel::from_i32(user.hw_model).unwrap_or_default().as_str_name());
+                    });
+                    row.col(|ui| {
+                        // Convert the i32 to a Role object, convert to string.
+                        ui.label(Role::from_i32(user.role).unwrap_or_default().as_str_name());
+                    });
+                    row.col(|ui| {
+                        ui.label(device.battery_level.to_string());
+                    });
+                    row.col(|ui| {
+                        ui.label(humanised);
+                    });
+                });
+            }
+        });
+}
+
+
+
+// /// Initialise the raw GUI.
+// /// We pass all tiling 'widgets' to their own files.
+// pub(crate) async fn run_gui() {
+//     debug!("Running in GUI Mode!");
+//     use eframe::{App, egui};
+
+//     struct MyApp;
+
+//     impl App for MyApp {
+//         fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
+//             // Table Panel, showing nodes in a list.
+//             egui::CentralPanel::default().show(ctx, |ui| {
+                
+//             });
+//         }
+//     }
+
+//     let native_options = eframe::NativeOptions::default();
+//     eframe::run_native(
+//         "Yet Another Meshtastic Server (GUI)",
+//         native_options,
+//         Box::new(|_cc| Ok(Box::new(MyApp))),
+//     ).unwrap();
+// }
\ No newline at end of file
diff --git a/src/gui/widgets/settings.rs b/src/gui/widgets/settings.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f351a9e51bb20e36ffdeb0bcacf80f24d681c0bf
--- /dev/null
+++ b/src/gui/widgets/settings.rs
@@ -0,0 +1,5 @@
+/// Settings
+/// allows you to configure global settings for the program.
+/// Dark / Light Modes
+/// metric / imperial
+/// more shit at a later date!
\ No newline at end of file
diff --git a/src/gui/widgets/signal_strength_history.rs b/src/gui/widgets/signal_strength_history.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3d89780a101cde9a157e51c3e56edd94857320c5
--- /dev/null
+++ b/src/gui/widgets/signal_strength_history.rs
@@ -0,0 +1,3 @@
+/// Shows a nice graph of the selected node's SNR/RSSI
+/// Ideally, it should look similar to the meshtastic graphs
+/// Allow picking multiple nodes - averages? (if you want multiple graphs, use multiple widgets?)
diff --git a/src/main.rs b/src/main.rs
index f9fdaa8f52e154734b612a304ef9e4198c6a072c..5d6934eed3621cdec8ecb6a0e1bb84bf99604250 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,6 +17,7 @@ mod packets;
 mod db;
 mod config;
 mod gui;
+
 use crate::config::AppConfig;
 
 // Setup the config globally because I can't figure out how to pass it to functions I don't call directly.
@@ -107,7 +108,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // match on gui or server
     match args[1].as_str() {
         "server" => run_server().await,
-        "gui" => gui::run_gui().await,
+        "gui" => gui::run_gui().await.expect("uh oh"),
         _ => {
             error!("Unknown mode: {}", args[1]);
             info!("Usage: yams <mode>");
@@ -117,4 +118,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     }
 
     Ok(())
+}
+
+pub mod built_info {
+    include!(concat!(env!("OUT_DIR"), "/built.rs"));
 }
\ No newline at end of file