diff --git a/.gitignore b/.gitignore
index 43ab8ae..682effd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
# cmake files
/build
+# cargo files
+/ui/target
+
# ide/lsp files
/.zed
/.vscode
diff --git a/README.md b/README.md
index bddfc79..4fe55c1 100644
--- a/README.md
+++ b/README.md
@@ -14,5 +14,7 @@ curl -sSf https://pancake.gay/lsfg-vk.sh | sh
Please see the [Wiki](https://github.com/PancakeTAS/lsfg-vk/wiki) for more information and join the [Discord](https://discord.gg/losslessscaling) for help (In order to see the linux channels, verify your Steam account.)
+Thanks to @Caliel666 for writing the GTK-based gui for lsfg-vk!
+
>[!WARNING]
> **Please do not open GitHub** issues for anything other than feature requests. Due to the nature of this project, it is much easier to deal with issues through Discord, than GitHub. Use the #linux-reports channel for game compatibility.
diff --git a/ui/Cargo.lock b/ui/Cargo.lock
new file mode 100644
index 0000000..fb3b967
--- /dev/null
+++ b/ui/Cargo.lock
@@ -0,0 +1,952 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "directories"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk4"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk4-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk4-sys"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "graphene-rs"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401"
+dependencies = [
+ "glib",
+ "graphene-sys",
+ "libc",
+]
+
+[[package]]
+name = "graphene-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gsk4"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e"
+dependencies = [
+ "cairo-rs",
+ "gdk4",
+ "glib",
+ "graphene-rs",
+ "gsk4-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gsk4-sys"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk4-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk4"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842"
+dependencies = [
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk-pixbuf",
+ "gdk4",
+ "gio",
+ "glib",
+ "graphene-rs",
+ "gsk4",
+ "gtk4-macros",
+ "gtk4-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gtk4-macros"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f"
+dependencies = [
+ "anyhow",
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "gtk4-sys"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "gsk4-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indexmap"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "libadwaita"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c"
+dependencies = [
+ "gdk-pixbuf",
+ "gdk4",
+ "gio",
+ "glib",
+ "gtk4",
+ "libadwaita-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "libadwaita-sys"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a"
+dependencies = [
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk4-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libredox"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "lsfg-vk-ui"
+version = "0.1.3"
+dependencies = [
+ "directories",
+ "dirs",
+ "gtk4",
+ "libadwaita",
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
diff --git a/ui/Cargo.toml b/ui/Cargo.toml
new file mode 100644
index 0000000..3524dea
--- /dev/null
+++ b/ui/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "lsfg-vk-ui"
+version = "0.1.3"
+edition = "2021"
+authors = ["Cali666"]
+description = "Lossless Scaling Frame Generation Configuration Tool"
+
+[dependencies]
+libadwaita = "0.5"
+gtk = { version = "0.7", package = "gtk4" }
+directories = "5.0"
+serde = { version = "1.0", features = ["derive"] }
+toml = "0.8"
+dirs = "5.0"
diff --git a/ui/resources/com.cali666.lsfg-vk-ui.desktop b/ui/resources/com.cali666.lsfg-vk-ui.desktop
new file mode 100644
index 0000000..640a161
--- /dev/null
+++ b/ui/resources/com.cali666.lsfg-vk-ui.desktop
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=LSFG-VK UI
+Comment=Lossless Scaling Frame Generation Configuration Tool
+Exec=lsfg-vk-ui %U
+Icon=com.cali666.lsfg-vk-ui
+Terminal=false
+Categories=Game;Settings;
+Keywords=lossless;scaling;frame;generation;gaming;graphics;configuration;
+StartupNotify=true
+StartupWMClass=com.cali666.lsfg-vk-ui
+MimeType=application/x-lsfg-profile;
diff --git a/ui/resources/icons/lsfg-vk.png b/ui/resources/icons/lsfg-vk.png
new file mode 100644
index 0000000..1c67ee2
Binary files /dev/null and b/ui/resources/icons/lsfg-vk.png differ
diff --git a/ui/resources/ui.ui b/ui/resources/ui.ui
new file mode 100644
index 0000000..d72e763
--- /dev/null
+++ b/ui/resources/ui.ui
@@ -0,0 +1,347 @@
+
+
+
+
+
diff --git a/ui/src/app_state.rs b/ui/src/app_state.rs
new file mode 100644
index 0000000..552da38
--- /dev/null
+++ b/ui/src/app_state.rs
@@ -0,0 +1,293 @@
+use gtk::prelude::*;
+use gtk::{glib, MessageDialog};
+use libadwaita::ApplicationWindow;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::config::{Config, save_config};
+use crate::utils::round_to_2_decimals;
+
+#[allow(dead_code)]
+pub struct AppState {
+ pub config: Config,
+ pub selected_profile_index: Option,
+ // Store references to the UI widgets for easy access and updates
+ pub main_window: ApplicationWindow,
+ pub sidebar_list_box: gtk::ListBox,
+ pub multiplier_dropdown: gtk::DropDown,
+ pub flow_scale_entry: gtk::Entry,
+ pub performance_mode_switch: gtk::Switch,
+ pub hdr_mode_switch: gtk::Switch,
+ pub experimental_present_mode_dropdown: gtk::DropDown,
+ pub save_button: gtk::Button,
+ pub main_settings_box: gtk::Box,
+ pub main_stack: gtk::Stack,
+ // Store SignalHandlerIds to block/unblock signals
+ pub multiplier_dropdown_handler_id: Option,
+ pub flow_scale_entry_handler_id: Option,
+ pub performance_mode_switch_handler_id: Option,
+ pub hdr_mode_switch_handler_id: Option,
+ pub experimental_present_mode_dropdown_handler_id: Option,
+}
+
+impl AppState {
+ // Saves the current configuration to the TOML file
+ pub fn save_current_config(&self) {
+ if let Err(e) = save_config(&self.config) {
+ eprintln!("Failed to save config: {}", e);
+ // In a real app, you'd show a user-friendly error dialog here
+ }
+ }
+
+ // Updates the main window UI with data from the currently selected profile
+ pub fn update_main_window_from_profile(&self) {
+ if let Some(index) = self.selected_profile_index {
+ if let Some(profile) = self.config.game.get(index) {
+ // Temporarily block signals to prevent re-entrancy
+ let _guard_mult = self.multiplier_dropdown_handler_id.as_ref().map(|id| self.multiplier_dropdown.block_signal(id));
+ let _guard_flow = self.flow_scale_entry_handler_id.as_ref().map(|id| self.flow_scale_entry.block_signal(id));
+ let _guard_perf = self.performance_mode_switch_handler_id.as_ref().map(|id| self.performance_mode_switch.block_signal(id));
+ let _guard_hdr = self.hdr_mode_switch_handler_id.as_ref().map(|id| self.hdr_mode_switch.block_signal(id));
+ let _guard_exp = self.experimental_present_mode_dropdown_handler_id.as_ref().map(|id| self.experimental_present_mode_dropdown.block_signal(id));
+
+ // Update Multiplier Dropdown
+ let multiplier_str = match profile.multiplier {
+ 1 => "off".to_string(),
+ _ => profile.multiplier.to_string(),
+ };
+ if let Some(pos) = self.multiplier_dropdown.model().and_then(|model| {
+ let list_model = model.downcast_ref::()?;
+ (0..list_model.n_items()).find(|&i| list_model.string(i).map_or(false, |s| s.as_str() == multiplier_str))
+ }) {
+ self.multiplier_dropdown.set_selected(pos);
+ }
+
+ // Update Flow Scale Entry (round to avoid floating point display issues)
+ let rounded_flow_scale = round_to_2_decimals(profile.flow_scale);
+ self.flow_scale_entry.set_text(&format!("{:.2}", rounded_flow_scale));
+
+ // Update Performance Mode Switch
+ self.performance_mode_switch.set_active(profile.performance_mode);
+
+ // Update HDR Mode Switch
+ self.hdr_mode_switch.set_active(profile.hdr_mode);
+
+ // Update Experimental Present Mode Dropdown
+ if let Some(pos) = self.experimental_present_mode_dropdown.model().and_then(|model| {
+ let list_model = model.downcast_ref::()?;
+ (0..list_model.n_items()).find(|&i| list_model.string(i).map_or(false, |s| s.as_str() == profile.experimental_present_mode))
+ }) {
+ self.experimental_present_mode_dropdown.set_selected(pos);
+ }
+ // Signal handlers are unblocked automatically when _guard_X go out of scope
+
+ // Switch to the settings page
+ self.main_stack.set_visible_child_name("settings_page");
+
+ }
+ } else {
+ // Clear or disable main window elements if no profile is selected
+ self.multiplier_dropdown.set_selected(0); // Default to 'off' or first item
+ self.flow_scale_entry.set_text("");
+ self.performance_mode_switch.set_active(false);
+ self.hdr_mode_switch.set_active(false);
+ self.experimental_present_mode_dropdown.set_selected(0); // Default to first item
+
+ // Switch to the about page
+ self.main_stack.set_visible_child_name("about_page");
+ }
+ }
+
+ // Populates sidebar with optional app_state for button handlers
+ pub fn populate_sidebar_with_handlers(&self, app_state: Option>>) {
+ // Clear existing rows
+ while let Some(child) = self.sidebar_list_box.first_child() {
+ self.sidebar_list_box.remove(&child);
+ }
+
+ let mut row_to_select: Option = None;
+
+ for (i, profile) in self.config.game.iter().enumerate() {
+ let row = gtk::ListBoxRow::new();
+
+ // Create a horizontal box to hold the profile name and buttons
+ let row_box = gtk::Box::builder()
+ .orientation(gtk::Orientation::Horizontal)
+ .spacing(8)
+ .margin_start(12)
+ .margin_end(12)
+ .margin_top(8)
+ .margin_bottom(8)
+ .build();
+
+ // Profile name label
+ let label = gtk::Label::builder()
+ .label(&profile.exe)
+ .halign(gtk::Align::Start)
+ .hexpand(true)
+ .build();
+
+ // Edit button
+ let edit_button = gtk::Button::builder()
+ .label("🖊")
+ .css_classes(["flat", "circular"])
+ .tooltip_text("Edit profile name")
+ .build();
+
+ // Remove button
+ let remove_button = gtk::Button::builder()
+ .label("𐄂")
+ .css_classes(["flat", "circular", "destructive-action"])
+ .tooltip_text("Remove profile")
+ .build();
+
+ // Add all elements to the row box
+ row_box.append(&label);
+ row_box.append(&edit_button);
+ row_box.append(&remove_button);
+
+ // Connect button handlers if app_state is available
+ if let Some(app_state_ref) = &app_state {
+ // Edit button handler
+ let app_state_clone = app_state_ref.clone();
+ let profile_index = i;
+ edit_button.connect_clicked(move |_| {
+ let state = app_state_clone.borrow();
+ let main_window = &state.main_window;
+
+ // Create edit dialog
+ let dialog = MessageDialog::new(
+ Some(main_window),
+ gtk::DialogFlags::MODAL,
+ gtk::MessageType::Question,
+ gtk::ButtonsType::None,
+ "Edit profile name:",
+ );
+ dialog.set_title(Some("Edit Profile"));
+
+ let entry = gtk::Entry::builder()
+ .placeholder_text("Profile Name")
+ .text(&state.config.game[profile_index].exe)
+ .margin_top(12)
+ .margin_bottom(12)
+ .margin_start(12)
+ .margin_end(12)
+ .build();
+
+ dialog.content_area().append(&entry);
+ dialog.add_button("Cancel", gtk::ResponseType::Cancel);
+ dialog.add_button("Save", gtk::ResponseType::Other(1));
+ dialog.set_default_response(gtk::ResponseType::Other(1));
+
+ // Allow pressing Enter in the entry to trigger the "Save" button
+ let dialog_clone = dialog.clone();
+ entry.connect_activate(move |_| {
+ dialog_clone.response(gtk::ResponseType::Other(1));
+ });
+
+ let app_state_clone_dialog = app_state_clone.clone();
+ let entry_clone = entry.clone();
+ dialog.connect_response(move |d, response| {
+ if response == gtk::ResponseType::Other(1) {
+ let new_name = entry_clone.text().to_string();
+ if !new_name.is_empty() {
+ let mut state = app_state_clone_dialog.borrow_mut();
+
+ // Check if profile with this name already exists (excluding current)
+ if state.config.game.iter().enumerate().any(|(idx, p)| idx != profile_index && p.exe == new_name) {
+ let error_dialog = MessageDialog::new(
+ Some(d),
+ gtk::DialogFlags::MODAL,
+ gtk::MessageType::Error,
+ gtk::ButtonsType::Ok,
+ "A profile with this name already exists",
+ );
+ error_dialog.set_title(Some("Error"));
+ error_dialog.connect_response(move |d, _| { d.close(); });
+ error_dialog.present();
+ return;
+ }
+
+ // Update profile name
+ state.config.game[profile_index].exe = new_name;
+ state.save_current_config();
+ state.populate_sidebar_with_handlers(Some(app_state_clone_dialog.clone()));
+ }
+ }
+ d.close();
+ });
+ dialog.present();
+ });
+
+ // Remove button handler
+ let app_state_clone = app_state_ref.clone();
+ let profile_index = i;
+ remove_button.connect_clicked(move |_| {
+ let state = app_state_clone.borrow();
+ let main_window = &state.main_window;
+ let profile_name = &state.config.game[profile_index].exe;
+
+ // Create confirmation dialog
+ let dialog = MessageDialog::new(
+ Some(main_window),
+ gtk::DialogFlags::MODAL,
+ gtk::MessageType::Warning,
+ gtk::ButtonsType::None,
+ &format!("Are you sure you want to remove the profile '{}'?", profile_name),
+ );
+ dialog.set_title(Some("Remove Profile"));
+ dialog.add_button("Cancel", gtk::ResponseType::Cancel);
+ dialog.add_button("Remove", gtk::ResponseType::Other(1));
+ dialog.set_default_response(gtk::ResponseType::Cancel);
+
+ let app_state_clone_dialog = app_state_clone.clone();
+ dialog.connect_response(move |d, response| {
+ if response == gtk::ResponseType::Other(1) {
+ let mut state = app_state_clone_dialog.borrow_mut();
+
+ // Remove the profile
+ state.config.game.remove(profile_index);
+
+ // Update selected index if needed
+ if let Some(selected) = state.selected_profile_index {
+ if selected == profile_index {
+ // If we removed the selected profile, select the first available or none
+ state.selected_profile_index = if state.config.game.is_empty() { None } else { Some(0) };
+ } else if selected > profile_index {
+ // Adjust index if we removed a profile before the selected one
+ state.selected_profile_index = Some(selected - 1);
+ }
+ }
+
+ state.save_current_config();
+ state.populate_sidebar_with_handlers(Some(app_state_clone_dialog.clone()));
+ drop(state);
+
+ // Update main window
+ app_state_clone_dialog.borrow().update_main_window_from_profile();
+ }
+ d.close();
+ });
+ dialog.present();
+ });
+ }
+
+ row.set_child(Some(&row_box));
+ self.sidebar_list_box.append(&row);
+
+ // Mark the row to be selected later
+ if self.selected_profile_index == Some(i) {
+ row_to_select = Some(row.clone()); // Clone the row to store it
+ }
+ }
+
+ // Perform selection in a separate idle callback
+ if let Some(row) = row_to_select {
+ let list_box_clone = self.sidebar_list_box.clone();
+ glib::idle_add_local(move || {
+ list_box_clone.select_row(Some(&row));
+ glib::ControlFlow::Break
+ });
+ }
+ }
+}
diff --git a/ui/src/config.rs b/ui/src/config.rs
new file mode 100644
index 0000000..7fdde70
--- /dev/null
+++ b/ui/src/config.rs
@@ -0,0 +1,141 @@
+use serde::{Deserialize, Serialize};
+use std::{fs, io};
+use std::path::PathBuf;
+use toml;
+use dirs;
+
+use crate::utils::round_to_2_decimals; // Import from utils module
+
+// --- Configuration Data Structures ---
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct Config {
+ pub version: u32, // Made public to be accessible from main.rs
+ #[serde(flatten)] // Flatten this struct into the parent, controlling order
+ pub ordered_global: OrderedGlobalConfig,
+ #[serde(default)]
+ pub game: Vec,
+}
+
+// Helper struct to control the serialization order of global config
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct OrderedGlobalConfig {
+ #[serde(default, skip_serializing_if = "Option::is_none")] // Only serialize if Some
+ pub global: Option,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct GlobalConfig {
+ #[serde(default, skip_serializing_if = "Option::is_none")] // Only serialize if Some
+ pub dll: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(default)]
+pub struct GameProfile {
+ pub exe: String,
+ pub multiplier: u32,
+ #[serde(serialize_with = "serialize_flow_scale", deserialize_with = "deserialize_flow_scale")]
+ pub flow_scale: f32,
+ pub performance_mode: bool,
+ pub hdr_mode: bool,
+ pub experimental_present_mode: String,
+}
+
+// Default values for a new game profile
+impl Default for GameProfile {
+ fn default() -> Self {
+ GameProfile {
+ exe: String::new(),
+ multiplier: 1, // Default to "off" (1)
+ flow_scale: round_to_2_decimals(0.7),
+ performance_mode: true,
+ hdr_mode: false,
+ experimental_present_mode: "vsync".to_string(),
+ }
+ }
+}
+
+// Custom serde functions to ensure flow_scale is always rounded
+fn serialize_flow_scale(value: &f32, serializer: S) -> Result
+where
+ S: serde::Serializer,
+{
+ // Force to 2 decimal places and serialize as a precise decimal
+ let rounded = round_to_2_decimals(*value);
+ let formatted = format!("{:.2}", rounded);
+ let precise_value: f64 = formatted.parse().unwrap_or(*value as f64);
+ serializer.serialize_f64(precise_value)
+}
+
+fn deserialize_flow_scale<'de, D>(deserializer: D) -> Result
+where
+ D: serde::Deserializer<'de>,
+{
+ use serde::Deserialize;
+ let value = f64::deserialize(deserializer)?;
+ Ok(round_to_2_decimals(value as f32))
+}
+
+// --- Configuration File Handling Functions ---
+
+pub fn get_config_path() -> Result {
+ let config_dir = dirs::config_dir()
+ .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not find config directory"))?
+ .join("lsfg-vk");
+
+ fs::create_dir_all(&config_dir)?; // Ensure directory exists
+ println!("Config directory: {:?}", config_dir);
+ Ok(config_dir.join("conf.toml"))
+}
+
+
+pub fn load_config() -> Result {
+ let config_path = get_config_path()?;
+ println!("Attempting to load config from: {:?}", config_path);
+ if config_path.exists() {
+ let contents = fs::read_to_string(&config_path)?;
+ println!("Successfully read config contents ({} bytes).", contents.len());
+ // Load configuration with default values when the format is invalid
+ let mut config: Config = toml::from_str(&contents).unwrap_or_else(|_| Config::default());
+
+ // Old way to load config
+ // let mut config: Config = toml::from_str(&contents).map_err(|e| {
+ // io::Error::new(
+ // io::ErrorKind::InvalidData,
+ // format!("Failed to parse TOML: {}", e),
+ // )
+ // })?;
+
+
+ // Clean up any floating point precision issues in existing configs
+ let mut needs_save = false;
+ for profile in &mut config.game {
+ let original = profile.flow_scale;
+ profile.flow_scale = round_to_2_decimals(profile.flow_scale);
+ if (original - profile.flow_scale).abs() > f32::EPSILON {
+ needs_save = true;
+ }
+ }
+
+ // Save the cleaned config if we made changes
+ if needs_save {
+ let _ = save_config(&config);
+ }
+
+ Ok(config)
+ } else {
+ println!("Config file not found at {:?}, creating default.", config_path);
+ Ok(Config { version: 1, ordered_global: OrderedGlobalConfig { global: None }, game: Vec::new() })
+ }
+}
+
+pub fn save_config(config: &Config) -> Result<(), io::Error> {
+ let config_path = get_config_path()?;
+ println!("Attempting to save config to: {:?}", config_path);
+ let toml_string = toml::to_string_pretty(config)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to serialize TOML: {}", e)))?;
+ fs::write(&config_path, toml_string)?;
+ println!("Successfully saved config.");
+ Ok(())
+}
diff --git a/ui/src/main.rs b/ui/src/main.rs
new file mode 100644
index 0000000..f3d066e
--- /dev/null
+++ b/ui/src/main.rs
@@ -0,0 +1,497 @@
+use gtk::prelude::*;
+use gtk::{glib, CssProvider, Builder, Label};
+use libadwaita::ApplicationWindow;
+use libadwaita::prelude::AdwApplicationWindowExt;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+// Import modules
+mod config;
+mod app_state;
+mod utils;
+mod settings_window;
+
+use config::load_config;
+use app_state::AppState;
+use utils::round_to_2_decimals;
+use config::OrderedGlobalConfig;
+
+fn main() -> glib::ExitCode {
+ let application = libadwaita::Application::builder()
+ .application_id("com.cali666.lsfg-vk-ui")
+ .build();
+
+ // Set the desktop file name for proper GNOME integration
+ glib::set_application_name("LSFG-VK UI");
+ glib::set_prgname(Some("lsfg-vk-ui"));
+
+ application.connect_startup(move |_app| {
+ // Load CSS for sidebar background
+ let provider = CssProvider::new();
+ provider.load_from_data(&format!(
+ ".settings-icon-button {{
+ font-size: 1.4rem;
+ }}
+
+ .sidebar {{
+ background-color: @theme_bg_color;
+ }}
+
+ .sidebar-content {{
+ background-color: shade(@theme_bg_color, {});
+ color: @theme_fg_color;
+ padding: 12px;
+ }}\n
+ .linked-button-box {{
+ margin-top: 12px;
+ margin-bottom: 12px;
+ }}",
+ 0.95
+ ));
+ gtk::style_context_add_provider_for_display(
+ >k::gdk::Display::default().expect("Could not connect to a display."),
+ &provider,
+ gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
+ );
+
+ // Set up icon theme for the application icon
+ if let Some(display) = gtk::gdk::Display::default() {
+ let icon_theme = gtk::IconTheme::for_display(&display);
+ icon_theme.add_resource_path("/com/cali666/lsfg-vk-ui/icons");
+ }
+ });
+
+ application.connect_activate(move |app| {
+ // Load initial configuration
+ let initial_config = load_config().unwrap_or_else(|e| {
+ eprintln!("Error loading config: {}", e);
+ // Corrected Config initialization
+ config::Config { version: 1, ordered_global: OrderedGlobalConfig { global: None }, game: Vec::new() }
+ });
+
+ // Load UI from .ui file
+ let ui_bytes = include_bytes!("../resources/ui.ui");
+ let builder = Builder::from_string(std::str::from_utf8(ui_bytes).unwrap());
+
+ // Get main window and other widgets
+ let main_window: ApplicationWindow = builder
+ .object("main_window")
+ .expect("Could not get main_window from builder");
+ main_window.set_application(Some(app));
+
+ let settings_button: gtk::Button = builder
+ .object("settings_button")
+ .expect("Could not get settings_button from builder");
+
+ // Set application icon for proper dock integration
+ main_window.set_icon_name(Some("com.cali666.lsfg-vk-ui"));
+
+ let sidebar_list_box: gtk::ListBox = builder
+ .object("sidebar_list_box")
+ .expect("Could not get sidebar_list_box from builder");
+ let create_profile_button: gtk::Button = builder
+ .object("create_profile_button")
+ .expect("Could not get create_profile_button from builder");
+
+ let multiplier_dropdown: gtk::DropDown = builder
+ .object("multiplier_dropdown")
+ .expect("Could not get multiplier_dropdown from builder");
+ let flow_scale_entry: gtk::Entry = builder
+ .object("flow_scale_entry")
+ .expect("Could not get flow_scale_entry from builder");
+ let performance_mode_switch: gtk::Switch = builder
+ .object("performance_mode_switch")
+ .expect("Could not get performance_mode_switch from builder");
+ let hdr_mode_switch: gtk::Switch = builder
+ .object("hdr_mode_switch")
+ .expect("Could not get hdr_mode_switch from builder");
+ let experimental_present_mode_dropdown: gtk::DropDown = builder
+ .object("experimental_present_mode_dropdown")
+ .expect("Could not get experimental_present_mode_dropdown from builder");
+
+ let main_stack: gtk::Stack = builder
+ .object("main_stack")
+ .expect("Could not get main_stack from builder. Ensure it has id='main_stack' in ui.ui.");
+ let main_stack_switcher: gtk::StackSwitcher = builder
+ .object("main_stack_switcher")
+ .expect("Could not get main_stack_switcher from builder. Ensure it has id='main_stack_switcher' in ui.ui.");
+
+ main_stack_switcher.set_stack(Some(&main_stack));
+
+ let main_settings_box: gtk::Box = builder
+ .object("main_box")
+ .expect("Could not get main_box from builder");
+
+ let save_button = gtk::Button::builder()
+ .label("Save Changes")
+ .halign(gtk::Align::End)
+ .margin_end(12)
+ .margin_bottom(12)
+ .build();
+
+ main_settings_box.append(&save_button);
+
+ // Initialize application state (with None for handler IDs initially)
+ let app_state = Rc::new(RefCell::new(AppState {
+ config: initial_config,
+ selected_profile_index: None,
+ main_window: main_window.clone(),
+ sidebar_list_box: sidebar_list_box.clone(),
+ multiplier_dropdown: multiplier_dropdown.clone(),
+ flow_scale_entry: flow_scale_entry.clone(),
+ performance_mode_switch: performance_mode_switch.clone(),
+ hdr_mode_switch: hdr_mode_switch.clone(),
+ experimental_present_mode_dropdown: experimental_present_mode_dropdown.clone(),
+ save_button: save_button.clone(),
+ main_settings_box: main_settings_box.clone(),
+ main_stack: main_stack.clone(),
+ multiplier_dropdown_handler_id: None,
+ flow_scale_entry_handler_id: None,
+ performance_mode_switch_handler_id: None,
+ hdr_mode_switch_handler_id: None,
+ experimental_present_mode_dropdown_handler_id: None,
+ }));
+
+ // --- Connect Signals ---
+
+ // Connect settings button
+ let main_window_clone = main_window.clone();
+ let app_state_clone_for_settings = app_state.clone(); // Clone for settings window
+ settings_button.connect_clicked(move |_| {
+ let settings_win = settings_window::create_settings_window(&main_window_clone, app_state_clone_for_settings.clone());
+ settings_win.present();
+ });
+
+ let app_state_clone = app_state.clone();
+ sidebar_list_box.connect_row_activated(move |_list_box, row| {
+ let index = row.index() as usize;
+ let mut state = app_state_clone.borrow_mut();
+ state.selected_profile_index = Some(index);
+ drop(state);
+
+ let app_state_for_idle = app_state_clone.clone();
+ glib::idle_add_local(move || {
+ app_state_for_idle.borrow().update_main_window_from_profile();
+ glib::ControlFlow::Break
+ });
+ });
+
+ let app_state_clone = app_state.clone();
+ create_profile_button.connect_clicked(move |_| {
+ let dialog = gtk::MessageDialog::new(
+ Some(&app_state_clone.borrow().main_window),
+ gtk::DialogFlags::MODAL,
+ gtk::MessageType::Question,
+ gtk::ButtonsType::None,
+ "",
+ );
+ dialog.set_title(Some("New Profile"));
+ dialog.set_secondary_text(Some("Enter or browse Application Name"));
+
+ let entry = gtk::Entry::builder()
+ .placeholder_text("Application Name")
+ .hexpand(true)
+ .build();
+
+ let pick_process_button = gtk::Button::builder()
+ .label("🖵")
+ .tooltip_text("Pick a running Vulkan process")
+ .css_classes(["flat", "square", "icon-button"])
+ .build();
+
+ let entry_box = gtk::Box::builder()
+ .orientation(gtk::Orientation::Horizontal)
+ .spacing(6)
+ .margin_top(12)
+ .margin_bottom(12)
+ .margin_start(12)
+ .margin_end(12)
+ .build();
+ entry_box.append(&entry);
+ entry_box.append(&pick_process_button);
+
+ dialog.content_area().append(&entry_box);
+
+ dialog.add_button("Cancel", gtk::ResponseType::Cancel);
+ dialog.add_button("Create", gtk::ResponseType::Other(1));
+
+ dialog.set_default_response(gtk::ResponseType::Other(1));
+
+ // Allow pressing Enter in the entry to trigger the "Create" button
+ let dialog_clone = dialog.clone();
+ entry.connect_activate(move |_| {
+ dialog_clone.response(gtk::ResponseType::Other(1));
+ });
+
+ // --- Process Picker Button Logic ---
+ let entry_clone_for_picker = entry.clone();
+ let main_window_clone_for_picker = app_state_clone.borrow().main_window.clone();
+
+ pick_process_button.connect_clicked(move |_| {
+ let process_picker_window = libadwaita::ApplicationWindow::builder()
+ .title("Select Process")
+ .transient_for(&main_window_clone_for_picker)
+ .modal(true)
+ .default_width(400)
+ .default_height(600)
+ .build();
+
+ let scrolled_window = gtk::ScrolledWindow::builder()
+ .hscrollbar_policy(gtk::PolicyType::Never)
+ .vscrollbar_policy(gtk::PolicyType::Automatic)
+ .hexpand(true) // Make the scrolled window expand horizontally
+ .vexpand(true) // Make the scrolled window expand vertically
+ .margin_top(12)
+ .margin_start(12)
+ .margin_end(12)
+ .build();
+
+ let process_list_box = gtk::ListBox::builder()
+ .selection_mode(gtk::SelectionMode::Single)
+ .build();
+ scrolled_window.set_child(Some(&process_list_box));
+
+ let content_box = gtk::Box::builder()
+ .orientation(gtk::Orientation::Vertical)
+ .build();
+ content_box.append(&scrolled_window); // Add scrolled window first to take up space
+
+ let close_button = gtk::Button::builder()
+ .label("Close")
+ .halign(gtk::Align::End)
+ .margin_end(12)
+ .margin_bottom(12)
+ .build();
+ content_box.append(&close_button); // Add close button at the bottom
+
+ process_picker_window.set_content(Some(&content_box));
+
+ // Populate the list with processes
+ let processes = utils::get_vulkan_processes(); // Call the new function from utils.rs
+ for proc_name in processes {
+ let row = gtk::ListBoxRow::new();
+ let label = gtk::Label::builder()
+ .label(&proc_name)
+ .halign(gtk::Align::Start)
+ .margin_start(12)
+ .margin_end(12)
+ .margin_top(8)
+ .margin_bottom(8)
+ .build();
+ row.set_child(Some(&label));
+ process_list_box.append(&row);
+ }
+
+ // Connect selection handler
+ let entry_clone_for_select = entry_clone_for_picker.clone();
+ let picker_window_clone = process_picker_window.clone();
+ process_list_box.connect_row_activated(move |_list_box, row| {
+ if let Some(label_widget) = row.child().and_then(|c| c.downcast::().ok()) {
+ let process_name = label_widget.label().to_string();
+ entry_clone_for_select.set_text(&process_name);
+ picker_window_clone.close();
+ }
+ });
+
+ // Connect close button
+ let picker_window_clone_for_close = process_picker_window.clone();
+ close_button.connect_clicked(move |_| {
+ picker_window_clone_for_close.close();
+ });
+
+ process_picker_window.present();
+ });
+ // --- End Process Picker Button Logic ---
+
+ let app_state_clone_dialog = app_state_clone.clone();
+ let entry_clone = entry.clone();
+ dialog.connect_response(
+ move |d: >k::MessageDialog, response: gtk::ResponseType| {
+ if response == gtk::ResponseType::Other(1) {
+ let game_name = entry_clone.text().to_string();
+ if !game_name.is_empty() {
+ let mut state = app_state_clone_dialog.borrow_mut();
+
+ if state.config.game.iter().any(|p| p.exe == game_name) {
+ let error_dialog = gtk::MessageDialog::new(
+ Some(d),
+ gtk::DialogFlags::MODAL,
+ gtk::MessageType::Error,
+ gtk::ButtonsType::Ok,
+ "A profile with this name already exists",
+ );
+ error_dialog.set_title(Some("Error"));
+ error_dialog.connect_response(move |d, _| { d.close(); });
+ error_dialog.present();
+ return;
+ }
+
+ let new_profile = config::GameProfile {
+ exe: game_name,
+ ..Default::default()
+ };
+
+ state.config.game.push(new_profile);
+ state.selected_profile_index = Some(state.config.game.len() - 1);
+
+ state.save_current_config();
+
+ state.populate_sidebar_with_handlers(Some(app_state_clone_dialog.clone()));
+ drop(state);
+
+ let app_state_for_idle = app_state_clone_dialog.clone();
+ glib::idle_add_local(move || {
+ app_state_for_idle.borrow().update_main_window_from_profile();
+ glib::ControlFlow::Break
+ });
+ }
+ }
+ d.close();
+ }
+ );
+ dialog.present();
+ });
+
+ let app_state_clone_for_handler_mult = app_state.clone();
+ let multiplier_handler_id = multiplier_dropdown.connect_selected_item_notify(move |dropdown| {
+ let mut state = app_state_clone_for_handler_mult.borrow_mut();
+
+ if let Some(index) = state.selected_profile_index {
+ if index < state.config.game.len() {
+ if let Some(profile) = state.config.game.get_mut(index) {
+ if let Some(item) = dropdown.selected_item() {
+ if let Some(string_obj) = item.downcast_ref::() {
+ let text = string_obj.string();
+ profile.multiplier = match text.as_str() {
+ "off" => 1,
+ _ => text.parse().unwrap_or(1),
+ };
+ }
+ }
+ }
+ }
+ }
+ });
+ app_state.borrow_mut().multiplier_dropdown_handler_id = Some(multiplier_handler_id);
+
+ let app_state_clone_for_handler_flow = app_state.clone();
+ let flow_handler_id = flow_scale_entry.connect_changed(move |entry| {
+ let mut state = app_state_clone_for_handler_flow.borrow_mut();
+ if let Some(index) = state.selected_profile_index {
+ if let Some(profile) = state.config.game.get_mut(index) {
+ if let Ok(value) = entry.text().parse::() {
+ profile.flow_scale = round_to_2_decimals(value);
+ }
+ }
+ }
+ });
+ app_state.borrow_mut().flow_scale_entry_handler_id = Some(flow_handler_id);
+
+ let app_state_clone_for_handler_perf = app_state.clone();
+ let perf_handler_id = performance_mode_switch.connect_state_set(move |_sw, active| {
+ let mut state = app_state_clone_for_handler_perf.borrow_mut();
+ if let Some(index) = state.selected_profile_index {
+ if let Some(profile) = state.config.game.get_mut(index) {
+ profile.performance_mode = active;
+ }
+ }
+ drop(state);
+ glib::Propagation::Proceed
+ });
+ app_state.borrow_mut().performance_mode_switch_handler_id = Some(perf_handler_id);
+
+ let app_state_clone_for_handler_hdr = app_state.clone();
+ let hdr_handler_id = hdr_mode_switch.connect_state_set(move |_sw, active| {
+ let mut state = app_state_clone_for_handler_hdr.borrow_mut();
+ if let Some(index) = state.selected_profile_index {
+ if let Some(profile) = state.config.game.get_mut(index) {
+ profile.hdr_mode = active;
+ }
+ }
+ drop(state);
+ glib::Propagation::Proceed
+ });
+ app_state.borrow_mut().hdr_mode_switch_handler_id = Some(hdr_handler_id);
+
+ let app_state_clone_for_handler_exp = app_state.clone();
+ let exp_handler_id = experimental_present_mode_dropdown.connect_selected_item_notify(move |dropdown| {
+ let mut state = app_state_clone_for_handler_exp.borrow_mut();
+ if let Some(index) = state.selected_profile_index {
+ if let Some(profile) = state.config.game.get_mut(index) {
+ let selected_text = dropdown.selected_item().and_then(|item| item.downcast_ref::().map(|s| s.string().to_string()));
+ if let Some(text) = selected_text {
+ profile.experimental_present_mode = text;
+ }
+ }
+ }
+ });
+ app_state.borrow_mut().experimental_present_mode_dropdown_handler_id = Some(exp_handler_id);
+
+ let app_state_clone_save = app_state.clone();
+ save_button.connect_clicked(move |_| {
+ let state_ref = app_state_clone_save.borrow();
+ if let Some(index) = state_ref.selected_profile_index {
+ let multiplier_str = state_ref.multiplier_dropdown.selected_item().and_then(|item| item.downcast_ref::().map(|s| s.string().to_string()));
+ let flow_scale_text = state_ref.flow_scale_entry.text().to_string();
+ let performance_mode_active = state_ref.performance_mode_switch.is_active();
+ let hdr_mode_active = state_ref.hdr_mode_switch.is_active();
+ let exp_mode_str = state_ref.experimental_present_mode_dropdown.selected_item().and_then(|item| item.downcast_ref::().map(|s| s.string().to_string()));
+
+ drop(state_ref);
+
+ let mut state = app_state_clone_save.borrow_mut();
+ if let Some(profile) = state.config.game.get_mut(index) {
+ if let Some(text) = multiplier_str {
+ profile.multiplier = if text == "off" { 1 } else { text.parse().unwrap_or(1) };
+ }
+
+ if let Ok(value) = flow_scale_text.parse::() {
+ profile.flow_scale = round_to_2_decimals(value);
+ }
+
+ profile.performance_mode = performance_mode_active;
+ profile.hdr_mode = hdr_mode_active;
+
+ if let Some(text) = exp_mode_str {
+ profile.experimental_present_mode = text;
+ }
+
+ state.save_current_config();
+
+ let feedback_label = Label::new(Some("Saved!"));
+ feedback_label.set_halign(gtk::Align::End);
+ feedback_label.set_margin_end(12);
+ feedback_label.set_margin_bottom(12);
+
+ let main_settings_box_clone = state.main_settings_box.clone();
+
+ main_settings_box_clone.append(&feedback_label);
+
+ glib::timeout_add_local(std::time::Duration::new(2, 0), move || {
+ main_settings_box_clone.remove(&feedback_label);
+ glib::ControlFlow::Break
+ });
+ }
+ }
+ });
+
+ let app_state_clone_initial = app_state.clone();
+ glib::idle_add_local(move || {
+ let mut state = app_state_clone_initial.borrow_mut();
+ if state.config.game.first().is_some() {
+ state.selected_profile_index = Some(0);
+ }
+ state.populate_sidebar_with_handlers(Some(app_state_clone_initial.clone()));
+ drop(state);
+
+ if app_state_clone_initial.borrow().selected_profile_index.is_some() {
+ app_state_clone_initial.borrow().update_main_window_from_profile();
+ }
+ glib::ControlFlow::Break
+ });
+
+ main_window.present();
+ });
+
+ application.run()
+}
diff --git a/ui/src/settings_window.rs b/ui/src/settings_window.rs
new file mode 100644
index 0000000..73417e7
--- /dev/null
+++ b/ui/src/settings_window.rs
@@ -0,0 +1,155 @@
+use gtk::prelude::*;
+use gtk::{glib, Label, Switch, Entry, Box, Orientation};
+use libadwaita::prelude::*;
+use libadwaita::{ApplicationWindow, PreferencesGroup, PreferencesPage, PreferencesWindow, ActionRow};
+use std::rc::Rc;
+use std::cell::RefCell;
+
+use crate::app_state::AppState;
+
+pub fn create_settings_window(parent: &ApplicationWindow, app_state: Rc>) -> PreferencesWindow {
+ let settings_window = PreferencesWindow::builder()
+ .title("Settings")
+ .transient_for(parent)
+ .modal(true)
+ .search_enabled(false)
+ .default_width(450) // Set default width
+ .default_height(300) // Set default height
+ .build();
+
+ let page = PreferencesPage::builder()
+ .title("General")
+ .icon_name("preferences-system-symbolic")
+ .build();
+
+ let group = PreferencesGroup::builder()
+ .title("Global Settings")
+ .build();
+
+ // --- Custom DLL Toggle and Path (Programmatically created) ---
+ let custom_dll_switch = Switch::builder()
+ .halign(gtk::Align::End)
+ .valign(gtk::Align::Center)
+ .build();
+
+ let custom_dll_path_entry = Entry::builder()
+ .placeholder_text("/path/to/Lossless.dll")
+ .hexpand(true)
+ .build();
+
+ let custom_dll_row = ActionRow::builder()
+ .title("Custom Path to Lossless.dll")
+ .build();
+ custom_dll_row.add_suffix(&custom_dll_switch);
+ custom_dll_row.set_activatable_widget(Some(&custom_dll_switch));
+
+ let custom_dll_box = Box::builder()
+ .orientation(Orientation::Vertical)
+ .spacing(6)
+ .margin_top(6)
+ .margin_bottom(6)
+ .build();
+ custom_dll_box.append(&custom_dll_row);
+ custom_dll_box.append(&custom_dll_path_entry);
+
+ group.add(&custom_dll_box); // Add the box directly to the group
+
+ // Initial state setup for Custom DLL
+ let current_dll_path = app_state.borrow().config.ordered_global.global.as_ref()
+ .and_then(|g| g.dll.clone());
+
+ if let Some(path) = current_dll_path {
+ custom_dll_switch.set_active(true);
+ custom_dll_path_entry.set_text(&path);
+ custom_dll_path_entry.set_visible(true);
+ } else {
+ custom_dll_switch.set_active(false);
+ custom_dll_path_entry.set_visible(false);
+ }
+
+ // Connect switch to show/hide entry and update config
+ let app_state_clone_switch = app_state.clone();
+ let custom_dll_path_entry_clone = custom_dll_path_entry.clone();
+ custom_dll_switch.connect_state_set(move |_sw, active| {
+ custom_dll_path_entry_clone.set_visible(active);
+ let mut state = app_state_clone_switch.borrow_mut();
+ if active {
+ // If activating, ensure global config exists and set DLL path
+ let current_path = custom_dll_path_entry_clone.text().to_string();
+ state.config.ordered_global.global.get_or_insert_with(Default::default).dll = Some(current_path);
+ } else {
+ // If deactivating, set DLL path to None
+ if let Some(global_config) = state.config.ordered_global.global.as_mut() {
+ global_config.dll = None;
+ }
+ }
+ glib::Propagation::Proceed
+ });
+
+ // Connect entry to update config
+ let app_state_clone_entry = app_state.clone();
+ let custom_dll_switch_clone = custom_dll_switch.clone();
+ custom_dll_path_entry.connect_changed(move |entry| {
+ let mut state = app_state_clone_entry.borrow_mut();
+ if custom_dll_switch_clone.is_active() {
+ let path = entry.text().to_string();
+ if !path.is_empty() {
+ state.config.ordered_global.global.get_or_insert_with(Default::default).dll = Some(path);
+ } else {
+ // If path is cleared, set dll to None
+ if let Some(global_config) = state.config.ordered_global.global.as_mut() {
+ global_config.dll = None;
+ }
+ }
+ }
+ });
+
+ // Save button for settings
+ let save_settings_button = gtk::Button::builder()
+ .label("Save Global Settings")
+ .halign(gtk::Align::End)
+ .margin_end(12)
+ .margin_bottom(12)
+ .margin_top(12)
+ .build();
+
+ // Create a box to hold the feedback label
+ let feedback_container_box = Box::builder()
+ .orientation(Orientation::Vertical)
+ .halign(gtk::Align::End)
+ .margin_end(12)
+ .margin_bottom(12)
+ .build();
+
+ group.add(&save_settings_button); // Add button first
+ group.add(&feedback_container_box); // Then add the container for feedback
+
+ let app_state_clone_save = app_state.clone();
+ let feedback_container_box_clone = feedback_container_box.clone(); // Clone for the closure
+ save_settings_button.connect_clicked(move |_| {
+ let state = app_state_clone_save.borrow_mut(); // Removed 'mut'
+ state.save_current_config();
+
+ let feedback_label = Label::new(Some("Saved!"));
+ feedback_label.set_halign(gtk::Align::End);
+ feedback_label.set_margin_end(12);
+ feedback_label.set_margin_bottom(12);
+
+ // Append to the dedicated feedback container box
+ feedback_container_box_clone.append(&feedback_label);
+
+ glib::timeout_add_local(std::time::Duration::new(2, 0), {
+ let feedback_label_clone = feedback_label.clone(); // Clone for the timeout
+ let feedback_container_box_clone_for_remove = feedback_container_box_clone.clone(); // Clone for the timeout
+ move || {
+ feedback_container_box_clone_for_remove.remove(&feedback_label_clone);
+ glib::ControlFlow::Break
+ }
+ });
+ });
+
+ page.add(&group);
+ settings_window.add(&page);
+
+ settings_window
+}
diff --git a/ui/src/utils.rs b/ui/src/utils.rs
new file mode 100644
index 0000000..7fe2612
--- /dev/null
+++ b/ui/src/utils.rs
@@ -0,0 +1,59 @@
+use std::process::Command;
+use std::io::{BufReader, BufRead};
+
+pub fn round_to_2_decimals(value: f32) -> f32 {
+ // Use string formatting to get exactly 2 decimal places and then parse back
+ // This avoids floating point precision issues
+ format!("{:.2}", value).parse().unwrap_or(value)
+}
+
+/// Executes a bash command to find running processes that use Vulkan
+/// and are owned by the current user.
+/// Returns a vector of process names.
+pub fn get_vulkan_processes() -> Vec {
+ let mut processes = Vec::new();
+ let command_str = r#"
+ for pid in /proc/[0-9]*; do
+ owner=$(stat -c %U "$pid" 2>/dev/null)
+ if [[ "$owner" == "$USER" ]]; then
+ if grep -qi 'vulkan' "$pid/maps" 2>/dev/null; then
+ procname=$(cat "$pid/comm" 2>/dev/null)
+ if [[ -n "$procname" ]]; then
+ printf "%s\n" "$procname" # Only print the process name
+ fi
+ fi
+ fi
+ done
+ "#;
+
+ // Execute the bash command
+ let output = Command::new("bash")
+ .arg("-c")
+ .arg(command_str)
+ .output();
+
+ match output {
+ Ok(output) => {
+ if output.status.success() {
+ // Read stdout line by line
+ let reader = BufReader::new(output.stdout.as_slice());
+ for line in reader.lines() {
+ if let Ok(proc_name) = line {
+ let trimmed_name = proc_name.trim().to_string();
+ if !trimmed_name.is_empty() {
+ processes.push(trimmed_name);
+ }
+ }
+ }
+ } else {
+ // Print stderr if the command failed
+ eprintln!("Command failed with error: {}", String::from_utf8_lossy(&output.stderr));
+ }
+ },
+ Err(e) => {
+ // Print error if the command could not be executed
+ eprintln!("Failed to execute command: {}", e);
+ }
+ }
+ processes
+}