diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 3b14174e..2e3d746c 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -84,7 +84,7 @@ jobs: args: ${{ matrix.args }} publish-release: - needs: build-installers + needs: [build-installers, create-pre-release] permissions: contents: write runs-on: ubuntu-latest @@ -117,4 +117,4 @@ jobs: repo: context.repo.repo, release_id: process.env.releaseId, draft: false, - }); \ No newline at end of file + }); diff --git a/.gitignore b/.gitignore index c61a7cec..01b6bc89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode dist +.dist out node_modules .env diff --git a/Cargo.lock b/Cargo.lock index 10c9e06e..a6297d12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,9 +529,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.5.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -540,9 +540,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -804,53 +804,56 @@ checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ "bitflags 1.3.2", "block", - "cocoa-foundation", + "cocoa-foundation 0.1.2", "core-foundation 0.9.4", - "core-graphics", + "core-graphics 0.23.2", "foreign-types", "libc", "objc", ] [[package]] -name = "cocoa-foundation" -version = "0.1.2" +name = "cocoa" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "block", - "core-foundation 0.9.4", - "core-graphics-types", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", "libc", "objc", ] [[package]] -name = "color-eyre" -version = "0.6.3" +name = "cocoa-foundation" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", ] [[package]] -name = "color-spantrace" -version = "0.2.1" +name = "cocoa-foundation" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", + "bitflags 2.6.0", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", ] [[package]] @@ -954,7 +957,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys 0.8.6", + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys 0.8.7", "libc", ] @@ -966,9 +979,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" @@ -978,7 +991,20 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "foreign-types", "libc", ] @@ -994,6 +1020,17 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1419,6 +1456,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "evalexpr" +version = "11.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b41cb9dd076076058a4523f009c900c582279536d0b2e45a29aa930e083cc5" + [[package]] name = "event-listener" version = "5.3.1" @@ -1456,16 +1499,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - [[package]] name = "fastrand" version = "2.1.0" @@ -1531,6 +1564,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "flume" version = "0.11.0" @@ -2220,7 +2262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", - "core-foundation-sys 0.8.6", + "core-foundation-sys 0.8.7", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", @@ -2272,19 +2314,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "image" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "num-traits", - "png", -] - [[package]] name = "image" version = "0.25.1" @@ -2324,12 +2353,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - [[package]] name = "indexmap" version = "1.9.3" @@ -2354,9 +2377,9 @@ dependencies = [ [[package]] name = "infer" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199" +checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" dependencies = [ "cfb", ] @@ -2524,15 +2547,27 @@ dependencies = [ [[package]] name = "json-patch" -version = "1.4.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" +checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" dependencies = [ + "jsonptr", "serde", "serde_json", "thiserror", ] +[[package]] +name = "jsonptr" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +dependencies = [ + "fluent-uri", + "serde", + "serde_json", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -2847,11 +2882,11 @@ dependencies = [ [[package]] name = "muda" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145" +checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ - "cocoa", + "cocoa 0.26.0", "crossbeam-channel", "dpi", "gtk", @@ -2861,20 +2896,21 @@ dependencies = [ "png", "serde", "thiserror", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "ndk" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "jni-sys", + "log", "ndk-sys", "num_enum", - "raw-window-handle 0.5.2", + "raw-window-handle", "thiserror", ] @@ -2886,9 +2922,9 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.4.1+23.1.7779620" +version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ "jni-sys", ] @@ -3065,23 +3101,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -3313,9 +3349,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "pango" @@ -3581,7 +3617,7 @@ dependencies = [ "base64 0.21.7", "indexmap 2.2.6", "line-wrap", - "quick-xml", + "quick-xml 0.31.0", "serde", "time", ] @@ -3770,6 +3806,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "encoding_rs", + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.2" @@ -3963,12 +4010,6 @@ dependencies = [ "rgb", ] -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4143,7 +4184,7 @@ dependencies = [ "objc", "objc-foundation", "objc_id", - "raw-window-handle 0.6.2", + "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4382,29 +4423,32 @@ dependencies = [ "serde_yaml", "sys-locale", "uuid", - "windows 0.57.0", + "windows 0.58.0", ] [[package]] name = "seelen-ui" -version = "1.10.6" +version = "2.0.0-beta.17" dependencies = [ "arc-swap", + "backtrace", "base64 0.22.1", "battery", "clap", - "color-eyre", "crossbeam-channel", "encoding_rs", + "evalexpr", "getset", - "image 0.25.1", + "image", "itertools", "lazy_static", "log", "notify-debouncer-full", "os_info", + "owo-colors", "parking_lot", "phf 0.11.2", + "quick-xml 0.36.2", "regex", "seelen-core", "serde", @@ -4425,8 +4469,8 @@ dependencies = [ "uuid", "widestring", "win-screenshot", - "windows 0.57.0", - "windows-core 0.57.0", + "windows 0.58.0", + "windows-core 0.58.0", "winreg 0.52.0", "winvd", ] @@ -4743,7 +4787,7 @@ checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" dependencies = [ "bytemuck", "cfg_aliases 0.2.1", - "core-graphics", + "core-graphics 0.23.2", "foreign-types", "js-sys", "log", @@ -4751,7 +4795,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "objc2-quartz-core", - "raw-window-handle 0.6.2", + "raw-window-handle", "redox_syscall 0.5.2", "wasm-bindgen", "web-sys", @@ -4854,9 +4898,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swift-rs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bbdb58577b6301f8d17ae2561f32002a5bae056d444e0f69e611e504a276204" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" dependencies = [ "base64 0.21.7", "serde", @@ -4919,7 +4963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" dependencies = [ "cfg-if", - "core-foundation-sys 0.8.6", + "core-foundation-sys 0.8.7", "libc", "ntapi", "once_cell", @@ -4944,7 +4988,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "core-foundation-sys 0.8.6", + "core-foundation-sys 0.8.7", "libc", ] @@ -4963,14 +5007,14 @@ dependencies = [ [[package]] name = "tao" -version = "0.28.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea538df05fbc2dcbbd740ba0cfe8607688535f4798d213cbbfa13ce494f3451f" +checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e" dependencies = [ "bitflags 2.6.0", - "cocoa", - "core-foundation 0.9.4", - "core-graphics", + "cocoa 0.26.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -4989,13 +5033,13 @@ dependencies = [ "objc", "once_cell", "parking_lot", - "raw-window-handle 0.6.2", + "raw-window-handle", "scopeguard", "tao-macros", "unicode-segmentation", "url", - "windows 0.57.0", - "windows-core 0.57.0", + "windows 0.58.0", + "windows-core 0.58.0", "windows-version", "x11-dl", ] @@ -5036,13 +5080,12 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tauri" -version = "2.0.0-rc.2" +version = "2.0.0-rc.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ee93e545e49458813d4ed16179c67ee6141dba140ec3d4f078dda3b8d4e0d1" +checksum = "2fa32e2741bda64c1da02d93252a466893180052fc6de61c8803b0356504b70d" dependencies = [ "anyhow", "bytes", - "cocoa", "dirs 5.0.1", "dunce", "embed_plist", @@ -5053,15 +5096,18 @@ dependencies = [ "heck 0.5.0", "http", "http-range", - "image 0.24.9", + "image", "jni", "libc", "log", "mime", "muda", - "objc", + "objc2", + "objc2-app-kit", + "objc2-foundation", "percent-encoding", - "raw-window-handle 0.6.2", + "plist", + "raw-window-handle", "reqwest", "serde", "serde_json", @@ -5082,14 +5128,14 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.57.0", + "windows 0.58.0", ] [[package]] name = "tauri-build" -version = "2.0.0-rc.2" +version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a58b3a716b51d7f671f729bb8c0a53cd2551eec8450c64e828ef4e6c9f948e" +checksum = "148441d64674b2885c1ba5baf3ae61662bb8753859ffcfb541962cbc6b847f39" dependencies = [ "anyhow", "cargo_toml", @@ -5109,9 +5155,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.0.0-rc.2" +version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a9e63ecd827d57228864764e0234935c9aac230099cf145197c8c08e754ced" +checksum = "72a15c3f9282c82871c69ddb65d02ae552738bbac848c8adcab521bf14d8b9e6" dependencies = [ "base64 0.22.1", "brotli", @@ -5136,9 +5182,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-rc.2" +version = "2.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54f5d5b289aa6215ffcfed7d4ff9960a04b7a854436d04519a9fcf911050cba" +checksum = "f12d1aa317bec56f78388cf6012d788876d838595a48f95cbd7835642db356a0" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5150,9 +5196,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.0.0-rc.2" +version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce2ac5e182251ff932750d69c9b240a78e44901a7a6234814d63c595b43660" +checksum = "d82a2adea16b8a71b7a5ad23f720bb13f8d2830b820cc1c266512314ba99bf67" dependencies = [ "anyhow", "glob", @@ -5167,9 +5213,9 @@ dependencies = [ [[package]] name = "tauri-plugin-autostart" -version = "2.0.0-rc.0" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25958a42daab7aaff4faff15cbaffd5505873a0654e6382c74fca1729a791898" +checksum = "992fef0cc6ef3637a8b336c9bf9758b8e718db934afd744846422c1da877dac0" dependencies = [ "auto-launch", "log", @@ -5182,9 +5228,9 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" -version = "2.0.0-rc.0" +version = "2.0.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db97f4b54f2e6f24681c3fffbcb7e9cfff24003b92bb8d3944a39072b8a1178" +checksum = "4a2e49d1fb1aef2bd3a973aa7634474cfdac6bb894854f76a238e2fadf939d37" dependencies = [ "dunce", "log", @@ -5202,13 +5248,12 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.0.0-rc.0" +version = "2.0.0-rc.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c538457a755a75b8bb1594ed40d1512f8f6386251d3fcde492f8f46768ec85b" +checksum = "785722c81beb4a6b729ae55d06aeb68d47166c933e64b727e33254dcb5d4d82d" dependencies = [ - "dunce", "log", - "raw-window-handle 0.6.2", + "raw-window-handle", "rfd", "serde", "serde_json", @@ -5216,16 +5261,19 @@ dependencies = [ "tauri-plugin", "tauri-plugin-fs", "thiserror", + "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.0.0-rc.0" +version = "2.0.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df6b25b1f2b7b61565e66c4dbee9eb39e5635d2a763206e380e07cc3f601a67" +checksum = "4cb1dfbbea322afbc9dec49351bc29edf4e85e74d37d9a3fcc72d67ed55ffdbd" dependencies = [ "anyhow", + "dunce", "glob", + "percent-encoding", "schemars", "serde", "serde_json", @@ -5239,9 +5287,9 @@ dependencies = [ [[package]] name = "tauri-plugin-http" -version = "2.0.0-rc.0" +version = "2.0.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eef17218eaa8bd0fc6cafb7831c63d82ef83b3950d59dc817d92d5320c4f20c" +checksum = "cf6e753f6b98a326ab5e2d604973e761a7970dec0c0bcd5ba83ce7392623dfa8" dependencies = [ "data-url", "http", @@ -5261,13 +5309,13 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.0.0-rc.0" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380d27f23c39cde6a73024e65d8ec9b5b0af861e968dbe16b3aad86cd2c578e5" +checksum = "b57e4666c4a5d81f81b7bb8eacf51ae32c4e69c35071aabb480ad20a80836e4e" dependencies = [ "android_logger", "byte-unit", - "cocoa", + "cocoa 0.25.0", "fern", "log", "objc", @@ -5283,9 +5331,9 @@ dependencies = [ [[package]] name = "tauri-plugin-process" -version = "2.0.0-rc.0" +version = "2.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d3663df0cd3e96feb37d46aad5d499d2edfcca5c62548ad34f1684e0019168" +checksum = "9c9eb80b601682dcbd45dc5ed5f7cc214f1d994aeea730d500899cc616784559" dependencies = [ "tauri", "tauri-plugin", @@ -5293,9 +5341,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.0-rc.0" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9209f6c32caec61e156a5616f7d80ba7683ca4a0a5641cbe5d3086ab371aaab2" +checksum = "e83800ddf78b820172efb5ed7310344e8e4f97fd30cd8237a3f20c12a79eb136" dependencies = [ "encoding_rs", "log", @@ -5314,9 +5362,9 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.0.0-rc.0" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5f10ba18d2fc65e16bdf053b7beccb621dcf880c52d2ab08bdeb2d685e3e14" +checksum = "391ebb8ae8cd6aec44b5d96d3005659d88cde69c57326f639bbc660116a30d63" dependencies = [ "base64 0.22.1", "dirs 5.0.1", @@ -5337,42 +5385,44 @@ dependencies = [ "time", "tokio", "url", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "zip", ] [[package]] name = "tauri-runtime" -version = "2.0.0-rc.2" +version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f01b129b1ebdf09563c354760dbe7c0e96a166b4e33362d9c8d207f527c7ea5" +checksum = "389f78c8e8e6eff3897d8d9581087943b5976ea96a0ab5036be691f28c2b0df0" dependencies = [ "dpi", "gtk", "http", "jni", - "raw-window-handle 0.6.2", + "raw-window-handle", "serde", "serde_json", "tauri-utils", "thiserror", "url", - "windows 0.57.0", + "windows 0.58.0", ] [[package]] name = "tauri-runtime-wry" -version = "2.0.0-rc.2" +version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcda27639094ace2bf25f00bc10e35ea4e3af2f92753b1bdd2a174d1fa5a6292" +checksum = "e17625b7cf63958d53945e199391d11c9f195fb3d1cb8aeb64dc3084d0091b92" dependencies = [ - "cocoa", "gtk", "http", "jni", "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", "percent-encoding", - "raw-window-handle 0.6.2", + "raw-window-handle", "softbuffer", "tao", "tauri-runtime", @@ -5380,15 +5430,15 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.57.0", + "windows 0.58.0", "wry", ] [[package]] name = "tauri-utils" -version = "2.0.0-rc.2" +version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28bb83cffa26e9cb7a2b3d0c31ab87bf277f44aaaa90f17159aef4d37aabd051" +checksum = "3019641087c9039b57ebfca95fa42a93c07056845b7d8d57c8966061bcee83b4" dependencies = [ "brotli", "cargo_metadata", @@ -5728,16 +5778,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-error" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" -dependencies = [ - "tracing", - "tracing-subscriber", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -5769,22 +5809,23 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.14.3" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60" +checksum = "044d7738b3d50f288ddef035b793228740ad4d927f5466b0af55dc15e7e03cfe" dependencies = [ - "cocoa", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dirs 5.0.1", "libappindicator", "muda", - "objc", + "objc2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "png", "serde", "thiserror", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5926,11 +5967,10 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" dependencies = [ - "derive_more", "regex", "serde", "unic-ucd-ident", @@ -6194,23 +6234,23 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6516cfa64c6b3212686080eeec378e662c2af54bb2a5b2a22749673f5cb2226f" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.57.0", - "windows-core 0.57.0", - "windows-implement", - "windows-interface", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-implement 0.58.0", + "windows-interface 0.58.0", ] [[package]] name = "webview2-com-macros" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", @@ -6219,13 +6259,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76d5b77320ff155660be1df3e6588bc85c75f1a9feef938cc4dc4dd60d1d7cf" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" dependencies = [ "thiserror", - "windows 0.57.0", - "windows-core 0.57.0", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] @@ -6286,9 +6326,9 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33082acd404763b315866e14a0d5193f3422c81086657583937a750cdd3ec340" dependencies = [ - "cocoa", + "cocoa 0.25.0", "objc", - "raw-window-handle 0.6.2", + "raw-window-handle", "windows-sys 0.52.0", "windows-version", ] @@ -6322,6 +6362,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -6337,12 +6387,25 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.57.0", + "windows-interface 0.57.0", "windows-result 0.1.2", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -6354,6 +6417,17 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -6365,6 +6439,17 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "windows-registry" version = "0.2.0" @@ -6431,6 +6516,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -6649,25 +6743,25 @@ dependencies = [ [[package]] name = "winvd" version = "0.0.47" -source = "git+https://github.com/eythaann/virtualdesktopaccessor.git#cb8bfef0e07fc8e8eaa8c06fa1a91b4965b933af" +source = "git+https://github.com/eythaann/virtualdesktopaccessor.git#745ab4ac959a070ae0efbf90ff47de644eca3da8" dependencies = [ "macro_rules_attribute", - "windows 0.57.0", - "windows-core 0.57.0", - "windows-implement", - "windows-interface", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-implement 0.58.0", + "windows-interface 0.58.0", ] [[package]] name = "wry" -version = "0.41.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b00c945786b02d7805d09a969fa36d0eee4e0bd4fb3ec2a79d2bf45a1b44cd" +checksum = "f4d715cf5fe88e9647f3d17b207b6d060d4a88e7171d4ccb2d2c657dd1d44728" dependencies = [ "base64 0.22.1", "block", - "cocoa", - "core-graphics", + "cocoa 0.26.0", + "core-graphics 0.24.0", "crossbeam-channel", "dpi", "dunce", @@ -6680,13 +6774,11 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "ndk-context", - "ndk-sys", "objc", "objc_id", "once_cell", "percent-encoding", - "raw-window-handle 0.6.2", + "raw-window-handle", "sha2", "soup3", "tao-macros", @@ -6694,8 +6786,8 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.57.0", - "windows-core 0.57.0", + "windows 0.58.0", + "windows-core 0.58.0", "windows-version", "x11-dl", ] diff --git a/Cargo.toml b/Cargo.toml index 35583d5f..2e7e6dfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["profile-rustflags"] [package] name = "seelen-ui" -version = "1.10.6" +version = "2.0.0-beta.17" description = "Seelen UI Background" authors = ["eythaann"] license = "Polyform Strict License" @@ -20,34 +20,40 @@ path = "src/background/main.rs" incremental = true rustflags = ["-Z", "threads=8"] +[profile.release] +debug = 1 +opt-level = "z" +lto = true +codegen-units = 1 +rustflags = ["-Z", "threads=8"] + [build-dependencies] -tauri-build = { version = "2.0.0-beta", features = [] } +tauri-build = { version = "2.0.0-rc", features = [] } [dependencies] seelen-core = { path = "./lib" } -tauri = { version = "2.0.0-beta", features = [ +tauri = { version = "2.0.0-rc", features = [ "protocol-asset", "tray-icon", "image-png", ] } -tauri-plugin-fs = "2.0.0-beta.2" -tauri-plugin-dialog = "2.0.0-beta.2" -tauri-plugin-autostart = "2.0.0-beta.2" -tauri-plugin-shell = "2.0.0-beta.2" -tauri-plugin-process = "2.0.0-beta.2" -tauri-plugin-log = "2.0.0-beta.2" -tauri-plugin-updater = "2.0.0-beta.2" -tauri-plugin-deep-link = "2.0.0-beta.10" -tauri-plugin-http = "2.0.0-beta.0" +tauri-plugin-fs = "2.0.0-rc" +tauri-plugin-dialog = "2.0.0-rc" +tauri-plugin-autostart = "2.0.0-rc" +tauri-plugin-shell = "2.0.0-rc" +tauri-plugin-process = "2.0.0-rc" +tauri-plugin-log = "2.0.0-rc" +tauri-plugin-updater = "2.0.0-rc" +tauri-plugin-deep-link = "2.0.0-rc" +tauri-plugin-http = "2.0.0-rc" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9.34" -color-eyre = "0.6.2" lazy_static = "1.4.0" parking_lot = "0.12.1" log = "0.4" uuid = "1.8.0" -image = "0.25.0" +image = { version = "0.25.1", features = ["ico"] } widestring = "1.0.2" itertools = "0.12.1" clap = { version = "4.5.4", features = ["derive", "string"] } @@ -60,15 +66,19 @@ sysinfo = "0.30.12" battery = "0.7.8" winvd = { git = "https://github.com/eythaann/virtualdesktopaccessor.git" } winreg = "0.52.0" -windows-core = "=0.57.0" # windows-rs already depends and reexports this, but we need it as a direct dependency (implement macro) +windows-core = "=0.58.0" # windows-rs already depends and reexports this, but we need it as a direct dependency (implement macro) win-screenshot = "4.0.8" base64 = "0.22.1" arc-swap = "1.7.1" notify-debouncer-full = "0.3.1" encoding_rs = "0.8.34" +evalexpr = "=11.3.0" +quick-xml = { version = "0.36.2", features = ["serialize", "encoding"] } +backtrace = "0.3.71" +owo-colors = "4.1.0" [dependencies.windows] -version = "=0.57.0" +version = "=0.58.0" features = [ "Win32_Foundation", "ApplicationModel", @@ -97,6 +107,7 @@ features = [ "Wdk_System_SystemServices", # required to get system info (PROCESS_EXTENDED_BASIC_INFORMATION) "Win32_System_Power", # required for power management (battery - AC) "Win32_System_Shutdown", # required for power management (shutdown) + "Win32_Storage_FileSystem", # PKEYS and Devices/Storage/etc "Win32_Storage_EnhancedStorage", # PKEYS and Devices/Storage/etc "Win32_Storage_Packaging_Appx", # UWP apps "Win32_Media_Audio_Endpoints", # required for audio module diff --git a/capabilities/launcher.json b/capabilities/launcher.json new file mode 100644 index 00000000..13e9ca06 --- /dev/null +++ b/capabilities/launcher.json @@ -0,0 +1,17 @@ +{ + "$schema": "../gen/schemas/windows-schema.json", + "identifier": "launcher", + "windows": [ + "seelen-launcher" + ], + "permissions": [ + "log:default", + "core:webview:default", + "core:event:default", + "core:window:default", + "core:window:allow-set-ignore-cursor-events", + "core:window:allow-hide", + "core:path:default", + "fs:allow-write-text-file" + ] +} \ No newline at end of file diff --git a/capabilities/migrated.json b/capabilities/migrated.json index 83ffbba5..df89a4a7 100644 --- a/capabilities/migrated.json +++ b/capabilities/migrated.json @@ -1,73 +1,70 @@ -{ - "$schema": "../gen/schemas/windows-schema.json", - "identifier": "migrated", - "description": "permissions that were migrated from v1", - "local": true, - "windows": [ - "settings", - "seelenweg/*", - "seelenweg-hitbox/*", - "updater", - "window-manager/*", - "fancy-toolbar/*", - "fancy-toolbar-hitbox/*" - ], - "permissions": [ - "core:path:default", - "core:event:default", - "core:window:default", - "core:webview:default", - "core:app:default", - "core:resources:default", - "core:menu:default", - "core:tray:default", - "deep-link:default", - - "fs:allow-read-text-file", - "fs:allow-write-text-file", - "fs:allow-exists", - "fs:allow-mkdir", - "fs:allow-read-dir", - "fs:allow-copy-file", - "fs:allow-remove", - - { - "identifier": "fs:scope", - "allow": [ - { - "path": "**" - }, - { - "path": "**/*" - }, - { - "path": "/**/*" - } - ] - }, - - "core:window:allow-show", - "core:window:allow-close", - "core:window:allow-start-dragging", - "core:window:allow-set-size", - "core:window:allow-set-position", - "core:window:allow-set-ignore-cursor-events", - - "autostart:allow-enable", - "autostart:allow-disable", - "autostart:allow-is-enabled", - - "dialog:allow-save", - "dialog:allow-open", - - "process:allow-restart", - "process:allow-exit", - - "log:allow-log", - - "updater:allow-check", - "updater:allow-download-and-install", - - "shell:allow-open" - ] +{ + "$schema": "../gen/schemas/windows-schema.json", + "identifier": "migrated", + "description": "permissions that were migrated from v1", + "local": true, + "windows": [ + "settings", + "seelenweg/*", + "updater", + "window-manager/*", + "fancy-toolbar/*" + ], + "permissions": [ + "core:path:default", + "core:event:default", + "core:window:default", + "core:webview:default", + "core:app:default", + "core:resources:default", + "core:menu:default", + "core:tray:default", + "deep-link:default", + + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-exists", + "fs:allow-mkdir", + "fs:allow-read-dir", + "fs:allow-copy-file", + "fs:allow-remove", + + { + "identifier": "fs:scope", + "allow": [ + { + "path": "**" + }, + { + "path": "**/*" + }, + { + "path": "/**/*" + } + ] + }, + + "core:window:allow-show", + "core:window:allow-close", + "core:window:allow-start-dragging", + "core:window:allow-set-size", + "core:window:allow-set-position", + "core:window:allow-set-ignore-cursor-events", + + "autostart:allow-enable", + "autostart:allow-disable", + "autostart:allow-is-enabled", + + "dialog:allow-save", + "dialog:allow-open", + + "process:allow-restart", + "process:allow-exit", + + "log:allow-log", + + "updater:default", + + "shell:allow-open" + ] } \ No newline at end of file diff --git a/capabilities/wall.json b/capabilities/wall.json new file mode 100644 index 00000000..f1237d8a --- /dev/null +++ b/capabilities/wall.json @@ -0,0 +1,15 @@ +{ + "$schema": "../gen/schemas/windows-schema.json", + "identifier": "wall", + "description": "permissions for wall", + "local": true, + "windows": [ + "seelen-wall" + ], + "permissions": [ + "log:default", + "core:webview:default", + "core:event:default", + "core:window:allow-show" + ] +} \ No newline at end of file diff --git a/changelog.md b/changelog.md index e2a6c0ca..8d623e17 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,50 @@ # Changelog -## [Unreleased] +## [Unreleased] +### breaking changes +- Window Manager Layout Conditions was reimplemented, old conditions (v1) will fail. + +### refactor +- refactors, more and more refactors, refactors for everyone. +- reimplementation of Tiling Window Manager. +- remove Update modal at startup by an update button on settings. + +### features +- make the dock/taskbar solid when hide mode is `never`. +- add app launcher (rofi for windows). +- add seelen wall (rain-meter and wallpaper engine alternatives). +- expose function to pin items into the dock. +- settings by monitor. +- window manager multimonitor support. +- allow users change date format directly on UI settings. +- add context menu to toolbar items. + +### enhancements +- improve quality icons from all app/files items. +- improve init loading performance. +- improve fullscreen matching system. +- reduce UI total size from 355mb (v1) to 121mb (v2-beta4) to 93mb (v2-beta8). +- reduce Installer size from 75mb (v1) to 40mb (v2-beta4) to 28.8mb (v2-beta8). +- allow drop files, apps and folders into the dock to pin them. +- now Virtual Desktop shortcuts doesn't require Tiling WM be enabled to work. +- now Themes are wrapped in a CSS layer, making easier the override theming. +- allow change size of Window Manager Layouts via window resizing with the mouse. +- allow close windows by middle clicking on dock items. +- show icon of app in media players that are not uwp/msix. +- show pwa apps like a separeted app from browser on dock. + +### fix +- missing icons for files with a different extension than `exe`. +- losing cursor events on clicking a dock item. +- app allowing be closed via Alt + F4. +- native taskbar being hidden regardless of whether the program starts successfully or not. +- app continuing running when the program fails to start (case: WebView2 Runtime not installed). +- no stoping correctly secondary processes/threads on app close. +- showing unmanageable windows on dock. +- restart seelen-ui button not working properly. +- tray icons not working on others language than english. +- edge tabs open in file explorer. + ## [1.10.6] ### fix - tray module only working when the system language is english. diff --git a/documentation/images/app_launcher_preview.png b/documentation/images/app_launcher_preview.png new file mode 100644 index 00000000..74f1c7b4 Binary files /dev/null and b/documentation/images/app_launcher_preview.png differ diff --git a/documentation/images/media_module_preview.png b/documentation/images/media_module_preview.png index 2d24c7ac..c878dbae 100644 Binary files a/documentation/images/media_module_preview.png and b/documentation/images/media_module_preview.png differ diff --git a/documentation/images/preview3.png b/documentation/images/preview3.png deleted file mode 100644 index e7ab3f6e..00000000 Binary files a/documentation/images/preview3.png and /dev/null differ diff --git a/documentation/images/seelen.png b/documentation/images/seelen.png deleted file mode 100644 index 75471a23..00000000 Binary files a/documentation/images/seelen.png and /dev/null differ diff --git a/documentation/images/mosaico.png b/documentation/images/settings_preview.png similarity index 100% rename from documentation/images/mosaico.png rename to documentation/images/settings_preview.png diff --git a/documentation/images/twm_preview.png b/documentation/images/twm_preview.png new file mode 100644 index 00000000..9f3f1d85 Binary files /dev/null and b/documentation/images/twm_preview.png differ diff --git a/documentation/schemas/icon_pack.schema.json b/documentation/schemas/icon_pack.schema.json new file mode 100644 index 00000000..33f634e3 --- /dev/null +++ b/documentation/schemas/icon_pack.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IconPack", + "type": "object", + "properties": { + "apps": { + "description": "Key can be user model id, filename or a full path.\n\nValue is the path to the icon relative to the icon pack folder.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "info": { + "default": { + "author": "Unknown", + "description": "", + "displayName": "Unknown", + "filename": "", + "tags": [] + }, + "allOf": [ + { + "$ref": "#/definitions/ResourceMetadata" + } + ] + } + }, + "definitions": { + "ResourceMetadata": { + "type": "object", + "properties": { + "author": { + "default": "Unknown", + "type": "string" + }, + "description": { + "default": "", + "type": "string" + }, + "displayName": { + "default": "Unknown", + "type": "string" + }, + "filename": { + "default": "", + "type": "string" + }, + "tags": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/documentation/schemas/layout.schema.json b/documentation/schemas/layout.schema.json index da6d16d3..22dec383 100644 --- a/documentation/schemas/layout.schema.json +++ b/documentation/schemas/layout.schema.json @@ -86,11 +86,11 @@ { "type": "object", "required": [ + "children", "type" ], "properties": { "children": { - "default": [], "type": "array", "items": { "$ref": "#/definitions/WmNode" @@ -107,7 +107,7 @@ "description": "How much of the remaining space this node will take", "default": 1.0, "type": "number", - "format": "double" + "format": "float" }, "priority": { "description": "Order in how the tree will be traversed (1 = first, 2 = second, etc.)", @@ -135,11 +135,11 @@ { "type": "object", "required": [ + "children", "type" ], "properties": { "children": { - "default": [], "type": "array", "items": { "$ref": "#/definitions/WmNode" @@ -156,7 +156,7 @@ "description": "How much of the remaining space this node will take", "default": 1.0, "type": "number", - "format": "double" + "format": "float" }, "priority": { "description": "Order in how the tree will be traversed (1 = first, 2 = second, etc.)", @@ -198,7 +198,7 @@ "description": "How much of the remaining space this node will take", "default": 1.0, "type": "number", - "format": "double" + "format": "float" }, "handle": { "description": "window handle (HWND) in the node", @@ -239,6 +239,8 @@ "properties": { "active": { "description": "active window handle (HWND) in the node", + "default": null, + "readOnly": true, "type": [ "integer", "null" @@ -256,11 +258,12 @@ "description": "How much of the remaining space this node will take", "default": 1.0, "type": "number", - "format": "double" + "format": "float" }, "handles": { "description": "window handles (HWND) in the node", "default": [], + "readOnly": true, "type": "array", "items": { "type": "integer", @@ -298,6 +301,8 @@ "properties": { "active": { "description": "active window handle (HWND) in the node", + "default": null, + "readOnly": true, "type": [ "integer", "null" @@ -315,11 +320,12 @@ "description": "How much of the remaining space this node will take", "default": 1.0, "type": "number", - "format": "double" + "format": "float" }, "handles": { "description": "window handles (HWND) in the node", "default": [], + "readOnly": true, "type": "array", "items": { "type": "integer", diff --git a/documentation/schemas/placeholder.schema.json b/documentation/schemas/placeholder.schema.json index cbc11173..d1ab1f10 100644 --- a/documentation/schemas/placeholder.schema.json +++ b/documentation/schemas/placeholder.schema.json @@ -117,10 +117,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -186,10 +184,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -254,7 +250,7 @@ ] }, "each": { - "description": "Time unit to refresh the showing date", + "description": "@deprecated -- v2 uses settings date format instead (it will perform the minimal updates)", "default": "minute", "allOf": [ { @@ -263,16 +259,14 @@ ] }, "format": { - "description": "Format of the date, see [moment.js displaying format](https://momentjs.com/docs/#/displaying/format/)", + "description": "@deprecated -- v2 uses settings date format instead", "default": "MMM Do, HH:mm", "type": "string" }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -338,10 +332,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -407,10 +399,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -481,10 +471,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -555,10 +543,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -624,10 +610,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -693,10 +677,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -762,10 +744,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "onClick": { "description": "Deprecated use `onClickV2` instead.", @@ -831,10 +811,8 @@ }, "id": { "description": "Id to identify the item, should be unique.", - "type": [ - "string", - "null" - ] + "default": "", + "type": "string" }, "mode": { "default": "dotted", diff --git a/documentation/schemas/settings.schema.json b/documentation/schemas/settings.schema.json index 1c545507..39776a99 100644 --- a/documentation/schemas/settings.schema.json +++ b/documentation/schemas/settings.schema.json @@ -207,6 +207,11 @@ "default": false, "type": "boolean" }, + "dateFormat": { + "description": "MomentJS date format", + "default": "ddd D MMM, hh:mm A", + "type": "string" + }, "devTools": { "description": "enable or disable dev tools tab in settings", "default": false, @@ -226,32 +231,77 @@ } ] }, + "iconPacks": { + "description": "list of selected icon packs", + "default": [ + "system" + ], + "type": "array", + "items": { + "type": "string" + } + }, "language": { "description": "language to use, if null the system locale is used", - "default": "en", + "default": "es", "type": [ "string", "null" ] }, + "launcher": { + "description": "App launcher settings", + "default": { + "enabled": true, + "monitor": "Mouse-Over", + "runners": [ + { + "id": "RUN", + "label": "t:app_launcher.runners.explorer", + "program": "explorer.exe", + "readonly": true + }, + { + "id": "CMD", + "label": "t:app_launcher.runners.cmd", + "program": "cmd.exe", + "readonly": true + } + ] + }, + "allOf": [ + { + "$ref": "#/definitions/SeelenLauncherSettings" + } + ] + }, "monitors": { "description": "list of monitors", "default": [ { - "workAreaOffset": null, - "workspaces": [ - { - "gap": null, - "layout": "BSP", - "name": "New Workspace", - "padding": null - } - ] + "tb": { + "enabled": true + }, + "wall": { + "backgrounds": null, + "enabled": true + }, + "weg": { + "enabled": true + }, + "wm": { + "enabled": true, + "gap": null, + "layout": null, + "margin": null, + "padding": null + }, + "workspacesV2": [] } ], "type": "array", "items": { - "$ref": "#/definitions/Monitor" + "$ref": "#/definitions/MonitorConfiguration" } }, "seelenweg": { @@ -274,7 +324,7 @@ } ] }, - "selectedTheme": { + "selectedThemes": { "description": "list of selected themes", "default": [ "default" @@ -293,6 +343,19 @@ } ] }, + "wall": { + "description": "background and virtual desktops config", + "default": { + "backgrounds": [], + "enabled": true, + "interval": 60 + }, + "allOf": [ + { + "$ref": "#/definitions/SeelenWallSettings" + } + ] + }, "windowManager": { "description": "window manager config", "default": { @@ -308,15 +371,15 @@ "height": 500.0, "width": 800.0 }, - "globalWorkAreaOffset": { + "resizeDelta": 10.0, + "workspaceGap": 10, + "workspaceMargin": { "bottom": 0, "left": 0, "right": 0, "top": 0 }, - "resizeDelta": 10.0, - "workspaceGap": 10.0, - "workspacePadding": 10.0 + "workspacePadding": 10 }, "allOf": [ { @@ -902,6 +965,15 @@ } } }, + "FancyToolbarSettingsByMonitor": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + } + }, "FloatingWindowSettings": { "type": "object", "properties": { @@ -942,32 +1014,60 @@ } ] }, - "Monitor": { + "MonitorConfiguration": { "type": "object", "properties": { - "workAreaOffset": { - "default": null, - "anyOf": [ + "tb": { + "default": { + "enabled": true + }, + "allOf": [ { - "$ref": "#/definitions/Rect" - }, + "$ref": "#/definitions/FancyToolbarSettingsByMonitor" + } + ] + }, + "wall": { + "default": { + "backgrounds": null, + "enabled": true + }, + "allOf": [ { - "type": "null" + "$ref": "#/definitions/SeelenWallSettingsByMonitor" } ] }, - "workspaces": { - "default": [ + "weg": { + "default": { + "enabled": true + }, + "allOf": [ { - "gap": null, - "layout": "BSP", - "name": "New Workspace", - "padding": null + "$ref": "#/definitions/SeelenWegSettingsByMonitor" } - ], + ] + }, + "wm": { + "default": { + "enabled": true, + "gap": null, + "layout": null, + "margin": null, + "padding": null + }, + "allOf": [ + { + "$ref": "#/definitions/WindowManagerSettingsByMonitor" + } + ] + }, + "workspacesV2": { + "description": "list of settings by workspace on this monitor", + "default": [], "type": "array", "items": { - "$ref": "#/definitions/Workspace" + "$ref": "#/definitions/WorkspaceConfiguration" } } } @@ -999,6 +1099,128 @@ } } }, + "SeelenLauncherMonitor": { + "type": "string", + "enum": [ + "primary", + "Mouse-Over" + ] + }, + "SeelenLauncherRunner": { + "type": "object", + "properties": { + "id": { + "default": "", + "type": "string" + }, + "label": { + "default": "", + "type": "string" + }, + "program": { + "default": "", + "type": "string" + }, + "readonly": { + "default": false, + "type": "boolean" + } + } + }, + "SeelenLauncherSettings": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "monitor": { + "default": "Mouse-Over", + "allOf": [ + { + "$ref": "#/definitions/SeelenLauncherMonitor" + } + ] + }, + "runners": { + "default": [ + { + "id": "RUN", + "label": "t:app_launcher.runners.explorer", + "program": "explorer.exe", + "readonly": true + }, + { + "id": "CMD", + "label": "t:app_launcher.runners.cmd", + "program": "cmd.exe", + "readonly": true + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/SeelenLauncherRunner" + } + } + } + }, + "SeelenWallSettings": { + "type": "object", + "properties": { + "backgrounds": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/SeelenWallWallpaper" + } + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "interval": { + "description": "update interval in seconds", + "default": 60, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "SeelenWallSettingsByMonitor": { + "type": "object", + "properties": { + "backgrounds": { + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/SeelenWallWallpaper" + } + }, + "enabled": { + "default": true, + "type": "boolean" + } + } + }, + "SeelenWallWallpaper": { + "type": "object", + "required": [ + "id", + "path" + ], + "properties": { + "id": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, "SeelenWegMode": { "type": "string", "enum": [ @@ -1083,6 +1305,15 @@ } } }, + "SeelenWegSettingsByMonitor": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + } + }, "SeelenWegSide": { "type": "string", "enum": [ @@ -1142,7 +1373,20 @@ } ] }, - "globalWorkAreaOffset": { + "resizeDelta": { + "description": "the resize size in % to be used when resizing via cli", + "default": 10.0, + "type": "number", + "format": "float" + }, + "workspaceGap": { + "description": "default gap between containers", + "default": 10, + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "workspaceMargin": { "description": "default workspace margin", "default": { "bottom": 0, @@ -1156,54 +1400,107 @@ } ] }, - "resizeDelta": { - "description": "the resize size in % to be used when resizing via cli", - "default": 10.0, - "type": "number", - "format": "double" - }, - "workspaceGap": { - "description": "default gap between containers", - "default": 10.0, - "type": "number", - "format": "double" - }, "workspacePadding": { "description": "default workspace padding", - "default": 10.0, - "type": "number", - "format": "double" + "default": 10, + "type": "integer", + "format": "uint32", + "minimum": 0.0 } } }, - "Workspace": { + "WindowManagerSettingsByMonitor": { "type": "object", "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, "gap": { "default": null, "type": [ - "number", + "integer", "null" ], - "format": "double" + "format": "uint32", + "minimum": 0.0 }, "layout": { - "default": "BSP", - "type": "string" + "default": null, + "type": [ + "string", + "null" + ] }, - "name": { - "default": "New Workspace", - "type": "string" + "margin": { + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Rect" + }, + { + "type": "null" + } + ] }, "padding": { "default": null, "type": [ - "number", + "integer", "null" ], - "format": "double" + "format": "uint32", + "minimum": 0.0 } } + }, + "WorkspaceConfiguration": { + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "backgrounds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/SeelenWallWallpaper" + } + }, + "identifier": { + "$ref": "#/definitions/WorkspaceIdentifier" + }, + "layout": { + "type": [ + "string", + "null" + ] + } + } + }, + "WorkspaceIdentifier": { + "type": "object", + "required": [ + "id", + "kind" + ], + "properties": { + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/WorkspaceIdentifierType" + } + } + }, + "WorkspaceIdentifierType": { + "type": "string", + "enum": [ + "Name", + "Index" + ] } } } \ No newline at end of file diff --git a/documentation/schemas/settings_by_app.schema.json b/documentation/schemas/settings_by_app.schema.json index b4af7aae..230f00eb 100644 --- a/documentation/schemas/settings_by_app.schema.json +++ b/documentation/schemas/settings_by_app.schema.json @@ -23,11 +23,13 @@ "minimum": 0.0 }, "boundWorkspace": { - "description": "workspace name that the app should be bound to", + "description": "workspace index that the app should be bound to", "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint", + "minimum": 0.0 }, "category": { "description": "category to group the app under", @@ -155,8 +157,7 @@ "StartsWith", "EndsWith", "Contains", - "Regex", - "Legacy" + "Regex" ] } } diff --git a/documentation/schemas/theme.schema.json b/documentation/schemas/theme.schema.json index 1cf198f0..cbc25c29 100644 --- a/documentation/schemas/theme.schema.json +++ b/documentation/schemas/theme.schema.json @@ -21,7 +21,9 @@ "styles": { "description": "Css Styles of the theme", "default": { + "launcher": "", "toolbar": "", + "wall": "", "weg": "", "wm": "" }, @@ -36,8 +38,18 @@ "ThemeCss": { "type": "object", "properties": { + "launcher": { + "description": "Css Styles for the app launcher", + "default": "", + "type": "string" + }, "toolbar": { - "description": "Css Styles for the window manager", + "description": "Css Styles for the toolbar", + "default": "", + "type": "string" + }, + "wall": { + "description": "Css Styles for the wall", "default": "", "type": "string" }, diff --git a/documentation/schemas/weg_items.schema.json b/documentation/schemas/weg_items.schema.json index d53dec73..7c74c425 100644 --- a/documentation/schemas/weg_items.schema.json +++ b/documentation/schemas/weg_items.schema.json @@ -8,6 +8,28 @@ "definitions": { "WegItem": { "oneOf": [ + { + "type": "object", + "required": [ + "is_dir", + "path", + "type" + ], + "properties": { + "is_dir": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Pinned" + ] + } + } + }, { "type": "object", "required": [ @@ -17,11 +39,10 @@ ], "properties": { "exe": { - "description": "executable path", "type": "string" }, "execution_path": { - "description": "command to open the app using explorer.exe (uwp apps starts with `shell:AppsFolder`)", + "description": "command to open the app using explorer.exe (UWP apps start with `shell:AppsFolder`)", "type": "string" }, "type": { @@ -41,11 +62,10 @@ ], "properties": { "exe": { - "description": "executable path", "type": "string" }, "execution_path": { - "description": "command to open the app using explorer.exe (uwp apps starts with `shell:AppsFolder`)", + "description": "command to open the app using explorer.exe (UWP apps start with `shell:AppsFolder`)", "type": "string" }, "type": { diff --git a/eslint.config.js b/eslint.config.js index bbc35447..139ce22b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,96 +1,96 @@ -const stylistic = require('@stylistic/eslint-plugin'); -const simpleImportSort = require('eslint-plugin-simple-import-sort'); -const tsEslint = require('typescript-eslint'); - -module.exports = [ - { - ignores: ['node_modules/', '.git/', 'dist/', 'target/'], - }, - { - files: ['**/*.{js,jsx,ts,tsx,mjs}'], - plugins: { - '@stylistic': stylistic, - 'simple-import-sort': simpleImportSort, - '@ts': tsEslint.plugin, - }, - languageOptions: { - parser: tsEslint.parser, - }, - rules: { - 'simple-import-sort/imports': [ - 'error', - { - groups: [ - [''], - ['.*/(infra|infrastructure).*'], - ['.*/app'], - ['.*/domain.*'], - ['.*.module.css$'], - ['.*.css$'], - ], - }, - ], - 'no-dupe-keys': 'error', - '@stylistic/key-spacing': ['error', { beforeColon: false }], - '@stylistic/block-spacing': 'error', - '@stylistic/arrow-spacing': 'error', - '@stylistic/one-var-declaration-per-line': ['error', 'always'], - '@stylistic/object-curly-spacing': ['error', 'always'], - - '@stylistic/brace-style': ['error', '1tbs'], - '@stylistic/jsx-quotes': ['error', 'prefer-double'], - 'no-nested-ternary': 'error', - - '@stylistic/comma-dangle': ['error', 'always-multiline'], - '@stylistic/comma-spacing': [ - 'error', - { - before: false, - after: true, - }, - ], - '@stylistic/keyword-spacing': 'error', - '@stylistic/space-before-blocks': 'error', - '@stylistic/no-multiple-empty-lines': [ - 'error', - { - max: 1, - maxEOF: 1, - }, - ], - '@stylistic/lines-between-class-members': [ - 'error', - 'always', - { exceptAfterSingleLine: true }, - ], - '@stylistic/padded-blocks': ['error', 'never'], - '@stylistic/arrow-parens': ['error', 'always'], - '@stylistic/space-before-function-paren': [ - 'error', - { - anonymous: 'always', - named: 'never', - asyncArrow: 'always', - }, - ], - '@stylistic/quotes': ['error', 'single'], - '@stylistic/semi': 'error', - '@stylistic/no-multi-spaces': ['error'], - '@stylistic/no-trailing-spaces': ['error'], - '@stylistic/space-infix-ops': ['error'], - '@stylistic/indent': ['error', 2], - '@stylistic/jsx-indent': ['error', 2], - '@stylistic/member-delimiter-style': ['error'], - '@stylistic/type-annotation-spacing': ['error'], - '@ts/no-unused-vars': [ - 'warn', - { - varsIgnorePattern: '^_', - argsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - destructuredArrayIgnorePattern: '^_', - }, - ], - }, - }, -]; +const stylistic = require('@stylistic/eslint-plugin'); +const simpleImportSort = require('eslint-plugin-simple-import-sort'); +const tsEslint = require('typescript-eslint'); + +module.exports = [ + { + ignores: ['node_modules/', '.git/', 'dist/', 'target/'], + }, + { + files: ['**/*.{js,jsx,ts,tsx,mjs}'], + plugins: { + '@stylistic': stylistic, + 'simple-import-sort': simpleImportSort, + '@ts': tsEslint.plugin, + }, + languageOptions: { + parser: tsEslint.parser, + }, + rules: { + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + [''], + ['.*/(infra|infrastructure).*'], + ['.*/app.*'], + ['.*/domain.*'], + ['\\.+/', '.*.module.css$'], + ['.*.css$'], + ], + }, + ], + 'no-dupe-keys': 'error', + '@stylistic/key-spacing': ['error', { beforeColon: false }], + '@stylistic/block-spacing': 'error', + '@stylistic/arrow-spacing': 'error', + '@stylistic/one-var-declaration-per-line': ['error', 'always'], + '@stylistic/object-curly-spacing': ['error', 'always'], + + '@stylistic/brace-style': ['error', '1tbs'], + '@stylistic/jsx-quotes': ['error', 'prefer-double'], + 'no-nested-ternary': 'error', + + '@stylistic/comma-dangle': ['error', 'always-multiline'], + '@stylistic/comma-spacing': [ + 'error', + { + before: false, + after: true, + }, + ], + '@stylistic/keyword-spacing': 'error', + '@stylistic/space-before-blocks': 'error', + '@stylistic/no-multiple-empty-lines': [ + 'error', + { + max: 1, + maxEOF: 1, + }, + ], + '@stylistic/lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true }, + ], + '@stylistic/padded-blocks': ['error', 'never'], + '@stylistic/arrow-parens': ['error', 'always'], + '@stylistic/space-before-function-paren': [ + 'error', + { + anonymous: 'always', + named: 'never', + asyncArrow: 'always', + }, + ], + '@stylistic/quotes': ['error', 'single'], + '@stylistic/semi': 'error', + '@stylistic/no-multi-spaces': ['error'], + '@stylistic/no-trailing-spaces': ['error'], + '@stylistic/space-infix-ops': ['error'], + '@stylistic/indent': ['error', 2], + '@stylistic/jsx-indent': ['error', 2], + '@stylistic/member-delimiter-style': ['error'], + '@stylistic/type-annotation-spacing': ['error'], + '@ts/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + }, + ], + }, + }, +]; diff --git a/lefthook.yml b/lefthook.yml index 0a47c560..1e7ac4d0 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -6,24 +6,45 @@ commit-msg: pre-commit: commands: js-linter: + priority: 1 glob: "**/*.{js,jsx,ts,tsx}" run: npm run lint rust-linter: + priority: 2 glob: "**/*.rs" run: cargo fmt -- --check + pre-hook: + priority: 3 + glob: "**/*.rs" + run: rust-script ./scripts/lefthook/pre_hook.rs rust-code-check: + priority: 4 glob: "**/*.rs" run: cargo clippy -- -D warnings build-schemas: + priority: 5 glob: "**/*.rs" run: npm run build:schemas && git add documentation/schemas/*.schema.json + post-hook: + priority: 6 + glob: "**/*.rs" + run: rust-script ./scripts/lefthook/post_hook.rs pre-push: - parallel: true commands: js-test: + priority: 1 glob: "**/*.{js,jsx,ts,tsx}" run: npm run test + pre-hook: + priority: 2 + glob: "**/*.rs" + run: rust-script ./scripts/lefthook/pre_hook.rs rust-test: + priority: 3 glob: "**/*.rs" run: cargo test + post-hook: + priority: 4 + glob: "**/*.rs" + run: rust-script ./scripts/lefthook/post_hook.rs diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 723a6e71..4714e975 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -331,9 +331,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows" -version = "0.57.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ "windows-core", "windows-targets", @@ -341,21 +341,22 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.57.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ "windows-implement", "windows-interface", "windows-result", + "windows-strings", "windows-targets", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", @@ -364,9 +365,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", @@ -375,13 +376,23 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ "windows-targets", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 6a191047..5578ca31 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,18 +1,18 @@ -[package] -name = "seelen-core" -version = "1.9.7" -edition = "2021" - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.9.34" -serde_alias = "0.0.2" -schemars = "0.8.21" -regex = "1.10.4" -sys-locale = "0.3.1" -uuid = { version = "1.8.0", features = ["v4"] } - -[dependencies.windows] -version = "=0.57.0" -features = ["Win32_Foundation"] +[package] +name = "seelen-core" +version = "1.9.7" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9.34" +serde_alias = "0.0.2" +schemars = "0.8.21" +regex = "1.10.4" +sys-locale = "0.3.1" +uuid = { version = "1.8.0", features = ["v4"] } + +[dependencies.windows] +version = "=0.58.0" +features = ["Win32_Foundation"] diff --git a/lib/src/handlers/events.ts b/lib/src/handlers/events.ts new file mode 100644 index 00000000..2b6bfd08 --- /dev/null +++ b/lib/src/handlers/events.ts @@ -0,0 +1,63 @@ +export enum SeelenEvent { + WorkspacesChanged = 'workspaces-changed', + ActiveWorkspaceChanged = 'active-workspace-changed', + + GlobalFocusChanged = 'global-focus-changed', + GlobalMouseMove = 'global-mouse-move', + GlobalMonitorsChanged = 'global-monitors-changed', + + HandleLayeredHitboxes = 'handle-layered', + + MediaSessions = 'media-sessions', + MediaInputs = 'media-inputs', + MediaOutputs = 'media-outputs', + + NetworkDefaultLocalIp = 'network-default-local-ip', + NetworkAdapters = 'network-adapters', + NetworkInternetConnection = 'network-internet-connection', + NetworkWlanScanned = 'wlan-scanned', + + Notifications = 'notifications', + + PowerStatus = 'power-status', + BatteriesStatus = 'batteries-status', + + ColorsChanged = 'colors-changed', + + TrayInfo = 'tray-info', + + ToolbarOverlaped = 'set-auto-hide', + + WegOverlaped = 'set-auto-hide', + WegSetFocusedHandle = 'set-focused-handle', + WegSetFocusedExecutable = 'set-focused-executable', + WegUpdateOpenAppInfo = 'update-open-app-info', + WegAddOpenApp = 'add-open-app', + WegRemoveOpenApp = 'remove-open-app', + + WMSetReservation = 'set-reservation', + WMUpdateHeight = 'update-height', + WMUpdateWidth = 'update-width', + WMResetWorkspaceSize = 'reset-workspace-size', + WMFocus = 'focus', + WMSetActiveWorkspace = 'set-active-workspace', + WMAddWindow = 'add-window', + WMUpdateWindow = 'update-window', + WMRemoveWindow = 'remove-window', + + WMForceRetiling = 'wm-force-retiling', + WMSetLayout = 'wm-set-layout', + WMSetOverlayVisibility = 'wm-set-overlay-visibility', + WMSetActiveWindow = 'wm-set-active-window', + + WallStop = 'wall-stop', + + StateSettingsChanged = 'settings-changed', + StateWegItemsChanged = 'weg-items', + StateThemesChanged = 'themes', + StatePlaceholdersChanged = 'placeholders', + StateLayoutsChanged = 'layouts', + StateSettingsByAppChanged = 'settings-by-app', + StateHistoryChanged = 'history', + StateIconPacksChanged = 'icon-packs', +} diff --git a/lib/src/handlers/index.ts b/lib/src/handlers/index.ts new file mode 100644 index 00000000..76ccb425 --- /dev/null +++ b/lib/src/handlers/index.ts @@ -0,0 +1,22 @@ +export * from './invokers'; +export * from './events'; + +import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; + +import { SeelenEvent } from './events'; +import { SeelenCommand } from './invokers'; + +export function Obtainable(invokeKey: SeelenCommand, eventKey: SeelenEvent) { + return class { + static async getAsync(): Promise { + return await invoke(invokeKey); + } + + static async onChange(cb: (value: T) => void) { + await listen(eventKey, (event) => { + cb(event.payload); + }); + } + }; +} diff --git a/lib/src/handlers/invokers.ts b/lib/src/handlers/invokers.ts new file mode 100644 index 00000000..deea2fd3 --- /dev/null +++ b/lib/src/handlers/invokers.ts @@ -0,0 +1,76 @@ +export enum SeelenCommand { + // General + Run = 'run', + IsDevMode = 'is_dev_mode', + OpenFile = 'open_file', + RunAsAdmin = 'run_as_admin', + SelectFileOnExplorer = 'select_file_on_explorer', + IsVirtualDesktopSupported = 'is_virtual_desktop_supported', + GetUserEnvs = 'get_user_envs', + ShowAppSettings = 'show_app_settings', + SwitchWorkspace = 'switch_workspace', + SendKeys = 'send_keys', + GetIcon = 'get_icon', + GetSystemColors = 'get_system_colors', + SimulateFullscreen = 'simulate_fullscreen', + + // Seelen Settings + SetAutoStart = 'set_auto_start', + GetAutoStartStatus = 'get_auto_start_status', + StateGetThemes = 'state_get_themes', + StateGetPlaceholders = 'state_get_placeholders', + StateGetLayouts = 'state_get_layouts', + StateGetWegItems = 'state_get_weg_items', + StateGetSettings = 'state_get_settings', + StateGetSpecificAppsConfigurations = 'state_get_specific_apps_configurations', + StateGetWallpaper = 'state_get_wallpaper', + StateSetWallpaper = 'state_set_wallpaper', + StateGetHistory = 'state_get_history', + + // Media + MediaPrev = 'media_prev', + MediaTogglePlayPause = 'media_toggle_play_pause', + MediaNext = 'media_next', + SetVolumeLevel = 'set_volume_level', + MediaToggleMute = 'media_toggle_mute', + MediaSetDefaultDevice = 'media_set_default_device', + + // Brightness + GetMainMonitorBrightness = 'get_main_monitor_brightness', + SetMainMonitorBrightness = 'set_main_monitor_brightness', + + // Power + LogOut = 'log_out', + Suspend = 'suspend', + Restart = 'restart', + Shutdown = 'shutdown', + + // SeelenWeg + WegCloseApp = 'weg_close_app', + WegToggleWindowState = 'weg_toggle_window_state', + WegRequestUpdatePreviews = 'weg_request_update_previews', + WegPinItem = 'weg_pin_item', + + // Windows Manager + SetWindowPosition = 'set_window_position', + RequestFocus = 'request_focus', + + // App Launcher + LauncherGetApps = 'launcher_get_apps', + + // Tray Icons + TempGetByEventTrayInfo = 'temp_get_by_event_tray_info', + OnClickTrayIcon = 'on_click_tray_icon', + OnContextMenuTrayIcon = 'on_context_menu_tray_icon', + + // Network + WlanGetProfiles = 'wlan_get_profiles', + WlanStartScanning = 'wlan_start_scanning', + WlanStopScanning = 'wlan_stop_scanning', + WlanConnect = 'wlan_connect', + WlanDisconnect = 'wlan_disconnect', + + // Notifications + NotificationsClose = 'notifications_close', + NotificationsCloseAll = 'notifications_close_all', +} \ No newline at end of file diff --git a/lib/src/handlers/mod.rs b/lib/src/handlers/mod.rs new file mode 100644 index 00000000..62f0c2a3 --- /dev/null +++ b/lib/src/handlers/mod.rs @@ -0,0 +1,67 @@ +pub struct SeelenEvent; + +#[allow(non_upper_case_globals)] +impl SeelenEvent { + pub const WorkspacesChanged: &str = "workspaces-changed"; + pub const ActiveWorkspaceChanged: &str = "active-workspace-changed"; + + pub const GlobalFocusChanged: &str = "global-focus-changed"; + pub const GlobalMouseMove: &str = "global-mouse-move"; + pub const GlobalMonitorsChanged: &str = "global-monitors-changed"; + + + pub const HandleLayeredHitboxes: &str = "handle-layered"; + + pub const MediaSessions: &str = "media-sessions"; + pub const MediaInputs: &str = "media-inputs"; + pub const MediaOutputs: &str = "media-outputs"; + + pub const NetworkDefaultLocalIp: &str = "network-default-local-ip"; + pub const NetworkAdapters: &str = "network-adapters"; + pub const NetworkInternetConnection: &str = "network-internet-connection"; + pub const NetworkWlanScanned: &str = "wlan-scanned"; + + pub const Notifications: &str = "notifications"; + + pub const PowerStatus: &str = "power-status"; + pub const BatteriesStatus: &str = "batteries-status"; + + pub const ColorsChanged: &str = "colors-changed"; + + pub const TrayInfo: &str = "tray-info"; + + pub const ToolbarOverlaped: &str = "set-auto-hide"; + + pub const WegOverlaped: &str = "set-auto-hide"; + pub const WegSetFocusedHandle: &str = "set-focused-handle"; + pub const WegSetFocusedExecutable: &str = "set-focused-executable"; + pub const WegUpdateOpenAppInfo: &str = "update-open-app-info"; + pub const WegAddOpenApp: &str = "add-open-app"; + pub const WegRemoveOpenApp: &str = "remove-open-app"; + + pub const WMSetReservation: &str = "set-reservation"; + pub const WMUpdateHeight: &str = "update-height"; + pub const WMUpdateWidth: &str = "update-width"; + pub const WMResetWorkspaceSize: &str = "reset-workspace-size"; + pub const WMFocus: &str = "focus"; + pub const WMSetActiveWorkspace: &str = "set-active-workspace"; + pub const WMAddWindow: &str = "add-window"; + pub const WMUpdateWindow: &str = "update-window"; + pub const WMRemoveWindow: &str = "remove-window"; + + pub const WMForceRetiling: &str = "wm-force-retiling"; + pub const WMSetLayout: &str = "wm-set-layout"; + pub const WMSetOverlayVisibility: &str = "wm-set-overlay-visibility"; + pub const WMSetActiveWindow: &str = "wm-set-active-window"; + + pub const WallStop: &str = "wall-stop"; + + pub const StateSettingsChanged: &str = "settings-changed"; + pub const StateWegItemsChanged: &str = "weg-items"; + pub const StateThemesChanged: &str = "themes"; + pub const StatePlaceholdersChanged: &str = "placeholders"; + pub const StateLayoutsChanged: &str = "layouts"; + pub const StateSettingsByAppChanged: &str = "settings-by-app"; + pub const StateHistoryChanged: &str = "history"; + pub const StateIconPacksChanged: &str = "icon-packs"; +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3534c741..e9457b08 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,2 +1,4 @@ pub mod rect; pub mod state; +pub mod system_state; +pub mod handlers; \ No newline at end of file diff --git a/lib/src/lib.ts b/lib/src/lib.ts new file mode 100644 index 00000000..b64962fd --- /dev/null +++ b/lib/src/lib.ts @@ -0,0 +1,4 @@ +export * from './state'; +export * from './system_state'; +export * from './utils'; +export * from './handlers'; \ No newline at end of file diff --git a/lib/src/main.rs b/lib/src/main.rs index 18aba74f..1bdee014 100644 --- a/lib/src/main.rs +++ b/lib/src/main.rs @@ -1,4 +1,6 @@ -use seelen_core::state::{AppConfig, Placeholder, Settings, Theme, WegItem, WindowManagerLayout}; +use seelen_core::state::{ + AppConfig, IconPack, Placeholder, Settings, Theme, WegItem, WindowManagerLayout, +}; fn write_schema(path: &str) where @@ -15,4 +17,5 @@ fn main() { write_schema::("./dist/layout.schema.json"); write_schema::>("./dist/settings_by_app.schema.json"); write_schema::>("./dist/weg_items.schema.json"); + write_schema::("./dist/icon_pack.schema.json"); } diff --git a/lib/src/state/icon_pack.rs b/lib/src/state/icon_pack.rs new file mode 100644 index 00000000..9ad1e2d4 --- /dev/null +++ b/lib/src/state/icon_pack.rs @@ -0,0 +1,16 @@ +use std::{collections::HashMap, path::PathBuf}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::ResourceMetadata; + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct IconPack { + pub info: ResourceMetadata, + /// Key can be user model id, filename or a full path. + /// + /// Value is the path to the icon relative to the icon pack folder. + pub apps: HashMap, +} diff --git a/lib/src/state/icon_pack.ts b/lib/src/state/icon_pack.ts new file mode 100644 index 00000000..583cae1b --- /dev/null +++ b/lib/src/state/icon_pack.ts @@ -0,0 +1,6 @@ +import { ResourceMetadata } from '.'; + +export class IconPack { + info: ResourceMetadata = new ResourceMetadata(); + apps: Record = {}; +} diff --git a/lib/src/state/index.ts b/lib/src/state/index.ts new file mode 100644 index 00000000..0c856cc3 --- /dev/null +++ b/lib/src/state/index.ts @@ -0,0 +1,26 @@ +import { Obtainable, SeelenCommand, SeelenEvent } from '../handlers'; + +export * from './theme'; +export * from './settings'; +export * from './weg_items'; +export * from './wm_layout'; +export * from './placeholder'; +export * from './settings_by_app'; +export * from './settings_by_monitor'; +export * from './icon_pack'; + +export interface LauncherHistory { + [x: string]: string[]; +} +export const LauncherHistory = Obtainable( + SeelenCommand.StateGetHistory, + SeelenEvent.StateHistoryChanged, +); + +export class ResourceMetadata { + displayName: string = 'Unknown'; + author: string = 'Unknown'; + description: string = ''; + filename: string = ''; + tags: string[] = []; +} \ No newline at end of file diff --git a/lib/src/state/mod.rs b/lib/src/state/mod.rs index 2369d282..95c58c89 100644 --- a/lib/src/state/mod.rs +++ b/lib/src/state/mod.rs @@ -1,13 +1,42 @@ -mod placeholder; -mod settings; -mod settings_by_app; -mod theme; -mod weg_items; -mod wm_layout; - -pub use placeholder::*; -pub use settings::*; -pub use settings_by_app::*; -pub use theme::*; -pub use weg_items::*; -pub use wm_layout::*; +mod icon_pack; +mod placeholder; +mod settings; +mod settings_by_app; +mod settings_by_monitor; +mod theme; +mod weg_items; +mod wm_layout; + +pub use icon_pack::*; +pub use placeholder::*; +pub use settings::*; +pub use settings_by_app::*; +pub use settings_by_monitor::*; +pub use theme::*; +pub use weg_items::*; +pub use wm_layout::*; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct ResourceMetadata { + pub display_name: String, + pub author: String, + pub description: String, + pub filename: String, + pub tags: Vec, +} + +impl Default for ResourceMetadata { + fn default() -> Self { + Self { + display_name: "Unknown".to_string(), + author: "Unknown".to_string(), + description: String::new(), + filename: String::new(), + tags: Vec::new(), + } + } +} diff --git a/lib/src/state/placeholder.rs b/lib/src/state/placeholder.rs index df4d84d2..77a40f24 100644 --- a/lib/src/state/placeholder.rs +++ b/lib/src/state/placeholder.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -17,7 +17,8 @@ macro_rules! common_item { #[serde(rename_all = "camelCase")] pub struct $name { /// Id to identify the item, should be unique. - id: Option, + #[serde(default)] + id: String, /// Content to display in the item. /// /// Should follow the [mathjs expression syntax](https://mathjs.org/docs/expressions/syntax.html). @@ -119,10 +120,10 @@ common_item! { /// const date: string; // the formatted date /// ``` struct DateToolbarItem { - /// Time unit to refresh the showing date + /// @deprecated -- v2 uses settings date format instead (it will perform the minimal updates) #[serde(default = "DateToolbarItem::default_interval")] each: DateUpdateInterval, - /// Format of the date, see [moment.js displaying format](https://momentjs.com/docs/#/displaying/format/) + /// @deprecated -- v2 uses settings date format instead #[serde(default = "DateToolbarItem::default_format")] format: String, } @@ -280,6 +281,40 @@ pub enum ToolbarItem { Workspaces(WorkspaceToolbarItem), } +impl ToolbarItem { + pub fn id(&self) -> String { + match self { + ToolbarItem::Text(item) => item.id.clone(), + ToolbarItem::Generic(item) => item.id.clone(), + ToolbarItem::Date(item) => item.id.clone(), + ToolbarItem::Power(item) => item.id.clone(), + ToolbarItem::Network(item) => item.id.clone(), + ToolbarItem::Media(item) => item.id.clone(), + ToolbarItem::Notifications(item) => item.id.clone(), + ToolbarItem::Tray(item) => item.id.clone(), + ToolbarItem::Device(item) => item.id.clone(), + ToolbarItem::Settings(item) => item.id.clone(), + ToolbarItem::Workspaces(item) => item.id.clone(), + } + } + + pub fn set_id(&mut self, id: String) { + match self { + ToolbarItem::Text(item) => item.id = id, + ToolbarItem::Generic(item) => item.id = id, + ToolbarItem::Date(item) => item.id = id, + ToolbarItem::Power(item) => item.id = id, + ToolbarItem::Network(item) => item.id = id, + ToolbarItem::Media(item) => item.id = id, + ToolbarItem::Notifications(item) => item.id = id, + ToolbarItem::Tray(item) => item.id = id, + ToolbarItem::Device(item) => item.id = id, + ToolbarItem::Settings(item) => item.id = id, + ToolbarItem::Workspaces(item) => item.id = id, + } + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase")] pub struct PlaceholderInfo { @@ -305,3 +340,26 @@ pub struct Placeholder { /// Items to be displayed in the toolbar pub right: Vec, } + +impl Placeholder { + fn sanitize_items(dict: &mut HashSet, items: Vec) -> Vec { + let mut result = Vec::new(); + for mut item in items { + if item.id().is_empty() { + item.set_id(uuid::Uuid::new_v4().to_string()); + } + if !dict.contains(&item.id()) { + dict.insert(item.id()); + result.push(item); + } + } + result + } + + pub fn sanitize(&mut self) { + let mut dict = HashSet::new(); + self.left = Self::sanitize_items(&mut dict, std::mem::take(&mut self.left)); + self.center = Self::sanitize_items(&mut dict, std::mem::take(&mut self.center)); + self.right = Self::sanitize_items(&mut dict, std::mem::take(&mut self.right)); + } +} diff --git a/lib/src/state/placeholder.ts b/lib/src/state/placeholder.ts new file mode 100644 index 00000000..63fb2561 --- /dev/null +++ b/lib/src/state/placeholder.ts @@ -0,0 +1,117 @@ +export enum ToolbarModuleType { + Generic = 'generic', + Text = 'text', + Date = 'date', + Power = 'power', + Settings = 'settings', + Network = 'network', + Workspaces = 'workspaces', + Media = 'media', + Tray = 'tray', + Device = 'device', + Notifications = 'notifications', +} + +export enum WorkspaceTMMode { + Dotted = 'dotted', + Named = 'named', + Numbered = 'numbered', +} + +export enum TimeUnit { + SECOND = 'second', + MINUTE = 'minute', + HOUR = 'hour', + DAY = 'day', +} + +export enum DeviceTMSubType { + Disk = 'disk', + CPU = 'cpu', + Memory = 'memory', +} + +export interface BaseToolbarModule { + id: string; + type: ToolbarModuleType; + template: string; + tooltip: string | null; + badge: string | null; + /** @deprecated, use `onClickV2` instead */ + onClick: string | null; + onClickV2: string | null; + style: Record; +} + +export interface GenericToolbarModule extends BaseToolbarModule { + type: ToolbarModuleType.Generic | ToolbarModuleType.Text; +} + +export interface TrayTM extends BaseToolbarModule { + type: ToolbarModuleType.Tray; +} + +export interface DateToolbarModule extends BaseToolbarModule { + type: ToolbarModuleType.Date; + /** @deprecated v2 uses settings date format instead (it will perform the minimal updates) */ + each: TimeUnit; + /** @deprecated v2 uses settings date format instead */ + format: string; +} + +export interface PowerToolbarModule extends BaseToolbarModule { + type: ToolbarModuleType.Power; +} + +export interface NetworkTM extends BaseToolbarModule { + type: ToolbarModuleType.Network; + withWlanSelector: boolean; +} + +export interface MediaTM extends BaseToolbarModule { + type: ToolbarModuleType.Media; + withMediaControls: boolean; +} + +export interface NotificationsTM extends BaseToolbarModule { + type: ToolbarModuleType.Notifications; +} + +export interface DeviceTM extends BaseToolbarModule { + type: ToolbarModuleType.Device; +} + +export interface SettingsToolbarModule extends BaseToolbarModule { + type: ToolbarModuleType.Settings; +} + +export interface WorkspacesTM extends BaseToolbarModule { + type: ToolbarModuleType.Workspaces; + mode: WorkspaceTMMode; +} + +export type ToolbarModule = + | GenericToolbarModule + | DateToolbarModule + | PowerToolbarModule + | SettingsToolbarModule + | WorkspacesTM + | TrayTM + | NetworkTM + | MediaTM + | DeviceTM + | NotificationsTM; + +export interface CreatorInfo { + displayName: string; + author: string; + description: string; + filename: string; +} + +export interface Placeholder { + info: CreatorInfo; + left: ToolbarModule[]; + center: ToolbarModule[]; + right: ToolbarModule[]; +} diff --git a/lib/src/state/settings.rs b/lib/src/state/settings.rs index eea8c0a7..f2907f8a 100644 --- a/lib/src/state/settings.rs +++ b/lib/src/state/settings.rs @@ -1,6 +1,9 @@ /* In this file we use #[serde_alias(SnakeCase)] as backward compatibility from versions below v1.9.8 */ -use std::collections::HashMap; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,70 +11,7 @@ use serde_alias::serde_alias; use crate::rect::Rect; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum VirtualDesktopStrategy { - Native, - Seelen, -} - -#[serde_alias(SnakeCase)] -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(default, rename_all = "camelCase")] -pub struct Settings { - /// fancy toolbar config - pub fancy_toolbar: FancyToolbarSettings, - /// seelenweg (dock/taskbar) config - pub seelenweg: SeelenWegSettings, - /// window manager config - pub window_manager: WindowManagerSettings, - /// list of monitors - pub monitors: Vec, - /// enable or disable ahk - pub ahk_enabled: bool, - /// ahk variables - pub ahk_variables: AhkVarList, - /// list of selected themes - pub selected_theme: Vec, - /// enable or disable dev tools tab in settings - pub dev_tools: bool, - /// language to use, if null the system locale is used - pub language: Option, - /// what virtual desktop implementation will be used, in case Native is not available we use Seelen - pub virtual_desktop_strategy: VirtualDesktopStrategy, - /// enable experimental/beta updates - pub beta_channel: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - ahk_enabled: true, - selected_theme: vec!["default".to_string()], - monitors: vec![Monitor::default()], - fancy_toolbar: FancyToolbarSettings::default(), - seelenweg: SeelenWegSettings::default(), - window_manager: WindowManagerSettings::default(), - ahk_variables: AhkVarList::default(), - dev_tools: false, - language: Some(Self::get_system_language()), - virtual_desktop_strategy: VirtualDesktopStrategy::Native, - beta_channel: false, - } - } -} - -impl Settings { - pub fn get_locale() -> Option { - sys_locale::get_locale() - } - - pub fn get_system_language() -> String { - match sys_locale::get_locale() { - Some(l) => l.split('-').next().unwrap_or("en").to_string(), - None => "en".to_string(), - } - } -} +use super::MonitorConfiguration; // ============== Fancy Toolbar Settings ============== @@ -172,6 +112,13 @@ impl Default for SeelenWegSettings { } } +impl SeelenWegSettings { + /// total height or width of the dock, depending on the Position + pub fn total_size(&self) -> u32 { + self.size + (self.padding * 2) + (self.margin * 2) + } +} + // ============== Window Manager Settings ============== #[serde_alias(SnakeCase)] @@ -202,13 +149,14 @@ pub struct WindowManagerSettings { /// window manager border pub border: Border, /// the resize size in % to be used when resizing via cli - pub resize_delta: f64, + pub resize_delta: f32, /// default gap between containers - pub workspace_gap: f64, + pub workspace_gap: u32, /// default workspace padding - pub workspace_padding: f64, + pub workspace_padding: u32, /// default workspace margin - pub global_work_area_offset: Rect, + #[serde(alias = "global_work_area_offset")] + pub workspace_margin: Rect, /// floating window settings pub floating: FloatingWindowSettings, /// default layout @@ -241,54 +189,112 @@ impl Default for WindowManagerSettings { auto_stacking_by_category: true, border: Border::default(), resize_delta: 10.0, - workspace_gap: 10.0, - workspace_padding: 10.0, - global_work_area_offset: Rect::default(), + workspace_gap: 10, + workspace_padding: 10, + workspace_margin: Rect::default(), floating: FloatingWindowSettings::default(), default_layout: String::from("default.yml"), } } } -// ============== Settings by Monitor ============== -#[serde_alias(SnakeCase)] +// ================= Seelen Launcher ================ + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum SeelenLauncherMonitor { + Primary, + #[serde(rename = "Mouse-Over")] + MouseOver, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase")] -pub struct Workspace { - pub name: String, - pub layout: String, - pub padding: Option, - pub gap: Option, +pub struct SeelenLauncherRunner { + pub id: String, + pub label: String, + pub program: String, + pub readonly: bool, } -#[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase")] -pub struct Monitor { - pub workspaces: Vec, - pub work_area_offset: Option, +pub struct SeelenLauncherSettings { + pub enabled: bool, + pub monitor: SeelenLauncherMonitor, + pub runners: Vec, } -impl Default for Workspace { +impl Default for SeelenLauncherSettings { fn default() -> Self { Self { - name: "New Workspace".to_string(), - layout: "BSP".to_string(), - padding: None, - gap: None, + enabled: true, + monitor: SeelenLauncherMonitor::MouseOver, + runners: vec![ + SeelenLauncherRunner { + id: "RUN".to_owned(), + label: "t:app_launcher.runners.explorer".to_owned(), + program: "explorer.exe".to_owned(), + readonly: true, + }, + SeelenLauncherRunner { + id: "CMD".to_owned(), + label: "t:app_launcher.runners.cmd".to_owned(), + program: "cmd.exe".to_owned(), + readonly: true, + }, + ], } } } -impl Default for Monitor { +impl SeelenLauncherSettings { + pub fn sanitize(&mut self) { + let mut dict = HashSet::new(); + self.runners + .retain(|runner| !runner.program.is_empty() && dict.insert(runner.program.clone())); + for runner in &mut self.runners { + if runner.id.is_empty() { + runner.id = uuid::Uuid::new_v4().to_string(); + } + } + } +} + +// ================= Seelen Wall ================ + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct SeelenWallWallpaper { + pub id: String, + pub path: PathBuf, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct SeelenWallSettings { + pub enabled: bool, + pub backgrounds: Vec, + /// update interval in seconds + pub interval: u64, +} + +impl Default for SeelenWallSettings { fn default() -> Self { Self { - workspaces: vec![Workspace::default()], - work_area_offset: None, + enabled: true, + backgrounds: vec![], + interval: 60, } } } +impl SeelenWallSettings { + pub fn sanitize(&mut self) { + self.backgrounds.retain(|b| b.path.exists()); + } +} + // ============== Ahk Variables ============== #[macro_export] @@ -436,3 +442,102 @@ impl Default for AhkVarList { } } } + +// ======================== Final Settings Struct =============================== + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum VirtualDesktopStrategy { + Native, + Seelen, +} + +#[serde_alias(SnakeCase)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct Settings { + /// fancy toolbar config + pub fancy_toolbar: FancyToolbarSettings, + /// seelenweg (dock/taskbar) config + pub seelenweg: SeelenWegSettings, + /// window manager config + pub window_manager: WindowManagerSettings, + /// background and virtual desktops config + pub wall: SeelenWallSettings, + /// App launcher settings + pub launcher: SeelenLauncherSettings, + /// list of monitors + pub monitors: Vec, + /// enable or disable ahk + pub ahk_enabled: bool, + /// ahk variables + pub ahk_variables: AhkVarList, + /// list of selected themes + #[serde(alias = "selected_theme")] + pub selected_themes: Vec, + /// list of selected icon packs + pub icon_packs: Vec, + /// enable or disable dev tools tab in settings + pub dev_tools: bool, + /// language to use, if null the system locale is used + pub language: Option, + /// MomentJS date format + pub date_format: String, + /// what virtual desktop implementation will be used, in case Native is not available we use Seelen + pub virtual_desktop_strategy: VirtualDesktopStrategy, + /// enable experimental/beta updates + pub beta_channel: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + ahk_enabled: true, + selected_themes: vec!["default".to_string()], + icon_packs: vec!["system".to_string()], + monitors: vec![MonitorConfiguration::default()], + fancy_toolbar: FancyToolbarSettings::default(), + seelenweg: SeelenWegSettings::default(), + window_manager: WindowManagerSettings::default(), + wall: SeelenWallSettings::default(), + launcher: SeelenLauncherSettings::default(), + ahk_variables: AhkVarList::default(), + dev_tools: false, + language: Some(Self::get_system_language()), + date_format: "ddd D MMM, hh:mm A".to_owned(), + virtual_desktop_strategy: VirtualDesktopStrategy::Native, + beta_channel: false, + } + } +} + +impl Settings { + pub fn get_locale() -> Option { + sys_locale::get_locale() + } + + pub fn get_system_language() -> String { + match sys_locale::get_locale() { + Some(l) => l.split('-').next().unwrap_or("en").to_string(), + None => "en".to_string(), + } + } + + pub fn sanitize(&mut self) { + self.launcher.sanitize(); + self.wall.sanitize(); + + if self.language.is_none() { + self.language = Some(Self::get_system_language()); + } + + let default_theme = "default".to_owned(); + if !self.selected_themes.contains(&default_theme) { + self.selected_themes.insert(0, default_theme); + } + + let default_icon_pack = "system".to_owned(); + if !self.icon_packs.contains(&default_icon_pack) { + self.icon_packs.insert(0, default_icon_pack); + } + } +} diff --git a/lib/src/state/settings.ts b/lib/src/state/settings.ts new file mode 100644 index 00000000..cfb5080e --- /dev/null +++ b/lib/src/state/settings.ts @@ -0,0 +1,178 @@ +import { Obtainable, SeelenCommand, SeelenEvent } from '../handlers'; +import { Rect } from '../utils'; +import { MonitorConfiguration } from './settings_by_monitor'; + +export enum VirtualDesktopStrategy { + Native = 'Native', + Seelen = 'Seelen', +} + +export enum SeelenWegMode { + FullWidth = 'Full-Width', + MinContent = 'Min-Content', +} + +export enum HideMode { + Never = 'Never', + Always = 'Always', + OnOverlap = 'On-Overlap', +} + +export enum SeelenWegSide { + Left = 'Left', + Right = 'Right', + Top = 'Top', + Bottom = 'Bottom', +} + +export class SeelenWallWallpaper { + id: string = crypto.randomUUID(); + path: string = ''; +} + +export class SeelenWallSettings { + enabled: boolean = true; + backgrounds: SeelenWallWallpaper[] = []; + /** Interval in seconds */ + interval: number = 60; +} + +export enum SeelenLauncherMonitor { + Primary = 'Primary', + MouseOver = 'Mouse-Over', +} + +export class SeelenLauncherRunner { + id: string = crypto.randomUUID(); + label: string = ''; + program: string = ''; + readonly: boolean = false; +} + +export class SeelenLauncherSettings { + enabled: boolean = true; + monitor: SeelenLauncherMonitor = SeelenLauncherMonitor.MouseOver; + runners: SeelenLauncherRunner[] = []; +} + +export class Settings extends Obtainable( + SeelenCommand.StateGetSettings, + SeelenEvent.StateSettingsChanged, +) { + fancyToolbar: FancyToolbarSettings = new FancyToolbarSettings(); + seelenweg: SeelenWegSettings = new SeelenWegSettings(); + windowManager: WindowManagerSettings = new WindowManagerSettings(); + wall: SeelenWallSettings = new SeelenWallSettings(); + launcher: SeelenLauncherSettings = new SeelenLauncherSettings(); + monitors: MonitorConfiguration[] = [new MonitorConfiguration()]; + ahkEnabled: boolean = true; + ahkVariables: AhkVarList = new AhkVarList(); + selectedThemes: string[] = ['default']; + iconPacks: string[] = ['system']; + devTools: boolean = false; + language: string = ''; + dateFormat: string = 'ddd D MMM, hh:mm A'; + virtualDesktopStrategy: VirtualDesktopStrategy = VirtualDesktopStrategy.Native; + betaChannel: boolean = false; +} + +export class FancyToolbarSettings { + enabled: boolean = true; + height: number = 30; + placeholder: string = 'default.yml'; + hideMode: HideMode = HideMode.Never; +} + +export class SeelenWegSettings { + enabled: boolean = true; + mode: SeelenWegMode = SeelenWegMode.MinContent; + hideMode: HideMode = HideMode.OnOverlap; + position: SeelenWegSide = SeelenWegSide.Bottom; + visibleSeparators: boolean = true; + size: number = 40; + zoomSize: number = 70; + margin: number = 8; + padding: number = 8; + spaceBetweenItems: number = 8; +} + +export class Border { + enabled: boolean = true; + width: number = 3.0; + offset: number = 0.0; +} + +export class FloatingWindowSettings { + width: number = 800.0; + height: number = 500.0; +} + +export class WindowManagerSettings { + enabled: boolean = false; + autoStackingByCategory: boolean = true; + border: Border = new Border(); + resizeDelta: number = 10.0; + workspaceGap: number = 10.0; + workspacePadding: number = 10.0; + workspaceMargin: Rect = new Rect(); + floating: FloatingWindowSettings = new FloatingWindowSettings(); + defaultLayout: string = 'default.yml'; +} + +export class AhkVar { + fancy: string; + ahk: string; + constructor(fancy: string = '', ahk: string = '') { + this.fancy = fancy; + this.ahk = ahk; + } +} + +export class AhkVarList { + reserveTop = new AhkVar('Win + Shift + I', '#+i'); + reserveBottom = new AhkVar('Win + Shift + K', '#+k'); + reserveLeft = new AhkVar('Win + Shift + J', '#+j'); + reserveRight = new AhkVar('Win + Shift + L', '#+l'); + reserveFloat = new AhkVar('Win + Shift + U', '#+u'); + reserveStack = new AhkVar('Win + Shift + O', '#+o'); + focusTop = new AhkVar('Win + Shift + W', '#+w'); + focusBottom = new AhkVar('Win + Shift + S', '#+s'); + focusLeft = new AhkVar('Win + Shift + A', '#+a'); + focusRight = new AhkVar('Win + Shift + D', '#+d'); + focusLatest = new AhkVar('Win + Shift + E', '#+e'); + increaseWidth = new AhkVar('Win + Alt + =', '#!='); + decreaseWidth = new AhkVar('Win + Alt + -', '#!-'); + increaseHeight = new AhkVar('Win + Shift + =', '#+='); + decreaseHeight = new AhkVar('Win + Shift + -', '#+-'); + restoreSizes = new AhkVar('Win + Alt + 0', '#!0'); + switchWorkspace0 = new AhkVar('Alt + 1', '!1'); + switchWorkspace1 = new AhkVar('Alt + 2', '!2'); + switchWorkspace2 = new AhkVar('Alt + 3', '!3'); + switchWorkspace3 = new AhkVar('Alt + 4', '!4'); + switchWorkspace4 = new AhkVar('Alt + 5', '!5'); + switchWorkspace5 = new AhkVar('Alt + 6', '!6'); + switchWorkspace6 = new AhkVar('Alt + 7', '!7'); + switchWorkspace7 = new AhkVar('Alt + 8', '!8'); + switchWorkspace8 = new AhkVar('Alt + 9', '!9'); + switchWorkspace9 = new AhkVar('Alt + 0', '!0'); + moveToWorkspace0 = new AhkVar('Alt + Shift + 1', '!+1'); + moveToWorkspace1 = new AhkVar('Alt + Shift + 2', '!+2'); + moveToWorkspace2 = new AhkVar('Alt + Shift + 3', '!+3'); + moveToWorkspace3 = new AhkVar('Alt + Shift + 4', '!+4'); + moveToWorkspace4 = new AhkVar('Alt + Shift + 5', '!+5'); + moveToWorkspace5 = new AhkVar('Alt + Shift + 6', '!+6'); + moveToWorkspace6 = new AhkVar('Alt + Shift + 7', '!+7'); + moveToWorkspace7 = new AhkVar('Alt + Shift + 8', '!+8'); + moveToWorkspace8 = new AhkVar('Alt + Shift + 9', '!+9'); + moveToWorkspace9 = new AhkVar('Alt + Shift + 0', '!+0'); + sendToWorkspace0 = new AhkVar('Win + Shift + 1', '#+1'); + sendToWorkspace1 = new AhkVar('Win + Shift + 2', '#+2'); + sendToWorkspace2 = new AhkVar('Win + Shift + 3', '#+3'); + sendToWorkspace3 = new AhkVar('Win + Shift + 4', '#+4'); + sendToWorkspace4 = new AhkVar('Win + Shift + 5', '#+5'); + sendToWorkspace5 = new AhkVar('Win + Shift + 6', '#+6'); + sendToWorkspace6 = new AhkVar('Win + Shift + 7', '#+7'); + sendToWorkspace7 = new AhkVar('Win + Shift + 8', '#+8'); + sendToWorkspace8 = new AhkVar('Win + Shift + 9', '#+9'); + sendToWorkspace9 = new AhkVar('Win + Shift + 0', '#+0'); +} diff --git a/lib/src/state/settings_by_app.rs b/lib/src/state/settings_by_app.rs index 66687818..5980031b 100644 --- a/lib/src/state/settings_by_app.rs +++ b/lib/src/state/settings_by_app.rs @@ -32,7 +32,7 @@ pub enum AppIdentifierType { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub enum MatchingStrategy { - #[serde(alias = "equals")] + #[serde(alias = "equals", alias = "legacy", alias = "Legacy")] Equals, #[serde(alias = "startsWith")] StartsWith, @@ -42,9 +42,6 @@ pub enum MatchingStrategy { Contains, #[serde(alias = "regex")] Regex, - // only for backwards compatibility - #[serde(alias = "legacy")] - Legacy, } #[serde_alias(SnakeCase)] @@ -60,7 +57,6 @@ pub struct AppIdentifier { pub and: Vec, #[serde(default)] pub or: Vec, - // cache #[serde(skip)] pub regex: Option, } @@ -77,7 +73,7 @@ impl AppIdentifier { pub fn validate(&self, title: &str, class: &str, exe: &str, path: &str) -> bool { let mut self_result = match self.matching_strategy { - MatchingStrategy::Legacy | MatchingStrategy::Equals => match self.kind { + MatchingStrategy::Equals => match self.kind { AppIdentifierType::Title => title.eq(&self.id), AppIdentifierType::Class => class.eq(&self.id), AppIdentifierType::Exe => exe.eq(&self.id), @@ -138,8 +134,8 @@ pub struct AppConfig { pub category: Option, /// monitor index that the app should be bound to pub bound_monitor: Option, - /// workspace name that the app should be bound to - pub bound_workspace: Option, + /// workspace index that the app should be bound to + pub bound_workspace: Option, /// app identifier pub identifier: AppIdentifier, /// extra specific options/settings for the app diff --git a/lib/src/state/settings_by_app.ts b/lib/src/state/settings_by_app.ts new file mode 100644 index 00000000..111a1226 --- /dev/null +++ b/lib/src/state/settings_by_app.ts @@ -0,0 +1,68 @@ +export enum AppExtraFlag { + Float = 'float', + Force = 'force', + Unmanage = 'unmanage', + Pinned = 'pinned', + Hidden = 'hidden', +} + +export enum AppIdentifierType { + Exe = 'Exe', + Class = 'Class', + Title = 'Title', + Path = 'Path', +} + +export enum MatchingStrategy { + Equals = 'Equals', + StartsWith = 'StartsWith', + EndsWith = 'EndsWith', + Contains = 'Contains', + Regex = 'Regex', +} + +export interface AppIdentifier { + id: string; + kind: AppIdentifierType; + matchingStrategy: MatchingStrategy; + negation: boolean; + and: AppIdentifier[]; + or: AppIdentifier[]; +} + +export class AppIdentifier { + static placeholder(): AppIdentifier { + return { + id: 'new-app.exe', + kind: AppIdentifierType.Exe, + matchingStrategy: MatchingStrategy.Equals, + negation: false, + and: [], + or: [], + }; + } +} + +export interface AppConfiguration { + name: string; + category: string | null; + boundMonitor: number | null; + boundWorkspace: number | null; + identifier: AppIdentifier; + options: Array; + isBundled: boolean; +} + +export class AppConfiguration { + static placeholder(): AppConfiguration { + return { + name: 'New App', + category: null, + boundWorkspace: null, + boundMonitor: null, + identifier: AppIdentifier.placeholder(), + isBundled: false, + options: [], + }; + } +} diff --git a/lib/src/state/settings_by_monitor.rs b/lib/src/state/settings_by_monitor.rs new file mode 100644 index 00000000..4f100667 --- /dev/null +++ b/lib/src/state/settings_by_monitor.rs @@ -0,0 +1,102 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::rect::Rect; + +use super::SeelenWallWallpaper; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct FancyToolbarSettingsByMonitor { + pub enabled: bool, +} + +impl Default for FancyToolbarSettingsByMonitor { + fn default() -> Self { + Self { enabled: true } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct SeelenWegSettingsByMonitor { + pub enabled: bool, +} + +impl Default for SeelenWegSettingsByMonitor { + fn default() -> Self { + Self { enabled: true } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct WindowManagerSettingsByMonitor { + pub enabled: bool, + pub padding: Option, + pub margin: Option, + pub gap: Option, + pub layout: Option, +} + +impl Default for WindowManagerSettingsByMonitor { + fn default() -> Self { + Self { + enabled: true, + padding: None, + margin: None, + gap: None, + layout: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct SeelenWallSettingsByMonitor { + pub enabled: bool, + pub backgrounds: Option>, +} + +impl Default for SeelenWallSettingsByMonitor { + fn default() -> Self { + Self { + enabled: true, + backgrounds: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub enum WorkspaceIdentifierType { + #[serde(alias = "name")] + Name, + #[serde(alias = "index")] + Index, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceIdentifier { + pub id: String, + pub kind: WorkspaceIdentifierType, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceConfiguration { + pub identifier: WorkspaceIdentifier, + pub layout: Option, + pub backgrounds: Option>, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct MonitorConfiguration { + pub tb: FancyToolbarSettingsByMonitor, + pub weg: SeelenWegSettingsByMonitor, + pub wm: WindowManagerSettingsByMonitor, + pub wall: SeelenWallSettingsByMonitor, + /// list of settings by workspace on this monitor + pub workspaces_v2: Vec, +} diff --git a/lib/src/state/settings_by_monitor.ts b/lib/src/state/settings_by_monitor.ts new file mode 100644 index 00000000..3df6601c --- /dev/null +++ b/lib/src/state/settings_by_monitor.ts @@ -0,0 +1,57 @@ +import { Rect } from '../utils'; +import { SeelenWallWallpaper } from './settings'; + +export class FancyToolbarSettingsByMonitor { + enabled: boolean = true; +} + +export class SeelenWegSettingsByMonitor { + enabled: boolean = true; +} + +export class WindowManagerSettingsByMonitor { + enabled: boolean = true; + padding: number | null = null; + margin: Rect | null = null; + gap: number | null = null; + layout: string | null = null; +} + +export class SeelenWallSettingsByMonitor { + enabled: boolean = true; + backgrounds: SeelenWallWallpaper[] | null = null; +} + +export enum WorkspaceIdentifierType { + Name = 'name', + Index = 'index', +} + +export class WorkspaceIdentifier { + id: string; + kind: WorkspaceIdentifierType; + + constructor(id: string, kind: WorkspaceIdentifierType) { + this.id = id; + this.kind = kind; + } +} + +export class WorkspaceConfiguration { + identifier: WorkspaceIdentifier; + layout: string | null = null; + backgrounds: SeelenWallWallpaper[] | null = null; + + constructor(identifier: WorkspaceIdentifier) { + this.identifier = identifier; + } +} + +export class MonitorConfiguration { + tb: FancyToolbarSettingsByMonitor = new FancyToolbarSettingsByMonitor(); + wall: SeelenWallSettingsByMonitor = new SeelenWallSettingsByMonitor(); + weg: SeelenWegSettingsByMonitor = new SeelenWegSettingsByMonitor(); + wm: WindowManagerSettingsByMonitor = new WindowManagerSettingsByMonitor(); + /** list of settings by workspace on this monitor */ + workspacesV2: WorkspaceConfiguration[] = []; +} diff --git a/lib/src/state/theme.rs b/lib/src/state/theme.rs index e41f2c98..1dfc270c 100644 --- a/lib/src/state/theme.rs +++ b/lib/src/state/theme.rs @@ -1,37 +1,41 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(default, rename_all = "camelCase")] -pub struct ThemeCss { - /// Css Styles for the dock/taskbar - pub weg: String, - /// Css Styles for the window manager - pub toolbar: String, - /// Css Styles for the window manager - pub wm: String, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(default, rename_all = "camelCase")] -pub struct ThemeInfo { - /// Display name of the theme - pub display_name: String, - /// Author of the theme - pub author: String, - /// Description of the theme - pub description: String, - /// Filename of the theme, is overridden by the program on load. - pub filename: String, - /// Tags to be used in search - pub tags: Vec, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(default, rename_all = "camelCase")] -pub struct Theme { - /// Metadata about the theme - pub info: ThemeInfo, - /// Css Styles of the theme - pub styles: ThemeCss, -} +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct ThemeCss { + /// Css Styles for the dock/taskbar + pub weg: String, + /// Css Styles for the toolbar + pub toolbar: String, + /// Css Styles for the window manager + pub wm: String, + /// Css Styles for the app launcher + pub launcher: String, + /// Css Styles for the wall + pub wall: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct ThemeInfo { + /// Display name of the theme + pub display_name: String, + /// Author of the theme + pub author: String, + /// Description of the theme + pub description: String, + /// Filename of the theme, is overridden by the program on load. + pub filename: String, + /// Tags to be used in search + pub tags: Vec, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct Theme { + /// Metadata about the theme + pub info: ThemeInfo, + /// Css Styles of the theme + pub styles: ThemeCss, +} diff --git a/lib/src/state/theme.ts b/lib/src/state/theme.ts new file mode 100644 index 00000000..a6072b96 --- /dev/null +++ b/lib/src/state/theme.ts @@ -0,0 +1,32 @@ +export interface ThemeCssByApp { + /** Css Styles for the dock/taskbar */ + weg: string; + /** Css Styles for the window manager */ + toolbar: string; + /** Css Styles for the window manager */ + wm: string; + /** Css Styles for the app launcher */ + launcher: string; + /** Css Styles for the wall */ + wall: string; +} + +export interface ThemeInfo { + /** Display name of the theme */ + displayName: string; + /** Author of the theme */ + author: string; + /** Description of the theme */ + description: string; + /** Filename of the theme, is overridden by the program on load */ + filename: string; + /** Tags to be used in search */ + tags: string[]; +} + +export interface Theme { + /** Metadata about the theme */ + info: ThemeInfo; + /** Css Styles of the theme */ + styles: ThemeCssByApp; +} diff --git a/lib/src/state/weg_items.rs b/lib/src/state/weg_items.rs index a68137f8..7c48ce8b 100644 --- a/lib/src/state/weg_items.rs +++ b/lib/src/state/weg_items.rs @@ -3,28 +3,26 @@ use std::{collections::HashSet, path::PathBuf}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct PinnedWegItem { - /// executable path - exe: String, - /// command to open the app using explorer.exe (uwp apps starts with `shell:AppsFolder`) - execution_path: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct TemporalPinnedWegItem { - /// executable path - exe: String, - /// command to open the app using explorer.exe (uwp apps starts with `shell:AppsFolder`) - execution_path: String, -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type")] pub enum WegItem { - PinnedApp(PinnedWegItem), - TemporalPin(TemporalPinnedWegItem), - Separator { id: String }, + Pinned { + path: PathBuf, + is_dir: bool, + }, + PinnedApp { + exe: PathBuf, + /// command to open the app using explorer.exe (UWP apps start with `shell:AppsFolder`) + execution_path: String, + }, + TemporalPin { + exe: PathBuf, + /// command to open the app using explorer.exe (UWP apps start with `shell:AppsFolder`) + execution_path: String, + }, + Separator { + id: String, + }, Media, StartMenu, } @@ -32,43 +30,56 @@ pub enum WegItem { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(default)] pub struct WegItems { - left: Vec, - center: Vec, - right: Vec, + pub left: Vec, + pub center: Vec, + pub right: Vec, } impl Default for WegItems { fn default() -> Self { Self { left: vec![WegItem::StartMenu], - center: vec![WegItem::PinnedApp(PinnedWegItem { - exe: "C:\\Windows\\explorer.exe".to_string(), - execution_path: "C:\\Windows\\explorer.exe".to_string(), - })], + center: vec![WegItem::PinnedApp { + exe: "C:\\Windows\\explorer.exe".into(), + execution_path: "C:\\Windows\\explorer.exe".into(), + }], right: vec![WegItem::Media], } } } impl WegItems { - fn clean_items(dict: &mut HashSet, items: Vec) -> Vec { + fn sanitize_items(dict: &mut HashSet, items: Vec) -> Vec { let mut result = Vec::new(); for item in items { match &item { - WegItem::PinnedApp(app) => { - if !dict.contains(&app.exe) { - dict.insert(app.exe.clone()); + WegItem::Pinned { path, is_dir: _ } => { + let file = path.to_string_lossy().to_string(); + if !dict.contains(&file) { + dict.insert(file); + result.push(item); + } + } + WegItem::PinnedApp { + exe, + execution_path, + } => { + if !dict.contains(execution_path) { + dict.insert(execution_path.clone()); // remove apps that don't exist - if PathBuf::from(&app.exe).exists() { + if PathBuf::from(exe).exists() { result.push(item); } } } - WegItem::TemporalPin(app) => { - if !dict.contains(&app.exe) { - dict.insert(app.exe.clone()); + WegItem::TemporalPin { + exe, + execution_path, + } => { + if !dict.contains(execution_path) { + dict.insert(execution_path.clone()); // remove apps that don't exist - if PathBuf::from(&app.exe).exists() { + if PathBuf::from(exe).exists() { result.push(item); } } @@ -96,10 +107,10 @@ impl WegItems { result } - pub fn clean_all_items(&mut self) { + pub fn sanitize(&mut self) { let mut dict = HashSet::new(); - self.left = Self::clean_items(&mut dict, std::mem::take(&mut self.left)); - self.center = Self::clean_items(&mut dict, std::mem::take(&mut self.center)); - self.right = Self::clean_items(&mut dict, std::mem::take(&mut self.right)); + self.left = Self::sanitize_items(&mut dict, std::mem::take(&mut self.left)); + self.center = Self::sanitize_items(&mut dict, std::mem::take(&mut self.center)); + self.right = Self::sanitize_items(&mut dict, std::mem::take(&mut self.right)); } } diff --git a/lib/src/state/weg_items.ts b/lib/src/state/weg_items.ts new file mode 100644 index 00000000..ab2d4101 --- /dev/null +++ b/lib/src/state/weg_items.ts @@ -0,0 +1,57 @@ +export enum SwItemType { + PinnedApp = 'PinnedApp', + Pinned = 'Pinned', + TemporalApp = 'TemporalPin', + Separator = 'Separator', + Media = 'Media', + Start = 'StartMenu', +} + +export interface PinnedWegItem { + type: SwItemType.Pinned; + path: string; + is_dir: boolean; +}; + +export interface PinnedAppWegItem { + type: SwItemType.PinnedApp; + /** executable path */ + exe: string; + /** command to open the app using explorer.exe (UWP apps start with `shell:AppsFolder`) */ + execution_path: string; +} + +export interface TemporalPinnedWegItem { + type: SwItemType.TemporalApp; + /** executable path */ + exe: string; + /** command to open the app using explorer.exe (UWP apps start with `shell:AppsFolder`) */ + execution_path: string; +} + +export interface SeparatorWegItem { + type: SwItemType.Separator; + id: string; +} + +export interface MediaWegItem { + type: SwItemType.Media; +} + +export interface StartWegItem { + type: SwItemType.Start; +} + +export type WegItem = + | PinnedWegItem + | PinnedAppWegItem + | TemporalPinnedWegItem + | SeparatorWegItem + | MediaWegItem + | StartWegItem; + +export interface WegItems { + left: WegItem[]; + center: WegItem[]; + right: WegItem[]; +} diff --git a/lib/src/state/wm_layout.rs b/lib/src/state/wm_layout.rs index 2f346345..e4665563 100644 --- a/lib/src/state/wm_layout.rs +++ b/lib/src/state/wm_layout.rs @@ -1,133 +1,201 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -macro_rules! common_item { - ( - $( - struct $name:ident { - $($rest:tt)* - } - )* - ) => { - $( - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] - #[serde(rename_all = "camelCase")] - pub struct $name { - #[serde(default = "WmNode::default_subtype")] - subtype: NodeSubtype, - /// Order in how the tree will be traversed (1 = first, 2 = second, etc.) - #[serde(default = "WmNode::default_priority")] - priority: u32, - /// How much of the remaining space this node will take - #[serde(default = "WmNode::default_grow_factor")] - grow_factor: f64, - /// Math Condition for the node to be shown, e.g: n >= 3 - condition: Option, - $($rest)* - } - )* - }; -} - -common_item! { - struct WmVerticalNode { - #[serde(default)] - children: Vec, - } - struct WmHorizontalNode { - #[serde(default)] - children: Vec, - } - struct WmLeafNode { - /// window handle (HWND) in the node - handle: Option, - } - struct WmStackNode { - /// active window handle (HWND) in the node - active: Option, - /// window handles (HWND) in the node - #[serde(default)] - handles: Vec, - } - struct WmFallbackNode { - /// active window handle (HWND) in the node - active: Option, - /// window handles (HWND) in the node - #[serde(default)] - handles: Vec, - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type")] -pub enum WmNode { - Vertical(WmVerticalNode), - Horizontal(WmHorizontalNode), - Leaf(WmLeafNode), - Stack(WmStackNode), - Fallback(WmFallbackNode), -} - -impl WmNode { - fn default_subtype() -> NodeSubtype { - NodeSubtype::Permanent - } - - fn default_priority() -> u32 { - 1 - } - - fn default_grow_factor() -> f64 { - 1f64 - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(default, rename_all = "camelCase")] -pub struct WManagerLayoutInfo { - /// Display name of the layout - pub display_name: String, - /// Author of the layout - pub author: String, - /// Description of the layout - pub description: String, - /// Filename of the layout, is overridden by the program on load. - pub filename: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub enum NodeSubtype { - Temporal, - Permanent, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub enum NoFallbackBehavior { - Float, - Unmanaged, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(default, rename_all = "camelCase")] -pub struct WindowManagerLayout { - pub info: WManagerLayoutInfo, - pub structure: WmNode, - pub no_fallback_behavior: NoFallbackBehavior, -} - -impl Default for WindowManagerLayout { - fn default() -> Self { - Self { - info: Default::default(), - structure: WmNode::Fallback(WmFallbackNode { - subtype: WmNode::default_subtype(), - priority: WmNode::default_priority(), - grow_factor: WmNode::default_grow_factor(), - condition: None, - active: None, - handles: vec![], - }), - no_fallback_behavior: NoFallbackBehavior::Float, - } - } -} +use std::cell::Cell; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +macro_rules! common_item { + ( + $( + struct $name:ident { + $($rest:tt)* + } + )* + ) => { + $( + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct $name { + #[serde(default = "WmNode::default_subtype")] + pub subtype: NodeSubtype, + /// Order in how the tree will be traversed (1 = first, 2 = second, etc.) + #[serde(default = "WmNode::default_priority")] + pub priority: u32, + /// How much of the remaining space this node will take + #[serde(default = "WmNode::default_grow_factor")] + pub grow_factor: Cell, + /// Math Condition for the node to be shown, e.g: n >= 3 + pub condition: Option, + $($rest)* + } + )* + }; +} + +common_item! { + struct WmVerticalNode { + pub children: Vec, + } + struct WmHorizontalNode { + pub children: Vec, + } + struct WmLeafNode { + /// window handle (HWND) in the node + pub handle: Option, + } + struct WmStackNode { + /// active window handle (HWND) in the node + #[serde(skip_deserializing)] + pub active: Option, + /// window handles (HWND) in the node + #[serde(skip_deserializing)] + pub handles: Vec, + } + struct WmFallbackNode { + /// active window handle (HWND) in the node + #[serde(skip_deserializing)] + pub active: Option, + /// window handles (HWND) in the node + #[serde(skip_deserializing)] + pub handles: Vec, + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type")] +pub enum WmNode { + Vertical(WmVerticalNode), + Horizontal(WmHorizontalNode), + Leaf(WmLeafNode), + Stack(WmStackNode), + Fallback(WmFallbackNode), +} + +fn format_children(children: &[WmNode]) -> String { + let mut result = Vec::new(); + for child in children { + result.push(child.to_string()); + } + result.join(", ") +} + +impl std::fmt::Display for WmNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WmNode::Vertical(node) => write!(f, "Vertical [{}]", format_children(&node.children)), + WmNode::Horizontal(node) => { + write!(f, "Horizontal [{}]", format_children(&node.children)) + } + WmNode::Leaf(node) => write!(f, "Leaf({:?})", node.handle), + WmNode::Stack(node) => write!(f, "Stack({:?})", node.handles), + WmNode::Fallback(node) => write!(f, "Fallback({:?})", node.handles), + } + } +} + +impl WmNode { + fn default_subtype() -> NodeSubtype { + NodeSubtype::Permanent + } + + fn default_priority() -> u32 { + 1 + } + + fn default_grow_factor() -> Cell { + Cell::new(1.0) + } + + pub fn priority(&self) -> u32 { + match self { + WmNode::Leaf(n) => n.priority, + WmNode::Stack(n) => n.priority, + WmNode::Fallback(n) => n.priority, + WmNode::Vertical(n) => n.priority, + WmNode::Horizontal(n) => n.priority, + } + } + + pub fn grow_factor(&self) -> &Cell { + match self { + WmNode::Leaf(n) => &n.grow_factor, + WmNode::Stack(n) => &n.grow_factor, + WmNode::Fallback(n) => &n.grow_factor, + WmNode::Vertical(n) => &n.grow_factor, + WmNode::Horizontal(n) => &n.grow_factor, + } + } + + pub fn condition(&self) -> Option<&String> { + match self { + WmNode::Leaf(n) => n.condition.as_ref(), + WmNode::Stack(n) => n.condition.as_ref(), + WmNode::Fallback(n) => n.condition.as_ref(), + WmNode::Vertical(n) => n.condition.as_ref(), + WmNode::Horizontal(n) => n.condition.as_ref(), + } + } + + pub fn len(&self) -> usize { + match self { + WmNode::Leaf(n) => n.handle.is_some() as usize, + WmNode::Stack(n) => n.handles.len(), + WmNode::Fallback(n) => n.handles.len(), + WmNode::Vertical(n) => n.children.iter().map(Self::len).sum(), + WmNode::Horizontal(n) => n.children.iter().map(Self::len).sum(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct WManagerLayoutInfo { + /// Display name of the layout + pub display_name: String, + /// Author of the layout + pub author: String, + /// Description of the layout + pub description: String, + /// Filename of the layout, is overridden by the program on load. + pub filename: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub enum NodeSubtype { + Temporal, + Permanent, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub enum NoFallbackBehavior { + Float, + Unmanaged, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct WindowManagerLayout { + pub info: WManagerLayoutInfo, + pub structure: WmNode, + pub no_fallback_behavior: NoFallbackBehavior, +} + +impl Default for WindowManagerLayout { + fn default() -> Self { + Self { + info: Default::default(), + structure: WmNode::Fallback(WmFallbackNode { + subtype: WmNode::default_subtype(), + priority: WmNode::default_priority(), + grow_factor: WmNode::default_grow_factor(), + condition: None, + active: None, + handles: vec![], + }), + no_fallback_behavior: NoFallbackBehavior::Float, + } + } +} diff --git a/lib/src/state/wm_layout.ts b/lib/src/state/wm_layout.ts new file mode 100644 index 00000000..5bc09b18 --- /dev/null +++ b/lib/src/state/wm_layout.ts @@ -0,0 +1,66 @@ +export enum NodeType { + Vertical = 'Vertical', + Horizontal = 'Horizontal', + Leaf = 'Leaf', + Stack = 'Stack', + Fallback = 'Fallback', +} + +export enum NodeSubtype { + Temporal = 'Temporal', + Permanent = 'Permanent', +} + +export enum NoFallbackBehavior { + Float = 'Float', + Unmanaged = 'Unmanaged', +} + +export interface WManagerLayoutInfo { + displayName: string; + author: string; + description: string; + filename: string; +} + +export interface WmNodeBase { + subtype: NodeSubtype; + priority: number; + growFactor: number; + condition: string | null; +} + +export interface WmVerticalNode extends WmNodeBase { + type: NodeType.Vertical; + children: WmNode[]; +} + +export interface WmHorizontalNode extends WmNodeBase { + type: NodeType.Horizontal; + children: WmNode[]; +} + +export interface WmLeafNode extends WmNodeBase { + type: NodeType.Leaf; + handle: number | null; +} + +export interface WmStackNode extends WmNodeBase { + type: NodeType.Stack; + active: number | null; + handles: number[]; +} + +export interface WmFallbackNode extends WmNodeBase { + type: NodeType.Fallback; + active: number | null; + handles: number[]; +} + +export type WmNode = WmVerticalNode | WmHorizontalNode | WmLeafNode | WmStackNode | WmFallbackNode; + +export interface WindowManagerLayout { + info: WManagerLayoutInfo; + structure: WmNode; + noFallbackBehavior: NoFallbackBehavior; +} diff --git a/lib/src/system_state/index.ts b/lib/src/system_state/index.ts new file mode 100644 index 00000000..d00ece2e --- /dev/null +++ b/lib/src/system_state/index.ts @@ -0,0 +1,51 @@ +import { Obtainable, SeelenCommand, SeelenEvent } from '../handlers'; + +export interface UIColors { + background: string; + foreground: string; + accent_darkest: string; + accent_darker: string; + accent_dark: string; + accent: string; + accent_light: string; + accent_lighter: string; + accent_lightest: string; + complement: string | null; +} + +export class UIColors extends Obtainable( + SeelenCommand.GetSystemColors, + SeelenEvent.ColorsChanged, +) { + static default(): UIColors { + return { + background: '#ffffff', + foreground: '#000000', + accent_darkest: '#990000', + accent_darker: '#aa0000', + accent_dark: '#bb0000', + accent: '#cc0000', + accent_light: '#dd0000', + accent_lighter: '#ee0000', + accent_lightest: '#ff0000', + complement: null, + }; + } + + static setAssCssVariables(colors: UIColors) { + for (const [key, value] of Object.entries(colors)) { + if (typeof value !== 'string') { + continue; + } + let hex = value.replace('#', '').slice(0, 6); + var color = parseInt(hex, 16); + var r = (color >> 16) & 255; + var g = (color >> 8) & 255; + var b = color & 255; + // replace rust snake case with kebab case + let name = key.replace('_', '-'); + document.documentElement.style.setProperty(`--config-${name}-color`, value.slice(0, 7)); + document.documentElement.style.setProperty(`--config-${name}-color-rgb`, `${r}, ${g}, ${b}`); + } + } +} diff --git a/lib/src/system_state/mod.rs b/lib/src/system_state/mod.rs new file mode 100644 index 00000000..27b92e5c --- /dev/null +++ b/lib/src/system_state/mod.rs @@ -0,0 +1,16 @@ +use serde::Serialize; + +/// https://learn.microsoft.com/is-is/uwp/api/windows.ui.viewmanagement.uicolortype?view=winrt-19041 +#[derive(Debug, Default, Serialize)] +pub struct UIColors { + pub background: String, + pub foreground: String, + pub accent_darkest: String, + pub accent_darker: String, + pub accent_dark: String, + pub accent: String, + pub accent_light: String, + pub accent_lighter: String, + pub accent_lightest: String, + pub complement: Option, +} diff --git a/lib/src/utils/hooks.ts b/lib/src/utils/hooks.ts new file mode 100644 index 00000000..efc09c31 --- /dev/null +++ b/lib/src/utils/hooks.ts @@ -0,0 +1,27 @@ +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { useEffect, useRef } from 'react'; + +export function useWindowFocusChange(cb: (focused: boolean) => void) { + useEffect(() => { + const promise = getCurrentWindow().onFocusChanged((event) => { + cb(event.payload); + }); + return () => { + promise.then((unlisten) => unlisten()); + }; + }, []); +} + +export function useInterval(cb: () => void, ms: number, deps: any[] = []) { + const ref = useRef(null); + const clearLastInterval = () => { + if (ref.current) { + clearInterval(ref.current); + } + }; + useEffect(() => { + clearLastInterval(); + ref.current = setInterval(cb, ms); + return clearLastInterval; + }, [ms, ...deps]); +} diff --git a/lib/src/utils/index.ts b/lib/src/utils/index.ts new file mode 100644 index 00000000..101f8f29 --- /dev/null +++ b/lib/src/utils/index.ts @@ -0,0 +1,48 @@ +export * from './hooks'; +export * from './layered_hitbox'; + +export function getRootElement() { + const element = document.getElementById('root'); + if (!element) { + throw new Error('Root element not found'); + } + return element; +} + +export class Rect { + left = 0; + top = 0; + right = 0; + bottom = 0; +} + +export function disableWebviewShortcutsAndContextMenu() { + window.addEventListener('keydown', function (event) { + // prevent refresh + if (event.key === 'F5') { + event.preventDefault(); + } + + // prevent close + if (event.altKey && event.key === 'F4') { + event.preventDefault(); + } + + // others + if (event.ctrlKey) { + switch (event.key) { + case 'r': // reload + case 'f': // search + case 'g': // find + case 'p': // print + case 'j': // downloads + case 'u': // source + event.preventDefault(); + break; + } + } + }); + window.addEventListener('contextmenu', (e) => e.preventDefault()); + window.addEventListener('drop', (e) => e.preventDefault()); + window.addEventListener('dragover', (e) => e.preventDefault()); +} diff --git a/src/apps/toolbar/events.ts b/lib/src/utils/layered_hitbox.ts similarity index 55% rename from src/apps/toolbar/events.ts rename to lib/src/utils/layered_hitbox.ts index ffce9a9f..1e9d089b 100644 --- a/src/apps/toolbar/events.ts +++ b/lib/src/utils/layered_hitbox.ts @@ -1,29 +1,16 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import { CallbacksManager } from './modules/shared/utils/app'; +import { SeelenEvent } from '../handlers'; -export const ExtraCallbacksOnBlur = new CallbacksManager(); -export const ExtraCallbacksOnFocus = new CallbacksManager(); - -export async function registerDocumentEvents() { - let appFocused = true; +export async function declareDocumentAsLayeredHitbox() { const webview = getCurrentWebviewWindow(); - - webview.onFocusChanged((event) => { - appFocused = event.payload; - webview.setIgnoreCursorEvents(!appFocused); - if (appFocused) { - return ExtraCallbacksOnFocus.execute(); - } - ExtraCallbacksOnBlur.execute(); - }); - - // this is started as true on rust side but to be secure we set it to false - let ignoring_cursor_events = false; - const { x, y } = await webview.outerPosition(); const { width, height } = await webview.outerSize(); + let webviewRect = { x, y, width, height }; + let ignoring_cursor_events = true; + let is_layered_enabled = true; + await webview.setIgnoreCursorEvents(true); webview.onMoved((e) => { webviewRect.x = e.payload.x; @@ -35,8 +22,12 @@ export async function registerDocumentEvents() { webviewRect.height = e.payload.height; }); - webview.listen<[x: number, y: number]>('global-mouse-move', async (event) => { - if (!(await webview.isVisible())) { + webview.listen(SeelenEvent.HandleLayeredHitboxes, (event) => { + is_layered_enabled = event.payload; + }); + + webview.listen<[x: number, y: number]>(SeelenEvent.GlobalMouseMove, (event) => { + if (!is_layered_enabled) { return; } @@ -57,13 +48,15 @@ export async function registerDocumentEvents() { const adjustedX = (mouseX - windowX) / window.devicePixelRatio; const adjustedY = (mouseY - windowY) / window.devicePixelRatio; - let element = document.elementFromPoint(adjustedX, adjustedY); - if (element != document.body && ignoring_cursor_events) { - webview.setIgnoreCursorEvents(false); - ignoring_cursor_events = false; - } else if (element == document.body && (!ignoring_cursor_events || appFocused)) { - webview.setIgnoreCursorEvents(true); + let isOverBody = document.elementFromPoint(adjustedX, adjustedY) == document.body; + if (isOverBody && !ignoring_cursor_events) { ignoring_cursor_events = true; + webview.setIgnoreCursorEvents(true); + } + + if (!isOverBody && ignoring_cursor_events) { + ignoring_cursor_events = false; + webview.setIgnoreCursorEvents(false); } }); } diff --git a/package-lock.json b/package-lock.json index cdb01ccd..98055fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "seelen-ui", - "version": "1.10.6", + "version": "2.0.0-beta.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "seelen-ui", - "version": "1.10.6", + "version": "2.0.0-beta.17", "hasInstallScript": true, "license": "Polyform Strict License", "dependencies": { @@ -26,12 +26,16 @@ "@stylistic/eslint-plugin": "^1.6.2", "@tauri-apps/api": "^2.0.0-beta.4", "@tauri-apps/cli": "^2.0.0-beta.8", + "@types/d3": "^7.4.3", + "@types/glob": "^8.1.0", "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.7", "@types/node": "^20.11.19", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "antd": "^5.14.1", + "d3": "^7.9.0", "esbuild": "^0.20.1", "eslint": "^8.56.0", "eslint-plugin-simple-import-sort": "^12.0.0", @@ -41,7 +45,6 @@ "i18next": "^23.12.2", "jest": "^29.7.0", "js-yaml": "^4.1.0", - "json-schema-to-typescript": "^13.1.2", "lefthook": "^1.6.18", "lodash": "^4.17.21", "mathjs": "^12.4.2", @@ -60,8 +63,7 @@ "typescript": "5.3.3", "typescript-eslint": "^7.0.1", "yargs": "^17.7.2", - "zod": "^3.23.4", - "zod-to-json-schema": "^3.23.0" + "zod": "^3.23.4" } }, "node_modules/@ampproject/remapping": { @@ -736,24 +738,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bcherny/json-schema-ref-parser": { - "version": "10.0.5-fork", - "resolved": "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz", - "integrity": "sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==", - "dev": true, - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -2533,12 +2517,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2840,23 +2818,18 @@ } }, "node_modules/@tauri-apps/api": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.0.tgz", - "integrity": "sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==", - "engines": { - "node": ">= 18.18", - "npm": ">= 6.6.0", - "yarn": ">= 1.19.1" - }, + "version": "2.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz", + "integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/tauri" } }, "node_modules/@tauri-apps/cli": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.3.tgz", - "integrity": "sha512-iNF95pieBmverl1EmQyqh+fhcIClS544fN5Ex5lAbYLTiHZ/gm3lOfVBhF6NPaKd/sfLuy7K1tfDXlHztBfANw==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.15.tgz", + "integrity": "sha512-w5cq2WBKYRJDCE5wi5gyjM79Cq3AchTFImbcGBB+uyB/m3PDBXEidaTBTHqwiup2hKbMivuBAzGUCHt+OfgBhA==", "dev": true, "bin": { "tauri": "tauri.js" @@ -2869,22 +2842,22 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.0.0-rc.3", - "@tauri-apps/cli-darwin-x64": "2.0.0-rc.3", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.3", - "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.3", - "@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.3", - "@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.3", - "@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.3", - "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.3", - "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.3", - "@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.3" + "@tauri-apps/cli-darwin-arm64": "2.0.0-rc.15", + "@tauri-apps/cli-darwin-x64": "2.0.0-rc.15", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.15", + "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.15", + "@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.15", + "@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.15", + "@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.15", + "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.15", + "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.15", + "@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.15" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.3.tgz", - "integrity": "sha512-szYCSr/ChbCF+S6Wnr15TYpI2cZR07d+AQOiFGuScP0preM8Pbsk/sb0hfLwqzepjVFFNVWQba9sG7FEW2Y2XA==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.15.tgz", + "integrity": "sha512-WuzQRELJTeSHe/uLu6IClCCEkwQy4qtZdHUmcAW3baKD217WCytn4jQ5+NFs2GxhK1a2GLHMQtQZSFTLkKiXkw==", "cpu": [ "arm64" ], @@ -2898,9 +2871,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.3.tgz", - "integrity": "sha512-BJv6EJOY1DJbRzVtfg8CcBAlnS5OjhBAc5YKjh4BT7EyOcop8HStBSxhL6yjWrUP7eLR1iIsW/uSehVJwzYIdQ==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.15.tgz", + "integrity": "sha512-71H1dNWlEr+Hyi096Ir3SnlClw4CSR4MhJ8UG8IUBqYwydJPYFzA+GFWRAgnPgcV6sBzdt8trcV9BLV4teDzEw==", "cpu": [ "x64" ], @@ -2914,9 +2887,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.3.tgz", - "integrity": "sha512-fwx805/xL4sF/EdMYqcUHQHzMYwo+OVTBTz5x/JWK8D57rnmLHAP+ZhnfFsZQLRo2QRT2l1Ye3bDyU+QRA1JFA==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.15.tgz", + "integrity": "sha512-hO7AS09l6XZRCu/vqvB/iv6CvIlD//h9njhyw++0tJPCNH3X4rl13ji6SnoO0V6ZUCEeCeQBTAALsanYAlZelQ==", "cpu": [ "arm" ], @@ -2930,9 +2903,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.3.tgz", - "integrity": "sha512-3KauzO1Ls4kuY0nr82S4X8XFxlQAMN+Mqp8LLqvQ+PPMp92XQAkPH7osQdoHIEoW5gsE69U2JaiQ5tHSqNM9og==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.15.tgz", + "integrity": "sha512-r9FrwY83TD4w3vX7J9zS3GPSeis0YWq52p/MVLYR1i8sSJppbvYY72EXi5pR2CZ3vb+6z9/w7LpYTv+hOd2RbA==", "cpu": [ "arm64" ], @@ -2946,9 +2919,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.3.tgz", - "integrity": "sha512-ngHS0foffm1xO5gqnDKGeYMKj8ceGmrFP5dDldoaaMQubw1SyFa0pRUjb7fZSYiO7F4SOSa8NYeMqlF9peZmnQ==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.15.tgz", + "integrity": "sha512-PqMn3/GiqLAhs7p0jr5XqwWN1t7SAgvo6+bFuYNL/SWx1Ui6mOck3ncfDkf+dQAnXnrhX2Qfwkl3agiOZxUZtA==", "cpu": [ "arm64" ], @@ -2962,9 +2935,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.3.tgz", - "integrity": "sha512-0/am9pVvuUHGmz32M8ffz1fpLnc08j3nzcRe5wUdL2AxfT+wKMII+Dn99GtCVgcdDW4jSXDMRUwrBkGocGC2OA==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.15.tgz", + "integrity": "sha512-o8lvgVBGXwthMV8+8EzEwXQY5jk2q+c700xeC/LY+J0lBL5ai3i0revlhO+3RwKnjnRLZMCXatr5K3gGtXIsoQ==", "cpu": [ "x64" ], @@ -2978,9 +2951,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.3.tgz", - "integrity": "sha512-r7mRi8q8TqTFVjb9kAsU7IgwUgno2s8Ip4xwq9psQhlRE3JGEZQmSEcy1jqTjfl6KFh6lJcDR7l+9/EMhL/D3Q==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.15.tgz", + "integrity": "sha512-cmANCyhcdInZSfIM3CPjA0eDu1toYABapSttA1rHbNrcJrHIq2KPKRCNuXIjWiBggxfIhJKWX7mTgQCQIyHd/w==", "cpu": [ "x64" ], @@ -2994,9 +2967,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.3.tgz", - "integrity": "sha512-2J6KjmDIQCw6HF1X6/yPcd+JLl7pxrH2zVMGmNllaoWhHeByvRobqFWnT7gcdHaA3dGTo432CwWvOgTgrINQpQ==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.15.tgz", + "integrity": "sha512-FyWwCQb+uCCBtEDTDKtILH3wv0TWCQ2mXwMyZlibpbZ4RbaV5yDY82h8h7usfEuPHtBtAJHknHvX5WV1ETl5kw==", "cpu": [ "arm64" ], @@ -3010,9 +2983,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.3.tgz", - "integrity": "sha512-8q75CsHDSEDdgi6xPwim+BaQZFCswK2Dn/qL38V3Mh9kmVvC8oGJMPC66bC20dF+v3KWeFm2FNNGQqOSXCveHg==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.15.tgz", + "integrity": "sha512-+bOBX7EdpmkCSBxgd9HcC/p9LoG/q1a5dJebWFuL9GhmdPeb5hv4plB/OTUAtg1OnEVGPXhTiSkcdRatZVryfA==", "cpu": [ "ia32" ], @@ -3026,9 +2999,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.3.tgz", - "integrity": "sha512-qeBRJYalahxEXolekcpZJ/HBrIJacG2NWJBGhhi797mIwnbmlpbHMc8blIJtNNNwVUb2BjXuxKQVfojQ5YYrcg==", + "version": "2.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.15.tgz", + "integrity": "sha512-Phpk18bs1YxC+OFYaZNWiddYRmiZvMjB9Rzjl6M128gIkgnqDGnZyfWtM5GZ85/BmX1HVGgILK/46RU6Q88z1g==", "cpu": [ "x64" ], @@ -3042,67 +3015,67 @@ } }, "node_modules/@tauri-apps/plugin-autostart": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-autostart/-/plugin-autostart-2.0.0-rc.0.tgz", - "integrity": "sha512-V49lm++vhrMPPDGMtmOcbJLF4TYu78ZmAiMhyd4FFnbYlgin6ZTjiMCFEl4JKVy2lqP3C8DQvXf/gkUMuER7Iw==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-autostart/-/plugin-autostart-2.0.0-rc.1.tgz", + "integrity": "sha512-5h801HJf6Z5CtUmJhwv+PBSn2nNAWqsuKmu0hABn/IpP2AElZev4XicMzrnYVevJeIhWgRA8HNpg3s8pbic1Rw==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-deep-link": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-deep-link/-/plugin-deep-link-2.0.0-rc.0.tgz", - "integrity": "sha512-LdwxGeQAkxbOYBcamfOT6hAokstkhKz7t5mZcm5wCoUSTPIzMX/+7lNS8hsQouiTg7EXCXGaLW3nzwF9qwMA6g==", + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-deep-link/-/plugin-deep-link-2.0.0-rc.2.tgz", + "integrity": "sha512-EisZPKRXXWVdryRAwGyZBYSCYjANPH3BRyZLW3QarLGG49ziLwkvFC1f5gA6fH2xTeSrgngu3LeHyK8xSIf3Cw==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.0.tgz", - "integrity": "sha512-DPOXYe8SQ6Radk/67EOdaomlxL7oF99JO/ZUaPp1IBEs3Wro7lhlz63CfdKIBfKIZTLJLzP1R7/EiPL/GTA3Bg==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz", + "integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-fs": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-rc.0.tgz", - "integrity": "sha512-74VCXEZlzTJ+Jv1V3KrV0qIHhSePpE/ljsF78rcEuvSfyTxLtt/Sb5CIUmVhFlKTRFOH9dX50T4dTZ3qFLyRnA==", + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-rc.2.tgz", + "integrity": "sha512-TFjCfso3tN4b5s2EBjqP8N2gYrPh93Ds3VNKj8pCXv4wbvnItyfG0aHO0haUsedBOHQryDwv9vDAdPX6/T0a+g==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-log": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.0.0-rc.0.tgz", - "integrity": "sha512-ztKfzUcq03dtr08vBu+xcwIEPusP6mCpuLAt6kpXEwG+HvYsC8e1/KFFokn3xvfwD+oBJ3UTL1h4kdM30GAqGw==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.0.0-rc.1.tgz", + "integrity": "sha512-+Tz0zo4FDtC/5j7neeIq5ievgKbUXBV2+X5HtbaR8ZZ2bcksCp8UqeHd6cyyN+FSk4qaU01LIGkuExtxk1h/FA==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-process": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.0.0-rc.0.tgz", - "integrity": "sha512-Z12D/kmQzG1vCVf+jLXPhPDUA0pEjFrsg4p0uwO2sotVLM9287IuTM+aIz9cuAYOxFLKcsnDG7amSCL9IfA1gw==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.0.0-rc.1.tgz", + "integrity": "sha512-Bl22xdoiu+AqEP6rzjb7DUJwdLDnejuRFukpkdrqF1/VEWJK5PuE903l+8mIOsd17zZ1Ua8y8WaBWnOXx4QHmw==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-shell": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.0.tgz", - "integrity": "sha512-bhUcQcrqZoK8H1DFXapr5r1Z75oh6Kd5Tltz97XpZFLREEqp+KhN2Fvyh8r/fKAyenYsTYUIsDsyGdjdueuF9g==", + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz", + "integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tauri-apps/plugin-updater": { - "version": "2.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-rc.0.tgz", - "integrity": "sha512-EKajf/sBpFif0cwXhTo3BmNvTZ2t2DDLRyhA8FFKugZNoOeqU97bHhPT5DIqMUPRE1tyDk9o7sXm8dKf7oz+EA==", + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-rc.2.tgz", + "integrity": "sha512-Ngvpa/km/00KASvOsFqRQbVf/6BaAX/gYwQs9eFxjygDpxxlZkZDVP1Fg0urW8s5dY7ELD6UAFB/ZI/g8D0QvQ==", "dependencies": { - "@tauri-apps/api": "^2.0.0-rc.0" + "@tauri-apps/api": "^2.0.0-rc.4" } }, "node_modules/@tsconfig/node10": { @@ -3179,6 +3152,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -3195,13 +3421,19 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true + }, "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", "dev": true, "dependencies": { - "@types/minimatch": "*", + "@types/minimatch": "^5.1.2", "@types/node": "*" } }, @@ -3261,9 +3493,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", - "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/minimatch": { @@ -3281,12 +3513,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true - }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -4017,12 +4243,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -4295,12 +4515,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4427,22 +4641,6 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "dev": true }, - "node_modules/cli-color": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", - "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.64", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.15", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4491,6 +4689,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -4579,111 +4786,499 @@ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", "dev": true, - "dependencies": { - "toggle-selection": "^1.0.6" + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", + "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "dev": true, + "dependencies": { + "jiti": "^1.19.1" + }, + "engines": { + "node": ">=v16" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=8.2", + "typescript": ">=4" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "engines": { + "node": ">=12" } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dev": true, "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "d3-path": "^3.1.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", - "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "dev": true, "dependencies": { - "jiti": "^1.19.1" + "d3-array": "2 - 3" }, "engines": { - "node": ">=v16" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=8.2", - "typescript": ">=4" + "node": ">=12" } }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dev": true, "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" + "d3-time": "1 - 3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "engines": { + "node": ">=12" + } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "dev": true, "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": ">=0.12" + "node": ">=12" } }, "node_modules/dargs": { @@ -4756,6 +5351,15 @@ "node": ">=0.10.0" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dev": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4861,58 +5465,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "dev": true, - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, "node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -5092,21 +5644,6 @@ "node": "*" } }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -5179,16 +5716,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5237,15 +5764,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, - "dependencies": { - "type": "^2.7.2" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5464,18 +5982,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5538,25 +6044,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-promise": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-4.2.2.tgz", - "integrity": "sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==", - "dev": true, - "dependencies": { - "@types/glob": "^7.1.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/ahmadnassri" - }, - "peerDependencies": { - "glob": "^7.1.6" - } - }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5722,6 +6209,18 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -5827,6 +6326,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5926,12 +6434,6 @@ "node": ">=8" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -6652,34 +7154,6 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, - "node_modules/json-schema-to-typescript": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-13.1.2.tgz", - "integrity": "sha512-17G+mjx4nunvOpkPvcz7fdwUwYCEwyH8vR3Ym3rFiQ8uzAL3go+c1306Kk7iGRk8HuXBXqy+JJJmpYl0cvOllw==", - "dev": true, - "dependencies": { - "@bcherny/json-schema-ref-parser": "10.0.5-fork", - "@types/json-schema": "^7.0.11", - "@types/lodash": "^4.14.182", - "@types/prettier": "^2.6.1", - "cli-color": "^2.0.2", - "get-stdin": "^8.0.0", - "glob": "^7.1.6", - "glob-promise": "^4.2.2", - "is-glob": "^4.0.3", - "lodash": "^4.17.21", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "mz": "^2.7.0", - "prettier": "^2.6.2" - }, - "bin": { - "json2ts": "dist/src/cli.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7010,15 +7484,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", - "dev": true, - "dependencies": { - "es5-ext": "~0.10.2" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -7072,25 +7537,6 @@ "node": ">= 18" } }, - "node_modules/memoizee": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", - "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", - "dev": true, - "dependencies": { - "d": "^1.0.2", - "es5-ext": "^0.10.64", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/meow": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", @@ -7176,18 +7622,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -7203,29 +7637,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7330,15 +7747,6 @@ "node": ">=8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7591,21 +7999,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -8586,6 +8979,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8609,6 +9008,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -8913,27 +9324,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -8949,19 +9339,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/timers-ext": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", - "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", - "dev": true, - "dependencies": { - "es5-ext": "^0.10.64", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", @@ -9124,12 +9501,6 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9564,15 +9935,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.1.tgz", - "integrity": "sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==", - "dev": true, - "peerDependencies": { - "zod": "^3.23.3" - } } } } diff --git a/package.json b/package.json index bb6000a8..5d7ed1d2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "seelen-ui", "productName": "seelen-ui", - "version": "1.10.6", + "version": "2.0.0-beta.17", "description": "Seelen UI Project", "scripts": { "tauri": "tauri", @@ -43,12 +43,16 @@ "@stylistic/eslint-plugin": "^1.6.2", "@tauri-apps/api": "^2.0.0-beta.4", "@tauri-apps/cli": "^2.0.0-beta.8", + "@types/glob": "^8.1.0", + "@types/d3": "^7.4.3", "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.7", "@types/node": "^20.11.19", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "antd": "^5.14.1", + "d3": "^7.9.0", "esbuild": "^0.20.1", "eslint": "^8.56.0", "eslint-plugin-simple-import-sort": "^12.0.0", @@ -58,7 +62,6 @@ "i18next": "^23.12.2", "jest": "^29.7.0", "js-yaml": "^4.1.0", - "json-schema-to-typescript": "^13.1.2", "lefthook": "^1.6.18", "lodash": "^4.17.21", "mathjs": "^12.4.2", @@ -77,7 +80,6 @@ "typescript": "5.3.3", "typescript-eslint": "^7.0.1", "yargs": "^17.7.2", - "zod": "^3.23.4", - "zod-to-json-schema": "^3.23.0" + "zod": "^3.23.4" } } diff --git a/readme.md b/readme.md index da529a60..e96cb7bd 100644 --- a/readme.md +++ b/readme.md @@ -67,18 +67,32 @@ Seelen UI is a tool designed to enhance your Windows desktop experience with a f * **Customize Your Desktop**: Seelen UI lets you tailor your desktop to fit your style and needs. You can adjust menus, widgets, and other elements to create a workspace that works best for you. - Banner showcasing various customization options in Seelen UI + ![Seelen UI Minimal Desktop](./documentation/images/preview2.png)
* **Enhance Your Productivity**: Seelen UI helps you organize your desktop efficiently. With a Tiling Windows Manager, windows automatically arrange themselves to support multitasking, making your work more streamlined. + + ![Seelen UI Tiling Window Manager](./documentation/images/twm_preview.png) + +
+ +* **Enjoy your music**: With an integrated media module that's compatible with most music players, Seelen UI allows you to enjoy your music seamlessly. You can pause, resume, and skip tracks at any time without the need to open additional windows. + + ![Seelen UI Media Module](./documentation/images/media_module_preview.png) + +
+ +* **Be faster!**: With the app launcher inpired in Rofi, Seelen UI provides a simple and intuitive way to quickly access your applications and ejecute commands. + + ![Seelen UI App Launcher](./documentation/images/app_launcher_preview.png) - Seelen UI desktop with organized windows for efficient multitasking
* **User-Friendly Configuration**: Seelen UI offers an intuitive interface for easy customization. Adjust settings such as themes, taskbar layouts, icons, etc. With just a few clicks. - Example of customizable desktop settings in Seelen UI + ![Seelen UI Settings](./documentation/images/settings_preview.png) +
## Installation @@ -124,7 +138,7 @@ For in-depth details on various aspects of Seelen UI, explore the following docu I’m excited to share some upcoming features for Seelen UI! Here’s a glimpse of what’s planned for the future: -### App Launcher +### ~~App Launcher~~ ✅ I’m planning to develop an app launcher inspired by [Rofi](https://github.com/davatorium/rofi) on Linux. This feature will provide a sleek and highly customizable way to quickly access your applications. ![App Launcher Preview](https://raw.githubusercontent.com/adi1090x/files/master/rofi/previews/colorful/main.gif) diff --git a/scripts/UpdateTauri.ts b/scripts/UpdateTauri.ts index 8059f4c3..a2acc21a 100644 --- a/scripts/UpdateTauri.ts +++ b/scripts/UpdateTauri.ts @@ -1,31 +1,32 @@ -import packageJson from '../package.json'; -import { execSync } from 'child_process'; -import { readFileSync } from 'fs'; -import toml from 'toml'; - -let dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; -let toUpdate: string[] = []; - -for (let key in dependencies) { - if (key.startsWith('@tauri-apps/')) { - toUpdate.push(key); - } -} - -let command = `npm update ${toUpdate.join(' ')}`; -console.log(`${command}\n`); -execSync(command, { stdio: 'inherit' }); - -const cargoToml = toml.parse(readFileSync('Cargo.toml', 'utf-8')); -dependencies = { ...cargoToml['build-dependencies'], ...cargoToml.dependencies }; -toUpdate = []; - -for (let key in dependencies) { - if (key.startsWith('tauri')) { - toUpdate.push(key); - } -} - -command = `cargo update ${toUpdate.join(' ')}`; -console.log(`${command}\n`); -execSync(command, { stdio: 'inherit' }); +import { execSync } from 'child_process'; +import { readFileSync } from 'fs'; +import toml from 'toml'; + +import packageJson from '../package.json'; + +let dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; +let toUpdate: string[] = []; + +for (let key in dependencies) { + if (key.startsWith('@tauri-apps/')) { + toUpdate.push(key); + } +} + +let command = `npm update ${toUpdate.join(' ')}`; +console.log(`${command}\n`); +execSync(command, { stdio: 'inherit' }); + +const cargoToml = toml.parse(readFileSync('Cargo.toml', 'utf-8')); +dependencies = { ...cargoToml['build-dependencies'], ...cargoToml.dependencies }; +toUpdate = []; + +for (let key in dependencies) { + if (key.startsWith('tauri')) { + toUpdate.push(key); + } +} + +command = `cargo update ${toUpdate.join(' ')}`; +console.log(`${command}\n`); +execSync(command, { stdio: 'inherit' }); diff --git a/scripts/build.ts b/scripts/build.ts index 27d5bf10..51e2ee29 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,17 +1,80 @@ import esbuild from 'esbuild'; import fs from 'fs'; import path from 'path'; +import { renderToStaticMarkup } from 'react-dom/server'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +async function ssrIcons() { + const promises = [ + import('react-icons/ai'), + import('react-icons/bi'), + import('react-icons/bs'), + import('react-icons/cg'), + import('react-icons/ci'), + import('react-icons/di'), + import('react-icons/fa'), + import('react-icons/fa6'), + import('react-icons/fc'), + import('react-icons/fi'), + import('react-icons/gi'), + import('react-icons/go'), + import('react-icons/gr'), + import('react-icons/hi'), + import('react-icons/hi2'), + import('react-icons/im'), + import('react-icons/io'), + import('react-icons/io5'), + import('react-icons/lia'), + import('react-icons/lu'), + import('react-icons/md'), + import('react-icons/pi'), + import('react-icons/ri'), + import('react-icons/rx'), + import('react-icons/si'), + import('react-icons/sl'), + import('react-icons/tb'), + import('react-icons/tfi'), + import('react-icons/ti'), + import('react-icons/vsc'), + import('react-icons/wi'), + ]; + + let families = await Promise.all(promises); + for (const family of families) { + for (const [name, ElementConstructor] of Object.entries(family.default)) { + const element = ElementConstructor({ size: '1em' }); + const svg = renderToStaticMarkup(element); + fs.writeFileSync(`./dist/icons/${name}.svg`, svg); + } + } +} async function main() { + console.time('Build UI'); + const argv = await yargs(hideBin(process.argv)).option('production', { + type: 'boolean', + description: 'Enable Production Minified Bundle', + }).argv; + + const isProdMode = !!argv.production; + const appFolders = fs .readdirSync('src/apps') .filter((item) => item !== 'shared' && fs.statSync(path.join('src/apps', item)).isDirectory()); + appFolders.forEach((folder) => { + const filePath = path.join('dist', folder); + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { recursive: true, force: true }); + } + }); + await esbuild.build({ entryPoints: appFolders.map((folder) => `./src/apps/${folder}/index.tsx`), bundle: true, - minify: false, - sourcemap: true, + minify: isProdMode, + sourcemap: !isProdMode, outdir: './dist', jsx: 'automatic', define: { @@ -25,8 +88,18 @@ async function main() { }); appFolders.forEach((folder) => { - fs.cpSync(`src/apps/${folder}/index.html`, `dist/${folder}/index.html`); + let source = `src/apps/${folder}/public`; + let target = `dist/${folder}`; + fs.cpSync(source, target, { recursive: true }); }); + console.timeEnd('Build UI'); + + if (!fs.existsSync('./dist/icons')) { + console.time('Bundle Lazy Icons'); + fs.mkdirSync('./dist/icons', { recursive: true }); + await ssrIcons(); + console.timeEnd('Bundle Lazy Icons'); + } } main(); diff --git a/scripts/bundle.msix.ts b/scripts/bundle.msix.ts index 40f47cd1..8e775542 100644 --- a/scripts/bundle.msix.ts +++ b/scripts/bundle.msix.ts @@ -1,52 +1,53 @@ -import packageJson from '../package.json'; -import tauriConfig from '../tauri.conf.json'; -import { execSync } from 'child_process'; -import fs from 'fs'; -import glob from 'glob'; -import path from 'path'; - -console.info('Building MSIX...'); -const msixCmdsPath = path.resolve('target/release/msix/commands.txt'); -const msixTemplatePath = path.resolve('templates/installer.msix'); - -const packageVersion = packageJson.version + '.0'; -const installer_msix_path = path.resolve( - `target/release/bundle/msix/Seelen.SeelenUI_${packageVersion}_x64__p6yyn03m1894e.msix`, -); - -if (fs.existsSync(msixCmdsPath)) { - fs.rmSync(msixCmdsPath); -} else { - fs.mkdirSync(path.dirname(msixCmdsPath)); -} - -fs.appendFileSync(msixCmdsPath, `setIdentity --packageVersion ${packageVersion}\n`); - -fs.appendFileSync( - msixCmdsPath, - `addFile --target "${packageJson.productName}.exe" --source "${path.resolve( - `target/release/${packageJson.productName}.exe`, - )}"\n`, -); - -tauriConfig.bundle.resources.forEach((pattern) => { - let files = glob.sync(pattern, { nodir: true }); - files.forEach((file) => { - fs.appendFileSync( - msixCmdsPath, - `addFile --target "${file}" --source "${path.resolve(`target/release/${file}`)}"\n`, - ); - }); -}); - -try { - fs.mkdirSync(installer_msix_path.split(path.sep).slice(0, -1).join(path.sep), { - recursive: true, - }); - fs.copyFileSync(msixTemplatePath, installer_msix_path); - - const buffer = execSync(`msixHeroCli edit "${installer_msix_path}" list "${msixCmdsPath}"`); - console.info(buffer.toString()); -} catch (error) { - console.error('\n', error); -} +import { execSync } from 'child_process'; +import fs from 'fs'; +import glob from 'glob'; +import path from 'path'; + +import packageJson from '../package.json'; +import tauriConfig from '../tauri.conf.json'; + +console.info('Building MSIX...'); +const msixCmdsPath = path.resolve('target/release/msix/commands.txt'); +const msixTemplatePath = path.resolve('templates/installer.msix'); + +const packageVersion = packageJson.version + '.0'; +const installer_msix_path = path.resolve( + `target/release/bundle/msix/Seelen.SeelenUI_${packageVersion}_x64__p6yyn03m1894e.msix`, +); + +if (fs.existsSync(msixCmdsPath)) { + fs.rmSync(msixCmdsPath); +} else { + fs.mkdirSync(path.dirname(msixCmdsPath)); +} + +fs.appendFileSync(msixCmdsPath, `setIdentity --packageVersion ${packageVersion}\n`); + +fs.appendFileSync( + msixCmdsPath, + `addFile --target "${packageJson.productName}.exe" --source "${path.resolve( + `target/release/${packageJson.productName}.exe`, + )}"\n`, +); + +tauriConfig.bundle.resources.forEach((pattern) => { + let files = glob.sync(pattern, { nodir: true }); + files.forEach((file) => { + fs.appendFileSync( + msixCmdsPath, + `addFile --target "${file}" --source "${path.resolve(`target/release/${file}`)}"\n`, + ); + }); +}); + +try { + fs.mkdirSync(installer_msix_path.split(path.sep).slice(0, -1).join(path.sep), { + recursive: true, + }); + fs.copyFileSync(msixTemplatePath, installer_msix_path); + + const buffer = execSync(`msixHeroCli edit "${installer_msix_path}" list "${msixCmdsPath}"`); + console.info(buffer.toString()); +} catch (error) { + console.error('\n', error); +} diff --git a/scripts/lefthook/post_hook.rs b/scripts/lefthook/post_hook.rs new file mode 100644 index 00000000..08378be3 --- /dev/null +++ b/scripts/lefthook/post_hook.rs @@ -0,0 +1,10 @@ +// #!/usr/bin/env rust-script + +fn main() { + let temp = std::path::PathBuf::from(".dist"); + let dist = std::path::PathBuf::from("dist"); + if temp.exists() { + std::fs::remove_dir(&dist).unwrap(); + std::fs::rename(temp, dist).unwrap(); + } +} diff --git a/scripts/lefthook/pre_hook.rs b/scripts/lefthook/pre_hook.rs new file mode 100644 index 00000000..3ce9897c --- /dev/null +++ b/scripts/lefthook/pre_hook.rs @@ -0,0 +1,13 @@ +// #!/usr/bin/env rust-script +use std::fs; + +fn main() { + let dist = std::path::PathBuf::from("dist"); + let temp = std::path::PathBuf::from(".dist"); + if dist.exists() && !temp.exists() { + fs::rename(&dist, temp).unwrap(); + } + if !dist.exists() { + fs::create_dir(dist).unwrap(); + } +} diff --git a/scripts/translate.ts b/scripts/translate.ts index 03485c20..fa411119 100644 --- a/scripts/translate.ts +++ b/scripts/translate.ts @@ -102,7 +102,7 @@ async function main() { await completeTranslationsFor('toolbar', keysToUpdate, deleteKeys); await completeTranslationsFor('seelenweg', keysToUpdate, deleteKeys); await completeTranslationsFor('settings', keysToUpdate, deleteKeys); - await completeTranslationsFor('update', keysToUpdate, deleteKeys); + await completeTranslationsFor('seelen_rofi', keysToUpdate, deleteKeys); } main().catch(console.error); diff --git a/src/apps/seelen_rofi/App.tsx b/src/apps/seelen_rofi/App.tsx new file mode 100644 index 00000000..d5bf8cd4 --- /dev/null +++ b/src/apps/seelen_rofi/App.tsx @@ -0,0 +1,24 @@ +import { ConfigProvider, theme } from 'antd'; +import { useSelector } from 'react-redux'; + +import { Launcher } from './modules/launcher/infra'; + +import { Selectors } from './modules/shared/store/app'; + +import { useDarkMode } from '../shared/styles'; + +export function App() { + const isDarkMode = useDarkMode(); + const colors = useSelector(Selectors.colors); + + return + + ; +} \ No newline at end of file diff --git a/src/apps/seelen_rofi/events.ts b/src/apps/seelen_rofi/events.ts new file mode 100644 index 00000000..e60fad3d --- /dev/null +++ b/src/apps/seelen_rofi/events.ts @@ -0,0 +1,12 @@ +export async function registerDocumentEvents() { + document.addEventListener('focusin', (event) => { + const element = event.target as HTMLElement; + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest', + }); + } + }); +} diff --git a/src/apps/update/i18n/index.ts b/src/apps/seelen_rofi/i18n/index.ts similarity index 97% rename from src/apps/update/i18n/index.ts rename to src/apps/seelen_rofi/i18n/index.ts index 5ab49876..bd07905e 100644 --- a/src/apps/update/i18n/index.ts +++ b/src/apps/seelen_rofi/i18n/index.ts @@ -1,98 +1,99 @@ -import { Lang } from '../../shared/lang'; -import i18n from 'i18next'; -import yaml from 'js-yaml'; -import { initReactI18next } from 'react-i18next'; - -i18n.use(initReactI18next).init( - { - lng: 'en', - fallbackLng: 'en', - interpolation: { - escapeValue: false, - }, - debug: true, - resources: {}, - }, - undefined, -); - -export async function loadTranslations() { - const translations: Record = { - en: await import('./translations/en.yml'), - es: await import('./translations/es.yml'), - de: await import('./translations/de.yml'), - zh: await import('./translations/zh.yml'), - ko: await import('./translations/ko.yml'), - fr: await import('./translations/fr.yml'), - ar: await import('./translations/ar.yml'), - pt: await import('./translations/pt.yml'), - hi: await import('./translations/hi.yml'), - ru: await import('./translations/ru.yml'), - ja: await import('./translations/ja.yml'), - it: await import('./translations/it.yml'), - nl: await import('./translations/nl.yml'), - tr: await import('./translations/tr.yml'), - pl: await import('./translations/pl.yml'), - uk: await import('./translations/uk.yml'), - id: await import('./translations/id.yml'), - cs: await import('./translations/cs.yml'), - th: await import('./translations/th.yml'), - vi: await import('./translations/vi.yml'), - ms: await import('./translations/ms.yml'), - he: await import('./translations/he.yml'), - ro: await import('./translations/ro.yml'), - el: await import('./translations/el.yml'), - sv: await import('./translations/sv.yml'), - no: await import('./translations/no.yml'), - fi: await import('./translations/fi.yml'), - da: await import('./translations/da.yml'), - hu: await import('./translations/hu.yml'), - lt: await import('./translations/lt.yml'), - bg: await import('./translations/bg.yml'), - sk: await import('./translations/sk.yml'), - hr: await import('./translations/hr.yml'), - lv: await import('./translations/lv.yml'), - et: await import('./translations/et.yml'), - tl: await import('./translations/tl.yml'), - ca: await import('./translations/ca.yml'), - af: await import('./translations/af.yml'), - bn: await import('./translations/bn.yml'), - fa: await import('./translations/fa.yml'), - pa: await import('./translations/pa.yml'), - sw: await import('./translations/sw.yml'), - ta: await import('./translations/ta.yml'), - ur: await import('./translations/ur.yml'), - cy: await import('./translations/cy.yml'), - am: await import('./translations/am.yml'), - hy: await import('./translations/hy.yml'), - az: await import('./translations/az.yml'), - eu: await import('./translations/eu.yml'), - bs: await import('./translations/bs.yml'), - ka: await import('./translations/ka.yml'), - gu: await import('./translations/gu.yml'), - is: await import('./translations/is.yml'), - km: await import('./translations/km.yml'), - ku: await import('./translations/ku.yml'), - lo: await import('./translations/lo.yml'), - lb: await import('./translations/lb.yml'), - mk: await import('./translations/mk.yml'), - mt: await import('./translations/mt.yml'), - mn: await import('./translations/mn.yml'), - ne: await import('./translations/ne.yml'), - ps: await import('./translations/ps.yml'), - sr: await import('./translations/sr.yml'), - si: await import('./translations/si.yml'), - so: await import('./translations/so.yml'), - tg: await import('./translations/tg.yml'), - te: await import('./translations/te.yml'), - uz: await import('./translations/uz.yml'), - yo: await import('./translations/yo.yml'), - zu: await import('./translations/zu.yml'), - }; - - for (const [key, value] of Object.entries(translations)) { - i18n.addResourceBundle(key, 'translation', yaml.load(value.default)); - } -} - -export default i18n; +import i18n from 'i18next'; +import yaml from 'js-yaml'; +import { initReactI18next } from 'react-i18next'; + +import { Lang } from '../../shared/lang'; + +i18n.use(initReactI18next).init( + { + lng: 'en', + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + debug: true, + resources: {}, + }, + undefined, +); + +export async function loadTranslations() { + const translations: Record = { + en: await import('./translations/en.yml'), + es: await import('./translations/es.yml'), + de: await import('./translations/de.yml'), + zh: await import('./translations/zh.yml'), + ko: await import('./translations/ko.yml'), + fr: await import('./translations/fr.yml'), + ar: await import('./translations/ar.yml'), + ru: await import('./translations/ru.yml'), + pt: await import('./translations/pt.yml'), + ja: await import('./translations/ja.yml'), + hi: await import('./translations/hi.yml'), + it: await import('./translations/it.yml'), + nl: await import('./translations/nl.yml'), + tr: await import('./translations/tr.yml'), + pl: await import('./translations/pl.yml'), + uk: await import('./translations/uk.yml'), + id: await import('./translations/id.yml'), + cs: await import('./translations/cs.yml'), + th: await import('./translations/th.yml'), + vi: await import('./translations/vi.yml'), + ms: await import('./translations/ms.yml'), + he: await import('./translations/he.yml'), + ro: await import('./translations/ro.yml'), + el: await import('./translations/el.yml'), + sv: await import('./translations/sv.yml'), + no: await import('./translations/no.yml'), + fi: await import('./translations/fi.yml'), + da: await import('./translations/da.yml'), + hu: await import('./translations/hu.yml'), + lt: await import('./translations/lt.yml'), + bg: await import('./translations/bg.yml'), + sk: await import('./translations/sk.yml'), + hr: await import('./translations/hr.yml'), + lv: await import('./translations/lv.yml'), + et: await import('./translations/et.yml'), + tl: await import('./translations/tl.yml'), + ca: await import('./translations/ca.yml'), + af: await import('./translations/af.yml'), + bn: await import('./translations/bn.yml'), + fa: await import('./translations/fa.yml'), + pa: await import('./translations/pa.yml'), + sw: await import('./translations/sw.yml'), + ta: await import('./translations/ta.yml'), + ur: await import('./translations/ur.yml'), + cy: await import('./translations/cy.yml'), + am: await import('./translations/am.yml'), + hy: await import('./translations/hy.yml'), + az: await import('./translations/az.yml'), + eu: await import('./translations/eu.yml'), + bs: await import('./translations/bs.yml'), + ka: await import('./translations/ka.yml'), + gu: await import('./translations/gu.yml'), + is: await import('./translations/is.yml'), + km: await import('./translations/km.yml'), + ku: await import('./translations/ku.yml'), + lo: await import('./translations/lo.yml'), + lb: await import('./translations/lb.yml'), + mk: await import('./translations/mk.yml'), + mt: await import('./translations/mt.yml'), + mn: await import('./translations/mn.yml'), + ne: await import('./translations/ne.yml'), + ps: await import('./translations/ps.yml'), + sr: await import('./translations/sr.yml'), + si: await import('./translations/si.yml'), + so: await import('./translations/so.yml'), + tg: await import('./translations/tg.yml'), + te: await import('./translations/te.yml'), + uz: await import('./translations/uz.yml'), + yo: await import('./translations/yo.yml'), + zu: await import('./translations/zu.yml'), + }; + + for (const [key, value] of Object.entries(translations)) { + i18n.addResourceBundle(key, 'translation', yaml.load(value.default)); + } +} + +export default i18n; diff --git a/src/apps/seelen_rofi/i18n/translations/af.yml b/src/apps/seelen_rofi/i18n/translations/af.yml new file mode 100644 index 00000000..38c28012 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/af.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Kommandeer + explorer: Wedloop +footer: + shortcuts: Wys kortpaaie +header: + search: App, opdrag of pad +item: + open_location: Oop lêlokasie + pin: Pin to Dock diff --git a/src/apps/seelen_rofi/i18n/translations/am.yml b/src/apps/seelen_rofi/i18n/translations/am.yml new file mode 100644 index 00000000..b3860d4d --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/am.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: ሩጫ + cmd: ትእዛዝ +footer: + shortcuts: አቋራጮችን አሳይ +header: + search: መተግበሪያ, ትእዛዝ ወይም ዱካ +item: + open_location: የፋይል ቦታ ይክፈቱ + pin: ፒን diff --git a/src/apps/seelen_rofi/i18n/translations/ar.yml b/src/apps/seelen_rofi/i18n/translations/ar.yml new file mode 100644 index 00000000..6268ba44 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ar.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: يأمر + explorer: يجري +footer: + shortcuts: عرض اختصارات +header: + search: التطبيق أو الأمر أو المسار +item: + pin: دبوس إلى قفص الاتهام + open_location: افتح موقع الملف diff --git a/src/apps/seelen_rofi/i18n/translations/az.yml b/src/apps/seelen_rofi/i18n/translations/az.yml new file mode 100644 index 00000000..f3806b7a --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/az.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Qaçmaq + cmd: Əmr etmək +footer: + shortcuts: Qısa yol göstərmək +header: + search: Tətbiq, əmr və ya yol +item: + open_location: Fayl yeri açın + pin: Doka pin diff --git a/src/apps/seelen_rofi/i18n/translations/bg.yml b/src/apps/seelen_rofi/i18n/translations/bg.yml new file mode 100644 index 00000000..7b73c481 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/bg.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Изпълнете + cmd: Команда +footer: + shortcuts: Покажете преки пътища +header: + search: Приложение, команда или път +item: + open_location: Отворете местоположението на файла + pin: Пин към док diff --git a/src/apps/seelen_rofi/i18n/translations/bn.yml b/src/apps/seelen_rofi/i18n/translations/bn.yml new file mode 100644 index 00000000..969739fe --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/bn.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: কমান্ড + explorer: চালানো +footer: + shortcuts: শর্টকাটগুলি দেখান +header: + search: অ্যাপ্লিকেশন, কমান্ড বা পথ +item: + pin: ডক পিন + open_location: ফাইলের অবস্থান খুলুন diff --git a/src/apps/seelen_rofi/i18n/translations/bs.yml b/src/apps/seelen_rofi/i18n/translations/bs.yml new file mode 100644 index 00000000..6c2bac28 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/bs.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Trčati + cmd: Naredba +footer: + shortcuts: Prikaži prečice +header: + search: Aplikacija, naredba ili staza +item: + pin: Pin za pristajanje + open_location: Otvorite lokaciju datoteke diff --git a/src/apps/seelen_rofi/i18n/translations/ca.yml b/src/apps/seelen_rofi/i18n/translations/ca.yml new file mode 100644 index 00000000..65dfe341 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ca.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Manar + explorer: Dirigir +footer: + shortcuts: Mostra dreceres +header: + search: Aplicació, ordre o ruta +item: + open_location: Obriu la ubicació del fitxer + pin: Pin a Dock diff --git a/src/apps/seelen_rofi/i18n/translations/cs.yml b/src/apps/seelen_rofi/i18n/translations/cs.yml new file mode 100644 index 00000000..d7a1a1c9 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/cs.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Běh + cmd: Příkaz +footer: + shortcuts: Zobrazit zkratky +header: + search: Aplikace, příkaz nebo cesta +item: + open_location: Otevřete umístění souboru + pin: Pin to Dock diff --git a/src/apps/seelen_rofi/i18n/translations/cy.yml b/src/apps/seelen_rofi/i18n/translations/cy.yml new file mode 100644 index 00000000..622eeb24 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/cy.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Gorchmynnwn + explorer: Redych +footer: + shortcuts: Dangos llwybrau byr +header: + search: Ap, gorchymyn neu lwybr +item: + open_location: Lleoliad Ffeil Agored + pin: Pin i doc diff --git a/src/apps/seelen_rofi/i18n/translations/da.yml b/src/apps/seelen_rofi/i18n/translations/da.yml new file mode 100644 index 00000000..bf35f4d8 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/da.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Løbe + cmd: Kommando +footer: + shortcuts: Vis genveje +header: + search: App, kommando eller sti +item: + open_location: Åben filplacering + pin: Pin til dock diff --git a/src/apps/seelen_rofi/i18n/translations/de.yml b/src/apps/seelen_rofi/i18n/translations/de.yml new file mode 100644 index 00000000..8e24ecbd --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/de.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Befehl + explorer: Laufen +footer: + shortcuts: Zeigen Sie Verknüpfungen +header: + search: App, Befehl oder Pfad +item: + open_location: Dateispeicherort öffnen + pin: Stift zum Anlegen diff --git a/src/apps/seelen_rofi/i18n/translations/el.yml b/src/apps/seelen_rofi/i18n/translations/el.yml new file mode 100644 index 00000000..4a9887bf --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/el.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Τρέξιμο + cmd: Εντολή +footer: + shortcuts: Εμφάνιση συντομεύσεων +header: + search: Εφαρμογή, εντολή ή διαδρομή +item: + open_location: Ανοίξτε την τοποθεσία αρχείου + pin: Καρφίτσα στην αποβάθρα diff --git a/src/apps/seelen_rofi/i18n/translations/en.yml b/src/apps/seelen_rofi/i18n/translations/en.yml new file mode 100644 index 00000000..cc005245 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/en.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Run + cmd: Command +header: + search: App, Command or Path +item: + pin: Pin to Dock + open_location: Open File Location +footer: + shortcuts: Show Shortcuts diff --git a/src/apps/seelen_rofi/i18n/translations/es.yml b/src/apps/seelen_rofi/i18n/translations/es.yml new file mode 100644 index 00000000..b151f6cc --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/es.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Correr + cmd: Dominio +footer: + shortcuts: Mostrar atajos +header: + search: Aplicación, comando o ruta +item: + open_location: Abra la ubicación del archivo + pin: Pin para acoplar diff --git a/src/apps/seelen_rofi/i18n/translations/et.yml b/src/apps/seelen_rofi/i18n/translations/et.yml new file mode 100644 index 00000000..ad9c9689 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/et.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Käsk + explorer: Jooksma +footer: + shortcuts: Näita otseteid +header: + search: Rakendus, käsk või tee +item: + pin: PIN -kood dokkimiseks + open_location: Avatud faili asukoht diff --git a/src/apps/seelen_rofi/i18n/translations/eu.yml b/src/apps/seelen_rofi/i18n/translations/eu.yml new file mode 100644 index 00000000..0b61abca --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/eu.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Korrika + cmd: Agindu +footer: + shortcuts: Erakutsi lasterbideak +header: + search: Aplikazioa, komandoa edo bidea +item: + open_location: Ireki fitxategiaren kokapena + pin: Pin kaia diff --git a/src/apps/seelen_rofi/i18n/translations/fa.yml b/src/apps/seelen_rofi/i18n/translations/fa.yml new file mode 100644 index 00000000..99b57513 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/fa.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: فرمان + explorer: دویدن +footer: + shortcuts: میانبرها را نشان دهید +header: + search: برنامه ، فرمان یا مسیر +item: + pin: پین به حوض + open_location: مکان فایل را باز کنید diff --git a/src/apps/seelen_rofi/i18n/translations/fi.yml b/src/apps/seelen_rofi/i18n/translations/fi.yml new file mode 100644 index 00000000..e881dd22 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/fi.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Juoksua + cmd: Komento +footer: + shortcuts: Näytä pikakuvakkeet +header: + search: Sovellus, komento tai polku +item: + pin: Pistä tippaan + open_location: Avaa tiedoston sijainti diff --git a/src/apps/seelen_rofi/i18n/translations/fr.yml b/src/apps/seelen_rofi/i18n/translations/fr.yml new file mode 100644 index 00000000..a2690557 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/fr.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Courir + cmd: Commande +footer: + shortcuts: Montrer les raccourcis +header: + search: Application, commande ou chemin +item: + pin: Épingle à docker + open_location: Emplacement du fichier ouvert diff --git a/src/apps/seelen_rofi/i18n/translations/gu.yml b/src/apps/seelen_rofi/i18n/translations/gu.yml new file mode 100644 index 00000000..e74a8558 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/gu.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: આદેશ આપવો + explorer: દોડવું +footer: + shortcuts: શોર્ટકટ્સ બતાવો +header: + search: એપ્લિકેશન, આદેશ અથવા પાથ +item: + open_location: ફાઇલ સ્થાન ખોલો + pin: ગોદી પર પિન કરવા માટે diff --git a/src/apps/seelen_rofi/i18n/translations/he.yml b/src/apps/seelen_rofi/i18n/translations/he.yml new file mode 100644 index 00000000..f2f4229d --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/he.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: לָרוּץ + cmd: פְּקוּדָה +footer: + shortcuts: הצג קיצורי דרך +header: + search: אפליקציה, פקודה או נתיב +item: + pin: סיכה לעגינה + open_location: פתח את מיקום הקובץ diff --git a/src/apps/seelen_rofi/i18n/translations/hi.yml b/src/apps/seelen_rofi/i18n/translations/hi.yml new file mode 100644 index 00000000..86723837 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/hi.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: दौड़ना + cmd: आज्ञा +footer: + shortcuts: शॉर्टकट दिखाओ +header: + search: ऐप, कमांड या पथ +item: + open_location: फ़ाइल के स्थान को खोलें + pin: पिन टू डॉक diff --git a/src/apps/seelen_rofi/i18n/translations/hr.yml b/src/apps/seelen_rofi/i18n/translations/hr.yml new file mode 100644 index 00000000..2137e63a --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/hr.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Naredba + explorer: Trčanje +footer: + shortcuts: Prikaži prečace +header: + search: Aplikacija, naredba ili put +item: + open_location: Otvorena lokacija datoteke + pin: Pin do pristaništa diff --git a/src/apps/seelen_rofi/i18n/translations/hu.yml b/src/apps/seelen_rofi/i18n/translations/hu.yml new file mode 100644 index 00000000..fa9e2668 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/hu.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Fut + cmd: Parancs +footer: + shortcuts: Mutassa meg a parancsikonokat +header: + search: Alkalmazás, parancs vagy elérési út +item: + pin: Pin a dokkolóhoz + open_location: Nyissa meg a fájl helyét diff --git a/src/apps/seelen_rofi/i18n/translations/hy.yml b/src/apps/seelen_rofi/i18n/translations/hy.yml new file mode 100644 index 00000000..bd690f74 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/hy.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Հրաման + explorer: Վազք +footer: + shortcuts: Show ուցադրել դյուրանցումները +header: + search: Ծրագիր, հրաման կամ ուղի +item: + pin: Քորոց դեպի նավահանգիստ + open_location: Բաց ֆայլի գտնվելու վայրը diff --git a/src/apps/seelen_rofi/i18n/translations/id.yml b/src/apps/seelen_rofi/i18n/translations/id.yml new file mode 100644 index 00000000..332b6d43 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/id.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Berlari + cmd: Memerintah +footer: + shortcuts: Tampilkan jalan pintas +header: + search: Aplikasi, perintah atau jalur +item: + open_location: Buka Lokasi File + pin: Pin untuk berlabuh diff --git a/src/apps/seelen_rofi/i18n/translations/is.yml b/src/apps/seelen_rofi/i18n/translations/is.yml new file mode 100644 index 00000000..bf240524 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/is.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Skipan + explorer: Hlaupa +footer: + shortcuts: Sýna flýtileiðir +header: + search: App, skipun eða slóð +item: + open_location: Opnaðu staðsetningu skráar + pin: Pinna við bryggju diff --git a/src/apps/seelen_rofi/i18n/translations/it.yml b/src/apps/seelen_rofi/i18n/translations/it.yml new file mode 100644 index 00000000..3b273229 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/it.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Comando + explorer: Correre +footer: + shortcuts: Mostra scorciatoie +header: + search: App, comando o percorso +item: + open_location: Apri la posizione del file + pin: Pin to Dock diff --git a/src/apps/seelen_rofi/i18n/translations/ja.yml b/src/apps/seelen_rofi/i18n/translations/ja.yml new file mode 100644 index 00000000..1bb6d650 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ja.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: 走る + cmd: 指示 +footer: + shortcuts: ショートカットを表示します +header: + search: アプリ、コマンド、またはパス +item: + pin: ドックにピン + open_location: ファイルの場所を開きます diff --git a/src/apps/seelen_rofi/i18n/translations/ka.yml b/src/apps/seelen_rofi/i18n/translations/ka.yml new file mode 100644 index 00000000..a49089b6 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ka.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: ბრძანება + explorer: სირბილი +footer: + shortcuts: აჩვენეთ მალსახმობები +header: + search: აპლიკაცია, ბრძანება ან გზა +item: + pin: Pin dock + open_location: გახსენით ფაილის ადგილმდებარეობა diff --git a/src/apps/seelen_rofi/i18n/translations/km.yml b/src/apps/seelen_rofi/i18n/translations/km.yml new file mode 100644 index 00000000..a25de78d --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/km.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: ការបង្គាប់ + explorer: ការរត់ +footer: + shortcuts: បង្ហាញផ្លូវកាត់ +header: + search: កម្មវិធីពាក្យបញ្ជាឬផ្លូវ +item: + open_location: បើកទីតាំងឯកសារ + pin: ម្ជុលទៅចត diff --git a/src/apps/seelen_rofi/i18n/translations/ko.yml b/src/apps/seelen_rofi/i18n/translations/ko.yml new file mode 100644 index 00000000..15caf4ed --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ko.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: 달리다 + cmd: 명령 +footer: + shortcuts: 쇼트 컷을 보여줍니다 +header: + search: 앱, 명령 또는 경로 +item: + open_location: 파일 위치를여십시오 + pin: 도킹하려면 핀 diff --git a/src/apps/seelen_rofi/i18n/translations/ku.yml b/src/apps/seelen_rofi/i18n/translations/ku.yml new file mode 100644 index 00000000..12a1fee2 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ku.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Ferman + explorer: Rev +footer: + shortcuts: Kurtefîlm nîşan bide +header: + search: App, emir an rê +item: + open_location: Cihê pelê vekirî + pin: Pin to Dock diff --git a/src/apps/seelen_rofi/i18n/translations/lb.yml b/src/apps/seelen_rofi/i18n/translations/lb.yml new file mode 100644 index 00000000..e859c9d5 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/lb.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Kommun de Kommentar + explorer: Lafe +footer: + shortcuts: Show Ofkiirzungen +header: + search: App, Kommando oder Wee +item: + open_location: Open Dateiplaz + pin: PIN op Dock diff --git a/src/apps/seelen_rofi/i18n/translations/lo.yml b/src/apps/seelen_rofi/i18n/translations/lo.yml new file mode 100644 index 00000000..a7e62939 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/lo.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: ແລ່ນ + cmd: ບັນຊາ +footer: + shortcuts: ສະແດງທາງລັດ +header: + search: ແອັບ, Command ຫຼື Path +item: + pin: PIN ໃຫ້ DOCK + open_location: ເປີດສະຖານທີ່ເອກະສານ diff --git a/src/apps/seelen_rofi/i18n/translations/lt.yml b/src/apps/seelen_rofi/i18n/translations/lt.yml new file mode 100644 index 00000000..0e68e8a9 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/lt.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Komanda + explorer: Bėgti +footer: + shortcuts: Rodyti nuorodas +header: + search: Programa, komanda ar kelias +item: + open_location: Atidarykite failo vietą + pin: PIN iki doko diff --git a/src/apps/seelen_rofi/i18n/translations/lv.yml b/src/apps/seelen_rofi/i18n/translations/lv.yml new file mode 100644 index 00000000..0926ae33 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/lv.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Izkropļot + cmd: Vadība +footer: + shortcuts: Rādīt īsceļus +header: + search: Lietotne, komanda vai ceļš +item: + pin: Piespraudiet piestātnei + open_location: Atveriet faila atrašanās vietu diff --git a/src/apps/seelen_rofi/i18n/translations/mk.yml b/src/apps/seelen_rofi/i18n/translations/mk.yml new file mode 100644 index 00000000..ee8a5b6d --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/mk.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Трчај + cmd: Команда +footer: + shortcuts: Покажете кратенки +header: + search: Апликација, команда или патека +item: + pin: Игла до пристаништето + open_location: Отворете ја локацијата на датотеката diff --git a/src/apps/seelen_rofi/i18n/translations/mn.yml b/src/apps/seelen_rofi/i18n/translations/mn.yml new file mode 100644 index 00000000..915f8e70 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/mn.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Тушаах + explorer: Урсгал +footer: + shortcuts: Товчлолыг харуул +header: + search: Апп, тушаал эсвэл зам +item: + open_location: Нээлттэй файлын байршлыг нээнэ үү + pin: Док руу зүү diff --git a/src/apps/seelen_rofi/i18n/translations/ms.yml b/src/apps/seelen_rofi/i18n/translations/ms.yml new file mode 100644 index 00000000..d49babcf --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ms.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Perintah + explorer: Jalankan +footer: + shortcuts: Tunjukkan pintasan +header: + search: Aplikasi, Perintah atau Laluan +item: + pin: Pin ke dok + open_location: Buka lokasi fail diff --git a/src/apps/seelen_rofi/i18n/translations/mt.yml b/src/apps/seelen_rofi/i18n/translations/mt.yml new file mode 100644 index 00000000..9d26b5bb --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/mt.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Ġirja + cmd: Kmand +footer: + shortcuts: Uri shortcuts +header: + search: App, kmand jew triq +item: + open_location: Post tal-fajl miftuħ + pin: Pin to Dock diff --git a/src/apps/seelen_rofi/i18n/translations/ne.yml b/src/apps/seelen_rofi/i18n/translations/ne.yml new file mode 100644 index 00000000..4f62558c --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ne.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: दगुर्ने काम + cmd: आदेश +footer: + shortcuts: सर्टकटहरू देखाउनुहोस् +header: + search: अनुप्रयोग, आदेश वा मार्ग +item: + pin: पिन गर्न पिन + open_location: फाइल स्थान खोल्नुहोस् diff --git a/src/apps/seelen_rofi/i18n/translations/nl.yml b/src/apps/seelen_rofi/i18n/translations/nl.yml new file mode 100644 index 00000000..a998ec72 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/nl.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Commando + explorer: Loop +footer: + shortcuts: Toon snelkoppelingen +header: + search: App, opdracht of pad +item: + pin: Pin om aan te meren + open_location: Open de bestandslocatie diff --git a/src/apps/seelen_rofi/i18n/translations/no.yml b/src/apps/seelen_rofi/i18n/translations/no.yml new file mode 100644 index 00000000..797602cd --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/no.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Kommando + explorer: Løp +footer: + shortcuts: Vis snarveier +header: + search: App, kommando eller sti +item: + pin: Pin til Dock + open_location: Åpen filplassering diff --git a/src/apps/seelen_rofi/i18n/translations/pa.yml b/src/apps/seelen_rofi/i18n/translations/pa.yml new file mode 100644 index 00000000..bb7e5780 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/pa.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: ਕਮਾਂਡ + explorer: ਚਲਾਓ +footer: + shortcuts: ਸ਼ਾਰਟਕੱਟ ਦਿਖਾਓ +header: + search: ਐਪ, ਕਮਾਂਡ ਜਾਂ ਮਾਰਗ +item: + pin: ਡੌਕ ਕਰਨ ਲਈ ਪਿੰਨ + open_location: ਓਪਨ ਫਾਈਲ ਟਿਕਾਣਾ diff --git a/src/apps/seelen_rofi/i18n/translations/pl.yml b/src/apps/seelen_rofi/i18n/translations/pl.yml new file mode 100644 index 00000000..b333e90a --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/pl.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Rozkaz + explorer: Uruchomić +footer: + shortcuts: Pokaż skróty +header: + search: Aplikacja, polecenie lub ścieżka +item: + open_location: Otwórz lokalizację pliku + pin: Pin do Dock diff --git a/src/apps/seelen_rofi/i18n/translations/ps.yml b/src/apps/seelen_rofi/i18n/translations/ps.yml new file mode 100644 index 00000000..956d1c57 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ps.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: منډه وهل + cmd: قومانده +footer: + shortcuts: لنډیزونه وښیه +header: + search: ایپ، قومانده یا لاره +item: + open_location: د فایل ځای خلاص کړئ + pin: پنبې پنبه diff --git a/src/apps/seelen_rofi/i18n/translations/pt.yml b/src/apps/seelen_rofi/i18n/translations/pt.yml new file mode 100644 index 00000000..a7b969fc --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/pt.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Correr + cmd: Comando +footer: + shortcuts: Mostre atalhos +header: + search: Aplicativo, comando ou caminho +item: + pin: Pin para doca + open_location: Localização do arquivo aberto diff --git a/src/apps/seelen_rofi/i18n/translations/ro.yml b/src/apps/seelen_rofi/i18n/translations/ro.yml new file mode 100644 index 00000000..f4c862af --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ro.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Comanda + explorer: Alerga +footer: + shortcuts: Afișați comenzile rapide +header: + search: Aplicație, comandă sau cale +item: + open_location: Deschideți locația fișierului + pin: Pin la doc diff --git a/src/apps/seelen_rofi/i18n/translations/ru.yml b/src/apps/seelen_rofi/i18n/translations/ru.yml new file mode 100644 index 00000000..bacac1ac --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ru.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Командование + explorer: Бегать +footer: + shortcuts: Показать ярлыки +header: + search: Приложение, команда или путь +item: + pin: Прикованная к пристани + open_location: Откройте местоположение файла diff --git a/src/apps/seelen_rofi/i18n/translations/si.yml b/src/apps/seelen_rofi/i18n/translations/si.yml new file mode 100644 index 00000000..4f6ea761 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/si.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: විධානය + explorer: දුවන්න +footer: + shortcuts: කෙටිමං පෙන්වන්න +header: + search: යෙදුම, අණ හෝ මාවත +item: + pin: තටාකයට පින් කරන්න + open_location: ගොනු ස්ථානය විවෘත කරන්න diff --git a/src/apps/seelen_rofi/i18n/translations/sk.yml b/src/apps/seelen_rofi/i18n/translations/sk.yml new file mode 100644 index 00000000..0077f5da --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/sk.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Príkaz + explorer: Spustenie +footer: + shortcuts: Zobraziť skratky +header: + search: Aplikácia, príkaz alebo cesta +item: + open_location: Otvorte umiestnenie súboru + pin: Pin do prístavu diff --git a/src/apps/seelen_rofi/i18n/translations/so.yml b/src/apps/seelen_rofi/i18n/translations/so.yml new file mode 100644 index 00000000..846e1ce4 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/so.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Amrid + explorer: Ordid +footer: + shortcuts: Muuji gaagaaban +header: + search: App, amar ama dariiqa +item: + pin: Pin to dock + open_location: Goobta faylka furan diff --git a/src/apps/seelen_rofi/i18n/translations/sr.yml b/src/apps/seelen_rofi/i18n/translations/sr.yml new file mode 100644 index 00000000..29c4c455 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/sr.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Трчати + cmd: Командант +footer: + shortcuts: Прикажи пречице +header: + search: Апликација, наредбу или стаза +item: + pin: ПИН за пристајање + open_location: Отворите локацију датотеке diff --git a/src/apps/seelen_rofi/i18n/translations/sv.yml b/src/apps/seelen_rofi/i18n/translations/sv.yml new file mode 100644 index 00000000..97a48295 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/sv.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Kommando + explorer: Sikt +footer: + shortcuts: Visa genvägar +header: + search: App, kommando eller sökväg +item: + open_location: Öppen filplats + pin: Kippa diff --git a/src/apps/seelen_rofi/i18n/translations/sw.yml b/src/apps/seelen_rofi/i18n/translations/sw.yml new file mode 100644 index 00000000..18fda2d1 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/sw.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Amri + explorer: Kukimbia +footer: + shortcuts: Onyesha njia za mkato +header: + search: Programu, amri au njia +item: + open_location: Fungua eneo la faili + pin: Piga kizimbani diff --git a/src/apps/seelen_rofi/i18n/translations/ta.yml b/src/apps/seelen_rofi/i18n/translations/ta.yml new file mode 100644 index 00000000..af4bc4dd --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ta.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: ஓடு + cmd: கட்டளை +footer: + shortcuts: குறுக்குவழிகளைக் காட்டு +header: + search: பயன்பாடு, கட்டளை அல்லது பாதை +item: + open_location: கோப்பு இருப்பிடத்தைத் திறக்கவும் + pin: கப்பல்துறைக்கு முள் diff --git a/src/apps/seelen_rofi/i18n/translations/te.yml b/src/apps/seelen_rofi/i18n/translations/te.yml new file mode 100644 index 00000000..a217d38d --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/te.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: రన్ + cmd: కమాండ్ +footer: + shortcuts: సత్వరమార్గాలను చూపించు +header: + search: అనువర్తనం, ఆదేశం లేదా మార్గం +item: + open_location: ఫైల్ స్థానం తెరవండి + pin: పిన్ టు డాక్ diff --git a/src/apps/seelen_rofi/i18n/translations/tg.yml b/src/apps/seelen_rofi/i18n/translations/tg.yml new file mode 100644 index 00000000..53c23d3e --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/tg.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Давидан + cmd: Фармон +footer: + shortcuts: Миёнабурҳоро нишон диҳед +header: + search: Барнома, фармон ё роҳ +item: + pin: PIN ба Dock + open_location: Кушодани макони кушод diff --git a/src/apps/seelen_rofi/i18n/translations/th.yml b/src/apps/seelen_rofi/i18n/translations/th.yml new file mode 100644 index 00000000..b1f4fa50 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/th.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: สั่งการ + explorer: วิ่ง +footer: + shortcuts: แสดงทางลัด +header: + search: แอพคำสั่งหรือเส้นทาง +item: + pin: พินไปที่ท่าเรือ + open_location: เปิดตำแหน่งไฟล์ diff --git a/src/apps/seelen_rofi/i18n/translations/tl.yml b/src/apps/seelen_rofi/i18n/translations/tl.yml new file mode 100644 index 00000000..a2f6bbef --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/tl.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Tumakbo + cmd: Utos +footer: + shortcuts: Ipakita ang mga shortcut +header: + search: App, utos o landas +item: + pin: Pin sa pantalan + open_location: Buksan ang lokasyon ng file diff --git a/src/apps/seelen_rofi/i18n/translations/tr.yml b/src/apps/seelen_rofi/i18n/translations/tr.yml new file mode 100644 index 00000000..2848d5ce --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/tr.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Emretmek + explorer: Koşmak +footer: + shortcuts: Kısayolları göster +header: + search: Uygulama, komut veya yol +item: + pin: Pin Dock + open_location: Dosya Konumu Aç diff --git a/src/apps/seelen_rofi/i18n/translations/uk.yml b/src/apps/seelen_rofi/i18n/translations/uk.yml new file mode 100644 index 00000000..ceabd0ae --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/uk.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Пробігати + cmd: Командування +footer: + shortcuts: Показати ярлики +header: + search: Додаток, команда або шлях +item: + pin: PIN до причалу + open_location: Розташування відкритого файлу diff --git a/src/apps/seelen_rofi/i18n/translations/ur.yml b/src/apps/seelen_rofi/i18n/translations/ur.yml new file mode 100644 index 00000000..10cfb44a --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/ur.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: حکم + explorer: چلائیں +footer: + shortcuts: شارٹ کٹ دکھائیں +header: + search: ایپ ، کمانڈ یا راستہ +item: + pin: گودی سے پن + open_location: فائل کا مقام کھولیں diff --git a/src/apps/seelen_rofi/i18n/translations/uz.yml b/src/apps/seelen_rofi/i18n/translations/uz.yml new file mode 100644 index 00000000..b41236af --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/uz.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Buyruq + explorer: Yugurish +footer: + shortcuts: Yorliqlarni ko'rsating +header: + search: Ilova, buyruq yoki yo'l +item: + pin: Dok uchun pin + open_location: Fayl joylashuvi diff --git a/src/apps/seelen_rofi/i18n/translations/vi.yml b/src/apps/seelen_rofi/i18n/translations/vi.yml new file mode 100644 index 00000000..e1408f8a --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/vi.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + explorer: Chạy + cmd: Yêu cầu +footer: + shortcuts: Hiển thị các phím tắt +header: + search: Ứng dụng, lệnh hoặc đường dẫn +item: + pin: Ghim vào bến + open_location: Mở vị trí tệp diff --git a/src/apps/seelen_rofi/i18n/translations/yo.yml b/src/apps/seelen_rofi/i18n/translations/yo.yml new file mode 100644 index 00000000..b37b4604 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/yo.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Paṣẹ + explorer: Sare +footer: + shortcuts: Ṣe awọn ọna abuja +header: + search: App, aṣẹ tabi ọna +item: + pin: Pin si Dock + open_location: Ṣii ipo faili ṣii diff --git a/src/apps/seelen_rofi/i18n/translations/zh.yml b/src/apps/seelen_rofi/i18n/translations/zh.yml new file mode 100644 index 00000000..b8eec495 --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/zh.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: 命令行 + explorer: 文件资源管理器 +footer: + shortcuts: 显示快捷方式 +header: + search: 应用,命令或路径 +item: + pin: 引脚到停靠 + open_location: 打开文件位置 diff --git a/src/apps/seelen_rofi/i18n/translations/zu.yml b/src/apps/seelen_rofi/i18n/translations/zu.yml new file mode 100644 index 00000000..6290543a --- /dev/null +++ b/src/apps/seelen_rofi/i18n/translations/zu.yml @@ -0,0 +1,11 @@ +app_launcher: + runners: + cmd: Juba + explorer: Gijima +footer: + shortcuts: Khombisa izinqamuleli +header: + search: Uhlelo lokusebenza, umyalo noma indlela +item: + open_location: Vula Indawo yefayela + pin: Pin to dock diff --git a/src/apps/seelen_rofi/index.tsx b/src/apps/seelen_rofi/index.tsx new file mode 100644 index 00000000..cbbd52e4 --- /dev/null +++ b/src/apps/seelen_rofi/index.tsx @@ -0,0 +1,33 @@ +import { createRoot } from 'react-dom/client'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { declareDocumentAsLayeredHitbox } from 'seelen-core'; + +import { initStore, store } from './modules/shared/store/infra'; + +import { getRootContainer } from '../shared'; +import { wrapConsole } from '../shared/ConsoleWrapper'; +import { App } from './App'; +import { registerDocumentEvents } from './events'; +import i18n, { loadTranslations } from './i18n'; + +import '../shared/styles/reset.css'; +import '../shared/styles/colors.css'; + +async function Main() { + wrapConsole(); + await declareDocumentAsLayeredHitbox(); + await loadTranslations(); + await initStore(); + registerDocumentEvents(); + + createRoot(getRootContainer()).render( + + + + + , + ); +} + +Main(); diff --git a/src/apps/seelen_rofi/modules/launcher/app.ts b/src/apps/seelen_rofi/modules/launcher/app.ts new file mode 100644 index 00000000..b774d2df --- /dev/null +++ b/src/apps/seelen_rofi/modules/launcher/app.ts @@ -0,0 +1,9 @@ +import { path } from '@tauri-apps/api'; +import { writeTextFile } from '@tauri-apps/plugin-fs'; +import yaml from 'js-yaml'; +import { LauncherHistory } from 'seelen-core'; + +export async function SaveHistory(history: LauncherHistory) { + const yaml_route = await path.join(await path.appDataDir(), 'history'); + await writeTextFile(yaml_route, yaml.dump(history)); +} diff --git a/src/apps/seelen_rofi/modules/launcher/infra/CommandInput.tsx b/src/apps/seelen_rofi/modules/launcher/infra/CommandInput.tsx new file mode 100644 index 00000000..f0e13214 --- /dev/null +++ b/src/apps/seelen_rofi/modules/launcher/infra/CommandInput.tsx @@ -0,0 +1,46 @@ +import { AutoComplete, Tooltip } from 'antd'; +import { KeyboardEventHandler, RefObject } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface CommandInputProps { + command: string; + setCommand: (value: string) => void; + showHistory: boolean; + setShowHistory: (value: boolean) => void; + matchingHistory: Array<{ value: string }>; + onInputKeyDown: KeyboardEventHandler; + inputRef: RefObject; + showHelp: boolean; +} + +export const CommandInput = ({ + command, + setCommand, + showHistory, + setShowHistory, + matchingHistory, + onInputKeyDown, + inputRef, + showHelp, +}: CommandInputProps) => { + const { t } = useTranslation(); + + return + + + + ; +}; \ No newline at end of file diff --git a/src/apps/seelen_rofi/modules/launcher/infra/Item.tsx b/src/apps/seelen_rofi/modules/launcher/infra/Item.tsx new file mode 100644 index 00000000..21dc9ebc --- /dev/null +++ b/src/apps/seelen_rofi/modules/launcher/infra/Item.tsx @@ -0,0 +1,59 @@ +import { convertFileSrc, invoke } from '@tauri-apps/api/core'; +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { Dropdown, Menu } from 'antd'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SeelenCommand } from 'seelen-core'; + +import { OverflowTooltip } from 'src/apps/shared/components/OverflowTooltip'; + +import { StartMenuApp } from '../../shared/store/domain'; + +export const Item = memo(({ item, hidden }: { item: StartMenuApp; hidden: boolean }) => { + const { label, icon, path } = item; + + const { t } = useTranslation(); + + function onClick() { + invoke(SeelenCommand.OpenFile, { path }); + getCurrentWindow().hide(); + } + + const shortPath = path.slice(path.indexOf('\\Programs\\') + 10); + + return ( + ( + + )} + > + + + ); +}); diff --git a/src/apps/seelen_rofi/modules/launcher/infra/RunnerSelector.tsx b/src/apps/seelen_rofi/modules/launcher/infra/RunnerSelector.tsx new file mode 100644 index 00000000..c59e55a4 --- /dev/null +++ b/src/apps/seelen_rofi/modules/launcher/infra/RunnerSelector.tsx @@ -0,0 +1,39 @@ +import { Select, Tooltip } from 'antd'; +import { forwardRef, RefObject } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface RunnerSelectorProps { + selectedRunner: number; + runners: Array<{ id: string; label: string }>; + setSelectedRunner: (value: number) => void; + helpRef: RefObject; + showHelp: boolean; +} + +export const RunnerSelector = forwardRef((props: RunnerSelectorProps, ref) => { + const { selectedRunner, runners, setSelectedRunner, helpRef, showHelp } = props; + + const { t } = useTranslation(); + + return ( + + + + + + + {t('app_launcher.runners.label')} + + {runners.map((runner, idx) => ( + + onChangeRunnerLabel(idx, e.target.value)} + /> + onChangeRunnerProgram(idx, e.target.value)} + /> + + + ))} + + + + + ); +} diff --git a/src/apps/settings/modules/ByMonitor/infra/index.module.css b/src/apps/settings/modules/ByMonitor/infra/index.module.css new file mode 100644 index 00000000..a31fe633 --- /dev/null +++ b/src/apps/settings/modules/ByMonitor/infra/index.module.css @@ -0,0 +1,24 @@ +.itemContainer { + display: flex; + align-items: center; + gap: 10px; + + .itemLeft { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 5px; + + .actions { + display: flex; + align-items: center; + gap: 5px; + width: 100%; + + > button { + flex: 1; + } + } + } +} diff --git a/src/apps/settings/modules/ByMonitor/infra/index.tsx b/src/apps/settings/modules/ByMonitor/infra/index.tsx new file mode 100644 index 00000000..7007685b --- /dev/null +++ b/src/apps/settings/modules/ByMonitor/infra/index.tsx @@ -0,0 +1,197 @@ +import { Button, Modal, Switch } from 'antd'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { MonitorConfiguration, Rect } from 'seelen-core'; + +import { WindowManagerSpacingSettings } from '../../WindowManager/main/infra/GlobalPaddings'; + +import { newSelectors, RootActions } from '../../shared/store/app/reducer'; +import { Monitor } from 'src/apps/settings/components/monitor'; +import { SettingsGroup, SettingsOption } from 'src/apps/settings/components/SettingsBox'; + +import cs from './index.module.css'; + +interface MonitorConfigProps { + index: number; + monitor: MonitorConfiguration; + onChange: (monitor: MonitorConfiguration) => void; + onDelete: () => void; + onInsert: () => void; +} + +interface MoreMonitorConfigProps { + index: number; + monitor: MonitorConfiguration; + onChange: (monitor: MonitorConfiguration) => void; +} + +export function MoreMonitorConfig({ index, monitor: m, onChange }: MoreMonitorConfigProps) { + const [open, setOpen] = useState(false); + const { t } = useTranslation(); + + function onChangeGap(v: number | null) { + onChange({ + ...m, + wm: { + ...m.wm, + gap: v ? Math.round(v) : null, + }, + }); + } + + function onChangePadding(v: number | null) { + onChange({ + ...m, + wm: { + ...m.wm, + padding: v ? Math.round(v) : null, + }, + }); + } + + function onChangeMargins(side: keyof Rect, v: number | null) { + onChange({ + ...m, + wm: { + ...m.wm, + margin: { + ...(m.wm.margin || new Rect()), + [side]: Math.round(v || 0), + }, + }, + }); + } + + function onClear() { + onChange({ + ...m, + wm: { + ...m.wm, + gap: null, + padding: null, + margin: null, + }, + }); + } + + return ( + <> + + setOpen(false)} + centered + footer={null} + > + + + + ); +} + +export function MonitorConfig({ + monitor: m, + index, + onChange, + onDelete, + onInsert, +}: MonitorConfigProps) { + const { t } = useTranslation(); + + function onToggle(key: string, value: boolean) { + onChange({ + ...m, + [key]: { + enabled: value, + }, + }); + } + + return ( + +
+
+ +
+ + + +
+
+ + + {t('toolbar.enable')} + onToggle('tb', v)} /> + + {/* + {t('wm.enable')} + onToggle('wm', v)} /> + */} + + {t('weg.enable')} + onToggle('weg', v)} /> + + {/* + {t('wall.enable')} + onToggle('wall', v)} /> + */} + +
+
+ ); +} + +export function SettingsByMonitor() { + const monitors = useSelector(newSelectors.monitors); + + const dispatch = useDispatch(); + + function onMonitorChange(idx: number, monitor: MonitorConfiguration) { + let newMonitors = [...monitors]; + newMonitors[idx] = monitor; + dispatch(RootActions.setMonitors(newMonitors)); + } + + function insertNewAfter(idx: number) { + let newMonitors = [...monitors]; + newMonitors.splice(idx + 1, 0, new MonitorConfiguration()); + dispatch(RootActions.setMonitors(newMonitors)); + } + + function onDelete(idx: number) { + let newMonitors = [...monitors]; + newMonitors.splice(idx, 1); + dispatch(RootActions.setMonitors(newMonitors)); + } + + return ( + <> + {monitors.map((m, idx) => ( + + ))} + + ); +} diff --git a/src/apps/settings/modules/Home/MiniStore.module.css b/src/apps/settings/modules/Home/MiniStore.module.css new file mode 100644 index 00000000..c45c9c72 --- /dev/null +++ b/src/apps/settings/modules/Home/MiniStore.module.css @@ -0,0 +1,21 @@ +.title { + font-size: 1.1rem; + font-weight: 600; + margin: 0 10px 10px; +} + +.miniStore { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(125px, 1fr)); + gap: 10px; + + .product { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 6px; + background-color: var(--color-gray-50); + border-radius: 8px; + } +} diff --git a/src/apps/settings/modules/Home/MiniStore.tsx b/src/apps/settings/modules/Home/MiniStore.tsx new file mode 100644 index 00000000..5958f598 --- /dev/null +++ b/src/apps/settings/modules/Home/MiniStore.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from 'antd'; +import { useState } from 'react'; + +import cs from './MiniStore.module.css'; + +export function ProductSkeleton() { + return ( +
+ + +
+ ); +} + +export function MiniStore() { + const [products, _setProducts] = useState([]); + + return ( + <> +

New Resources

+
+ {products.length === 0 && + Array.from({ length: 10 }).map((_, i) => )} +
+ + ); +} diff --git a/src/apps/settings/modules/Home/News.module.css b/src/apps/settings/modules/Home/News.module.css new file mode 100644 index 00000000..0952daa1 --- /dev/null +++ b/src/apps/settings/modules/Home/News.module.css @@ -0,0 +1,80 @@ +.notices { + width: 100%; + height: 50vh; + gap: 10px; + display: grid; + grid-template-rows: 1fr min-content; + + .notice { + width: 100%; + height: 100%; + position: relative; + border-radius: 10px; + overflow: hidden; + + .image { + min-width: 100%; + height: 100%; + object-fit: cover; + background-color: var(--color-gray-300); + } + + .content { + z-index: 1; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: min-content min-content; + padding: 60px 20px 10px 20px; + background: linear-gradient(transparent, #0002 100%); + backdrop-filter: blur(1px); + color: white; + + .title { + font-size: 1.4rem; + font-weight: 600; + } + + .message { + display: block; + text-overflow: ellipsis; + word-wrap: break-word; + overflow: hidden; + font-size: 0.9rem; + max-height: 3.6em; + line-height: 1.8em; + } + + .linkButton { + grid-column: 2 / 3; + grid-row: 1 / 3; + display: flex; + align-items: center; + justify-content: flex-end; + } + } + } + + .pagination { + align-self: center; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + + .paginationDot { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--color-gray-400); + transition: background-color 0.2s ease-in-out; + + &.active { + background-color: var(--config-accent-color); + } + } + } +} diff --git a/src/apps/settings/modules/Home/News.tsx b/src/apps/settings/modules/Home/News.tsx new file mode 100644 index 00000000..520709c6 --- /dev/null +++ b/src/apps/settings/modules/Home/News.tsx @@ -0,0 +1,117 @@ +import { Button, Skeleton } from 'antd'; +import { useAnimate } from 'framer-motion'; +import { useEffect, useState } from 'react'; +import { useInterval } from 'seelen-core'; + +import { cx } from 'src/apps/shared/styles'; + +import cs from './News.module.css'; + +const BASE_NEWS_URL = 'https://raw.githubusercontent.com/Seelen-Inc/slu-blog/refs/heads/main/news'; + +async function getNewNames(): Promise { + let response = await fetch(BASE_NEWS_URL + '/show_on_app.json'); + + if (response.ok) { + let data = await response.json(); + return data; + } + + return []; +} + +interface New { + title: string; + message: string; + url: string; + image: string; +} + +async function getNew(name: string): Promise { + try { + let response = await fetch(`${BASE_NEWS_URL}/${name}/metadata.json`); + if (response.ok) { + let data: New = await response.json(); + data.image = `${BASE_NEWS_URL}/${name}/image.png`; + return data; + } + } catch (error) { + return null; + } + return null; +} + +export function NoticeSlider() { + const [news, setNews] = useState([]); + const [currentIdx, setCurrentIdx] = useState(0); + + const [scope, animate] = useAnimate(); + + useEffect(() => { + async function fetchData() { + let names = await getNewNames(); + let news = await Promise.all(names.map((name) => getNew(name))); + setNews(news.filter((item) => item !== null) as New[]); + } + fetchData(); + }); + + useInterval( + () => { + animate(scope.current, { opacity: 0 }).then(() => { + setCurrentIdx((v) => v + 1); + animate(scope.current, { opacity: 1 }); + }); + }, + 10000, + [currentIdx], + ); + + let current = news[currentIdx % news.length]; + + return ( +
+
+ {current ? ( + <> + {current.title} +
+

{current.title}

+

{current.message}

+
+ +
+
+ + ) : ( + <> +
+
+ + +
+ +
+
+ + )} +
+
+ {news.map((_item, index) => ( +
{ + animate(scope.current, { opacity: 0 }).then(() => { + setCurrentIdx(index); + animate(scope.current, { opacity: 1 }); + }); + }} + /> + ))} +
+
+ ); +} diff --git a/src/apps/settings/modules/Home/index.module.css b/src/apps/settings/modules/Home/index.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/apps/settings/modules/Home/index.tsx b/src/apps/settings/modules/Home/index.tsx new file mode 100644 index 00000000..1a1d694b --- /dev/null +++ b/src/apps/settings/modules/Home/index.tsx @@ -0,0 +1,11 @@ +import { MiniStore } from './MiniStore'; +import { NoticeSlider } from './News'; + +export function Home() { + return ( + <> + + + + ); +} diff --git a/src/apps/settings/modules/StartUser/infra.tsx b/src/apps/settings/modules/StartUser/infra.tsx index f1fdebc9..c15fbd47 100644 --- a/src/apps/settings/modules/StartUser/infra.tsx +++ b/src/apps/settings/modules/StartUser/infra.tsx @@ -1,35 +1,35 @@ -import i18n from '../../i18n'; -import { Modal } from 'antd'; - -import { SaveStore, store } from '../shared/store/infra'; -import { startup } from '../shared/tauri/infra'; - -import { RootActions } from '../shared/store/app/reducer'; - -import cs from './index.module.css'; - -export const StartUser = () => { - startup.enable(); - store.dispatch(RootActions.setAutostart(true)); - - const modal = Modal.confirm({ - title: i18n.t('start.title'), - className: cs.welcome, - content: ( -
-

- {i18n.t('start.message')} -

- {i18n.t('start.message_accent')} -
- ), - okText: 'Continue', - onOk: () => { - SaveStore(); - modal.destroy(); - }, - icon:
🎉
, - cancelButtonProps: { style: { display: 'none' } }, - centered: true, - }); -}; +import { Modal } from 'antd'; + +import { SaveStore, store } from '../shared/store/infra'; +import { startup } from '../shared/tauri/infra'; + +import { RootActions } from '../shared/store/app/reducer'; + +import i18n from '../../i18n'; +import cs from './index.module.css'; + +export const StartUser = () => { + startup.enable(); + store.dispatch(RootActions.setAutostart(true)); + + const modal = Modal.confirm({ + title: i18n.t('start.title'), + className: cs.welcome, + content: ( +
+

+ {i18n.t('start.message')} +

+ {i18n.t('start.message_accent')} +
+ ), + okText: 'Continue', + onOk: () => { + SaveStore(); + modal.destroy(); + }, + icon:
🎉
, + cancelButtonProps: { style: { display: 'none' } }, + centered: true, + }); +}; diff --git a/src/apps/settings/modules/Wall/index.module.css b/src/apps/settings/modules/Wall/index.module.css new file mode 100644 index 00000000..b47d4278 --- /dev/null +++ b/src/apps/settings/modules/Wall/index.module.css @@ -0,0 +1,34 @@ +.backgroundList { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 8px; + + .background { + display: grid; + align-items: center; + padding: 8px; + gap: 8px; + border-radius: 8px; + grid-template-columns: 100px 1fr 40px; + background-color: var(--color-white); + + .image { + border-radius: 8px; + height: 60px; + } + + .video { + height: 60px; + padding: 0 40px; + border-radius: 8px; + background-color: var(--color-gray-200); + } + } +} + +.backgroundAdd { + align-self: flex-end; + width: 40px; + margin-right: 8px; +} diff --git a/src/apps/settings/modules/Wall/infra.tsx b/src/apps/settings/modules/Wall/infra.tsx new file mode 100644 index 00000000..f55fd5cd --- /dev/null +++ b/src/apps/settings/modules/Wall/infra.tsx @@ -0,0 +1,117 @@ +import { convertFileSrc } from '@tauri-apps/api/core'; +import { Button, InputNumber, Switch } from 'antd'; +import { Reorder } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { SeelenWallWallpaper } from 'seelen-core'; + +import { dialog } from '../shared/tauri/infra'; + +import { newSelectors, RootActions } from '../shared/store/app/reducer'; +import { Icon } from 'src/apps/shared/components/Icon'; + +import { SettingsGroup, SettingsOption } from '../../components/SettingsBox'; +import cs from './index.module.css'; + +export function WallSettings() { + const wall = useSelector(newSelectors.wall); + const { enabled, backgrounds, interval } = wall; + + const dispatch = useDispatch(); + const { t } = useTranslation(); + + function onChangeEnabled(value: boolean) { + dispatch(RootActions.setWall({ ...wall, enabled: value })); + } + + function onChangeInterval(interval: number | null) { + if (interval) { + dispatch(RootActions.setWall({ ...wall, interval })); + } + } + + function onChangeBackgrounds(backgrounds: SeelenWallWallpaper[]) { + dispatch(RootActions.setWall({ ...wall, backgrounds })); + } + + async function onAddBackgrounds() { + let newBackgrounds: SeelenWallWallpaper[] = []; + + const files = await dialog.open({ + multiple: true, + title: t('wall.select'), + filters: [ + { name: 'Media', extensions: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'mp4', 'mkv', 'wav'] }, + ], + }); + + if (!files) { + return; + } + + for (const file of [files].flat()) { + newBackgrounds.push({ ...new SeelenWallWallpaper(), path: file }); + } + + onChangeBackgrounds([...backgrounds, ...newBackgrounds]); + } + + function onRemoveBackground(idx: number) { + let newBackgrounds = [...backgrounds]; + newBackgrounds.splice(idx, 1); + onChangeBackgrounds(newBackgrounds); + } + + return ( + <> + + + {t('wall.enable')} + + + + {t('wall.interval')} + + + + + + {t('wall.backgrounds')} + {!!backgrounds.length && ( + + {backgrounds.map((bg, idx) => { + let is_video = ['mp4', 'mkv', 'wav'].some((ext) => bg.path.endsWith(ext)); + + return ( + + {is_video ? ( +
+ +
+ ) : ( + + )} + {bg.path.split('\\').pop()} + +
+ ); + })} +
+ )} + +
{!backgrounds.length && t('wall.no_background')}
+ +
+
+ + ); +} diff --git a/src/apps/settings/modules/WindowManager/border/app.ts b/src/apps/settings/modules/WindowManager/border/app.ts index 617399e4..390d084e 100644 --- a/src/apps/settings/modules/WindowManager/border/app.ts +++ b/src/apps/settings/modules/WindowManager/border/app.ts @@ -1,16 +1,15 @@ -import { parseAsCamel } from '../../../../shared/schemas'; -import { Border, BorderSchema } from '../../../../shared/schemas/WindowManager'; -import { createSlice } from '@reduxjs/toolkit'; - -import { reducersFor, selectorsFor } from '../../shared/utils/app'; - -const initialState: Border = parseAsCamel(BorderSchema, {}); - -export const BorderSlice = createSlice({ - name: 'windowManager/border', - initialState, - reducers: reducersFor(initialState), - selectors: selectorsFor(initialState), -}); - -export const BorderActions = BorderSlice.actions; \ No newline at end of file +import { createSlice } from '@reduxjs/toolkit'; +import { Border } from 'seelen-core'; + +import { reducersFor, selectorsFor } from '../../shared/utils/app'; + +const initialState = new Border(); + +export const BorderSlice = createSlice({ + name: 'windowManager/border', + initialState, + reducers: reducersFor(initialState), + selectors: selectorsFor(initialState), +}); + +export const BorderActions = BorderSlice.actions; diff --git a/src/apps/settings/modules/WindowManager/border/infra.tsx b/src/apps/settings/modules/WindowManager/border/infra.tsx index e63bd040..24a3d873 100644 --- a/src/apps/settings/modules/WindowManager/border/infra.tsx +++ b/src/apps/settings/modules/WindowManager/border/infra.tsx @@ -1,49 +1,50 @@ -import { SettingsOption, SettingsSubGroup } from '../../../components/SettingsBox'; -import { InputNumber, Switch } from 'antd'; -import { useTranslation } from 'react-i18next'; - -import { useAppDispatch, useAppSelector, useDispatchCallback } from '../../shared/utils/infra'; - -import { BorderSelectors } from '../../shared/store/app/selectors'; -import { BorderActions } from './app'; - -export const BorderSettings = () => { - const enabled = useAppSelector(BorderSelectors.enabled); - const offset = useAppSelector(BorderSelectors.offset); - const width = useAppSelector(BorderSelectors.width); - - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const toggleEnabled = useDispatchCallback((value: boolean) => { - dispatch(BorderActions.setEnabled(value)); - }); - - const updateOffset = useDispatchCallback((value: number | null) => { - dispatch(BorderActions.setOffset(value || 0)); - }); - - const updateWidth = useDispatchCallback((value: number | null) => { - dispatch(BorderActions.setWidth(value || 0)); - }); - - return ( - - {t('wm.border.enable')} - - - } - > - - {t('wm.border.offset')} - - - - {t('wm.border.width')} - - - - ); -}; +import { InputNumber, Switch } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import { useAppDispatch, useAppSelector, useDispatchCallback } from '../../shared/utils/infra'; + +import { BorderSelectors } from '../../shared/store/app/selectors'; +import { BorderActions } from './app'; + +import { SettingsOption, SettingsSubGroup } from '../../../components/SettingsBox'; + +export const BorderSettings = () => { + const enabled = useAppSelector(BorderSelectors.enabled); + const offset = useAppSelector(BorderSelectors.offset); + const width = useAppSelector(BorderSelectors.width); + + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const toggleEnabled = useDispatchCallback((value: boolean) => { + dispatch(BorderActions.setEnabled(value)); + }); + + const updateOffset = useDispatchCallback((value: number | null) => { + dispatch(BorderActions.setOffset(value || 0)); + }); + + const updateWidth = useDispatchCallback((value: number | null) => { + dispatch(BorderActions.setWidth(value || 0)); + }); + + return ( + + {t('wm.border.enable')} + + + } + > + + {t('wm.border.offset')} + + + + {t('wm.border.width')} + + + + ); +}; diff --git a/src/apps/settings/modules/WindowManager/main/app.ts b/src/apps/settings/modules/WindowManager/main/app.ts index 4380778e..085d4cf8 100644 --- a/src/apps/settings/modules/WindowManager/main/app.ts +++ b/src/apps/settings/modules/WindowManager/main/app.ts @@ -1,25 +1,25 @@ -import { parseAsCamel } from '../../../../shared/schemas'; -import { WindowManager, WindowManagerSchema } from '../../../../shared/schemas/WindowManager'; -import { createSlice } from '@reduxjs/toolkit'; - -import { matcher, reducersFor, selectorsFor } from '../../shared/utils/app'; -import { BorderSlice } from '../border/app'; - -let initialState: WindowManager = parseAsCamel(WindowManagerSchema, {}); - -export const SeelenManagerSlice = createSlice({ - name: 'windowManager', - initialState, - selectors: selectorsFor(initialState), - reducers: { - ...reducersFor(initialState), - }, - extraReducers: (builder) => { - builder - .addMatcher(matcher(BorderSlice), (state, action) => { - state.border = BorderSlice.reducer(state.border, action); - }); - }, -}); - -export const WManagerSettingsActions = SeelenManagerSlice.actions; +import { createSlice } from '@reduxjs/toolkit'; + +import { matcher, reducersFor, selectorsFor } from '../../shared/utils/app'; +import { BorderSlice } from '../border/app'; + +import { WindowManagerSettings } from '../../../../../../lib/src/state'; + +let initialState = new WindowManagerSettings(); + +export const SeelenManagerSlice = createSlice({ + name: 'windowManager', + initialState, + selectors: selectorsFor(initialState), + reducers: { + ...reducersFor(initialState), + }, + extraReducers: (builder) => { + builder + .addMatcher(matcher(BorderSlice), (state, action) => { + state.border = BorderSlice.reducer(state.border, action); + }); + }, +}); + +export const WManagerSettingsActions = SeelenManagerSlice.actions; diff --git a/src/apps/settings/modules/WindowManager/main/infra/GlobalPaddings.tsx b/src/apps/settings/modules/WindowManager/main/infra/GlobalPaddings.tsx index 6202621e..d4aedac7 100644 --- a/src/apps/settings/modules/WindowManager/main/infra/GlobalPaddings.tsx +++ b/src/apps/settings/modules/WindowManager/main/infra/GlobalPaddings.tsx @@ -1,69 +1,132 @@ -import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../../../components/SettingsBox'; -import { InputNumber } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; - -import { useAppSelector } from '../../../shared/utils/infra'; - -import { SeelenWmSelectors } from '../../../shared/store/app/selectors'; -import { Rect } from '../../../shared/utils/app/Rect'; -import { WManagerSettingsActions } from '../app'; - -export const GlobalPaddings = () => { - const workspaceGap = useAppSelector(SeelenWmSelectors.workspaceGap); - const workspacePadding = useAppSelector(SeelenWmSelectors.workspacePadding); - const workAreaOffset = useAppSelector(SeelenWmSelectors.globalWorkAreaOffset); - - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const onChangeGlobalOffset = (side: keyof Rect, value: number | null) => { - dispatch( - WManagerSettingsActions.setGlobalWorkAreaOffset({ - ...workAreaOffset, - [side]: value || 0, - }), - ); - }; - - const onChangeDefaultGap = (value: number | null) => { - dispatch(WManagerSettingsActions.setWorkspaceGap(value || 0)); - }; - - const onChangeDefaultPadding = (value: number | null) => { - dispatch(WManagerSettingsActions.setWorkspacePadding(value || 0)); - }; - - return ( - -
- - {t('wm.space_between_containers')} - - - - {t('wm.workspace_padding')} - - -
- - - {t('sides.left')} - - - - {t('sides.top')} - - - - {t('sides.right')} - - - - {t('sides.bottom')} - - - -
- ); -}; +import { Button, InputNumber } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { useAppSelector } from '../../../shared/utils/infra'; + +import { SeelenWmSelectors } from '../../../shared/store/app/selectors'; +import { Rect } from '../../../shared/utils/app/Rect'; +import { WManagerSettingsActions } from '../app'; +import { Icon } from 'src/apps/shared/components/Icon'; + +import { + SettingsGroup, + SettingsOption, + SettingsSubGroup, +} from '../../../../components/SettingsBox'; + +export const GlobalPaddings = () => { + const workspaceGap = useAppSelector(SeelenWmSelectors.workspaceGap); + const workspacePadding = useAppSelector(SeelenWmSelectors.workspacePadding); + const workAreaOffset = useAppSelector(SeelenWmSelectors.workspaceMargin); + + const dispatch = useDispatch(); + + const onChangeGlobalOffset = (side: keyof Rect, value: number | null) => { + dispatch( + WManagerSettingsActions.setWorkspaceMargin({ + ...workAreaOffset, + [side]: Math.round(value || 0), + }), + ); + }; + + const onChangeDefaultGap = (value: number | null) => { + dispatch(WManagerSettingsActions.setWorkspaceGap(Math.round(value || 0))); + }; + + const onChangeDefaultPadding = (value: number | null) => { + dispatch(WManagerSettingsActions.setWorkspacePadding(Math.round(value || 0))); + }; + + return ( + + ); +}; + +interface WindowManagerSpacingSettings { + gap: number | null; + padding: number | null; + margins: Rect | null; + onChangeGap: (v: number | null) => void; + onChangePadding: (v: number | null) => void; + onChangeMargins: (side: keyof Rect, value: number | null) => void; + onClear?: () => void; +} + +export function WindowManagerSpacingSettings(props: WindowManagerSpacingSettings) { + const { gap, padding, margins, onChangeGap, onChangePadding, onChangeMargins, onClear } = props; + + const { t } = useTranslation(); + + return ( + + {onClear && ( + + {t('header.labels.seelen_wm')} + + + )} + + {t('wm.space_between_containers')} + + + + {t('wm.workspace_padding')} + + + + + {t('sides.left')} + + + + {t('sides.top')} + + + + {t('sides.right')} + + + + {t('sides.bottom')} + + + + + ); +} diff --git a/src/apps/settings/modules/WindowManager/main/infra/Others.tsx b/src/apps/settings/modules/WindowManager/main/infra/Others.tsx index 7ecb8ebb..754de983 100644 --- a/src/apps/settings/modules/WindowManager/main/infra/Others.tsx +++ b/src/apps/settings/modules/WindowManager/main/infra/Others.tsx @@ -1,31 +1,32 @@ -import { SettingsGroup, SettingsOption } from '../../../../components/SettingsBox'; -import { InputNumber } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; - -import { useAppSelector } from '../../../shared/utils/infra'; - -import { SeelenWmSelectors } from '../../../shared/store/app/selectors'; -import { WManagerSettingsActions } from '../app'; - -export const OthersConfigs = () => { - const resizeDelta = useAppSelector(SeelenWmSelectors.resizeDelta); - - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const onChangeResizeDelta = (value: number | null) => { - dispatch(WManagerSettingsActions.setResizeDelta(value || 0)); - }; - - return ( - <> - - - {t('wm.resize_delta')} - - - - - ); -}; +import { InputNumber } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { useAppSelector } from '../../../shared/utils/infra'; + +import { SeelenWmSelectors } from '../../../shared/store/app/selectors'; +import { WManagerSettingsActions } from '../app'; + +import { SettingsGroup, SettingsOption } from '../../../../components/SettingsBox'; + +export const OthersConfigs = () => { + const resizeDelta = useAppSelector(SeelenWmSelectors.resizeDelta); + + const dispatch = useDispatch(); + const { t } = useTranslation(); + + const onChangeResizeDelta = (value: number | null) => { + dispatch(WManagerSettingsActions.setResizeDelta(value || 0)); + }; + + return ( + <> + + + {t('wm.resize_delta')} + + + + + ); +}; diff --git a/src/apps/settings/modules/WindowManager/main/infra/index.tsx b/src/apps/settings/modules/WindowManager/main/infra/index.tsx index 4cf0435d..c16ca5f3 100644 --- a/src/apps/settings/modules/WindowManager/main/infra/index.tsx +++ b/src/apps/settings/modules/WindowManager/main/infra/index.tsx @@ -1,12 +1,9 @@ -import { VirtualDesktopStrategy } from '../../../../../shared/schemas/Settings'; -import { SettingsGroup, SettingsOption } from '../../../../components/SettingsBox'; -import { GlobalPaddings } from './GlobalPaddings'; -import { OthersConfigs } from './Others'; import { invoke } from '@tauri-apps/api/core'; import { Alert, Button, ConfigProvider, Select, Switch } from 'antd'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; +import { VirtualDesktopStrategy } from 'seelen-core'; import { BorderSettings } from '../../border/infra'; @@ -14,6 +11,10 @@ import { newSelectors, RootActions } from '../../../shared/store/app/reducer'; import { RootSelectors } from '../../../shared/store/app/selectors'; import { WManagerSettingsActions } from '../app'; +import { SettingsGroup, SettingsOption } from '../../../../components/SettingsBox'; +import { GlobalPaddings } from './GlobalPaddings'; +import { OthersConfigs } from './Others'; + export function WindowManagerSettings() { const [isWinVerSupported, setIsWinVerSupported] = useState(false); diff --git a/src/apps/settings/modules/appsConfigurations/app/reducer.ts b/src/apps/settings/modules/appsConfigurations/app/reducer.ts index 5ae55002..81e3802b 100644 --- a/src/apps/settings/modules/appsConfigurations/app/reducer.ts +++ b/src/apps/settings/modules/appsConfigurations/app/reducer.ts @@ -1,45 +1,44 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -import { AppConfiguration } from '../domain'; - -const initialState: AppConfiguration[] = []; - -interface AppPayload { - idx: number; -} - -export const AppsConfigSlice = createSlice({ - name: 'monitors', - initialState, - reducers: { - delete: (state, action: PayloadAction) => { - state.splice(action.payload, 1); - }, - deleteMany: (state, action: PayloadAction) => { - const newState: any[] = [...state]; - action.payload.forEach((key) => { - newState[key] = undefined; - }); - return newState.filter(Boolean); - }, - push: (state, action: PayloadAction) => { - state.push(...action.payload); - }, - replace: (state, action: PayloadAction) => { - const { idx, app } = action.payload; - state[idx] = app; - }, - swap: (state, action: PayloadAction<[number, number]>) => { - const [idx1, idx2] = action.payload; - const App1 = state[idx1]; - const App2 = state[idx2]; - - if (App1 && App2) { - state[idx1] = App2; - state[idx2] = App1; - } - }, - }, -}); - -export const AppsConfigActions = AppsConfigSlice.actions; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { AppConfiguration } from 'seelen-core'; + +const initialState: AppConfiguration[] = []; + +interface AppPayload { + idx: number; +} + +export const AppsConfigSlice = createSlice({ + name: 'monitors', + initialState, + reducers: { + delete: (state, action: PayloadAction) => { + state.splice(action.payload, 1); + }, + deleteMany: (state, action: PayloadAction) => { + const newState: any[] = [...state]; + action.payload.forEach((key) => { + newState[key] = undefined; + }); + return newState.filter(Boolean); + }, + push: (state, action: PayloadAction) => { + state.push(...action.payload); + }, + replace: (state, action: PayloadAction) => { + const { idx, app } = action.payload; + state[idx] = app; + }, + swap: (state, action: PayloadAction<[number, number]>) => { + const [idx1, idx2] = action.payload; + const App1 = state[idx1]; + const App2 = state[idx2]; + + if (App1 && App2) { + state[idx1] = App2; + state[idx2] = App1; + } + }, + }, +}); + +export const AppsConfigActions = AppsConfigSlice.actions; diff --git a/src/apps/settings/modules/appsConfigurations/domain.ts b/src/apps/settings/modules/appsConfigurations/domain.ts index 0f91d23e..5bdaf005 100644 --- a/src/apps/settings/modules/appsConfigurations/domain.ts +++ b/src/apps/settings/modules/appsConfigurations/domain.ts @@ -1,63 +1,16 @@ -import { IdWithIdentifier } from '../../../shared/schemas/AppsConfigurations'; +import { AppConfiguration, AppExtraFlag } from 'seelen-core'; export enum WmApplicationOptions { - Float = 'float', - Unmanage = 'unmanage', - ForceManage = 'force', - Pinned = 'pinned', + Float = `${AppExtraFlag.Float}`, + Unmanage = `${AppExtraFlag.Unmanage}`, + ForceManage = `${AppExtraFlag.Force}`, + Pinned = `${AppExtraFlag.Pinned}`, } export enum WegApplicationOptions { - Hidden = 'hidden', -} - -export enum ApplicationIdentifier { - Exe = 'Exe', - Class = 'Class', - Title = 'Title', - Path = 'Path', -} - -export enum MatchingStrategy { - Legacy = 'Legacy', - Equals = 'Equals', - StartsWith = 'StartsWith', - EndsWith = 'EndsWith', - Contains = 'Contains', - Regex = 'Regex', -} - -export interface AppConfiguration { - name: string; - category: string | null; - workspace: string | null; - monitor: number | null; - identifier: IdWithIdentifier; - isBundled: boolean; - options: Array; + Hidden = `${AppExtraFlag.Hidden}`, } export interface AppConfigurationExtended extends AppConfiguration { key: number; } - -export class AppConfiguration { - static default(): AppConfiguration { - return { - name: 'New App', - category: null, - workspace: null, - monitor: null, - identifier: { - id: 'new-app.exe', - kind: ApplicationIdentifier.Exe, - matchingStrategy: MatchingStrategy.Equals, - negation: false, - and: [], - or: [], - }, - isBundled: false, - options: [], - }; - } -} diff --git a/src/apps/settings/modules/appsConfigurations/infra/EditModal.tsx b/src/apps/settings/modules/appsConfigurations/infra/EditModal.tsx index c4148454..380ca8a7 100644 --- a/src/apps/settings/modules/appsConfigurations/infra/EditModal.tsx +++ b/src/apps/settings/modules/appsConfigurations/infra/EditModal.tsx @@ -1,22 +1,17 @@ -import { IdWithIdentifier } from '../../../../shared/schemas/AppsConfigurations'; -import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../../components/SettingsBox'; -import { Identifier } from './Identifier'; import { createSelector } from '@reduxjs/toolkit'; import { ConfigProvider, Input, Modal, Select, Switch } from 'antd'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import { AppConfiguration, AppExtraFlag, AppIdentifier } from 'seelen-core'; import { ownSelector, RootSelectors } from '../../shared/store/app/selectors'; import { RootState } from '../../shared/store/domain'; -import { - AppConfiguration, - AppConfigurationExtended, - WegApplicationOptions, - WmApplicationOptions, -} from '../domain'; +import { AppConfigurationExtended, WegApplicationOptions, WmApplicationOptions } from '../domain'; +import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../../components/SettingsBox'; +import { Identifier } from './Identifier'; import cs from './index.module.css'; interface Props { @@ -30,7 +25,7 @@ interface Props { const getAppSelector = (idx: number | undefined, isNew: boolean) => createSelector([ownSelector], (state: RootState) => { - return idx != null && !isNew ? state.appsConfigurations[idx]! : AppConfiguration.default(); + return idx != null && !isNew ? state.appsConfigurations[idx]! : AppConfiguration.placeholder(); }); export const EditAppModal = ({ idx, onCancel, onSave, isNew, open, readonlyApp }: Props) => { @@ -59,20 +54,23 @@ export const EditAppModal = ({ idx, onCancel, onSave, isNew, open, readonlyApp } const updateCategory = (e: React.ChangeEvent) => setApp({ ...app, category: e.target.value || null }); - const onChangeIdentifier = (identifier: IdWithIdentifier) => setApp({ ...app, identifier }); + const onChangeIdentifier = (identifier: AppIdentifier) => setApp({ ...app, identifier }); - const onSelectMonitor = (value: number | null) => setApp({ ...app, monitor: value }); - const onSelectWorkspace = (value: string | null) => setApp({ ...app, workspace: value }); + const onSelectMonitor = (value: number | null) => setApp({ ...app, boundMonitor: value }); + const onSelectWorkspace = (value: number | null) => setApp({ ...app, boundWorkspace: value }); - const onChangeOption = (option: WmApplicationOptions | WegApplicationOptions, checked: boolean) => { - setApp({ ...app, options: checked ? [...app.options, option] : app.options.filter((o) => o !== option) }); + const onChangeOption = (option: AppExtraFlag, checked: boolean) => { + setApp({ + ...app, + options: checked ? [...app.options, option] : app.options.filter((o) => o !== option), + }); }; const monitorsOptions = monitors.map((_, i) => ({ label: `Monitor ${i + 1}`, value: i })); - const workspaceOptions = - app.monitor != null && monitors[app.monitor] - ? monitors[app.monitor]?.workspaces.map(({ name }) => ({ label: name, value: name })) - : []; + const workspaceOptions = Array.from({ length: 10 }).map((_, i) => ({ + label: `Workspace ${i + 1}`, + value: i, + })); let title = t('apps_configurations.app.title_edit'); let okText = t('apps_configurations.app.ok_edit'); @@ -108,20 +106,18 @@ export const EditAppModal = ({ idx, onCancel, onSave, isNew, open, readonlyApp } )} -
- - {t('apps_configurations.app.name')} - - - - {t('apps_configurations.app.category')} - - -
+ + {t('apps_configurations.app.name')} + + + + {t('apps_configurations.app.category')} + +
@@ -131,23 +127,21 @@ export const EditAppModal = ({ idx, onCancel, onSave, isNew, open, readonlyApp } {t('apps_configurations.app.monitor')} @@ -158,7 +152,10 @@ export const EditAppModal = ({ idx, onCancel, onSave, isNew, open, readonlyApp } {Object.values(WmApplicationOptions).map((value, i) => ( {t(`apps_configurations.app.options.${value}`)} - + ))} @@ -167,7 +164,10 @@ export const EditAppModal = ({ idx, onCancel, onSave, isNew, open, readonlyApp } {Object.values(WegApplicationOptions).map((value, i) => ( {t(`apps_configurations.app.options.${value}`)} - + ))} diff --git a/src/apps/settings/modules/appsConfigurations/infra/Identifier.tsx b/src/apps/settings/modules/appsConfigurations/infra/Identifier.tsx index 33795d94..c5532256 100644 --- a/src/apps/settings/modules/appsConfigurations/infra/Identifier.tsx +++ b/src/apps/settings/modules/appsConfigurations/infra/Identifier.tsx @@ -1,136 +1,126 @@ -import { Icon } from '../../../../shared/components/Icon'; -import { - ApplicationIdentifier, - IdWithIdentifier, - MatchingStrategy, -} from '../../../../shared/schemas/AppsConfigurations'; -import { SettingsGroup, SettingsOption } from '../../../components/SettingsBox'; -import { Button, Input, Select, Switch } from 'antd'; -import { useTranslation } from 'react-i18next'; - -import { OptionsFromEnum } from '../../shared/utils/app'; - -import cs from './Identifier.module.css'; - -interface Props { - identifier: IdWithIdentifier; - onChange: (id: IdWithIdentifier) => void; - onRemove?: () => void; -} - -export function Identifier({ identifier, onChange, onRemove }: Props) { - const { id, kind, matchingStrategy } = identifier; - - const { t } = useTranslation(); - - const onChangeId = (e: React.ChangeEvent) => { - onChange({ ...identifier, id: e.target.value }); - }; - - const onSelectKind = (value: ApplicationIdentifier) => { - onChange({ ...identifier, kind: value }); - }; - - const onSelectMatchingStrategy = (value: MatchingStrategy) => { - onChange({ ...identifier, matchingStrategy: value }); - }; - - const onChangeNegation = (value: boolean) => { - onChange({ ...identifier, negation: value }); - }; - - const onChangeAndItem = (idx: number, value: IdWithIdentifier) => { - onChange({ ...identifier, and: identifier.and.map((id, i) => (i === idx ? value : id)) }); - }; - - const onChangeOrItem = (idx: number, value: IdWithIdentifier) => { - onChange({ ...identifier, or: identifier.or.map((id, i) => (i === idx ? value : id)) }); - }; - - const onRemoveAndItem = (idx: number) => { - onChange({ ...identifier, and: identifier.and.filter((_, i) => i !== idx) }); - }; - - const onRemoveOrItem = (idx: number) => { - onChange({ ...identifier, or: identifier.or.filter((_, i) => i !== idx) }); - }; - - const onAddAndItem = () => { - onChange({ ...identifier, and: [IdWithIdentifier.default(), ...identifier.and] }); - }; - - const onAddOrItem = () => { - onChange({ ...identifier, or: [IdWithIdentifier.default(), ...identifier.or] }); - }; - - return ( - -
- {onRemove && ( - - {t('apps_configurations.identifier.remove')} - - - )} - - {t('apps_configurations.identifier.id')} - - - - {t('apps_configurations.identifier.kind')} - - - - {t('apps_configurations.identifier.negation')} - - - -
- - - {t('apps_configurations.identifier.and')} - - - {identifier.and.map((id, idx) => ( - onChangeAndItem(idx, value)} - onRemove={() => onRemoveAndItem(idx)} - /> - ))} - - - {t('apps_configurations.identifier.or')} - - - {identifier.or.map((id, idx) => ( - onChangeOrItem(idx, value)} - onRemove={() => onRemoveOrItem(idx)} - /> - ))} -
-
- ); -} +import { Button, Input, Select, Switch } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { AppIdentifier, AppIdentifierType, MatchingStrategy } from 'seelen-core'; + +import { OptionsFromEnum } from '../../shared/utils/app'; + +import { Icon } from '../../../../shared/components/Icon'; +import { SettingsGroup, SettingsOption } from '../../../components/SettingsBox'; +import cs from './Identifier.module.css'; + +interface Props { + identifier: AppIdentifier; + onChange: (id: AppIdentifier) => void; + onRemove?: () => void; +} + +export function Identifier({ identifier, onChange, onRemove }: Props) { + const { id, kind, matchingStrategy } = identifier; + + const { t } = useTranslation(); + + const onChangeId = (e: React.ChangeEvent) => { + onChange({ ...identifier, id: e.target.value }); + }; + + const onSelectKind = (value: AppIdentifierType) => { + onChange({ ...identifier, kind: value }); + }; + + const onSelectMatchingStrategy = (value: MatchingStrategy) => { + onChange({ ...identifier, matchingStrategy: value }); + }; + + const onChangeNegation = (value: boolean) => { + onChange({ ...identifier, negation: value }); + }; + + const onChangeAndItem = (idx: number, value: AppIdentifier) => { + onChange({ ...identifier, and: identifier.and.map((id, i) => (i === idx ? value : id)) }); + }; + + const onChangeOrItem = (idx: number, value: AppIdentifier) => { + onChange({ ...identifier, or: identifier.or.map((id, i) => (i === idx ? value : id)) }); + }; + + const onRemoveAndItem = (idx: number) => { + onChange({ ...identifier, and: identifier.and.filter((_, i) => i !== idx) }); + }; + + const onRemoveOrItem = (idx: number) => { + onChange({ ...identifier, or: identifier.or.filter((_, i) => i !== idx) }); + }; + + const onAddAndItem = () => { + onChange({ ...identifier, and: [AppIdentifier.placeholder(), ...identifier.and] }); + }; + + const onAddOrItem = () => { + onChange({ ...identifier, or: [AppIdentifier.placeholder(), ...identifier.or] }); + }; + + return ( + + {onRemove && ( + + {t('apps_configurations.identifier.remove')} + + + )} + + {t('apps_configurations.identifier.id')} + + + + {t('apps_configurations.identifier.kind')} + + + + {t('apps_configurations.identifier.negation')} + + + +
+ + + {t('apps_configurations.identifier.and')} + + + {identifier.and.map((id, idx) => ( + onChangeAndItem(idx, value)} + onRemove={() => onRemoveAndItem(idx)} + /> + ))} + + + {t('apps_configurations.identifier.or')} + + + {identifier.or.map((id, idx) => ( + onChangeOrItem(idx, value)} + onRemove={() => onRemoveOrItem(idx)} + /> + ))} +
+ ); +} diff --git a/src/apps/settings/modules/appsConfigurations/infra/index.module.css b/src/apps/settings/modules/appsConfigurations/infra/index.module.css index c95e815c..634f4deb 100644 --- a/src/apps/settings/modules/appsConfigurations/infra/index.module.css +++ b/src/apps/settings/modules/appsConfigurations/infra/index.module.css @@ -1,61 +1,69 @@ -.newBtn { - width: 100%; - background-color: var(--color-green-600); - padding: 0; - - &:hover { - background-color: var(--color-green-700) !important; - } -} - -.actions { - button { - width: 100%; - } -} - -.table { - :global(.ant-pagination-options) { - display: none; - } - - :global(.ant-empty) { - margin: 119px; - } - - :global(.ant-table-body) { - :global(.ant-table-cell) { - text-wrap: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - } - - :global(.ant-pagination) { - margin-top: 10px !important; - margin-bottom: 0 !important; - } -} - - -.editModal { - :global(.ant-modal-body) { - padding-right: 10px; - } - - :global(.ant-select) { - min-width: 150px; - } -} - -.footer { - display: flex; - gap: 10px; - align-items: center; - position: absolute; - bottom: 15px; - - button { - width: 80px; - } +.container { + height: 100%; + width: 100%; + display: grid; + grid-template-rows: 1fr 40px; + grid-template-columns: 100%; +} + +.newBtn { + width: 100%; + background-color: var(--color-green-600); + padding: 0; + + &:hover { + background-color: var(--color-green-700) !important; + } +} + +.actions { + button { + width: 100%; + } +} + +.table { + :global(.ant-pagination-options) { + display: none; + } + + :global(.ant-empty) { + margin: 119px; + } + + :global(.ant-table-body) { + :global(.ant-table-cell) { + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + + :global(.ant-pagination) { + margin-top: 10px !important; + margin-bottom: 0 !important; + } +} + + +.editModal { + :global(.ant-modal-body) { + padding-right: 10px; + } + + :global(.ant-select) { + min-width: 150px; + } +} + +.footer { + position: absolute; + bottom: 15px; + display: flex; + align-items: center; + gap: 10px; + + button { + width: 80px; + } } \ No newline at end of file diff --git a/src/apps/settings/modules/appsConfigurations/infra/infra.tsx b/src/apps/settings/modules/appsConfigurations/infra/infra.tsx index b4c0e191..f3006572 100644 --- a/src/apps/settings/modules/appsConfigurations/infra/infra.tsx +++ b/src/apps/settings/modules/appsConfigurations/infra/infra.tsx @@ -1,5 +1,3 @@ -import { ExportApps, ImportApps } from '../../shared/store/storeApi'; -import { EditAppModal } from './EditModal'; import { Button, Input, Modal, Switch, Table, Tooltip } from 'antd'; import { ColumnsType, ColumnType } from 'antd/es/table'; import { TFunction } from 'i18next'; @@ -7,17 +5,19 @@ import { cloneDeep } from 'lodash'; import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; +import { AppConfiguration } from 'seelen-core'; import { useAppSelector } from '../../shared/utils/infra'; import { RootSelectors } from '../../shared/store/app/selectors'; -import { YamlToState_Apps } from '../../shared/store/app/StateBridge'; import { cx, debounce } from '../../shared/utils/app'; import { getSorterByBool, getSorterByText } from '../app/filters'; import { AppsConfigActions } from '../app/reducer'; -import { AppConfiguration, AppConfigurationExtended, WmApplicationOptions } from '../domain'; +import { AppConfigurationExtended, WmApplicationOptions } from '../domain'; +import { ExportApps } from '../../shared/store/storeApi'; +import { EditAppModal } from './EditModal'; import cs from './index.module.css'; const ReadonlySwitch = (value: boolean, record: AppConfigurationExtended, _index: number) => { @@ -172,9 +172,10 @@ export function AppsConfiguration() { const { t } = useTranslation(); const importApps = useCallback(async () => { - const yamlApps = await ImportApps(); + // TODO reimplement Import Apps + /* const yamlApps = await ImportApps(); const newApps = YamlToState_Apps(yamlApps); - dispatch(AppsConfigActions.push(newApps)); + dispatch(AppsConfigActions.push(newApps)); */ }, []); const performSwap = useCallback(() => { @@ -219,13 +220,13 @@ export function AppsConfiguration() { ); return ( - <> +
- + ); } diff --git a/src/apps/settings/modules/developer/app.ts b/src/apps/settings/modules/developer/app.ts index 951357fc..733b02aa 100644 --- a/src/apps/settings/modules/developer/app.ts +++ b/src/apps/settings/modules/developer/app.ts @@ -1,19 +1,19 @@ -import { path } from '@tauri-apps/api'; -import * as dialog from '@tauri-apps/plugin-dialog'; - -import { LoadSettingsToStore } from '../shared/store/infra'; - -export async function LoadCustomConfigFile() { - const file = await dialog.open({ - defaultPath: await path.homeDir(), - multiple: false, - title: 'Select settings file', - filters: [{ name: 'settings', extensions: ['json'] }], - }); - - if (!file) { - return; - } - - LoadSettingsToStore(file.path); -} \ No newline at end of file +import { path } from '@tauri-apps/api'; +import * as dialog from '@tauri-apps/plugin-dialog'; + +import { LoadSettingsToStore } from '../shared/store/infra'; + +export async function LoadCustomConfigFile() { + const file = await dialog.open({ + defaultPath: await path.homeDir(), + multiple: false, + title: 'Select settings file', + filters: [{ name: 'settings', extensions: ['json'] }], + }); + + if (!file) { + return; + } + + LoadSettingsToStore(file); +} diff --git a/src/apps/settings/modules/developer/infra.tsx b/src/apps/settings/modules/developer/infra.tsx index b60eb6b4..d9147910 100644 --- a/src/apps/settings/modules/developer/infra.tsx +++ b/src/apps/settings/modules/developer/infra.tsx @@ -1,69 +1,82 @@ -import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; -import { path } from '@tauri-apps/api'; -import { invoke } from '@tauri-apps/api/core'; -import { Button, Switch } from 'antd'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; - -import { resolveDataPath } from '../shared/config/infra'; - -import { newSelectors, RootActions } from '../shared/store/app/reducer'; -import { LoadCustomConfigFile } from './app'; - -export function DeveloperTools() { - const devTools = useSelector(newSelectors.devTools); - - const dispatch = useDispatch(); - const { t } = useTranslation(); - - function onToggleDevTools(value: boolean) { - dispatch(RootActions.setDevTools(value)); - } - - async function openSettingsFile() { - invoke('open_file', { path: await resolveDataPath('settings.json') }); - } - - async function openInstallFolder() { - invoke('open_file', { path: await path.resourceDir() }); - } - - async function openDataFolder() { - invoke('open_file', { path: await path.appDataDir() }); - } - - return ( - <> - - - {t('devtools.enable')} - - - - - - - - {t('devtools.install_folder')} - - - - {t('devtools.data_folder')} - - - - - - - - {t('devtools.settings_file')} - - - - {t('devtools.custom_config_file')}: - - - - - ); -} +import { path } from '@tauri-apps/api'; +import { invoke } from '@tauri-apps/api/core'; +import { Button, Switch } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { SeelenCommand } from 'seelen-core'; + +import { resolveDataPath } from '../shared/config/infra'; + +import { newSelectors, RootActions } from '../shared/store/app/reducer'; +import { LoadCustomConfigFile } from './app'; + +import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; + +export function DeveloperTools() { + const devTools = useSelector(newSelectors.devTools); + + const dispatch = useDispatch(); + const { t } = useTranslation(); + + function onToggleDevTools(value: boolean) { + dispatch(RootActions.setDevTools(value)); + } + + async function openSettingsFile() { + invoke(SeelenCommand.OpenFile, { path: await resolveDataPath('settings.json') }); + } + + async function openInstallFolder() { + invoke(SeelenCommand.OpenFile, { path: await path.resourceDir() }); + } + + async function openDataFolder() { + invoke(SeelenCommand.OpenFile, { path: await path.appDataDir() }); + } + + async function simulateFullscreen(value: boolean) { + invoke(SeelenCommand.SimulateFullscreen, { value }); + } + + return ( + <> + + + {t('devtools.enable')} + + + + + + + + {t('devtools.install_folder')} + + + + {t('devtools.data_folder')} + + + + + + + + {t('devtools.settings_file')} + + + + {t('devtools.custom_config_file')}: + + + + + + + {t('devtools.simulate_fullscreen')} + + + + + ); +} diff --git a/src/apps/settings/modules/fancyToolbar/app.ts b/src/apps/settings/modules/fancyToolbar/app.ts index 11d3cffd..b32fb1c6 100644 --- a/src/apps/settings/modules/fancyToolbar/app.ts +++ b/src/apps/settings/modules/fancyToolbar/app.ts @@ -1,16 +1,14 @@ -import { parseAsCamel } from '../../../shared/schemas'; -import { FancyToolbar, FancyToolbarSchema } from '../../../shared/schemas/FancyToolbar'; -import { createSlice } from '@reduxjs/toolkit'; - -import { reducersFor, selectorsFor } from '../shared/utils/app'; - -const initialState: FancyToolbar = parseAsCamel(FancyToolbarSchema, {}); - -export const FancyToolbarSlice = createSlice({ - name: 'fancyToolbar', - initialState, - selectors: selectorsFor(initialState), - reducers: reducersFor(initialState), -}); - +import { createSlice } from '@reduxjs/toolkit'; +import { FancyToolbarSettings } from 'seelen-core'; + +import { reducersFor } from '../shared/utils/app'; + +const initialState = new FancyToolbarSettings(); + +export const FancyToolbarSlice = createSlice({ + name: 'fancyToolbar', + initialState, + reducers: reducersFor(initialState), +}); + export const FancyToolbarActions = FancyToolbarSlice.actions; \ No newline at end of file diff --git a/src/apps/settings/modules/fancyToolbar/infra.tsx b/src/apps/settings/modules/fancyToolbar/infra.tsx index 577abace..4c253e4c 100644 --- a/src/apps/settings/modules/fancyToolbar/infra.tsx +++ b/src/apps/settings/modules/fancyToolbar/infra.tsx @@ -1,14 +1,15 @@ -import { AppBarHideMode } from '../../../shared/schemas/Seelenweg'; -import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; import { InputNumber, Select, Switch } from 'antd'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; +import { HideMode } from 'seelen-core'; import { newSelectors } from '../shared/store/app/reducer'; import { RootSelectors } from '../shared/store/app/selectors'; import { OptionsFromEnum } from '../shared/utils/app'; import { FancyToolbarActions } from './app'; +import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; + export function FancyToolbarSettings() { const settings = useSelector(RootSelectors.fancyToolbar); const placeholders = useSelector(newSelectors.availablePlaceholders); @@ -83,7 +84,7 @@ export function FancyToolbarSettings() { dispatch(RootActions.setLanguage(value))} - /> - - - - - - - - - - -
- {t('general.theme.label')} -
- -
- - ); -} +import { Input, Select, Switch, Tooltip } from 'antd'; +import { ChangeEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import { startup } from '../../../shared/tauri/infra'; +import { useAppDispatch } from '../../../shared/utils/infra'; + +import { RootActions } from '../../../shared/store/app/reducer'; +import { RootSelectors } from '../../../shared/store/app/selectors'; +import { Icon } from 'src/apps/shared/components/Icon'; + +import { LanguageList } from '../../../../../shared/lang'; +import { SettingsGroup, SettingsOption } from '../../../../components/SettingsBox'; +import { Colors } from './Colors'; +import { Themes } from './Themes'; +import { Wallpaper } from './Wallpaper'; + +export function General() { + const [changingAutostart, setChangingAutostart] = useState(false); + + const autostartStatus = useSelector(RootSelectors.autostart); + const language = useSelector(RootSelectors.language); + const dateFormat = useSelector(RootSelectors.dateFormat); + + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const onAutoStart = async (value: boolean) => { + setChangingAutostart(true); + if (value) { + await startup.enable(); + } else { + await startup.disable(); + } + setChangingAutostart(false); + dispatch(RootActions.setAutostart(value)); + }; + + const onDateFormatChange = (e: ChangeEvent) => + dispatch(RootActions.setDateFormat(e.target.value)); + + return ( + <> + + + {t('general.startup')} + + + + + + {t('general.language')} + + + + + + + + + + + +
+ {t('general.theme.label')} +
+ +
+ + ); +} diff --git a/src/apps/settings/modules/information/infrastructure.tsx b/src/apps/settings/modules/information/infrastructure.tsx index b07f4780..ecb22ff9 100644 --- a/src/apps/settings/modules/information/infrastructure.tsx +++ b/src/apps/settings/modules/information/infrastructure.tsx @@ -1,6 +1,3 @@ -import { wasInstalledUsingMSIX } from '../../../shared'; -import { Icon } from '../../../shared/components/Icon'; -import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; import { exit, relaunch } from '@tauri-apps/plugin-process'; import { Button, Switch } from 'antd'; import { useEffect, useState } from 'react'; @@ -12,6 +9,10 @@ import cs from './infra.module.css'; import { newSelectors, RootActions } from '../shared/store/app/reducer'; +import { wasInstalledUsingMSIX } from '../../../shared'; +import { Icon } from '../../../shared/components/Icon'; +import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; + export function Information() { const [isMsixBuild, setIsMsixBuild] = useState(false); @@ -76,7 +77,7 @@ export function Information() { {t('extras.relaunch')} diff --git a/src/apps/settings/modules/monitors/layouts/domain.ts b/src/apps/settings/modules/monitors/layouts/domain.ts deleted file mode 100644 index 39e84ca3..00000000 --- a/src/apps/settings/modules/monitors/layouts/domain.ts +++ /dev/null @@ -1,13 +0,0 @@ - -export enum Layout { - BSP = 'BSP', - COLUMNS = 'Columns', - ROWS = 'Rows', - VERTICAL_STACK = 'VerticalStack', - HORIZONTAL_STACK = 'HorizontalStack', - ULTRAWIDE_VERTICAL_STACK = 'UltrawideVerticalStack', - GRID = 'Grid', -} - -// aspect ratio in base to 720p to 144px (height of monitor in preview) -export const RELATION_ASPECT_RATIO = 5; \ No newline at end of file diff --git a/src/apps/settings/modules/monitors/layouts/infra.module.css b/src/apps/settings/modules/monitors/layouts/infra.module.css deleted file mode 100644 index 41e922c6..00000000 --- a/src/apps/settings/modules/monitors/layouts/infra.module.css +++ /dev/null @@ -1,17 +0,0 @@ - -.colums { - flex: 1; - display: flex; -} - -.rows { - flex: 1; - display: flex; - flex-direction: column; -} - -.window { - flex: 1; - border: 1px dashed var(--color-red-900); - border-radius: 4px; -} \ No newline at end of file diff --git a/src/apps/settings/modules/monitors/layouts/infra.tsx b/src/apps/settings/modules/monitors/layouts/infra.tsx deleted file mode 100644 index ef0c05f7..00000000 --- a/src/apps/settings/modules/monitors/layouts/infra.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { useEffect, useState } from 'react'; - -import cs from './infra.module.css'; - -import { Layout, RELATION_ASPECT_RATIO } from './domain'; - -interface Props { - containerPadding: number; - workspacePadding: number; -} - -const BSPLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 4 ? 1 : v + 1)), 1000); - }, []); - - const style = { - gap: containerPadding / RELATION_ASPECT_RATIO, - }; - - return ( -
-
- {counter >= 2 && ( -
-
- {counter >= 3 && ( -
-
- {counter >= 4 &&
} -
- )} -
- )} -
- ); -}; - -const ColumsLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 4 ? 1 : v + 1)), 1000); - }, []); - - return ( -
-
- {counter >= 2 &&
} - {counter >= 3 &&
} - {counter >= 4 &&
} -
- ); -}; - -const RowsLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 4 ? 1 : v + 1)), 1000); - }, []); - - return ( -
-
- {counter >= 2 &&
} - {counter >= 3 &&
} - {counter >= 4 &&
} -
- ); -}; - -const HorizontalStackLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 4 ? 1 : v + 1)), 1000); - }, []); - - const style = { - gap: containerPadding / RELATION_ASPECT_RATIO, - }; - - return ( -
-
- {counter >= 2 && ( -
-
- {counter >= 3 &&
} - {counter >= 4 &&
} -
- )} -
- ); -}; - -const VerticalStackLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 4 ? 1 : v + 1)), 1000); - }, []); - - const style = { - gap: containerPadding / RELATION_ASPECT_RATIO, - }; - - return ( -
-
- {counter >= 2 && ( -
-
- {counter >= 3 &&
} - {counter >= 4 &&
} -
- )} -
- ); -}; - -const UltrawideVerticalStackLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 4 ? 1 : v + 1)), 1000); - }, []); - - const style = { - gap: containerPadding / RELATION_ASPECT_RATIO, - }; - - return ( -
-
- {counter >= 2 &&
= 3 ? 2 : 1 }} />} - {counter >= 3 && ( -
-
- {counter >= 4 &&
} - {counter >= 5 &&
} -
- )} -
- ); -}; - -const GridLayoutExample = ({ containerPadding, workspacePadding }: Props) => { - const [counter, setCounter] = useState(1); - - useEffect(() => { - setInterval(() => setCounter((v) => (v >= 8 ? 1 : v + 1)), 1000); - }, []); - - const style = { - gap: containerPadding / RELATION_ASPECT_RATIO, - }; - - return ( -
-
-
- {counter >= 4 && counter != 5 &&
} - {counter >= 9 &&
} -
- {counter >= 2 && ( -
-
- {counter >= 3 &&
} - {counter >= 8 &&
} -
- )} - {counter >= 5 && ( -
-
-
- {counter >= 7 &&
} -
- )} -
- ); -}; - -export const LayoutExamples: Record> = { - [Layout.BSP]: BSPLayoutExample, - [Layout.COLUMNS]: ColumsLayoutExample, - [Layout.ROWS]: RowsLayoutExample, - [Layout.HORIZONTAL_STACK]: HorizontalStackLayoutExample, - [Layout.VERTICAL_STACK]: VerticalStackLayoutExample, - [Layout.ULTRAWIDE_VERTICAL_STACK]: UltrawideVerticalStackLayoutExample, - [Layout.GRID]: GridLayoutExample, -}; diff --git a/src/apps/settings/modules/monitors/main/app.ts b/src/apps/settings/modules/monitors/main/app.ts deleted file mode 100644 index 31c036f7..00000000 --- a/src/apps/settings/modules/monitors/main/app.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { parseAsCamel } from '../../../../shared/schemas'; -import { Monitor, MonitorSchema, Workspace, WorkspaceSchema } from '../../../../shared/schemas/Monitors'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -const initialState: Monitor[] = [parseAsCamel(MonitorSchema, {})]; - -interface ForMonitor { - monitorIdx: number; -} - -interface ForWorkspace extends ForMonitor { - workspaceIdx: number; -} - -export const MonitorsSlice = createSlice({ - name: 'monitors', - initialState, - reducers: { - delete: (state, action: PayloadAction) => { - state.splice(action.payload, 1); - }, - insert: (state, action: PayloadAction) => { - state.splice(action.payload, 0, parseAsCamel(MonitorSchema, {})); - }, - changeEditingWorkspace: (state, action: PayloadAction) => { - const { monitorIdx, workspaceIdx } = action.payload; - const monitor = state[monitorIdx]; - if (!monitor) { - return; - } - monitor.edditingWorkspace = workspaceIdx; - }, - newWorkspace: (state, action: PayloadAction) => { - const { monitorIdx, name } = action.payload; - const monitor = state[monitorIdx]; - if (!monitor) { - return; - } - const newWorkspace = parseAsCamel(WorkspaceSchema, {}); - const length = monitor.workspaces.push(newWorkspace); - newWorkspace.name = name || `Workspace ${length}`; - }, - updateWorkspace: (state: Monitor[], action: PayloadAction) => { - const { workspaceIdx, monitorIdx, key, value } = action.payload; - let workspace = state[monitorIdx]?.workspaces[workspaceIdx]; - if (!workspace) { - return; - } - workspace[key] = value; - }, - updateMonitor: (state: Monitor[], action: PayloadAction) => { - const { monitorIdx, key, value } = action.payload; - const monitor = state[monitorIdx]; - if (!monitor) { - return; - } - monitor[key] = value; - }, - }, -}); - -export const MonitorsActions = MonitorsSlice.actions; \ No newline at end of file diff --git a/src/apps/settings/modules/monitors/main/infra.module.css b/src/apps/settings/modules/monitors/main/infra.module.css deleted file mode 100644 index 2b06e160..00000000 --- a/src/apps/settings/modules/monitors/main/infra.module.css +++ /dev/null @@ -1,69 +0,0 @@ -.monitors { - display: flex; - flex-direction: column; - gap: 20px; - - .monitor { - height: 100%; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 6px; - - .border { - --padding: 6px; - - background-color: black; - border-radius: 12px; - display: grid; - place-items: center; - padding: var(--padding); - height: calc(144px + var(--padding)); - width: calc(256px + var(--padding)); - - .screen { - border-radius: 8px; - background: linear-gradient( - 40deg, - var(--color-blue-500) 2%, - var(--color-blue-300) 60%, - var(--color-blue-100) 100% - ); - width: 100%; - height: 100%; - display: flex; - } - } - - > button { - flex: 2; - - &.advancedTrigger { - flex: 1; - } - } - } - - .config { - display: grid; - grid-template-columns: min-content 1fr; - align-items: center; - gap: 10px; - - .title { - font-weight: 600; - text-align: center; - font-size: 0.8rem; - margin-bottom: 10px; - } - - .workspaceSelector { - width: 100%; - } - } -} - -.advancedModal { - padding-right: 10px; -} diff --git a/src/apps/settings/modules/monitors/main/infra.tsx b/src/apps/settings/modules/monitors/main/infra.tsx deleted file mode 100644 index 0154a292..00000000 --- a/src/apps/settings/modules/monitors/main/infra.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { SettingsGroup } from '../../../components/SettingsBox'; -import { Button, Input, Select, Space } from 'antd'; -import { useState } from 'react'; -import { useDispatch } from 'react-redux'; - -import { useAppSelector } from '../../shared/utils/infra'; -import { LayoutExamples } from '../layouts/infra'; -import { WorkspaceConfig } from '../workspace/infra'; -import cs from './infra.module.css'; -import { AdvancedConfig } from './infra_advanced'; - -import { getMonitorSelector, RootSelectors, SeelenWmSelectors } from '../../shared/store/app/selectors'; -import { defaultOnNull } from '../../shared/utils/app'; -import { MonitorsActions } from './app'; - -export const MonitorConfig = ({ monitorIdx }: { monitorIdx: number }) => { - const [newWorkspaceName, setNewWorkspaceName] = useState(''); - const monitor = useAppSelector(getMonitorSelector(monitorIdx)); - - const dispatch = useDispatch(); - - if (!monitor) { - return null; - } - - const workspace = monitor.workspaces[monitor.edditingWorkspace]!; - const LayoutExample = LayoutExamples[workspace.layout]; - - const containerPadding = defaultOnNull( - workspace.gap, - useAppSelector(SeelenWmSelectors.workspaceGap), - ); - - const workspacePadding = defaultOnNull( - workspace.padding, - useAppSelector(SeelenWmSelectors.workspacePadding), - ); - - const onDelete = () => { - dispatch(MonitorsActions.delete(monitorIdx)); - }; - - const onInsert = () => { - dispatch(MonitorsActions.insert(monitorIdx + 1)); - }; - - const onChangeWorkspace = (workspaceIdx: number) => { - dispatch(MonitorsActions.changeEditingWorkspace({ monitorIdx, workspaceIdx })); - }; - - const onChangeNewWorkspaceName = (event: React.ChangeEvent) => { - setNewWorkspaceName(event.target.value); - }; - const onAddWorkspace = () => { - dispatch(MonitorsActions.newWorkspace({ monitorIdx, name: newWorkspaceName })); - setNewWorkspaceName(''); - }; - - return ( -
-
-
-
- {LayoutExample && } -
-
- - - -
- -
-
Monitor {monitorIdx + 1}
- - - - - )} - options={monitor.workspaces.map((workspace, index) => ({ - label: workspace.name, - value: index, - }))} - onChange={onChangeWorkspace} - /> -
- -
-
- ); -}; - -export function Monitors() { - const monitors = useAppSelector(RootSelectors.monitors); - - return ( -
- {monitors.map((_, index) => ( - - ))} -
- ); -} diff --git a/src/apps/settings/modules/monitors/main/infra_advanced.tsx b/src/apps/settings/modules/monitors/main/infra_advanced.tsx deleted file mode 100644 index 6360fece..00000000 --- a/src/apps/settings/modules/monitors/main/infra_advanced.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../../components/SettingsBox'; -import { Button, InputNumber, Modal } from 'antd'; -import { useState } from 'react'; -import { useDispatch } from 'react-redux'; - -import { useAppSelector } from '../../shared/utils/infra'; -import cs from './infra.module.css'; - -import { getMonitorSelector, getWorkspaceSelector } from '../../shared/store/app/selectors'; -import { Rect } from '../../shared/utils/app/Rect'; -import { MonitorsActions } from './app'; - -interface Props { - workspaceIdx: number; - monitorIdx: number; -} - -export const AdvancedConfig = ({ workspaceIdx, monitorIdx }: Props) => { - const [isModalOpen, setIsModalOpen] = useState(false); - const workspace = useAppSelector(getWorkspaceSelector(workspaceIdx, monitorIdx)); - const { workAreaOffset } = useAppSelector(getMonitorSelector(monitorIdx))!; - - const dispatch = useDispatch(); - - if (!workspace) { - return; - } - - const showModal = () => { - setIsModalOpen(true); - }; - - const handleOk = () => { - setIsModalOpen(false); - }; - - const handleCancel = () => { - setIsModalOpen(false); - }; - - const resetOffset = () => - dispatch(MonitorsActions.updateMonitor({ monitorIdx, key: 'workAreaOffset', value: null })); - const onChangeOffset = (side: keyof Rect, value: number | null) => { - dispatch( - MonitorsActions.updateMonitor({ - monitorIdx, - key: 'workAreaOffset', - value: { - ...(workAreaOffset || new Rect().toJSON()), - [side]: value || 0, - }, - }), - ); - }; - - return ( - <> - - -
- - - Specifit monitor offsets (margins) - - - } - > - - Left - - - - Top - - - - Right - - - - Bottom - - - - -
-
- - ); -}; diff --git a/src/apps/settings/modules/monitors/workspace/app.ts b/src/apps/settings/modules/monitors/workspace/app.ts deleted file mode 100644 index 28a263a1..00000000 --- a/src/apps/settings/modules/monitors/workspace/app.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getWorkspaceSelector, SeelenWmSelectors } from '../../shared/store/app/selectors'; -import { defaultOnNull } from '../../shared/utils/app'; - -import { RootState } from '../../shared/store/domain'; - -export const getWorkspacePaddingSelector = (idx: number, monitorIdx: number) => (state: RootState) => { - return defaultOnNull( - getWorkspaceSelector(idx, monitorIdx)(state)?.padding, - SeelenWmSelectors.workspacePadding(state), - ); -}; - -export const getWorkspaceGapSelector = (idx: number, monitorIdx: number) => (state: RootState) => { - return defaultOnNull( - getWorkspaceSelector(idx, monitorIdx)(state)?.gap, - SeelenWmSelectors.workspaceGap(state), - ); -}; diff --git a/src/apps/settings/modules/monitors/workspace/infra.module.css b/src/apps/settings/modules/monitors/workspace/infra.module.css index ed4220e1..e01ae15f 100644 --- a/src/apps/settings/modules/monitors/workspace/infra.module.css +++ b/src/apps/settings/modules/monitors/workspace/infra.module.css @@ -1,5 +1,5 @@ -.workspaceConfig { - :global(.ant-select) { - max-width: 100px; - } -} +.workspaceConfig { + :global(.ant-select) { + max-width: 100px; + } +} diff --git a/src/apps/settings/modules/monitors/workspace/infra.tsx b/src/apps/settings/modules/monitors/workspace/infra.tsx deleted file mode 100644 index e8092384..00000000 --- a/src/apps/settings/modules/monitors/workspace/infra.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { SettingsOption } from '../../../components/SettingsBox'; -import { InputNumber, Select } from 'antd'; -import { useDispatch } from 'react-redux'; - -import { useAppSelector } from '../../shared/utils/infra'; -import cs from './infra.module.css'; - -import { getWorkspaceSelector } from '../../shared/store/app/selectors'; -import { OptionsFromEnum } from '../../shared/utils/app'; -import { MonitorsActions } from '../main/app'; - -import { Layout } from '../layouts/domain'; - -interface Props { - monitorIdx: number; - workspaceIdx: number; -} - -export const WorkspaceConfig = ({ monitorIdx, workspaceIdx }: Props) => { - const workspace = useAppSelector(getWorkspaceSelector(workspaceIdx, monitorIdx)); - - const dispatch = useDispatch(); - - if (!workspace) { - return null; - } - - const onSelectLayout = (layout: Layout) => { - dispatch(MonitorsActions.updateWorkspace({ monitorIdx, workspaceIdx, key: 'layout', value: layout })); - }; - - const onChangeGap = (value: number | null) => { - dispatch(MonitorsActions.updateWorkspace({ monitorIdx, workspaceIdx, key: 'gap', value })); - }; - - const onChangePadding = (value: number | null) => { - dispatch(MonitorsActions.updateWorkspace({ monitorIdx, workspaceIdx, key: 'padding', value })); - }; - - return ( -
- - padding - - - - gap - - - - layout - dispatch(SeelenWegActions.setHideMode(value))} />
{t('weg.dock_side')}
- onChangeVar(key, e)} - /> -
- ); - }) - } - -
- ); -} +import { Button, Input, Switch, Tooltip } from 'antd'; +import { pick } from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { AhkVarList } from 'seelen-core'; + +import { RootActions } from '../shared/store/app/reducer'; +import { RootSelectors } from '../shared/store/app/selectors'; +import { AhkVariablesActions, KeyCodeToAHK } from './app'; +import { Icon } from 'src/apps/shared/components/Icon'; + +import { VariableConvention } from '../../../shared/schemas'; +import { SettingsGroup, SettingsOption, SettingsSubGroup } from '../../components/SettingsBox'; + +interface AhkOptionsProps { + variables: Array; + onChangeVar: anyFunction; +} + +function AhkOptions({ variables, onChangeVar }: AhkOptionsProps) { + const all = useSelector(RootSelectors.ahkVariables); + + const { t } = useTranslation(); + + const toUse = pick(all, variables); + + return Object.entries(toUse).map(([key, value]) => { + return ( + +
{t(`shortcuts.labels.${VariableConvention.camelToSnake(key)}`)}
+ onChangeVar(key as keyof AhkVarList, e)} /> +
+ ); + }); +} + +export function Shortcuts() { + const ahkEnable = useSelector(RootSelectors.ahkEnabled); + + const dispatch = useDispatch(); + const { t } = useTranslation(); + + function onChangeEnabled(value: boolean) { + dispatch(RootActions.setAhkEnabled(value)); + dispatch(RootActions.setToBeSaved(true)); + } + + function onChangeVar(name: keyof AhkVarList, e: React.KeyboardEvent) { + const result = KeyCodeToAHK(e); + if (result) { + dispatch(AhkVariablesActions.setVariable({ name, value: result })); + } + } + + function onReset() { + dispatch(AhkVariablesActions.reset()); + } + + return ( +
+ + + + {t('shortcuts.enable')} + + + + + + + + {t('shortcuts.reset')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/apps/settings/public/index.html b/src/apps/settings/public/index.html new file mode 100644 index 00000000..75a0a4c0 --- /dev/null +++ b/src/apps/settings/public/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + +
+
+ +
+ + diff --git a/src/apps/settings/public/logo.svg b/src/apps/settings/public/logo.svg new file mode 100644 index 00000000..90d69197 --- /dev/null +++ b/src/apps/settings/public/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/apps/settings/public/splashscreen.png b/src/apps/settings/public/splashscreen.png new file mode 100644 index 00000000..702a55a7 Binary files /dev/null and b/src/apps/settings/public/splashscreen.png differ diff --git a/src/apps/settings/styles/colors.css b/src/apps/settings/styles/colors.css deleted file mode 100644 index ec8ed844..00000000 --- a/src/apps/settings/styles/colors.css +++ /dev/null @@ -1,599 +0,0 @@ -:root { - /* Persisted colors variables (not changed on dark mode) */ - --color-persist-white: #ffffff; - --color-persist-gray-50: #fdfdfd; - --color-persist-gray-100: #f8f8f8; - --color-persist-gray-200: #e6e6e6; - --color-persist-gray-300: #d5d5d5; - --color-persist-gray-400: #b1b1b1; - --color-persist-gray-500: #909090; - --color-persist-gray-600: #6d6d6d; - --color-persist-gray-700: #464646; - --color-persist-gray-800: #222222; - --color-persist-gray-900: #151515; - --color-persist-black: #000000; - - --color-persist-blue-100: #e0f2ff; - --color-persist-blue-200: #cae8ff; - --color-persist-blue-300: #b5deff; - --color-persist-blue-400: #96cefd; - --color-persist-blue-500: #78bbfa; - --color-persist-blue-600: #59a7f6; - --color-persist-blue-700: #3892f3; - --color-persist-blue-800: #147af3; - --color-persist-blue-900: #0265dc; - --color-persist-blue-1000: #0054b6; - --color-persist-blue-1100: #004491; - --color-persist-blue-1200: #003571; - --color-persist-blue-1300: #002754; - - --color-persist-green-100: #cef8e0; - --color-persist-green-200: #adf4ce; - --color-persist-green-300: #89ecbc; - --color-persist-green-400: #67dea8; - --color-persist-green-500: #49cc93; - --color-persist-green-600: #2fb880; - --color-persist-green-700: #15a46e; - --color-persist-green-800: #008f5d; - --color-persist-green-900: #007a4d; - --color-persist-green-1000: #00653e; - --color-persist-green-1100: #005132; - --color-persist-green-1200: #053f27; - --color-persist-green-1300: #0a2e1d; - - --color-persist-orange-100: #ffeccc; - --color-persist-orange-200: #ffdfad; - --color-persist-orange-300: #fdd291; - --color-persist-orange-400: #ffbb63; - --color-persist-orange-500: #ffa037; - --color-persist-orange-600: #f68511; - --color-persist-orange-700: #e46f00; - --color-persist-orange-800: #cb5d00; - --color-persist-orange-900: #b14c00; - --color-persist-orange-1000: #953d00; - --color-persist-orange-1100: #7a2f00; - --color-persist-orange-1200: #612300; - --color-persist-orange-1300: #491901; - - --color-persist-red-100: #ffebe7; - --color-persist-red-200: #ffddd6; - --color-persist-red-300: #ffcdc3; - --color-persist-red-400: #ffb7a9; - --color-persist-red-500: #ff9b88; - --color-persist-red-600: #ff7c65; - --color-persist-red-700: #f75c46; - --color-persist-red-800: #ea3829; - --color-persist-red-900: #d31510; - --color-persist-red-1000: #b40000; - --color-persist-red-1100: #930000; - --color-persist-red-1200: #740000; - --color-persist-red-1300: #590000; - - --color-persist-celery-100: #cdfcbf; - --color-persist-celery-200: #aef69d; - --color-persist-celery-300: #96ee85; - --color-persist-celery-400: #72e06a; - --color-persist-celery-500: #4ecf50; - --color-persist-celery-600: #27bb36; - --color-persist-celery-700: #07a721; - --color-persist-celery-800: #009112; - --color-persist-celery-900: #007c0f; - --color-persist-celery-1000: #00670f; - --color-persist-celery-1100: #00530d; - --color-persist-celery-1200: #00400a; - --color-persist-celery-1300: #003007; - - --color-persist-chartreuse-100: #dbfc6e; - --color-persist-chartreuse-200: #cbf443; - --color-persist-chartreuse-300: #bce92a; - --color-persist-chartreuse-400: #aad816; - --color-persist-chartreuse-500: #98c50a; - --color-persist-chartreuse-600: #87b103; - --color-persist-chartreuse-700: #769c00; - --color-persist-chartreuse-800: #678800; - --color-persist-chartreuse-900: #577400; - --color-persist-chartreuse-1000: #486000; - --color-persist-chartreuse-1100: #3a4d00; - --color-persist-chartreuse-1200: #2c3b00; - --color-persist-chartreuse-1300: #212c00; - - --color-persist-cyan-100: #c5f8ff; - --color-persist-cyan-200: #a4f0ff; - --color-persist-cyan-300: #88e7fa; - --color-persist-cyan-400: #60d8f3; - --color-persist-cyan-500: #33c5e8; - --color-persist-cyan-600: #12b0da; - --color-persist-cyan-700: #019cc8; - --color-persist-cyan-800: #0086b4; - --color-persist-cyan-900: #00719f; - --color-persist-cyan-1000: #005d89; - --color-persist-cyan-1100: #004a73; - --color-persist-cyan-1200: #00395d; - --color-persist-cyan-1300: #002a46; - - --color-persist-fuchsia-100: #ffe9fc; - --color-persist-fuchsia-200: #ffdafa; - --color-persist-fuchsia-300: #fec7f8; - --color-persist-fuchsia-400: #fbaef6; - --color-persist-fuchsia-500: #f592f3; - --color-persist-fuchsia-600: #ed74ed; - --color-persist-fuchsia-700: #e055e2; - --color-persist-fuchsia-800: #cd3ace; - --color-persist-fuchsia-900: #b622b7; - --color-persist-fuchsia-1000: #9d039e; - --color-persist-fuchsia-1100: #800081; - --color-persist-fuchsia-1200: #640664; - --color-persist-fuchsia-1300: #470e46; - - --color-persist-indigo-100: #edeeff; - --color-persist-indigo-200: #e0e2ff; - --color-persist-indigo-300: #d3d5ff; - --color-persist-indigo-400: #c1c4ff; - --color-persist-indigo-500: #acafff; - --color-persist-indigo-600: #9599ff; - --color-persist-indigo-700: #7e84fc; - --color-persist-indigo-800: #686df4; - --color-persist-indigo-900: #5258e4; - --color-persist-indigo-1000: #4046ca; - --color-persist-indigo-1100: #3236a8; - --color-persist-indigo-1200: #262986; - --color-persist-indigo-1300: #1b1e64; - - --color-persist-magenta-100: #ffeaf1; - --color-persist-magenta-200: #ffdce8; - --color-persist-magenta-300: #ffcadd; - --color-persist-magenta-400: #ffb2ce; - --color-persist-magenta-500: #ff95bd; - --color-persist-magenta-600: #fa77aa; - --color-persist-magenta-700: #ef5a98; - --color-persist-magenta-800: #de3d82; - --color-persist-magenta-900: #c82269; - --color-persist-magenta-1000: #ad0955; - --color-persist-magenta-1100: #8e0045; - --color-persist-magenta-1200: #700037; - --color-persist-magenta-1300: #54032a; - - --color-persist-purple-100: #f6ebff; - --color-persist-purple-200: #eeddff; - --color-persist-purple-300: #e6d0ff; - --color-persist-purple-400: #dbbbfe; - --color-persist-purple-500: #cca4fd; - --color-persist-purple-600: #bd8bfc; - --color-persist-purple-700: #ae72f9; - --color-persist-purple-800: #9d57f4; - --color-persist-purple-900: #893de7; - --color-persist-purple-1000: #7326d3; - --color-persist-purple-1100: #5d13b7; - --color-persist-purple-1200: #470c94; - --color-persist-purple-1300: #33106a; - - --color-persist-seafoam-100: #cef7f3; - --color-persist-seafoam-200: #aaf1ea; - --color-persist-seafoam-300: #8ce9e2; - --color-persist-seafoam-400: #65dad2; - --color-persist-seafoam-500: #3fc9c1; - --color-persist-seafoam-600: #0fb5ae; - --color-persist-seafoam-700: #00a19a; - --color-persist-seafoam-800: #008c87; - --color-persist-seafoam-900: #007772; - --color-persist-seafoam-1000: #00635f; - --color-persist-seafoam-1100: #0c4f4c; - --color-persist-seafoam-1200: #123c3a; - --color-persist-seafoam-1300: #122c2b; - - --color-persist-yellow-100: #fbf198; - --color-persist-yellow-200: #f8e750; - --color-persist-yellow-300: #f8d904; - --color-persist-yellow-400: #e8c600; - --color-persist-yellow-500: #d7b300; - --color-persist-yellow-600: #c49f00; - --color-persist-yellow-700: #b08c00; - --color-persist-yellow-800: #9b7800; - --color-persist-yellow-900: #856600; - --color-persist-yellow-1000: #705300; - --color-persist-yellow-1100: #5b4300; - --color-persist-yellow-1200: #483300; - --color-persist-yellow-1300: #362500; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-white: #000000; - - --color-gray-50: #151515; - --color-gray-100: #222222; - --color-gray-200: #464646; - --color-gray-300: #6d6d6d; - --color-gray-400: #909090; - --color-gray-500: #b1b1b1; - --color-gray-600: #d5d5d5; - --color-gray-700: #e6e6e6; - --color-gray-800: #f8f8f8; - --color-gray-900: #fdfdfd; - - --color-black: #ffffff; - - --color-blue-100: #003877; - --color-blue-200: #00418a; - --color-blue-300: #004da3; - --color-blue-400: #0059c2; - --color-blue-500: #0367e0; - --color-blue-600: #1379f3; - --color-blue-700: #348ff4; - --color-blue-800: #54a3f6; - --color-blue-900: #72b7f9; - --color-blue-1000: #8fcafc; - --color-blue-1100: #aedbfe; - --color-blue-1200: #cce9ff; - --color-blue-1300: #e8f6ff; - - --color-green-100: #044329; - --color-green-200: #004e2f; - --color-green-300: #005c38; - --color-green-400: #006c43; - --color-green-500: #007d4e; - --color-green-600: #008f5d; - --color-green-700: #12a26c; - --color-green-800: #2bb47d; - --color-green-900: #43c78f; - --color-green-1000: #5ed9a2; - --color-green-1100: #81e9b8; - --color-green-1200: #b1f4d1; - --color-green-1300: #dffaea; - - --color-orange-100: #662500; - --color-orange-200: #752d00; - --color-orange-300: #893700; - --color-orange-400: #9e4200; - --color-orange-500: #b44e00; - --color-orange-600: #ca5d00; - --color-orange-700: #e16d00; - --color-orange-800: #f4810c; - --color-orange-900: #fe9a2e; - --color-orange-1000: #ffb558; - --color-orange-1100: #fdce88; - --color-orange-1200: #ffe1b3; - --color-orange-1300: #fff2dd; - - --color-red-100: #7b0000; - --color-red-200: #8d0000; - --color-red-300: #a50000; - --color-red-400: #be0403; - --color-red-500: #d71913; - --color-red-600: #ea3829; - --color-red-700: #f65843; - --color-red-800: #ff755e; - --color-red-900: #ff9581; - --color-red-1000: #ffb0a1; - --color-red-1100: #ffc9bd; - --color-red-1200: #ffded8; - --color-red-1300: #fff1ee; - - --color-celery-100: #00450a; - --color-celery-200: #00500c; - --color-celery-300: #005e0e; - --color-celery-400: #006d0f; - --color-celery-500: #007f0f; - --color-celery-600: #009112; - --color-celery-700: #04a51e; - --color-celery-800: #22b833; - --color-celery-900: #44ca49; - --color-celery-1000: #69dc63; - --color-celery-1100: #8eeb7f; - --color-celery-1200: #b4f7a2; - --color-celery-1300: #ddfdd3; - - --color-chartreuse-100: #304000; - --color-chartreuse-200: #374a00; - --color-chartreuse-300: #415700; - --color-chartreuse-400: #4c6600; - --color-chartreuse-500: #597600; - --color-chartreuse-600: #668800; - --color-chartreuse-700: #759a00; - --color-chartreuse-800: #84ad01; - --color-chartreuse-900: #94c008; - --color-chartreuse-1000: #a6d312; - --color-chartreuse-1100: #b8e525; - --color-chartreuse-1200: #cdf547; - --color-chartreuse-1300: #e7fe9a; - - --color-cyan-100: #003d62; - --color-cyan-200: #00476f; - --color-cyan-300: #00557f; - --color-cyan-400: #006491; - --color-cyan-500: #0074a2; - --color-cyan-600: #0086b4; - --color-cyan-700: #0099c6; - --color-cyan-800: #0eadd7; - --color-cyan-900: #2cc1e6; - --color-cyan-1000: #54d3f1; - --color-cyan-1100: #7fe4f9; - --color-cyan-1200: #a7f1ff; - --color-cyan-1300: #d7faff; - - --color-fuchsia-100: #6b036a; - --color-fuchsia-200: #7b007b; - --color-fuchsia-300: #900091; - --color-fuchsia-400: #a50da6; - --color-fuchsia-500: #b925b9; - --color-fuchsia-600: #cd39ce; - --color-fuchsia-700: #df51e0; - --color-fuchsia-800: #eb6eec; - --color-fuchsia-900: #f48cf2; - --color-fuchsia-1000: #faa8f5; - --color-fuchsia-1100: #fec2f8; - --color-fuchsia-1200: #ffdbfa; - --color-fuchsia-1300: #ffeffc; - - --color-indigo-100: #282c8c; - --color-indigo-200: #2f34a3; - --color-indigo-300: #393fbb; - --color-indigo-400: #464bd3; - --color-indigo-500: #555be7; - --color-indigo-600: #686df4; - --color-indigo-700: #7c81fb; - --color-indigo-800: #9195ff; - --color-indigo-900: #a7aaff; - --color-indigo-1000: #bcbeff; - --color-indigo-1100: #d0d2ff; - --color-indigo-1200: #e2e4ff; - --color-indigo-1300: #f3f3fe; - - --color-magenta-100: #76003a; - --color-magenta-200: #890042; - --color-magenta-300: #a0004d; - --color-magenta-400: #b6125a; - --color-magenta-500: #cb266d; - --color-magenta-600: #de3d82; - --color-magenta-700: #ed5795; - --color-magenta-800: #f972a7; - --color-magenta-900: #ff8fb9; - --color-magenta-1000: #ffacca; - --color-magenta-1100: #ffc6da; - --color-magenta-1200: #ffdde9; - --color-magenta-1300: #fff0f5; - - --color-purple-100: #4c0d9d; - --color-purple-200: #5911b1; - --color-purple-300: #691cc8; - --color-purple-400: #7a2dda; - --color-purple-500: #8c41e9; - --color-purple-600: #9d57f3; - --color-purple-700: #ac6ff9; - --color-purple-800: #bb87fb; - --color-purple-900: #ca9ffc; - --color-purple-1000: #d7b6fe; - --color-purple-1100: #e4ccfe; - --color-purple-1200: #efdfff; - --color-purple-1300: #f9f0ff; - - --color-seafoam-100: #12413f; - --color-seafoam-200: #0e4c49; - --color-seafoam-300: #045a57; - --color-seafoam-400: #006965; - --color-seafoam-500: #007a75; - --color-seafoam-600: #008c87; - --color-seafoam-700: #009e98; - --color-seafoam-800: #03b2ab; - --color-seafoam-900: #36c5bd; - --color-seafoam-1000: #5dd6cf; - --color-seafoam-1100: #84e6df; - --color-seafoam-1200: #b0f2ec; - --color-seafoam-1300: #dff9f6; - - --color-yellow-100: #4c3600; - --color-yellow-200: #584000; - --color-yellow-300: #674c00; - --color-yellow-400: #775900; - --color-yellow-500: #886800; - --color-yellow-600: #9b7800; - --color-yellow-700: #ae8900; - --color-yellow-800: #c09c00; - --color-yellow-900: #d3ae00; - --color-yellow-1000: #e4c200; - --color-yellow-1100: #f4d500; - --color-yellow-1200: #f9e85c; - --color-yellow-1300: #fcf6bb; - } -} - -@media (prefers-color-scheme: light) { - :root { - --color-white: #ffffff; - - --color-gray-50: #fdfdfd; - --color-gray-100: #f8f8f8; - --color-gray-200: #e6e6e6; - --color-gray-300: #d5d5d5; - --color-gray-400: #b1b1b1; - --color-gray-500: #909090; - --color-gray-600: #6d6d6d; - --color-gray-700: #464646; - --color-gray-800: #222222; - --color-gray-900: #151515; - - --color-black: #000000; - - --color-blue-100: #e0f2ff; - --color-blue-200: #cae8ff; - --color-blue-300: #b5deff; - --color-blue-400: #96cefd; - --color-blue-500: #78bbfa; - --color-blue-600: #59a7f6; - --color-blue-700: #3892f3; - --color-blue-800: #147af3; - --color-blue-900: #0265dc; - --color-blue-1000: #0054b6; - --color-blue-1100: #004491; - --color-blue-1200: #003571; - --color-blue-1300: #002754; - - --color-green-100: #cef8e0; - --color-green-200: #adf4ce; - --color-green-300: #89ecbc; - --color-green-400: #67dea8; - --color-green-500: #49cc93; - --color-green-600: #2fb880; - --color-green-700: #15a46e; - --color-green-800: #008f5d; - --color-green-900: #007a4d; - --color-green-1000: #00653e; - --color-green-1100: #005132; - --color-green-1200: #053f27; - --color-green-1300: #0a2e1d; - - --color-orange-100: #ffeccc; - --color-orange-200: #ffdfad; - --color-orange-300: #fdd291; - --color-orange-400: #ffbb63; - --color-orange-500: #ffa037; - --color-orange-600: #f68511; - --color-orange-700: #e46f00; - --color-orange-800: #cb5d00; - --color-orange-900: #b14c00; - --color-orange-1000: #953d00; - --color-orange-1100: #7a2f00; - --color-orange-1200: #612300; - --color-orange-1300: #491901; - - --color-red-100: #ffebe7; - --color-red-200: #ffddd6; - --color-red-300: #ffcdc3; - --color-red-400: #ffb7a9; - --color-red-500: #ff9b88; - --color-red-600: #ff7c65; - --color-red-700: #f75c46; - --color-red-800: #ea3829; - --color-red-900: #d31510; - --color-red-1000: #b40000; - --color-red-1100: #930000; - --color-red-1200: #740000; - --color-red-1300: #590000; - - --color-celery-100: #cdfcbf; - --color-celery-200: #aef69d; - --color-celery-300: #96ee85; - --color-celery-400: #72e06a; - --color-celery-500: #4ecf50; - --color-celery-600: #27bb36; - --color-celery-700: #07a721; - --color-celery-800: #009112; - --color-celery-900: #007c0f; - --color-celery-1000: #00670f; - --color-celery-1100: #00530d; - --color-celery-1200: #00400a; - --color-celery-1300: #003007; - - --color-chartreuse-100: #dbfc6e; - --color-chartreuse-200: #cbf443; - --color-chartreuse-300: #bce92a; - --color-chartreuse-400: #aad816; - --color-chartreuse-500: #98c50a; - --color-chartreuse-600: #87b103; - --color-chartreuse-700: #769c00; - --color-chartreuse-800: #678800; - --color-chartreuse-900: #577400; - --color-chartreuse-1000: #486000; - --color-chartreuse-1100: #3a4d00; - --color-chartreuse-1200: #2c3b00; - --color-chartreuse-1300: #212c00; - - --color-cyan-100: #c5f8ff; - --color-cyan-200: #a4f0ff; - --color-cyan-300: #88e7fa; - --color-cyan-400: #60d8f3; - --color-cyan-500: #33c5e8; - --color-cyan-600: #12b0da; - --color-cyan-700: #019cc8; - --color-cyan-800: #0086b4; - --color-cyan-900: #00719f; - --color-cyan-1000: #005d89; - --color-cyan-1100: #004a73; - --color-cyan-1200: #00395d; - --color-cyan-1300: #002a46; - - --color-fuchsia-100: #ffe9fc; - --color-fuchsia-200: #ffdafa; - --color-fuchsia-300: #fec7f8; - --color-fuchsia-400: #fbaef6; - --color-fuchsia-500: #f592f3; - --color-fuchsia-600: #ed74ed; - --color-fuchsia-700: #e055e2; - --color-fuchsia-800: #cd3ace; - --color-fuchsia-900: #b622b7; - --color-fuchsia-1000: #9d039e; - --color-fuchsia-1100: #800081; - --color-fuchsia-1200: #640664; - --color-fuchsia-1300: #470e46; - - --color-indigo-100: #edeeff; - --color-indigo-200: #e0e2ff; - --color-indigo-300: #d3d5ff; - --color-indigo-400: #c1c4ff; - --color-indigo-500: #acafff; - --color-indigo-600: #9599ff; - --color-indigo-700: #7e84fc; - --color-indigo-800: #686df4; - --color-indigo-900: #5258e4; - --color-indigo-1000: #4046ca; - --color-indigo-1100: #3236a8; - --color-indigo-1200: #262986; - --color-indigo-1300: #1b1e64; - - --color-magenta-100: #ffeaf1; - --color-magenta-200: #ffdce8; - --color-magenta-300: #ffcadd; - --color-magenta-400: #ffb2ce; - --color-magenta-500: #ff95bd; - --color-magenta-600: #fa77aa; - --color-magenta-700: #ef5a98; - --color-magenta-800: #de3d82; - --color-magenta-900: #c82269; - --color-magenta-1000: #ad0955; - --color-magenta-1100: #8e0045; - --color-magenta-1200: #700037; - --color-magenta-1300: #54032a; - - --color-purple-100: #f6ebff; - --color-purple-200: #eeddff; - --color-purple-300: #e6d0ff; - --color-purple-400: #dbbbfe; - --color-purple-500: #cca4fd; - --color-purple-600: #bd8bfc; - --color-purple-700: #ae72f9; - --color-purple-800: #9d57f4; - --color-purple-900: #893de7; - --color-purple-1000: #7326d3; - --color-purple-1100: #5d13b7; - --color-purple-1200: #470c94; - --color-purple-1300: #33106a; - - --color-seafoam-100: #cef7f3; - --color-seafoam-200: #aaf1ea; - --color-seafoam-300: #8ce9e2; - --color-seafoam-400: #65dad2; - --color-seafoam-500: #3fc9c1; - --color-seafoam-600: #0fb5ae; - --color-seafoam-700: #00a19a; - --color-seafoam-800: #008c87; - --color-seafoam-900: #007772; - --color-seafoam-1000: #00635f; - --color-seafoam-1100: #0c4f4c; - --color-seafoam-1200: #123c3a; - --color-seafoam-1300: #122c2b; - - --color-yellow-100: #fbf198; - --color-yellow-200: #f8e750; - --color-yellow-300: #f8d904; - --color-yellow-400: #e8c600; - --color-yellow-500: #d7b300; - --color-yellow-600: #c49f00; - --color-yellow-700: #b08c00; - --color-yellow-800: #9b7800; - --color-yellow-900: #856600; - --color-yellow-1000: #705300; - --color-yellow-1100: #5b4300; - --color-yellow-1200: #483300; - --color-yellow-1300: #362500; - } -} diff --git a/src/apps/settings/styles/global.css b/src/apps/settings/styles/global.css index 62c1524c..99feece7 100644 --- a/src/apps/settings/styles/global.css +++ b/src/apps/settings/styles/global.css @@ -1,127 +1,102 @@ -#root { - display: grid; - grid-template-columns: min-content 1fr; - grid-template-rows: 50px 1fr; - height: 100vh; - width: 100vw; -} - -#splashscreen { - will-change: contents; - position: fixed; - z-index: 1000; - width: 100%; - height: 100%; - top: 0; - left: 0; - background-color: var(--color-gray-50); - transition: opacity 300ms linear; - opacity: 1; - - &::before { - position: absolute; - width: 100%; - height: 100%; - content: " "; - background: linear-gradient(217deg, var(--color-red-100), #0000 70%), - linear-gradient(127deg, var(--color-blue-100), #0000 70%), - linear-gradient(336deg, var(--color-cyan-100), #0000 70%); - } - - &.hidden { - display: none; - } - - &.vanish { - opacity: 0; - } - - .splashscreen-content { - position: absolute; - top: 50%; - left: 50%; - display: flex; - align-items: center; - gap: 20px; - transform: translate(-50%, -50%); - - .splashscreen-logo { - width: 100px; - } - - .splashscreen-text { - font-size: 5rem; - line-height: 5rem; - margin-top: -5px; - text-wrap: nowrap; - } - } -} - -body { - overflow: hidden; - cursor: default; - - :not(input):not(textarea), - :not(input):not(textarea)::after, - :not(input):not(textarea)::before { - -webkit-user-select: none; - user-select: none; - } -} - -hr { - margin: 5px 0; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background-color: transparent; -} - -::-webkit-scrollbar-thumb { - background-color: var(--color-gray-500); - border-radius: 6px; -} - -::-webkit-scrollbar-thumb:hover { - background-color: var(--color-gray-600); -} - -b { - font-weight: 600; -} - -.content { - height: 100%; - background: linear-gradient(217deg, var(--color-red-100), rgba(255, 0, 0, 0) 70%), - linear-gradient(127deg, var(--color-blue-100), rgba(0, 0, 255, 0) 70%), - linear-gradient(336deg, var(--color-cyan-100), rgba(0, 255, 0, 0) 70%); - padding: 10px; - overflow: auto; -} - -.ant-select { - min-width: 100px; -} - -.ant-input { - min-width: 90px; -} - -.ant-color-picker-trigger { - min-width: 90px !important; -} - -.ant-modal-body { - max-height: 65vh !important; - overflow-y: auto !important; -} - -.ant-modal-content { - padding: 12px 24px !important; -} +#root { + display: grid; + grid-template-columns: min-content 1fr; + grid-template-rows: 50px 1fr; + height: 100vh; + width: 100vw; +} + +#splashscreen { + will-change: contents; + position: fixed; + z-index: 1000; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: var(--color-gray-50); + transition: opacity 300ms linear; + opacity: 1; + + &.hidden { + display: none; + } + + &.vanish { + opacity: 0; + } + + .splashscreen-image { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +body { + overflow: hidden; + cursor: default; + + :not(input):not(textarea), + :not(input):not(textarea)::after, + :not(input):not(textarea)::before { + -webkit-user-select: none; + user-select: none; + } +} + +hr { + margin: 5px 0; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: var(--color-gray-500); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: var(--color-gray-600); +} + +b { + font-weight: 600; +} + +.content { + height: 100%; + background: linear-gradient(217deg, var(--color-red-100), rgba(255, 0, 0, 0) 70%), + linear-gradient(127deg, var(--color-blue-100), rgba(0, 0, 255, 0) 70%), + linear-gradient(336deg, var(--color-cyan-100), rgba(0, 255, 0, 0) 70%); + padding: 10px; + overflow: auto; +} + +.ant-select { + min-width: 100px; +} + +.ant-input { + min-width: 90px; +} + +.ant-color-picker-trigger { + min-width: 90px !important; +} + +.ant-modal-body { + max-height: 65vh !important; + overflow-y: auto !important; +} + +.ant-modal-content { + padding: 12px 24px !important; +} diff --git a/src/apps/settings/styles/reset.css b/src/apps/settings/styles/reset.css deleted file mode 100644 index 00912b16..00000000 --- a/src/apps/settings/styles/reset.css +++ /dev/null @@ -1,84 +0,0 @@ -:root { - font-size: 100%; - --main-typo: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --primary-color: var(--color-gray-900); - --secondary-color: var(--color-gray-50); -} - -*, *:after, *:before { - margin: 0; - padding: 0; - border: 0; - outline: none; - box-sizing: border-box; - vertical-align: baseline; -} - -img, image, picture, video, iframe, figure { - max-width: 100%; - width: 100%; - display: block; -} - -a { - display: block; -} - -p a { - display: inline; -} - -li { - list-style-type: none; -} - -html { - scroll-behavior: smooth; -} - -h1, h2, h3, h4, h5, h6, p, span, a, strong, blockquote, i, b, em, pre { - font-size: 1em; - font-weight: inherit; - font-style: inherit; - text-decoration: none; - color: inherit; -} - -form, input, textarea, select, button, label { - font-family: inherit; - font-size: inherit; - hyphens: auto; - background-color: transparent; - display: block; - color: inherit; -} - -table, tr, td { - border-collapse: collapse; - border-spacing: 0; -} - -svg { - width: 100%; - display: block; - fill: currentColor; -} - -body { - font-size: 1em; - line-height: 1.4em; - font-family: var(--main-typo); - color: var(--primary-color); - background-color: var(--secondary-color); - hyphens: auto; - font-smooth: always; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -hr { - border: 1px solid; - margin: 1em 0; - opacity: 0.8; - color: var(--color-gray-200); -} \ No newline at end of file diff --git a/src/apps/shared/ConsoleWrapper.ts b/src/apps/shared/ConsoleWrapper.ts index 4ca6f219..b9f583b6 100644 --- a/src/apps/shared/ConsoleWrapper.ts +++ b/src/apps/shared/ConsoleWrapper.ts @@ -1,69 +1,58 @@ -import { getCurrentWebview } from '@tauri-apps/api/webview'; -import * as Logger from '@tauri-apps/plugin-log'; - -export function wrapConsole() { - const WebConsole = { - info: console.info, - warn: console.warn, - error: console.error, - debug: console.debug, - trace: console.trace, - }; - - const label = getCurrentWebview().label; - const StringifyParams = (params: any[]): string => { - return label + ':' + params.reduce((a, b) => { - if (typeof b === 'string') { - return a + ' ' + b; - } - return a + ' ' + JSON.stringify(b, null, 2); - }, ''); - }; - - window.addEventListener('unhandledrejection', (event) => { - console.error(`Unhandled Rejection - ${event.reason}`); - }); - - console.error = (...params: any[]) => { - WebConsole.error(...params); - Logger.error(StringifyParams(params)); - }; - - console.warn = (...params: any[]) => { - WebConsole.warn(...params); - Logger.warn(StringifyParams(params)); - }; - - console.info = (...params: any[]) => { - WebConsole.info(...params); - Logger.info(StringifyParams(params)); - }; - - console.debug = (...params: any[]) => { - WebConsole.debug(...params); - Logger.debug(StringifyParams(params)); - }; - - console.trace = (...params: any[]) => { - WebConsole.trace(...params); - Logger.trace(StringifyParams(params)); - }; - - disableRefreshAndContextMenu(); -} - -export function disableRefreshAndContextMenu() { - document.addEventListener('keydown', function (event) { - if ( - event.key === 'F5' || - (event.ctrlKey && event.key === 'r') || - (event.metaKey && event.key === 'r') - ) { - event.preventDefault(); - } - }); - - document.addEventListener('contextmenu', function (event) { - event.preventDefault(); - }); -} +import { getCurrentWebview } from '@tauri-apps/api/webview'; +import * as Logger from '@tauri-apps/plugin-log'; +import { disableWebviewShortcutsAndContextMenu } from 'seelen-core'; + +export function wrapConsole() { + const WebConsole = { + info: console.info, + warn: console.warn, + error: console.error, + debug: console.debug, + trace: console.trace, + }; + + const label = getCurrentWebview().label; + const StringifyParams = (params: any[]): string => { + return ( + label + + ':' + + params.reduce((a, b) => { + if (typeof b === 'string') { + return a + ' ' + b; + } + return a + ' ' + JSON.stringify(b, null, 2); + }, '') + ); + }; + + window.addEventListener('unhandledrejection', (event) => { + console.error(`Unhandled Rejection - ${event.reason}`); + }); + + console.error = (...params: any[]) => { + WebConsole.error(...params); + Logger.error(StringifyParams(params)); + }; + + console.warn = (...params: any[]) => { + WebConsole.warn(...params); + Logger.warn(StringifyParams(params)); + }; + + console.info = (...params: any[]) => { + WebConsole.info(...params); + Logger.info(StringifyParams(params)); + }; + + console.debug = (...params: any[]) => { + WebConsole.debug(...params); + Logger.debug(StringifyParams(params)); + }; + + console.trace = (...params: any[]) => { + WebConsole.trace(...params); + Logger.trace(StringifyParams(params)); + }; + + disableWebviewShortcutsAndContextMenu(); +} diff --git a/src/apps/shared/StateBuilder.ts b/src/apps/shared/StateBuilder.ts index 713327be..539bb1c8 100644 --- a/src/apps/shared/StateBuilder.ts +++ b/src/apps/shared/StateBuilder.ts @@ -1,100 +1,75 @@ -import { Action, ActionReducerMapBuilder, CaseReducer, PayloadAction, Slice } from '@reduxjs/toolkit'; -import { cast, isStrictObject, prettify, TupleReduce } from 'readable-types'; - -export type SelectorsFor = { [K in keyof T]: (state: T) => T[K] }; -export type ReducersFor = { - [K in keyof T as `set${Capitalize>}`]: CaseReducer>; -}; - -export const capitalize = (text: string) => { - return text.slice(0, 1).toUpperCase() + text.slice(1); -}; -export const matcher = (slice: Slice) => (action: Action) => action.type.startsWith(slice.name); - -interface $GetState extends $<[acc: Record, current: Slice]> { - return: this[0] & { [x in this[1]['name']]: ReturnType }; -} - -export type SelectorFor2 = $if, { - then: ((state: State) => Current) & { [K in keyof Current]: SelectorFor2 }; - else: (state: State) => Current; -}>; - -export class StateBuilder { - static selectorsFor(state: T): SelectorsFor { - const selectors = {} as SelectorsFor; - for (const key in state) { - selectors[key] = (state: T) => state[key]; - } - return selectors; - } - - static reducersFor(state: T): ReducersFor { - const reducers: any = {}; - for (const key in state) { - reducers[`set${capitalize(key)}`] = (state: T, action: any) => { - state[key] = action.payload; - }; - } - return reducers; - } - - static addSliceAsExtraReducer(slice: Slice, builder: ActionReducerMapBuilder<{ [x in Slice['name']]: any }>) { - builder.addMatcher(matcher(slice), (state, action) => { - state[slice.name] = slice.reducer(state[slice.name], action); - }); - } - - static compositeInitialState>(...slices: [...T]): prettify>; - static compositeInitialState(...slices: Slice[]) { - return slices.reduce((acc, slice) => { - acc[slice.name] = slice.getInitialState(); - return acc; - }, {} as anyObject); - } - - static compositeSelector(obj: T, selfSelector: any = (self: any) => self): SelectorFor2 { - for (const key in obj) { - selfSelector[key] = (state: any) => selfSelector(state)[key]; - - if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { - selfSelector[key] = StateBuilder.compositeSelector(obj[key], selfSelector[key]); - } - } - return selfSelector; - } - -/* - children: Slices = [] as any; - - constructor(public name: Name) {}; - - appendChild(child: T): StateBuilder { - this.children.push(child); - return this as any; - } - - static addSlicesAsChildren(builder: ActionReducerMapBuilder, slices: Slice[]) { - slices.forEach((slice) => { - builder.addMatcher(matcher(slice), (state, action) => { - state[slice.name] = slice.reducer(state[slice.name], action); - }); - }); - } - - build() { - const slices = this.children; - - const initialState = {}; - - return createSlice({ - name: this.name, - initialState, - reducers: reducersFor(initialState), - selectors: selectorsFor(initialState), - extraReducers(builder) { - StateBuilder.addSlicesAsChildren(builder, slices); - }, - }); - }; */ -} +import { + Action, + ActionReducerMapBuilder, + CaseReducer, + PayloadAction, + Slice, +} from '@reduxjs/toolkit'; +import { cast, isStrictObject, prettify, TupleReduce } from 'readable-types'; + +type ReducersFor = { + [K in keyof T as `set${Capitalize>}`]: CaseReducer>; +}; + +const capitalize = (text: string) => { + return text.slice(0, 1).toUpperCase() + text.slice(1); +}; + +const matcher = (slice: Slice) => (action: Action) => action.type.startsWith(slice.name); + +interface $GetState extends $<[acc: Record, current: Slice]> { + return: this[0] & { [x in this[1]['name']]: ReturnType }; +} + +export type SelectorFor2 = $if< + isStrictObject, + { + then: ((state: State) => Current) & { [K in keyof Current]: SelectorFor2 }; + else: (state: State) => Current; + } +>; + +export class StateBuilder { + static reducersFor(state: T): ReducersFor { + const reducers: any = {}; + for (const key in state) { + reducers[`set${capitalize(key)}`] = (state: T, action: any) => { + state[key] = action.payload; + }; + } + return reducers; + } + + static addSliceAsExtraReducer( + slice: Slice, + builder: ActionReducerMapBuilder<{ [x in Slice['name']]: any }>, + ) { + builder.addMatcher(matcher(slice), (state, action) => { + state[slice.name] = slice.reducer(state[slice.name], action); + }); + } + + static compositeInitialState>( + ...slices: [...T] + ): prettify>; + static compositeInitialState(...slices: Slice[]) { + return slices.reduce((acc, slice) => { + acc[slice.name] = slice.getInitialState(); + return acc; + }, {} as anyObject); + } + + static compositeSelector( + obj: T, + selfSelector: any = (self: any) => self, + ): SelectorFor2 { + for (const key in obj) { + selfSelector[key] = (state: any) => selfSelector(state)[key]; + + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + selfSelector[key] = StateBuilder.compositeSelector(obj[key], selfSelector[key]); + } + } + return selfSelector; + } +} diff --git a/src/apps/shared/Timing.ts b/src/apps/shared/Timing.ts deleted file mode 100644 index ae600ea9..00000000 --- a/src/apps/shared/Timing.ts +++ /dev/null @@ -1,44 +0,0 @@ -export function throttle( - func: T, - delay: number, -): T { - let lastInvokeTime = 0; - let timeoutId: ReturnType | null = null; - - return function (this: ThisParameterType, ...args: Parameters) { - const now = Date.now(); - - if (now - lastInvokeTime < delay) { - if (timeoutId) { - clearTimeout(timeoutId); - } - - timeoutId = setTimeout(() => { - lastInvokeTime = now; - func.apply(this, args); - }, delay); - } else { - lastInvokeTime = now; - func.apply(this, args); - } - } as T; -} - -export interface TimeoutIdRef { - current: ReturnType | null; -} - -export function debounce any>( - func: T, - delay: number, - timeoutId: TimeoutIdRef = { current: null }, -): (...args: Parameters) => void { - return function debouncedFunction(this: ThisParameterType, ...args: Parameters) { - const context = this; - - clearTimeout(timeoutId.current!); - timeoutId.current = setTimeout(function () { - func.apply(context, args); - }, delay); - }; -} diff --git a/src/apps/shared/components/Icon/index.module.css b/src/apps/shared/components/Icon/index.module.css index f0265d44..fd6ef30c 100644 --- a/src/apps/shared/components/Icon/index.module.css +++ b/src/apps/shared/components/Icon/index.module.css @@ -1,6 +1,6 @@ -.icon { - > svg { - font-size: 1rem; - vertical-align: middle; - } -} +.icon { + height: 1rem; + > svg { + vertical-align: middle; + } +} diff --git a/src/apps/shared/components/Icon/index.tsx b/src/apps/shared/components/Icon/index.tsx index 61ea59d9..6f2f31e5 100644 --- a/src/apps/shared/components/Icon/index.tsx +++ b/src/apps/shared/components/Icon/index.tsx @@ -1,100 +1,24 @@ -import { IconBaseProps } from 'react-icons'; -import * as ai from 'react-icons/ai'; -import * as bi from 'react-icons/bi'; -import * as bs from 'react-icons/bs'; -import * as cg from 'react-icons/cg'; -import * as ci from 'react-icons/ci'; -import * as di from 'react-icons/di'; -import * as fa from 'react-icons/fa'; -import * as fa6 from 'react-icons/fa6'; -import * as fc from 'react-icons/fc'; -import * as fi from 'react-icons/fi'; -import * as gi from 'react-icons/gi'; -import * as go from 'react-icons/go'; -import * as gr from 'react-icons/gr'; -import * as hi from 'react-icons/hi'; -import * as hi2 from 'react-icons/hi2'; -import * as im from 'react-icons/im'; -import * as io from 'react-icons/io'; -import * as io5 from 'react-icons/io5'; -import * as lia from 'react-icons/lia'; -import * as lu from 'react-icons/lu'; -import * as md from 'react-icons/md'; -import * as pi from 'react-icons/pi'; -import * as ri from 'react-icons/ri'; -import * as rx from 'react-icons/rx'; -import * as si from 'react-icons/si'; -import * as sl from 'react-icons/sl'; -import * as tb from 'react-icons/tb'; -import * as tfi from 'react-icons/tfi'; -import * as ti from 'react-icons/ti'; -import * as vsc from 'react-icons/vsc'; -import * as wi from 'react-icons/wi'; - -import cs from './index.module.css'; - -export type IconName = keyof typeof icons; -const icons = { - ...ai, - ...bi, - ...bs, - ...cg, - ...ci, - ...di, - ...fa, - ...fa6, - ...fc, - ...fi, - ...gi, - ...go, - ...gr, - ...hi, - ...hi2, - ...im, - ...io, - ...io5, - ...lia, - ...lu, - ...md, - ...pi, - ...ri, - ...rx, - ...si, - ...sl, - ...tb, - ...tfi, - ...ti, - ...vsc, - ...wi, -}; - -export const exposedIcons = Object.keys(icons).reduce((acc, icon) => { - acc[icon] = `[ICON:${icon}]`; - return acc; -}, {} as any); - -export function isValidIconName(str: string) { - const [name] = str.split(':'); - return !!icons[name as IconName]; -} - -interface typesPropsIcon { - iconName: IconName; - propsIcon?: IconBaseProps; -} - -export function Icon(props: typesPropsIcon) { - const { iconName, propsIcon } = props; - - const Icon = icons[iconName] || null; - - if (!Icon) { - return null; - } - - return ( - - - - ); -} +import { HTMLAttributes } from 'react'; + +import { cx } from '../../styles'; +import InlineSVG from '../InlineSvg'; +import cs from './index.module.css'; + +interface typesPropsIcon extends HTMLAttributes { + iconName: string; + size?: string | number; + color?: string; +} + +export function Icon(props: typesPropsIcon) { + const { iconName, size, color, className, ...rest } = props; + + return ( + + ); +} diff --git a/src/apps/shared/components/InlineSvg/index.module.css b/src/apps/shared/components/InlineSvg/index.module.css new file mode 100644 index 00000000..f7387bf6 --- /dev/null +++ b/src/apps/shared/components/InlineSvg/index.module.css @@ -0,0 +1,6 @@ +.inlineSvg { + > svg { + width: 100%; + height: 100%; + } +} \ No newline at end of file diff --git a/src/apps/shared/components/InlineSvg/index.tsx b/src/apps/shared/components/InlineSvg/index.tsx new file mode 100644 index 00000000..619e2796 --- /dev/null +++ b/src/apps/shared/components/InlineSvg/index.tsx @@ -0,0 +1,45 @@ +import { HTMLAttributes, useEffect, useState } from 'react'; + +import { cx } from '../../../../apps/shared/styles'; + +import cs from './index.module.css'; + +interface Props extends HTMLAttributes { + src: string; +} + +const InlineSVG = ({ src, className, ...rest }: Props) => { + const [svgContent, setSvgContent] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchSVG = async () => { + try { + const response = await fetch(src); + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.statusText}`); + } + const svgText = await response.text(); + setSvgContent(svgText); + } catch (err: any) { + setError(err?.message); + } + }; + + fetchSVG(); + }, [src]); + + if (error || !svgContent) { + return null; + } + + return ( + + ); +}; + +export default InlineSVG; diff --git a/src/apps/shared/components/OverflowTooltip/index.tsx b/src/apps/shared/components/OverflowTooltip/index.tsx index 39d483c8..7013cd76 100644 --- a/src/apps/shared/components/OverflowTooltip/index.tsx +++ b/src/apps/shared/components/OverflowTooltip/index.tsx @@ -1,7 +1,7 @@ -import { cx } from '../../styles'; import { Tooltip, TooltipProps } from 'antd'; -import { useEffect, useRef, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; +import { cx } from '../../styles'; import cs from './index.module.css'; interface Props { @@ -12,7 +12,7 @@ interface Props { arrow?: TooltipProps['arrow']; } -export function OverflowTooltip({ text, className, ...rest }: Props) { +function _OverflowTooltip({ text, className, ...rest }: Props) { const textRef = useRef(null); const [isOverflow, setIsOverflow] = useState(false); @@ -34,3 +34,5 @@ export function OverflowTooltip({ text, className, ...rest }: Props) { ); } + +export const OverflowTooltip = memo(_OverflowTooltip); \ No newline at end of file diff --git a/src/apps/shared/events.ts b/src/apps/shared/events.ts deleted file mode 100644 index b35c733d..00000000 --- a/src/apps/shared/events.ts +++ /dev/null @@ -1,11 +0,0 @@ - -export enum FileChange { - Settings = 'settings-changed', - Themes = 'themes', - WegItems = 'weg-items', - Placeholders = 'placeholders', -} - -export enum GlobalEvent { - FocusChanged = 'global-focus-changed', -} \ No newline at end of file diff --git a/src/apps/shared/index.ts b/src/apps/shared/index.ts index fa4e0883..56407701 100644 --- a/src/apps/shared/index.ts +++ b/src/apps/shared/index.ts @@ -1,82 +1,13 @@ -import { UserSettings } from '../../shared.interfaces'; -import { Theme } from './schemas/Theme'; -import { path } from '@tauri-apps/api'; -import { PhysicalSize } from '@tauri-apps/api/dpi'; -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; - -export function getRootContainer() { - const container = document.getElementById('root'); - if (!container) { - throw new Error('Root container not found'); - } - return container; -} - -export function toPhysicalPixels(size: number): number { - return Math.floor(size * window.devicePixelRatio); -} - -export async function wasInstalledUsingMSIX() { - let installPath = await path.resourceDir(); - return installPath.startsWith('C:\\Program Files\\WindowsApps'); -} - -export const setWindowAsFullSize = () => { - const screenWidth = toPhysicalPixels(window.screen.width); - const screenHeight = toPhysicalPixels(window.screen.height); - getCurrentWebviewWindow().setSize(new PhysicalSize(screenWidth, screenHeight)); -}; - -export function setColorsAsCssVariables(colors: anyObject) { - for (const [key, value] of Object.entries(colors)) { - if (typeof value !== 'string') { - continue; - } - let hex = value.replace('#', '').slice(0, 6); - var color = parseInt(hex, 16); - var r = (color >> 16) & 255; - var g = (color >> 8) & 255; - var b = color & 255; - // replace rust snake case with kebab case - let name = key.replace('_', '-'); - document.documentElement.style.setProperty(`--config-${name}-color`, value.slice(0, 7)); - document.documentElement.style.setProperty(`--config-${name}-color-rgb`, `${r}, ${g}, ${b}`); - } -} - -export function loadThemeCSS(config: Pick) { - let selected = config.jsonSettings.selectedTheme; - let themes: Theme[] = config.themes - .filter((theme) => selected.includes(theme.info.filename)) - .sort((a, b) => { - return selected.indexOf(a.info.filename) - selected.indexOf(b.info.filename); - }); - - if (themes.length === 0) { - let defaultTheme = config.themes.find((theme) => theme.info.filename === 'default'); - themes = defaultTheme ? [defaultTheme] : []; - } - - const label = getCurrentWebviewWindow().label; - let theme_key: keyof Theme['styles'] | null = null; - if (label.startsWith('fancy-toolbar')) { - theme_key = 'toolbar'; - } else if (label.startsWith('seelenweg')) { - theme_key = 'weg'; - } else if (label.startsWith('window-manager')) { - theme_key = 'wm'; - } - - if (!theme_key) { - return; - } - - document.getElementById(theme_key)?.remove(); - let element = document.createElement('style'); - element.id = theme_key.toString(); - element.textContent = ''; - document.head.appendChild(element); - for (const theme of themes) { - element.textContent += theme.styles[theme_key] + '\n'; - } -} +import { path } from '@tauri-apps/api'; +import { getRootElement } from 'seelen-core'; + +export const getRootContainer = getRootElement; + +export function toPhysicalPixels(size: number): number { + return Math.round(size * window.devicePixelRatio); +} + +export async function wasInstalledUsingMSIX() { + let installPath = await path.resourceDir(); + return installPath.startsWith('C:\\Program Files\\WindowsApps'); +} diff --git a/src/apps/shared/interfaces/common.ts b/src/apps/shared/interfaces/common.ts index 6bba0246..5acdca08 100644 --- a/src/apps/shared/interfaces/common.ts +++ b/src/apps/shared/interfaces/common.ts @@ -1,7 +1,3 @@ -export interface IModule { - enable: boolean; -} - export interface FocusedApp { hwnd: number; name: string; diff --git a/src/apps/shared/redux.ts b/src/apps/shared/redux.ts deleted file mode 100644 index 2bcf7e25..00000000 --- a/src/apps/shared/redux.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CaseReducerActions, SliceCaseReducers } from '@reduxjs/toolkit'; -import { Event, listen } from '@tauri-apps/api/event'; -import { Store } from 'redux'; - -export class TauriReduxExtension, Name extends string> { - constructor(private store: Store, private actions: CaseReducerActions) {} - - handleAction(action: K, event: Event) { - const actionObj = this.actions[action](event.payload); - this.store.dispatch(actionObj); - } - - /** Actions exposed to Tauri as `redux://{KeyOfActionCreator}` */ - async globalExpose(...x: [K]) { - let action = x[0]; - if (typeof action !== 'string') { - return; - } - await listen(`redux://${action}`, this.handleAction.bind(this, action)); - } -} diff --git a/src/apps/shared/schemas/AppsConfigurations.ts b/src/apps/shared/schemas/AppsConfigurations.ts deleted file mode 100644 index 4b0bf7cc..00000000 --- a/src/apps/shared/schemas/AppsConfigurations.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { z } from 'zod'; - -export enum ApplicationIdentifier { - Exe = 'Exe', - Class = 'Class', - Title = 'Title', - Path = 'Path', -} - -export enum MatchingStrategy { - Legacy = 'Legacy', - Equals = 'Equals', - StartsWith = 'StartsWith', - EndsWith = 'EndsWith', - Contains = 'Contains', - Regex = 'Regex', -} - -function stringInsensitiveToEnum>(value: string, enumObj: Enum) { - return Object.values(enumObj).find((v) => v.toLocaleLowerCase() === value.toLocaleLowerCase()) as Enum[keyof Enum] | undefined; -} - -interface _IdWithIdentifier { - id: string; - kind: ApplicationIdentifier; - matching_strategy: MatchingStrategy; - negation: boolean; - and: _IdWithIdentifier[]; - or: _IdWithIdentifier[]; -} - -export const IdWithIdentifierSchema = z.object({ - id: z.string().default('new-app.exe'), - kind: z - .string() - .transform((arg) => stringInsensitiveToEnum(arg, ApplicationIdentifier)) - .default(ApplicationIdentifier.Exe), - matching_strategy: z - .string() - .transform((arg) => stringInsensitiveToEnum(arg, MatchingStrategy)) - .default(MatchingStrategy.Equals), - negation: z.boolean().default(false), - and: z.array(z.lazy(() => IdWithIdentifierSchema)).default([]), - or: z.array(z.lazy(() => IdWithIdentifierSchema)).default([]), -}) as z.ZodType<_IdWithIdentifier>; - -export interface IdWithIdentifier { - id: _IdWithIdentifier['id']; - kind: _IdWithIdentifier['kind']; - matchingStrategy: _IdWithIdentifier['matching_strategy']; - negation: _IdWithIdentifier['negation']; - and: IdWithIdentifier[]; - or: IdWithIdentifier[]; -} - -export class IdWithIdentifier { - static default(): IdWithIdentifier { - return { - id: 'new-app.exe', - kind: ApplicationIdentifier.Exe, - matchingStrategy: MatchingStrategy.Equals, - negation: false, - and: [], - or: [], - }; - } -} \ No newline at end of file diff --git a/src/apps/shared/schemas/FancyToolbar.ts b/src/apps/shared/schemas/FancyToolbar.ts deleted file mode 100644 index 48b7b3a3..00000000 --- a/src/apps/shared/schemas/FancyToolbar.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AppBarHideMode } from './Seelenweg'; -import z from 'zod'; - -export const FancyToolbarSchema = z.object({ - enabled: z.boolean().default(true), - height: z.number().positive().default(30), - placeholder: z.string().nullable().default(null), - hideMode: z.nativeEnum(AppBarHideMode).default(AppBarHideMode.Never), -}); - -export type FancyToolbar = z.infer; diff --git a/src/apps/shared/schemas/Layout.ts b/src/apps/shared/schemas/Layout.ts deleted file mode 100644 index a953a226..00000000 --- a/src/apps/shared/schemas/Layout.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { CreatorInfoSchema } from '.'; -import { modify } from 'readable-types'; -import z from 'zod'; - -export enum NodeType { - Vertical = 'Vertical', - Horizontal = 'Horizontal', - Leaf = 'Leaf', - Stack = 'Stack', - Fallback = 'Fallback', -} - -export enum NodeSubtype { - Temporal = 'Temporal', - Permanent = 'Permanent', -} - -export enum NoFallbackBehavior { - Float = 'Float', - Unmanaged = 'Unmanaged', -} - -export const hwndSchema = z.number().nonnegative().describe('Window handle'); - -export type BaseNode = z.infer; -const BaseNodeSchema = z.object({ - type: z.nativeEnum(NodeType), - subtype: z.nativeEnum(NodeSubtype).default(NodeSubtype.Permanent), - priority: z - .number() - .positive() - .describe('Order in how the tree will be traversed (1 = first, 2 = second, etc.)') - .default(1), - growFactor: z.number().describe('How much of the remaining space this node will take').default(1), - condition: z.string().optional().nullable().describe('Math Condition for the node to be shown, e.g: n >= 3'), -}); - -export type StackNode = z.infer; -const StackNodeSchema = BaseNodeSchema.extend({ - type: z.literal(NodeType.Stack), - active: hwndSchema.nullable().default(null), - handles: z.array(hwndSchema).default([]), -}); - -export type FallbackNode = z.infer; -const FallbackNodeSchema = BaseNodeSchema.extend({ - type: z.literal(NodeType.Fallback), - subtype: z.literal(NodeSubtype.Permanent).default(NodeSubtype.Permanent), - active: hwndSchema.nullable().default(null), - handles: z.array(hwndSchema).default([]), -}); - -export type LeafNode = z.infer; -const LeafNodeSchema = BaseNodeSchema.extend({ - type: z.literal(NodeType.Leaf), - handle: hwndSchema.nullable().default(null), -}); - -export type HorizontalBranchNode = BaseNode & { type: NodeType.Horizontal; children: Node[] }; -const HorizontalBranchNodeSchema = BaseNodeSchema.extend({ - type: z.literal(NodeType.Horizontal), - children: z.array(z.lazy(() => NodeSchema)).min(1), -}) as z.ZodType; - -export type VerticalBranchNode = BaseNode & { type: NodeType.Vertical; children: Node[] }; -const VerticalBranchNodeSchema = BaseNodeSchema.extend({ - type: z.literal(NodeType.Vertical), - children: z.array(z.lazy(() => NodeSchema)).min(1), -}) as z.ZodType; - -export type Node = z.infer; -export const NodeSchema = z.union([ - StackNodeSchema, - FallbackNodeSchema, - LeafNodeSchema, - HorizontalBranchNodeSchema, - VerticalBranchNodeSchema, -]).describe('The layout tree'); - -type InnerLayout = z.infer; -export const LayoutSchema = z.object({ - info: CreatorInfoSchema.default({}), - structure: NodeSchema.default({ type: NodeType.Fallback }), - no_fallback_behavior: z.nativeEnum(NoFallbackBehavior).optional().nullable(), -}); - -export interface Layout { - info: modify; - structure: InnerLayout['structure']; - noFallbackBehavior: InnerLayout['no_fallback_behavior']; -} \ No newline at end of file diff --git a/src/apps/shared/schemas/Monitors.ts b/src/apps/shared/schemas/Monitors.ts deleted file mode 100644 index a62dd2ae..00000000 --- a/src/apps/shared/schemas/Monitors.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RectSchema } from './WindowManager'; -import z from 'zod'; - -export type Workspace = z.infer; -export const WorkspaceSchema = z.object({ - name: z.string().default('New Workspace'), - layout: z.string().default('BSP'), - padding: z.number().nonnegative().optional().nullable(), - gap: z.number().nonnegative().optional().nullable(), -}); - -type InnerMonitor = z.infer; -export const MonitorSchema = z.object({ - workspaces: z.array(WorkspaceSchema).min(1).default([WorkspaceSchema.parse({})]), - work_area_offset: RectSchema.optional().nullable(), - editing_workspace: z.number().nonnegative().default(0), -}); - -export interface Monitor { - workAreaOffset: InnerMonitor['work_area_offset']; - workspaces: InnerMonitor['workspaces']; - edditingWorkspace: InnerMonitor['editing_workspace']; -} \ No newline at end of file diff --git a/src/apps/shared/schemas/Placeholders.ts b/src/apps/shared/schemas/Placeholders.ts deleted file mode 100644 index 53f25246..00000000 --- a/src/apps/shared/schemas/Placeholders.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { CreatorInfoSchema } from '.'; -import z from 'zod'; - -export enum ToolbarModuleType { - // generic types - Generic = 'generic', - Text = 'text', - // special types - Date = 'date', - Power = 'power', - Settings = 'settings', - Network = 'network', - Workspaces = 'workspaces', - Media = 'media', - Tray = 'tray', - Device = 'device', - Notifications = 'notifications', -} - -export enum WorkspaceTMMode { - Dotted = 'dotted', - Named = 'named', - Numbered = 'numbered', -} - -export enum TimeUnit { - SECOND = 'second', - MINUTE = 'minute', - HOUR = 'hour', - DAY = 'day', -} - -export enum DeviceTMSubType { - Disk = 'disk', - CPU = 'cpu', - Memory = 'memory', -} - -export const BaseTMSchema = z.object({ - id: z.string().default(() => crypto.randomUUID()), - type: z.nativeEnum(ToolbarModuleType), - template: z - .string() - .transform((value) => value.trimEnd()) - .refine((value) => !value.endsWith('\n'), { - message: 'Template must not end with a newline', - }) - .default('"Unset"'), - tooltip: z.string().nullable().default(null), - badge: z.string().nullable().default(null), - onClick: z.string().nullable().default(null).describe('Deprecated, use `onClickV2` instead'), - onClickV2: z.string().nullable().default(null), - style: z.record(z.string(), z.any()).default({}), -}); - -export type GenericToolbarModule = z.infer; -export const GenericToolbarModuleSchema = BaseTMSchema.extend({ - type: z.union([z.literal(ToolbarModuleType.Generic), z.literal(ToolbarModuleType.Text)]), -}); - -export type TrayTM = z.infer; -export const TrayTMSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Tray), -}); - -export type DateToolbarModule = z.infer; -export const DateToolbarModuleSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Date), - each: z - .nativeEnum(TimeUnit) - .describe('Time unit to update the showing date') - .default(TimeUnit.MINUTE), - format: z.string().default('MMM Do, HH:mm'), -}); - -export type PowerToolbarModule = z.infer; -export const PowerToolbarModuleSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Power), -}); - -export type NetworkTM = z.infer; -export const NetworkTMSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Network), - withWlanSelector: z - .boolean() - .describe('Show Wi-fi settings on click (overrides onClick property)') - .default(false), -}); - -export type MediaTM = z.infer; -export const MediaTMSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Media), - withMediaControls: z.boolean().default(false), -}); - -export type NotificationsTM = z.infer; -export const NotificationsTMSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Notifications), -}); - -export type DeviceTM = z.infer; -export const DeviceTMSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Device), -}); - -export type SettingsToolbarModule = z.infer; -export const SettingsToolbarModuleSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Settings), -}); - -export type WorkspacesTM = z.infer; -export const WorkspaceTMSchema = BaseTMSchema.extend({ - type: z.literal(ToolbarModuleType.Workspaces), - mode: z.nativeEnum(WorkspaceTMMode).default(WorkspaceTMMode.Numbered), -}); - -export type ToolbarModule = z.infer; -export const ToolbarModuleSchema = z.union([ - GenericToolbarModuleSchema, - DateToolbarModuleSchema, - PowerToolbarModuleSchema, - SettingsToolbarModuleSchema, - WorkspaceTMSchema, - TrayTMSchema, - NetworkTMSchema, - MediaTMSchema, - DeviceTMSchema, - NotificationsTMSchema, -]); - -type InnerPlaceholder = z.infer; -export const PlaceholderSchema = z.object({ - info: CreatorInfoSchema.default({}), - left: z.array(ToolbarModuleSchema).default([]), - center: z.array(ToolbarModuleSchema).default([]), - right: z.array(ToolbarModuleSchema).default([]), -}); - -export interface Placeholder extends InnerPlaceholder {} \ No newline at end of file diff --git a/src/apps/shared/schemas/SeelenWegItems.ts b/src/apps/shared/schemas/SeelenWegItems.ts deleted file mode 100644 index 0e3601b5..00000000 --- a/src/apps/shared/schemas/SeelenWegItems.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { z } from 'zod'; - -export enum SwItemType { - PinnedApp = 'PinnedApp', - TemporalApp = 'TemporalPin', - Separator = 'Separator', - Media = 'Media', - Start = 'StartMenu', -} - -export type SavedPinnedApp = z.infer; -const PinnedAppSchema = z.object({ - type: z.literal(SwItemType.PinnedApp), - /** Path to executable */ - exe: z.string(), - /** Path to execute the app using explorer.exe (uwp apps starts with `shell:AppsFolder`) */ - execution_path: z.string(), -}); - -export type SavedSeparatorItem = z.infer; -const SeparatorSchema = z.object({ - type: z.literal(SwItemType.Separator), -}); - -export type SavedMediaItem = z.infer; -const MediaItemSchema = z.object({ - type: z.literal(SwItemType.Media), -}); - -export type StartMenuItem = z.infer; -const StartMenuItemSchema = z.object({ - type: z.literal(SwItemType.Start), -}); - -export type SwSavedItem = z.infer; -export const SwSavedItemSchema = z.union([ - PinnedAppSchema, - SeparatorSchema, - MediaItemSchema, - StartMenuItemSchema, -]); - -export interface SwSaveFile extends z.infer {} -export const SwSaveFileSchema = z.object({ - left: z.array(SwSavedItemSchema).default([ - { - type: SwItemType.Start, - }, - ]), - center: z.array(SwSavedItemSchema).default([]), - right: z.array(SwSavedItemSchema).default([ - { - type: SwItemType.Media, - }, - ]), -}); diff --git a/src/apps/shared/schemas/Seelenweg.ts b/src/apps/shared/schemas/Seelenweg.ts deleted file mode 100644 index e0df2186..00000000 --- a/src/apps/shared/schemas/Seelenweg.ts +++ /dev/null @@ -1,46 +0,0 @@ -import z from 'zod'; - -export enum SeelenWegMode { - FULL_WIDTH = 'Full-Width', - MIN_CONTENT = 'Min-Content', -} - -export enum AppBarHideMode { - Never = 'Never', - Always = 'Always', - OnOverlap = 'On-Overlap', -} - -export enum SeelenWegSide { - LEFT = 'Left', - RIGHT = 'Right', - TOP = 'Top', - BOTTOM = 'Bottom', -} - -export const SeelenWegSchema = z.object({ - enabled: z.boolean().default(true), - mode: z.nativeEnum(SeelenWegMode).default(SeelenWegMode.MIN_CONTENT), - hide_mode: z.nativeEnum(AppBarHideMode).default(AppBarHideMode.OnOverlap), - position: z.nativeEnum(SeelenWegSide).default(SeelenWegSide.BOTTOM), - visible_separators: z.boolean().default(true), - size: z.number().positive().default(40).describe('Item size in pixels'), - zoom_size: z.number().positive().default(70).describe('Zoomed item size in pixels'), - margin: z.number().nonnegative().default(8).describe('Dock/Bar margin in pixels'), - padding: z.number().nonnegative().default(8).describe('Dock/Bar padding in pixels'), - space_between_items: z.number().nonnegative().default(8).describe('Space between items (gap) in pixels'), -}); - -type inner = z.infer & {}; -export interface Seelenweg { - enabled: inner['enabled']; - mode: inner['mode']; - hideMode: inner['hide_mode']; - position: inner['position']; - visibleSeparators: inner['visible_separators']; - size: inner['size']; - zoomSize: inner['zoom_size']; - margin: inner['margin']; - padding: inner['padding']; - spaceBetweenItems: inner['space_between_items']; -} \ No newline at end of file diff --git a/src/apps/shared/schemas/Settings.ts b/src/apps/shared/schemas/Settings.ts deleted file mode 100644 index a9f0fe1a..00000000 --- a/src/apps/shared/schemas/Settings.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { FancyToolbar } from './FancyToolbar'; -import { Monitor } from './Monitors'; -import { Seelenweg } from './Seelenweg'; -import { WindowManager } from './WindowManager'; -import z from 'zod'; - -export type AhkVariables = Record; -export type AhkVar = z.infer; -export const AhkVarSchema = z.object({ - fancy: z.string(), - ahk: z.string(), -}); - -export const AhkVariablesSchema = z.object({ - // open_settings: AhkVarSchema.default({ fancy: 'Win + K', ahk: '#k' }), - // pause_wm: AhkVarSchema.default({ fancy: 'Win + Control + Alt + P', ahk: '^#!p' }), - // reservations - reserve_top: AhkVarSchema.default({ fancy: 'Win + Shift + I', ahk: '#+i' }), - reserve_bottom: AhkVarSchema.default({ fancy: 'Win + Shift + K', ahk: '#+k' }), - reserve_left: AhkVarSchema.default({ fancy: 'Win + Shift + J', ahk: '#+j' }), - reserve_right: AhkVarSchema.default({ fancy: 'Win + Shift + L', ahk: '#+l' }), - reserve_float: AhkVarSchema.default({ fancy: 'Win + Shift + U', ahk: '#+u' }), - reserve_stack: AhkVarSchema.default({ fancy: 'Win + Shift + O', ahk: '#+o' }), - // focus - focus_top: AhkVarSchema.default({ fancy: 'Win + Shift + W', ahk: '#+w' }), - focus_bottom: AhkVarSchema.default({ fancy: 'Win + Shift + S', ahk: '#+s' }), - focus_left: AhkVarSchema.default({ fancy: 'Win + Shift + A', ahk: '#+a' }), - focus_right: AhkVarSchema.default({ fancy: 'Win + Shift + D', ahk: '#+d' }), - focus_latest: AhkVarSchema.default({ fancy: 'Win + Shift + E', ahk: '#+e' }), - // window size - increase_width: AhkVarSchema.default({ fancy: 'Win + Alt + =', ahk: '#!=' }), - decrease_width: AhkVarSchema.default({ fancy: 'Win + Alt + -', ahk: '#!-' }), - increase_height: AhkVarSchema.default({ fancy: 'Win + Shift + =', ahk: '#+=' }), - decrease_height: AhkVarSchema.default({ fancy: 'Win + Shift + -', ahk: '#+-' }), - restore_sizes: AhkVarSchema.default({ fancy: 'Win + Alt + 0', ahk: '#!0' }), - // switch - switch_workspace_0: AhkVarSchema.default({ fancy: 'Alt + 1', ahk: '!1' }), - switch_workspace_1: AhkVarSchema.default({ fancy: 'Alt + 2', ahk: '!2' }), - switch_workspace_2: AhkVarSchema.default({ fancy: 'Alt + 3', ahk: '!3' }), - switch_workspace_3: AhkVarSchema.default({ fancy: 'Alt + 4', ahk: '!4' }), - switch_workspace_4: AhkVarSchema.default({ fancy: 'Alt + 5', ahk: '!5' }), - switch_workspace_5: AhkVarSchema.default({ fancy: 'Alt + 6', ahk: '!6' }), - switch_workspace_6: AhkVarSchema.default({ fancy: 'Alt + 7', ahk: '!7' }), - switch_workspace_7: AhkVarSchema.default({ fancy: 'Alt + 8', ahk: '!8' }), - switch_workspace_8: AhkVarSchema.default({ fancy: 'Alt + 9', ahk: '!9' }), - switch_workspace_9: AhkVarSchema.default({ fancy: 'Alt + 0', ahk: '!0' }), - // move - move_to_workspace_0: AhkVarSchema.default({ fancy: 'Alt + Shift + 1', ahk: '!+1' }), - move_to_workspace_1: AhkVarSchema.default({ fancy: 'Alt + Shift + 2', ahk: '!+2' }), - move_to_workspace_2: AhkVarSchema.default({ fancy: 'Alt + Shift + 3', ahk: '!+3' }), - move_to_workspace_3: AhkVarSchema.default({ fancy: 'Alt + Shift + 4', ahk: '!+4' }), - move_to_workspace_4: AhkVarSchema.default({ fancy: 'Alt + Shift + 5', ahk: '!+5' }), - move_to_workspace_5: AhkVarSchema.default({ fancy: 'Alt + Shift + 6', ahk: '!+6' }), - move_to_workspace_6: AhkVarSchema.default({ fancy: 'Alt + Shift + 7', ahk: '!+7' }), - move_to_workspace_7: AhkVarSchema.default({ fancy: 'Alt + Shift + 8', ahk: '!+8' }), - move_to_workspace_8: AhkVarSchema.default({ fancy: 'Alt + Shift + 9', ahk: '!+9' }), - move_to_workspace_9: AhkVarSchema.default({ fancy: 'Alt + Shift + 0', ahk: '!+0' }), - // send - send_to_workspace_0: AhkVarSchema.default({ fancy: 'Win + Shift + 1', ahk: '#+1' }), - send_to_workspace_1: AhkVarSchema.default({ fancy: 'Win + Shift + 2', ahk: '#+2' }), - send_to_workspace_2: AhkVarSchema.default({ fancy: 'Win + Shift + 3', ahk: '#+3' }), - send_to_workspace_3: AhkVarSchema.default({ fancy: 'Win + Shift + 4', ahk: '#+4' }), - send_to_workspace_4: AhkVarSchema.default({ fancy: 'Win + Shift + 5', ahk: '#+5' }), - send_to_workspace_5: AhkVarSchema.default({ fancy: 'Win + Shift + 6', ahk: '#+6' }), - send_to_workspace_6: AhkVarSchema.default({ fancy: 'Win + Shift + 7', ahk: '#+7' }), - send_to_workspace_7: AhkVarSchema.default({ fancy: 'Win + Shift + 8', ahk: '#+8' }), - send_to_workspace_8: AhkVarSchema.default({ fancy: 'Win + Shift + 9', ahk: '#+9' }), - send_to_workspace_9: AhkVarSchema.default({ fancy: 'Win + Shift + 0', ahk: '#+0' }), -}); - -export enum VirtualDesktopStrategy { - Native = 'Native', - Seelen = 'Seelen', -} - -export interface ISettings { - fancyToolbar: FancyToolbar; - seelenweg: Seelenweg; - windowManager: WindowManager; - monitors: Monitor[]; - ahkEnabled: boolean; - ahkVariables: AhkVariables; - selectedTheme: string[]; - devTools: boolean; - language: string; - virtualDesktopStrategy: VirtualDesktopStrategy; - betaChannel: boolean; -} diff --git a/src/apps/shared/schemas/Theme.ts b/src/apps/shared/schemas/Theme.ts deleted file mode 100644 index 13991f35..00000000 --- a/src/apps/shared/schemas/Theme.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CreatorInfoSchema } from '.'; -import { z } from 'zod'; - -export const ThemeSchema = z.object({ - info: CreatorInfoSchema.extend({ - tags: z.array(z.string()), - }), - styles: z.object({ - weg: z.string(), - toolbar: z.string(), - wm: z.string(), - }), -}); - -export interface Theme extends z.infer {} diff --git a/src/apps/shared/schemas/WindowManager.ts b/src/apps/shared/schemas/WindowManager.ts deleted file mode 100644 index 8271eb79..00000000 --- a/src/apps/shared/schemas/WindowManager.ts +++ /dev/null @@ -1,47 +0,0 @@ -import z from 'zod'; - -export type Rect = z.infer; -export const RectSchema = z.object({ - top: z.number().default(0), - left: z.number().default(0), - right: z.number().default(0), - bottom: z.number().default(0), -}); - -export type Border = z.infer; -export const BorderSchema = z.object({ - enabled: z.boolean().default(true), - width: z.number().min(0).default(3), - offset: z.number().default(-1), -}); - -export type FloatingWindowSettings = z.infer; -export const FloatingWindowSchema = z.object({ - width: z.number().positive().default(800), - height: z.number().positive().default(500), -}); - -export const WindowManagerSchema = z.object({ - enabled: z.boolean().default(false), - auto_stacking_by_category: z.boolean().default(true), - border: BorderSchema.default({}), - resize_delta: z.number().default(10).describe('% to add or remove on resize of windows using the CLI'), - workspace_gap: z.number().nonnegative().default(10).describe('Space between windows'), - workspace_padding: z.number().nonnegative().default(10), - global_work_area_offset: RectSchema.default({}), - floating: FloatingWindowSchema.default({}), - default_layout: z.string().nullable().default(null), -}); - -type inner = z.infer & {}; -export interface WindowManager { - enabled: inner['enabled']; - autoStackingByCategory: inner['auto_stacking_by_category']; - border: inner['border']; - resizeDelta: inner['resize_delta']; - workspaceGap: inner['workspace_gap']; - workspacePadding: inner['workspace_padding']; - globalWorkAreaOffset: inner['global_work_area_offset']; - floating: inner['floating']; - defaultLayout: inner['default_layout']; -} \ No newline at end of file diff --git a/src/apps/shared/schemas/index.ts b/src/apps/shared/schemas/index.ts index d7c3e083..b12e8dff 100644 --- a/src/apps/shared/schemas/index.ts +++ b/src/apps/shared/schemas/index.ts @@ -1,88 +1,66 @@ -import z from 'zod'; - -export class VariableConvention { - static snakeToCamel(text: string) { - let camel = ''; - let prevCharIsDash = false; - for (const char of text.split('')) { - if (char === '_') { - prevCharIsDash = true; - continue; - } - if (prevCharIsDash) { - camel += char.toUpperCase(); - prevCharIsDash = false; - } else { - camel += char; - } - } - return camel; - } - - static camelToSnake(text: string) { - let snake = ''; - for (let i = 0; i < text.length; i++) { - const char = text[i]!; - if ((char === char.toLowerCase() && !char.match(/[0-9]/)) || i === 0) { - snake += char.toLowerCase(); - } else { - snake += `_${char.toLowerCase()}`; - } - } - return snake; - } - - static camelToUser(text: string) { - return VariableConvention.camelToSnake(text).replace(/_/g, ' '); - } - - static deepKeyParser(obj: anyObject, parser: (text: string) => string): anyObject { - if (Array.isArray(obj)) { - return obj.map((x) => { - if (typeof x === 'object' && x != null) { - return VariableConvention.deepKeyParser(x, parser); - } - return x; - }); - } - - let newObj = {} as anyObject; - for (const key in obj) { - const value = obj[key]; - if (typeof value === 'object' && value != null) { - newObj[parser(key)] = VariableConvention.deepKeyParser(value, parser); - } else { - newObj[parser(key)] = value; - } - } - return newObj; - } - - static fromSnakeToCamel(value: any): any { - return VariableConvention.deepKeyParser(value, VariableConvention.snakeToCamel); - } - - static fromCamelToSnake(value: any): any { - return VariableConvention.deepKeyParser(value, VariableConvention.camelToSnake); - } -} - -export function parseAsCamel(schema: z.Schema, value: any) { - return VariableConvention.fromSnakeToCamel(schema.parse(value)); -} - -export function safeParseAsCamel(schema: z.Schema, value: any) { - let result = schema.safeParse(value); - if (result.error) { - console.error(result.error); - return; - } - return VariableConvention.fromSnakeToCamel(result.data); -} - -export const CreatorInfoSchema = z.object({ - displayName: z.string().default('Unknown'), - author: z.string().default('Unknown'), - description: z.string().default('Empty'), - filename: z.string().default(''), -}); +export class VariableConvention { + static snakeToCamel(text: string) { + let camel = ''; + let prevCharIsDash = false; + for (const char of text.split('')) { + if (char === '_') { + prevCharIsDash = true; + continue; + } + if (prevCharIsDash) { + camel += char.toUpperCase(); + prevCharIsDash = false; + } else { + camel += char; + } + } + return camel; + } + + static camelToSnake(text: string) { + let snake = ''; + for (let i = 0; i < text.length; i++) { + const char = text[i]!; + if ((char === char.toLowerCase() && !char.match(/[0-9]/)) || i === 0) { + snake += char.toLowerCase(); + } else { + snake += `_${char.toLowerCase()}`; + } + } + return snake; + } + + static camelToUser(text: string) { + return VariableConvention.camelToSnake(text).replace(/_/g, ' '); + } + + static deepKeyParser(obj: anyObject, parser: (text: string) => string): anyObject { + if (Array.isArray(obj)) { + return obj.map((x) => { + if (typeof x === 'object' && x != null) { + return VariableConvention.deepKeyParser(x, parser); + } + return x; + }); + } + + let newObj = {} as anyObject; + for (const key in obj) { + const value = obj[key]; + if (typeof value === 'object' && value != null) { + newObj[parser(key)] = VariableConvention.deepKeyParser(value, parser); + } else { + newObj[parser(key)] = value; + } + } + return newObj; + } + + static fromSnakeToCamel(value: any): any { + return VariableConvention.deepKeyParser(value, VariableConvention.snakeToCamel); + } + + static fromCamelToSnake(value: any): any { + return VariableConvention.deepKeyParser(value, VariableConvention.camelToSnake); + } +} diff --git a/src/apps/shared/styles.ts b/src/apps/shared/styles.ts index 2d109128..898f5584 100644 --- a/src/apps/shared/styles.ts +++ b/src/apps/shared/styles.ts @@ -1,40 +1,105 @@ -import { useEffect, useState } from 'react'; - -type Args = undefined | string | { [x: string]: any }; -export const cx = (...args: Args[]): string => { - return args - .map((arg) => { - if (!arg) { - return; - } - - if (typeof arg === 'string') { - return arg; - } - - let classnames = ''; - Object.keys(arg).forEach((key) => { - if (arg[key]) { - classnames += ` ${key}`; - } - }); - - return classnames.trimStart(); - }) - .join(' '); -}; - -export function useDarkMode() { - const [isDarkMode, setIsDarkMode] = useState( - window.matchMedia('(prefers-color-scheme: dark)').matches, - ); - - useEffect(() => { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - const listener = () => setIsDarkMode(mediaQuery.matches); - mediaQuery.addEventListener('change', listener); - return () => mediaQuery.removeEventListener('change', listener); - }); - - return isDarkMode; -} +import { listen } from '@tauri-apps/api/event'; +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { useEffect, useState } from 'react'; +import { Settings, Theme, UIColors } from 'seelen-core'; + +import { UserSettingsLoader } from '../settings/modules/shared/store/storeApi'; + +type Args = undefined | string | { [x: string]: any }; +export const cx = (...args: Args[]): string => { + return args + .map((arg) => { + if (!arg) { + return; + } + + if (typeof arg === 'string') { + return arg; + } + + let classnames = ''; + Object.keys(arg).forEach((key) => { + if (arg[key]) { + classnames += ` ${key}`; + } + }); + + return classnames.trimStart(); + }) + .join(' '); +}; + +export function useDarkMode() { + const [isDarkMode, setIsDarkMode] = useState( + window.matchMedia('(prefers-color-scheme: dark)').matches, + ); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const listener = () => setIsDarkMode(mediaQuery.matches); + mediaQuery.addEventListener('change', listener); + return () => mediaQuery.removeEventListener('change', listener); + }); + + return isDarkMode; +} + +const KeyByLabel: Record = { + 'fancy-toolbar': 'toolbar', + seelenweg: 'weg', + 'window-manager': 'wm', + 'seelen-launcher': 'launcher', + 'seelen-wall': 'wall', +}; + +async function loadThemes(allThemes: Theme[], selected: string[]) { + const themes = allThemes + .filter((theme) => selected.includes(theme.info.filename)) + .sort((a, b) => { + return selected.indexOf(a.info.filename) - selected.indexOf(b.info.filename); + }); + + const webviewId = getCurrentWebviewWindow().label; + const [label, _monitor] = webviewId.split('/'); + if (!label) { + return; + } + + const theme_key = KeyByLabel[label] as keyof Theme['styles'] | undefined; + if (!theme_key) { + return; + } + + document.getElementById(webviewId)?.remove(); + let element = document.createElement('style'); + element.id = webviewId; + element.textContent = ''; + + for (const theme of themes) { + let layerName = theme.info.filename.replace(/[\.]/g, '-') + '-theme'; + element.textContent += `@layer ${layerName} {\n${theme.styles[theme_key]}\n}\n`; + } + + document.head.appendChild(element); +} + +export async function StartThemingTool() { + const userSettings = await new UserSettingsLoader().withThemes().load(); + let allThemes = userSettings.themes; + let selected = userSettings.jsonSettings.selectedThemes; + + await listen('themes', (event) => { + allThemes = event.payload; + loadThemes(allThemes, selected); + }); + + await Settings.onChange((settings) => { + selected = settings.selectedThemes; + loadThemes(allThemes, selected); + }); + + UIColors.setAssCssVariables(await UIColors.getAsync()); + UIColors.onChange(UIColors.setAssCssVariables); + + await loadThemes(allThemes, selected); +} diff --git a/src/apps/toolbar/styles/colors.css b/src/apps/shared/styles/colors.css similarity index 96% rename from src/apps/toolbar/styles/colors.css rename to src/apps/shared/styles/colors.css index ec8ed844..f12ab260 100644 --- a/src/apps/toolbar/styles/colors.css +++ b/src/apps/shared/styles/colors.css @@ -1,599 +1,599 @@ -:root { - /* Persisted colors variables (not changed on dark mode) */ - --color-persist-white: #ffffff; - --color-persist-gray-50: #fdfdfd; - --color-persist-gray-100: #f8f8f8; - --color-persist-gray-200: #e6e6e6; - --color-persist-gray-300: #d5d5d5; - --color-persist-gray-400: #b1b1b1; - --color-persist-gray-500: #909090; - --color-persist-gray-600: #6d6d6d; - --color-persist-gray-700: #464646; - --color-persist-gray-800: #222222; - --color-persist-gray-900: #151515; - --color-persist-black: #000000; - - --color-persist-blue-100: #e0f2ff; - --color-persist-blue-200: #cae8ff; - --color-persist-blue-300: #b5deff; - --color-persist-blue-400: #96cefd; - --color-persist-blue-500: #78bbfa; - --color-persist-blue-600: #59a7f6; - --color-persist-blue-700: #3892f3; - --color-persist-blue-800: #147af3; - --color-persist-blue-900: #0265dc; - --color-persist-blue-1000: #0054b6; - --color-persist-blue-1100: #004491; - --color-persist-blue-1200: #003571; - --color-persist-blue-1300: #002754; - - --color-persist-green-100: #cef8e0; - --color-persist-green-200: #adf4ce; - --color-persist-green-300: #89ecbc; - --color-persist-green-400: #67dea8; - --color-persist-green-500: #49cc93; - --color-persist-green-600: #2fb880; - --color-persist-green-700: #15a46e; - --color-persist-green-800: #008f5d; - --color-persist-green-900: #007a4d; - --color-persist-green-1000: #00653e; - --color-persist-green-1100: #005132; - --color-persist-green-1200: #053f27; - --color-persist-green-1300: #0a2e1d; - - --color-persist-orange-100: #ffeccc; - --color-persist-orange-200: #ffdfad; - --color-persist-orange-300: #fdd291; - --color-persist-orange-400: #ffbb63; - --color-persist-orange-500: #ffa037; - --color-persist-orange-600: #f68511; - --color-persist-orange-700: #e46f00; - --color-persist-orange-800: #cb5d00; - --color-persist-orange-900: #b14c00; - --color-persist-orange-1000: #953d00; - --color-persist-orange-1100: #7a2f00; - --color-persist-orange-1200: #612300; - --color-persist-orange-1300: #491901; - - --color-persist-red-100: #ffebe7; - --color-persist-red-200: #ffddd6; - --color-persist-red-300: #ffcdc3; - --color-persist-red-400: #ffb7a9; - --color-persist-red-500: #ff9b88; - --color-persist-red-600: #ff7c65; - --color-persist-red-700: #f75c46; - --color-persist-red-800: #ea3829; - --color-persist-red-900: #d31510; - --color-persist-red-1000: #b40000; - --color-persist-red-1100: #930000; - --color-persist-red-1200: #740000; - --color-persist-red-1300: #590000; - - --color-persist-celery-100: #cdfcbf; - --color-persist-celery-200: #aef69d; - --color-persist-celery-300: #96ee85; - --color-persist-celery-400: #72e06a; - --color-persist-celery-500: #4ecf50; - --color-persist-celery-600: #27bb36; - --color-persist-celery-700: #07a721; - --color-persist-celery-800: #009112; - --color-persist-celery-900: #007c0f; - --color-persist-celery-1000: #00670f; - --color-persist-celery-1100: #00530d; - --color-persist-celery-1200: #00400a; - --color-persist-celery-1300: #003007; - - --color-persist-chartreuse-100: #dbfc6e; - --color-persist-chartreuse-200: #cbf443; - --color-persist-chartreuse-300: #bce92a; - --color-persist-chartreuse-400: #aad816; - --color-persist-chartreuse-500: #98c50a; - --color-persist-chartreuse-600: #87b103; - --color-persist-chartreuse-700: #769c00; - --color-persist-chartreuse-800: #678800; - --color-persist-chartreuse-900: #577400; - --color-persist-chartreuse-1000: #486000; - --color-persist-chartreuse-1100: #3a4d00; - --color-persist-chartreuse-1200: #2c3b00; - --color-persist-chartreuse-1300: #212c00; - - --color-persist-cyan-100: #c5f8ff; - --color-persist-cyan-200: #a4f0ff; - --color-persist-cyan-300: #88e7fa; - --color-persist-cyan-400: #60d8f3; - --color-persist-cyan-500: #33c5e8; - --color-persist-cyan-600: #12b0da; - --color-persist-cyan-700: #019cc8; - --color-persist-cyan-800: #0086b4; - --color-persist-cyan-900: #00719f; - --color-persist-cyan-1000: #005d89; - --color-persist-cyan-1100: #004a73; - --color-persist-cyan-1200: #00395d; - --color-persist-cyan-1300: #002a46; - - --color-persist-fuchsia-100: #ffe9fc; - --color-persist-fuchsia-200: #ffdafa; - --color-persist-fuchsia-300: #fec7f8; - --color-persist-fuchsia-400: #fbaef6; - --color-persist-fuchsia-500: #f592f3; - --color-persist-fuchsia-600: #ed74ed; - --color-persist-fuchsia-700: #e055e2; - --color-persist-fuchsia-800: #cd3ace; - --color-persist-fuchsia-900: #b622b7; - --color-persist-fuchsia-1000: #9d039e; - --color-persist-fuchsia-1100: #800081; - --color-persist-fuchsia-1200: #640664; - --color-persist-fuchsia-1300: #470e46; - - --color-persist-indigo-100: #edeeff; - --color-persist-indigo-200: #e0e2ff; - --color-persist-indigo-300: #d3d5ff; - --color-persist-indigo-400: #c1c4ff; - --color-persist-indigo-500: #acafff; - --color-persist-indigo-600: #9599ff; - --color-persist-indigo-700: #7e84fc; - --color-persist-indigo-800: #686df4; - --color-persist-indigo-900: #5258e4; - --color-persist-indigo-1000: #4046ca; - --color-persist-indigo-1100: #3236a8; - --color-persist-indigo-1200: #262986; - --color-persist-indigo-1300: #1b1e64; - - --color-persist-magenta-100: #ffeaf1; - --color-persist-magenta-200: #ffdce8; - --color-persist-magenta-300: #ffcadd; - --color-persist-magenta-400: #ffb2ce; - --color-persist-magenta-500: #ff95bd; - --color-persist-magenta-600: #fa77aa; - --color-persist-magenta-700: #ef5a98; - --color-persist-magenta-800: #de3d82; - --color-persist-magenta-900: #c82269; - --color-persist-magenta-1000: #ad0955; - --color-persist-magenta-1100: #8e0045; - --color-persist-magenta-1200: #700037; - --color-persist-magenta-1300: #54032a; - - --color-persist-purple-100: #f6ebff; - --color-persist-purple-200: #eeddff; - --color-persist-purple-300: #e6d0ff; - --color-persist-purple-400: #dbbbfe; - --color-persist-purple-500: #cca4fd; - --color-persist-purple-600: #bd8bfc; - --color-persist-purple-700: #ae72f9; - --color-persist-purple-800: #9d57f4; - --color-persist-purple-900: #893de7; - --color-persist-purple-1000: #7326d3; - --color-persist-purple-1100: #5d13b7; - --color-persist-purple-1200: #470c94; - --color-persist-purple-1300: #33106a; - - --color-persist-seafoam-100: #cef7f3; - --color-persist-seafoam-200: #aaf1ea; - --color-persist-seafoam-300: #8ce9e2; - --color-persist-seafoam-400: #65dad2; - --color-persist-seafoam-500: #3fc9c1; - --color-persist-seafoam-600: #0fb5ae; - --color-persist-seafoam-700: #00a19a; - --color-persist-seafoam-800: #008c87; - --color-persist-seafoam-900: #007772; - --color-persist-seafoam-1000: #00635f; - --color-persist-seafoam-1100: #0c4f4c; - --color-persist-seafoam-1200: #123c3a; - --color-persist-seafoam-1300: #122c2b; - - --color-persist-yellow-100: #fbf198; - --color-persist-yellow-200: #f8e750; - --color-persist-yellow-300: #f8d904; - --color-persist-yellow-400: #e8c600; - --color-persist-yellow-500: #d7b300; - --color-persist-yellow-600: #c49f00; - --color-persist-yellow-700: #b08c00; - --color-persist-yellow-800: #9b7800; - --color-persist-yellow-900: #856600; - --color-persist-yellow-1000: #705300; - --color-persist-yellow-1100: #5b4300; - --color-persist-yellow-1200: #483300; - --color-persist-yellow-1300: #362500; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-white: #000000; - - --color-gray-50: #151515; - --color-gray-100: #222222; - --color-gray-200: #464646; - --color-gray-300: #6d6d6d; - --color-gray-400: #909090; - --color-gray-500: #b1b1b1; - --color-gray-600: #d5d5d5; - --color-gray-700: #e6e6e6; - --color-gray-800: #f8f8f8; - --color-gray-900: #fdfdfd; - - --color-black: #ffffff; - - --color-blue-100: #003877; - --color-blue-200: #00418a; - --color-blue-300: #004da3; - --color-blue-400: #0059c2; - --color-blue-500: #0367e0; - --color-blue-600: #1379f3; - --color-blue-700: #348ff4; - --color-blue-800: #54a3f6; - --color-blue-900: #72b7f9; - --color-blue-1000: #8fcafc; - --color-blue-1100: #aedbfe; - --color-blue-1200: #cce9ff; - --color-blue-1300: #e8f6ff; - - --color-green-100: #044329; - --color-green-200: #004e2f; - --color-green-300: #005c38; - --color-green-400: #006c43; - --color-green-500: #007d4e; - --color-green-600: #008f5d; - --color-green-700: #12a26c; - --color-green-800: #2bb47d; - --color-green-900: #43c78f; - --color-green-1000: #5ed9a2; - --color-green-1100: #81e9b8; - --color-green-1200: #b1f4d1; - --color-green-1300: #dffaea; - - --color-orange-100: #662500; - --color-orange-200: #752d00; - --color-orange-300: #893700; - --color-orange-400: #9e4200; - --color-orange-500: #b44e00; - --color-orange-600: #ca5d00; - --color-orange-700: #e16d00; - --color-orange-800: #f4810c; - --color-orange-900: #fe9a2e; - --color-orange-1000: #ffb558; - --color-orange-1100: #fdce88; - --color-orange-1200: #ffe1b3; - --color-orange-1300: #fff2dd; - - --color-red-100: #7b0000; - --color-red-200: #8d0000; - --color-red-300: #a50000; - --color-red-400: #be0403; - --color-red-500: #d71913; - --color-red-600: #ea3829; - --color-red-700: #f65843; - --color-red-800: #ff755e; - --color-red-900: #ff9581; - --color-red-1000: #ffb0a1; - --color-red-1100: #ffc9bd; - --color-red-1200: #ffded8; - --color-red-1300: #fff1ee; - - --color-celery-100: #00450a; - --color-celery-200: #00500c; - --color-celery-300: #005e0e; - --color-celery-400: #006d0f; - --color-celery-500: #007f0f; - --color-celery-600: #009112; - --color-celery-700: #04a51e; - --color-celery-800: #22b833; - --color-celery-900: #44ca49; - --color-celery-1000: #69dc63; - --color-celery-1100: #8eeb7f; - --color-celery-1200: #b4f7a2; - --color-celery-1300: #ddfdd3; - - --color-chartreuse-100: #304000; - --color-chartreuse-200: #374a00; - --color-chartreuse-300: #415700; - --color-chartreuse-400: #4c6600; - --color-chartreuse-500: #597600; - --color-chartreuse-600: #668800; - --color-chartreuse-700: #759a00; - --color-chartreuse-800: #84ad01; - --color-chartreuse-900: #94c008; - --color-chartreuse-1000: #a6d312; - --color-chartreuse-1100: #b8e525; - --color-chartreuse-1200: #cdf547; - --color-chartreuse-1300: #e7fe9a; - - --color-cyan-100: #003d62; - --color-cyan-200: #00476f; - --color-cyan-300: #00557f; - --color-cyan-400: #006491; - --color-cyan-500: #0074a2; - --color-cyan-600: #0086b4; - --color-cyan-700: #0099c6; - --color-cyan-800: #0eadd7; - --color-cyan-900: #2cc1e6; - --color-cyan-1000: #54d3f1; - --color-cyan-1100: #7fe4f9; - --color-cyan-1200: #a7f1ff; - --color-cyan-1300: #d7faff; - - --color-fuchsia-100: #6b036a; - --color-fuchsia-200: #7b007b; - --color-fuchsia-300: #900091; - --color-fuchsia-400: #a50da6; - --color-fuchsia-500: #b925b9; - --color-fuchsia-600: #cd39ce; - --color-fuchsia-700: #df51e0; - --color-fuchsia-800: #eb6eec; - --color-fuchsia-900: #f48cf2; - --color-fuchsia-1000: #faa8f5; - --color-fuchsia-1100: #fec2f8; - --color-fuchsia-1200: #ffdbfa; - --color-fuchsia-1300: #ffeffc; - - --color-indigo-100: #282c8c; - --color-indigo-200: #2f34a3; - --color-indigo-300: #393fbb; - --color-indigo-400: #464bd3; - --color-indigo-500: #555be7; - --color-indigo-600: #686df4; - --color-indigo-700: #7c81fb; - --color-indigo-800: #9195ff; - --color-indigo-900: #a7aaff; - --color-indigo-1000: #bcbeff; - --color-indigo-1100: #d0d2ff; - --color-indigo-1200: #e2e4ff; - --color-indigo-1300: #f3f3fe; - - --color-magenta-100: #76003a; - --color-magenta-200: #890042; - --color-magenta-300: #a0004d; - --color-magenta-400: #b6125a; - --color-magenta-500: #cb266d; - --color-magenta-600: #de3d82; - --color-magenta-700: #ed5795; - --color-magenta-800: #f972a7; - --color-magenta-900: #ff8fb9; - --color-magenta-1000: #ffacca; - --color-magenta-1100: #ffc6da; - --color-magenta-1200: #ffdde9; - --color-magenta-1300: #fff0f5; - - --color-purple-100: #4c0d9d; - --color-purple-200: #5911b1; - --color-purple-300: #691cc8; - --color-purple-400: #7a2dda; - --color-purple-500: #8c41e9; - --color-purple-600: #9d57f3; - --color-purple-700: #ac6ff9; - --color-purple-800: #bb87fb; - --color-purple-900: #ca9ffc; - --color-purple-1000: #d7b6fe; - --color-purple-1100: #e4ccfe; - --color-purple-1200: #efdfff; - --color-purple-1300: #f9f0ff; - - --color-seafoam-100: #12413f; - --color-seafoam-200: #0e4c49; - --color-seafoam-300: #045a57; - --color-seafoam-400: #006965; - --color-seafoam-500: #007a75; - --color-seafoam-600: #008c87; - --color-seafoam-700: #009e98; - --color-seafoam-800: #03b2ab; - --color-seafoam-900: #36c5bd; - --color-seafoam-1000: #5dd6cf; - --color-seafoam-1100: #84e6df; - --color-seafoam-1200: #b0f2ec; - --color-seafoam-1300: #dff9f6; - - --color-yellow-100: #4c3600; - --color-yellow-200: #584000; - --color-yellow-300: #674c00; - --color-yellow-400: #775900; - --color-yellow-500: #886800; - --color-yellow-600: #9b7800; - --color-yellow-700: #ae8900; - --color-yellow-800: #c09c00; - --color-yellow-900: #d3ae00; - --color-yellow-1000: #e4c200; - --color-yellow-1100: #f4d500; - --color-yellow-1200: #f9e85c; - --color-yellow-1300: #fcf6bb; - } -} - -@media (prefers-color-scheme: light) { - :root { - --color-white: #ffffff; - - --color-gray-50: #fdfdfd; - --color-gray-100: #f8f8f8; - --color-gray-200: #e6e6e6; - --color-gray-300: #d5d5d5; - --color-gray-400: #b1b1b1; - --color-gray-500: #909090; - --color-gray-600: #6d6d6d; - --color-gray-700: #464646; - --color-gray-800: #222222; - --color-gray-900: #151515; - - --color-black: #000000; - - --color-blue-100: #e0f2ff; - --color-blue-200: #cae8ff; - --color-blue-300: #b5deff; - --color-blue-400: #96cefd; - --color-blue-500: #78bbfa; - --color-blue-600: #59a7f6; - --color-blue-700: #3892f3; - --color-blue-800: #147af3; - --color-blue-900: #0265dc; - --color-blue-1000: #0054b6; - --color-blue-1100: #004491; - --color-blue-1200: #003571; - --color-blue-1300: #002754; - - --color-green-100: #cef8e0; - --color-green-200: #adf4ce; - --color-green-300: #89ecbc; - --color-green-400: #67dea8; - --color-green-500: #49cc93; - --color-green-600: #2fb880; - --color-green-700: #15a46e; - --color-green-800: #008f5d; - --color-green-900: #007a4d; - --color-green-1000: #00653e; - --color-green-1100: #005132; - --color-green-1200: #053f27; - --color-green-1300: #0a2e1d; - - --color-orange-100: #ffeccc; - --color-orange-200: #ffdfad; - --color-orange-300: #fdd291; - --color-orange-400: #ffbb63; - --color-orange-500: #ffa037; - --color-orange-600: #f68511; - --color-orange-700: #e46f00; - --color-orange-800: #cb5d00; - --color-orange-900: #b14c00; - --color-orange-1000: #953d00; - --color-orange-1100: #7a2f00; - --color-orange-1200: #612300; - --color-orange-1300: #491901; - - --color-red-100: #ffebe7; - --color-red-200: #ffddd6; - --color-red-300: #ffcdc3; - --color-red-400: #ffb7a9; - --color-red-500: #ff9b88; - --color-red-600: #ff7c65; - --color-red-700: #f75c46; - --color-red-800: #ea3829; - --color-red-900: #d31510; - --color-red-1000: #b40000; - --color-red-1100: #930000; - --color-red-1200: #740000; - --color-red-1300: #590000; - - --color-celery-100: #cdfcbf; - --color-celery-200: #aef69d; - --color-celery-300: #96ee85; - --color-celery-400: #72e06a; - --color-celery-500: #4ecf50; - --color-celery-600: #27bb36; - --color-celery-700: #07a721; - --color-celery-800: #009112; - --color-celery-900: #007c0f; - --color-celery-1000: #00670f; - --color-celery-1100: #00530d; - --color-celery-1200: #00400a; - --color-celery-1300: #003007; - - --color-chartreuse-100: #dbfc6e; - --color-chartreuse-200: #cbf443; - --color-chartreuse-300: #bce92a; - --color-chartreuse-400: #aad816; - --color-chartreuse-500: #98c50a; - --color-chartreuse-600: #87b103; - --color-chartreuse-700: #769c00; - --color-chartreuse-800: #678800; - --color-chartreuse-900: #577400; - --color-chartreuse-1000: #486000; - --color-chartreuse-1100: #3a4d00; - --color-chartreuse-1200: #2c3b00; - --color-chartreuse-1300: #212c00; - - --color-cyan-100: #c5f8ff; - --color-cyan-200: #a4f0ff; - --color-cyan-300: #88e7fa; - --color-cyan-400: #60d8f3; - --color-cyan-500: #33c5e8; - --color-cyan-600: #12b0da; - --color-cyan-700: #019cc8; - --color-cyan-800: #0086b4; - --color-cyan-900: #00719f; - --color-cyan-1000: #005d89; - --color-cyan-1100: #004a73; - --color-cyan-1200: #00395d; - --color-cyan-1300: #002a46; - - --color-fuchsia-100: #ffe9fc; - --color-fuchsia-200: #ffdafa; - --color-fuchsia-300: #fec7f8; - --color-fuchsia-400: #fbaef6; - --color-fuchsia-500: #f592f3; - --color-fuchsia-600: #ed74ed; - --color-fuchsia-700: #e055e2; - --color-fuchsia-800: #cd3ace; - --color-fuchsia-900: #b622b7; - --color-fuchsia-1000: #9d039e; - --color-fuchsia-1100: #800081; - --color-fuchsia-1200: #640664; - --color-fuchsia-1300: #470e46; - - --color-indigo-100: #edeeff; - --color-indigo-200: #e0e2ff; - --color-indigo-300: #d3d5ff; - --color-indigo-400: #c1c4ff; - --color-indigo-500: #acafff; - --color-indigo-600: #9599ff; - --color-indigo-700: #7e84fc; - --color-indigo-800: #686df4; - --color-indigo-900: #5258e4; - --color-indigo-1000: #4046ca; - --color-indigo-1100: #3236a8; - --color-indigo-1200: #262986; - --color-indigo-1300: #1b1e64; - - --color-magenta-100: #ffeaf1; - --color-magenta-200: #ffdce8; - --color-magenta-300: #ffcadd; - --color-magenta-400: #ffb2ce; - --color-magenta-500: #ff95bd; - --color-magenta-600: #fa77aa; - --color-magenta-700: #ef5a98; - --color-magenta-800: #de3d82; - --color-magenta-900: #c82269; - --color-magenta-1000: #ad0955; - --color-magenta-1100: #8e0045; - --color-magenta-1200: #700037; - --color-magenta-1300: #54032a; - - --color-purple-100: #f6ebff; - --color-purple-200: #eeddff; - --color-purple-300: #e6d0ff; - --color-purple-400: #dbbbfe; - --color-purple-500: #cca4fd; - --color-purple-600: #bd8bfc; - --color-purple-700: #ae72f9; - --color-purple-800: #9d57f4; - --color-purple-900: #893de7; - --color-purple-1000: #7326d3; - --color-purple-1100: #5d13b7; - --color-purple-1200: #470c94; - --color-purple-1300: #33106a; - - --color-seafoam-100: #cef7f3; - --color-seafoam-200: #aaf1ea; - --color-seafoam-300: #8ce9e2; - --color-seafoam-400: #65dad2; - --color-seafoam-500: #3fc9c1; - --color-seafoam-600: #0fb5ae; - --color-seafoam-700: #00a19a; - --color-seafoam-800: #008c87; - --color-seafoam-900: #007772; - --color-seafoam-1000: #00635f; - --color-seafoam-1100: #0c4f4c; - --color-seafoam-1200: #123c3a; - --color-seafoam-1300: #122c2b; - - --color-yellow-100: #fbf198; - --color-yellow-200: #f8e750; - --color-yellow-300: #f8d904; - --color-yellow-400: #e8c600; - --color-yellow-500: #d7b300; - --color-yellow-600: #c49f00; - --color-yellow-700: #b08c00; - --color-yellow-800: #9b7800; - --color-yellow-900: #856600; - --color-yellow-1000: #705300; - --color-yellow-1100: #5b4300; - --color-yellow-1200: #483300; - --color-yellow-1300: #362500; - } -} +:root { + /* Persisted colors variables (not changed on dark mode) */ + --color-persist-white: #ffffff; + --color-persist-gray-50: #fdfdfd; + --color-persist-gray-100: #f8f8f8; + --color-persist-gray-200: #e6e6e6; + --color-persist-gray-300: #d5d5d5; + --color-persist-gray-400: #b1b1b1; + --color-persist-gray-500: #909090; + --color-persist-gray-600: #6d6d6d; + --color-persist-gray-700: #464646; + --color-persist-gray-800: #222222; + --color-persist-gray-900: #151515; + --color-persist-black: #000000; + + --color-persist-blue-100: #e0f2ff; + --color-persist-blue-200: #cae8ff; + --color-persist-blue-300: #b5deff; + --color-persist-blue-400: #96cefd; + --color-persist-blue-500: #78bbfa; + --color-persist-blue-600: #59a7f6; + --color-persist-blue-700: #3892f3; + --color-persist-blue-800: #147af3; + --color-persist-blue-900: #0265dc; + --color-persist-blue-1000: #0054b6; + --color-persist-blue-1100: #004491; + --color-persist-blue-1200: #003571; + --color-persist-blue-1300: #002754; + + --color-persist-green-100: #cef8e0; + --color-persist-green-200: #adf4ce; + --color-persist-green-300: #89ecbc; + --color-persist-green-400: #67dea8; + --color-persist-green-500: #49cc93; + --color-persist-green-600: #2fb880; + --color-persist-green-700: #15a46e; + --color-persist-green-800: #008f5d; + --color-persist-green-900: #007a4d; + --color-persist-green-1000: #00653e; + --color-persist-green-1100: #005132; + --color-persist-green-1200: #053f27; + --color-persist-green-1300: #0a2e1d; + + --color-persist-orange-100: #ffeccc; + --color-persist-orange-200: #ffdfad; + --color-persist-orange-300: #fdd291; + --color-persist-orange-400: #ffbb63; + --color-persist-orange-500: #ffa037; + --color-persist-orange-600: #f68511; + --color-persist-orange-700: #e46f00; + --color-persist-orange-800: #cb5d00; + --color-persist-orange-900: #b14c00; + --color-persist-orange-1000: #953d00; + --color-persist-orange-1100: #7a2f00; + --color-persist-orange-1200: #612300; + --color-persist-orange-1300: #491901; + + --color-persist-red-100: #ffebe7; + --color-persist-red-200: #ffddd6; + --color-persist-red-300: #ffcdc3; + --color-persist-red-400: #ffb7a9; + --color-persist-red-500: #ff9b88; + --color-persist-red-600: #ff7c65; + --color-persist-red-700: #f75c46; + --color-persist-red-800: #ea3829; + --color-persist-red-900: #d31510; + --color-persist-red-1000: #b40000; + --color-persist-red-1100: #930000; + --color-persist-red-1200: #740000; + --color-persist-red-1300: #590000; + + --color-persist-celery-100: #cdfcbf; + --color-persist-celery-200: #aef69d; + --color-persist-celery-300: #96ee85; + --color-persist-celery-400: #72e06a; + --color-persist-celery-500: #4ecf50; + --color-persist-celery-600: #27bb36; + --color-persist-celery-700: #07a721; + --color-persist-celery-800: #009112; + --color-persist-celery-900: #007c0f; + --color-persist-celery-1000: #00670f; + --color-persist-celery-1100: #00530d; + --color-persist-celery-1200: #00400a; + --color-persist-celery-1300: #003007; + + --color-persist-chartreuse-100: #dbfc6e; + --color-persist-chartreuse-200: #cbf443; + --color-persist-chartreuse-300: #bce92a; + --color-persist-chartreuse-400: #aad816; + --color-persist-chartreuse-500: #98c50a; + --color-persist-chartreuse-600: #87b103; + --color-persist-chartreuse-700: #769c00; + --color-persist-chartreuse-800: #678800; + --color-persist-chartreuse-900: #577400; + --color-persist-chartreuse-1000: #486000; + --color-persist-chartreuse-1100: #3a4d00; + --color-persist-chartreuse-1200: #2c3b00; + --color-persist-chartreuse-1300: #212c00; + + --color-persist-cyan-100: #c5f8ff; + --color-persist-cyan-200: #a4f0ff; + --color-persist-cyan-300: #88e7fa; + --color-persist-cyan-400: #60d8f3; + --color-persist-cyan-500: #33c5e8; + --color-persist-cyan-600: #12b0da; + --color-persist-cyan-700: #019cc8; + --color-persist-cyan-800: #0086b4; + --color-persist-cyan-900: #00719f; + --color-persist-cyan-1000: #005d89; + --color-persist-cyan-1100: #004a73; + --color-persist-cyan-1200: #00395d; + --color-persist-cyan-1300: #002a46; + + --color-persist-fuchsia-100: #ffe9fc; + --color-persist-fuchsia-200: #ffdafa; + --color-persist-fuchsia-300: #fec7f8; + --color-persist-fuchsia-400: #fbaef6; + --color-persist-fuchsia-500: #f592f3; + --color-persist-fuchsia-600: #ed74ed; + --color-persist-fuchsia-700: #e055e2; + --color-persist-fuchsia-800: #cd3ace; + --color-persist-fuchsia-900: #b622b7; + --color-persist-fuchsia-1000: #9d039e; + --color-persist-fuchsia-1100: #800081; + --color-persist-fuchsia-1200: #640664; + --color-persist-fuchsia-1300: #470e46; + + --color-persist-indigo-100: #edeeff; + --color-persist-indigo-200: #e0e2ff; + --color-persist-indigo-300: #d3d5ff; + --color-persist-indigo-400: #c1c4ff; + --color-persist-indigo-500: #acafff; + --color-persist-indigo-600: #9599ff; + --color-persist-indigo-700: #7e84fc; + --color-persist-indigo-800: #686df4; + --color-persist-indigo-900: #5258e4; + --color-persist-indigo-1000: #4046ca; + --color-persist-indigo-1100: #3236a8; + --color-persist-indigo-1200: #262986; + --color-persist-indigo-1300: #1b1e64; + + --color-persist-magenta-100: #ffeaf1; + --color-persist-magenta-200: #ffdce8; + --color-persist-magenta-300: #ffcadd; + --color-persist-magenta-400: #ffb2ce; + --color-persist-magenta-500: #ff95bd; + --color-persist-magenta-600: #fa77aa; + --color-persist-magenta-700: #ef5a98; + --color-persist-magenta-800: #de3d82; + --color-persist-magenta-900: #c82269; + --color-persist-magenta-1000: #ad0955; + --color-persist-magenta-1100: #8e0045; + --color-persist-magenta-1200: #700037; + --color-persist-magenta-1300: #54032a; + + --color-persist-purple-100: #f6ebff; + --color-persist-purple-200: #eeddff; + --color-persist-purple-300: #e6d0ff; + --color-persist-purple-400: #dbbbfe; + --color-persist-purple-500: #cca4fd; + --color-persist-purple-600: #bd8bfc; + --color-persist-purple-700: #ae72f9; + --color-persist-purple-800: #9d57f4; + --color-persist-purple-900: #893de7; + --color-persist-purple-1000: #7326d3; + --color-persist-purple-1100: #5d13b7; + --color-persist-purple-1200: #470c94; + --color-persist-purple-1300: #33106a; + + --color-persist-seafoam-100: #cef7f3; + --color-persist-seafoam-200: #aaf1ea; + --color-persist-seafoam-300: #8ce9e2; + --color-persist-seafoam-400: #65dad2; + --color-persist-seafoam-500: #3fc9c1; + --color-persist-seafoam-600: #0fb5ae; + --color-persist-seafoam-700: #00a19a; + --color-persist-seafoam-800: #008c87; + --color-persist-seafoam-900: #007772; + --color-persist-seafoam-1000: #00635f; + --color-persist-seafoam-1100: #0c4f4c; + --color-persist-seafoam-1200: #123c3a; + --color-persist-seafoam-1300: #122c2b; + + --color-persist-yellow-100: #fbf198; + --color-persist-yellow-200: #f8e750; + --color-persist-yellow-300: #f8d904; + --color-persist-yellow-400: #e8c600; + --color-persist-yellow-500: #d7b300; + --color-persist-yellow-600: #c49f00; + --color-persist-yellow-700: #b08c00; + --color-persist-yellow-800: #9b7800; + --color-persist-yellow-900: #856600; + --color-persist-yellow-1000: #705300; + --color-persist-yellow-1100: #5b4300; + --color-persist-yellow-1200: #483300; + --color-persist-yellow-1300: #362500; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-white: #000000; + + --color-gray-50: #151515; + --color-gray-100: #222222; + --color-gray-200: #464646; + --color-gray-300: #6d6d6d; + --color-gray-400: #909090; + --color-gray-500: #b1b1b1; + --color-gray-600: #d5d5d5; + --color-gray-700: #e6e6e6; + --color-gray-800: #f8f8f8; + --color-gray-900: #fdfdfd; + + --color-black: #ffffff; + + --color-blue-100: #003877; + --color-blue-200: #00418a; + --color-blue-300: #004da3; + --color-blue-400: #0059c2; + --color-blue-500: #0367e0; + --color-blue-600: #1379f3; + --color-blue-700: #348ff4; + --color-blue-800: #54a3f6; + --color-blue-900: #72b7f9; + --color-blue-1000: #8fcafc; + --color-blue-1100: #aedbfe; + --color-blue-1200: #cce9ff; + --color-blue-1300: #e8f6ff; + + --color-green-100: #044329; + --color-green-200: #004e2f; + --color-green-300: #005c38; + --color-green-400: #006c43; + --color-green-500: #007d4e; + --color-green-600: #008f5d; + --color-green-700: #12a26c; + --color-green-800: #2bb47d; + --color-green-900: #43c78f; + --color-green-1000: #5ed9a2; + --color-green-1100: #81e9b8; + --color-green-1200: #b1f4d1; + --color-green-1300: #dffaea; + + --color-orange-100: #662500; + --color-orange-200: #752d00; + --color-orange-300: #893700; + --color-orange-400: #9e4200; + --color-orange-500: #b44e00; + --color-orange-600: #ca5d00; + --color-orange-700: #e16d00; + --color-orange-800: #f4810c; + --color-orange-900: #fe9a2e; + --color-orange-1000: #ffb558; + --color-orange-1100: #fdce88; + --color-orange-1200: #ffe1b3; + --color-orange-1300: #fff2dd; + + --color-red-100: #7b0000; + --color-red-200: #8d0000; + --color-red-300: #a50000; + --color-red-400: #be0403; + --color-red-500: #d71913; + --color-red-600: #ea3829; + --color-red-700: #f65843; + --color-red-800: #ff755e; + --color-red-900: #ff9581; + --color-red-1000: #ffb0a1; + --color-red-1100: #ffc9bd; + --color-red-1200: #ffded8; + --color-red-1300: #fff1ee; + + --color-celery-100: #00450a; + --color-celery-200: #00500c; + --color-celery-300: #005e0e; + --color-celery-400: #006d0f; + --color-celery-500: #007f0f; + --color-celery-600: #009112; + --color-celery-700: #04a51e; + --color-celery-800: #22b833; + --color-celery-900: #44ca49; + --color-celery-1000: #69dc63; + --color-celery-1100: #8eeb7f; + --color-celery-1200: #b4f7a2; + --color-celery-1300: #ddfdd3; + + --color-chartreuse-100: #304000; + --color-chartreuse-200: #374a00; + --color-chartreuse-300: #415700; + --color-chartreuse-400: #4c6600; + --color-chartreuse-500: #597600; + --color-chartreuse-600: #668800; + --color-chartreuse-700: #759a00; + --color-chartreuse-800: #84ad01; + --color-chartreuse-900: #94c008; + --color-chartreuse-1000: #a6d312; + --color-chartreuse-1100: #b8e525; + --color-chartreuse-1200: #cdf547; + --color-chartreuse-1300: #e7fe9a; + + --color-cyan-100: #003d62; + --color-cyan-200: #00476f; + --color-cyan-300: #00557f; + --color-cyan-400: #006491; + --color-cyan-500: #0074a2; + --color-cyan-600: #0086b4; + --color-cyan-700: #0099c6; + --color-cyan-800: #0eadd7; + --color-cyan-900: #2cc1e6; + --color-cyan-1000: #54d3f1; + --color-cyan-1100: #7fe4f9; + --color-cyan-1200: #a7f1ff; + --color-cyan-1300: #d7faff; + + --color-fuchsia-100: #6b036a; + --color-fuchsia-200: #7b007b; + --color-fuchsia-300: #900091; + --color-fuchsia-400: #a50da6; + --color-fuchsia-500: #b925b9; + --color-fuchsia-600: #cd39ce; + --color-fuchsia-700: #df51e0; + --color-fuchsia-800: #eb6eec; + --color-fuchsia-900: #f48cf2; + --color-fuchsia-1000: #faa8f5; + --color-fuchsia-1100: #fec2f8; + --color-fuchsia-1200: #ffdbfa; + --color-fuchsia-1300: #ffeffc; + + --color-indigo-100: #282c8c; + --color-indigo-200: #2f34a3; + --color-indigo-300: #393fbb; + --color-indigo-400: #464bd3; + --color-indigo-500: #555be7; + --color-indigo-600: #686df4; + --color-indigo-700: #7c81fb; + --color-indigo-800: #9195ff; + --color-indigo-900: #a7aaff; + --color-indigo-1000: #bcbeff; + --color-indigo-1100: #d0d2ff; + --color-indigo-1200: #e2e4ff; + --color-indigo-1300: #f3f3fe; + + --color-magenta-100: #76003a; + --color-magenta-200: #890042; + --color-magenta-300: #a0004d; + --color-magenta-400: #b6125a; + --color-magenta-500: #cb266d; + --color-magenta-600: #de3d82; + --color-magenta-700: #ed5795; + --color-magenta-800: #f972a7; + --color-magenta-900: #ff8fb9; + --color-magenta-1000: #ffacca; + --color-magenta-1100: #ffc6da; + --color-magenta-1200: #ffdde9; + --color-magenta-1300: #fff0f5; + + --color-purple-100: #4c0d9d; + --color-purple-200: #5911b1; + --color-purple-300: #691cc8; + --color-purple-400: #7a2dda; + --color-purple-500: #8c41e9; + --color-purple-600: #9d57f3; + --color-purple-700: #ac6ff9; + --color-purple-800: #bb87fb; + --color-purple-900: #ca9ffc; + --color-purple-1000: #d7b6fe; + --color-purple-1100: #e4ccfe; + --color-purple-1200: #efdfff; + --color-purple-1300: #f9f0ff; + + --color-seafoam-100: #12413f; + --color-seafoam-200: #0e4c49; + --color-seafoam-300: #045a57; + --color-seafoam-400: #006965; + --color-seafoam-500: #007a75; + --color-seafoam-600: #008c87; + --color-seafoam-700: #009e98; + --color-seafoam-800: #03b2ab; + --color-seafoam-900: #36c5bd; + --color-seafoam-1000: #5dd6cf; + --color-seafoam-1100: #84e6df; + --color-seafoam-1200: #b0f2ec; + --color-seafoam-1300: #dff9f6; + + --color-yellow-100: #4c3600; + --color-yellow-200: #584000; + --color-yellow-300: #674c00; + --color-yellow-400: #775900; + --color-yellow-500: #886800; + --color-yellow-600: #9b7800; + --color-yellow-700: #ae8900; + --color-yellow-800: #c09c00; + --color-yellow-900: #d3ae00; + --color-yellow-1000: #e4c200; + --color-yellow-1100: #f4d500; + --color-yellow-1200: #f9e85c; + --color-yellow-1300: #fcf6bb; + } +} + +@media (prefers-color-scheme: light) { + :root { + --color-white: #ffffff; + + --color-gray-50: #fdfdfd; + --color-gray-100: #f8f8f8; + --color-gray-200: #e6e6e6; + --color-gray-300: #d5d5d5; + --color-gray-400: #b1b1b1; + --color-gray-500: #909090; + --color-gray-600: #6d6d6d; + --color-gray-700: #464646; + --color-gray-800: #222222; + --color-gray-900: #151515; + + --color-black: #000000; + + --color-blue-100: #e0f2ff; + --color-blue-200: #cae8ff; + --color-blue-300: #b5deff; + --color-blue-400: #96cefd; + --color-blue-500: #78bbfa; + --color-blue-600: #59a7f6; + --color-blue-700: #3892f3; + --color-blue-800: #147af3; + --color-blue-900: #0265dc; + --color-blue-1000: #0054b6; + --color-blue-1100: #004491; + --color-blue-1200: #003571; + --color-blue-1300: #002754; + + --color-green-100: #cef8e0; + --color-green-200: #adf4ce; + --color-green-300: #89ecbc; + --color-green-400: #67dea8; + --color-green-500: #49cc93; + --color-green-600: #2fb880; + --color-green-700: #15a46e; + --color-green-800: #008f5d; + --color-green-900: #007a4d; + --color-green-1000: #00653e; + --color-green-1100: #005132; + --color-green-1200: #053f27; + --color-green-1300: #0a2e1d; + + --color-orange-100: #ffeccc; + --color-orange-200: #ffdfad; + --color-orange-300: #fdd291; + --color-orange-400: #ffbb63; + --color-orange-500: #ffa037; + --color-orange-600: #f68511; + --color-orange-700: #e46f00; + --color-orange-800: #cb5d00; + --color-orange-900: #b14c00; + --color-orange-1000: #953d00; + --color-orange-1100: #7a2f00; + --color-orange-1200: #612300; + --color-orange-1300: #491901; + + --color-red-100: #ffebe7; + --color-red-200: #ffddd6; + --color-red-300: #ffcdc3; + --color-red-400: #ffb7a9; + --color-red-500: #ff9b88; + --color-red-600: #ff7c65; + --color-red-700: #f75c46; + --color-red-800: #ea3829; + --color-red-900: #d31510; + --color-red-1000: #b40000; + --color-red-1100: #930000; + --color-red-1200: #740000; + --color-red-1300: #590000; + + --color-celery-100: #cdfcbf; + --color-celery-200: #aef69d; + --color-celery-300: #96ee85; + --color-celery-400: #72e06a; + --color-celery-500: #4ecf50; + --color-celery-600: #27bb36; + --color-celery-700: #07a721; + --color-celery-800: #009112; + --color-celery-900: #007c0f; + --color-celery-1000: #00670f; + --color-celery-1100: #00530d; + --color-celery-1200: #00400a; + --color-celery-1300: #003007; + + --color-chartreuse-100: #dbfc6e; + --color-chartreuse-200: #cbf443; + --color-chartreuse-300: #bce92a; + --color-chartreuse-400: #aad816; + --color-chartreuse-500: #98c50a; + --color-chartreuse-600: #87b103; + --color-chartreuse-700: #769c00; + --color-chartreuse-800: #678800; + --color-chartreuse-900: #577400; + --color-chartreuse-1000: #486000; + --color-chartreuse-1100: #3a4d00; + --color-chartreuse-1200: #2c3b00; + --color-chartreuse-1300: #212c00; + + --color-cyan-100: #c5f8ff; + --color-cyan-200: #a4f0ff; + --color-cyan-300: #88e7fa; + --color-cyan-400: #60d8f3; + --color-cyan-500: #33c5e8; + --color-cyan-600: #12b0da; + --color-cyan-700: #019cc8; + --color-cyan-800: #0086b4; + --color-cyan-900: #00719f; + --color-cyan-1000: #005d89; + --color-cyan-1100: #004a73; + --color-cyan-1200: #00395d; + --color-cyan-1300: #002a46; + + --color-fuchsia-100: #ffe9fc; + --color-fuchsia-200: #ffdafa; + --color-fuchsia-300: #fec7f8; + --color-fuchsia-400: #fbaef6; + --color-fuchsia-500: #f592f3; + --color-fuchsia-600: #ed74ed; + --color-fuchsia-700: #e055e2; + --color-fuchsia-800: #cd3ace; + --color-fuchsia-900: #b622b7; + --color-fuchsia-1000: #9d039e; + --color-fuchsia-1100: #800081; + --color-fuchsia-1200: #640664; + --color-fuchsia-1300: #470e46; + + --color-indigo-100: #edeeff; + --color-indigo-200: #e0e2ff; + --color-indigo-300: #d3d5ff; + --color-indigo-400: #c1c4ff; + --color-indigo-500: #acafff; + --color-indigo-600: #9599ff; + --color-indigo-700: #7e84fc; + --color-indigo-800: #686df4; + --color-indigo-900: #5258e4; + --color-indigo-1000: #4046ca; + --color-indigo-1100: #3236a8; + --color-indigo-1200: #262986; + --color-indigo-1300: #1b1e64; + + --color-magenta-100: #ffeaf1; + --color-magenta-200: #ffdce8; + --color-magenta-300: #ffcadd; + --color-magenta-400: #ffb2ce; + --color-magenta-500: #ff95bd; + --color-magenta-600: #fa77aa; + --color-magenta-700: #ef5a98; + --color-magenta-800: #de3d82; + --color-magenta-900: #c82269; + --color-magenta-1000: #ad0955; + --color-magenta-1100: #8e0045; + --color-magenta-1200: #700037; + --color-magenta-1300: #54032a; + + --color-purple-100: #f6ebff; + --color-purple-200: #eeddff; + --color-purple-300: #e6d0ff; + --color-purple-400: #dbbbfe; + --color-purple-500: #cca4fd; + --color-purple-600: #bd8bfc; + --color-purple-700: #ae72f9; + --color-purple-800: #9d57f4; + --color-purple-900: #893de7; + --color-purple-1000: #7326d3; + --color-purple-1100: #5d13b7; + --color-purple-1200: #470c94; + --color-purple-1300: #33106a; + + --color-seafoam-100: #cef7f3; + --color-seafoam-200: #aaf1ea; + --color-seafoam-300: #8ce9e2; + --color-seafoam-400: #65dad2; + --color-seafoam-500: #3fc9c1; + --color-seafoam-600: #0fb5ae; + --color-seafoam-700: #00a19a; + --color-seafoam-800: #008c87; + --color-seafoam-900: #007772; + --color-seafoam-1000: #00635f; + --color-seafoam-1100: #0c4f4c; + --color-seafoam-1200: #123c3a; + --color-seafoam-1300: #122c2b; + + --color-yellow-100: #fbf198; + --color-yellow-200: #f8e750; + --color-yellow-300: #f8d904; + --color-yellow-400: #e8c600; + --color-yellow-500: #d7b300; + --color-yellow-600: #c49f00; + --color-yellow-700: #b08c00; + --color-yellow-800: #9b7800; + --color-yellow-900: #856600; + --color-yellow-1000: #705300; + --color-yellow-1100: #5b4300; + --color-yellow-1200: #483300; + --color-yellow-1300: #362500; + } +} diff --git a/src/apps/shared/styles/reset.css b/src/apps/shared/styles/reset.css new file mode 100644 index 00000000..689634d2 --- /dev/null +++ b/src/apps/shared/styles/reset.css @@ -0,0 +1,166 @@ +@layer reset { + :root { + font-size: 100%; + --main-typo: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + --primary-color: var(--color-gray-900); + --secondary-color: var(--color-gray-50); + } + + *, + *:after, + *:before { + margin: 0; + padding: 0; + border: 0; + outline: none; + box-sizing: border-box; + vertical-align: baseline; + -webkit-user-select: none; + user-select: none; + } + + img, + image, + picture, + video, + iframe, + figure { + max-width: 100%; + width: 100%; + display: block; + } + + a { + display: block; + } + + p a { + display: inline; + } + + li { + list-style-type: none; + } + + html { + scroll-behavior: smooth; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p, + span, + a, + strong, + blockquote, + i, + b, + em, + pre { + font-size: 1em; + font-weight: inherit; + font-style: inherit; + text-decoration: none; + color: inherit; + } + + form, + input, + textarea, + select, + button, + label { + font-family: inherit; + font-size: inherit; + hyphens: auto; + background-color: transparent; + display: block; + color: inherit; + } + + table, + tr, + td { + border-collapse: collapse; + border-spacing: 0; + } + + svg { + width: 100%; + display: block; + } + + body { + font-size: 1em; + line-height: 1.4em; + font-family: var(--main-typo); + color: var(--primary-color); + background-color: var(--secondary-color); + hyphens: auto; + font-smooth: always; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + hr { + border: 1px solid; + margin: 1em 0; + opacity: 0.8; + color: var(--color-gray-200); + } +} + +/* Antd reset */ +#root { + .ant-popover { + .ant-popover-inner { + background: transparent; + border-radius: 0; + box-shadow: none; + padding: 0; + } + } + + .ant-dropdown-menu-submenu, + .ant-dropdown-menu, + .ant-menu { + background: transparent; + box-shadow: none; + display: flex; + flex-direction: column; + gap: 4px; + + .ant-menu-item, + .ant-dropdown-menu-item { + padding: 10px; + height: min-content; + width: 100%; + line-height: 12px; + margin: 0; + border-radius: 8px; + } + + .ant-menu-item:not(.ant-menu-item-danger), + .ant-dropdown-menu-item:not(.ant-dropdown-menu-item-danger) { + color: inherit; + + &:hover { + backdrop-filter: brightness(0.6); + } + } + + .ant-menu-item-divider, + .ant-dropdown-menu-item-divider { + backdrop-filter: brightness(0.85); + } + + .ant-dropdown-menu-submenu-title { + color: inherit; + } + } +} diff --git a/src/apps/toolbar/app.tsx b/src/apps/toolbar/app.tsx index a6ed934c..67e2c590 100644 --- a/src/apps/toolbar/app.tsx +++ b/src/apps/toolbar/app.tsx @@ -1,8 +1,3 @@ -import { ErrorBoundary } from '../seelenweg/components/Error'; -import { getRootContainer } from '../shared'; -import { useDarkMode } from '../shared/styles'; -import { ErrorFallback } from './components/Error'; -import { emit, emitTo } from '@tauri-apps/api/event'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { ConfigProvider, theme } from 'antd'; import { useEffect } from 'react'; @@ -12,12 +7,10 @@ import { ToolBar } from './modules/main/infra'; import { Selectors } from './modules/shared/store/app'; -async function onMount() { - let view = getCurrentWebviewWindow(); - await emitTo(view.label.replace('/', '-hitbox/'), 'init'); - await emit('register-colors-events'); - await view.show(); -} +import { ErrorBoundary } from '../seelenweg/components/Error'; +import { getRootContainer } from '../shared'; +import { useDarkMode } from '../shared/styles'; +import { ErrorFallback } from './components/Error'; export function App() { const version = useSelector(Selectors.version); @@ -28,7 +21,7 @@ export function App() { const isDarkMode = useDarkMode(); useEffect(() => { - onMount().catch(console.error); + getCurrentWebviewWindow().show(); }, []); if (!structure) { diff --git a/src/apps/toolbar/i18n/index.ts b/src/apps/toolbar/i18n/index.ts index 5779f454..92afa253 100644 --- a/src/apps/toolbar/i18n/index.ts +++ b/src/apps/toolbar/i18n/index.ts @@ -1,98 +1,99 @@ -import { Lang } from '../../shared/lang'; -import i18n from 'i18next'; -import yaml from 'js-yaml'; -import { initReactI18next } from 'react-i18next'; - -i18n.use(initReactI18next).init( - { - lng: 'en', - fallbackLng: 'en', - interpolation: { - escapeValue: false, - }, - debug: true, - resources: {}, - }, - undefined, -); - -export async function loadTranslations() { - const translations: Record = { - en: await import('./translations/en.yml'), - es: await import('./translations/es.yml'), - de: await import('./translations/de.yml'), - zh: await import('./translations/zh.yml'), - ko: await import('./translations/ko.yml'), - fr: await import('./translations/fr.yml'), - ar: await import('./translations/ar.yml'), - ru: await import('./translations/ru.yml'), - hi: await import('./translations/hi.yml'), - ja: await import('./translations/ja.yml'), - pt: await import('./translations/pt.yml'), - it: await import('./translations/it.yml'), - nl: await import('./translations/nl.yml'), - tr: await import('./translations/tr.yml'), - pl: await import('./translations/pl.yml'), - uk: await import('./translations/uk.yml'), - id: await import('./translations/id.yml'), - cs: await import('./translations/cs.yml'), - th: await import('./translations/th.yml'), - vi: await import('./translations/vi.yml'), - ms: await import('./translations/ms.yml'), - he: await import('./translations/he.yml'), - ro: await import('./translations/ro.yml'), - el: await import('./translations/el.yml'), - sv: await import('./translations/sv.yml'), - no: await import('./translations/no.yml'), - fi: await import('./translations/fi.yml'), - da: await import('./translations/da.yml'), - hu: await import('./translations/hu.yml'), - lt: await import('./translations/lt.yml'), - bg: await import('./translations/bg.yml'), - sk: await import('./translations/sk.yml'), - hr: await import('./translations/hr.yml'), - lv: await import('./translations/lv.yml'), - et: await import('./translations/et.yml'), - tl: await import('./translations/tl.yml'), - ca: await import('./translations/ca.yml'), - af: await import('./translations/af.yml'), - bn: await import('./translations/bn.yml'), - fa: await import('./translations/fa.yml'), - pa: await import('./translations/pa.yml'), - sw: await import('./translations/sw.yml'), - ta: await import('./translations/ta.yml'), - ur: await import('./translations/ur.yml'), - cy: await import('./translations/cy.yml'), - am: await import('./translations/am.yml'), - hy: await import('./translations/hy.yml'), - az: await import('./translations/az.yml'), - eu: await import('./translations/eu.yml'), - bs: await import('./translations/bs.yml'), - ka: await import('./translations/ka.yml'), - gu: await import('./translations/gu.yml'), - is: await import('./translations/is.yml'), - km: await import('./translations/km.yml'), - ku: await import('./translations/ku.yml'), - lo: await import('./translations/lo.yml'), - lb: await import('./translations/lb.yml'), - mk: await import('./translations/mk.yml'), - mt: await import('./translations/mt.yml'), - mn: await import('./translations/mn.yml'), - ne: await import('./translations/ne.yml'), - ps: await import('./translations/ps.yml'), - sr: await import('./translations/sr.yml'), - si: await import('./translations/si.yml'), - so: await import('./translations/so.yml'), - tg: await import('./translations/tg.yml'), - te: await import('./translations/te.yml'), - uz: await import('./translations/uz.yml'), - yo: await import('./translations/yo.yml'), - zu: await import('./translations/zu.yml'), - }; - - for (const [key, value] of Object.entries(translations)) { - i18n.addResourceBundle(key, 'translation', yaml.load(value.default)); - } -} - -export default i18n; +import i18n from 'i18next'; +import yaml from 'js-yaml'; +import { initReactI18next } from 'react-i18next'; + +import { Lang } from '../../shared/lang'; + +i18n.use(initReactI18next).init( + { + lng: 'en', + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + debug: true, + resources: {}, + }, + undefined, +); + +export async function loadTranslations() { + const translations: Record = { + en: await import('./translations/en.yml'), + es: await import('./translations/es.yml'), + de: await import('./translations/de.yml'), + zh: await import('./translations/zh.yml'), + ko: await import('./translations/ko.yml'), + fr: await import('./translations/fr.yml'), + ar: await import('./translations/ar.yml'), + ru: await import('./translations/ru.yml'), + hi: await import('./translations/hi.yml'), + ja: await import('./translations/ja.yml'), + pt: await import('./translations/pt.yml'), + it: await import('./translations/it.yml'), + nl: await import('./translations/nl.yml'), + tr: await import('./translations/tr.yml'), + pl: await import('./translations/pl.yml'), + uk: await import('./translations/uk.yml'), + id: await import('./translations/id.yml'), + cs: await import('./translations/cs.yml'), + th: await import('./translations/th.yml'), + vi: await import('./translations/vi.yml'), + ms: await import('./translations/ms.yml'), + he: await import('./translations/he.yml'), + ro: await import('./translations/ro.yml'), + el: await import('./translations/el.yml'), + sv: await import('./translations/sv.yml'), + no: await import('./translations/no.yml'), + fi: await import('./translations/fi.yml'), + da: await import('./translations/da.yml'), + hu: await import('./translations/hu.yml'), + lt: await import('./translations/lt.yml'), + bg: await import('./translations/bg.yml'), + sk: await import('./translations/sk.yml'), + hr: await import('./translations/hr.yml'), + lv: await import('./translations/lv.yml'), + et: await import('./translations/et.yml'), + tl: await import('./translations/tl.yml'), + ca: await import('./translations/ca.yml'), + af: await import('./translations/af.yml'), + bn: await import('./translations/bn.yml'), + fa: await import('./translations/fa.yml'), + pa: await import('./translations/pa.yml'), + sw: await import('./translations/sw.yml'), + ta: await import('./translations/ta.yml'), + ur: await import('./translations/ur.yml'), + cy: await import('./translations/cy.yml'), + am: await import('./translations/am.yml'), + hy: await import('./translations/hy.yml'), + az: await import('./translations/az.yml'), + eu: await import('./translations/eu.yml'), + bs: await import('./translations/bs.yml'), + ka: await import('./translations/ka.yml'), + gu: await import('./translations/gu.yml'), + is: await import('./translations/is.yml'), + km: await import('./translations/km.yml'), + ku: await import('./translations/ku.yml'), + lo: await import('./translations/lo.yml'), + lb: await import('./translations/lb.yml'), + mk: await import('./translations/mk.yml'), + mt: await import('./translations/mt.yml'), + mn: await import('./translations/mn.yml'), + ne: await import('./translations/ne.yml'), + ps: await import('./translations/ps.yml'), + sr: await import('./translations/sr.yml'), + si: await import('./translations/si.yml'), + so: await import('./translations/so.yml'), + tg: await import('./translations/tg.yml'), + te: await import('./translations/te.yml'), + uz: await import('./translations/uz.yml'), + yo: await import('./translations/yo.yml'), + zu: await import('./translations/zu.yml'), + }; + + for (const [key, value] of Object.entries(translations)) { + i18n.addResourceBundle(key, 'translation', yaml.load(value.default)); + } +} + +export default i18n; diff --git a/src/apps/toolbar/i18n/translations/en.yml b/src/apps/toolbar/i18n/translations/en.yml index 6598494d..b6eb6d8c 100644 --- a/src/apps/toolbar/i18n/translations/en.yml +++ b/src/apps/toolbar/i18n/translations/en.yml @@ -1,3 +1,5 @@ +context_menu: + remove: Remove Item media: master_volume: Master Volume output_device: Output device diff --git a/src/apps/toolbar/index.tsx b/src/apps/toolbar/index.tsx index 2bd29314..d9797fd7 100644 --- a/src/apps/toolbar/index.tsx +++ b/src/apps/toolbar/index.tsx @@ -1,32 +1,31 @@ -import { getRootContainer } from '../shared'; -import { wrapConsole } from '../shared/ConsoleWrapper'; -import { registerDocumentEvents } from './events'; -import i18n, { loadTranslations } from './i18n'; import { createRoot } from 'react-dom/client'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; +import { declareDocumentAsLayeredHitbox } from 'seelen-core'; import { loadStore, registerStoreEvents, store } from './modules/shared/store/infra'; import { loadConstants } from './modules/shared/utils/infra'; import { App } from './app'; -import './styles/colors.css'; +import { getRootContainer } from '../shared'; +import { wrapConsole } from '../shared/ConsoleWrapper'; +import i18n, { loadTranslations } from './i18n'; + +import '../shared/styles/colors.css'; import './styles/variables.css'; -import './styles/reset.css'; +import '../shared/styles/reset.css'; import './styles/global.css'; async function Main() { wrapConsole(); - await registerDocumentEvents(); - - const container = getRootContainer(); - + await declareDocumentAsLayeredHitbox(); await loadConstants(); - await registerStoreEvents(); await loadStore(); + await registerStoreEvents(); await loadTranslations(); + const container = getRootContainer(); createRoot(container).render( diff --git a/src/apps/toolbar/modules/Date/infra.tsx b/src/apps/toolbar/modules/Date/infra.tsx index 853d16b1..9f6a04a0 100644 --- a/src/apps/toolbar/modules/Date/infra.tsx +++ b/src/apps/toolbar/modules/Date/infra.tsx @@ -1,29 +1,29 @@ -import { DateToolbarModule, TimeUnit } from '../../../shared/schemas/Placeholders'; -import moment from 'moment'; -import { useEffect, useState } from 'react'; - -import { Item } from '../item/infra'; - -interface Props { - module: DateToolbarModule; -} - -const timeByUnit = { - [TimeUnit.SECOND]: 1000, - [TimeUnit.MINUTE]: 1000 * 60, - [TimeUnit.HOUR]: 1000 * 60 * 60, - [TimeUnit.DAY]: 1000 * 60 * 60 * 24, -}; - -export function DateModule({ module }: Props) { - const [date, setDate] = useState(moment().format(module.format)); - - useEffect(() => { - const id = setInterval(() => { - setDate(moment().format(module.format)); - }, timeByUnit[module.each]); - return () => clearInterval(id); - }, [module]); - - return ; -} \ No newline at end of file +import moment from 'moment'; +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { DateToolbarModule, useInterval } from 'seelen-core'; + +import { Item } from '../item/infra/infra'; + +import { Selectors } from '../shared/store/app'; + +interface Props { + module: DateToolbarModule; +} + +export function DateModule({ module }: Props) { + const dateFormat = useSelector(Selectors.dateFormat); + + const [date, setDate] = useState(moment().format(dateFormat)); + + let interval = dateFormat.includes('ss') ? 1000 : 1000 * 60; + useInterval( + () => { + setDate(moment().format(dateFormat)); + }, + interval, + [dateFormat], + ); + + return ; +} diff --git a/src/apps/toolbar/modules/Device/infra.tsx b/src/apps/toolbar/modules/Device/infra.tsx index 86cdf7a6..1b0baa04 100644 --- a/src/apps/toolbar/modules/Device/infra.tsx +++ b/src/apps/toolbar/modules/Device/infra.tsx @@ -1,11 +1,11 @@ -import { DeviceTM } from '../../../shared/schemas/Placeholders'; - -import { Item } from '../item/infra'; - -interface Props { - module: DeviceTM; -} - -export function DeviceModule({ module }: Props) { - return ; -} +import { DeviceTM } from 'seelen-core'; + +import { Item } from '../item/infra/infra'; + +interface Props { + module: DeviceTM; +} + +export function DeviceModule({ module }: Props) { + return ; +} diff --git a/src/apps/toolbar/modules/Notifications/infra/Module.tsx b/src/apps/toolbar/modules/Notifications/infra/Module.tsx index b2047ba5..9497e279 100644 --- a/src/apps/toolbar/modules/Notifications/infra/Module.tsx +++ b/src/apps/toolbar/modules/Notifications/infra/Module.tsx @@ -1,42 +1,45 @@ -import { NotificationsTM } from '../../../../shared/schemas/Placeholders'; -import { Notifications } from './Notifications'; -import { emit } from '@tauri-apps/api/event'; -import { Popover } from 'antd'; -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; - -import { Item } from '../../item/infra'; -import { useAppBlur } from '../../shared/hooks/infra'; - -import { Selectors } from '../../shared/store/app'; - -import { RootState } from '../../shared/store/domain'; - -interface Props { - module: NotificationsTM; -} - -export function NotificationsModule({ module }: Props) { - const [openPreview, setOpenPreview] = useState(false); - const count = useSelector((state: RootState) => Selectors.notifications(state).length); - - useAppBlur(() => { - setOpenPreview(false); - }); - - useEffect(() => { - emit('register-notifications-events'); - }, []); - - return ( - } - > - - - ); -} +import { emit } from '@tauri-apps/api/event'; +import { Popover } from 'antd'; +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useWindowFocusChange } from 'seelen-core'; +import { NotificationsTM } from 'seelen-core'; + +import { Item } from '../../item/infra/infra'; + +import { Selectors } from '../../shared/store/app'; + +import { RootState } from '../../shared/store/domain'; + +import { Notifications } from './Notifications'; + +interface Props { + module: NotificationsTM; +} + +export function NotificationsModule({ module }: Props) { + const [openPreview, setOpenPreview] = useState(false); + const count = useSelector((state: RootState) => Selectors.notifications(state).length); + + useWindowFocusChange((focused) => { + if (!focused) { + setOpenPreview(false); + } + }); + + useEffect(() => { + emit('register-notifications-events'); + }, []); + + return ( + } + > + + + ); +} diff --git a/src/apps/toolbar/modules/Notifications/infra/Notifications.tsx b/src/apps/toolbar/modules/Notifications/infra/Notifications.tsx index 811c125f..6f81397b 100644 --- a/src/apps/toolbar/modules/Notifications/infra/Notifications.tsx +++ b/src/apps/toolbar/modules/Notifications/infra/Notifications.tsx @@ -1,99 +1,101 @@ -import { Icon } from '../../../../shared/components/Icon'; -import { invoke } from '@tauri-apps/api/core'; -import { Button } from 'antd'; -import { AnimatePresence, motion } from 'framer-motion'; -import moment from 'moment'; -import { useSelector } from 'react-redux'; - -import { BackgroundByLayersV2 } from '../../../../seelenweg/components/BackgroundByLayers/infra'; - -import { Selectors } from '../../shared/store/app'; - -// Difference between Windows epoch (1601) and Unix epoch (1970) in milliseconds -const EPOCH_DIFF_MILLISECONDS = 11644473600000n; - -function WindowsDateFileTimeToDate(fileTime: bigint) { - return new Date(Number(fileTime / 10000n - EPOCH_DIFF_MILLISECONDS)); -} - -export function Notifications() { - const notifications = useSelector(Selectors.notifications); - - return ( - -
- Notifications - -
- -
- - {notifications.map((notification) => ( - -
-
- -
{notification.app_name}
- - -
- {moment(WindowsDateFileTimeToDate(BigInt(notification.date))).fromNow()} -
-
- -
-
-

{notification.body[0]}

- {notification.body.slice(1).map((body, idx) => ( -

{body}

- ))} -
-
- ))} -
- - {!notifications.length && ( - -

No notifications

-
- )} -
-
- -
-
- ); -} +import { invoke } from '@tauri-apps/api/core'; +import { Button } from 'antd'; +import { AnimatePresence, motion } from 'framer-motion'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; +import { SeelenCommand } from 'seelen-core'; + +import { BackgroundByLayersV2 } from '../../../../seelenweg/components/BackgroundByLayers/infra'; + +import { Selectors } from '../../shared/store/app'; + +import { Icon } from '../../../../shared/components/Icon'; + +// Difference between Windows epoch (1601) and Unix epoch (1970) in milliseconds +const EPOCH_DIFF_MILLISECONDS = 11644473600000n; + +function WindowsDateFileTimeToDate(fileTime: bigint) { + return new Date(Number(fileTime / 10000n - EPOCH_DIFF_MILLISECONDS)); +} + +export function Notifications() { + const notifications = useSelector(Selectors.notifications); + + return ( + +
+ Notifications + +
+ +
+ + {notifications.map((notification) => ( + +
+
+ +
{notification.app_name}
+ - +
+ {moment(WindowsDateFileTimeToDate(BigInt(notification.date))).fromNow()} +
+
+ +
+
+

{notification.body[0]}

+ {notification.body.slice(1).map((body, idx) => ( +

{body}

+ ))} +
+
+ ))} +
+ + {!notifications.length && ( + +

No notifications

+
+ )} +
+
+ +
+
+ ); +} diff --git a/src/apps/toolbar/modules/Power/infra.tsx b/src/apps/toolbar/modules/Power/infra.tsx index 157c7b9e..3cc5dd88 100644 --- a/src/apps/toolbar/modules/Power/infra.tsx +++ b/src/apps/toolbar/modules/Power/infra.tsx @@ -1,36 +1,36 @@ -import { PowerToolbarModule } from '../../../shared/schemas/Placeholders'; -import { emit } from '@tauri-apps/api/event'; -import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; - -import { Item } from '../item/infra'; - -import { Selectors } from '../shared/store/app'; - -interface Props { - module: PowerToolbarModule; -} - -export function PowerModule({ module }: Props) { - const power = useSelector(Selectors.powerStatus); - const batteries = useSelector(Selectors.batteries); - - useEffect(() => { - emit('register-power-events'); - }, []); - - if (!batteries.length) { - return null; - } - - return ( - - ); -} +import { emit } from '@tauri-apps/api/event'; +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { PowerToolbarModule } from 'seelen-core'; + +import { Item } from '../item/infra/infra'; + +import { Selectors } from '../shared/store/app'; + +interface Props { + module: PowerToolbarModule; +} + +export function PowerModule({ module }: Props) { + const power = useSelector(Selectors.powerStatus); + const batteries = useSelector(Selectors.batteries); + + useEffect(() => { + emit('register-power-events'); + }, []); + + if (!batteries.length) { + return null; + } + + return ( + + ); +} diff --git a/src/apps/toolbar/modules/Settings/infra.tsx b/src/apps/toolbar/modules/Settings/infra.tsx index d39f7720..f80c289f 100644 --- a/src/apps/toolbar/modules/Settings/infra.tsx +++ b/src/apps/toolbar/modules/Settings/infra.tsx @@ -1,158 +1,161 @@ -import { Icon } from '../../../shared/components/Icon'; -import { SettingsToolbarModule } from '../../../shared/schemas/Placeholders'; -import { invoke } from '@tauri-apps/api/core'; -import { emit } from '@tauri-apps/api/event'; -import { Popover, Slider, Tooltip } from 'antd'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; - -import { BackgroundByLayersV2 } from '../../../seelenweg/components/BackgroundByLayers/infra'; -import { Item } from '../item/infra'; -import { VolumeControl } from '../media/infra/MediaControls'; -import { useAppBlur } from '../shared/hooks/infra'; - -import { Selectors } from '../shared/store/app'; - -import { RootState } from '../shared/store/domain'; - -interface Props { - module: SettingsToolbarModule; -} - -interface Brightness { - min: number; - max: number; - current: number; -} - -export function SettingsModule({ module }: Props) { - const [openPreview, setOpenPreview] = useState(false); - const [brightness, setBrightness] = useState({ - min: 0, - max: 0, - current: 0, - }); - - const defaultInput = useSelector((state: RootState) => - Selectors.mediaInputs(state).find((d) => d.is_default_multimedia), - ); - const defaultOutput = useSelector((state: RootState) => - Selectors.mediaOutputs(state).find((d) => d.is_default_multimedia), - ); - - const { t } = useTranslation(); - - useEffect(() => { - emit('register-media-events'); - }, []); - - useEffect(() => { - invoke('get_main_monitor_brightness') - .then(setBrightness) - .catch(() => { - // TODO brightness is always failing - // console.error(e); - }); - }, [openPreview]); - - useAppBlur(() => { - setOpenPreview(false); - }); - - return ( - - -
- {t('settings.title')} - - - -
- - {!!(defaultInput || defaultOutput) && ( - {t('media.master_volume')} - )} - - {!!defaultOutput && ( -
- - } - /> -
- )} - - {!!defaultInput && ( -
- } - /> -
- )} - - {brightness.max > 0 && ( -
- - setBrightness({ ...brightness, current: value })} - min={brightness.min} - max={brightness.max} - /> -
- )} - - {t('settings.power')} -
- - - - - - - - - - - - -
-
- } - > - - - ); -} +import { invoke } from '@tauri-apps/api/core'; +import { emit } from '@tauri-apps/api/event'; +import { Popover, Slider, Tooltip } from 'antd'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { SeelenCommand, useWindowFocusChange } from 'seelen-core'; +import { SettingsToolbarModule } from 'seelen-core'; + +import { BackgroundByLayersV2 } from '../../../seelenweg/components/BackgroundByLayers/infra'; +import { Item } from '../item/infra/infra'; +import { VolumeControl } from '../media/infra/MediaControls'; + +import { Selectors } from '../shared/store/app'; + +import { RootState } from '../shared/store/domain'; + +import { Icon } from '../../../shared/components/Icon'; + +interface Props { + module: SettingsToolbarModule; +} + +interface Brightness { + min: number; + max: number; + current: number; +} + +export function SettingsModule({ module }: Props) { + const [openPreview, setOpenPreview] = useState(false); + const [brightness, setBrightness] = useState({ + min: 0, + max: 0, + current: 0, + }); + + const defaultInput = useSelector((state: RootState) => + Selectors.mediaInputs(state).find((d) => d.is_default_multimedia), + ); + const defaultOutput = useSelector((state: RootState) => + Selectors.mediaOutputs(state).find((d) => d.is_default_multimedia), + ); + + const { t } = useTranslation(); + + useEffect(() => { + emit('register-media-events'); + }, []); + + useEffect(() => { + invoke('get_main_monitor_brightness') + .then(setBrightness) + .catch(() => { + // TODO brightness is always failing + // console.error(e); + }); + }, [openPreview]); + + useWindowFocusChange((focused) => { + if (!focused) { + setOpenPreview(false); + } + }); + + return ( + + +
+ {t('settings.title')} + + + +
+ + {!!(defaultInput || defaultOutput) && ( + {t('media.master_volume')} + )} + + {!!defaultOutput && ( +
+ + } + /> +
+ )} + + {!!defaultInput && ( +
+ } + /> +
+ )} + + {brightness.max > 0 && ( +
+ + setBrightness({ ...brightness, current: value })} + min={brightness.min} + max={brightness.max} + /> +
+ )} + + {t('settings.power')} +
+ + + + + + + + + + + + +
+
+ } + > + + + ); +} diff --git a/src/apps/toolbar/modules/Tray/index.tsx b/src/apps/toolbar/modules/Tray/index.tsx index b9b9e994..17047ae3 100644 --- a/src/apps/toolbar/modules/Tray/index.tsx +++ b/src/apps/toolbar/modules/Tray/index.tsx @@ -1,21 +1,22 @@ -import { OverflowTooltip } from '../../../shared/components/OverflowTooltip'; -import { TrayTM } from '../../../shared/schemas/Placeholders'; import { convertFileSrc, invoke } from '@tauri-apps/api/core'; import { emit } from '@tauri-apps/api/event'; import { Popover } from 'antd'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import { SeelenCommand, useWindowFocusChange } from 'seelen-core'; +import { TrayTM } from 'seelen-core'; import { BackgroundByLayersV2 } from '../../../seelenweg/components/BackgroundByLayers/infra'; -import { Item } from '../item/infra'; -import { useAppBlur } from '../shared/hooks/infra'; +import { Item } from '../item/infra/infra'; import { LAZY_CONSTANTS } from '../shared/utils/infra'; import { Selectors } from '../shared/store/app'; import { TrayInfo } from '../shared/store/domain'; +import { OverflowTooltip } from '../../../shared/components/OverflowTooltip'; + interface Props { module: TrayTM; } @@ -29,11 +30,11 @@ function TrayItem(props: { tray: TrayInfo; onAction: anyFunction; idx: number })
  • { - invoke('on_click_tray_icon', { idx }); + invoke(SeelenCommand.OnClickTrayIcon, { idx }); onAction(); }} onContextMenu={() => { - invoke('on_context_menu_tray_icon', { idx }); + invoke(SeelenCommand.OnContextMenuTrayIcon, { idx }); onAction(); }} > @@ -61,14 +62,16 @@ export function TrayModule({ module }: Props) { emit('register-tray-events'); }, []); - useAppBlur(() => { - setOpenPreview(false); + useWindowFocusChange((focused) => { + if (!focused) { + setOpenPreview(false); + } }); useEffect(() => { if (openPreview) { intervalId.current = setInterval(() => { - invoke('temp_get_by_event_tray_info'); + invoke(SeelenCommand.TempGetByEventTrayInfo); }, 1000); } else if (intervalId) { clearInterval(intervalId.current); diff --git a/src/apps/toolbar/modules/Workspaces/index.tsx b/src/apps/toolbar/modules/Workspaces/index.tsx index 7fe88fa0..f4199d4e 100644 --- a/src/apps/toolbar/modules/Workspaces/index.tsx +++ b/src/apps/toolbar/modules/Workspaces/index.tsx @@ -1,12 +1,13 @@ -import { WorkspacesTM, WorkspaceTMMode } from '../../../shared/schemas/Placeholders'; -import { cx } from '../../../shared/styles'; import { invoke } from '@tauri-apps/api/core'; import { Tooltip } from 'antd'; import { Reorder } from 'framer-motion'; import { useSelector } from 'react-redux'; +import { SeelenCommand, WorkspacesTM, WorkspaceTMMode } from 'seelen-core'; import { Selectors } from '../shared/store/app'; +import { cx } from '../../../shared/styles'; + interface Props { module: WorkspacesTM; } @@ -28,7 +29,7 @@ export function WorkspacesModule({ module }: Props) { {workspaces.map((w, idx) => (
  • invoke('switch_workspace', { idx })} + onClick={() => invoke(SeelenCommand.SwitchWorkspace, { idx })} className={cx('workspace-dot', { 'workspace-dot-active': w.id === activeWorkspace, })} @@ -56,7 +57,7 @@ export function WorkspacesModule({ module }: Props) { 'ft-bar-item-clickable': true, 'ft-bar-item-active': w.id === activeWorkspace, })} - onClick={() => invoke('switch_workspace', { idx })} + onClick={() => invoke(SeelenCommand.SwitchWorkspace, { idx })} >
    {mode === WorkspaceTMMode.Named diff --git a/src/apps/toolbar/modules/item/app.ts b/src/apps/toolbar/modules/item/app.ts index 802c2a5d..b9518672 100644 --- a/src/apps/toolbar/modules/item/app.ts +++ b/src/apps/toolbar/modules/item/app.ts @@ -1,90 +1,91 @@ -import { invoke } from '@tauri-apps/api/core'; -import { evaluate } from 'mathjs'; - -/** @deprecated remove on v2 */ -export enum Actions { - Open = 'open', - CopyToClipboard = 'copy-to-clipboard', - SwitchWorkspace = 'switch-workspace', -} - -/** @deprecated remove on v2 */ -export function performClick(onClick: string | null, scope: any) { - if (!onClick) { - return; - } - - const [_action, _argument] = onClick.split('->'); - const action = _action?.trim(); - const argument = _argument?.trim(); - - if (!action) { - return; - } - - switch (action) { - case Actions.Open: - if (argument) { - invoke('open_file', { path: evaluate(argument, scope) }); - } - break; - case Actions.CopyToClipboard: - if (argument) { - navigator.clipboard.writeText(evaluate(argument, scope)); - } - case Actions.SwitchWorkspace: - if (argument) { - invoke('switch_workspace', { idx: evaluate(argument, scope) }); - } - } -} - -export class Scope { - scope: Map; - - constructor() { - this.scope = new Map(); - } - - get(key: string) { - return this.scope.get(key); - } - - set(key: string, value: any) { - return this.scope.set(key, value); - } - - has(key: string) { - return this.scope.has(key); - } - - keys(): string[] | IterableIterator { - return this.scope.keys(); - } - - loadInvokeActions() { - for (const [key, value] of Object.entries(ActionsScope)) { - this.set(key, value); - } - } -} - -const ActionsScope = { - open(path: string) { - invoke('open_file', { path }).catch(console.error); - }, - run(program: string, ...args: string[]) { - invoke('run', { program, args }).catch(console.error); - }, - copyClipboard(text: string) { - navigator.clipboard.writeText(text); - }, -}; - -export function safeEval(expression: string, scope: Scope) { - try { - evaluate(expression, scope); - } catch (error) { - console.error(error); - } -} \ No newline at end of file +import { invoke } from '@tauri-apps/api/core'; +import { evaluate } from 'mathjs'; +import { SeelenCommand } from 'seelen-core'; + +/** @deprecated remove on v2 */ +export enum Actions { + Open = 'open', + CopyToClipboard = 'copy-to-clipboard', + SwitchWorkspace = 'switch-workspace', +} + +/** @deprecated remove on v2 */ +export function performClick(onClick: string | null, scope: any) { + if (!onClick) { + return; + } + + const [_action, _argument] = onClick.split('->'); + const action = _action?.trim(); + const argument = _argument?.trim(); + + if (!action) { + return; + } + + switch (action) { + case Actions.Open: + if (argument) { + invoke(SeelenCommand.OpenFile, { path: evaluate(argument, scope) }); + } + break; + case Actions.CopyToClipboard: + if (argument) { + navigator.clipboard.writeText(evaluate(argument, scope)); + } + case Actions.SwitchWorkspace: + if (argument) { + invoke(SeelenCommand.SwitchWorkspace, { idx: evaluate(argument, scope) }); + } + } +} + +export class Scope { + scope: Map; + + constructor() { + this.scope = new Map(); + } + + get(key: string) { + return this.scope.get(key); + } + + set(key: string, value: any) { + return this.scope.set(key, value); + } + + has(key: string) { + return this.scope.has(key); + } + + keys(): string[] | IterableIterator { + return this.scope.keys(); + } + + loadInvokeActions() { + for (const [key, value] of Object.entries(ActionsScope)) { + this.set(key, value); + } + } +} + +const ActionsScope = { + open(path: string) { + invoke(SeelenCommand.OpenFile, { path }).catch(console.error); + }, + run(program: string, ...args: string[]) { + invoke(SeelenCommand.Run, { program, args }).catch(console.error); + }, + copyClipboard(text: string) { + navigator.clipboard.writeText(text); + }, +}; + +export function safeEval(expression: string, scope: Scope) { + try { + evaluate(expression, scope); + } catch (error) { + console.error(error); + } +} diff --git a/src/apps/toolbar/modules/item/infra.tsx b/src/apps/toolbar/modules/item/infra/Inner.tsx similarity index 78% rename from src/apps/toolbar/modules/item/infra.tsx rename to src/apps/toolbar/modules/item/infra/Inner.tsx index a092f5ba..a781378b 100644 --- a/src/apps/toolbar/modules/item/infra.tsx +++ b/src/apps/toolbar/modules/item/infra/Inner.tsx @@ -1,6 +1,3 @@ -import { exposedIcons, Icon, IconName } from '../../../shared/components/Icon'; -import { GenericToolbarModule, ToolbarModule } from '../../../shared/schemas/Placeholders'; -import { cx } from '../../../shared/styles'; import { convertFileSrc, invoke } from '@tauri-apps/api/core'; import { Tooltip } from 'antd'; import { Reorder } from 'framer-motion'; @@ -9,16 +6,21 @@ import { evaluate, isResultSet } from 'mathjs'; import React, { PropsWithChildren, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import { ToolbarModule } from 'seelen-core'; -import { LAZY_CONSTANTS } from '../shared/utils/infra'; +import { LAZY_CONSTANTS } from '../../shared/utils/infra'; -import { Selectors } from '../shared/store/app'; -import { performClick, safeEval, Scope } from './app'; +import { Selectors } from '../../shared/store/app'; +import { performClick, safeEval, Scope } from '../app'; -interface Props extends PropsWithChildren { +import { Icon } from '../../../../shared/components/Icon'; +import { cx } from '../../../../shared/styles'; + +export interface InnerItemProps extends PropsWithChildren { module: ToolbarModule; extraVars?: Record; active?: boolean; + clickable?: boolean; // needed for dropdown/popup wrappers onClick?: (e: React.MouseEvent) => void; onKeydown?: (e: React.KeyboardEvent) => void; @@ -119,12 +121,7 @@ class StringToElement extends React.PureComponent - ); + return ; } return {this.props.text}; @@ -152,7 +149,7 @@ export function ElementsFromEvaluated(content: any) { return result; } -export function Item(props: Props) { +export function InnerItem(props: InnerItemProps) { const { extraVars, module, @@ -160,6 +157,8 @@ export function Item(props: Props) { onClick: onClickProp, onKeydown: onKeydownProp, children, + clickable = true, + ...rest } = props; const { template, tooltip, onClick: oldOnClick, onClickV2, style, id, badge } = module; @@ -172,7 +171,6 @@ export function Item(props: Props) { useEffect(() => { scope.current.loadInvokeActions(); - scope.current.set('icon', cloneDeep(exposedIcons)); scope.current.set('env', cloneDeep(env)); scope.current.set('getIcon', StringToElement.getIcon); @@ -193,25 +191,33 @@ export function Item(props: Props) { }); } - const elements = template ? ElementsFromEvaluated(evaluate(template, scope.current)) : []; + function parseStringToElements(text: string) { + /// backward compatibility with v1 icon object + let expr = text.replaceAll(/icon\.(\w+)/g, 'getIcon("$1")'); + return ElementsFromEvaluated(evaluate(expr, scope.current)); + } + + const elements = template ? parseStringToElements(template) : []; if (!elements.length && !children) { return null; } - const badgeContent = badge ? ElementsFromEvaluated(evaluate(badge, scope.current)) : null; + const badgeContent = badge ? parseStringToElements(badge) : null; return ( ); } - -export function GenericItem({ module }: { module: GenericToolbarModule }) { - const window = useSelector(Selectors.focused) || { - name: 'None', - title: 'No Window Focused', - exe: null, - }; - return ; -} diff --git a/src/apps/toolbar/modules/item/infra/infra.tsx b/src/apps/toolbar/modules/item/infra/infra.tsx new file mode 100644 index 00000000..b6a3eaf9 --- /dev/null +++ b/src/apps/toolbar/modules/item/infra/infra.tsx @@ -0,0 +1,62 @@ +import { Dropdown, Menu } from 'antd'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { GenericToolbarModule, useWindowFocusChange } from 'seelen-core'; + +import { BackgroundByLayersV2 } from 'src/apps/seelenweg/components/BackgroundByLayers/infra'; + +import { SavePlaceholderAsCustom } from '../../main/application'; +import { RootActions, Selectors } from '../../shared/store/app'; + +import { InnerItem, InnerItemProps } from './Inner'; + +export function Item(props: InnerItemProps) { + const [openContextMenu, setOpenContextMenu] = useState(false); + + const d = useDispatch(); + const { t } = useTranslation(); + + useWindowFocusChange((focused) => { + if (!focused) { + setOpenContextMenu(false); + } + }); + + return ( + ( + + + + )} + > + + + ); +} + +export function GenericItem({ module }: { module: GenericToolbarModule }) { + const window = useSelector(Selectors.focused) || { + name: 'None', + title: 'No Window Focused', + exe: null, + }; + return ; +} diff --git a/src/apps/toolbar/modules/main/application.ts b/src/apps/toolbar/modules/main/application.ts index 791d309b..5083fbc6 100644 --- a/src/apps/toolbar/modules/main/application.ts +++ b/src/apps/toolbar/modules/main/application.ts @@ -1,40 +1,41 @@ -import { - saveJsonSettings, - UserSettingsLoader, -} from '../../../settings/modules/shared/store/storeApi'; -import { path } from '@tauri-apps/api'; -import { writeTextFile } from '@tauri-apps/plugin-fs'; -import yaml from 'js-yaml'; -import { cloneDeep, debounce } from 'lodash'; - -import { store } from '../shared/store/infra'; - -export const IsSavingCustom = { - current: false, -}; - -export const SavePlaceholderAsCustom = debounce(async () => { - const { placeholder, env } = store.getState(); - - if (!placeholder) return; - - const toBeSaved = cloneDeep(placeholder); - toBeSaved.info.author = env.USERNAME || 'Me'; - toBeSaved.info.displayName = 'Custom'; - toBeSaved.info.description = 'Customized by me'; - toBeSaved.info.filename = 'custom.yml'; - - const filePath = await path.join( - await path.appDataDir(), - 'placeholders', - toBeSaved.info.filename, - ); - - await writeTextFile(filePath, yaml.dump(toBeSaved)); - - let { jsonSettings } = await new UserSettingsLoader().withThemes(false).load(); - jsonSettings.fancyToolbar.placeholder = toBeSaved.info.filename; - - IsSavingCustom.current = true; - await saveJsonSettings(jsonSettings); -}, 1000); +import { path } from '@tauri-apps/api'; +import { writeTextFile } from '@tauri-apps/plugin-fs'; +import yaml from 'js-yaml'; +import { cloneDeep, debounce } from 'lodash'; + +import { store } from '../shared/store/infra'; + +import { + saveJsonSettings, + UserSettingsLoader, +} from '../../../settings/modules/shared/store/storeApi'; + +export const IsSavingCustom = { + current: false, +}; + +export const SavePlaceholderAsCustom = debounce(async () => { + const { placeholder, env } = store.getState(); + + if (!placeholder) return; + + const toBeSaved = cloneDeep(placeholder); + toBeSaved.info.author = env.USERNAME || 'Me'; + toBeSaved.info.displayName = 'Custom'; + toBeSaved.info.description = 'Customized by me'; + toBeSaved.info.filename = 'custom.yml'; + + const filePath = await path.join( + await path.appDataDir(), + 'placeholders', + toBeSaved.info.filename, + ); + + await writeTextFile(filePath, yaml.dump(toBeSaved)); + + let { jsonSettings } = await new UserSettingsLoader().load(); + jsonSettings.fancyToolbar.placeholder = toBeSaved.info.filename; + + IsSavingCustom.current = true; + await saveJsonSettings(jsonSettings); +}, 1000); diff --git a/src/apps/toolbar/modules/main/infra.tsx b/src/apps/toolbar/modules/main/infra.tsx index 8e8d28b0..106625fc 100644 --- a/src/apps/toolbar/modules/main/infra.tsx +++ b/src/apps/toolbar/modules/main/infra.tsx @@ -1,31 +1,31 @@ -import { - Placeholder, - ToolbarModule, - ToolbarModuleType, -} from '../../../shared/schemas/Placeholders'; -import { AppBarHideMode } from '../../../shared/schemas/Seelenweg'; -import { cx } from '../../../shared/styles'; -import { TrayModule } from '../Tray'; -import { WorkspacesModule } from '../Workspaces'; import { Reorder, useForceUpdate } from 'framer-motion'; import { debounce } from 'lodash'; import { JSXElementConstructor, useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { HideMode, useWindowFocusChange } from 'seelen-core'; +import { + Placeholder, + ToolbarModule, + ToolbarModuleType, +} from 'seelen-core'; import { BackgroundByLayersV2 } from '../../../seelenweg/components/BackgroundByLayers/infra'; import { DateModule } from '../Date/infra'; import { DeviceModule } from '../Device/infra'; -import { GenericItem, Item } from '../item/infra'; +import { GenericItem, Item } from '../item/infra/infra'; import { MediaModule } from '../media/infra/Module'; import { NetworkModule } from '../network/infra/Module'; import { NotificationsModule } from '../Notifications/infra/Module'; import { PowerModule } from '../Power/infra'; import { SettingsModule } from '../Settings/infra'; -import { useAppActivation, useAppBlur } from '../shared/hooks/infra'; import { RootActions, Selectors } from '../shared/store/app'; import { SavePlaceholderAsCustom } from './application'; +import { cx } from '../../../shared/styles'; +import { TrayModule } from '../Tray'; +import { WorkspacesModule } from '../Workspaces'; + const modulesByType: Record> = { [ToolbarModuleType.Text]: Item, [ToolbarModuleType.Generic]: GenericItem, @@ -56,20 +56,16 @@ function componentByModule(module: ToolbarModule, idx: number) { } export function ToolBar({ structure }: Props) { - const [isActive, setActive] = useState(false); + const [isAppFocused, setAppFocus] = useState(false); const isOverlaped = useSelector(Selectors.isOverlaped); const hideMode = useSelector(Selectors.settings.hideMode); const dispatch = useDispatch(); const [forceUpdate] = useForceUpdate(); - useAppActivation(() => { - setActive(true); - }, []); - - useAppBlur(() => { - setActive(false); - }, []); + useWindowFocusChange((focused) => { + setAppFocus(focused); + }); const onReorderPinned = useCallback( debounce((apps: (ToolbarModule | string)[]) => { @@ -96,9 +92,7 @@ export function ToolBar({ structure }: Props) { ); const shouldBeHidden = - !isActive && - hideMode !== AppBarHideMode.Never && - (isOverlaped || hideMode === AppBarHideMode.Always); + !isAppFocused && hideMode !== HideMode.Never && (isOverlaped || hideMode === HideMode.Always); return ( {session.author}
    @@ -93,15 +94,15 @@ function Device({ device }: { device: MediaDevice }) { const onClickMultimedia = () => { if (!device.is_default_multimedia) { - invoke('media_set_default_device', { id: device.id, role: 'multimedia' }) - .then(() => invoke('media_set_default_device', { id: device.id, role: 'console' })) + invoke(SeelenCommand.MediaSetDefaultDevice, { id: device.id, role: 'multimedia' }) + .then(() => invoke(SeelenCommand.MediaSetDefaultDevice, { id: device.id, role: 'console' })) .catch(console.error); } }; const onClickCommunications = () => { if (!device.is_default_communications) { - invoke('media_set_default_device', { id: device.id, role: 'communications' }).catch( + invoke(SeelenCommand.MediaSetDefaultDevice, { id: device.id, role: 'communications' }).catch( console.error, ); } @@ -115,7 +116,7 @@ function Device({ device }: { device: MediaDevice }) { type={device.is_default_multimedia ? 'primary' : 'default'} onClick={onClickMultimedia} > - + @@ -161,7 +162,7 @@ export const VolumeControl = memo((props: VolumeControlProps) => { const onExternalChange = useCallback( debounce((value: number) => { - invoke('set_volume_level', { id: deviceId, level: value }).catch(console.error); + invoke(SeelenCommand.SetVolumeLevel, { id: deviceId, level: value }).catch(console.error); }, 100), [deviceId, sessionId], ); @@ -173,7 +174,7 @@ export const VolumeControl = memo((props: VolumeControlProps) => { return (
    - { }} /> {withRightAction && ( - )} @@ -284,8 +288,10 @@ export function WithMediaControls({ children }: PropsWithChildren) { closeVolumeNotifier(); }, [defaultOutput?.volume]); - useAppBlur(() => { - setOpenControls(false); + useWindowFocusChange((focused) => { + if (!focused) { + setOpenControls(false); + } }); return ( @@ -305,7 +311,7 @@ export function WithMediaControls({ children }: PropsWithChildren) { diff --git a/src/apps/toolbar/modules/media/infra/Module.tsx b/src/apps/toolbar/modules/media/infra/Module.tsx index f62e5e7f..d7e3148c 100644 --- a/src/apps/toolbar/modules/media/infra/Module.tsx +++ b/src/apps/toolbar/modules/media/infra/Module.tsx @@ -1,51 +1,52 @@ -import { MediaTM } from '../../../../shared/schemas/Placeholders'; -import { WithMediaControls } from './MediaControls'; -import { emit } from '@tauri-apps/api/event'; -import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; - -import { Item } from '../../item/infra'; - -import { Selectors } from '../../shared/store/app'; - -interface Props { - module: MediaTM; -} - -function MediaModuleItem({ module, ...rest }: Props) { - const { volume = 0, muted: isMuted = true } = - useSelector((state: any) => - Selectors.mediaOutputs(state).find((d) => d.is_default_multimedia), - ) || {}; - - const { volume: inputVolume = 0, muted: inputIsMuted = true } = - useSelector((state: any) => - Selectors.mediaInputs(state).find((d) => d.is_default_multimedia), - ) || {}; - - const mediaSession = useSelector((state: any) => - Selectors.mediaSessions(state).find((d) => d.default), - ) || null; - - return ( - - ); -} - -export function MediaModule({ module }: Props) { - useEffect(() => { - emit('register-media-events'); - }, []); - - return module.withMediaControls ? ( - - - - ) : ( - - ); -} +import { emit } from '@tauri-apps/api/event'; +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { MediaTM } from 'seelen-core'; + +import { Item } from '../../item/infra/infra'; + +import { Selectors } from '../../shared/store/app'; + +import { WithMediaControls } from './MediaControls'; + +interface Props { + module: MediaTM; +} + +function MediaModuleItem({ module, ...rest }: Props) { + const { volume = 0, muted: isMuted = true } = + useSelector((state: any) => + Selectors.mediaOutputs(state).find((d) => d.is_default_multimedia), + ) || {}; + + const { volume: inputVolume = 0, muted: inputIsMuted = true } = + useSelector((state: any) => + Selectors.mediaInputs(state).find((d) => d.is_default_multimedia), + ) || {}; + + const mediaSession = useSelector((state: any) => + Selectors.mediaSessions(state).find((d) => d.default), + ) || null; + + return ( + + ); +} + +export function MediaModule({ module }: Props) { + useEffect(() => { + emit('register-media-events'); + }, []); + + return module.withMediaControls ? ( + + + + ) : ( + + ); +} diff --git a/src/apps/toolbar/modules/network/infra/Module.tsx b/src/apps/toolbar/modules/network/infra/Module.tsx index 26e0fa7a..c3b546a2 100644 --- a/src/apps/toolbar/modules/network/infra/Module.tsx +++ b/src/apps/toolbar/modules/network/infra/Module.tsx @@ -1,47 +1,48 @@ -import { NetworkTM } from '../../../../shared/schemas/Placeholders'; -import { WithWlanSelector } from './WlanSelector'; -import { emit } from '@tauri-apps/api/event'; -import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; - -import { Item } from '../../item/infra'; - -import { Selectors } from '../../shared/store/app'; - -interface Props { - module: NetworkTM; -} - -function NetworkModuleItem({ module, ...rest }: Props) { - const networkAdapters = useSelector(Selectors.networkAdapters); - const defaultIp = useSelector(Selectors.networkLocalIp); - const online = useSelector(Selectors.online); - - const usingAdapter = networkAdapters.find((i) => i.ipv4 === defaultIp) || null; - - return ( - - ); -} - -export function NetworkModule({ module }: Props) { - useEffect(() => { - emit('register-network-events'); - }, []); - - return module.withWlanSelector ? ( - - - - ) : ( - - ); -} +import { emit } from '@tauri-apps/api/event'; +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { NetworkTM } from 'seelen-core'; + +import { Item } from '../../item/infra/infra'; + +import { Selectors } from '../../shared/store/app'; + +import { WithWlanSelector } from './WlanSelector'; + +interface Props { + module: NetworkTM; +} + +function NetworkModuleItem({ module, ...rest }: Props) { + const networkAdapters = useSelector(Selectors.networkAdapters); + const defaultIp = useSelector(Selectors.networkLocalIp); + const online = useSelector(Selectors.online); + + const usingAdapter = networkAdapters.find((i) => i.ipv4 === defaultIp) || null; + + return ( + + ); +} + +export function NetworkModule({ module }: Props) { + useEffect(() => { + emit('register-network-events'); + }, []); + + return module.withWlanSelector ? ( + + + + ) : ( + + ); +} diff --git a/src/apps/toolbar/modules/network/infra/WlanSelector.tsx b/src/apps/toolbar/modules/network/infra/WlanSelector.tsx index b02ec17c..f7c3470f 100644 --- a/src/apps/toolbar/modules/network/infra/WlanSelector.tsx +++ b/src/apps/toolbar/modules/network/infra/WlanSelector.tsx @@ -1,99 +1,102 @@ -import { WlanSelectorEntry } from './WlanSelectorEntry'; -import { invoke } from '@tauri-apps/api/core'; -import { Popover } from 'antd'; -import { PropsWithChildren, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; - -import { BackgroundByLayersV2 } from '../../../../seelenweg/components/BackgroundByLayers/infra'; -import { useAppBlur } from '../../shared/hooks/infra'; - -import { Selectors } from '../../shared/store/app'; - -function WlanSelector({ open }: { open: boolean }) { - const [selected, setSelected] = useState(null); - - const entries = useSelector(Selectors.wlanBssEntries); - const { t } = useTranslation(); - - useEffect(() => { - if (!open) { - setSelected(null); - } - }, [open]); - - let ssids = new Set(); - let filtered = entries - .toSorted((a, b) => b.signal - a.signal) - .filter((entry) => { - let ssid = entry.ssid || '__HIDDEN_SSID__'; - if (ssids.has(ssid)) { - return false; - } - ssids.add(ssid); - return true; - }); - - return ( -
    - -
    - {filtered.length === 0 && ( -
    {t('network.not_found')}
    - )} - {filtered.map((entry) => { - let ssid = entry.ssid || '__HIDDEN_SSID__'; - return ( - setSelected(ssid)} - /> - ); - })} -
    -
    - invoke('open_file', { path: 'ms-settings:network' })}> - {t('network.more')} - -
    -
    - ); -} - -export function WithWlanSelector({ children }: PropsWithChildren) { - const [mounted, setMounted] = useState(false); - const [openPreview, setOpenPreview] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (!mounted) { - return; - } - if (openPreview) { - invoke('wlan_start_scanning'); - } else { - invoke('wlan_stop_scanning'); - } - }, [openPreview]); - - useAppBlur(() => { - setOpenPreview(false); - }); - - return ( - } - > - {children} - - ); -} +import { invoke } from '@tauri-apps/api/core'; +import { Popover } from 'antd'; +import { PropsWithChildren, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { SeelenCommand, useWindowFocusChange } from 'seelen-core'; + +import { BackgroundByLayersV2 } from '../../../../seelenweg/components/BackgroundByLayers/infra'; + +import { Selectors } from '../../shared/store/app'; + +import { WlanSelectorEntry } from './WlanSelectorEntry'; + +function WlanSelector({ open }: { open: boolean }) { + const [selected, setSelected] = useState(null); + + const entries = useSelector(Selectors.wlanBssEntries); + const { t } = useTranslation(); + + useEffect(() => { + if (!open) { + setSelected(null); + } + }, [open]); + + let ssids = new Set(); + let filtered = entries + .toSorted((a, b) => b.signal - a.signal) + .filter((entry) => { + let ssid = entry.ssid || '__HIDDEN_SSID__'; + if (ssids.has(ssid)) { + return false; + } + ssids.add(ssid); + return true; + }); + + return ( +
    + +
    + {filtered.length === 0 && ( +
    {t('network.not_found')}
    + )} + {filtered.map((entry) => { + let ssid = entry.ssid || '__HIDDEN_SSID__'; + return ( + setSelected(ssid)} + /> + ); + })} +
    +
    + invoke(SeelenCommand.OpenFile, { path: 'ms-settings:network' })}> + {t('network.more')} + +
    +
    + ); +} + +export function WithWlanSelector({ children }: PropsWithChildren) { + const [mounted, setMounted] = useState(false); + const [openPreview, setOpenPreview] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) { + return; + } + if (openPreview) { + invoke(SeelenCommand.WlanStartScanning); + } else { + invoke(SeelenCommand.WlanStopScanning); + } + }, [openPreview]); + + useWindowFocusChange((focused) => { + if (!focused) { + setOpenPreview(false); + } + }); + + return ( + } + > + {children} + + ); +} diff --git a/src/apps/toolbar/modules/network/infra/WlanSelectorEntry.tsx b/src/apps/toolbar/modules/network/infra/WlanSelectorEntry.tsx index 4b234985..6b130bca 100644 --- a/src/apps/toolbar/modules/network/infra/WlanSelectorEntry.tsx +++ b/src/apps/toolbar/modules/network/infra/WlanSelectorEntry.tsx @@ -1,141 +1,143 @@ -import { Icon, IconName } from '../../../../shared/components/Icon'; -import { cx } from '../../../../shared/styles'; -import { invoke } from '@tauri-apps/api/core'; -import { Button, Input } from 'antd'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { WlanBssEntry, WlanProfile } from '../domain'; - -export function WlanSelectorEntry(props: { - entry: WlanBssEntry; - selected: boolean; - onClick: () => void; -}) { - let { entry, selected, onClick } = props; - - let [loading, setLoading] = useState(false); - let [showFields, setShowFields] = useState(false); - let [showErrors, setShowErrors] = useState(false); - - let [ssid, setSsid] = useState(entry.ssid || ''); - let [password, setPassword] = useState(''); - - const { t } = useTranslation(); - - useEffect(() => { - if (!selected) { - setShowFields(false); - setShowErrors(false); - setSsid(entry.ssid || ''); - setPassword(''); - } - }, [selected]); - - function onConnection() { - setLoading(true); - - function onrejected(error: any) { - console.error(error); - setLoading(false); - setShowErrors(true); - } - - if (entry.connected) { - invoke('wlan_disconnect').then(() => setLoading(false), onrejected); - return; - } - - if (!entry.ssid && !showFields) { - setShowFields(true); - setLoading(false); - return; - } - - function onfulfilled(success: boolean) { - setLoading(false); - setShowFields(!success); - setShowErrors(!success); - } - - if (showFields) { - invoke('wlan_connect', { ssid, password, hidden: !entry.ssid }).then( - onfulfilled, - onrejected, - ); - return; - } - - invoke('wlan_get_profiles') - .then((profiles) => { - let profile = profiles.find((profile) => profile.ssid === entry.ssid); - if (!profile) { - setShowFields(true); - setLoading(false); - return; - } - - invoke('wlan_connect', { - ssid: profile.ssid, - password: profile.password, - hidden: !entry.ssid, - }).then(onfulfilled, onrejected); - }) - .catch(onrejected); - } - - let signalIcon: IconName = 'GrWifiNone'; - if (entry.signal > 75) { - signalIcon = 'GrWifi'; - } else if (entry.signal > 50) { - signalIcon = 'GrWifiMedium'; - } else if (entry.signal > 25) { - signalIcon = 'GrWifiLow'; - } - - return ( -
    -
    - - {entry.ssid || t('network.hidden')} -
    - {showFields && ( -
    - {!entry.ssid && ( - setSsid(e.target.value)} - autoFocus - onPressEnter={(e) => (e.currentTarget.nextSibling as HTMLInputElement)?.focus()} - /> - )} - setPassword(e.target.value)} - onPressEnter={onConnection} - autoFocus={!!entry.ssid} - /> - - )} - {selected && ( -
    - -
    - )} -
    - ); -} +import { invoke } from '@tauri-apps/api/core'; +import { Button, Input } from 'antd'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SeelenCommand } from 'seelen-core'; + +import { WlanBssEntry, WlanProfile } from '../domain'; + +import { Icon } from '../../../../shared/components/Icon'; +import { cx } from '../../../../shared/styles'; + +export function WlanSelectorEntry(props: { + entry: WlanBssEntry; + selected: boolean; + onClick: () => void; +}) { + let { entry, selected, onClick } = props; + + let [loading, setLoading] = useState(false); + let [showFields, setShowFields] = useState(false); + let [showErrors, setShowErrors] = useState(false); + + let [ssid, setSsid] = useState(entry.ssid || ''); + let [password, setPassword] = useState(''); + + const { t } = useTranslation(); + + useEffect(() => { + if (!selected) { + setShowFields(false); + setShowErrors(false); + setSsid(entry.ssid || ''); + setPassword(''); + } + }, [selected]); + + function onConnection() { + setLoading(true); + + function onrejected(error: any) { + console.error(error); + setLoading(false); + setShowErrors(true); + } + + if (entry.connected) { + invoke(SeelenCommand.WlanDisconnect).then(() => setLoading(false), onrejected); + return; + } + + if (!entry.ssid && !showFields) { + setShowFields(true); + setLoading(false); + return; + } + + function onfulfilled(success: boolean) { + setLoading(false); + setShowFields(!success); + setShowErrors(!success); + } + + if (showFields) { + invoke('wlan_connect', { ssid, password, hidden: !entry.ssid }).then( + onfulfilled, + onrejected, + ); + return; + } + + invoke('wlan_get_profiles') + .then((profiles) => { + let profile = profiles.find((profile) => profile.ssid === entry.ssid); + if (!profile) { + setShowFields(true); + setLoading(false); + return; + } + + invoke('wlan_connect', { + ssid: profile.ssid, + password: profile.password, + hidden: !entry.ssid, + }).then(onfulfilled, onrejected); + }) + .catch(onrejected); + } + + let signalIcon = 'GrWifiNone'; + if (entry.signal > 75) { + signalIcon = 'GrWifi'; + } else if (entry.signal > 50) { + signalIcon = 'GrWifiMedium'; + } else if (entry.signal > 25) { + signalIcon = 'GrWifiLow'; + } + + return ( +
    +
    + + {entry.ssid || t('network.hidden')} +
    + {showFields && ( +
    + {!entry.ssid && ( + setSsid(e.target.value)} + autoFocus + onPressEnter={(e) => (e.currentTarget.nextSibling as HTMLInputElement)?.focus()} + /> + )} + setPassword(e.target.value)} + onPressEnter={onConnection} + autoFocus={!!entry.ssid} + /> + + )} + {selected && ( +
    + +
    + )} +
    + ); +} diff --git a/src/apps/toolbar/modules/shared/hooks/infra.ts b/src/apps/toolbar/modules/shared/hooks/infra.ts deleted file mode 100644 index c4d94a57..00000000 --- a/src/apps/toolbar/modules/shared/hooks/infra.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ExtraCallbacksOnBlur, ExtraCallbacksOnFocus } from '../../../events'; -import { useEffect, useRef } from 'react'; - -export function useAppBlur(cb: () => void, deps: any[] = []) { - const key = useRef(crypto.randomUUID()); - useEffect(() => { - ExtraCallbacksOnBlur.remove(key.current); - ExtraCallbacksOnBlur.add(cb, key.current); - return () => { - ExtraCallbacksOnBlur.remove(key.current); - }; - }, deps); -} - -export function useAppActivation(cb: () => void, deps: any[] = []) { - const key = useRef(crypto.randomUUID()); - useEffect(() => { - ExtraCallbacksOnFocus.remove(key.current); - ExtraCallbacksOnFocus.add(cb, key.current); - return () => { - ExtraCallbacksOnFocus.remove(key.current); - }; - }, deps); -} - -export function useInterval(callback: () => void, delay: number, deps: any[] = []) { - const key = useRef(); - - // Set up the interval. - useEffect(() => { - if (key.current) { - clearInterval(key.current); - } - key.current = window.setInterval(callback, delay); - return () => clearInterval(key.current); - }, [callback, delay, ...deps]); -} \ No newline at end of file diff --git a/src/apps/toolbar/modules/shared/store/app.ts b/src/apps/toolbar/modules/shared/store/app.ts index 48076854..f085de8e 100644 --- a/src/apps/toolbar/modules/shared/store/app.ts +++ b/src/apps/toolbar/modules/shared/store/app.ts @@ -1,17 +1,18 @@ -import { parseAsCamel } from '../../../../shared/schemas'; -import { FancyToolbarSchema } from '../../../../shared/schemas/FancyToolbar'; -import { Placeholder, ToolbarModule } from '../../../../shared/schemas/Placeholders'; -import { StateBuilder } from '../../../../shared/StateBuilder'; import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { FancyToolbarSettings, UIColors } from 'seelen-core'; +import { Placeholder, ToolbarModule } from 'seelen-core'; import { RootState } from './domain'; +import { StateBuilder } from '../../../../shared/StateBuilder'; + const initialState: RootState = { version: 0, + dateFormat: '', isOverlaped: false, focused: null, placeholder: null, - settings: parseAsCamel(FancyToolbarSchema, {}), + settings: new FancyToolbarSettings(), env: {}, // default values of https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-system_power_status powerStatus: { @@ -34,18 +35,7 @@ const initialState: RootState = { mediaOutputs: [], mediaInputs: [], notifications: [], - colors: { - background: '#ffffff', - foreground: '#000000', - accent_darkest: '#000000', - accent_darker: '#000000', - accent_dark: '#000000', - accent: '#000000', - accent_light: '#000000', - accent_lighter: '#000000', - accent_lightest: '#000000', - complement: null, - }, + colors: UIColors.default(), }; export const RootSlice = createSlice({ @@ -72,6 +62,14 @@ export const RootSlice = createSlice({ state.placeholder.right = action.payload; } }, + removeItem(state, action: PayloadAction) { + let id = action.payload; + if (state.placeholder) { + state.placeholder.left = state.placeholder.left.filter((d) => d.id !== id); + state.placeholder.center = state.placeholder.center.filter((d) => d.id !== id); + state.placeholder.right = state.placeholder.right.filter((d) => d.id !== id); + } + }, }, }); diff --git a/src/apps/toolbar/modules/shared/store/domain.ts b/src/apps/toolbar/modules/shared/store/domain.ts index 7dbef999..0b1fc899 100644 --- a/src/apps/toolbar/modules/shared/store/domain.ts +++ b/src/apps/toolbar/modules/shared/store/domain.ts @@ -1,11 +1,12 @@ -import { IRootState } from '../../../../../shared.interfaces'; -import { FocusedApp } from '../../../../shared/interfaces/common'; -import { FancyToolbar } from '../../../../shared/schemas/FancyToolbar'; -import { Placeholder } from '../../../../shared/schemas/Placeholders'; import { SoftOpaque } from 'readable-types'; +import { FancyToolbarSettings, Settings } from 'seelen-core'; +import { Placeholder } from 'seelen-core'; import { WlanBssEntry } from '../../network/domain'; +import { IRootState } from '../../../../../shared.interfaces'; +import { FocusedApp } from '../../../../shared/interfaces/common'; + /** https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-system_power_status */ export interface PowerStatus { acLineStatus: number; @@ -103,26 +104,13 @@ export interface AppNotification { date: number; } -export interface UIColors { - background: string; - foreground: string; - accent_darkest: string; - accent_darker: string; - accent_dark: string; - accent: string; - accent_light: string; - accent_lighter: string; - accent_lightest: string; - complement: string | null; -} - export type WorkspaceId = SoftOpaque; export interface Workspace { id: WorkspaceId; name: string | null; } -export interface RootState extends IRootState { +export interface RootState extends IRootState, Pick { version: number; isOverlaped: boolean; focused: FocusedApp | null; @@ -141,5 +129,4 @@ export interface RootState extends IRootState { mediaOutputs: MediaDevice[]; mediaInputs: MediaDevice[]; notifications: AppNotification[]; - colors: UIColors; } diff --git a/src/apps/toolbar/modules/shared/store/infra.ts b/src/apps/toolbar/modules/shared/store/infra.ts index de2cca54..5e1ecb3b 100644 --- a/src/apps/toolbar/modules/shared/store/infra.ts +++ b/src/apps/toolbar/modules/shared/store/infra.ts @@ -1,14 +1,9 @@ -import { UserSettings } from '../../../../../shared.interfaces'; -import { UserSettingsLoader } from '../../../../settings/modules/shared/store/storeApi'; -import { loadThemeCSS, setColorsAsCssVariables } from '../../../../shared'; -import { FileChange, GlobalEvent } from '../../../../shared/events'; -import { FocusedApp } from '../../../../shared/interfaces/common'; -import { FancyToolbar } from '../../../../shared/schemas/FancyToolbar'; -import i18n from '../../../i18n'; import { configureStore } from '@reduxjs/toolkit'; import { listen as listenGlobal } from '@tauri-apps/api/event'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { debounce, throttle } from 'lodash'; +import { SeelenEvent, UIColors } from 'seelen-core'; +import { FancyToolbarSettings } from 'seelen-core'; import { IsSavingCustom } from '../../main/application'; import { RootActions, RootSlice } from './app'; @@ -22,15 +17,33 @@ import { NetworkAdapter, PowerStatus, TrayInfo, - UIColors, Workspace, WorkspaceId, } from './domain'; +import { UserSettings } from '../../../../../shared.interfaces'; +import { UserSettingsLoader } from '../../../../settings/modules/shared/store/storeApi'; +import { FocusedApp } from '../../../../shared/interfaces/common'; +import { StartThemingTool } from '../../../../shared/styles'; +import i18n from '../../../i18n'; + export const store = configureStore({ reducer: RootSlice.reducer, + middleware(getDefaultMiddleware) { + return getDefaultMiddleware({ + serializableCheck: false, + }); + }, }); +async function initUIColors() { + function loadColors(colors: UIColors) { + store.dispatch(RootActions.setColors(colors)); + } + loadColors(await UIColors.getAsync()); + await UIColors.onChange(loadColors); +} + export async function registerStoreEvents() { const view = getCurrentWebviewWindow(); @@ -41,14 +54,14 @@ export async function registerStoreEvents() { const onFocusChanged = debounce((app: FocusedApp) => { store.dispatch(RootActions.setFocused(app)); }, 200); - await listenGlobal(GlobalEvent.FocusChanged, (e) => { + await listenGlobal(SeelenEvent.GlobalFocusChanged, (e) => { onFocusChanged(e.payload); if (e.payload.name != 'Seelen UI') { onFocusChanged.flush(); } }); - await listenGlobal(FileChange.Settings, async (_event) => { + await listenGlobal(SeelenEvent.StateSettingsChanged, async (_event) => { await loadStore(); }); @@ -73,7 +86,6 @@ export async function registerStoreEvents() { }); await listenGlobal('media-sessions', (event) => { - console.log(event.payload); store.dispatch(RootActions.setMediaSessions(event.payload)); }); @@ -108,17 +120,7 @@ export async function registerStoreEvents() { store.dispatch(RootActions.setWlanBssEntries(event.payload)); }); - await listenGlobal('colors', (event) => { - setColorsAsCssVariables(event.payload); - store.dispatch(RootActions.setColors(event.payload)); - }); - - await listenGlobal(FileChange.Themes, async () => { - const userSettings = await new UserSettingsLoader().load(); - loadThemeCSS(userSettings); - }); - - await listenGlobal(FileChange.Placeholders, async () => { + await listenGlobal(SeelenEvent.StatePlaceholdersChanged, async () => { if (IsSavingCustom.current) { IsSavingCustom.current = false; return; @@ -127,6 +129,8 @@ export async function registerStoreEvents() { setPlaceholder(userSettings); }); + await initUIColors(); + await StartThemingTool(); await view.emitTo(view.label, 'store-events-ready'); } @@ -147,14 +151,14 @@ export async function loadStore() { loadSettingsCSS(settings); store.dispatch(RootActions.setSettings(settings)); + store.dispatch(RootActions.setDateFormat(userSettings.jsonSettings.dateFormat)); - loadThemeCSS(userSettings); setPlaceholder(userSettings); store.dispatch(RootActions.setEnv(userSettings.env)); } -export function loadSettingsCSS(settings: FancyToolbar) { +export function loadSettingsCSS(settings: FancyToolbarSettings) { const styles = document.documentElement.style; styles.setProperty('--config-height', `${settings.height}px`); diff --git a/src/apps/toolbar/modules/shared/utils/app.ts b/src/apps/toolbar/modules/shared/utils/app.ts deleted file mode 100644 index 66ed0791..00000000 --- a/src/apps/toolbar/modules/shared/utils/app.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class CallbacksManager { - callbacks: Record void> = {}; - - add(cb: () => void, key: string) { - this.callbacks[key] = cb; - } - - remove(key: string) { - delete this.callbacks[key]; - } - - execute() { - Object.values(this.callbacks).forEach((cb) => cb()); - } -} \ No newline at end of file diff --git a/src/apps/toolbar/modules/shared/utils/domain.ts b/src/apps/toolbar/modules/shared/utils/domain.ts deleted file mode 100644 index a13cdc56..00000000 --- a/src/apps/toolbar/modules/shared/utils/domain.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const HITBOX_TARGET = 'fancy-toolbar-hitbox'; -export const SELF_TARGET = 'fancy-toolbar'; \ No newline at end of file diff --git a/src/apps/toolbar/index.html b/src/apps/toolbar/public/index.html similarity index 96% rename from src/apps/toolbar/index.html rename to src/apps/toolbar/public/index.html index 8c587313..e6e676d6 100644 --- a/src/apps/toolbar/index.html +++ b/src/apps/toolbar/public/index.html @@ -1,10 +1,10 @@ - - - - - - - -
    - - + + + + + + + +
    + + diff --git a/src/apps/toolbar/styles/global.css b/src/apps/toolbar/styles/global.css index c79c48fd..885a3bee 100644 --- a/src/apps/toolbar/styles/global.css +++ b/src/apps/toolbar/styles/global.css @@ -8,19 +8,19 @@ body { background: transparent; width: 100vw; height: 100vh; - border-top: 1px solid #000; } -.fancy-toolbar { +#root { width: 100vw; - margin-top: -1px; transition: transform 0.2s ease-in-out; &:has(.ft-bar-hidden):not(:hover) { transform: translateY(calc(-100% + 1px)); border-bottom: 1px solid transparent; } +} +.fancy-toolbar { .ft-bar { position: relative; width: 100%; diff --git a/src/apps/toolbar/styles/reset.css b/src/apps/toolbar/styles/reset.css deleted file mode 100644 index af4e57ca..00000000 --- a/src/apps/toolbar/styles/reset.css +++ /dev/null @@ -1,135 +0,0 @@ -:root { - font-size: 100%; - --main-typo: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --primary-color: var(--color-gray-900); - --secondary-color: var(--color-gray-50); -} - -*, *:after, *:before { - margin: 0; - padding: 0; - border: 0; - outline: none; - box-sizing: border-box; - vertical-align: baseline; - -webkit-user-select: none; - user-select: none; -} - -img, image, picture, video, iframe, figure { - max-width: 100%; - width: 100%; - display: block; -} - -a { - display: block; -} - -p a { - display: inline; -} - -li { - list-style-type: none; -} - -html { - scroll-behavior: smooth; -} - -h1, h2, h3, h4, h5, h6, p, span, a, strong, blockquote, i, b, em, pre { - font-size: 1em; - font-weight: inherit; - font-style: inherit; - text-decoration: none; - color: inherit; -} - -form, input, textarea, select, button, label { - font-family: inherit; - font-size: inherit; - hyphens: auto; - background-color: transparent; - display: block; - color: inherit; -} - -table, tr, td { - border-collapse: collapse; - border-spacing: 0; -} - -svg { - width: 100%; - display: block; -} - -body { - font-size: 1em; - line-height: 1.4em; - font-family: var(--main-typo); - color: var(--primary-color); - background-color: var(--secondary-color); - hyphens: auto; - font-smooth: always; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -hr { - border: 1px solid; - margin: 1em 0; - opacity: 0.8; - color: var(--color-gray-200); -} - -/* Antd reset */ -#root { - .ant-popover { - .ant-popover-inner { - background: transparent; - border-radius: 0; - box-shadow: none; - padding: 0; - } - } - - .ant-dropdown-menu-submenu, - .ant-dropdown-menu, - .ant-menu { - background: transparent; - box-shadow: none; - display: flex; - flex-direction: column; - gap: 4px; - - .ant-menu-item, - .ant-dropdown-menu-item { - padding: 10px; - height: min-content; - width: 100%; - line-height: 12px; - margin: 0; - border-radius: 8px; - } - - .ant-menu-item:not(.ant-menu-item-danger), - .ant-dropdown-menu-item:not(.ant-dropdown-menu-item-danger) { - color: inherit; - - &:hover { - backdrop-filter: brightness(0.6); - } - } - - .ant-menu-item-divider, - .ant-dropdown-menu-item-divider { - backdrop-filter: brightness(0.85); - } - - .ant-dropdown-menu-submenu-title { - color: inherit; - } - } -} \ No newline at end of file diff --git a/src/apps/update/app.tsx b/src/apps/update/app.tsx deleted file mode 100644 index 2c9bdd8f..00000000 --- a/src/apps/update/app.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { UserSettingsLoader } from '../settings/modules/shared/store/storeApi'; -import { useDarkMode } from '../shared/styles'; -import { UpdateModal } from './update'; -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import { check, Update } from '@tauri-apps/plugin-updater'; -import { ConfigProvider, theme } from 'antd'; -import { useEffect, useState } from 'react'; - -export function App() { - const [update, setUpdate] = useState(null); - - const isDarkMode = useDarkMode(); - - useEffect(() => { - async function checkUpdate() { - const webview = getCurrentWebviewWindow(); - const update = await check({}); - const { jsonSettings } = await new UserSettingsLoader().onlySettings().load(); - - if (!update || (!jsonSettings.betaChannel && update.version.includes('beta'))) { - webview.close(); - return; - } - - webview.show(); - setUpdate(update); - } - checkUpdate().catch(() => getCurrentWebviewWindow().close()); - }, []); - - if (!update) { - return null; - } - - return ( - - - - ); -} diff --git a/src/apps/update/i18n/translations/af.yml b/src/apps/update/i18n/translations/af.yml deleted file mode 100644 index 1ce99c46..00000000 --- a/src/apps/update/i18n/translations/af.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - title: Opdatering beskikbaar! - date: Datum - version: Weergawe - downloading: Laai tans af... - cancel: Later - installing: Installeer tans … - page: GitHub-vrystellingbladsy - download: Dateer op en installeer - extra_info: >- - Om die volledige veranderingslog te lees, besoek asseblief die - veranderingslogbladsy diff --git a/src/apps/update/i18n/translations/am.yml b/src/apps/update/i18n/translations/am.yml deleted file mode 100644 index a630a537..00000000 --- a/src/apps/update/i18n/translations/am.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - page: GitHub የሚለቀቅበት ገጽ - installing: በመጫን ላይ... - extra_info: ሙሉውን የለውጥ ማስታወሻ ለማንበብ፣ እባክዎን የለውጥ መዝገብ ገጹን ይጎብኙ - title: ማዘመን አለ! - download: አዘምን እና ጫን - downloading: በማውረድ ላይ... - date: ቀን - cancel: በኋላ - version: ሥሪት diff --git a/src/apps/update/i18n/translations/ar.yml b/src/apps/update/i18n/translations/ar.yml deleted file mode 100644 index a5a0b8c9..00000000 --- a/src/apps/update/i18n/translations/ar.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: التحديث متاح! - date: تاريخ - version: إصدار - extra_info: لقراءة سجل التغيير الكامل، يرجى زيارة صفحة سجل التغيير - page: صفحة إصدار جيثب - cancel: لاحقاً - download: تحديث - downloading: جارى التحميل... - installing: جارٍ التثبيت... diff --git a/src/apps/update/i18n/translations/az.yml b/src/apps/update/i18n/translations/az.yml deleted file mode 100644 index dd0d2550..00000000 --- a/src/apps/update/i18n/translations/az.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - date: Tarix - cancel: Daha sonra - page: GitHub Buraxılış Səhifəsi - title: Yeniləmə əlçatandır! - download: Yeniləyin və Quraşdırın - installing: Quraşdırılır... - downloading: Endirilir... - extra_info: >- - Tam dəyişiklik jurnalını oxumaq üçün lütfən dəyişiklik jurnalı səhifəsinə - daxil olun - version: Versiya diff --git a/src/apps/update/i18n/translations/bg.yml b/src/apps/update/i18n/translations/bg.yml deleted file mode 100644 index b16c4928..00000000 --- a/src/apps/update/i18n/translations/bg.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Версия - cancel: По късно - date: Дата - extra_info: За да прочетете пълния Changelog, моля, посетете страницата на Changelog - download: Актуализирайте и инсталирайте - installing: Инсталиране ... - downloading: Изтегляне ... - title: Налична актуализация! - page: Страница за освобождаване на GitHub diff --git a/src/apps/update/i18n/translations/bn.yml b/src/apps/update/i18n/translations/bn.yml deleted file mode 100644 index c680a161..00000000 --- a/src/apps/update/i18n/translations/bn.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: তারিখ - version: সংস্করণ - cancel: পরে - installing: ইনস্টল করা হচ্ছে... - downloading: ডাউনলোড হচ্ছে... - title: আপডেট উপলব্ধ! - download: আপডেট এবং ইনস্টল করুন - page: গিটহাব রিলিজ পৃষ্ঠা - extra_info: সম্পূর্ণ চেঞ্জলগ পড়তে, অনুগ্রহ করে চেঞ্জলগ পৃষ্ঠায় যান diff --git a/src/apps/update/i18n/translations/bs.yml b/src/apps/update/i18n/translations/bs.yml deleted file mode 100644 index 225676a2..00000000 --- a/src/apps/update/i18n/translations/bs.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Kasnije - date: Datum - version: Verzija - page: Stranica izdanja GitHub-a - downloading: Preuzimanje... - download: Ažurirajte i instalirajte - installing: Instaliranje... - extra_info: Da pročitate cijeli dnevnik promjena, posjetite stranicu dnevnika promjena - title: Dostupno je ažuriranje! diff --git a/src/apps/update/i18n/translations/ca.yml b/src/apps/update/i18n/translations/ca.yml deleted file mode 100644 index 3d657544..00000000 --- a/src/apps/update/i18n/translations/ca.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Actualització disponible! - version: Versió - date: Data - page: Pàgina de llançament de GitHub - download: Actualitzar i instal·lar - downloading: Descarregant ... - installing: Instal·lació ... - extra_info: Per llegir el canvi complet, visiteu la pàgina de Changelog - cancel: Més tard diff --git a/src/apps/update/i18n/translations/cs.yml b/src/apps/update/i18n/translations/cs.yml deleted file mode 100644 index c39ecc1f..00000000 --- a/src/apps/update/i18n/translations/cs.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Aktualizace k dispozici! - date: Datum - version: Verze - extra_info: Pro přečtení úplného seznamu změn navštivte stránku změn - page: Stránka vydání na GitHubu - cancel: Později - download: Aktualizovat a nainstalovat - downloading: Stahování... - installing: Instalace... diff --git a/src/apps/update/i18n/translations/cy.yml b/src/apps/update/i18n/translations/cy.yml deleted file mode 100644 index ee1fd808..00000000 --- a/src/apps/update/i18n/translations/cy.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Dyddiad - cancel: Yn ddiweddarach - version: Fersiwn - download: Diweddaru a Gosod - title: Diweddariad ar gael! - page: Tudalen Rhyddhau GitHub - installing: Wrthi'n gosod... - downloading: Wrthi'n llwytho i lawr... - extra_info: I ddarllen y changelog llawn, ewch i'r dudalen changelog diff --git a/src/apps/update/i18n/translations/da.yml b/src/apps/update/i18n/translations/da.yml deleted file mode 100644 index aa7c253a..00000000 --- a/src/apps/update/i18n/translations/da.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Opdatering tilgængelig! - date: Dato - version: Version - extra_info: For at læse den fulde ændringslog, besøg venligst ændringslog-siden - page: GitHub-udgivelsesside - cancel: Senere - download: Opdater & installer - downloading: Downloader... - installing: Installerer... diff --git a/src/apps/update/i18n/translations/de.yml b/src/apps/update/i18n/translations/de.yml deleted file mode 100644 index 40aa7e70..00000000 --- a/src/apps/update/i18n/translations/de.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - title: Update verfügbar! - date: Datum - version: Version - extra_info: >- - Um das vollständige Änderungsprotokoll zu lesen, besuchen Sie bitte die - Änderungsprotokollseite - page: GitHub-Veröffentlichungsseite - cancel: Später - download: Aktualisieren & installieren - downloading: Herunterladen... - installing: Installieren... diff --git a/src/apps/update/i18n/translations/el.yml b/src/apps/update/i18n/translations/el.yml deleted file mode 100644 index ab96179d..00000000 --- a/src/apps/update/i18n/translations/el.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Ημερομηνία - cancel: Αργότερα - version: Εκδοχή - title: Διαθέσιμη ενημέρωση! - download: Ενημέρωση και εγκατάσταση - extra_info: Για να διαβάσετε το πλήρες Changelog, επισκεφθείτε τη σελίδα Changelog - downloading: Λήψη ... - page: Σελίδα απελευθέρωσης GitHub - installing: Εγκατάσταση ... diff --git a/src/apps/update/i18n/translations/en.yml b/src/apps/update/i18n/translations/en.yml deleted file mode 100644 index 117474d5..00000000 --- a/src/apps/update/i18n/translations/en.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Update available! - date: Date - version: Version - extra_info: To read the full changelog, please visit the changelog page - page: GitHub Release Page - cancel: Later - download: Update & Install - downloading: Downloading... - installing: Installing... diff --git a/src/apps/update/i18n/translations/es.yml b/src/apps/update/i18n/translations/es.yml deleted file mode 100644 index 5472ed54..00000000 --- a/src/apps/update/i18n/translations/es.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - title: ¡Actualización disponible! - date: Fecha - version: Versión - extra_info: >- - Para leer el registro completo de cambios, por favor visita la página del - registro de cambios - page: Página de lanzamientos en GitHub - cancel: Más tarde - download: Actualizar e instalar - downloading: Descargando... - installing: Instalando... diff --git a/src/apps/update/i18n/translations/et.yml b/src/apps/update/i18n/translations/et.yml deleted file mode 100644 index 8f3287c6..00000000 --- a/src/apps/update/i18n/translations/et.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Uuendus saadaval! - date: Kuupäev - version: Versioon - extra_info: Täieliku muutuste logi lugemiseks külastage palun muutuste lehte - page: GitHubi väljaannete leht - cancel: Hiljem - download: Uuenda ja installi - downloading: Allalaadimine... - installing: Installimine... diff --git a/src/apps/update/i18n/translations/eu.yml b/src/apps/update/i18n/translations/eu.yml deleted file mode 100644 index 07614561..00000000 --- a/src/apps/update/i18n/translations/eu.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - downloading: Deskargatzen... - cancel: Beranduago - date: Data - version: Bertsioa - page: GitHub bertsioaren orria - title: Eguneratzea eskuragarri! - download: Eguneratu eta instalatu - installing: Instalatzen... - extra_info: >- - Aldaketa-erregistro osoa irakurtzeko, mesedez bisitatu aldaketa-erregistroa - orria diff --git a/src/apps/update/i18n/translations/fa.yml b/src/apps/update/i18n/translations/fa.yml deleted file mode 100644 index 2ecc7eff..00000000 --- a/src/apps/update/i18n/translations/fa.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: به روز رسانی در دسترس است! - downloading: در حال دانلود... - version: نسخه - installing: در حال نصب... - cancel: بعد - date: تاریخ - page: صفحه انتشار GitHub - download: به روز رسانی و نصب - extra_info: برای مطالعه تغییرات کامل، لطفاً به صفحه تغییرات وارد شوید diff --git a/src/apps/update/i18n/translations/fi.yml b/src/apps/update/i18n/translations/fi.yml deleted file mode 100644 index 4063742a..00000000 --- a/src/apps/update/i18n/translations/fi.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Versio - cancel: Myöhemmin - title: Päivitys saatavilla! - date: Päivämäärä - download: Päivitä ja asenna - extra_info: Jos haluat lukea koko muutoslogin, käy ChangeLog -sivulla - downloading: Lataaminen ... - installing: Asentaminen ... - page: GitHub -julkaisusivu diff --git a/src/apps/update/i18n/translations/fr.yml b/src/apps/update/i18n/translations/fr.yml deleted file mode 100644 index 4fba3c30..00000000 --- a/src/apps/update/i18n/translations/fr.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - title: Mise à jour disponible! - date: Date - version: Version - extra_info: >- - Pour lire le journal des modifications complet, veuillez visiter la page du - journal des modifications - page: Page de publication de GitHub - cancel: Plus tard - download: Mise à jour - downloading: Téléchargement... - installing: Installation... diff --git a/src/apps/update/i18n/translations/gu.yml b/src/apps/update/i18n/translations/gu.yml deleted file mode 100644 index d8942dc4..00000000 --- a/src/apps/update/i18n/translations/gu.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: તારીખ - version: સંસ્કરણ - cancel: બાદમાં - extra_info: સંપૂર્ણ ચેન્જલોગ વાંચવા માટે, કૃપા કરીને ચેન્જલોગ પૃષ્ઠની મુલાકાત લો - installing: ઇન્સ્ટોલ કરી રહ્યું છે... - title: અપડેટ ઉપલબ્ધ છે! - downloading: ડાઉનલોડ કરી રહ્યું છે... - page: GitHub પ્રકાશન પૃષ્ઠ - download: અપડેટ અને ઇન્સ્ટોલ કરો diff --git a/src/apps/update/i18n/translations/he.yml b/src/apps/update/i18n/translations/he.yml deleted file mode 100644 index c57196b9..00000000 --- a/src/apps/update/i18n/translations/he.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: יותר מאוחר - title: עדכון זמין! - version: גִרְסָה - date: תַאֲרִיך - downloading: מוריד ... - extra_info: לקריאת ה- ChangeLog המלאה, בקרו בדף ChangeLog - download: עדכן והתקנה - page: דף שחרור GitHub - installing: התקנה ... diff --git a/src/apps/update/i18n/translations/hi.yml b/src/apps/update/i18n/translations/hi.yml deleted file mode 100644 index 220dd479..00000000 --- a/src/apps/update/i18n/translations/hi.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - downloading: डाउनलोड करना ... - extra_info: पूर्ण चांगेलॉग पढ़ने के लिए, कृपया चांगलोग पेज पर जाएं - cancel: बाद में - page: Github रिलीज पेज - installing: स्थापित करना ... - version: संस्करण - title: उपलब्ध अद्यतन! - date: तारीख - download: अद्यतन और स्थापित करें diff --git a/src/apps/update/i18n/translations/hr.yml b/src/apps/update/i18n/translations/hr.yml deleted file mode 100644 index acdae16b..00000000 --- a/src/apps/update/i18n/translations/hr.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Verzija - cancel: Kasnije - date: Datum - title: Ažuriranje dostupno! - download: Ažurirajte i instalirajte - installing: Instaliranje ... - downloading: Preuzimanje ... - page: GitHub stranica za izdanje - extra_info: Da biste pročitali cijeli Changelog, posjetite stranicu ChangeLog diff --git a/src/apps/update/i18n/translations/hu.yml b/src/apps/update/i18n/translations/hu.yml deleted file mode 100644 index 02478acb..00000000 --- a/src/apps/update/i18n/translations/hu.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - version: Változat - cancel: A későbbiekben - title: Frissítés elérhető! - date: Dátum - extra_info: >- - A teljes változási mód elolvasásához kérjük, látogasson el a ChangeLog - oldalra - installing: Telepítés ... - downloading: Letöltés ... - download: Frissítés és telepítés - page: GitHub kiadási oldal diff --git a/src/apps/update/i18n/translations/hy.yml b/src/apps/update/i18n/translations/hy.yml deleted file mode 100644 index feaf4f06..00000000 --- a/src/apps/update/i18n/translations/hy.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - cancel: Ավելի ուշ - version: Տարբերակ - date: Ամսաթիվ - downloading: Ներբեռնվում է... - title: 'Թարմացումը հասանելի է:' - installing: Տեղադրվում է... - download: Թարմացնել և տեղադրել - page: GitHub-ի թողարկման էջ - extra_info: >- - Փոփոխությունների ամբողջական տեղեկագիրը կարդալու համար այցելեք փոփոխության - մատյան էջ diff --git a/src/apps/update/i18n/translations/id.yml b/src/apps/update/i18n/translations/id.yml deleted file mode 100644 index 53865cfc..00000000 --- a/src/apps/update/i18n/translations/id.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Pembaruan tersedia! - version: 'Versi: kapan' - date: Tanggal - cancel: Nanti - downloading: Mengunduh ... - download: Perbarui & Instal - extra_info: Untuk membaca changelog lengkap, silakan kunjungi halaman Changelog - installing: Menginstal ... - page: Halaman rilis GitHub diff --git a/src/apps/update/i18n/translations/is.yml b/src/apps/update/i18n/translations/is.yml deleted file mode 100644 index 6ba5a68f..00000000 --- a/src/apps/update/i18n/translations/is.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Seinna - installing: Setur upp... - title: Uppfærsla í boði! - date: Dagsetning - version: Útgáfa - download: Uppfærðu og settu upp - downloading: Sækir... - page: GitHub útgáfusíða - extra_info: Til að lesa breytingaskrána í heild sinni skaltu fara á breytingaskrársíðuna diff --git a/src/apps/update/i18n/translations/it.yml b/src/apps/update/i18n/translations/it.yml deleted file mode 100644 index 6bcc5ed2..00000000 --- a/src/apps/update/i18n/translations/it.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Dopo - version: Versione - title: Aggiornamento disponibile! - date: Data - extra_info: Per leggere l'intero Changelog, visitare la pagina Changelog - downloading: Download ... - page: Pagina di rilascio di GitHub - download: Aggiornamento e installazione - installing: Installazione ... diff --git a/src/apps/update/i18n/translations/ja.yml b/src/apps/update/i18n/translations/ja.yml deleted file mode 100644 index 254ce003..00000000 --- a/src/apps/update/i18n/translations/ja.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: キャンセル - version: バージョン - date: 日付 - title: 更新が利用可能です! - page: GitHubリリースページ - installing: インストール中... - extra_info: 完全なChangelogを読むには、Changelogページにアクセスしてください - download: ダウンロード - downloading: ダウンロード中... diff --git a/src/apps/update/i18n/translations/ka.yml b/src/apps/update/i18n/translations/ka.yml deleted file mode 100644 index a307c545..00000000 --- a/src/apps/update/i18n/translations/ka.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - downloading: მიმდინარეობს ჩამოტვირთვა... - title: Განახლება შესაძლებელია! - cancel: მოგვიანებით - installing: მიმდინარეობს ინსტალაცია... - date: თარიღი - version: ვერსია - extra_info: >- - ცვლილებების სრული ჟურნალის წასაკითხად, გთხოვთ, ეწვიოთ ცვლილებების ჟურნალის - გვერდს - download: განახლება და ინსტალაცია - page: GitHub გამოშვების გვერდი diff --git a/src/apps/update/i18n/translations/km.yml b/src/apps/update/i18n/translations/km.yml deleted file mode 100644 index 077ff46c..00000000 --- a/src/apps/update/i18n/translations/km.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - downloading: កំពុងទាញយក... - date: កាលបរិច្ឆេទ - cancel: ពេលក្រោយ - version: កំណែ - title: អាប់ដេតមានហើយ! - page: ទំព័រចេញផ្សាយ GitHub - download: ធ្វើបច្ចុប្បន្នភាព និងដំឡើង - extra_info: ដើម្បីអានកំណត់ហេតុផ្លាស់ប្តូរពេញលេញ សូមចូលទៅកាន់ទំព័រផ្លាស់ប្តូរកំណត់ហេតុ - installing: កំពុងដំឡើង... diff --git a/src/apps/update/i18n/translations/ko.yml b/src/apps/update/i18n/translations/ko.yml deleted file mode 100644 index f7406beb..00000000 --- a/src/apps/update/i18n/translations/ko.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: 업데이트가 있습니다! - date: 날짜 - version: 버전 - extra_info: 전체 변경 로그를 읽으려면 변경 로그 페이지를 방문하세요 - page: GitHub 릴리스 페이지 - cancel: 나중에 - download: 업데이트 및 설치 - downloading: 다운로드 중... - installing: 설치 중... diff --git a/src/apps/update/i18n/translations/ku.yml b/src/apps/update/i18n/translations/ku.yml deleted file mode 100644 index c4c4de11..00000000 --- a/src/apps/update/i18n/translations/ku.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Rojek - cancel: Paşan - version: Awa - download: Nûvekirin & Sazkirin - extra_info: Ji bo xwendina guhertoya tevahî, ji kerema xwe biçin rûpela guhartinê - title: Nûvekirin heye! - downloading: Daxistin... - installing: Sazkirin... - page: Rûpelê Weşandina GitHub diff --git a/src/apps/update/i18n/translations/lb.yml b/src/apps/update/i18n/translations/lb.yml deleted file mode 100644 index f07851be..00000000 --- a/src/apps/update/i18n/translations/lb.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Datum - version: Versioun - cancel: Méi spéit - title: Update verfügbar! - downloading: Eroflueden... - page: GitHub Release Säit - download: Update & Installéieren - extra_info: Fir de komplette Changelog ze liesen, besicht w.e.g. d'changelog Säit - installing: Installéiert ... diff --git a/src/apps/update/i18n/translations/lo.yml b/src/apps/update/i18n/translations/lo.yml deleted file mode 100644 index e51c3749..00000000 --- a/src/apps/update/i18n/translations/lo.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: ຕໍ່ມາ - version: ຮຸ່ນ - date: ວັນທີ - title: ມີອັບເດດແລ້ວ! - downloading: ກຳລັງດາວໂຫຼດ... - download: ອັບເດດ ແລະຕິດຕັ້ງ - page: ຫນ້າປ່ອຍ GitHub - installing: ກຳລັງຕິດຕັ້ງ... - extra_info: ເພື່ອອ່ານບັນທຶກການປ່ຽນແປງເຕັມ, ກະລຸນາເຂົ້າໄປທີ່ໜ້າ changelog diff --git a/src/apps/update/i18n/translations/lt.yml b/src/apps/update/i18n/translations/lt.yml deleted file mode 100644 index 3e4fc9c0..00000000 --- a/src/apps/update/i18n/translations/lt.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Versija - date: Data - title: Galimas atnaujinimas! - extra_info: Norėdami perskaityti visą „Changelog“, apsilankykite „Changelog“ puslapyje - downloading: Atsisiuntimas ... - cancel: Vėliau - installing: Diegimas ... - page: „GitHub“ išleidimo puslapis - download: Atnaujinkite ir įdiekite diff --git a/src/apps/update/i18n/translations/lv.yml b/src/apps/update/i18n/translations/lv.yml deleted file mode 100644 index 755c719f..00000000 --- a/src/apps/update/i18n/translations/lv.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Datums - version: Versija - extra_info: Lai izlasītu pilnu maiņu, lūdzu, apmeklējiet vietni Changelog - download: Atjaunināt un instalēt - downloading: Lejupielāde ... - cancel: Vēlāk - title: Pieejams atjauninājums! - installing: Instalēšana ... - page: GitHub izlaišanas lapa diff --git a/src/apps/update/i18n/translations/mk.yml b/src/apps/update/i18n/translations/mk.yml deleted file mode 100644 index 75f9badb..00000000 --- a/src/apps/update/i18n/translations/mk.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - date: Датум - version: Верзија - cancel: Подоцна - extra_info: >- - За да го прочитате целосниот дневник за промени, посетете ја страницата за - дневник на промени - download: Ажурирај и инсталирај - title: Достапно е ажурирање! - downloading: Се презема... - installing: Се инсталира... - page: Страница за издавање на GitHub diff --git a/src/apps/update/i18n/translations/mn.yml b/src/apps/update/i18n/translations/mn.yml deleted file mode 100644 index 9698e05e..00000000 --- a/src/apps/update/i18n/translations/mn.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - title: Шинэчлэх боломжтой! - version: Хувилбар - cancel: Дараа нь - date: Огноо - extra_info: >- - Өөрчлөлтийн бүртгэлийг бүрэн эхээр нь уншихыг хүсвэл өөрчлөлтийн бүртгэлийн - хуудсанд зочилно уу - installing: Суулгаж байна... - page: GitHub хувилбарын хуудас - downloading: Татаж авч байна... - download: Шинэчлэх & Суулгах diff --git a/src/apps/update/i18n/translations/ms.yml b/src/apps/update/i18n/translations/ms.yml deleted file mode 100644 index 1466126f..00000000 --- a/src/apps/update/i18n/translations/ms.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Versi - page: Halaman pelepasan GitHub - title: Kemaskini ada! - date: Tarikh - downloading: Memuat turun ... - extra_info: Untuk membaca changelog penuh, sila lawati halaman Changelog - cancel: Kemudian - installing: Memasang ... - download: Kemas kini & Pasang diff --git a/src/apps/update/i18n/translations/mt.yml b/src/apps/update/i18n/translations/mt.yml deleted file mode 100644 index f0f1562d..00000000 --- a/src/apps/update/i18n/translations/mt.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Data - cancel: Aktar tard - download: Aġġorna u Installa - title: Aġġornament disponibbli! - page: Paġna ta' Rilaxx ta' GitHub - version: Verżjoni - downloading: Niżżel... - installing: Installazzjoni... - extra_info: Biex taqra l-changelog sħiħ, jekk jogħġbok żur il-paġna changelog diff --git a/src/apps/update/i18n/translations/ne.yml b/src/apps/update/i18n/translations/ne.yml deleted file mode 100644 index e58d0232..00000000 --- a/src/apps/update/i18n/translations/ne.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: अपडेट उपलब्ध छ! - download: अपडेट र स्थापना गर्नुहोस् - date: मिति - cancel: पछि - downloading: डाउनलोड गर्दै... - version: संस्करण - page: GitHub रिलीज पृष्ठ - installing: स्थापना गर्दै... - extra_info: पूरा चेन्जलग पढ्नको लागि, कृपया चेन्जलग पृष्ठमा जानुहोस् diff --git a/src/apps/update/i18n/translations/nl.yml b/src/apps/update/i18n/translations/nl.yml deleted file mode 100644 index 8a8b13b9..00000000 --- a/src/apps/update/i18n/translations/nl.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Later - version: Versie - title: Update beschikbaar! - date: Datum - extra_info: Ga naar de Changelog -pagina om de volledige changelog te lezen - installing: Installeren ... - page: GitHub release pagina - downloading: Downloaden ... - download: Update & installeren diff --git a/src/apps/update/i18n/translations/no.yml b/src/apps/update/i18n/translations/no.yml deleted file mode 100644 index fe8d05fb..00000000 --- a/src/apps/update/i18n/translations/no.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Oppdatering tilgjengelig! - version: Versjon - cancel: Seinere - date: Dato - download: Oppdater og installer - installing: Installere ... - extra_info: For å lese hele Changelog, besøk Changelog -siden - downloading: Last ned ... - page: GitHub Release Page diff --git a/src/apps/update/i18n/translations/pa.yml b/src/apps/update/i18n/translations/pa.yml deleted file mode 100644 index 19aba255..00000000 --- a/src/apps/update/i18n/translations/pa.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: ਸੰਸਕਰਣ - cancel: ਬਾਅਦ ਵਿੱਚ - date: ਤਾਰੀਖ਼ - downloading: ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ... - title: ਅੱਪਡੇਟ ਉਪਲਬਧ ਹੈ! - extra_info: ਪੂਰਾ ਚੇਂਜਲੌਗ ਪੜ੍ਹਨ ਲਈ, ਕਿਰਪਾ ਕਰਕੇ ਚੇਂਜਲੌਗ ਪੰਨੇ 'ਤੇ ਜਾਓ - page: GitHub ਰੀਲੀਜ਼ ਪੰਨਾ - download: ਅੱਪਡੇਟ ਅਤੇ ਸਥਾਪਿਤ ਕਰੋ - installing: ਸਥਾਪਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ... diff --git a/src/apps/update/i18n/translations/pl.yml b/src/apps/update/i18n/translations/pl.yml deleted file mode 100644 index f7b568d2..00000000 --- a/src/apps/update/i18n/translations/pl.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Wersja - cancel: Później - date: Data - title: Dostępna aktualizacja! - downloading: Ściąganie... - download: Zaktualizuj i zainstaluj - extra_info: Aby przeczytać pełny Changelog, odwiedź stronę Changelog - page: Strona wydania Github - installing: Instalowanie ... diff --git a/src/apps/update/i18n/translations/ps.yml b/src/apps/update/i18n/translations/ps.yml deleted file mode 100644 index 5bf64d1a..00000000 --- a/src/apps/update/i18n/translations/ps.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: نیټه - version: نسخه - installing: نصب کول... - title: تازه معلومات شتون لري! - download: تازه کول او نصب کول - extra_info: د بشپړ بدلون لاګ لوستلو لپاره ، مهرباني وکړئ د چینج لاګ پا pageې ته لاړشئ - page: د GitHub خوشې پاڼه - cancel: وروسته - downloading: کښته کول... diff --git a/src/apps/update/i18n/translations/pt.yml b/src/apps/update/i18n/translations/pt.yml deleted file mode 100644 index 018624e6..00000000 --- a/src/apps/update/i18n/translations/pt.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Atualização disponível! - date: Data - version: Versão - extra_info: Para ler o changelog completo, visite a página changelog - page: Página de lançamento do GitHub - cancel: Mais tarde - download: Atualizar - downloading: Baixando... - installing: Instalando... diff --git a/src/apps/update/i18n/translations/ro.yml b/src/apps/update/i18n/translations/ro.yml deleted file mode 100644 index a6067352..00000000 --- a/src/apps/update/i18n/translations/ro.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Versiune - cancel: Mai tarziu - downloading: Descărcarea... - title: Actualizare disponibila! - date: Data - extra_info: Pentru a citi ChangeLog complet, vă rugăm să vizitați pagina ChangeLog - installing: Instalare ... - page: Pagina de eliberare Github - download: Actualizați și instalați diff --git a/src/apps/update/i18n/translations/ru.yml b/src/apps/update/i18n/translations/ru.yml deleted file mode 100644 index 9b040cc0..00000000 --- a/src/apps/update/i18n/translations/ru.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - title: Доступно обновление! - date: Дата - version: Версия - extra_info: >- - Чтобы ознакомиться с полным списком изменений, посетите страницу - списка изменений. - page: Страница релизов на GitHub - cancel: Не сейчас - download: Скачать и установить - downloading: Скачивание... - installing: Установка... diff --git a/src/apps/update/i18n/translations/si.yml b/src/apps/update/i18n/translations/si.yml deleted file mode 100644 index 757f48ea..00000000 --- a/src/apps/update/i18n/translations/si.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: පිටපත - installing: ස්ථාපනය කරමින්... - cancel: පසු - date: දිනය - title: යාවත්කාලීන ලබා ගත හැක! - downloading: බාගනිමින්... - download: යාවත්කාලීන කිරීම සහ ස්ථාපනය කිරීම - page: GitHub නිකුතු පිටුව - extra_info: සම්පූර්ණ චේන්ජ්ලොග් කියවීමට, කරුණාකර චේන්ජ්ලොග් පිටුවට පිවිසෙන්න diff --git a/src/apps/update/i18n/translations/sk.yml b/src/apps/update/i18n/translations/sk.yml deleted file mode 100644 index 271a347f..00000000 --- a/src/apps/update/i18n/translations/sk.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Verzia - title: Aktualizácia je dostupná! - date: Dátum - download: Aktualizovať a inštalovať - installing: Inštalácia ... - extra_info: Ak si chcete prečítať úplný Changelog, navštívte stránku Changelog - downloading: Sťahovanie ... - cancel: Neskôr - page: Stránka vydania GitHub diff --git a/src/apps/update/i18n/translations/so.yml b/src/apps/update/i18n/translations/so.yml deleted file mode 100644 index 1563f454..00000000 --- a/src/apps/update/i18n/translations/so.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Ka dib - downloading: Soo dejinaya... - date: Taariikhda - download: Cusbooneysii & Ku rakib - title: Cusbooneysii waa la heli karaa! - installing: Rakibaya... - version: Nooca - page: Bogga Siideynta GitHub - extra_info: Si aad u akhrido qoraalka buuxa, fadlan booqo bogga beddelka diff --git a/src/apps/update/i18n/translations/sr.yml b/src/apps/update/i18n/translations/sr.yml deleted file mode 100644 index 8dc6cd01..00000000 --- a/src/apps/update/i18n/translations/sr.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Располагању новија верзија! - cancel: Касније - date: Датум - download: Ажурирајте и инсталирајте - version: Верзија - installing: Инсталирање... - extra_info: Да бисте прочитали цео дневник промена, посетите страницу дневника промена - page: Страница издања ГитХуб-а - downloading: Преузимање... diff --git a/src/apps/update/i18n/translations/sv.yml b/src/apps/update/i18n/translations/sv.yml deleted file mode 100644 index ac5db693..00000000 --- a/src/apps/update/i18n/translations/sv.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: Uppdatering tillgänglig! - cancel: Senare - downloading: Laddar ner... - date: Datum - version: Version - page: Github släppsida - extra_info: För att läsa hela ChangeLog, besök Changelog -sidan - download: Uppdatering och installation - installing: Installera ... diff --git a/src/apps/update/i18n/translations/sw.yml b/src/apps/update/i18n/translations/sw.yml deleted file mode 100644 index 0434bf02..00000000 --- a/src/apps/update/i18n/translations/sw.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - cancel: Baadae - date: Tarehe - downloading: Inapakua... - title: Sasisho linapatikana! - download: Sasisha na Usakinishe - installing: Inasakinisha... - page: Ukurasa wa Kutolewa wa GitHub - version: Toleo - extra_info: >- - Ili kusoma logi kamili ya mabadiliko, tafadhali tembelea ukurasa wa - mabadiliko diff --git a/src/apps/update/i18n/translations/ta.yml b/src/apps/update/i18n/translations/ta.yml deleted file mode 100644 index 3ff3f103..00000000 --- a/src/apps/update/i18n/translations/ta.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: தேதி - version: பதிப்பு - cancel: பின்னர் - installing: நிறுவுகிறது... - extra_info: முழு சேஞ்ச்லாக் படிக்க, சேஞ்ச்லாக் பக்கத்தைப் பார்வையிடவும் - title: புதுப்பிப்பு கிடைக்கிறது! - download: புதுப்பித்து நிறுவவும் - page: GitHub வெளியீட்டு பக்கம் - downloading: பதிவிறக்குகிறது... diff --git a/src/apps/update/i18n/translations/te.yml b/src/apps/update/i18n/translations/te.yml deleted file mode 100644 index 3359022d..00000000 --- a/src/apps/update/i18n/translations/te.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: 'సంస్కరణ: Telugu' - date: తేదీ - downloading: డౌన్‌లోడ్ చేస్తోంది... - cancel: తరువాత - extra_info: పూర్తి చేంజ్లాగ్ చదవడానికి, దయచేసి చేంజ్లాగ్ పేజీని సందర్శించండి - title: అందుబాటులో నవీకరణ! - download: అప్‌డేట్ & ఇన్‌స్టాల్ చేయండి - installing: ఇన్‌స్టాల్ చేస్తోంది... - page: GitHub విడుదల పేజీ diff --git a/src/apps/update/i18n/translations/tg.yml b/src/apps/update/i18n/translations/tg.yml deleted file mode 100644 index 9610ecf8..00000000 --- a/src/apps/update/i18n/translations/tg.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Версия - cancel: Баъдтар - date: Сана - title: Навсозии дастрас! - downloading: Зеркашӣ карда мешавад... - download: Навсозӣ ва насб - installing: Насб карда мешавад... - page: Саҳифаи нашри GitHub - extra_info: Барои хондани гузориши пурраи тағирот, лутфан ба саҳифаи тағирот ворид шавед diff --git a/src/apps/update/i18n/translations/th.yml b/src/apps/update/i18n/translations/th.yml deleted file mode 100644 index 2253e8c8..00000000 --- a/src/apps/update/i18n/translations/th.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: วันที่ - version: รุ่น - cancel: ภายหลัง - title: อัปเดตพร้อมใช้งาน! - download: อัปเดตและติดตั้ง - extra_info: หากต้องการอ่านการเปลี่ยนแปลงเต็มรูปแบบโปรดไปที่หน้า Changelog - page: หน้าปล่อย GitHub - downloading: ดาวน์โหลด ... - installing: การติดตั้ง ... diff --git a/src/apps/update/i18n/translations/tl.yml b/src/apps/update/i18n/translations/tl.yml deleted file mode 100644 index bab0522b..00000000 --- a/src/apps/update/i18n/translations/tl.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - version: Bersyon - extra_info: >- - Upang mabasa ang buong Changelog, mangyaring bisitahin ang pahina ng - Changelog - cancel: Kalaunan - download: I -update at i -install - title: Mag -update Magagamit! - downloading: Pag -download ... - page: Pahina ng paglabas ng Github - date: Petsa - installing: Pag -install ... diff --git a/src/apps/update/i18n/translations/tr.yml b/src/apps/update/i18n/translations/tr.yml deleted file mode 100644 index c36473bf..00000000 --- a/src/apps/update/i18n/translations/tr.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Daha sonra - version: Versiyon - title: Güncelleme uygun! - date: Tarih - extra_info: Değişikliğin tamamını okumak için lütfen ChangeLog sayfasını ziyaret edin - page: Github Sürüm Sayfası - downloading: İndirme ... - download: Güncelleme ve Kurulum - installing: Yükleme ... diff --git a/src/apps/update/i18n/translations/uk.yml b/src/apps/update/i18n/translations/uk.yml deleted file mode 100644 index 94681406..00000000 --- a/src/apps/update/i18n/translations/uk.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: Версія - date: Дата - downloading: Завантаження ... - download: Оновлення та встановлення - extra_info: Щоб прочитати повний Changelog, відвідайте сторінку Changelog - page: Сторінка випуску Github - cancel: Пізніше - installing: Встановлення ... - title: Оновлення доступне! diff --git a/src/apps/update/i18n/translations/ur.yml b/src/apps/update/i18n/translations/ur.yml deleted file mode 100644 index a420bd23..00000000 --- a/src/apps/update/i18n/translations/ur.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - version: ورژن - downloading: ڈاؤن لوڈ ہو رہا ہے... - cancel: بعد میں - date: تاریخ - installing: انسٹال ہو رہا ہے... - download: اپ ڈیٹ اور انسٹال کریں۔ - page: GitHub ریلیز صفحہ - title: اپ ڈیٹ دستیاب ہے! - extra_info: مکمل چینج لاگ پڑھنے کے لیے، براہ کرم چینج لاگ کا صفحہ دیکھیں diff --git a/src/apps/update/i18n/translations/uz.yml b/src/apps/update/i18n/translations/uz.yml deleted file mode 100644 index bf682083..00000000 --- a/src/apps/update/i18n/translations/uz.yml +++ /dev/null @@ -1,12 +0,0 @@ -update: - downloading: Yuklab olinmoqda... - date: Sana - installing: Oʻrnatilmoqda... - cancel: Keyinchalik - version: Versiya - title: Yangilanish mavjud! - download: Yangilash va oʻrnatish - page: GitHub nashr sahifasi - extra_info: >- - Toʻliq oʻzgarishlar jurnalini oʻqish uchun oʻzgarishlar jurnali sahifasiga - tashrif buyuring diff --git a/src/apps/update/i18n/translations/vi.yml b/src/apps/update/i18n/translations/vi.yml deleted file mode 100644 index f15a8ffa..00000000 --- a/src/apps/update/i18n/translations/vi.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - date: Ngày - version: Phiên bản - cancel: Sau đó - title: Cập nhật có sẵn! - downloading: Tải xuống ... - download: Cập nhật & Cài đặt - page: Trang phát hành GitHub - installing: Cài đặt ... - extra_info: Để đọc toàn bộ Changelog, vui lòng truy cập trang Changelog diff --git a/src/apps/update/i18n/translations/yo.yml b/src/apps/update/i18n/translations/yo.yml deleted file mode 100644 index acd9bc84..00000000 --- a/src/apps/update/i18n/translations/yo.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Nigbamii - date: Ọjọ - extra_info: Lati ka iwe iyipada ni kikun, jọwọ ṣabẹwo si oju-iwe changelog - download: Imudojuiwọn & Fi sori ẹrọ - title: Imudojuiwọn wa! - version: Ẹya - downloading: Gbigbasilẹ... - page: Oju-iwe Tu GitHub - installing: Nfi sori ẹrọ... diff --git a/src/apps/update/i18n/translations/zh.yml b/src/apps/update/i18n/translations/zh.yml deleted file mode 100644 index 651615aa..00000000 --- a/src/apps/update/i18n/translations/zh.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - title: 有更新可用! - date: 日期 - version: 版本 - extra_info: 要阅读完整的更新日志,请访问更新日志页面 - page: GitHub发布页面 - cancel: 稍后 - download: 更新并安装 - downloading: 下载中... - installing: 安装中... diff --git a/src/apps/update/i18n/translations/zu.yml b/src/apps/update/i18n/translations/zu.yml deleted file mode 100644 index b9058797..00000000 --- a/src/apps/update/i18n/translations/zu.yml +++ /dev/null @@ -1,10 +0,0 @@ -update: - cancel: Kamuva - version: Inguqulo - date: Usuku - title: Isibuyekezo siyatholakala! - extra_info: Ukuze ufunde ukuguqulwa okuphelele, sicela uvakashele ikhasi lelogi - download: Buyekeza futhi ufake - downloading: Iyalanda... - installing: Iyafaka... - page: Ikhasi lokukhishwa kwe-GitHub diff --git a/src/apps/update/index.tsx b/src/apps/update/index.tsx deleted file mode 100644 index 4c10e30a..00000000 --- a/src/apps/update/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { UserSettingsLoader } from '../settings/modules/shared/store/storeApi'; -import { getRootContainer } from '../shared'; -import { wrapConsole } from '../shared/ConsoleWrapper'; -import i18n, { loadTranslations } from './i18n'; -import { createRoot } from 'react-dom/client'; -import { I18nextProvider } from 'react-i18next'; - -import { App } from './app'; - -import './styles/colors.css'; -import './styles/reset.css'; -import './styles/global.css'; - -async function main() { - const container = getRootContainer(); - wrapConsole(); - - let { jsonSettings } = await new UserSettingsLoader().withThemes(false).load(); - await loadTranslations(); - i18n.changeLanguage(jsonSettings.language); - - createRoot(container).render( - - - , - ); -} - -main(); \ No newline at end of file diff --git a/src/apps/update/styles/colors.css b/src/apps/update/styles/colors.css deleted file mode 100644 index ec8ed844..00000000 --- a/src/apps/update/styles/colors.css +++ /dev/null @@ -1,599 +0,0 @@ -:root { - /* Persisted colors variables (not changed on dark mode) */ - --color-persist-white: #ffffff; - --color-persist-gray-50: #fdfdfd; - --color-persist-gray-100: #f8f8f8; - --color-persist-gray-200: #e6e6e6; - --color-persist-gray-300: #d5d5d5; - --color-persist-gray-400: #b1b1b1; - --color-persist-gray-500: #909090; - --color-persist-gray-600: #6d6d6d; - --color-persist-gray-700: #464646; - --color-persist-gray-800: #222222; - --color-persist-gray-900: #151515; - --color-persist-black: #000000; - - --color-persist-blue-100: #e0f2ff; - --color-persist-blue-200: #cae8ff; - --color-persist-blue-300: #b5deff; - --color-persist-blue-400: #96cefd; - --color-persist-blue-500: #78bbfa; - --color-persist-blue-600: #59a7f6; - --color-persist-blue-700: #3892f3; - --color-persist-blue-800: #147af3; - --color-persist-blue-900: #0265dc; - --color-persist-blue-1000: #0054b6; - --color-persist-blue-1100: #004491; - --color-persist-blue-1200: #003571; - --color-persist-blue-1300: #002754; - - --color-persist-green-100: #cef8e0; - --color-persist-green-200: #adf4ce; - --color-persist-green-300: #89ecbc; - --color-persist-green-400: #67dea8; - --color-persist-green-500: #49cc93; - --color-persist-green-600: #2fb880; - --color-persist-green-700: #15a46e; - --color-persist-green-800: #008f5d; - --color-persist-green-900: #007a4d; - --color-persist-green-1000: #00653e; - --color-persist-green-1100: #005132; - --color-persist-green-1200: #053f27; - --color-persist-green-1300: #0a2e1d; - - --color-persist-orange-100: #ffeccc; - --color-persist-orange-200: #ffdfad; - --color-persist-orange-300: #fdd291; - --color-persist-orange-400: #ffbb63; - --color-persist-orange-500: #ffa037; - --color-persist-orange-600: #f68511; - --color-persist-orange-700: #e46f00; - --color-persist-orange-800: #cb5d00; - --color-persist-orange-900: #b14c00; - --color-persist-orange-1000: #953d00; - --color-persist-orange-1100: #7a2f00; - --color-persist-orange-1200: #612300; - --color-persist-orange-1300: #491901; - - --color-persist-red-100: #ffebe7; - --color-persist-red-200: #ffddd6; - --color-persist-red-300: #ffcdc3; - --color-persist-red-400: #ffb7a9; - --color-persist-red-500: #ff9b88; - --color-persist-red-600: #ff7c65; - --color-persist-red-700: #f75c46; - --color-persist-red-800: #ea3829; - --color-persist-red-900: #d31510; - --color-persist-red-1000: #b40000; - --color-persist-red-1100: #930000; - --color-persist-red-1200: #740000; - --color-persist-red-1300: #590000; - - --color-persist-celery-100: #cdfcbf; - --color-persist-celery-200: #aef69d; - --color-persist-celery-300: #96ee85; - --color-persist-celery-400: #72e06a; - --color-persist-celery-500: #4ecf50; - --color-persist-celery-600: #27bb36; - --color-persist-celery-700: #07a721; - --color-persist-celery-800: #009112; - --color-persist-celery-900: #007c0f; - --color-persist-celery-1000: #00670f; - --color-persist-celery-1100: #00530d; - --color-persist-celery-1200: #00400a; - --color-persist-celery-1300: #003007; - - --color-persist-chartreuse-100: #dbfc6e; - --color-persist-chartreuse-200: #cbf443; - --color-persist-chartreuse-300: #bce92a; - --color-persist-chartreuse-400: #aad816; - --color-persist-chartreuse-500: #98c50a; - --color-persist-chartreuse-600: #87b103; - --color-persist-chartreuse-700: #769c00; - --color-persist-chartreuse-800: #678800; - --color-persist-chartreuse-900: #577400; - --color-persist-chartreuse-1000: #486000; - --color-persist-chartreuse-1100: #3a4d00; - --color-persist-chartreuse-1200: #2c3b00; - --color-persist-chartreuse-1300: #212c00; - - --color-persist-cyan-100: #c5f8ff; - --color-persist-cyan-200: #a4f0ff; - --color-persist-cyan-300: #88e7fa; - --color-persist-cyan-400: #60d8f3; - --color-persist-cyan-500: #33c5e8; - --color-persist-cyan-600: #12b0da; - --color-persist-cyan-700: #019cc8; - --color-persist-cyan-800: #0086b4; - --color-persist-cyan-900: #00719f; - --color-persist-cyan-1000: #005d89; - --color-persist-cyan-1100: #004a73; - --color-persist-cyan-1200: #00395d; - --color-persist-cyan-1300: #002a46; - - --color-persist-fuchsia-100: #ffe9fc; - --color-persist-fuchsia-200: #ffdafa; - --color-persist-fuchsia-300: #fec7f8; - --color-persist-fuchsia-400: #fbaef6; - --color-persist-fuchsia-500: #f592f3; - --color-persist-fuchsia-600: #ed74ed; - --color-persist-fuchsia-700: #e055e2; - --color-persist-fuchsia-800: #cd3ace; - --color-persist-fuchsia-900: #b622b7; - --color-persist-fuchsia-1000: #9d039e; - --color-persist-fuchsia-1100: #800081; - --color-persist-fuchsia-1200: #640664; - --color-persist-fuchsia-1300: #470e46; - - --color-persist-indigo-100: #edeeff; - --color-persist-indigo-200: #e0e2ff; - --color-persist-indigo-300: #d3d5ff; - --color-persist-indigo-400: #c1c4ff; - --color-persist-indigo-500: #acafff; - --color-persist-indigo-600: #9599ff; - --color-persist-indigo-700: #7e84fc; - --color-persist-indigo-800: #686df4; - --color-persist-indigo-900: #5258e4; - --color-persist-indigo-1000: #4046ca; - --color-persist-indigo-1100: #3236a8; - --color-persist-indigo-1200: #262986; - --color-persist-indigo-1300: #1b1e64; - - --color-persist-magenta-100: #ffeaf1; - --color-persist-magenta-200: #ffdce8; - --color-persist-magenta-300: #ffcadd; - --color-persist-magenta-400: #ffb2ce; - --color-persist-magenta-500: #ff95bd; - --color-persist-magenta-600: #fa77aa; - --color-persist-magenta-700: #ef5a98; - --color-persist-magenta-800: #de3d82; - --color-persist-magenta-900: #c82269; - --color-persist-magenta-1000: #ad0955; - --color-persist-magenta-1100: #8e0045; - --color-persist-magenta-1200: #700037; - --color-persist-magenta-1300: #54032a; - - --color-persist-purple-100: #f6ebff; - --color-persist-purple-200: #eeddff; - --color-persist-purple-300: #e6d0ff; - --color-persist-purple-400: #dbbbfe; - --color-persist-purple-500: #cca4fd; - --color-persist-purple-600: #bd8bfc; - --color-persist-purple-700: #ae72f9; - --color-persist-purple-800: #9d57f4; - --color-persist-purple-900: #893de7; - --color-persist-purple-1000: #7326d3; - --color-persist-purple-1100: #5d13b7; - --color-persist-purple-1200: #470c94; - --color-persist-purple-1300: #33106a; - - --color-persist-seafoam-100: #cef7f3; - --color-persist-seafoam-200: #aaf1ea; - --color-persist-seafoam-300: #8ce9e2; - --color-persist-seafoam-400: #65dad2; - --color-persist-seafoam-500: #3fc9c1; - --color-persist-seafoam-600: #0fb5ae; - --color-persist-seafoam-700: #00a19a; - --color-persist-seafoam-800: #008c87; - --color-persist-seafoam-900: #007772; - --color-persist-seafoam-1000: #00635f; - --color-persist-seafoam-1100: #0c4f4c; - --color-persist-seafoam-1200: #123c3a; - --color-persist-seafoam-1300: #122c2b; - - --color-persist-yellow-100: #fbf198; - --color-persist-yellow-200: #f8e750; - --color-persist-yellow-300: #f8d904; - --color-persist-yellow-400: #e8c600; - --color-persist-yellow-500: #d7b300; - --color-persist-yellow-600: #c49f00; - --color-persist-yellow-700: #b08c00; - --color-persist-yellow-800: #9b7800; - --color-persist-yellow-900: #856600; - --color-persist-yellow-1000: #705300; - --color-persist-yellow-1100: #5b4300; - --color-persist-yellow-1200: #483300; - --color-persist-yellow-1300: #362500; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-white: #000000; - - --color-gray-50: #151515; - --color-gray-100: #222222; - --color-gray-200: #464646; - --color-gray-300: #6d6d6d; - --color-gray-400: #909090; - --color-gray-500: #b1b1b1; - --color-gray-600: #d5d5d5; - --color-gray-700: #e6e6e6; - --color-gray-800: #f8f8f8; - --color-gray-900: #fdfdfd; - - --color-black: #ffffff; - - --color-blue-100: #003877; - --color-blue-200: #00418a; - --color-blue-300: #004da3; - --color-blue-400: #0059c2; - --color-blue-500: #0367e0; - --color-blue-600: #1379f3; - --color-blue-700: #348ff4; - --color-blue-800: #54a3f6; - --color-blue-900: #72b7f9; - --color-blue-1000: #8fcafc; - --color-blue-1100: #aedbfe; - --color-blue-1200: #cce9ff; - --color-blue-1300: #e8f6ff; - - --color-green-100: #044329; - --color-green-200: #004e2f; - --color-green-300: #005c38; - --color-green-400: #006c43; - --color-green-500: #007d4e; - --color-green-600: #008f5d; - --color-green-700: #12a26c; - --color-green-800: #2bb47d; - --color-green-900: #43c78f; - --color-green-1000: #5ed9a2; - --color-green-1100: #81e9b8; - --color-green-1200: #b1f4d1; - --color-green-1300: #dffaea; - - --color-orange-100: #662500; - --color-orange-200: #752d00; - --color-orange-300: #893700; - --color-orange-400: #9e4200; - --color-orange-500: #b44e00; - --color-orange-600: #ca5d00; - --color-orange-700: #e16d00; - --color-orange-800: #f4810c; - --color-orange-900: #fe9a2e; - --color-orange-1000: #ffb558; - --color-orange-1100: #fdce88; - --color-orange-1200: #ffe1b3; - --color-orange-1300: #fff2dd; - - --color-red-100: #7b0000; - --color-red-200: #8d0000; - --color-red-300: #a50000; - --color-red-400: #be0403; - --color-red-500: #d71913; - --color-red-600: #ea3829; - --color-red-700: #f65843; - --color-red-800: #ff755e; - --color-red-900: #ff9581; - --color-red-1000: #ffb0a1; - --color-red-1100: #ffc9bd; - --color-red-1200: #ffded8; - --color-red-1300: #fff1ee; - - --color-celery-100: #00450a; - --color-celery-200: #00500c; - --color-celery-300: #005e0e; - --color-celery-400: #006d0f; - --color-celery-500: #007f0f; - --color-celery-600: #009112; - --color-celery-700: #04a51e; - --color-celery-800: #22b833; - --color-celery-900: #44ca49; - --color-celery-1000: #69dc63; - --color-celery-1100: #8eeb7f; - --color-celery-1200: #b4f7a2; - --color-celery-1300: #ddfdd3; - - --color-chartreuse-100: #304000; - --color-chartreuse-200: #374a00; - --color-chartreuse-300: #415700; - --color-chartreuse-400: #4c6600; - --color-chartreuse-500: #597600; - --color-chartreuse-600: #668800; - --color-chartreuse-700: #759a00; - --color-chartreuse-800: #84ad01; - --color-chartreuse-900: #94c008; - --color-chartreuse-1000: #a6d312; - --color-chartreuse-1100: #b8e525; - --color-chartreuse-1200: #cdf547; - --color-chartreuse-1300: #e7fe9a; - - --color-cyan-100: #003d62; - --color-cyan-200: #00476f; - --color-cyan-300: #00557f; - --color-cyan-400: #006491; - --color-cyan-500: #0074a2; - --color-cyan-600: #0086b4; - --color-cyan-700: #0099c6; - --color-cyan-800: #0eadd7; - --color-cyan-900: #2cc1e6; - --color-cyan-1000: #54d3f1; - --color-cyan-1100: #7fe4f9; - --color-cyan-1200: #a7f1ff; - --color-cyan-1300: #d7faff; - - --color-fuchsia-100: #6b036a; - --color-fuchsia-200: #7b007b; - --color-fuchsia-300: #900091; - --color-fuchsia-400: #a50da6; - --color-fuchsia-500: #b925b9; - --color-fuchsia-600: #cd39ce; - --color-fuchsia-700: #df51e0; - --color-fuchsia-800: #eb6eec; - --color-fuchsia-900: #f48cf2; - --color-fuchsia-1000: #faa8f5; - --color-fuchsia-1100: #fec2f8; - --color-fuchsia-1200: #ffdbfa; - --color-fuchsia-1300: #ffeffc; - - --color-indigo-100: #282c8c; - --color-indigo-200: #2f34a3; - --color-indigo-300: #393fbb; - --color-indigo-400: #464bd3; - --color-indigo-500: #555be7; - --color-indigo-600: #686df4; - --color-indigo-700: #7c81fb; - --color-indigo-800: #9195ff; - --color-indigo-900: #a7aaff; - --color-indigo-1000: #bcbeff; - --color-indigo-1100: #d0d2ff; - --color-indigo-1200: #e2e4ff; - --color-indigo-1300: #f3f3fe; - - --color-magenta-100: #76003a; - --color-magenta-200: #890042; - --color-magenta-300: #a0004d; - --color-magenta-400: #b6125a; - --color-magenta-500: #cb266d; - --color-magenta-600: #de3d82; - --color-magenta-700: #ed5795; - --color-magenta-800: #f972a7; - --color-magenta-900: #ff8fb9; - --color-magenta-1000: #ffacca; - --color-magenta-1100: #ffc6da; - --color-magenta-1200: #ffdde9; - --color-magenta-1300: #fff0f5; - - --color-purple-100: #4c0d9d; - --color-purple-200: #5911b1; - --color-purple-300: #691cc8; - --color-purple-400: #7a2dda; - --color-purple-500: #8c41e9; - --color-purple-600: #9d57f3; - --color-purple-700: #ac6ff9; - --color-purple-800: #bb87fb; - --color-purple-900: #ca9ffc; - --color-purple-1000: #d7b6fe; - --color-purple-1100: #e4ccfe; - --color-purple-1200: #efdfff; - --color-purple-1300: #f9f0ff; - - --color-seafoam-100: #12413f; - --color-seafoam-200: #0e4c49; - --color-seafoam-300: #045a57; - --color-seafoam-400: #006965; - --color-seafoam-500: #007a75; - --color-seafoam-600: #008c87; - --color-seafoam-700: #009e98; - --color-seafoam-800: #03b2ab; - --color-seafoam-900: #36c5bd; - --color-seafoam-1000: #5dd6cf; - --color-seafoam-1100: #84e6df; - --color-seafoam-1200: #b0f2ec; - --color-seafoam-1300: #dff9f6; - - --color-yellow-100: #4c3600; - --color-yellow-200: #584000; - --color-yellow-300: #674c00; - --color-yellow-400: #775900; - --color-yellow-500: #886800; - --color-yellow-600: #9b7800; - --color-yellow-700: #ae8900; - --color-yellow-800: #c09c00; - --color-yellow-900: #d3ae00; - --color-yellow-1000: #e4c200; - --color-yellow-1100: #f4d500; - --color-yellow-1200: #f9e85c; - --color-yellow-1300: #fcf6bb; - } -} - -@media (prefers-color-scheme: light) { - :root { - --color-white: #ffffff; - - --color-gray-50: #fdfdfd; - --color-gray-100: #f8f8f8; - --color-gray-200: #e6e6e6; - --color-gray-300: #d5d5d5; - --color-gray-400: #b1b1b1; - --color-gray-500: #909090; - --color-gray-600: #6d6d6d; - --color-gray-700: #464646; - --color-gray-800: #222222; - --color-gray-900: #151515; - - --color-black: #000000; - - --color-blue-100: #e0f2ff; - --color-blue-200: #cae8ff; - --color-blue-300: #b5deff; - --color-blue-400: #96cefd; - --color-blue-500: #78bbfa; - --color-blue-600: #59a7f6; - --color-blue-700: #3892f3; - --color-blue-800: #147af3; - --color-blue-900: #0265dc; - --color-blue-1000: #0054b6; - --color-blue-1100: #004491; - --color-blue-1200: #003571; - --color-blue-1300: #002754; - - --color-green-100: #cef8e0; - --color-green-200: #adf4ce; - --color-green-300: #89ecbc; - --color-green-400: #67dea8; - --color-green-500: #49cc93; - --color-green-600: #2fb880; - --color-green-700: #15a46e; - --color-green-800: #008f5d; - --color-green-900: #007a4d; - --color-green-1000: #00653e; - --color-green-1100: #005132; - --color-green-1200: #053f27; - --color-green-1300: #0a2e1d; - - --color-orange-100: #ffeccc; - --color-orange-200: #ffdfad; - --color-orange-300: #fdd291; - --color-orange-400: #ffbb63; - --color-orange-500: #ffa037; - --color-orange-600: #f68511; - --color-orange-700: #e46f00; - --color-orange-800: #cb5d00; - --color-orange-900: #b14c00; - --color-orange-1000: #953d00; - --color-orange-1100: #7a2f00; - --color-orange-1200: #612300; - --color-orange-1300: #491901; - - --color-red-100: #ffebe7; - --color-red-200: #ffddd6; - --color-red-300: #ffcdc3; - --color-red-400: #ffb7a9; - --color-red-500: #ff9b88; - --color-red-600: #ff7c65; - --color-red-700: #f75c46; - --color-red-800: #ea3829; - --color-red-900: #d31510; - --color-red-1000: #b40000; - --color-red-1100: #930000; - --color-red-1200: #740000; - --color-red-1300: #590000; - - --color-celery-100: #cdfcbf; - --color-celery-200: #aef69d; - --color-celery-300: #96ee85; - --color-celery-400: #72e06a; - --color-celery-500: #4ecf50; - --color-celery-600: #27bb36; - --color-celery-700: #07a721; - --color-celery-800: #009112; - --color-celery-900: #007c0f; - --color-celery-1000: #00670f; - --color-celery-1100: #00530d; - --color-celery-1200: #00400a; - --color-celery-1300: #003007; - - --color-chartreuse-100: #dbfc6e; - --color-chartreuse-200: #cbf443; - --color-chartreuse-300: #bce92a; - --color-chartreuse-400: #aad816; - --color-chartreuse-500: #98c50a; - --color-chartreuse-600: #87b103; - --color-chartreuse-700: #769c00; - --color-chartreuse-800: #678800; - --color-chartreuse-900: #577400; - --color-chartreuse-1000: #486000; - --color-chartreuse-1100: #3a4d00; - --color-chartreuse-1200: #2c3b00; - --color-chartreuse-1300: #212c00; - - --color-cyan-100: #c5f8ff; - --color-cyan-200: #a4f0ff; - --color-cyan-300: #88e7fa; - --color-cyan-400: #60d8f3; - --color-cyan-500: #33c5e8; - --color-cyan-600: #12b0da; - --color-cyan-700: #019cc8; - --color-cyan-800: #0086b4; - --color-cyan-900: #00719f; - --color-cyan-1000: #005d89; - --color-cyan-1100: #004a73; - --color-cyan-1200: #00395d; - --color-cyan-1300: #002a46; - - --color-fuchsia-100: #ffe9fc; - --color-fuchsia-200: #ffdafa; - --color-fuchsia-300: #fec7f8; - --color-fuchsia-400: #fbaef6; - --color-fuchsia-500: #f592f3; - --color-fuchsia-600: #ed74ed; - --color-fuchsia-700: #e055e2; - --color-fuchsia-800: #cd3ace; - --color-fuchsia-900: #b622b7; - --color-fuchsia-1000: #9d039e; - --color-fuchsia-1100: #800081; - --color-fuchsia-1200: #640664; - --color-fuchsia-1300: #470e46; - - --color-indigo-100: #edeeff; - --color-indigo-200: #e0e2ff; - --color-indigo-300: #d3d5ff; - --color-indigo-400: #c1c4ff; - --color-indigo-500: #acafff; - --color-indigo-600: #9599ff; - --color-indigo-700: #7e84fc; - --color-indigo-800: #686df4; - --color-indigo-900: #5258e4; - --color-indigo-1000: #4046ca; - --color-indigo-1100: #3236a8; - --color-indigo-1200: #262986; - --color-indigo-1300: #1b1e64; - - --color-magenta-100: #ffeaf1; - --color-magenta-200: #ffdce8; - --color-magenta-300: #ffcadd; - --color-magenta-400: #ffb2ce; - --color-magenta-500: #ff95bd; - --color-magenta-600: #fa77aa; - --color-magenta-700: #ef5a98; - --color-magenta-800: #de3d82; - --color-magenta-900: #c82269; - --color-magenta-1000: #ad0955; - --color-magenta-1100: #8e0045; - --color-magenta-1200: #700037; - --color-magenta-1300: #54032a; - - --color-purple-100: #f6ebff; - --color-purple-200: #eeddff; - --color-purple-300: #e6d0ff; - --color-purple-400: #dbbbfe; - --color-purple-500: #cca4fd; - --color-purple-600: #bd8bfc; - --color-purple-700: #ae72f9; - --color-purple-800: #9d57f4; - --color-purple-900: #893de7; - --color-purple-1000: #7326d3; - --color-purple-1100: #5d13b7; - --color-purple-1200: #470c94; - --color-purple-1300: #33106a; - - --color-seafoam-100: #cef7f3; - --color-seafoam-200: #aaf1ea; - --color-seafoam-300: #8ce9e2; - --color-seafoam-400: #65dad2; - --color-seafoam-500: #3fc9c1; - --color-seafoam-600: #0fb5ae; - --color-seafoam-700: #00a19a; - --color-seafoam-800: #008c87; - --color-seafoam-900: #007772; - --color-seafoam-1000: #00635f; - --color-seafoam-1100: #0c4f4c; - --color-seafoam-1200: #123c3a; - --color-seafoam-1300: #122c2b; - - --color-yellow-100: #fbf198; - --color-yellow-200: #f8e750; - --color-yellow-300: #f8d904; - --color-yellow-400: #e8c600; - --color-yellow-500: #d7b300; - --color-yellow-600: #c49f00; - --color-yellow-700: #b08c00; - --color-yellow-800: #9b7800; - --color-yellow-900: #856600; - --color-yellow-1000: #705300; - --color-yellow-1100: #5b4300; - --color-yellow-1200: #483300; - --color-yellow-1300: #362500; - } -} diff --git a/src/apps/update/styles/global.css b/src/apps/update/styles/global.css deleted file mode 100644 index 714e5380..00000000 --- a/src/apps/update/styles/global.css +++ /dev/null @@ -1,82 +0,0 @@ -body { - overflow: hidden; - cursor: default; - background: transparent; - - :not(input):not(textarea), - :not(input):not(textarea)::after, - :not(input):not(textarea)::before { - -webkit-user-select: none; - user-select: none; - } -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background-color: transparent; -} - -::-webkit-scrollbar-thumb { - background-color: var(--color-gray-500); - border-radius: 6px; -} - -::-webkit-scrollbar-thumb:hover { - background-color: var(--color-gray-600); -} - -#root { - height: 100vh; - width: 100vw; - border-radius: 20px; - background-color: var(--color-gray-100); - padding: 30px; - display: flex; - flex-direction: column; - gap: 14px; - overflow: hidden; - - .title { - font-size: 20px; - font-weight: bold; - - .package { - color: var(--color-blue-900); - } - } - - .description { - position: relative; - overflow: auto; - flex: 1; - - b { - font-weight: bold; - } - - a { - color: var(--color-blue-900); - } - - .progress { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } - - .footer { - display: flex; - gap: 14px; - justify-content: flex-end; - - > button { - font-weight: bold; - } - } -} \ No newline at end of file diff --git a/src/apps/update/styles/reset.css b/src/apps/update/styles/reset.css deleted file mode 100644 index 00912b16..00000000 --- a/src/apps/update/styles/reset.css +++ /dev/null @@ -1,84 +0,0 @@ -:root { - font-size: 100%; - --main-typo: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --primary-color: var(--color-gray-900); - --secondary-color: var(--color-gray-50); -} - -*, *:after, *:before { - margin: 0; - padding: 0; - border: 0; - outline: none; - box-sizing: border-box; - vertical-align: baseline; -} - -img, image, picture, video, iframe, figure { - max-width: 100%; - width: 100%; - display: block; -} - -a { - display: block; -} - -p a { - display: inline; -} - -li { - list-style-type: none; -} - -html { - scroll-behavior: smooth; -} - -h1, h2, h3, h4, h5, h6, p, span, a, strong, blockquote, i, b, em, pre { - font-size: 1em; - font-weight: inherit; - font-style: inherit; - text-decoration: none; - color: inherit; -} - -form, input, textarea, select, button, label { - font-family: inherit; - font-size: inherit; - hyphens: auto; - background-color: transparent; - display: block; - color: inherit; -} - -table, tr, td { - border-collapse: collapse; - border-spacing: 0; -} - -svg { - width: 100%; - display: block; - fill: currentColor; -} - -body { - font-size: 1em; - line-height: 1.4em; - font-family: var(--main-typo); - color: var(--primary-color); - background-color: var(--secondary-color); - hyphens: auto; - font-smooth: always; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -hr { - border: 1px solid; - margin: 1em 0; - opacity: 0.8; - color: var(--color-gray-200); -} \ No newline at end of file diff --git a/src/apps/update/update.tsx b/src/apps/update/update.tsx deleted file mode 100644 index 42908595..00000000 --- a/src/apps/update/update.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import { Update } from '@tauri-apps/plugin-updater'; -import { Button, Progress } from 'antd'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -interface Props { - update: Update; -} - -export function UpdateModal({ update }: Props) { - const [total, setTotal] = useState(null); - const [current, setCurrent] = useState(0); - - const { t } = useTranslation(); - - const onDownload = async () => { - update.downloadAndInstall((progress) => { - if (progress.event === 'Started' && progress.data.contentLength) { - setTotal(progress.data.contentLength); - } - - if (progress.event === 'Progress') { - setCurrent((v) => v + progress.data.chunkLength * 1000); - } - - if (progress.event === 'Finished') { - setCurrent(total!); - } - }); - }; - - if (total != null) { - const percent = Math.floor((current / total) * 100); - return ( - <> -
    - {percent === 100 ? t('update.installing') : t('update.downloading')} -
    -
    - -
    - - ); - } - - return ( - <> -
    {t('update.title')}
    -
    -
    - {t('update.date')}: {update.date ? update.date.replace(/\s.*/, '') : '-'} -
    -
    - {t('update.version')}: {update.version} -
    -
    -

    - - {t('update.extra_info')}:{' '} - - {t('update.page')} - - -

    -
    -
    - - -
    - - ); -} diff --git a/src/background/error_handler.rs b/src/background/error_handler.rs index 2094bb0c..53cf4e5d 100644 --- a/src/background/error_handler.rs +++ b/src/background/error_handler.rs @@ -2,32 +2,42 @@ macro_rules! define_app_errors { ($( $variant:ident($error_type:ty); )*) => { - #[derive(Debug)] - pub enum AppError { - $( - $variant($error_type), - )* - } - $( impl From<$error_type> for AppError { fn from(err: $error_type) -> Self { - AppError::$variant(err) + let backtrace = backtrace::Backtrace::new(); + AppError { msg: format!("{}({:?})", stringify!($variant), err), backtrace } } } )* }; } +#[macro_export] +macro_rules! log_error { + ($($result:expr),*) => { + $( + if let Err(err) = $result { + log::error!("{:?}", err); + } + )* + }; +} + +pub struct AppError { + msg: String, + backtrace: backtrace::Backtrace, +} + define_app_errors!( - Seelen(String); + Custom(String); Io(std::io::Error); Tauri(tauri::Error); TauriShell(tauri_plugin_shell::Error); - Eyre(color_eyre::eyre::Error); Windows(windows::core::Error); SerdeJson(serde_json::Error); SerdeYaml(serde_yaml::Error); + SerdeXml(quick_xml::de::DeError); Utf8(std::string::FromUtf8Error); Utf16(std::string::FromUtf16Error); CrossbeamRecv(crossbeam_channel::RecvError); @@ -39,11 +49,56 @@ define_app_errors!( Base64Decode(base64::DecodeError); WideStringNull(widestring::error::MissingNulTerminator); Reqwest(tauri_plugin_http::reqwest::Error); + WinScreenshot(win_screenshot::capture::WSError); + EvalExpr(evalexpr::EvalexprError); ); -impl From<&str> for AppError { - fn from(err: &str) -> Self { - AppError::Seelen(err.to_owned()) +impl std::fmt::Debug for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.msg)?; + + let frames = self.backtrace.frames(); + if !frames.is_empty() { + writeln!(f)?; + } + + let mut index = 0; + for frame in frames { + for symbol in frame.symbols() { + let name = match symbol.name() { + Some(name) => name.to_string(), + None => continue, + }; + + // skip backtrace traces + if name.starts_with("backtrace") { + continue; + } + + // 2) skip trace of other modules/libraries specially tracing of tao and tauri libs + if !name.starts_with("seelen_ui") { + index += 1; + continue; + } + + writeln!(f, " {}: {}", index, name)?; + if let Some(file) = symbol.filename() { + write!(f, " at: \"{}", file.to_string_lossy())?; + if let Some(line) = symbol.lineno() { + write!(f, ":{}", line)?; + if let Some(col) = symbol.colno() { + write!(f, ":{}", col)?; + } + } + writeln!(f, "\"")?; + } else { + writeln!(f, " at: ")? + } + + index += 1; + } + } + Ok(()) } } @@ -53,6 +108,12 @@ impl std::fmt::Display for AppError { } } +impl From<&str> for AppError { + fn from(err: &str) -> Self { + err.to_owned().into() + } +} + // needed to tauri::command macro (exposed functions to frontend) impl From for tauri::ipc::InvokeError { fn from(val: AppError) -> Self { @@ -60,12 +121,6 @@ impl From for tauri::ipc::InvokeError { } } -impl From for String { - fn from(err: AppError) -> String { - format!("{}", err) - } -} - impl From for AppError { fn from(output: tauri_plugin_shell::process::Output) -> Self { if !output.stderr.is_empty() { @@ -78,33 +133,4 @@ impl From for AppError { } } -impl std::error::Error for AppError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - AppError::Eyre(err) => Some(err.root_cause()), - AppError::Io(err) => Some(err), - AppError::Tauri(err) => Some(err), - AppError::Windows(err) => Some(err), - AppError::SerdeJson(err) => Some(err), - AppError::Utf8(err) => Some(err), - AppError::Utf16(err) => Some(err), - AppError::CrossbeamRecv(err) => Some(err), - AppError::TauriShell(err) => Some(err), - AppError::TryFromInt(err) => Some(err), - _ => None, - } - } -} - -pub type Result = core::result::Result; - -#[macro_export] -macro_rules! log_error { - ($($result:expr),*) => { - $( - if let Err(err) = $result { - log::error!("{:?}", err); - } - )* - }; -} +pub type Result = core::result::Result; diff --git a/src/background/exposed.rs b/src/background/exposed.rs index df361174..7b7746bf 100644 --- a/src/background/exposed.rs +++ b/src/background/exposed.rs @@ -1,188 +1,211 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::process::Command; - -use tauri::{Builder, Wry}; -use tauri_plugin_shell::ShellExt; - -use crate::error_handler::Result; -use crate::modules::input::Keyboard; -use crate::modules::virtual_desk::get_vd_manager; -use crate::seelen::{get_app_handle, Seelen, SEELEN}; -use crate::seelen_weg::handler::*; -use crate::seelen_weg::icon_extractor::extract_and_save_icon; -use crate::seelen_wm::handler::*; -use crate::state::infrastructure::*; -use crate::system::brightness::*; -use crate::utils::is_virtual_desktop_supported as virtual_desktop_supported; -use crate::{log_error, trace_lock}; - -use crate::modules::media::infrastructure::*; -use crate::modules::network::infrastructure::*; -use crate::modules::notifications::infrastructure::*; -use crate::modules::power::infrastructure::*; -use crate::modules::tray::infrastructure::*; - -#[tauri::command(async)] -fn select_file_on_explorer(path: String) { - log_error!(Command::new("explorer").args(["/select,", &path]).spawn()); -} - -#[tauri::command(async)] -fn open_file(path: String) { - log_error!(Command::new("explorer").args([&path]).spawn()); -} - -#[tauri::command(async)] -fn run_as_admin(path: String) { - tauri::async_runtime::spawn(async move { - let app = get_app_handle(); - log_error!( - app.shell() - .command("powershell") - .args(["-Command", &format!("Start-Process '{}' -Verb runAs", path)]) - .status() - .await - ); - }); -} - -#[tauri::command(async)] -fn run(program: String, args: Vec) { - tauri::async_runtime::spawn(async move { - log_error!( - get_app_handle() - .shell() - .command(program) - .args(args) - .status() - .await - ); - }); -} - -#[tauri::command(async)] -fn is_dev_mode() -> bool { - tauri::is_dev() -} - -#[tauri::command(async)] -pub fn get_user_envs() -> HashMap { - std::env::vars().collect::>() -} - -// https://docs.rs/tauri/latest/tauri/window/struct.WindowBuilder.html#known-issues -// https://github.com/tauri-apps/wry/issues/583 -#[tauri::command(async)] -fn show_app_settings() { - log_error!(Seelen::show_settings()); -} - -#[tauri::command(async)] -async fn set_auto_start(enabled: bool) -> Result<()> { - Seelen::set_auto_start(enabled).await -} - -#[tauri::command(async)] -async fn get_auto_start_status() -> Result { - Seelen::is_auto_start_enabled().await -} - -#[tauri::command(async)] -fn switch_workspace(idx: usize) -> Result<()> { - get_vd_manager().switch_to(idx) -} - -#[tauri::command(async)] -fn ensure_hitboxes_zorder() -> Result<()> { - let seelen = trace_lock!(SEELEN); - for monitor in seelen.monitors() { - if let Some(toolbar) = monitor.toolbar() { - toolbar.ensure_hitbox_zorder()?; - } - if let Some(weg) = monitor.weg() { - weg.ensure_hitbox_zorder()?; - } - } - Ok(()) -} - -#[tauri::command(async)] -fn send_keys(keys: String) -> Result<()> { - Keyboard::new().send_keys(&keys) -} - -#[tauri::command] -fn get_icon(path: String) -> Option { - extract_and_save_icon(&get_app_handle(), &path).ok() -} - -#[tauri::command(async)] -fn is_virtual_desktop_supported() -> bool { - virtual_desktop_supported() -} - -pub fn register_invoke_handler(app_builder: Builder) -> Builder { - app_builder.invoke_handler(tauri::generate_handler![ - // General - run, - is_dev_mode, - open_file, - run_as_admin, - select_file_on_explorer, - is_virtual_desktop_supported, - get_user_envs, - show_app_settings, - switch_workspace, - ensure_hitboxes_zorder, - send_keys, - get_icon, - // Seelen Settings - set_auto_start, - get_auto_start_status, - state_get_themes, - state_get_placeholders, - state_get_layouts, - state_get_weg_items, - state_get_settings, - state_get_specific_apps_configurations, - state_get_wallpaper, - state_set_wallpaper, - // Media - media_prev, - media_toggle_play_pause, - media_next, - set_volume_level, - media_toggle_mute, - media_set_default_device, - // Brightness - get_main_monitor_brightness, - set_main_monitor_brightness, - // Power - log_out, - suspend, - restart, - shutdown, - // SeelenWeg - weg_close_app, - weg_toggle_window_state, - weg_request_update_previews, - // Windows Manager - set_window_position, - bounce_handle, - request_focus, - // tray icons - temp_get_by_event_tray_info, - on_click_tray_icon, - on_context_menu_tray_icon, - // network - wlan_get_profiles, - wlan_start_scanning, - wlan_stop_scanning, - wlan_connect, - wlan_disconnect, - // notifications - notifications_close, - notifications_close_all, - ]) -} +use std::collections::HashMap; +use std::path::PathBuf; + +use tauri::{Builder, WebviewWindow, Wry}; +use tauri_plugin_shell::ShellExt; + +use crate::error_handler::Result; +use crate::hook::HookManager; +use crate::log_error; +use crate::modules::input::Keyboard; +use crate::modules::virtual_desk::get_vd_manager; +use crate::seelen::{get_app_handle, Seelen}; +use crate::seelen_rofi::handler::*; +use crate::seelen_weg::handler::*; +use crate::seelen_weg::icon_extractor::{ + extract_and_save_icon_from_file, extract_and_save_icon_umid, +}; +use crate::seelen_wm_v2::handler::*; +use crate::state::infrastructure::*; +use crate::system::brightness::*; +use crate::utils::is_virtual_desktop_supported as virtual_desktop_supported; +use crate::windows_api::WindowsApi; +use crate::winevent::{SyntheticFullscreenData, WinEvent}; + +use crate::modules::media::infrastructure::*; +use crate::modules::network::infrastructure::*; +use crate::modules::notifications::infrastructure::*; +use crate::modules::power::infrastructure::*; +use crate::modules::system_settings::infrastructure::*; +use crate::modules::tray::infrastructure::*; + +#[tauri::command(async)] +fn select_file_on_explorer(path: String) -> Result<()> { + get_app_handle() + .shell() + .command("explorer") + .args(["/select,", &path]) + .spawn()?; + Ok(()) +} + +#[tauri::command(async)] +fn open_file(path: String) -> Result<()> { + get_app_handle() + .shell() + .command("cmd") + .args(["/c", "explorer", &path]) + .spawn()?; + Ok(()) +} + +#[tauri::command(async)] +fn run_as_admin(path: String) { + tauri::async_runtime::spawn(async move { + let app = get_app_handle(); + log_error!( + app.shell() + .command("powershell") + .args(["-Command", &format!("Start-Process '{}' -Verb runAs", path)]) + .status() + .await + ); + }); +} + +#[tauri::command(async)] +fn run(program: String, args: Vec) { + tauri::async_runtime::spawn(async move { + log_error!( + get_app_handle() + .shell() + .command(program) + .args(args) + .status() + .await + ); + }); +} + +#[tauri::command(async)] +fn is_dev_mode() -> bool { + tauri::is_dev() +} + +#[tauri::command(async)] +pub fn get_user_envs() -> HashMap { + std::env::vars().collect::>() +} + +// https://docs.rs/tauri/latest/tauri/window/struct.WindowBuilder.html#known-issues +// https://github.com/tauri-apps/wry/issues/583 +#[tauri::command(async)] +fn show_app_settings() { + log_error!(Seelen::show_settings()); +} + +#[tauri::command(async)] +async fn set_auto_start(enabled: bool) -> Result<()> { + Seelen::set_auto_start(enabled).await +} + +#[tauri::command(async)] +async fn get_auto_start_status() -> Result { + Seelen::is_auto_start_enabled().await +} + +#[tauri::command(async)] +fn switch_workspace(idx: usize) -> Result<()> { + get_vd_manager().switch_to(idx) +} + +#[tauri::command(async)] +fn send_keys(keys: String) -> Result<()> { + Keyboard::new().send_keys(&keys) +} + +#[tauri::command(async)] +fn get_icon(path: String) -> Option { + if path.starts_with("shell:AppsFolder") { + let umid = path.replace("shell:AppsFolder\\", ""); + return extract_and_save_icon_umid(&umid).ok(); + } + extract_and_save_icon_from_file(&path).ok() +} + +#[tauri::command(async)] +fn is_virtual_desktop_supported() -> bool { + virtual_desktop_supported() +} + +#[tauri::command(async)] +fn simulate_fullscreen(webview: WebviewWindow, value: bool) -> Result<()> { + let handle = webview.hwnd()?; + let monitor = WindowsApi::monitor_from_window(handle); + let event = if value { + WinEvent::SyntheticFullscreenStart(SyntheticFullscreenData { handle, monitor }) + } else { + WinEvent::SyntheticFullscreenEnd(SyntheticFullscreenData { handle, monitor }) + }; + HookManager::emit_event(event, handle); + Ok(()) +} + +pub fn register_invoke_handler(app_builder: Builder) -> Builder { + app_builder.invoke_handler(tauri::generate_handler![ + // General + run, + is_dev_mode, + open_file, + run_as_admin, + select_file_on_explorer, + is_virtual_desktop_supported, + get_user_envs, + show_app_settings, + switch_workspace, + send_keys, + get_icon, + get_system_colors, + simulate_fullscreen, + // Seelen Settings + set_auto_start, + get_auto_start_status, + state_get_themes, + state_get_placeholders, + state_get_layouts, + state_get_weg_items, + state_get_settings, + state_get_specific_apps_configurations, + state_get_wallpaper, + state_set_wallpaper, + state_get_history, + // Media + media_prev, + media_toggle_play_pause, + media_next, + set_volume_level, + media_toggle_mute, + media_set_default_device, + // Brightness + get_main_monitor_brightness, + set_main_monitor_brightness, + // Power + log_out, + suspend, + restart, + shutdown, + // SeelenWeg + weg_close_app, + weg_toggle_window_state, + weg_request_update_previews, + weg_pin_item, + // Windows Manager + set_window_position, + request_focus, + // App Launcher + launcher_get_apps, + // tray icons + temp_get_by_event_tray_info, + on_click_tray_icon, + on_context_menu_tray_icon, + // network + wlan_get_profiles, + wlan_start_scanning, + wlan_stop_scanning, + wlan_connect, + wlan_disconnect, + // notifications + notifications_close, + notifications_close_all, + ]) +} diff --git a/src/background/hook.rs b/src/background/hook.rs index 6b06fc78..3d0dcba5 100644 --- a/src/background/hook.rs +++ b/src/background/hook.rs @@ -5,19 +5,20 @@ use std::{ atomic::{AtomicBool, AtomicIsize, Ordering}, Arc, }, + thread::JoinHandle, time::{Duration, Instant}, }; -use color_eyre::owo_colors::OwoColorize; use itertools::Itertools; use lazy_static::lazy_static; use parking_lot::Mutex; +use seelen_core::handlers::SeelenEvent; use serde::Serialize; use tauri::Emitter; use windows::Win32::{ Foundation::HWND, UI::{ - Accessibility::{SetWinEventHook, HWINEVENTHOOK}, + Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}, WindowsAndMessaging::{ DispatchMessageW, GetMessageW, TranslateMessage, EVENT_MAX, EVENT_MIN, MSG, }, @@ -33,20 +34,21 @@ use crate::{ }, seelen::{get_app_handle, Seelen, SEELEN}, seelen_weg::SeelenWeg, + seelen_wm_v2::instance::WindowManagerV2, state::{application::FULL_STATE, domain::AppExtraFlag}, trace_lock, - utils::{constants::IGNORE_FOCUS, spawn_named_thread}, + utils::spawn_named_thread, windows_api::{window::Window, WindowsApi}, winevent::WinEvent, }; lazy_static! { - pub static ref HOOK_MANAGER: Arc> = Arc::new(Mutex::new(HookManager::new())); - // Last active window omitting all the seelen apps - pub static ref LAST_ACTIVE_NOT_SEELEN: AtomicIsize = AtomicIsize::new(WindowsApi::get_foreground_window().0); + static ref HOOK_MANAGER: Arc> = Arc::new(Mutex::new(HookManager::new())); + // Last active window omitting all the seelen overlays + pub static ref LAST_ACTIVE_NOT_SEELEN: AtomicIsize = AtomicIsize::new(WindowsApi::get_foreground_window().0 as _); } -pub static WIN_EVENTS_ENABLED: AtomicBool = AtomicBool::new(false); +pub static LOG_WIN_EVENTS: AtomicBool = AtomicBool::new(false); pub struct HookManager { skip: HashMap>, @@ -61,28 +63,42 @@ pub struct FocusedApp { } impl HookManager { - pub fn new() -> Self { + fn new() -> Self { Self { skip: HashMap::new(), } } - pub fn skip(&mut self, event: WinEvent, hwnd: isize) { - self.skip.entry(hwnd).or_default().push(event) + pub fn run_with_async(f: F) -> JoinHandle + where + F: FnOnce(&mut HookManager) -> T, + F: Send + 'static, + T: Send + 'static, + { + std::thread::spawn(move || f(&mut *trace_lock!(HOOK_MANAGER))) } - pub fn should_skip(&self, event: WinEvent, hwnd: isize) -> bool { - if let Some(v) = self.skip.get(&hwnd) { + pub fn skip(&mut self, event: WinEvent, hwnd: HWND) { + self.skip.entry(hwnd.0 as _).or_default().push(event) + } + + fn should_skip(&self, event: WinEvent, hwnd: HWND) -> bool { + // skip foreground on invisible windows + if event == WinEvent::SystemForeground && !WindowsApi::is_window_visible(hwnd) { + return true; + } + if let Some(v) = self.skip.get(&(hwnd.0 as _)) { return v.contains(&event); } false } - pub fn skip_done(&mut self, event: WinEvent, hwnd: isize) { - if WIN_EVENTS_ENABLED.load(Ordering::Relaxed) { + fn skip_done(&mut self, event: WinEvent, hwnd: HWND) { + if LOG_WIN_EVENTS.load(Ordering::Relaxed) { log::debug!("Skipping WinEvent::{:?}", event); } + let hwnd = hwnd.0 as isize; if let Some(v) = self.skip.get_mut(&hwnd) { if let Some(pos) = v.iter().position(|e| e == &event) { v.remove(pos); @@ -94,13 +110,24 @@ impl HookManager { } fn log_event(event: WinEvent, origin: HWND) { - if !WIN_EVENTS_ENABLED.load(Ordering::Relaxed) || event == WinEvent::ObjectLocationChange { + if !LOG_WIN_EVENTS.load(Ordering::Relaxed) || event == WinEvent::ObjectLocationChange { return; } + let event_value = { + #[cfg(dev)] + { + use owo_colors::OwoColorize; + event.green() + } + #[cfg(not(dev))] + { + &event + } + }; log::debug!( - "{:?}({}) || {} || {} || {}", - event.green(), + "{:?}({:?}) || {} || {} || {}", + event_value, origin.0, WindowsApi::exe(origin).unwrap_or_default(), WindowsApi::get_class(origin).unwrap_or_default(), @@ -108,30 +135,26 @@ impl HookManager { ); } - pub fn event(&mut self, event: WinEvent, origin: HWND, seelen: &mut Seelen) { + fn _event(&mut self, event: WinEvent, origin: HWND, seelen: &mut Seelen) { Self::log_event(event, origin); - if self.should_skip(event, origin.0) { - self.skip_done(event, origin.0); + if self.should_skip(event, origin) { + self.skip_done(event, origin); return; } let window = Window::from(origin); if event == WinEvent::SystemForeground && !window.is_seelen_overlay() { - LAST_ACTIVE_NOT_SEELEN.store(origin.0, Ordering::Relaxed); + LAST_ACTIVE_NOT_SEELEN.store(origin.0 as _, Ordering::Relaxed); } if event == WinEvent::ObjectFocus || event == WinEvent::SystemForeground { let title = window.title(); - if IGNORE_FOCUS.contains(&title) { - log::trace!("Skipping WinEvent::{:?}", event); - return; - } log_error!(get_app_handle().emit( - "global-focus-changed", + SeelenEvent::GlobalFocusChanged, FocusedApp { title, - hwnd: origin.0, + hwnd: origin.0 as _, name: window .app_display_name() .unwrap_or(String::from("Error on App Name")), @@ -140,38 +163,76 @@ impl HookManager { )); } - std::thread::spawn(move || { - if let VirtualDesktopManager::Seelen(vd) = get_vd_manager().as_ref() { - log_error!(vd.on_win_event(event, origin)); + let log_error_event = move |name: &str, result: Result<()>| { + if let Err(err) = result { + log::error!( + "{} => Event: {:?} Error: {:?} Window: {:?}", + name, + event, + err, + window + ); } - }); + }; - if seelen.state().is_weg_enabled() { - log_error!(SeelenWeg::process_global_win_event(event, origin)); + if let VirtualDesktopManager::Seelen(vd) = get_vd_manager().as_ref() { + log_error_event("Virtual Desk", vd.on_win_event(event, &window)); + } + + let app_state = seelen.state(); + if app_state.is_weg_enabled() { + std::thread::spawn(move || { + log_error_event( + "Weg Global", + SeelenWeg::process_global_win_event(event, &window), + ); + }); + } + + if app_state.is_window_manager_enabled() { + std::thread::spawn(move || { + log_error_event( + "WM Global", + WindowManagerV2::process_win_event(event, &window), + ); + }); + } + + if let Some(wall) = seelen.wall_mut() { + log_error_event("Wall Instance", wall.process_win_event(event, &window)); } for monitor in seelen.monitors_mut() { if let Some(toolbar) = monitor.toolbar_mut() { - log_error!(toolbar.process_win_event(event, origin)); + log_error_event("Toolbar Instance", toolbar.process_win_event(event, origin)); } if let Some(weg) = monitor.weg_mut() { - log_error!(weg.process_individual_win_event(event, origin)); + log_error_event( + "Weg Instance", + weg.process_individual_win_event(event, origin), + ); } + } + } + + pub fn emit_event(event: WinEvent, origin: HWND) { + // Follows lock order: CLI -> DATA -> EVENT to avoid deadlocks + let mut seelen = trace_lock!(SEELEN); + let mut hook_manager = trace_lock!(HOOK_MANAGER); + hook_manager._event(event, origin, &mut seelen); - if let Some(wm) = monitor.wm_mut() { - log_error!(wm.process_win_event(event, origin)); + if let Ok(synthetics) = event.get_synthetics(origin) { + for synthetic_event in synthetics { + hook_manager._event(synthetic_event, origin, &mut seelen) } } } } pub fn process_vd_event(event: VirtualDesktopEvent) -> Result<()> { - let mut seelen = trace_lock!(SEELEN); - for monitor in seelen.monitors_mut() { - if let Some(wm) = monitor.wm_mut() { - log_error!(wm.process_vd_event(&event)); - } + if FULL_STATE.load().is_window_manager_enabled() { + log_error!(WindowManagerV2::process_vd_event(&event)); } match event { @@ -191,14 +252,14 @@ pub fn process_vd_event(event: VirtualDesktopEvent) -> Result<()> { .iter() .map(|d| d.as_serializable()) .collect_vec(); - seelen.handle().emit("workspaces-changed", &desktops)?; + get_app_handle().emit(SeelenEvent::WorkspacesChanged, &desktops)?; } VirtualDesktopEvent::DesktopChanged { new, old: _ } => { - seelen.handle().emit("active-workspace-changed", new.id())?; + get_app_handle().emit(SeelenEvent::ActiveWorkspaceChanged, new.id())?; } VirtualDesktopEvent::WindowChanged(window) => { - let hwnd = HWND(window); + let hwnd = HWND(window as _); if WindowsApi::is_window(hwnd) { if let Some(config) = FULL_STATE.load().get_app_config_by_window(hwnd) { let vd = get_vd_manager(); @@ -225,9 +286,9 @@ pub fn location_delay_completed(origin: HWND) -> bool { let last = LAST_LOCATION_CHANGED.load(Ordering::Acquire); let mut dict = trace_lock!(DICT); - let should_continue = match dict.entry(origin.0) { + let should_continue = match dict.entry(origin.0 as _) { std::collections::hash_map::Entry::Occupied(mut entry) => { - if last != origin.0 || entry.get().elapsed() > Duration::from_millis(50) { + if last != origin.0 as isize || entry.get().elapsed() > Duration::from_millis(50) { entry.insert(Instant::now()); true } else { @@ -241,65 +302,58 @@ pub fn location_delay_completed(origin: HWND) -> bool { }; if should_continue { - LAST_LOCATION_CHANGED.store(origin.0, Ordering::Release); + LAST_LOCATION_CHANGED.store(origin.0 as _, Ordering::Release); } should_continue } pub extern "system" fn win_event_hook( - _h_win_event_hook: HWINEVENTHOOK, + hook_handle: HWINEVENTHOOK, event: u32, - hwnd: HWND, + origin: HWND, id_object: i32, _id_child: i32, _id_event_thread: u32, _dwms_event_time: u32, ) { + let hook_was_invalidated = hook_handle.is_invalid(); + if !Seelen::is_running() { + if !hook_was_invalidated { + log::trace!("Exiting WinEventHook"); + let _ = unsafe { UnhookWinEvent(hook_handle) }; + } + return; + } + if id_object != 0 { return; } if FULL_STATE.load().is_weg_enabled() { // raw events should be only used for a fastest and immediately processing - log_error!(SeelenWeg::process_raw_win_event(event, hwnd)); + log_error!(SeelenWeg::process_raw_win_event(event, origin)); } - let event = match WinEvent::try_from(event) { - Ok(event) => event, - Err(_) => return, - }; - - if event == WinEvent::ObjectLocationChange && !location_delay_completed(hwnd) { + let event = WinEvent::from(event); + if event == WinEvent::ObjectLocationChange && !location_delay_completed(origin) { return; } - - // Follows lock order: CLI -> DATA -> EVENT to avoid deadlocks - let mut seelen = trace_lock!(SEELEN); - let mut hook_manager = trace_lock!(HOOK_MANAGER); - hook_manager.event(event, hwnd, &mut seelen); - - if let Some(synthetic_event) = event.get_synthetic(hwnd) { - hook_manager.event(synthetic_event, hwnd, &mut seelen); - } + HookManager::emit_event(event, origin) } pub fn register_win_hook() -> Result<()> { log::trace!("Registering Windows and Virtual Desktop Hooks"); - // let stack_size = 5 * 1024 * 1024; // 5 MB spawn_named_thread("WinEventHook", move || unsafe { SetWinEventHook(EVENT_MIN, EVENT_MAX, None, Some(win_event_hook), 0, 0, 0); - let mut msg: MSG = MSG::default(); loop { - if !GetMessageW(&mut msg, HWND(0), 0, 0).as_bool() { - log::info!("windows event processing shutdown"); + if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() { break; }; let _ = TranslateMessage(&msg); DispatchMessageW(&msg); - std::thread::sleep(Duration::from_millis(10)); } })?; @@ -317,7 +371,7 @@ pub fn register_win_hook() -> Result<()> { loop { if let Ok(pos) = Mouse::get_cursor_pos() { if last_pos != pos { - let _ = handle.emit("global-mouse-move", &[pos.get_x(), pos.get_y()]); + let _ = handle.emit(SeelenEvent::GlobalMouseMove, &[pos.get_x(), pos.get_y()]); last_pos = pos; } } diff --git a/src/background/instance.rs b/src/background/instance.rs new file mode 100644 index 00000000..1cd83999 --- /dev/null +++ b/src/background/instance.rs @@ -0,0 +1,111 @@ +use getset::{Getters, MutGetters}; + +use crate::{ + error_handler::Result, + log_error, + seelen_bar::FancyToolbar, + seelen_weg::SeelenWeg, + seelen_wm_v2::instance::WindowManagerV2, + state::application::FullState, + windows_api::{monitor::Monitor, WindowsApi}, +}; + +use windows::Win32::Graphics::Gdi::HMONITOR; + +#[derive(Getters, MutGetters)] +#[getset(get = "pub", get_mut = "pub")] +pub struct SeelenInstanceContainer { + handle: HMONITOR, + monitor: Monitor, + name: String, + toolbar: Option, + weg: Option, + wm: Option, +} + +unsafe impl Send for SeelenInstanceContainer {} + +impl SeelenInstanceContainer { + pub fn new(hmonitor: HMONITOR, settings: &FullState) -> Result { + if hmonitor.is_invalid() { + return Err("Invalid Monitor".into()); + } + let mut instance = Self { + handle: hmonitor, + monitor: Monitor::from(hmonitor), + name: WindowsApi::monitor_name(hmonitor)?, + toolbar: None, + weg: None, + wm: None, + }; + instance.load_settings(settings)?; + instance.ensure_positions()?; + Ok(instance) + } + + pub fn update_handle(&mut self, id: HMONITOR) { + self.handle = id; + self.monitor = Monitor::from(id); + log_error!(self.ensure_positions()); + } + + pub fn ensure_positions(&mut self) -> Result<()> { + if let Some(bar) = &mut self.toolbar { + bar.set_position(self.handle)?; + } + if let Some(weg) = &mut self.weg { + weg.set_position(self.handle)?; + } + if let Some(wm) = &mut self.wm { + wm.set_position(self.handle)?; + } + Ok(()) + } + + fn add_toolbar(&mut self) -> Result<()> { + if self.toolbar.is_none() { + self.toolbar = Some(FancyToolbar::new(&self.name)?); + } + Ok(()) + } + + fn add_weg(&mut self) -> Result<()> { + if self.weg.is_none() { + self.weg = Some(SeelenWeg::new(&self.name)?); + } + Ok(()) + } + + fn add_wm(&mut self) -> Result<()> { + if self.wm.is_none() { + self.wm = Some(WindowManagerV2::new(&self.name)?) + } + Ok(()) + } + + pub fn load_settings(&mut self, settings: &FullState) -> Result<()> { + if settings.is_bar_enabled_on_monitor(self.monitor.index()?) { + self.add_toolbar()?; + } else { + self.toolbar = None; + } + + if settings.is_weg_enabled_on_monitor(self.monitor.index()?) { + self.add_weg()?; + } else { + self.weg = None; + } + + if settings.is_window_manager_enabled() { + self.add_wm()?; + } else { + self.wm = None; + } + Ok(()) + } + + pub fn is_focused(&self) -> bool { + let hwnd = WindowsApi::get_foreground_window(); + self.handle == WindowsApi::monitor_from_window(hwnd) + } +} diff --git a/src/background/main.rs b/src/background/main.rs index e29d69a2..76226ff5 100644 --- a/src/background/main.rs +++ b/src/background/main.rs @@ -1,170 +1,229 @@ -// Prevents additional console window on Windows in release -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -mod error_handler; -mod exposed; -mod hook; -mod modules; -mod monitor; -mod plugins; -mod seelen; -mod seelen_bar; -mod seelen_weg; -mod seelen_wm; -mod state; -mod system; -mod tray; -mod utils; -mod windows_api; -mod winevent; - -use std::io::{BufWriter, Write}; - -use color_eyre::owo_colors::OwoColorize; -use error_handler::Result; -use exposed::register_invoke_handler; -use itertools::Itertools; -use modules::{ - cli::{ - application::{attach_console, is_just_getting_info, SEELEN_COMMAND_LINE}, - Client, - }, - tray::application::ensure_tray_overflow_creation, -}; -use plugins::register_plugins; -use seelen::{Seelen, SEELEN}; -use seelen_core::state::Settings; -use tray::try_register_tray_icon; -use utils::PERFORMANCE_HELPER; -use windows::Win32::Security::{SE_DEBUG_NAME, SE_SHUTDOWN_NAME}; -use windows_api::WindowsApi; - -fn register_panic_hook() { - std::panic::set_hook(Box::new(|info| { - let cause = info - .payload() - .downcast_ref::() - .map(|s| s.to_string()) - .unwrap_or_else(|| { - info.payload() - .downcast_ref::<&str>() - .unwrap_or(&"") - .to_string() - }); - - let mut string_location = String::from(""); - if let Some(location) = info.location() { - string_location = format!( - "{}:{}:{}", - location.file(), - location.line(), - location.column() - ); - } - - log::error!( - "A panic occurred:\n Cause: {}\n Location: {}", - cause.cyan(), - string_location.purple() - ); - })); -} - -fn setup(app: &mut tauri::App) -> Result<(), Box> { - let version = env!("CARGO_PKG_VERSION"); - log::info!("───────────────────── Starting Seelen UI v{version} ─────────────────────"); - log::info!("Operating System: {}", os_info::get()); - log::info!("Locate: {:?}", Settings::get_locale()); - log::info!("Elevated: {:?}", WindowsApi::is_elevated()); - Client::listen_tcp()?; - - log_error!(WindowsApi::enable_privilege(SE_SHUTDOWN_NAME)); - log_error!(WindowsApi::enable_privilege(SE_DEBUG_NAME)); - - // try it at start it on open the program to avoid do it before - log_error!(ensure_tray_overflow_creation()); - - let mut seelen = unsafe { SEELEN.make_guard_unchecked() }; - seelen.init(app.handle().clone())?; - - if !tauri::is_dev() { - let command = trace_lock!(SEELEN_COMMAND_LINE).clone(); - let matches = command.get_matches(); - if !matches.get_flag("silent") { - Seelen::show_settings()?; - } - Seelen::show_update_modal()?; - } - - seelen.start()?; - - log_error!(try_register_tray_icon(app)); - std::mem::forget(seelen); - Ok(()) -} - -fn app_callback(_: &tauri::AppHandle, event: tauri::RunEvent) { - match event { - tauri::RunEvent::ExitRequested { api, code, .. } => { - // prevent close background on webview windows closing - if code.is_none() { - api.prevent_exit(); - } - } - tauri::RunEvent::Exit => { - log::info!("───────────────────── Exiting Seelen ─────────────────────"); - trace_lock!(SEELEN).stop() - } - _ => {} - } -} - -fn main() -> Result<()> { - color_eyre::install().expect("Failed to install color_eyre"); - register_panic_hook(); - trace_lock!(PERFORMANCE_HELPER).start("init"); - - let command = trace_lock!(SEELEN_COMMAND_LINE).clone(); - let matches = match command.try_get_matches() { - Ok(m) => m, - // (help, --help or -h) is also managed as error - Err(e) => { - attach_console()?; - e.print()?; - return Ok(()); - } - }; - - if is_just_getting_info(&matches)? { - return Ok(()); - } - - let mut sys = sysinfo::System::new(); - sys.refresh_processes(); - let already_running = sys.processes_by_name("seelen-ui.exe").collect_vec().len() > 1; - - if already_running { - if let Ok(stream) = Client::connect_tcp() { - let mut writer = BufWriter::new(stream); - - let args = std::env::args().collect_vec(); - let msg = serde_json::to_string(&args).expect("could not serialize"); - - writer.write_all(msg.as_bytes()).expect("could not write"); - writer.flush().expect("could not flush"); - } - return Ok(()); - } - - let mut app_builder = tauri::Builder::default(); - app_builder = register_plugins(app_builder); - app_builder = register_invoke_handler(app_builder); - - let app = app_builder - .setup(setup) - .build(tauri::generate_context!()) - .expect("error while building tauri application"); - - app.run(app_callback); - Ok(()) -} +// Prevents additional console window on Windows in release +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod error_handler; +mod exposed; +mod hook; +mod instance; +mod modules; +mod plugins; +mod seelen; +mod seelen_bar; +mod seelen_rofi; +mod seelen_wall; +mod seelen_weg; +mod seelen_wm_v2; +mod state; +mod system; +mod tray; +mod utils; +mod windows_api; +mod winevent; + +use std::io::{BufWriter, Write}; + +use error_handler::Result; +use exposed::register_invoke_handler; +use itertools::Itertools; +use modules::{ + cli::{ + application::{attach_console, is_just_getting_info, SEELEN_COMMAND_LINE}, + Client, + }, + tray::application::ensure_tray_overflow_creation, +}; +use plugins::register_plugins; +use seelen::{Seelen, SEELEN}; +use seelen_core::state::Settings; +use tauri::webview_version; +use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; +use tauri_plugin_shell::ShellExt; +use tray::try_register_tray_icon; +use utils::PERFORMANCE_HELPER; +use windows::Win32::Security::{SE_DEBUG_NAME, SE_SHUTDOWN_NAME}; +use windows_api::WindowsApi; + +fn register_panic_hook() -> Result<()> { + std::panic::set_hook(Box::new(move |info| { + let cause = info + .payload() + .downcast_ref::() + .map(|s| s.to_string()) + .unwrap_or_else(|| { + info.payload() + .downcast_ref::<&str>() + .unwrap_or(&"") + .to_string() + }); + + let mut string_location = String::from(""); + if let Some(location) = info.location() { + string_location = format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ); + } + + log::error!( + "A panic occurred:\n Cause: {}\n Location: {}", + cause, + string_location + ); + })); + Ok(()) +} + +fn print_initial_information() { + let version = env!("CARGO_PKG_VERSION"); + log::info!("───────────────────── Starting Seelen UI v{version} ─────────────────────"); + log::info!("Operating System: {}", os_info::get()); + log::info!("WebView2 Runtime: {:?}", webview_version()); + log::info!("Elevated : {:?}", WindowsApi::is_elevated()); + log::info!("Locate : {:?}", Settings::get_locale()); +} + +fn validate_webview_runtime_is_installed(app: &tauri::AppHandle) -> Result<()> { + match webview_version() { + Ok(_version) => Ok(()), + Err(_) => { + let ok_pressed = app + .dialog() + .message("Seelen UI requires Webview2 Runtime. Please install it.") + .title("WebView2 Runtime not found") + .kind(MessageDialogKind::Error) + .ok_button_label("Go to download page") + .blocking_show(); + if ok_pressed { + let url = "https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH#download"; + app.shell().open(url, None)?; + } + Err("Webview2 Runtime not found".into()) + } + } +} + +fn setup(app: &mut tauri::App) -> Result<()> { + print_initial_information(); + Client::listen_tcp()?; + + validate_webview_runtime_is_installed(app.handle())?; + + let mut seelen = trace_lock!(SEELEN); + seelen.init(app.handle())?; + + log_error!(WindowsApi::enable_privilege(SE_SHUTDOWN_NAME)); + log_error!(WindowsApi::enable_privilege(SE_DEBUG_NAME)); + + // try it at start it on open the program to avoid do it before + log_error!(ensure_tray_overflow_creation()); + + if !tauri::is_dev() { + let command = trace_lock!(SEELEN_COMMAND_LINE).clone(); + if !command.get_matches().get_flag("silent") { + Seelen::show_settings()?; + } + } + + seelen.start()?; + log_error!(try_register_tray_icon(app)); + trace_lock!(PERFORMANCE_HELPER).end("setup"); + Ok(()) +} + +fn app_callback(_: &tauri::AppHandle, event: tauri::RunEvent) { + match event { + tauri::RunEvent::ExitRequested { api, code, .. } => { + // prevent close background on webview windows closing + if code.is_none() { + api.prevent_exit(); + } + } + tauri::RunEvent::Exit => { + log::info!("───────────────────── Exiting Seelen UI ─────────────────────"); + if Seelen::is_running() { + trace_lock!(SEELEN).stop(); + } + } + _ => {} + } +} + +fn is_already_runnning() -> bool { + let mut sys = sysinfo::System::new(); + sys.refresh_processes(); + sys.processes() + .values() + .filter(|p| p.exe().is_some_and(|path| path.ends_with("seelen-ui.exe"))) + .collect_vec() + .len() + > 1 +} + +fn main() -> Result<()> { + register_panic_hook()?; + trace_lock!(PERFORMANCE_HELPER).start("setup"); + + let command = trace_lock!(SEELEN_COMMAND_LINE).clone(); + let matches = match command.try_get_matches() { + Ok(m) => m, + // (help, --help or -h) are managed as error + Err(e) => { + attach_console()?; + e.print()?; + return Ok(()); + } + }; + + if is_just_getting_info(&matches)? { + return Ok(()); + } + + if is_already_runnning() { + let mut attempts = 0; + let mut connection = Client::connect_tcp(); + + while connection.is_err() && attempts < 10 { + attempts += 1; + std::thread::sleep(std::time::Duration::from_millis(100)); + connection = Client::connect_tcp(); + } + + if let Ok(stream) = connection { + let mut writer = BufWriter::new(stream); + + let args = std::env::args().collect_vec(); + let msg = serde_json::to_string(&args)?; + + writer.write_all(msg.as_bytes())?; + writer.flush()?; + return Ok(()); + } + + // if the connection fails probably is because the app is been closing + // so we check if the app is already running again to see if we have to + // let the instance be created or not + if is_already_runnning() { + return Ok(()); + } + } + + let mut app_builder = tauri::Builder::default(); + app_builder = register_plugins(app_builder); + app_builder = register_invoke_handler(app_builder); + + let app = app_builder + .setup(|app| { + if let Err(err) = setup(app) { + log::error!("Error while setting up: {:?}", err); + app.handle().exit(1); + } + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("Error while building tauri application"); + + app.run(app_callback); + Ok(()) +} diff --git a/src/background/modules/cli/application/debugger.rs b/src/background/modules/cli/application/debugger.rs index 8af9bec4..42c42ee9 100644 --- a/src/background/modules/cli/application/debugger.rs +++ b/src/background/modules/cli/application/debugger.rs @@ -3,7 +3,7 @@ use std::sync::atomic::Ordering; use clap::Command; use crate::{ - error_handler::Result, get_subcommands, hook::WIN_EVENTS_ENABLED, utils::TRACE_LOCK_ENABLED, + error_handler::Result, get_subcommands, hook::LOG_WIN_EVENTS, utils::TRACE_LOCK_ENABLED, }; get_subcommands![ @@ -28,10 +28,7 @@ impl CliDebugger { let subcommand = SubCommand::try_from(matches)?; match subcommand { SubCommand::ToggleWinEvents => { - WIN_EVENTS_ENABLED.store( - !WIN_EVENTS_ENABLED.load(Ordering::Acquire), - Ordering::Release, - ); + LOG_WIN_EVENTS.store(!LOG_WIN_EVENTS.load(Ordering::Acquire), Ordering::Release); } SubCommand::ToggleTraceLock => { TRACE_LOCK_ENABLED.store( diff --git a/src/background/modules/cli/application/mod.rs b/src/background/modules/cli/application/mod.rs index 3b7f4036..1dd4dbe6 100644 --- a/src/background/modules/cli/application/mod.rs +++ b/src/background/modules/cli/application/mod.rs @@ -12,10 +12,12 @@ use parking_lot::Mutex; use windows::Win32::System::Console::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use crate::error_handler::Result; +use crate::modules::virtual_desk::{VirtualDesktopManager, VIRTUAL_DESKTOP_MANAGER}; use crate::seelen::{Seelen, SEELEN}; use crate::seelen_bar::FancyToolbar; +use crate::seelen_rofi::SeelenRofi; use crate::seelen_weg::SeelenWeg; -use crate::seelen_wm::WindowManager; +use crate::seelen_wm_v2::instance::WindowManagerV2; use crate::state::application::FULL_STATE; use crate::trace_lock; @@ -65,10 +67,10 @@ macro_rules! get_subcommands { Ok(SubCommand::$subcommand$(($((sub_matches.get_one(stringify!($arg_name)) as Option<&$arg_type>).unwrap().clone()),*))?) }, )* - _ => Err(color_eyre::eyre::eyre!("Unknown subcommand.").into()), + _ => Err("Unknown subcommand.".into()), } } else { - Err(color_eyre::eyre::eyre!("No subcommand was provided.").into()) + Err("No subcommand was provided.".into()) } } } @@ -107,10 +109,12 @@ lazy_static! { ]) .subcommands([ Command::new("settings").about("Opens the Seelen settings gui."), + VirtualDesktopManager::get_cli(), CliDebugger::get_cli(), FancyToolbar::get_cli(), - WindowManager::get_cli(), + WindowManagerV2::get_cli(), SeelenWeg::get_cli(), + SeelenRofi::get_cli(), ]) )); } @@ -186,15 +190,14 @@ pub fn handle_cli_events(matches: &clap::ArgMatches) -> Result<()> { "settings" => { Seelen::show_settings()?; } + VirtualDesktopManager::CLI_IDENTIFIER => { + VIRTUAL_DESKTOP_MANAGER.load().process(matches)?; + } CliDebugger::CLI_IDENTIFIER => { CliDebugger::process(matches)?; } - WindowManager::CLI_IDENTIFIER => { - if let Some(monitor) = trace_lock!(SEELEN).focused_monitor_mut() { - if let Some(wm) = monitor.wm_mut() { - wm.process(matches)?; - } - } + WindowManagerV2::CLI_IDENTIFIER => { + WindowManagerV2::process(matches)?; } FancyToolbar::CLI_IDENTIFIER => { let mut seelen = trace_lock!(SEELEN); @@ -212,6 +215,11 @@ pub fn handle_cli_events(matches: &clap::ArgMatches) -> Result<()> { } } } + SeelenRofi::CLI_IDENTIFIER => { + if let Some(rofi) = trace_lock!(SEELEN).rofi_mut() { + rofi.process(matches)?; + } + } _ => {} } return Ok(()); diff --git a/src/background/modules/cli/mod.rs b/src/background/modules/cli/mod.rs index 059be669..3c5af94d 100644 --- a/src/background/modules/cli/mod.rs +++ b/src/background/modules/cli/mod.rs @@ -1,71 +1,77 @@ -pub mod application; -pub mod domain; - -use std::{ - fs, - io::{BufReader, Read}, - net::{TcpListener, TcpStream}, -}; - -use application::{handle_cli_events, SEELEN_COMMAND_LINE}; - -use crate::{error_handler::Result, log_error, trace_lock, utils::spawn_named_thread}; - -pub struct Client; -impl Client { - // const BUFFER_SIZE: usize = 5 * 1024 * 1024; // 5 MB - - fn handle_message(stream: TcpStream) { - let mut reader = BufReader::new(stream); - let mut buffer = vec![]; - match reader.read_to_end(&mut buffer) { - Ok(_) => { - let message = String::from_utf8_lossy(&buffer).to_string(); - match serde_json::from_str::>(&message) { - Ok(argv) => { - log::trace!(target: "slu::cli", "{}", argv[1..].join(" ")); - std::thread::spawn(move || { - let command = trace_lock!(SEELEN_COMMAND_LINE).clone(); - log_error!(handle_cli_events(&command.get_matches_from(argv))); - }); - } - Err(e) => { - log::error!("Failed to deserialize message: {}", e); - } - } - } - Err(e) => { - log::error!("Failed to read from stream: {}", e); - } - } - } - - pub fn listen_tcp() -> Result<()> { - let listener = TcpListener::bind("127.0.0.1:0")?; - let socket_addr = listener.local_addr()?; - let port = socket_addr.port(); - - log::info!("TCP server listening on 127.0.0.1:{}", port); - fs::write( - std::env::temp_dir().join("slu_tcp_socket"), - port.to_string(), - )?; - - spawn_named_thread("TCP Listener", move || { - for stream in listener.incoming() { - match stream { - Ok(stream) => Self::handle_message(stream), - Err(e) => { - log::error!("Failed to accept connection: {}", e); - } - } - } - })?; - Ok(()) - } - - pub fn connect_tcp() -> Result { - let port = fs::read_to_string(std::env::temp_dir().join("slu_tcp_socket"))?; - Ok(TcpStream::connect(format!("127.0.0.1:{}", port))?) - } -} +pub mod application; +pub mod domain; + +use std::{ + fs, + io::{BufReader, Read}, + net::{TcpListener, TcpStream}, +}; + +use application::{handle_cli_events, SEELEN_COMMAND_LINE}; + +use crate::{ + error_handler::Result, log_error, seelen::Seelen, trace_lock, utils::spawn_named_thread, +}; + +pub struct Client; +impl Client { + // const BUFFER_SIZE: usize = 5 * 1024 * 1024; // 5 MB + + fn handle_message(stream: TcpStream) { + let mut reader = BufReader::new(stream); + let mut buffer = vec![]; + match reader.read_to_end(&mut buffer) { + Ok(_) => { + let message = String::from_utf8_lossy(&buffer).to_string(); + match serde_json::from_str::>(&message) { + Ok(argv) => { + log::trace!(target: "slu::cli", "{}", argv[1..].join(" ")); + std::thread::spawn(move || { + let command = trace_lock!(SEELEN_COMMAND_LINE).clone(); + log_error!(handle_cli_events(&command.get_matches_from(argv))); + }); + } + Err(e) => { + log::error!("Failed to deserialize message: {}", e); + } + } + } + Err(e) => { + log::error!("Failed to read from stream: {}", e); + } + } + } + + pub fn listen_tcp() -> Result<()> { + let listener = TcpListener::bind("127.0.0.1:0")?; + let socket_addr = listener.local_addr()?; + let port = socket_addr.port(); + + log::info!("TCP server listening on 127.0.0.1:{}", port); + fs::write( + std::env::temp_dir().join("slu_tcp_socket"), + port.to_string(), + )?; + + spawn_named_thread("TCP Listener", move || { + for stream in listener.incoming() { + if !Seelen::is_running() { + log::trace!("Exiting TCP Listener"); + break; + } + match stream { + Ok(stream) => Self::handle_message(stream), + Err(e) => { + log::error!("Failed to accept connection: {}", e); + } + } + } + })?; + Ok(()) + } + + pub fn connect_tcp() -> Result { + let port = fs::read_to_string(std::env::temp_dir().join("slu_tcp_socket"))?; + Ok(TcpStream::connect(format!("127.0.0.1:{}", port))?) + } +} diff --git a/src/background/modules/input/domain.rs b/src/background/modules/input/domain.rs index 71e17cb0..7ce7c8ad 100644 --- a/src/background/modules/input/domain.rs +++ b/src/background/modules/input/domain.rs @@ -1,64 +1,73 @@ -use std::fmt::Debug; -use std::fmt::Display; - -use windows::Win32::Foundation::POINT; - -/// A Point type stores the x and y position. -#[derive(Clone, Copy, PartialEq, Eq, Default)] -pub struct Point(POINT); - -impl Point { - /// Creates a new position. - pub fn new(x: i32, y: i32) -> Self { - Self(POINT { x, y }) - } - - /// Retrieves the x position. - pub fn get_x(&self) -> i32 { - self.0.x - } - - /// Retrieves the y position. - pub fn get_y(&self) -> i32 { - self.0.y - } -} - -impl Debug for Point { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Point") - .field("x", &self.0.x) - .field("y", &self.0.y) - .finish() - } -} - -impl Display for Point { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({}, {})", self.0.x, self.0.y) - } -} - -impl From for Point { - fn from(point: POINT) -> Self { - Self(point) - } -} - -impl From for POINT { - fn from(val: Point) -> Self { - val.0 - } -} - -impl AsRef for Point { - fn as_ref(&self) -> &POINT { - &self.0 - } -} - -impl AsMut for Point { - fn as_mut(&mut self) -> &mut POINT { - &mut self.0 - } -} +use std::fmt::Debug; +use std::fmt::Display; + +use windows::Win32::Foundation::POINT; + +use seelen_core::rect::Rect; + +/// A Point type stores the x and y position. +#[derive(Clone, Copy, PartialEq, Eq, Default)] +pub struct Point(POINT); + +impl Point { + /// Creates a new position. + pub fn new(x: i32, y: i32) -> Self { + Self(POINT { x, y }) + } + + /// Retrieves the x position. + pub fn get_x(&self) -> i32 { + self.0.x + } + + /// Retrieves the y position. + pub fn get_y(&self) -> i32 { + self.0.y + } + + pub fn is_inside_rect(&self, rect: &Rect) -> bool { + self.0.x >= rect.left + && self.0.x <= rect.right + && self.0.y >= rect.top + && self.0.y <= rect.bottom + } +} + +impl Debug for Point { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Point") + .field("x", &self.0.x) + .field("y", &self.0.y) + .finish() + } +} + +impl Display for Point { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.0.x, self.0.y) + } +} + +impl From for Point { + fn from(point: POINT) -> Self { + Self(point) + } +} + +impl From for POINT { + fn from(val: Point) -> Self { + val.0 + } +} + +impl AsRef for Point { + fn as_ref(&self) -> &POINT { + &self.0 + } +} + +impl AsMut for Point { + fn as_mut(&mut self) -> &mut POINT { + &mut self.0 + } +} diff --git a/src/background/modules/media/application.rs b/src/background/modules/media/application.rs index da26df13..7f42c283 100644 --- a/src/background/modules/media/application.rs +++ b/src/background/modules/media/application.rs @@ -35,8 +35,7 @@ use windows_core::Interface; use crate::{ error_handler::Result, log_error, - seelen::get_app_handle, - seelen_weg::icon_extractor::{extract_and_save_icon, extract_and_save_icon_v2}, + seelen_weg::icon_extractor::{extract_and_save_icon_from_file, extract_and_save_icon_umid}, trace_lock, utils::pcwstr, windows_api::{Com, WindowEnumerator, WindowsApi}, @@ -87,7 +86,7 @@ enum MediaEvent { #[windows_core::implement(IMMNotificationClient)] struct MediaManagerEvents; -impl IMMNotificationClient_Impl for MediaManagerEvents { +impl IMMNotificationClient_Impl for MediaManagerEvents_Impl { fn OnDefaultDeviceChanged( &self, flow: EDataFlow, @@ -205,7 +204,7 @@ struct MediaDeviceEventHandler { device_id: String, } -impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler { +impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { fn OnNotify( &self, data: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA, @@ -221,7 +220,7 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler { } } -impl IAudioSessionNotification_Impl for MediaDeviceEventHandler { +impl IAudioSessionNotification_Impl for MediaDeviceEventHandler_Impl { fn OnSessionCreated( &self, _new_session: Option<&IAudioSessionControl>, @@ -234,7 +233,7 @@ impl IAudioSessionNotification_Impl for MediaDeviceEventHandler { #[windows::core::implement(IAudioSessionEvents)] struct MediaSessionEventHandler; -impl IAudioSessionEvents_Impl for MediaSessionEventHandler { +impl IAudioSessionEvents_Impl for MediaSessionEventHandler_Impl { fn OnChannelVolumeChanged( &self, _channel_count: u32, @@ -477,7 +476,7 @@ impl MediaManager { .to_string()?, } .replace(".exe", ""); - icon_path = extract_and_save_icon(&get_app_handle(), &path) + icon_path = extract_and_save_icon_from_file(&path) .ok() .map(|p| p.to_string_lossy().to_string()); } @@ -564,6 +563,7 @@ impl MediaManager { let playback_info = session.GetPlaybackInfo()?; let status = playback_info.PlaybackStatus()?; + // this is only needed when the player is not a uwp app like firefox player as example let owner = WindowEnumerator::new().find(|w| { if let Some(id) = w.app_user_model_id() { return id == source_app_user_model_id; @@ -576,8 +576,13 @@ impl MediaManager { title: properties.Title().unwrap_or_default().to_string_lossy(), author: properties.Artist().unwrap_or_default().to_string_lossy(), owner: owner.map(|w| MediaPlayerOwner { - name: w.title(), - icon_path: w.exe().and_then(extract_and_save_icon_v2).ok(), + name: w + .app_display_name() + .unwrap_or_else(|_| "Unknown App".to_string()), + icon_path: w + .app_user_model_id() + .and_then(|umid| extract_and_save_icon_umid(&umid).ok()) + .or_else(|| w.exe().and_then(extract_and_save_icon_from_file).ok()), }), thumbnail: properties .Thumbnail() diff --git a/src/background/modules/media/infrastructure.rs b/src/background/modules/media/infrastructure.rs index 7740f3f4..ef65343b 100644 --- a/src/background/modules/media/infrastructure.rs +++ b/src/background/modules/media/infrastructure.rs @@ -1,3 +1,4 @@ +use seelen_core::handlers::SeelenEvent; use std::sync::atomic::{AtomicBool, Ordering}; use tauri::Emitter; use windows::core::GUID; @@ -11,13 +12,16 @@ use super::domain::{Device, MediaPlayer}; fn emit_media_sessions(playing: &Vec) { let app = get_app_handle(); - app.emit("media-sessions", playing).expect("failed to emit"); + app.emit(SeelenEvent::MediaSessions, playing) + .expect("failed to emit"); } fn emit_media_devices(inputs: &Vec, outputs: &Vec) { let app = get_app_handle(); - app.emit("media-inputs", inputs).expect("failed to emit"); - app.emit("media-outputs", outputs).expect("failed to emit"); + app.emit(SeelenEvent::MediaInputs, inputs) + .expect("failed to emit"); + app.emit(SeelenEvent::MediaOutputs, outputs) + .expect("failed to emit"); } static REGISTERED: AtomicBool = AtomicBool::new(false); diff --git a/src/background/modules/monitors/mod.rs b/src/background/modules/monitors/mod.rs index 2839e671..18e4c1d0 100644 --- a/src/background/modules/monitors/mod.rs +++ b/src/background/modules/monitors/mod.rs @@ -39,33 +39,30 @@ pub enum MonitorManagerEvent { type OnMonitorsChange = Box; pub struct MonitorManager { - hwnd: isize, pub monitors: Vec<(String, HMONITOR)>, callbacks: Vec, } -impl MonitorManager { - pub fn hwnd(self) -> HWND { - HWND(self.hwnd) - } +unsafe impl Send for MonitorManager {} - pub extern "system" fn window_proc( +impl MonitorManager { + unsafe extern "system" fn window_proc( window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { - unsafe { - match message { - // Added based on this https://stackoverflow.com/a/33762334 - WM_DISPLAYCHANGE | WM_SETTINGCHANGE | WM_DEVICECHANGE => { - // log::debug!("Dispatching {}, {:?}, {:?}", message, wparam, lparam); + match message { + // Added based on this https://stackoverflow.com/a/33762334 + WM_DISPLAYCHANGE | WM_SETTINGCHANGE | WM_DEVICECHANGE => { + // log::debug!("Dispatching {}, {:?}, {:?}", message, wparam, lparam); + std::thread::spawn(move || { let mut manager = trace_lock!(MONITOR_MANAGER); let mut old_list = manager.monitors.clone(); let new_list = match Self::get_monitors() { Ok(monitors) => monitors, - Err(_) => return LRESULT(0), + Err(_) => return, }; for (name, id) in &new_list { @@ -91,14 +88,14 @@ impl MonitorManager { } manager.monitors = new_list.into_iter().collect(); - LRESULT(0) - } - _ => DefWindowProcW(window, message, wparam, lparam), + }); + LRESULT(0) } + _ => DefWindowProcW(window, message, wparam, lparam), } } - pub fn new() -> Result { + unsafe fn create_background_window() -> Result<()> { let wide_name: Vec = "Seelen Monitor Manager" .encode_utf16() .chain(Some(0)) @@ -117,14 +114,10 @@ impl MonitorManager { ..Default::default() }; - unsafe { - RegisterClassW(&wnd_class); - } + RegisterClassW(&wnd_class); - let (hwnd_sender, hwnd_receiver) = crossbeam_channel::bounded::(1); - - spawn_named_thread("Monitor Manager", move || unsafe { - let hwnd = CreateWindowExW( + let hwnd = unsafe { + CreateWindowExW( WINDOW_EX_STYLE::default(), PCWSTR(wide_class.as_ptr()), PCWSTR(wide_name.as_ptr()), @@ -137,34 +130,38 @@ impl MonitorManager { None, h_module, None, - ); - - log_error!(hwnd_sender.send(hwnd)); - - let mut notification_filter = DEV_BROADCAST_DEVICEINTERFACE_W { - dbcc_size: std::mem::size_of::() as u32, - dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0, - dbcc_reserved: 0, - dbcc_classguid: GUID_DEVINTERFACE_MONITOR, - dbcc_name: [0; 1], - }; - - log_error!(RegisterDeviceNotificationW( - hwnd, - &mut notification_filter as *mut _ as *mut _, - DEVICE_NOTIFY_WINDOW_HANDLE, - )); - - let mut msg = MSG::default(); - while GetMessageW(&mut msg, hwnd, 0, 0).into() { - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - std::thread::sleep(std::time::Duration::from_millis(10)); - } + )? + }; + + let mut notification_filter = DEV_BROADCAST_DEVICEINTERFACE_W { + dbcc_size: std::mem::size_of::() as u32, + dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0, + dbcc_reserved: 0, + dbcc_classguid: GUID_DEVINTERFACE_MONITOR, + dbcc_name: [0; 1], + }; + + RegisterDeviceNotificationW( + hwnd, + &mut notification_filter as *mut _ as *mut _, + DEVICE_NOTIFY_WINDOW_HANDLE, + )?; + + let mut msg = MSG::default(); + while GetMessageW(&mut msg, hwnd, 0, 0).as_bool() { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + Ok(()) + } + + pub fn new() -> Result { + spawn_named_thread("Monitor Manager", || unsafe { + log_error!(Self::create_background_window()); })?; Ok(Self { - hwnd: hwnd_receiver.recv()?.0, callbacks: Vec::new(), monitors: Self::get_monitors()?, }) @@ -172,7 +169,7 @@ impl MonitorManager { fn get_monitors() -> Result> { let mut monitors = Vec::new(); - for m in MonitorEnumerator::new_refreshed()? { + for m in MonitorEnumerator::get_all()? { monitors.push((WindowsApi::monitor_name(m)?, m)); } Ok(monitors) diff --git a/src/background/modules/network/infrastructure.rs b/src/background/modules/network/infrastructure.rs index 34476d3a..aff3410e 100644 --- a/src/background/modules/network/infrastructure.rs +++ b/src/background/modules/network/infrastructure.rs @@ -1,135 +1,136 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use tauri::Emitter; -use tauri_plugin_shell::ShellExt; -use windows::Win32::Networking::NetworkListManager::{ - INetworkListManager, NetworkListManager, NLM_CONNECTIVITY_IPV4_INTERNET, - NLM_CONNECTIVITY_IPV6_INTERNET, -}; - -use crate::{ - error_handler::Result, log_error, seelen::get_app_handle, utils::sleep_millis, windows_api::Com, -}; - -use super::{ - application::{get_local_ip_address, NetworkManager}, - domain::{NetworkAdapter, WlanProfile}, -}; - -fn emit_networks(ip: String, adapters: Vec, has_internet: bool) { - let handle = get_app_handle(); - log_error!(handle.emit("network-default-local-ip", ip)); - log_error!(handle.emit("network-adapters", adapters)); - log_error!(handle.emit("network-internet-connection", has_internet)); -} - -static REGISTERED: AtomicBool = AtomicBool::new(false); -pub fn register_network_events() -> Result<()> { - if !REGISTERED.load(Ordering::Acquire) { - REGISTERED.store(true, Ordering::Release); - log::trace!("Registering network events"); - NetworkManager::register_events(move |connectivity| { - log::trace!(target: "network", "Connectivity changed: {:?}", connectivity); - if let (Ok(ip), Ok(adapters)) = (get_local_ip_address(), NetworkManager::get_adapters()) - { - let has_internet_ipv4 = connectivity.0 & NLM_CONNECTIVITY_IPV4_INTERNET.0 - == NLM_CONNECTIVITY_IPV4_INTERNET.0; - let has_internet_ipv6 = connectivity.0 & NLM_CONNECTIVITY_IPV6_INTERNET.0 - == NLM_CONNECTIVITY_IPV6_INTERNET.0; - - emit_networks(ip, adapters, has_internet_ipv4 || has_internet_ipv6); - } - }); - } - - std::thread::spawn(|| -> Result<()> { - if let (Ok(ip), Ok(adapters)) = (get_local_ip_address(), NetworkManager::get_adapters()) { - let has_internet = Com::run_with_context(|| { - let list_manager: INetworkListManager = Com::create_instance(&NetworkListManager)?; - let connectivity = unsafe { list_manager.GetConnectivity()? }; - - let has_internet_ipv4 = connectivity.0 & NLM_CONNECTIVITY_IPV4_INTERNET.0 - == NLM_CONNECTIVITY_IPV4_INTERNET.0; - let has_internet_ipv6 = connectivity.0 & NLM_CONNECTIVITY_IPV6_INTERNET.0 - == NLM_CONNECTIVITY_IPV6_INTERNET.0; - - Ok(has_internet_ipv4 || has_internet_ipv6) - })?; - emit_networks(ip, adapters, has_internet); - } - Ok(()) - }); - - Ok(()) -} - -async fn try_connect_to_profile(ssid: &str) -> Result { - let handle = get_app_handle(); - let output = handle - .shell() - .command("netsh") - .args(["wlan", "connect", &format!("name={}", ssid)]) - .output() - .await?; - - if output.status.success() { - // wait to ensure connection - sleep_millis(2000); - Ok(NetworkManager::is_connected_to(ssid)?) - } else { - Err(output.into()) - } -} - -#[tauri::command(async)] -pub fn wlan_start_scanning() { - log::trace!("Start scanning networks"); - NetworkManager::start_scanning(|list| { - let app = get_app_handle(); - log_error!(app.emit("wlan-scanned", &list)); - }); -} - -#[tauri::command(async)] -pub fn wlan_stop_scanning() { - log::trace!("Stop scanning networks"); - NetworkManager::stop_scanning(); -} - -#[tauri::command(async)] -pub async fn wlan_get_profiles() -> Result> { - NetworkManager::get_wifi_profiles().await -} - -#[tauri::command(async)] -pub async fn wlan_connect(ssid: String, password: String, hidden: bool) -> Result { - NetworkManager::add_profile(&ssid, &password, hidden).await?; - match try_connect_to_profile(&ssid).await { - Ok(true) => Ok(true), - Ok(false) => { - NetworkManager::remove_profile(&ssid).await?; - Ok(false) - } - Err(err) => { - NetworkManager::remove_profile(&ssid).await?; - Err(err) - } - } -} - -#[tauri::command(async)] -pub async fn wlan_disconnect() -> Result<()> { - let handle = get_app_handle(); - let output = handle - .shell() - .command("netsh") - .args(["wlan", "disconnect"]) - .output() - .await?; - - if output.status.success() { - Ok(()) - } else { - Err(output.into()) - } -} +use std::sync::atomic::{AtomicBool, Ordering}; + +use seelen_core::handlers::SeelenEvent; +use tauri::Emitter; +use tauri_plugin_shell::ShellExt; +use windows::Win32::Networking::NetworkListManager::{ + INetworkListManager, NetworkListManager, NLM_CONNECTIVITY_IPV4_INTERNET, + NLM_CONNECTIVITY_IPV6_INTERNET, +}; + +use crate::{ + error_handler::Result, log_error, seelen::get_app_handle, utils::sleep_millis, windows_api::Com, +}; + +use super::{ + application::{get_local_ip_address, NetworkManager}, + domain::{NetworkAdapter, WlanProfile}, +}; + +fn emit_networks(ip: String, adapters: Vec, has_internet: bool) { + let handle = get_app_handle(); + log_error!(handle.emit(SeelenEvent::NetworkDefaultLocalIp, ip)); + log_error!(handle.emit(SeelenEvent::NetworkAdapters, adapters)); + log_error!(handle.emit(SeelenEvent::NetworkInternetConnection, has_internet)); +} + +static REGISTERED: AtomicBool = AtomicBool::new(false); +pub fn register_network_events() -> Result<()> { + if !REGISTERED.load(Ordering::Acquire) { + REGISTERED.store(true, Ordering::Release); + log::trace!("Registering network events"); + NetworkManager::register_events(move |connectivity| { + log::trace!(target: "network", "Connectivity changed: {:?}", connectivity); + if let (Ok(ip), Ok(adapters)) = (get_local_ip_address(), NetworkManager::get_adapters()) + { + let has_internet_ipv4 = connectivity.0 & NLM_CONNECTIVITY_IPV4_INTERNET.0 + == NLM_CONNECTIVITY_IPV4_INTERNET.0; + let has_internet_ipv6 = connectivity.0 & NLM_CONNECTIVITY_IPV6_INTERNET.0 + == NLM_CONNECTIVITY_IPV6_INTERNET.0; + + emit_networks(ip, adapters, has_internet_ipv4 || has_internet_ipv6); + } + }); + } + + std::thread::spawn(|| -> Result<()> { + if let (Ok(ip), Ok(adapters)) = (get_local_ip_address(), NetworkManager::get_adapters()) { + let has_internet = Com::run_with_context(|| { + let list_manager: INetworkListManager = Com::create_instance(&NetworkListManager)?; + let connectivity = unsafe { list_manager.GetConnectivity()? }; + + let has_internet_ipv4 = connectivity.0 & NLM_CONNECTIVITY_IPV4_INTERNET.0 + == NLM_CONNECTIVITY_IPV4_INTERNET.0; + let has_internet_ipv6 = connectivity.0 & NLM_CONNECTIVITY_IPV6_INTERNET.0 + == NLM_CONNECTIVITY_IPV6_INTERNET.0; + + Ok(has_internet_ipv4 || has_internet_ipv6) + })?; + emit_networks(ip, adapters, has_internet); + } + Ok(()) + }); + + Ok(()) +} + +async fn try_connect_to_profile(ssid: &str) -> Result { + let handle = get_app_handle(); + let output = handle + .shell() + .command("netsh") + .args(["wlan", "connect", &format!("name={}", ssid)]) + .output() + .await?; + + if output.status.success() { + // wait to ensure connection + sleep_millis(2000); + Ok(NetworkManager::is_connected_to(ssid)?) + } else { + Err(output.into()) + } +} + +#[tauri::command(async)] +pub fn wlan_start_scanning() { + log::trace!("Start scanning networks"); + NetworkManager::start_scanning(|list| { + let app = get_app_handle(); + log_error!(app.emit(SeelenEvent::NetworkWlanScanned, &list)); + }); +} + +#[tauri::command(async)] +pub fn wlan_stop_scanning() { + log::trace!("Stop scanning networks"); + NetworkManager::stop_scanning(); +} + +#[tauri::command(async)] +pub async fn wlan_get_profiles() -> Result> { + NetworkManager::get_wifi_profiles().await +} + +#[tauri::command(async)] +pub async fn wlan_connect(ssid: String, password: String, hidden: bool) -> Result { + NetworkManager::add_profile(&ssid, &password, hidden).await?; + match try_connect_to_profile(&ssid).await { + Ok(true) => Ok(true), + Ok(false) => { + NetworkManager::remove_profile(&ssid).await?; + Ok(false) + } + Err(err) => { + NetworkManager::remove_profile(&ssid).await?; + Err(err) + } + } +} + +#[tauri::command(async)] +pub async fn wlan_disconnect() -> Result<()> { + let handle = get_app_handle(); + let output = handle + .shell() + .command("netsh") + .args(["wlan", "disconnect"]) + .output() + .await?; + + if output.status.success() { + Ok(()) + } else { + Err(output.into()) + } +} diff --git a/src/background/modules/notifications/infrastructure.rs b/src/background/modules/notifications/infrastructure.rs index ece56b58..3936f76b 100644 --- a/src/background/modules/notifications/infrastructure.rs +++ b/src/background/modules/notifications/infrastructure.rs @@ -1,46 +1,47 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use tauri::Emitter; - -use crate::{error_handler::Result, log_error, seelen::get_app_handle, trace_lock}; - -use super::application::{AppNotification, NOTIFICATION_MANAGER}; - -fn emit_notifications(notifications: &Vec) { - get_app_handle() - .emit("notifications", notifications) - .expect("failed to emit"); -} - -static REGISTERED: AtomicBool = AtomicBool::new(false); -pub fn register_notification_events() { - let was_registered = REGISTERED.load(Ordering::Acquire); - if !was_registered { - REGISTERED.store(true, Ordering::Release); - } - std::thread::spawn(move || { - let mut manager = trace_lock!(NOTIFICATION_MANAGER); - if !was_registered { - log::trace!("Registering notifications events"); - manager.on_notifications_change(emit_notifications); - } - emit_notifications(manager.notifications()); - }); -} - -pub fn release_notification_events() { - if REGISTERED.load(Ordering::Acquire) { - log_error!(trace_lock!(NOTIFICATION_MANAGER).release()); - } -} - -#[tauri::command(async)] -pub fn notifications_close(id: u32) -> Result<()> { - trace_lock!(NOTIFICATION_MANAGER).remove_notification(id)?; - Ok(()) -} - -#[tauri::command(async)] -pub fn notifications_close_all() -> Result<()> { - trace_lock!(NOTIFICATION_MANAGER).clear_notifications() -} +use std::sync::atomic::{AtomicBool, Ordering}; + +use seelen_core::handlers::SeelenEvent; +use tauri::Emitter; + +use crate::{error_handler::Result, log_error, seelen::get_app_handle, trace_lock}; + +use super::application::{AppNotification, NOTIFICATION_MANAGER}; + +fn emit_notifications(notifications: &Vec) { + get_app_handle() + .emit(SeelenEvent::Notifications, notifications) + .expect("failed to emit"); +} + +static REGISTERED: AtomicBool = AtomicBool::new(false); +pub fn register_notification_events() { + let was_registered = REGISTERED.load(Ordering::Acquire); + if !was_registered { + REGISTERED.store(true, Ordering::Release); + } + std::thread::spawn(move || { + let mut manager = trace_lock!(NOTIFICATION_MANAGER); + if !was_registered { + log::trace!("Registering notifications events"); + manager.on_notifications_change(emit_notifications); + } + emit_notifications(manager.notifications()); + }); +} + +pub fn release_notification_events() { + if REGISTERED.load(Ordering::Acquire) { + log_error!(trace_lock!(NOTIFICATION_MANAGER).release()); + } +} + +#[tauri::command(async)] +pub fn notifications_close(id: u32) -> Result<()> { + trace_lock!(NOTIFICATION_MANAGER).remove_notification(id)?; + Ok(()) +} + +#[tauri::command(async)] +pub fn notifications_close_all() -> Result<()> { + trace_lock!(NOTIFICATION_MANAGER).clear_notifications() +} diff --git a/src/background/modules/power/infrastructure.rs b/src/background/modules/power/infrastructure.rs index 200dd30b..9b9236af 100644 --- a/src/background/modules/power/infrastructure.rs +++ b/src/background/modules/power/infrastructure.rs @@ -1,5 +1,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; +use seelen_core::handlers::SeelenEvent; use tauri::Emitter; use windows::{ core::PCWSTR, @@ -72,8 +73,8 @@ impl PowerManager { RegisterClassW(&wnd_class); } - spawn_named_thread("Power Manager Window", move || unsafe { - let hwnd = CreateWindowExW( + let hwnd = unsafe { + CreateWindowExW( WINDOW_EX_STYLE::default(), PCWSTR(wide_class.as_ptr()), PCWSTR(wide_name.as_ptr()), @@ -86,8 +87,12 @@ impl PowerManager { None, h_module, None, - ); + )? + }; + let addr = hwnd.0 as isize; + spawn_named_thread("Power Manager Message Loop", move || unsafe { + let hwnd = HWND(addr as _); let mut msg = MSG::default(); while GetMessageW(&mut msg, hwnd, 0, 0).into() { let _ = TranslateMessage(&msg); @@ -108,7 +113,7 @@ impl PowerManager { let handle = get_app_handle(); let power_status: PowerStatus = WindowsApi::get_system_power_status()?.into(); - handle.emit("power-status", power_status)?; + handle.emit(SeelenEvent::PowerStatus, power_status)?; let mut batteries: Vec = Vec::new(); let manager = battery::Manager::new()?; @@ -116,7 +121,7 @@ impl PowerManager { batteries.push(battery.try_into()?); } - handle.emit("batteries-status", batteries)?; + handle.emit(SeelenEvent::BatteriesStatus, batteries)?; Ok(()) } @@ -133,13 +138,13 @@ pub fn suspend() { } #[tauri::command(async)] -pub fn restart() -> Result<(), String> { +pub fn restart() -> Result<()> { WindowsApi::exit_windows(EWX_REBOOT, SHTDN_REASON_NONE)?; Ok(()) } #[tauri::command(async)] -pub fn shutdown() -> Result<(), String> { +pub fn shutdown() -> Result<()> { WindowsApi::exit_windows(EWX_SHUTDOWN, SHTDN_REASON_NONE)?; Ok(()) } diff --git a/src/background/modules/system_settings/application.rs b/src/background/modules/system_settings/application.rs index c1891b97..0f2d5c1c 100644 --- a/src/background/modules/system_settings/application.rs +++ b/src/background/modules/system_settings/application.rs @@ -1,110 +1,109 @@ -use std::sync::Arc; - -use crate::{error_handler::Result, log_error, trace_lock}; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use windows::{ - Foundation::{EventRegistrationToken, TypedEventHandler}, - UI::ViewManagement::{UIColorType, UISettings}, -}; -use windows_core::IInspectable; - -use super::domain::UIColors; - -lazy_static! { - pub static ref SYSTEM_SETTINGS: Arc> = Arc::new(Mutex::new( - SystemSettings::new().expect("Failed to create settings manager") - )); -} - -fn color_to_string(color: windows::UI::Color) -> String { - format!( - "#{:02X}{:02X}{:02X}{:02X}", - color.R, color.G, color.B, color.A - ) -} - -enum SettingsEvent { - ColorChanged, -} - -type ColorChangeCallback = Box; - -pub struct SystemSettings { - settings: UISettings, - color_event_handler: TypedEventHandler, - color_event_token: Option, - color_client_callbacks: Vec, -} - -unsafe impl Send for SystemSettings {} - -impl SystemSettings { - fn new() -> Result { - let mut settings = Self { - settings: UISettings::new()?, - color_event_handler: TypedEventHandler::new(Self::internal_on_colors_change), - color_event_token: None, - color_client_callbacks: Vec::new(), - }; - settings.init()?; - Ok(settings) - } - - fn init(&mut self) -> Result<()> { - self.color_event_token = Some( - self.settings - .ColorValuesChanged(&self.color_event_handler)?, - ); - Ok(()) - } - - pub fn release(&mut self) -> Result<()> { - self.color_client_callbacks.clear(); - if let Some(token) = self.color_event_token.take() { - self.settings.RemoveColorValuesChanged(token)?; - } - Ok(()) - } - - fn internal_on_colors_change( - _listener: &Option, - _args: &Option, - ) -> windows_core::Result<()> { - log_error!(trace_lock!(SYSTEM_SETTINGS).on_change(SettingsEvent::ColorChanged)); - Ok(()) - } - - pub fn get_colors(&self) -> Result { - let settings = &self.settings; - Ok(UIColors { - background: color_to_string(settings.GetColorValue(UIColorType::Background)?), - foreground: color_to_string(settings.GetColorValue(UIColorType::Foreground)?), - accent_darkest: color_to_string(settings.GetColorValue(UIColorType::AccentDark3)?), - accent_darker: color_to_string(settings.GetColorValue(UIColorType::AccentDark2)?), - accent_dark: color_to_string(settings.GetColorValue(UIColorType::AccentDark1)?), - accent: color_to_string(settings.GetColorValue(UIColorType::Accent)?), - accent_light: color_to_string(settings.GetColorValue(UIColorType::AccentLight1)?), - accent_lighter: color_to_string(settings.GetColorValue(UIColorType::AccentLight2)?), - accent_lightest: color_to_string(settings.GetColorValue(UIColorType::AccentLight3)?), - // https://learn.microsoft.com/is-is/uwp/api/windows.ui.viewmanagement.uisettings.getcolorvalue?view=winrt-19041#remarks - complement: None, - }) - } - - pub fn on_colors_change(&mut self, callback: ColorChangeCallback) { - self.color_client_callbacks.push(callback); - } - - fn on_change(&mut self, event: SettingsEvent) -> Result<()> { - match event { - SettingsEvent::ColorChanged => { - let colors = self.get_colors()?; - for callback in self.color_client_callbacks.iter() { - callback(&colors); - } - } - } - Ok(()) - } -} +use std::sync::Arc; + +use crate::{error_handler::Result, log_error, trace_lock}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use seelen_core::system_state::UIColors; +use windows::{ + Foundation::{EventRegistrationToken, TypedEventHandler}, + UI::ViewManagement::{UIColorType, UISettings}, +}; +use windows_core::IInspectable; + +lazy_static! { + pub static ref SYSTEM_SETTINGS: Arc> = Arc::new(Mutex::new( + SystemSettings::new().expect("Failed to create settings manager") + )); +} + +fn color_to_string(color: windows::UI::Color) -> String { + format!( + "#{:02X}{:02X}{:02X}{:02X}", + color.R, color.G, color.B, color.A + ) +} + +enum SettingsEvent { + ColorChanged, +} + +type ColorChangeCallback = Box; + +pub struct SystemSettings { + settings: UISettings, + color_event_handler: TypedEventHandler, + color_event_token: Option, + color_client_callbacks: Vec, +} + +unsafe impl Send for SystemSettings {} + +impl SystemSettings { + fn new() -> Result { + let mut settings = Self { + settings: UISettings::new()?, + color_event_handler: TypedEventHandler::new(Self::internal_on_colors_change), + color_event_token: None, + color_client_callbacks: Vec::new(), + }; + settings.init()?; + Ok(settings) + } + + fn init(&mut self) -> Result<()> { + self.color_event_token = Some( + self.settings + .ColorValuesChanged(&self.color_event_handler)?, + ); + Ok(()) + } + + pub fn release(&mut self) -> Result<()> { + self.color_client_callbacks.clear(); + if let Some(token) = self.color_event_token.take() { + self.settings.RemoveColorValuesChanged(token)?; + } + Ok(()) + } + + fn internal_on_colors_change( + _listener: &Option, + _args: &Option, + ) -> windows_core::Result<()> { + log_error!(trace_lock!(SYSTEM_SETTINGS).on_change(SettingsEvent::ColorChanged)); + Ok(()) + } + + pub fn get_colors(&self) -> Result { + let settings = &self.settings; + Ok(UIColors { + background: color_to_string(settings.GetColorValue(UIColorType::Background)?), + foreground: color_to_string(settings.GetColorValue(UIColorType::Foreground)?), + accent_darkest: color_to_string(settings.GetColorValue(UIColorType::AccentDark3)?), + accent_darker: color_to_string(settings.GetColorValue(UIColorType::AccentDark2)?), + accent_dark: color_to_string(settings.GetColorValue(UIColorType::AccentDark1)?), + accent: color_to_string(settings.GetColorValue(UIColorType::Accent)?), + accent_light: color_to_string(settings.GetColorValue(UIColorType::AccentLight1)?), + accent_lighter: color_to_string(settings.GetColorValue(UIColorType::AccentLight2)?), + accent_lightest: color_to_string(settings.GetColorValue(UIColorType::AccentLight3)?), + // https://learn.microsoft.com/is-is/uwp/api/windows.ui.viewmanagement.uisettings.getcolorvalue?view=winrt-19041#remarks + complement: None, + }) + } + + pub fn on_colors_change(&mut self, callback: ColorChangeCallback) { + self.color_client_callbacks.push(callback); + } + + fn on_change(&mut self, event: SettingsEvent) -> Result<()> { + match event { + SettingsEvent::ColorChanged => { + let colors = self.get_colors()?; + for callback in self.color_client_callbacks.iter() { + callback(&colors); + } + } + } + Ok(()) + } +} diff --git a/src/background/modules/system_settings/domain.rs b/src/background/modules/system_settings/domain.rs index 2dd6affd..d3f5a12f 100644 --- a/src/background/modules/system_settings/domain.rs +++ b/src/background/modules/system_settings/domain.rs @@ -1,16 +1 @@ -use serde::Serialize; - -/// https://learn.microsoft.com/is-is/uwp/api/windows.ui.viewmanagement.uicolortype?view=winrt-19041 -#[derive(Debug, Default, Serialize)] -pub struct UIColors { - pub background: String, - pub foreground: String, - pub accent_darkest: String, - pub accent_darker: String, - pub accent_dark: String, - pub accent: String, - pub accent_light: String, - pub accent_lighter: String, - pub accent_lightest: String, - pub complement: Option, -} + diff --git a/src/background/modules/system_settings/infrastructure.rs b/src/background/modules/system_settings/infrastructure.rs index 1fee71e0..70f2f996 100644 --- a/src/background/modules/system_settings/infrastructure.rs +++ b/src/background/modules/system_settings/infrastructure.rs @@ -1,35 +1,28 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use tauri::Emitter; - -use crate::{log_error, seelen::get_app_handle, trace_lock}; - -use super::{application::SYSTEM_SETTINGS, domain::UIColors}; - -fn emit_colors(colors: &UIColors) { - get_app_handle() - .emit("colors", colors) - .expect("failed to emit"); -} - -static REGISTERED: AtomicBool = AtomicBool::new(false); -pub fn register_colors_events() { - let was_registered = REGISTERED.load(Ordering::Acquire); - if !was_registered { - REGISTERED.store(true, Ordering::Release); - } - std::thread::spawn(move || { - let mut manager = trace_lock!(SYSTEM_SETTINGS); - if !was_registered { - log::trace!("Registering colors events"); - manager.on_colors_change(Box::new(emit_colors)); - } - emit_colors(&manager.get_colors().expect("Failed to get colors")); - }); -} - -pub fn release_colors_events() { - if REGISTERED.load(Ordering::Acquire) { - log_error!(trace_lock!(SYSTEM_SETTINGS).release()); - } -} +use seelen_core::{handlers::SeelenEvent, system_state::UIColors}; +use tauri::Emitter; + +use crate::{error_handler::Result, log_error, seelen::get_app_handle, trace_lock}; + +use super::application::SYSTEM_SETTINGS; + +fn emit_colors(colors: &UIColors) { + get_app_handle() + .emit(SeelenEvent::ColorsChanged, colors) + .expect("failed to emit"); +} + +pub fn register_colors_events() { + std::thread::spawn(move || { + let mut manager = trace_lock!(SYSTEM_SETTINGS); + manager.on_colors_change(Box::new(emit_colors)); + }); +} + +pub fn release_colors_events() { + log_error!(trace_lock!(SYSTEM_SETTINGS).release()); +} + +#[tauri::command(async)] +pub fn get_system_colors() -> Result { + trace_lock!(SYSTEM_SETTINGS).get_colors() +} diff --git a/src/background/modules/tray/application.rs b/src/background/modules/tray/application.rs index 98646ec3..930c28af 100644 --- a/src/background/modules/tray/application.rs +++ b/src/background/modules/tray/application.rs @@ -19,8 +19,7 @@ use winreg::{ use crate::{ error_handler::Result, pcstr, - seelen::get_app_handle, - seelen_weg::icon_extractor::extract_and_save_icon, + seelen_weg::icon_extractor::extract_and_save_icon_from_file, utils::{is_windows_10, is_windows_11, resolve_guid_path, sleep_millis}, windows_api::{AppBarData, AppBarDataState, Com, WindowsApi}, }; @@ -43,21 +42,13 @@ pub fn get_sub_tree( Ok(elements) } -fn get_tray_overflow_handle() -> Option { +pub fn get_tray_overflow_handle() -> Option { unsafe { if is_windows_10() { - let tray_overflow = FindWindowA(pcstr!("NotifyIconOverFlowWindow"), None); - if tray_overflow.0 == 0 { - return None; - } - return Some(tray_overflow); - } - - let tray_overflow = FindWindowA(pcstr!("TopLevelWindowForOverflowXamlIsland"), None); - if tray_overflow.0 == 0 { - return None; + FindWindowA(pcstr!("NotifyIconOverFlowWindow"), None).ok() + } else { + FindWindowA(pcstr!("TopLevelWindowForOverflowXamlIsland"), None).ok() } - Some(tray_overflow) } } @@ -65,24 +56,22 @@ fn get_tray_overflow_content_handle() -> Option { let tray_overflow = get_tray_overflow_handle()?; unsafe { if is_windows_10() { - let tray_overflow_content = - FindWindowExA(tray_overflow, HWND(0), pcstr!("ToolbarWindow32"), None); - if tray_overflow_content.0 == 0 { - return None; - } - return Some(tray_overflow_content); - } - - let tray_overflow_content = FindWindowExA( - tray_overflow, - HWND(0), - pcstr!("Windows.UI.Composition.DesktopWindowContentBridge"), - None, - ); - if tray_overflow_content.0 == 0 { - return None; + FindWindowExA( + tray_overflow, + HWND::default(), + pcstr!("ToolbarWindow32"), + None, + ) + .ok() + } else { + FindWindowExA( + tray_overflow, + HWND::default(), + pcstr!("Windows.UI.Composition.DesktopWindowContentBridge"), + None, + ) + .ok() } - Some(tray_overflow_content) } } @@ -92,7 +81,7 @@ pub fn ensure_tray_overflow_creation() -> Result<()> { } Com::run_with_context(|| unsafe { - let tray_hwnd = FindWindowA(pcstr!("Shell_TrayWnd"), None); + let tray_hwnd = FindWindowA(pcstr!("Shell_TrayWnd"), None)?; let tray_bar = AppBarData::from_handle(tray_hwnd); let tray_bar_state = tray_bar.state(); @@ -107,8 +96,8 @@ pub fn ensure_tray_overflow_creation() -> Result<()> { let element_array = element.FindAll(TreeScope_Subtree, &condition)?; for index in 0..element_array.Length().unwrap_or(0) { let element = element_array.GetElement(index)?; - if element.CurrentName()? == "Show Hidden Icons" - && element.CurrentAutomationId()? == "SystemTrayIcon" + if element.CurrentAutomationId()? == "SystemTrayIcon" + && element.CurrentClassName()? == "SystemTray.NormalButton" { let invoker = element .GetCurrentPatternAs::(UIA_InvokePatternId)?; @@ -116,15 +105,17 @@ pub fn ensure_tray_overflow_creation() -> Result<()> { invoker.Invoke()?; sleep_millis(10); invoker.Invoke()?; - - tray_bar.set_state(tray_bar_state); - return Ok(()); + break; } } tray_bar.set_state(tray_bar_state); - Err("Failed to force tray overflow creation".into()) - }) + Ok(()) + })?; + if get_tray_overflow_content_handle().is_none() { + return Err("Failed to create tray overflow".into()); + } + Ok(()) } pub fn get_tray_icons() -> Result> { @@ -144,10 +135,9 @@ pub fn get_tray_icons() -> Result> { children.extend(get_sub_tree(&element, &condition, TreeScope_Descendants)?); } - let is_win10 = is_windows_10(); for element in children { let name = element.CurrentName()?.to_string(); - if is_win10 || element.CurrentAutomationId()? == "NotifyItemIcon" { + if !element.CurrentAutomationId()?.is_empty() { let registry = tray_from_registry.iter().find(|t| { let trimmed = name.trim(); t.initial_tooltip == trimmed @@ -185,7 +175,7 @@ impl TrayIcon { } let path = self.registry.as_ref().unwrap().executable_path.clone(); - let icon = extract_and_save_icon(&get_app_handle(), &path)?; + let icon = extract_and_save_icon_from_file(&path)?; Ok(icon .to_string_lossy() .trim_start_matches("\\\\?\\") @@ -210,7 +200,7 @@ impl TrayIcon { if let Some(hwnd) = get_tray_overflow_handle() { WindowsApi::show_window(hwnd, SW_SHOW)?; - let rect = WindowsApi::get_window_rect(hwnd); + let rect = WindowsApi::get_outer_window_rect(hwnd)?; WindowsApi::move_window( hwnd, diff --git a/src/background/modules/tray/infrastructure.rs b/src/background/modules/tray/infrastructure.rs index ce947d78..aeed8a11 100644 --- a/src/background/modules/tray/infrastructure.rs +++ b/src/background/modules/tray/infrastructure.rs @@ -1,50 +1,51 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use itertools::Itertools; -use tauri::Emitter; - -use crate::{ - error_handler::Result, log_error, modules::tray::application::get_tray_icons, - seelen::get_app_handle, -}; - -fn emit_tray_info() -> Result<()> { - let handle = get_app_handle(); - let payload = get_tray_icons()?.iter().map(|t| t.info()).collect_vec(); - handle.emit("tray-info", payload)?; - Ok(()) -} - -static REGISTERED: AtomicBool = AtomicBool::new(false); -pub fn register_tray_events() { - if !REGISTERED.load(Ordering::Acquire) { - log::trace!("Registering tray events"); - // TODO: add event listener for tray events - REGISTERED.store(true, Ordering::Release); - } - // Eythan: I don't know why but it doesn't work without the thread::spawn - // it makes a deadlock and app crashes - std::thread::spawn(|| log_error!(emit_tray_info())); -} - -// TODO: remove when add event listener for tray events -#[tauri::command(async)] -pub fn temp_get_by_event_tray_info() { - log_error!(emit_tray_info()); -} - -#[tauri::command(async)] -pub fn on_click_tray_icon(idx: usize) -> Result<()> { - let icons = get_tray_icons()?; - let icon = icons.get(idx).ok_or("tray icon index out of bounds")?; - icon.invoke()?; - Ok(()) -} - -#[tauri::command(async)] -pub fn on_context_menu_tray_icon(idx: usize) -> Result<()> { - let icons = get_tray_icons()?; - let icon = icons.get(idx).ok_or("tray icon index out of bounds")?; - icon.context_menu()?; - Ok(()) -} +use std::sync::atomic::{AtomicBool, Ordering}; + +use itertools::Itertools; +use seelen_core::handlers::SeelenEvent; +use tauri::Emitter; + +use crate::{ + error_handler::Result, log_error, modules::tray::application::get_tray_icons, + seelen::get_app_handle, +}; + +fn emit_tray_info() -> Result<()> { + let handle = get_app_handle(); + let payload = get_tray_icons()?.iter().map(|t| t.info()).collect_vec(); + handle.emit(SeelenEvent::TrayInfo, payload)?; + Ok(()) +} + +static REGISTERED: AtomicBool = AtomicBool::new(false); +pub fn register_tray_events() { + if !REGISTERED.load(Ordering::Acquire) { + log::trace!("Registering tray events"); + // TODO: add event listener for tray events + REGISTERED.store(true, Ordering::Release); + } + // Eythan: I don't know why but it doesn't work without the thread::spawn + // it makes a deadlock and app crashes + std::thread::spawn(|| log_error!(emit_tray_info())); +} + +// TODO: remove when add event listener for tray events +#[tauri::command(async)] +pub fn temp_get_by_event_tray_info() { + log_error!(emit_tray_info()); +} + +#[tauri::command(async)] +pub fn on_click_tray_icon(idx: usize) -> Result<()> { + let icons = get_tray_icons()?; + let icon = icons.get(idx).ok_or("tray icon index out of bounds")?; + icon.invoke()?; + Ok(()) +} + +#[tauri::command(async)] +pub fn on_context_menu_tray_icon(idx: usize) -> Result<()> { + let icons = get_tray_icons()?; + let icon = icons.get(idx).ok_or("tray icon index out of bounds")?; + icon.context_menu()?; + Ok(()) +} diff --git a/src/background/modules/uwp/domain.rs b/src/background/modules/uwp/domain.rs new file mode 100644 index 00000000..14a65097 --- /dev/null +++ b/src/background/modules/uwp/domain.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct PackageManifest { + pub identity: ManifestIdentity, + pub properties: ManifestProperties, + pub applications: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManifestIdentity { + #[serde(rename = "@Name")] + pub name: String, + #[serde(rename = "@Version")] + pub version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ManifestProperties { + pub display_name: String, + pub publisher_display_name: String, + pub logo: String, + pub description: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ManifestApplications { + pub application: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ManifestApplication { + #[serde(rename = "@Id")] + pub id: String, + pub visual_elements: ManifestApplicationVisualElements, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ManifestApplicationVisualElements { + #[serde(rename = "@DisplayName")] + pub display_name: String, + #[serde(rename = "@Description")] + pub description: String, + #[serde(rename = "@BackgroundColor")] + pub background_color: String, + #[serde(rename = "@Square150x150Logo")] + pub logo_150: String, + #[serde(rename = "@Square44x44Logo")] + pub logo_44: String, +} diff --git a/src/background/modules/uwp/load_uwp_apps.ps1 b/src/background/modules/uwp/load_uwp_apps.ps1 deleted file mode 100644 index b0a462cf..00000000 --- a/src/background/modules/uwp/load_uwp_apps.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -$packages = Get-AppxPackage -$output = @() - -foreach ($package in $packages) { - $manifest = Get-AppxPackageManifest -Package $package.PackageFullName - - $applications = @() - foreach ($app in $manifest.Package.Applications.Application) { - if ($null -eq $app.Executable) { - continue - } - - $alias = $null - if ($app.Extensions.Extension) { - foreach ($extension in $app.Extensions.Extension) { - if ($extension.Category -eq "windows.appExecutionAlias" -and $extension.AppExecutionAlias) { - foreach ($executionAlias in $extension.AppExecutionAlias.ExecutionAlias) { - if ($executionAlias.Alias) { - $alias = $executionAlias.Alias - break - } - } - } - } - } - - $applications += [PSCustomObject]@{ - AppId = $app.Id - Executable = $app.Executable - Alias = $alias - Square150x150Logo = $app.VisualElements.Square150x150Logo - Square44x44Logo = $app.VisualElements.Square44x44Logo - } - } - - if ($applications.Count -eq 0) { - continue - } - - $resolvedInstallLocation = "" - - if ($null -ne $package.InstallLocation) { - $resolvedInstallLocation = $package.InstallLocation - } - - # Resolve install location in case it's a symlink - $target = (Get-Item -Path $package.InstallLocation).Target - if ($target -is [array]) { - $target = $target[0] - } - - if ($null -ne $target -and $target -ne "") { - $resolvedInstallLocation = $target - } - - # Convert to string if it's not already - $selected = [PSCustomObject]@{ - Name = $package.Name - Version = $package.Version - PublisherId = $package.PublisherId - PackageFullName = $package.PackageFullName - InstallLocation = $resolvedInstallLocation - StoreLogo = $manifest.Package.Properties.Logo - Applications = $applications - } - - $output += $selected -} - -$output | ConvertTo-Json -Depth 3 -Compress \ No newline at end of file diff --git a/src/background/modules/uwp/mod.rs b/src/background/modules/uwp/mod.rs index 5f702233..a38a3fe2 100644 --- a/src/background/modules/uwp/mod.rs +++ b/src/background/modules/uwp/mod.rs @@ -1,29 +1,18 @@ +// unused/deprecated code but could be useful for understanding how uwp packing works +pub mod domain; + +use domain::{ManifestApplication, PackageManifest}; use lazy_static::lazy_static; -use parking_lot::Mutex; -use serde::Deserialize; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use tauri::Manager; - -use crate::{ - error_handler::Result, - seelen::get_app_handle, - trace_lock, - utils::{pwsh::PwshScript, PERFORMANCE_HELPER}, -}; +use std::path::{Path, PathBuf}; +use windows::ApplicationModel::{AppInfo, Package}; + +use crate::error_handler::Result; pub static UWP_LIGHTUNPLATED_POSTFIX: &str = "_altform-lightunplated"; #[allow(dead_code)] pub static UWP_UNPLATED_POSTFIX: &str = "_altform-unplated"; lazy_static! { - pub static ref UWP_MANAGER: Arc> = Arc::new(Mutex::new({ - let mut manager = WindowsAppsManager::default(); - manager.refresh().expect("Failed to refresh UWP manager"); - manager - })); pub static ref UWP_TARGET_SIZE_POSTFIXES: Vec<&'static str> = vec![ ".targetsize-256", ".targetsize-96", @@ -40,146 +29,82 @@ lazy_static! { ]; } -#[derive(Deserialize, Debug, Default)] -#[serde(default, rename_all = "PascalCase")] -#[allow(dead_code)] -pub struct UWPPackage { - name: String, - version: String, - publisher_id: String, - package_full_name: String, - install_location: PathBuf, - store_logo: Option, - applications: Vec, -} - -#[derive(Deserialize, Debug, Default)] -#[serde(rename_all = "PascalCase")] -pub struct UWPApplication { - app_id: String, - /// subpath from UWPPackage.install_location - executable: String, - alias: Option, - /// subpath from UWPPackage.install_location - square150x150_logo: Option, - // subpath from UWPPackage.install_location - square44x44_logo: Option, -} +pub fn get_hightest_quality_posible(icon_path: &Path) -> Option { + let filename = icon_path.file_stem()?.to_str()?; + let extension = icon_path.extension()?.to_str()?; + + let size_postfixes = (*UWP_TARGET_SIZE_POSTFIXES) + .iter() + .chain((*UWP_SCALE_POSTFIXES).iter()); + + for size_postfix in size_postfixes { + let maybe_icon_path = icon_path.with_file_name(format!( + "{}{}{}.{}", + filename, size_postfix, UWP_LIGHTUNPLATED_POSTFIX, extension + )); + if maybe_icon_path.exists() { + return Some(maybe_icon_path); + } -impl UWPApplication { - pub fn get_44_icon(&self) -> Option<&String> { - self.square44x44_logo.as_ref() + let maybe_icon_path = + icon_path.with_file_name(format!("{}{}.{}", filename, size_postfix, extension)); + if maybe_icon_path.exists() { + return Some(maybe_icon_path); + } } - pub fn get_150_icon(&self) -> Option<&String> { - self.square150x150_logo.as_ref() + // Some apps only adds one icon and without any postfix + // but we prefer the light/dark specific icon + if icon_path.exists() { + return Some(icon_path.to_path_buf()); } -} -impl UWPPackage { - pub fn get_store_logo(&self) -> Option<&String> { - self.store_logo.as_ref() - } + None +} - pub fn get_app(&self, exe: &str) -> Option<&UWPApplication> { - self.applications - .iter() - .find(|app| app.executable.ends_with(exe) || app.alias.as_deref() == Some(exe)) +impl PackageManifest { + pub fn get_app(&self, id: &str) -> Option<&ManifestApplication> { + let apps = self.applications.as_ref()?; + apps.application.iter().find(|app| app.id == id) } +} - pub fn get_light_icon_path(icon_path: &Path) -> Option { - let filename = icon_path.file_stem()?.to_str()?; - let extension = icon_path.extension()?.to_str()?; - - let postfixes = (*UWP_TARGET_SIZE_POSTFIXES) - .iter() - .chain((*UWP_SCALE_POSTFIXES).iter()); - - for postfix in postfixes { - let maybe_icon_path = icon_path.with_file_name(format!( - "{}{}{}.{}", - filename, postfix, UWP_LIGHTUNPLATED_POSTFIX, extension - )); - if maybe_icon_path.exists() { - return Some(maybe_icon_path); - } - - let maybe_icon_path = - icon_path.with_file_name(format!("{}{}.{}", filename, postfix, extension)); - if maybe_icon_path.exists() { - return Some(maybe_icon_path); - } - } +pub struct UwpManager; - // Some apps only adds one icon and without any postfix - // but we prefer the light/dark specific icon - if icon_path.exists() { - return Some(icon_path.to_path_buf()); - } +impl UwpManager { + pub fn manifest_from_package(package: &Package) -> Result { + let package_path = PathBuf::from(package.InstalledPath()?.to_os_string()); + let manifest_path = package_path.join("AppxManifest.xml"); - None - } + let file = std::fs::File::open(&manifest_path)?; + let mut reader = std::io::BufReader::new(file); - pub fn get_light_icon(&self, exe: &str) -> Option { - let app = self.get_app(exe)?; - - app.get_44_icon() - .and_then(|sub_path| Self::get_light_icon_path(&self.install_location.join(sub_path))) - .or_else(|| { - app.get_150_icon().and_then(|sub_path| { - Self::get_light_icon_path(&self.install_location.join(sub_path)) - }) - }) - .or_else(|| { - self.get_store_logo().and_then(|sub_path| { - Self::get_light_icon_path(&self.install_location.join(sub_path)) - }) - }) + Ok(quick_xml::de::from_reader(&mut reader)?) } - pub fn get_shell_path(&self, exe: &str) -> Option { - let app = self.get_app(exe)?; - Some(format!( - "shell:AppsFolder\\{}_{}!{}", - self.name, self.publisher_id, app.app_id - )) - } -} + pub fn get_high_quality_icon_path(app_umid: &str) -> Result { + let app_info = AppInfo::GetFromAppUserModelId(&app_umid.into())?; + let package = app_info.Package()?; + let manifest = Self::manifest_from_package(&package)?; -#[derive(Debug, Default)] -pub struct WindowsAppsManager { - packages: Vec, -} + let package_path = PathBuf::from(package.InstalledPath()?.to_os_string()); + let store_logo = package_path.join(&manifest.properties.logo); -impl WindowsAppsManager { - fn get_save_path() -> Result { - Ok(get_app_handle() - .path() - .app_data_dir()? - .join("uwp_manifests.json")) - } + // if package does't have the app but it is still part of the package then use the package logo + let app_manifest = match manifest.get_app(&app_info.Id()?.to_string_lossy()) { + Some(app) => app, + None => { + return get_hightest_quality_posible(&store_logo) + .ok_or("Could not find package logo path".into()) + } + }; - pub fn refresh(&mut self) -> Result<()> { - log::trace!("Loading UWP packages"); - trace_lock!(PERFORMANCE_HELPER).start("uwp"); - let script = PwshScript::new(include_str!("load_uwp_apps.ps1")); - let contents = tauri::async_runtime::block_on(script.execute())?; - self.packages = serde_json::from_str(&contents)?; - std::fs::write(Self::get_save_path()?, &contents)?; - log::trace!( - "UWP packages loaded in: {:.2}s", - trace_lock!(PERFORMANCE_HELPER).elapsed("uwp").as_secs_f64() - ); - Ok(()) - } + let app_logo_44 = package_path.join(&app_manifest.visual_elements.logo_44); + let app_logo_150 = package_path.join(&app_manifest.visual_elements.logo_150); - pub fn get_from_path(&self, exe_path: &Path) -> Option<&UWPPackage> { - let exe = exe_path.file_name()?.to_string_lossy().to_string(); - self.packages.iter().find(|p| { - exe_path.starts_with(&p.install_location) - && p.applications - .iter() - .any(|app| app.executable.ends_with(&exe) || app.alias.as_deref() == Some(&exe)) - }) + get_hightest_quality_posible(&app_logo_44) + .or_else(|| get_hightest_quality_posible(&app_logo_150)) + .or_else(|| get_hightest_quality_posible(&store_logo)) + .ok_or_else(|| format!("App icon not found for {app_umid}").into()) } } diff --git a/src/background/modules/virtual_desk/cli.rs b/src/background/modules/virtual_desk/cli.rs new file mode 100644 index 00000000..1725b543 --- /dev/null +++ b/src/background/modules/virtual_desk/cli.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use clap::Command; +use lazy_static::lazy_static; +use parking_lot::Mutex; + +use crate::{error_handler::Result, get_subcommands, windows_api::WindowsApi}; + +use super::VirtualDesktopManager; + +get_subcommands![ + /** Sends the window to the specified workspace */ + SendToWorkspace(index: usize => "The index of the workspace to switch to."), + /** Switches to the specified workspace. */ + SwitchWorkspace(index: usize => "The index of the workspace to switch to."), + /** Sends the window to the specified workspace and switches to it. */ + MoveToWorkspace(index: usize => "The index of the workspace to switch to."), + /** Switch to the next workspace */ + SwitchNext, + /** Switch to the previous workspace */ + SwitchPrev, +]; + +lazy_static! { + static ref LOCKER: Arc> = Arc::new(Mutex::new(())); +} + +impl VirtualDesktopManager { + pub const CLI_IDENTIFIER: &'static str = "virtual-desk"; + + pub fn get_cli() -> Command { + Command::new(Self::CLI_IDENTIFIER) + .about("Manage the Seelen Window Manager.") + .visible_alias("vd") + .arg_required_else_help(true) + .subcommands(SubCommand::commands()) + } + + pub fn process(&self, matches: &clap::ArgMatches) -> Result<()> { + // Lock for the duration of the process to avoid concurrent switching of workspaces + let _guard = LOCKER.lock(); + let subcommand = SubCommand::try_from(matches)?; + match subcommand { + SubCommand::SendToWorkspace(index) => { + self.send_to(index, WindowsApi::get_foreground_window().0 as isize)?; + } + SubCommand::SwitchWorkspace(index) => { + self.switch_to(index)?; + } + SubCommand::MoveToWorkspace(index) => { + self.send_to(index, WindowsApi::get_foreground_window().0 as isize)?; + std::thread::sleep(std::time::Duration::from_millis(20)); + self.switch_to(index)?; + } + _ => log::warn!("Unimplemented command: {:?}", subcommand), + } + Ok(()) + } +} diff --git a/src/background/modules/virtual_desk/mod.rs b/src/background/modules/virtual_desk/mod.rs index 282cf079..1d86175e 100644 --- a/src/background/modules/virtual_desk/mod.rs +++ b/src/background/modules/virtual_desk/mod.rs @@ -1,3 +1,4 @@ +mod cli; mod native; mod workspaces; @@ -10,7 +11,7 @@ use std::sync::Arc; use crate::{error_handler::Result, state::application::FULL_STATE}; lazy_static! { - static ref VIRTUAL_DESKTOP_MANAGER: Arc> = + pub static ref VIRTUAL_DESKTOP_MANAGER: Arc> = Arc::new(ArcSwap::from_pointee( match FULL_STATE.load().settings().virtual_desktop_strategy { VirtualDesktopStrategy::Native => @@ -202,6 +203,8 @@ pub enum VirtualDesktopEvent { old_index: usize, new_index: usize, }, + /// Emitted when a window is moved of the virtual desktop. + /// If using native implementation, it also will be triggered when the window is created/destroyed WindowChanged(isize), } diff --git a/src/background/modules/virtual_desk/native.rs b/src/background/modules/virtual_desk/native.rs index 9a2459cf..dc878336 100644 --- a/src/background/modules/virtual_desk/native.rs +++ b/src/background/modules/virtual_desk/native.rs @@ -63,7 +63,7 @@ impl VirtualDesktopManagerTrait for NativeVirtualDesktopManager { } fn get_by_window(&self, window: isize) -> Result { - Ok(winvd::get_desktop_by_window(HWND(window))?.into()) + Ok(winvd::get_desktop_by_window(HWND(window as _))?.into()) } fn get_all(&self) -> Result> { @@ -87,22 +87,22 @@ impl VirtualDesktopManagerTrait for NativeVirtualDesktopManager { } fn send_to(&self, idx: usize, hwnd: isize) -> Result<()> { - winvd::move_window_to_desktop(idx as u32, &HWND(hwnd))?; + winvd::move_window_to_desktop(idx as u32, &HWND(hwnd as _))?; Ok(()) } fn pin_window(&self, hwnd: isize) -> Result<()> { - winvd::pin_window(HWND(hwnd))?; + winvd::pin_window(HWND(hwnd as _))?; Ok(()) } fn unpin_window(&self, hwnd: isize) -> Result<()> { - winvd::unpin_window(HWND(hwnd))?; + winvd::unpin_window(HWND(hwnd as _))?; Ok(()) } fn is_pinned_window(&self, hwnd: isize) -> Result { - Ok(winvd::is_pinned_window(HWND(hwnd))?) + Ok(winvd::is_pinned_window(HWND(hwnd as _))?) } fn listen_events(&self, sender: std::sync::mpsc::Sender) -> Result<()> { @@ -143,7 +143,9 @@ impl From for VirtualDesktopEvent { old_index: old_index as usize, new_index: new_index as usize, }, - DesktopEvent::WindowChanged(window) => VirtualDesktopEvent::WindowChanged(window.0), + DesktopEvent::WindowChanged(window) => { + VirtualDesktopEvent::WindowChanged(window.0 as _) + } } } } diff --git a/src/background/modules/virtual_desk/workspaces.rs b/src/background/modules/virtual_desk/workspaces.rs deleted file mode 100644 index f62fff63..00000000 --- a/src/background/modules/virtual_desk/workspaces.rs +++ /dev/null @@ -1,328 +0,0 @@ -use std::{ - path::PathBuf, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, -}; - -use arc_swap::ArcSwap; -use parking_lot::{Mutex, MutexGuard}; -use serde::{Deserialize, Serialize}; - -use crate::{ - error_handler::{AppError, Result}, - hook::HOOK_MANAGER, - log_error, - seelen_weg::SeelenWeg, - trace_lock, - windows_api::{WindowEnumerator, WindowsApi}, - winevent::WinEvent, -}; - -use super::{VirtualDesktop, VirtualDesktopEvent, VirtualDesktopManagerTrait, VirtualDesktopTrait}; -use windows::Win32::Foundation::HWND; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SeelenWorkspace { - id: String, - name: Option, - wallpaper: Option, - #[serde(skip)] - windows: Vec, -} - -impl From<&SeelenWorkspace> for VirtualDesktop { - fn from(value: &SeelenWorkspace) -> Self { - VirtualDesktop::Seelen(value.clone()) - } -} - -impl From for VirtualDesktop { - fn from(value: SeelenWorkspace) -> Self { - VirtualDesktop::Seelen(value) - } -} - -impl VirtualDesktopTrait for SeelenWorkspace { - fn id(&self) -> String { - self.id.clone() - } - - fn name(&self) -> Option { - self.name.clone() - } -} - -impl SeelenWorkspace { - fn new() -> Self { - Self { - id: uuid::Uuid::new_v4().to_string(), - name: None, - wallpaper: None, - windows: Vec::new(), - } - } - - fn remove_window(&mut self, window: isize) -> Result<()> { - self.windows.retain(|w| *w != window); - WindowsApi::set_minimize_animation(false)?; - trace_lock!(HOOK_MANAGER).skip(WinEvent::SystemMinimizeStart, window); - WindowsApi::minimize_window(HWND(window))?; - WindowsApi::set_minimize_animation(true)?; - Ok(()) - } - - fn hide(&mut self) -> Result<()> { - WindowsApi::set_minimize_animation(false)?; - let mut hook_manager = trace_lock!(HOOK_MANAGER); - for window in &self.windows { - hook_manager.skip(WinEvent::SystemMinimizeStart, *window); - WindowsApi::minimize_window(HWND(*window))?; - } - WindowsApi::set_minimize_animation(true)?; - Ok(()) - } - - fn restore(&self) -> Result<()> { - WindowsApi::set_minimize_animation(false)?; - let mut hook_manager = trace_lock!(HOOK_MANAGER); - for window in &self.windows { - let hwnd = HWND(*window); - // if is switching by restored window on other workspace it will be already shown - if WindowsApi::is_iconic(hwnd) { - hook_manager.skip(WinEvent::SystemMinimizeEnd, hwnd.0); - WindowsApi::restore_window(hwnd)?; - } - } - WindowsApi::set_minimize_animation(true)?; - Ok(()) - } -} - -#[derive(Debug, Default)] -pub struct SeelenWorkspacesManager { - current: AtomicUsize, - sender: ArcSwap>>, - workspaces: Mutex>, - pinned: Mutex>, -} - -fn none_err() -> AppError { - "Seelen Workspace not found".into() -} - -impl SeelenWorkspacesManager { - pub fn new() -> Self { - let manager = Self { - current: AtomicUsize::new(0), - workspaces: Mutex::new(vec![SeelenWorkspace::new()]), - pinned: Mutex::new(Vec::new()), - sender: ArcSwap::new(Arc::new(None)), - }; - log_error!(manager.load()); - manager - } - - /// TODO: try to move windows on others native virtual desktops to only one. - fn load(&self) -> Result<()> { - let mut workspaces = self.workspaces(); - let workspace = workspaces.get_mut(self._current()).ok_or_else(none_err)?; - WindowEnumerator::new().for_each(|hwnd| { - if SeelenWeg::should_be_added(hwnd) && !WindowsApi::is_iconic(hwnd) { - workspace.windows.push(hwnd.0); - } - })?; - Ok(()) - } - - /// should be called on a thread to avoid deadlocks - pub fn on_win_event(&self, event: WinEvent, origin: HWND) -> Result<()> { - match event { - WinEvent::SystemMinimizeStart | WinEvent::ObjectDestroy | WinEvent::ObjectHide => { - let mut workspaces = self.workspaces(); - let workspace = workspaces.get_mut(self._current()).ok_or_else(none_err)?; - if workspace.windows.contains(&origin.0) { - log::trace!("removing window: {}", origin.0); - workspace.windows.retain(|w| *w != origin.0); - } - } - WinEvent::SystemMinimizeEnd => { - let owner_idx = { - let workspaces = self.workspaces(); - workspaces - .iter() - .position(|w| w.windows.contains(&origin.0)) - }; - if let Some(owner_idx) = owner_idx { - self.switch_to(owner_idx)?; - } else if SeelenWeg::should_be_added(origin) { - log::trace!("adding window to workspace: {}", origin.0); - let mut workspaces = self.workspaces(); - let workspace = workspaces.get_mut(self._current()).ok_or_else(none_err)?; - workspace.windows.push(origin.0); - } - } - WinEvent::ObjectCreate | WinEvent::ObjectShow => { - if SeelenWeg::should_be_added(origin) { - log::trace!("adding window to workspace: {}", origin.0); - let mut workspaces = self.workspaces(); - let workspace = workspaces.get_mut(self._current()).ok_or_else(none_err)?; - workspace.windows.push(origin.0); - } - } - _ => {} - } - Ok(()) - } - - fn _current(&self) -> usize { - self.current.load(Ordering::Relaxed) - } - - fn workspaces(&self) -> MutexGuard<'_, Vec> { - trace_lock!(self.workspaces) - } - - fn pinned(&self) -> MutexGuard<'_, Vec> { - trace_lock!(self.pinned) - } - - fn emit(&self, event: VirtualDesktopEvent) -> Result<()> { - if let Some(sender) = self.sender.load().as_ref() { - sender.send(event).map_err(|_| "Failed to send event")?; - } - Ok(()) - } - - fn create_many_desktop(&self, count: usize) -> Result<()> { - log::trace!("Creating {} seelen workspaces", count); - for _ in 0..count { - self.create_desktop()?; - } - Ok(()) - } -} - -impl VirtualDesktopManagerTrait for SeelenWorkspacesManager { - fn uses_cloak(&self) -> bool { - false - } - - fn create_desktop(&self) -> Result<()> { - log::trace!("Creating new seelen workspace"); - let desk = SeelenWorkspace::new(); - self.workspaces().push(desk.clone()); - self.emit(VirtualDesktopEvent::DesktopCreated(desk.into()))?; - Ok(()) - } - - fn get(&self, idx: usize) -> Result> { - Ok(self.workspaces().get(idx).map(Into::into)) - } - - fn get_by_window(&self, window: isize) -> Result { - if self.is_pinned_window(window)? { - return self.get_current(); - } - let vd = { - self.workspaces() - .iter() - .find(|w| w.windows.contains(&window)) - .map(Into::into) - }; - vd.or_else(|| self.get_current().ok()).ok_or_else(none_err) - } - - fn get_all(&self) -> Result> { - Ok(self.workspaces().iter().map(Into::into).collect()) - } - - fn get_current(&self) -> Result { - Ok(self - .workspaces() - .get(self._current()) - .ok_or_else(none_err)? - .into()) - } - - fn get_current_idx(&self) -> Result { - Ok(self._current()) - } - - fn switch_to(&self, idx: usize) -> Result<()> { - { - let len = self.workspaces().len(); - if idx >= len { - // temporal until implement a UI to create seelen workspaces - self.create_many_desktop((idx + 1) - len)?; - // return Err("Tried to switch to non-existent workspace".into()); - } - } - - let mut workspaces = self.workspaces(); - let old = { - let old = workspaces.get_mut(self._current()).ok_or_else(none_err)?; - old.hide()?; - old.clone() - }; - - self.current.store(idx, Ordering::SeqCst); - - let new = workspaces.get(self._current()).ok_or_else(none_err)?; - new.restore()?; - - self.emit(VirtualDesktopEvent::DesktopChanged { - new: new.into(), - old: old.into(), - })?; - Ok(()) - } - - fn send_to(&self, idx: usize, window: isize) -> Result<()> { - { - let len = self.workspaces().len(); - if idx >= len { - // temporal until implement a UI to create seelen workspaces - self.create_many_desktop((idx + 1) - len)?; - // return Err("Tried to switch to non-existent workspace".into()); - } - } - let mut workspaces = self.workspaces(); - { - let old_idx = workspaces - .iter() - .position(|w| w.windows.contains(&window)) - .ok_or_else(none_err)?; - let old = workspaces.get_mut(old_idx).ok_or_else(none_err)?; - old.remove_window(window)?; - } - { - let new = workspaces.get_mut(idx).ok_or_else(none_err)?; - new.windows.push(window); - } - self.emit(VirtualDesktopEvent::WindowChanged(window))?; - Ok(()) - } - - fn pin_window(&self, hwnd: isize) -> Result<()> { - let mut pinned = self.pinned(); - if !pinned.contains(&hwnd) { - pinned.push(hwnd); - } - Ok(()) - } - - fn unpin_window(&self, hwnd: isize) -> Result<()> { - self.pinned().retain(|w| *w != hwnd); - Ok(()) - } - - fn is_pinned_window(&self, hwnd: isize) -> Result { - Ok(self.pinned().contains(&hwnd)) - } - - fn listen_events(&self, sender: std::sync::mpsc::Sender) -> Result<()> { - self.sender.store(Arc::new(Some(sender))); - Ok(()) - } -} diff --git a/src/background/modules/virtual_desk/workspaces/hook.rs b/src/background/modules/virtual_desk/workspaces/hook.rs new file mode 100644 index 00000000..0a4037af --- /dev/null +++ b/src/background/modules/virtual_desk/workspaces/hook.rs @@ -0,0 +1,99 @@ +use crate::{ + error_handler::Result, + log_error, + modules::virtual_desk::get_vd_manager, + seelen_weg::SeelenWeg, + trace_lock, + windows_api::{window::Window, WindowEnumerator}, + winevent::WinEvent, +}; + +use super::SeelenWorkspacesManager; + +impl SeelenWorkspacesManager { + fn should_be_added(window: &Window) -> bool { + SeelenWeg::should_be_added(window.hwnd()) && !window.is_minimized() + } + + fn contains(&self, window: &Window) -> bool { + trace_lock!(self.workspaces) + .iter() + .any(|w| w.windows.contains(&window.address())) + } + + fn add(&self, window: &Window) { + if let Some(workspace) = trace_lock!(self.workspaces).get_mut(self.current_idx()) { + log::trace!("adding window to workspace: {}", window.address()); + workspace.windows.push(window.address()); + }; + } + + /// TODO: try to move windows on others native virtual desktops to only one. + pub fn enumerate(&self) -> Result<()> { + WindowEnumerator::new() + .map(|hwnd| hwnd)? + .into_iter() + .rev() + .for_each(|hwnd| { + let window = Window::from(hwnd); + if Self::should_be_added(&window) { + self.add(&window); + } + }); + Ok(()) + } + + pub fn on_win_event(&self, event: WinEvent, window: &Window) -> Result<()> { + let addr = window.address(); + match event { + WinEvent::ObjectCreate | WinEvent::ObjectShow => { + if Self::should_be_added(window) && !self.contains(window) { + self.add(window); + } + } + WinEvent::SystemMinimizeEnd => { + let owner_idx = trace_lock!(self.workspaces) + .iter() + .position(|w| w.windows.contains(&addr)); + + if let Some(owner_idx) = owner_idx { + std::thread::spawn(move || log_error!(get_vd_manager().switch_to(owner_idx))); + } else if !self.contains(window) && Self::should_be_added(window) { + self.add(window); + } + } + WinEvent::ObjectDestroy | WinEvent::ObjectHide | WinEvent::SystemMinimizeStart => { + if let Some(workspace) = trace_lock!(self.workspaces).get_mut(self.current_idx()) { + if workspace.windows.contains(&addr) { + log::trace!("removing window: {}", addr); + workspace.windows.retain(|w| *w != addr); + } + } + } + WinEvent::SystemForeground | WinEvent::ObjectFocus => { + for w in trace_lock!(self.workspaces).iter_mut() { + if w.windows.contains(&addr) { + w.windows.retain(|w| *w != addr); + w.windows.push(addr); + break; + } + } + } + WinEvent::ObjectParentChange => { + if let Some(parent) = window.parent() { + if self.contains(window) { + trace_lock!(self.workspaces) + .iter_mut() + .for_each(|w| w.windows.retain(|w| *w != addr)); + } + + if !self.contains(&parent) && Self::should_be_added(&parent) { + self.add(&parent); + } + } + } + _ => {} + } + Ok(()) + } +} diff --git a/src/background/modules/virtual_desk/workspaces/mod.rs b/src/background/modules/virtual_desk/workspaces/mod.rs new file mode 100644 index 00000000..e98c787f --- /dev/null +++ b/src/background/modules/virtual_desk/workspaces/mod.rs @@ -0,0 +1,291 @@ +mod hook; + +use std::{ + path::PathBuf, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +use arc_swap::ArcSwap; +use parking_lot::{Mutex, MutexGuard}; +use serde::{Deserialize, Serialize}; + +use crate::{ + error_handler::{AppError, Result}, + hook::HookManager, + log_error, trace_lock, + windows_api::WindowsApi, + winevent::WinEvent, +}; + +use super::{VirtualDesktop, VirtualDesktopEvent, VirtualDesktopManagerTrait, VirtualDesktopTrait}; +use windows::Win32::{ + Foundation::HWND, + UI::WindowsAndMessaging::{SW_FORCEMINIMIZE, SW_MINIMIZE, SW_RESTORE}, +}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SeelenWorkspace { + id: String, + name: Option, + wallpaper: Option, + #[serde(skip)] + windows: Vec, +} + +impl From<&SeelenWorkspace> for VirtualDesktop { + fn from(value: &SeelenWorkspace) -> Self { + VirtualDesktop::Seelen(value.clone()) + } +} + +impl From for VirtualDesktop { + fn from(value: SeelenWorkspace) -> Self { + VirtualDesktop::Seelen(value) + } +} + +impl VirtualDesktopTrait for SeelenWorkspace { + fn id(&self) -> String { + self.id.clone() + } + + fn name(&self) -> Option { + self.name.clone() + } +} + +impl SeelenWorkspace { + fn new() -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + name: None, + wallpaper: None, + windows: Vec::new(), + } + } + + fn remove_window(&mut self, window: isize) { + self.windows.retain(|w| *w != window); + HookManager::run_with_async(move |hook_manager| { + hook_manager.skip(WinEvent::SystemMinimizeStart, HWND(window as _)); + log_error!(WindowsApi::show_window_async( + HWND(window as _), + SW_FORCEMINIMIZE + )) + }); + } + + fn hide(&self) { + let win_address = self.windows.clone(); + HookManager::run_with_async(move |hook_manager| { + for addr in win_address { + let hwnd = HWND(addr as _); + hook_manager.skip(WinEvent::SystemMinimizeStart, hwnd); + log_error!(WindowsApi::show_window_async(hwnd, SW_MINIMIZE)); + } + }); + } + + fn restore(&self) { + let win_address = self.windows.clone(); + HookManager::run_with_async(move |hook_manager| { + for addr in win_address { + let hwnd = HWND(addr as _); + // if is switching by restored window on other workspace it will be already shown + if WindowsApi::is_window(hwnd) && WindowsApi::is_iconic(hwnd) { + hook_manager.skip(WinEvent::SystemMinimizeEnd, hwnd); + // show_window_async will restore the windows unsorted so we use sync show here + log_error!(WindowsApi::show_window(hwnd, SW_RESTORE)); + } + } + }); + } +} + +#[derive(Debug, Default)] +pub struct SeelenWorkspacesManager { + current: AtomicUsize, + sender: ArcSwap>>, + workspaces: Mutex>, + pinned: Mutex>, +} + +pub fn none_err() -> AppError { + "Seelen Workspace not found".into() +} + +impl SeelenWorkspacesManager { + pub fn new() -> Self { + let manager = Self { + current: AtomicUsize::new(0), + workspaces: Mutex::new(vec![SeelenWorkspace::new()]), + pinned: Mutex::new(Vec::new()), + sender: ArcSwap::new(Arc::new(None)), + }; + log_error!(manager.enumerate()); + manager + } + + fn current_idx(&self) -> usize { + self.current.load(Ordering::Relaxed) + } + + fn pinned(&self) -> MutexGuard<'_, Vec> { + trace_lock!(self.pinned) + } + + fn emit(&self, event: VirtualDesktopEvent) -> Result<()> { + let sender = self.sender.load_full(); + std::thread::spawn(move || { + if let Some(sender) = sender.as_ref() { + log_error!(sender.send(event)); + } + }); + Ok(()) + } + + fn create_many_desktop(&self, count: usize) -> Result<()> { + log::trace!("Creating {} seelen workspaces", count); + for _ in 0..count { + self.create_desktop()?; + } + Ok(()) + } +} + +impl VirtualDesktopManagerTrait for SeelenWorkspacesManager { + fn uses_cloak(&self) -> bool { + false + } + + fn create_desktop(&self) -> Result<()> { + log::trace!("Creating new seelen workspace"); + let desk = SeelenWorkspace::new(); + trace_lock!(self.workspaces).push(desk.clone()); + self.emit(VirtualDesktopEvent::DesktopCreated(desk.into()))?; + Ok(()) + } + + fn get(&self, idx: usize) -> Result> { + if let Some(workspace) = trace_lock!(self.workspaces).get_mut(idx) { + return Ok(Some(workspace.clone().into())); + } + Ok(None) + } + + fn get_by_window(&self, window: isize) -> Result { + if self.is_pinned_window(window)? { + return self.get_current(); + } + let desk = { + trace_lock!(self.workspaces) + .iter() + .find(|w| w.windows.contains(&window)) + .map(Into::into) + }; + desk.or_else(|| self.get_current().ok()) + .ok_or_else(none_err) + } + + fn get_all(&self) -> Result> { + Ok(trace_lock!(self.workspaces) + .iter() + .map(Into::into) + .collect()) + } + + fn get_current(&self) -> Result { + if let Some(workspace) = trace_lock!(self.workspaces).get_mut(self.current_idx()) { + return Ok(workspace.clone().into()); + } + Err(none_err()) + } + + fn get_current_idx(&self) -> Result { + Ok(self.current_idx()) + } + + fn switch_to(&self, idx: usize) -> Result<()> { + if idx == self.current_idx() { + return Ok(()); + } + + let len = trace_lock!(self.workspaces).len(); + if idx >= len { + // temporal until implement a UI to create seelen workspaces + self.create_many_desktop((idx + 1) - len)?; + // return Err("Tried to switch to non-existent workspace".into()); + } + + let workspaces = trace_lock!(self.workspaces); + let old = workspaces.get(self.current_idx()).ok_or_else(none_err)?; + self.current.store(idx, Ordering::SeqCst); + let new = workspaces.get(self.current_idx()).ok_or_else(none_err)?; + + old.hide(); + self.emit(VirtualDesktopEvent::DesktopChanged { + new: new.into(), + old: old.into(), + })?; + new.restore(); + Ok(()) + } + + fn send_to(&self, idx: usize, window: isize) -> Result<()> { + let len = trace_lock!(self.workspaces).len(); + if idx >= len { + // temporal until implement a UI to create seelen workspaces + self.create_many_desktop((idx + 1) - len)?; + // return Err("Tried to switch to non-existent workspace".into()); + } + + let mut workspaces = trace_lock!(self.workspaces); + + let old_idx = match workspaces.iter().position(|w| w.windows.contains(&window)) { + Some(idx) => idx, + None => return Ok(()), + }; + + if old_idx == idx { + return Ok(()); + } + + { + let old = workspaces.get_mut(old_idx).ok_or_else(none_err)?; + old.remove_window(window); + } + { + let new = workspaces.get_mut(idx).ok_or_else(none_err)?; + new.windows.push(window); + if self.current_idx() == idx { + new.restore(); + } + } + + self.emit(VirtualDesktopEvent::WindowChanged(window)) + } + + fn pin_window(&self, hwnd: isize) -> Result<()> { + let mut pinned = self.pinned(); + if !pinned.contains(&hwnd) { + pinned.push(hwnd); + } + Ok(()) + } + + fn unpin_window(&self, hwnd: isize) -> Result<()> { + self.pinned().retain(|w| *w != hwnd); + Ok(()) + } + + fn is_pinned_window(&self, hwnd: isize) -> Result { + Ok(self.pinned().contains(&hwnd)) + } + + fn listen_events(&self, sender: std::sync::mpsc::Sender) -> Result<()> { + self.sender.store(Arc::new(Some(sender))); + Ok(()) + } +} diff --git a/src/background/monitor.rs b/src/background/monitor.rs deleted file mode 100644 index 19758d20..00000000 --- a/src/background/monitor.rs +++ /dev/null @@ -1,132 +0,0 @@ -use color_eyre::eyre::eyre; -use getset::{Getters, MutGetters}; - -use crate::{ - error_handler::Result, log_error, seelen_bar::FancyToolbar, seelen_weg::SeelenWeg, - seelen_wm::WindowManager, state::application::FullState, utils::sleep_millis, - windows_api::WindowsApi, -}; - -use windows::Win32::Graphics::Gdi::HMONITOR; - -#[derive(Getters, MutGetters)] -#[getset(get = "pub", get_mut = "pub")] -pub struct Monitor { - handle: HMONITOR, - name: String, - toolbar: Option, - weg: Option, - wm: Option, -} - -impl Monitor { - pub fn update_handle(&mut self, id: HMONITOR) { - self.handle = id; - log_error!(self.ensure_positions()); - } - - pub fn ensure_positions(&mut self) -> Result<()> { - if let Some(bar) = &mut self.toolbar { - bar.cached_monitor = self.handle; - bar.set_positions(self.handle.0)?; - } - if let Some(weg) = &mut self.weg { - weg.set_positions(self.handle.0)?; - } - Ok(()) - } - - fn add_toolbar(&mut self) -> Result<()> { - if self.toolbar.is_none() { - // Tauri can fail the on creation of the first window, thats's why we only should retry - // for the first window created, the next windows should work normally. - // Update(08/13/2024): I think this can be removed on recent tauri versions - for attempt in 1..4 { - match FancyToolbar::new(&self.name) { - Ok(bar) => { - self.toolbar = Some(bar); - break; - } - Err(e) => { - log::error!("Failed to create Toolbar (attempt {}): {}", attempt, e); - sleep_millis(30); - } - } - } - } - Ok(()) - } - - fn add_weg(&mut self) -> Result<()> { - if self.weg.is_none() { - self.weg = Some(SeelenWeg::new(&self.name)?) - } - Ok(()) - } - - fn add_wm(&mut self) -> Result<()> { - if self.wm.is_none() { - self.wm = Some(WindowManager::new(self.handle.0)?) - } - Ok(()) - } - - pub fn load_settings(&mut self, settings: &FullState) -> Result<()> { - if settings.is_bar_enabled() { - self.add_toolbar()?; - } else { - self.toolbar = None; - } - - if settings.is_weg_enabled() { - self.add_weg()?; - } else { - self.weg = None; - } - - if settings.is_window_manager_enabled() && self.handle == WindowsApi::primary_monitor() { - self.add_wm()?; - } else { - self.wm = None; - } - - self.ensure_positions()?; - Ok(()) - } - - pub fn new(hmonitor: HMONITOR, settings: &FullState) -> Result { - if hmonitor.is_invalid() { - return Err(eyre!("Invalid Monitor").into()); - } - let mut monitor = Self { - handle: hmonitor, - name: WindowsApi::monitor_name(hmonitor)?, - toolbar: None, - weg: None, - wm: None, - }; - monitor.load_settings(settings)?; - Ok(monitor) - } - - pub fn is_focused(&self) -> bool { - let hwnd = WindowsApi::get_foreground_window(); - self.handle == WindowsApi::monitor_from_window(hwnd) - } - - pub fn is_ready(&self) -> bool { - if let Some(weg) = &self.weg { - if !weg.ready() { - return false; - } - } - - if let Some(wm) = &self.wm { - if !wm.ready() { - return false; - } - } - - true - } -} diff --git a/src/background/plugins.rs b/src/background/plugins.rs index e87128e1..b045cdc1 100644 --- a/src/background/plugins.rs +++ b/src/background/plugins.rs @@ -1,63 +1,72 @@ -use color_eyre::owo_colors::OwoColorize; -use tauri::{Builder, Wry}; -use tauri_plugin_autostart::MacosLauncher; -use tauri_plugin_log::{Target, TargetKind}; - -pub fn register_plugins(app_builder: Builder) -> Builder { - let mut log_plugin_builder = tauri_plugin_log::Builder::new() - .targets([ - Target::new(TargetKind::Stdout), - Target::new(TargetKind::LogDir { file_name: None }), - Target::new(TargetKind::Webview), - ]) - .level_for("tao", log::LevelFilter::Off) - .level_for("os_info", log::LevelFilter::Off) - .level_for("notify", log::LevelFilter::Off) - .level_for("notify_debouncer_full", log::LevelFilter::Off); - - if tauri::is_dev() { - log_plugin_builder = log_plugin_builder.format(move |out, message, record| { - out.finish(format_args!( - "[{}][{}] {}", - match record.level() { - log::Level::Trace => "TRACE".bright_black().to_string(), - log::Level::Info => "INFO~".bright_blue().to_string(), - log::Level::Warn => "WARN~".yellow().to_string(), - log::Level::Error => "ERROR".red().to_string(), - log::Level::Debug => "DEBUG".bright_green().to_string(), - }, - if record.level() == log::Level::Error { - record - .file() - .map(|file| { - format!( - "{}:{}", - file.replace("\\", "/"), - record.line().unwrap_or_default() - ) - }) - .unwrap_or_else(|| record.target().to_owned()) - .bright_red() - .to_string() - } else { - record.target().bright_black().to_string() - }, - message - )) - }); - } - - app_builder - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_autostart::init( - MacosLauncher::LaunchAgent, - Some(vec!["--silent"]), - )) - .plugin(log_plugin_builder.build()) - .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_http::init()) -} +use tauri::{Builder, Wry}; +use tauri_plugin_autostart::MacosLauncher; +use tauri_plugin_log::{Target, TargetKind}; + +pub fn register_plugins(app_builder: Builder) -> Builder { + let log_plugin_builder = tauri_plugin_log::Builder::new() + .targets([ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::LogDir { file_name: None }), + Target::new(TargetKind::Webview), + ]) + .level_for("tao", log::LevelFilter::Off) + .level_for("os_info", log::LevelFilter::Off) + .level_for("notify", log::LevelFilter::Off) + .level_for("notify_debouncer_full", log::LevelFilter::Off); + + let log_plugin = { + #[cfg(not(dev))] + { + log_plugin_builder.build() + } + #[cfg(dev)] + { + use owo_colors::OwoColorize; + log_plugin_builder + .format(move |out, message, record| { + out.finish(format_args!( + "[{}][{}] {}", + match record.level() { + log::Level::Trace => "TRACE".bright_black().to_string(), + log::Level::Info => "INFO~".bright_blue().to_string(), + log::Level::Warn => "WARN~".yellow().to_string(), + log::Level::Error => "ERROR".red().to_string(), + log::Level::Debug => "DEBUG".bright_green().to_string(), + }, + if record.level() == log::Level::Error { + record + .file() + .map(|file| { + format!( + "{}:{}", + file.replace("\\", "/"), + record.line().unwrap_or_default() + ) + }) + .unwrap_or_else(|| record.target().to_owned()) + .bright_red() + .to_string() + } else { + record.target().bright_black().to_string() + }, + message + )) + }) + .build() + } + }; + + app_builder + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_autostart::init( + MacosLauncher::LaunchAgent, + Some(vec!["--silent"]), + )) + .plugin(log_plugin) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_http::init()) +} diff --git a/src/background/schedule.ps1 b/src/background/schedule.ps1 index e7fe5a8c..9143da7c 100644 --- a/src/background/schedule.ps1 +++ b/src/background/schedule.ps1 @@ -1,35 +1,35 @@ -param ( - [string]$ExeRoute, - [string]$Enabled -) - -$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -if (-not $isAdmin) { - $ownRoute = $MyInvocation.MyCommand.Definition - $arguments = @( - "-NoProfile" - "-ExecutionPolicy Bypass" - "-File `"$ownRoute`"" - "-ExeRoute `"$ExeRoute`"" - "-Enabled `"$Enabled`"" - ) - Start-Process powershell -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden - Exit -} - -$taskName = "Seelen-UI" -$taskPath = "\Seelen\$taskName" - -if ($Enabled -eq "true") { - $action = New-ScheduledTaskAction -Execute "$ExeRoute" -Argument "--silent" - $trigger = New-ScheduledTaskTrigger -AtLogon - $settings = New-ScheduledTaskSettingsSet -Priority 2 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden - - Register-ScheduledTask -Force -Action $action -Trigger $trigger -Settings $settings -TaskName $taskPath -User $env:USERNAME -RunLevel Highest -} -else { - $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue - if ($null -ne $existingTask) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false - } -} +param ( + [string]$ExeRoute, + [string]$Enabled +) + +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin) { + $ownRoute = $MyInvocation.MyCommand.Definition + $arguments = @( + "-NoProfile" + "-ExecutionPolicy Bypass" + "-File `"$ownRoute`"" + "-ExeRoute `"$ExeRoute`"" + "-Enabled `"$Enabled`"" + ) + Start-Process powershell -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden + Exit +} + +$taskName = "Seelen-UI" +$taskPath = "\Seelen\$taskName" + +if ($Enabled -eq "true") { + $action = New-ScheduledTaskAction -Execute "$ExeRoute" -Argument "--silent" + $trigger = New-ScheduledTaskTrigger -AtLogon + $settings = New-ScheduledTaskSettingsSet -Priority 4 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden + + Register-ScheduledTask -Force -Action $action -Trigger $trigger -Settings $settings -TaskName $taskPath -User $env:USERNAME -RunLevel Highest +} +else { + $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + if ($null -ne $existingTask) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false + } +} diff --git a/src/background/seelen.rs b/src/background/seelen.rs index 6e2484ae..a817c71b 100644 --- a/src/background/seelen.rs +++ b/src/background/seelen.rs @@ -1,409 +1,428 @@ -use std::{env::temp_dir, sync::Arc}; - -use arc_swap::ArcSwap; -use getset::{Getters, MutGetters}; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use tauri::{path::BaseDirectory, AppHandle, Manager, Wry}; -use tauri_plugin_shell::ShellExt; -use windows::Win32::Graphics::Gdi::HMONITOR; - -use crate::{ - error_handler::Result, - hook::register_win_hook, - log_error, - modules::monitors::{MonitorManagerEvent, MONITOR_MANAGER}, - monitor::Monitor, - seelen_weg::SeelenWeg, - state::application::{FullState, FULL_STATE}, - system::{declare_system_events_handlers, release_system_events_handlers}, - trace_lock, - utils::{ahk::AutoHotKey, sleep_millis, spawn_named_thread, PERFORMANCE_HELPER}, - windows_api::{WindowEnumerator, WindowsApi}, -}; - -lazy_static! { - pub static ref SEELEN: Arc> = Arc::new(Mutex::new(Seelen::default())); - pub static ref APP_HANDLE: Arc>>> = Arc::new(Mutex::new(None)); -} - -pub fn get_app_handle() -> AppHandle { - APP_HANDLE - .lock() - .clone() - .expect("get_app_handle called but app is still not initialized") -} - -/** Struct should be initialized first before calling any other methods */ -#[derive(Getters, MutGetters, Default)] -pub struct Seelen { - handle: Option>, - #[getset(get = "pub", get_mut = "pub")] - monitors: Vec, - state: Option>>, -} - -/* ============== Getters ============== */ -impl Seelen { - /** Ensure Seelen is initialized first before calling */ - pub fn handle(&self) -> &AppHandle { - self.handle.as_ref().unwrap() - } - - pub fn focused_monitor(&self) -> Option<&Monitor> { - self.monitors.iter().find(|m| m.is_focused()) - } - - pub fn focused_monitor_mut(&mut self) -> Option<&mut Monitor> { - self.monitors.iter_mut().find(|m| m.is_focused()) - } - - pub fn monitor_by_id_mut(&mut self, id: isize) -> Option<&mut Monitor> { - self.monitors.iter_mut().find(|m| m.handle().0 == id) - } - - pub fn monitor_by_name_mut(&mut self, name: &str) -> Option<&mut Monitor> { - self.monitors.iter_mut().find(|m| m.name() == name) - } - - pub fn state(&self) -> Arc { - self.state - .as_ref() - .expect("Seelen State not initialized") - .load_full() - } -} - -/* ============== Methods ============== */ -impl Seelen { - pub fn on_state_changed(&mut self) -> Result<()> { - let state = self.state(); - - if state.is_ahk_enabled() { - Self::start_ahk_shortcuts()?; - } else { - Self::kill_ahk_shortcuts()?; - } - - if state.is_weg_enabled() { - SeelenWeg::hide_taskbar(); - } else { - SeelenWeg::restore_taskbar()?; - } - - for monitor in &mut self.monitors { - monitor.load_settings(&state)?; - } - Ok(()) - } - - pub fn init(&mut self, app: AppHandle) -> Result<()> { - Self::ensure_folders(&app)?; - log::trace!("Initializing Seelen"); - - *APP_HANDLE.lock() = Some(app.clone()); - self.handle = Some(app.clone()); - self.state = Some(Arc::clone(&FULL_STATE)); - Ok(()) - } - - fn on_monitor_event(event: MonitorManagerEvent) { - log::trace!("Monitor event: {:?}", event); - let mut seelen = trace_lock!(SEELEN); - match event { - MonitorManagerEvent::Added(_name, id) => { - log_error!(seelen.add_monitor(id)); - } - MonitorManagerEvent::Removed(_name, id) => { - log_error!(seelen.remove_monitor(id)); - } - MonitorManagerEvent::Updated(name, id) => { - if let Some(m) = seelen.monitor_by_name_mut(&name) { - m.update_handle(id); - } - } - } - } - - fn start_async() -> Result<()> { - log_error!(Self::start_ahk_shortcuts()); - - let mut all_ready = false; - while !all_ready { - sleep_millis(50); - all_ready = trace_lock!(SEELEN).monitors().iter().all(|m| m.is_ready()); - } - - log::debug!( - "Seelen UI ready in: {:.2}s", - trace_lock!(PERFORMANCE_HELPER) - .elapsed("init") - .as_secs_f64() - ); - - log::trace!("Enumerating windows"); - WindowEnumerator::new().for_each(|hwnd| { - let mut seelen = trace_lock!(SEELEN); - - if SeelenWeg::should_be_added(hwnd) { - SeelenWeg::add_hwnd(hwnd); - } - - for monitor in seelen.monitors_mut() { - if let Some(wm) = monitor.wm_mut() { - if wm.should_be_added(hwnd) { - log_error!(wm.add_hwnd(hwnd)); - } - } - } - })?; - - register_win_hook()?; - Ok(()) - } - - pub fn start(&mut self) -> Result<()> { - declare_system_events_handlers()?; - - if self.state().is_weg_enabled() { - SeelenWeg::hide_taskbar(); - } - - log::trace!("Enumerating Monitors"); - let monitors = trace_lock!(MONITOR_MANAGER).monitors.clone(); - for (_name, id) in monitors { - log_error!(self.add_monitor(id)); - } - trace_lock!(MONITOR_MANAGER).listen_changes(Self::on_monitor_event); - - spawn_named_thread("Start Async", || log_error!(Self::start_async()))?; - tauri::async_runtime::spawn(async { - log_error!(Self::refresh_auto_start_path().await); - }); - Ok(()) - } - - /// Stop and release all resources - pub fn stop(&self) { - release_system_events_handlers(); - if self.state().is_weg_enabled() { - log_error!(SeelenWeg::restore_taskbar()); - } - if self.state().is_ahk_enabled() { - log_error!(Self::kill_ahk_shortcuts()); - } - } - - fn add_monitor(&mut self, hmonitor: HMONITOR) -> Result<()> { - self.monitors.push(Monitor::new(hmonitor, &self.state())?); - Ok(()) - } - - fn remove_monitor(&mut self, hmonitor: HMONITOR) -> Result<()> { - self.monitors.retain(|m| m.handle() != &hmonitor); - Ok(()) - } - - fn ensure_folders(handle: &AppHandle) -> Result<()> { - log::trace!("Ensuring folders"); - let path = handle.path(); - let data_path = path.app_data_dir()?; - - // migration of user settings files below v1.8.3 - let old_path = path.resolve(".config/seelen", BaseDirectory::Home)?; - if old_path.exists() { - log::trace!("Migrating user settings from {:?}", old_path); - for entry in std::fs::read_dir(&old_path)?.flatten() { - if entry.file_type()?.is_dir() { - continue; - } - std::fs::copy(entry.path(), data_path.join(entry.file_name()))?; - } - std::fs::remove_dir_all(&old_path)?; - } - - let create_if_needed = move |folder: &str| -> Result<()> { - let path = data_path.join(folder); - if !path.exists() { - std::fs::create_dir_all(path)?; - } - Ok(()) - }; - - create_if_needed("placeholders")?; - create_if_needed("themes")?; - create_if_needed("layouts")?; - create_if_needed("icons")?; - create_if_needed("wallpapers")?; - - Ok(()) - } - - pub async fn is_auto_start_enabled() -> Result { - let handle = get_app_handle(); - let output = handle - .shell() - .command("powershell") - .args([ - "-ExecutionPolicy", - "Bypass", - "-NoProfile", - "-Command", - "[bool](Get-ScheduledTask -TaskName Seelen-UI -ErrorAction SilentlyContinue)", - ]) - .output() - .await?; - - let (cow, _used, _has_errors) = encoding_rs::GBK.decode(&output.stdout); - let stdout = cow.to_string().trim().to_lowercase(); - Ok(stdout == "true") - } - - /// override auto-start task in case of location change, normally this happen on MSIX update - async fn refresh_auto_start_path() -> Result<()> { - if WindowsApi::is_elevated()? && Self::is_auto_start_enabled().await? { - Self::set_auto_start(true).await?; - } - Ok(()) - } - - pub async fn set_auto_start(enabled: bool) -> Result<()> { - let pwsh_script = include_str!("schedule.ps1"); - let pwsh_script_path = temp_dir().join("schedule.ps1"); - std::fs::write(&pwsh_script_path, pwsh_script).expect("Failed to write temp script file"); - - let exe_path = std::env::current_exe()?; - - let output = get_app_handle() - .shell() - .command("powershell") - .args([ - "-ExecutionPolicy", - "Bypass", - "-NoProfile", - "-File", - &pwsh_script_path.to_string_lossy(), - "-ExeRoute", - &exe_path.to_string_lossy(), - "-Enabled", - if enabled { "true" } else { "false" }, - ]) - .output() - .await?; - - if !output.status.success() { - return Err(output.into()); - } - Ok(()) - } - - pub fn start_ahk_shortcuts() -> Result<()> { - // kill all running shortcuts before starting again - Self::kill_ahk_shortcuts()?; - - let state = FULL_STATE.load(); - if state.is_ahk_enabled() { - log::trace!("Creating AHK shortcuts"); - - AutoHotKey::new(include_str!("utils/ahk/mocks/seelen.lib.ahk")) - .name("seelen.lib.ahk") - .save()?; - - AutoHotKey::new(include_str!("utils/ahk/mocks/seelen.ahk")) - .name("seelen.ahk") - .execute()?; - - if state.is_window_manager_enabled() { - AutoHotKey::from_template( - include_str!("utils/ahk/mocks/seelen.wm.ahk"), - state.get_ahk_variables(), - ) - .name("seelen.wm.ahk") - .execute()?; - } - } - log::trace!("AHK shortcuts started successfully"); - Ok(()) - } - - pub fn kill_ahk_shortcuts() -> Result<()> { - log::trace!("Killing AHK shortcuts"); - get_app_handle() - .shell() - .command("powershell") - .args([ - "-ExecutionPolicy", - "Bypass", - "-NoProfile", - "-Command", - r"Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like '*static\redis\AutoHotkey.exe*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }", - ]) - .spawn()?; - Ok(()) - } - - pub fn show_settings() -> Result<()> { - log::trace!("Show settings window"); - let handle = get_app_handle(); - let window = handle.get_webview_window("settings").or_else(|| { - tauri::WebviewWindowBuilder::new( - &handle, - "settings", - tauri::WebviewUrl::App("settings/index.html".into()), - ) - .title("Settings") - .inner_size(720.0, 480.0) - .maximizable(false) - .minimizable(true) - .resizable(false) - .visible(false) - .decorations(false) - .center() - .build() - .ok() - }); - - match window { - Some(window) => { - window.unminimize()?; - window.set_focus()?; - Ok(()) - } - None => Err("Failed to create settings window".into()), - } - } - - pub fn show_update_modal() -> Result<()> { - log::trace!("Showing update notification window"); - let handle = get_app_handle(); - // check if path is in windows apps folder - let installation_path = handle.path().resource_dir()?; - if installation_path - .to_string_lossy() - .contains(r"\Program Files\WindowsApps\") - { - log::trace!("Skipping update notification because it is installed as MSIX"); - return Ok(()); - } - - tauri::WebviewWindowBuilder::new( - &handle, - "updater", - tauri::WebviewUrl::App("update/index.html".into()), - ) - .inner_size(500.0, 260.0) - .maximizable(false) - .minimizable(true) - .resizable(false) - .title("Update Available") - .visible(false) - .decorations(false) - .transparent(true) - .shadow(false) - .center() - .always_on_top(true) - .build()?; - - Ok(()) - } -} +use std::{ + env::temp_dir, + sync::{atomic::AtomicBool, Arc, OnceLock}, +}; + +use getset::{Getters, MutGetters}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use seelen_core::handlers::SeelenEvent; +use tauri::{path::BaseDirectory, AppHandle, Emitter, Manager, Wry}; +use tauri_plugin_shell::ShellExt; +use windows::Win32::Graphics::Gdi::HMONITOR; + +use crate::{ + error_handler::Result, + hook::register_win_hook, + instance::SeelenInstanceContainer, + log_error, + modules::monitors::{MonitorManagerEvent, MONITOR_MANAGER}, + seelen_rofi::SeelenRofi, + seelen_wall::SeelenWall, + seelen_weg::SeelenWeg, + seelen_wm_v2::instance::WindowManagerV2, + state::application::{FullState, FULL_STATE}, + system::{declare_system_events_handlers, release_system_events_handlers}, + trace_lock, + utils::{ahk::AutoHotKey, PERFORMANCE_HELPER}, + windows_api::WindowsApi, +}; + +lazy_static! { + pub static ref SEELEN: Arc> = Arc::new(Mutex::new(Seelen::default())); +} + +static APP_HANDLE: OnceLock> = OnceLock::new(); +static SEELEN_IS_RUNNING: AtomicBool = AtomicBool::new(false); + +pub fn get_app_handle<'a>() -> &'a AppHandle { + APP_HANDLE + .get() + .expect("get_app_handle called but app is still not initialized") +} + +/** Struct should be initialized first before calling any other methods */ +#[derive(Getters, MutGetters, Default)] +pub struct Seelen { + #[getset(get = "pub", get_mut = "pub")] + monitors: Vec, + #[getset(get = "pub", get_mut = "pub")] + rofi: Option, + #[getset(get = "pub", get_mut = "pub")] + wall: Option, +} + +/* ============== Getters ============== */ +impl Seelen { + pub fn is_running() -> bool { + SEELEN_IS_RUNNING.load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn focused_monitor(&self) -> Option<&SeelenInstanceContainer> { + self.monitors.iter().find(|m| m.is_focused()) + } + + pub fn focused_monitor_mut(&mut self) -> Option<&mut SeelenInstanceContainer> { + self.monitors.iter_mut().find(|m| m.is_focused()) + } + + pub fn monitor_by_id_mut(&mut self, id: isize) -> Option<&mut SeelenInstanceContainer> { + self.monitors + .iter_mut() + .find(|m| m.handle().0 as isize == id) + } + + pub fn monitor_by_name_mut(&mut self, name: &str) -> Option<&mut SeelenInstanceContainer> { + self.monitors.iter_mut().find(|m| m.name() == name) + } + + pub fn state(&self) -> Arc { + FULL_STATE.load_full() + } +} + +/* ============== Methods ============== */ +impl Seelen { + fn add_rofi(&mut self) -> Result<()> { + if self.rofi.is_none() { + self.rofi = Some(SeelenRofi::new()?); + } + Ok(()) + } + + fn add_wall(&mut self) -> Result<()> { + if self.wall.is_none() { + let wall = SeelenWall::new()?; + wall.update_position()?; + self.wall = Some(wall) + } + Ok(()) + } + + fn refresh_windows_positions(&mut self) -> Result<()> { + if let Some(wall) = &self.wall { + wall.update_position()?; + } + for monitor in &mut self.monitors { + if WindowsApi::monitor_info(*monitor.handle()).is_ok() { + monitor.ensure_positions()?; + } + } + Ok(()) + } + + pub fn on_settings_change(&mut self) -> Result<()> { + let state = self.state(); + + match state.is_ahk_enabled() { + true => Self::start_ahk_shortcuts()?, + false => Self::kill_ahk_shortcuts()?, + } + + if state.is_weg_enabled() { + SeelenWeg::hide_taskbar(); + } else { + SeelenWeg::restore_taskbar()?; + } + + match state.is_window_manager_enabled() { + true => { + WindowManagerV2::init_state()?; + WindowManagerV2::enumerate_all_windows()?; + } + false => WindowManagerV2::clear_state(), + } + + match state.is_rofi_enabled() { + true => self.add_rofi()?, + false => self.rofi = None, + } + + match state.is_wall_enabled() { + true => self.add_wall()?, + false => self.wall = None, + } + + for monitor in &mut self.monitors { + monitor.load_settings(&state)?; + } + + self.refresh_windows_positions()?; + Ok(()) + } + + /// Initialize Seelen and Lazy static variables + pub fn init(&mut self, handle: &AppHandle) -> Result<()> { + log::trace!("Initializing Seelen"); + APP_HANDLE + .set(handle.to_owned()) + .map_err(|_| "Failed to set app handle")?; + Self::ensure_folders(handle)?; + Ok(()) + } + + fn on_monitor_event(event: MonitorManagerEvent) { + match event { + MonitorManagerEvent::Added(_name, id) => { + log_error!(trace_lock!(SEELEN).add_monitor(id)); + } + MonitorManagerEvent::Removed(_name, id) => { + log_error!(trace_lock!(SEELEN).remove_monitor(id)); + } + MonitorManagerEvent::Updated(name, id) => { + if let Some(m) = trace_lock!(SEELEN).monitor_by_name_mut(&name) { + m.update_handle(id); + } + } + } + log_error!(get_app_handle().emit(SeelenEvent::GlobalMonitorsChanged, ())); + } + + async fn start_async() -> Result<()> { + if FULL_STATE.load().is_weg_enabled() { + SeelenWeg::enumerate_all_windows()?; + } + + if FULL_STATE.load().is_window_manager_enabled() { + WindowManagerV2::enumerate_all_windows()?; + } + + Self::start_ahk_shortcuts()?; + Self::refresh_auto_start_path().await?; + Ok(()) + } + + pub fn start(&mut self) -> Result<()> { + SEELEN_IS_RUNNING.store(true, std::sync::atomic::Ordering::SeqCst); + declare_system_events_handlers()?; + + if self.state().is_rofi_enabled() { + self.add_rofi()?; + } + + if self.state().is_wall_enabled() { + self.add_wall()?; + } + + if self.state().is_weg_enabled() { + SeelenWeg::hide_taskbar(); + } + + log::trace!("Enumerating Monitors & Creating Instances"); + let monitors = trace_lock!(MONITOR_MANAGER).monitors.clone(); + for (_name, id) in monitors { + self.add_monitor(id)?; + } + trace_lock!(MONITOR_MANAGER).listen_changes(Self::on_monitor_event); + + tauri::async_runtime::spawn(async { + trace_lock!(PERFORMANCE_HELPER).start("lazy setup"); + log_error!(Self::start_async().await); + trace_lock!(PERFORMANCE_HELPER).end("lazy setup"); + }); + + self.refresh_windows_positions()?; + register_win_hook()?; + Ok(()) + } + + /// Stop and release all resources + pub fn stop(&self) { + SEELEN_IS_RUNNING.store(false, std::sync::atomic::Ordering::SeqCst); + + release_system_events_handlers(); + if self.state().is_weg_enabled() { + log_error!(SeelenWeg::restore_taskbar()); + } + if self.state().is_ahk_enabled() { + log_error!(Self::kill_ahk_shortcuts()); + } + } + + fn add_monitor(&mut self, hmonitor: HMONITOR) -> Result<()> { + self.monitors + .push(SeelenInstanceContainer::new(hmonitor, &self.state())?); + self.refresh_windows_positions()?; + Ok(()) + } + + fn remove_monitor(&mut self, hmonitor: HMONITOR) -> Result<()> { + self.monitors.retain(|m| m.handle() != &hmonitor); + self.refresh_windows_positions()?; + Ok(()) + } + + fn ensure_folders(handle: &AppHandle) -> Result<()> { + log::trace!("Ensuring folders"); + let path = handle.path(); + let data_path = path.app_data_dir()?; + + // migration of user settings files below v1.8.3 + let old_path = path.resolve(".config/seelen", BaseDirectory::Home)?; + if old_path.exists() { + log::trace!("Migrating user settings from {:?}", old_path); + for entry in std::fs::read_dir(&old_path)?.flatten() { + if entry.file_type()?.is_dir() { + continue; + } + std::fs::copy(entry.path(), data_path.join(entry.file_name()))?; + } + std::fs::remove_dir_all(&old_path)?; + } + + let create_if_needed = move |folder: &str| -> Result<()> { + let path = data_path.join(folder); + if !path.exists() { + std::fs::create_dir_all(path)?; + } + Ok(()) + }; + + create_if_needed("placeholders")?; + create_if_needed("themes")?; + create_if_needed("layouts")?; + create_if_needed("icons/system")?; + create_if_needed("wallpapers")?; + + Ok(()) + } + + pub async fn is_auto_start_enabled() -> Result { + let handle = get_app_handle(); + let output = handle + .shell() + .command("powershell") + .args([ + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-Command", + "[bool](Get-ScheduledTask -TaskName Seelen-UI -ErrorAction SilentlyContinue)", + ]) + .output() + .await?; + + let (cow, _used, _has_errors) = encoding_rs::GBK.decode(&output.stdout); + let stdout = cow.to_string().trim().to_lowercase(); + Ok(stdout == "true") + } + + /// override auto-start task in case of location change, normally this happen on MSIX update + async fn refresh_auto_start_path() -> Result<()> { + if WindowsApi::is_elevated()? && Self::is_auto_start_enabled().await? { + Self::set_auto_start(true).await?; + } + Ok(()) + } + + pub async fn set_auto_start(enabled: bool) -> Result<()> { + let pwsh_script = include_str!("schedule.ps1"); + let pwsh_script_path = temp_dir().join("schedule.ps1"); + std::fs::write(&pwsh_script_path, pwsh_script).expect("Failed to write temp script file"); + + let exe_path = std::env::current_exe()?; + + let output = get_app_handle() + .shell() + .command("powershell") + .args([ + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-File", + &pwsh_script_path.to_string_lossy(), + "-ExeRoute", + &exe_path.to_string_lossy(), + "-Enabled", + if enabled { "true" } else { "false" }, + ]) + .output() + .await?; + + if !output.status.success() { + return Err(output.into()); + } + Ok(()) + } + + pub fn start_ahk_shortcuts() -> Result<()> { + // kill all running shortcuts before starting again + Self::kill_ahk_shortcuts()?; + + let state = FULL_STATE.load(); + if state.is_ahk_enabled() { + log::trace!("Creating AHK shortcuts"); + + AutoHotKey::new(include_str!("utils/ahk/mocks/seelen.lib.ahk")) + .name("seelen.lib.ahk") + .save()?; + + AutoHotKey::new(include_str!("utils/ahk/mocks/seelen.ahk")) + .name("seelen.ahk") + .execute()?; + + AutoHotKey::from_template( + include_str!("utils/ahk/mocks/seelen.vd.ahk"), + state.get_ahk_variables(), + ) + .name("seelen.vd.ahk") + .execute()?; + + if state.is_window_manager_enabled() { + AutoHotKey::from_template( + include_str!("utils/ahk/mocks/seelen.wm.ahk"), + state.get_ahk_variables(), + ) + .name("seelen.wm.ahk") + .execute()?; + } + } + log::trace!("AHK shortcuts started successfully"); + Ok(()) + } + + pub fn kill_ahk_shortcuts() -> Result<()> { + log::trace!("Killing AHK shortcuts"); + get_app_handle() + .shell() + .command("powershell") + .args([ + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-Command", + r"Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like '*static\redis\AutoHotkey.exe*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }", + ]) + .spawn()?; + Ok(()) + } + + pub fn show_settings() -> Result<()> { + log::trace!("Show settings window"); + let handle = get_app_handle(); + let window = handle.get_webview_window("settings").or_else(|| { + tauri::WebviewWindowBuilder::new( + handle, + "settings", + tauri::WebviewUrl::App("settings/index.html".into()), + ) + .title("Settings") + .inner_size(750.0, 480.0) + .min_inner_size(700.0, 400.0) + .visible(false) + .decorations(false) + .center() + .build() + .ok() + }); + + match window { + Some(window) => { + window.unminimize()?; + window.set_focus()?; + Ok(()) + } + None => Err("Failed to create settings window".into()), + } + } +} diff --git a/src/background/seelen_bar/hook.rs b/src/background/seelen_bar/hook.rs index 1c6a3e01..c154cdf4 100644 --- a/src/background/seelen_bar/hook.rs +++ b/src/background/seelen_bar/hook.rs @@ -1,40 +1,48 @@ -use windows::Win32::Foundation::HWND; - -use crate::{error_handler::Result, windows_api::WindowsApi, winevent::WinEvent}; - -use super::FancyToolbar; - -impl FancyToolbar { - pub fn process_win_event(&mut self, event: WinEvent, origin: HWND) -> Result<()> { - match event { - WinEvent::ObjectNameChange => { - if self.last_focus == Some(origin.0) { - self.focus_changed(origin)?; - } - } - WinEvent::SystemForeground | WinEvent::ObjectFocus => { - self.focus_changed(origin)?; - self.handle_overlaped_status(origin)?; - } - WinEvent::ObjectLocationChange => { - if origin == WindowsApi::get_foreground_window() { - self.handle_overlaped_status(origin)?; - } - } - WinEvent::SyntheticFullscreenStart(event_data) => { - let monitor = WindowsApi::monitor_from_window(self.window.hwnd()?); - if monitor == event_data.monitor { - self.hide()?; - } - } - WinEvent::SyntheticFullscreenEnd(event_data) => { - let monitor = WindowsApi::monitor_from_window(self.window.hwnd()?); - if monitor == event_data.monitor { - self.show()?; - } - } - _ => {} - }; - Ok(()) - } -} +use windows::Win32::Foundation::HWND; + +use crate::{ + error_handler::Result, + windows_api::{window::Window, WindowsApi}, + winevent::WinEvent, +}; + +use super::FancyToolbar; + +impl FancyToolbar { + pub fn process_win_event(&mut self, event: WinEvent, origin: HWND) -> Result<()> { + let window = Window::from(origin); + match event { + WinEvent::ObjectNameChange => { + if self.last_focus == Some(origin) { + self.focus_changed(origin)?; + } + } + WinEvent::SystemForeground | WinEvent::ObjectFocus => { + self.focus_changed(origin)?; + self.handle_overlaped_status(origin)?; + } + WinEvent::ObjectLocationChange => { + if window.hwnd() == self.window.hwnd()? { + self.set_position(window.monitor().raw())?; + } + if origin == WindowsApi::get_foreground_window() { + self.handle_overlaped_status(origin)?; + } + } + WinEvent::SyntheticFullscreenStart(event_data) => { + let monitor = WindowsApi::monitor_from_window(self.window.hwnd()?); + if monitor == event_data.monitor { + self.hide()?; + } + } + WinEvent::SyntheticFullscreenEnd(event_data) => { + let monitor = WindowsApi::monitor_from_window(self.window.hwnd()?); + if monitor == event_data.monitor { + self.show()?; + } + } + _ => {} + }; + Ok(()) + } +} diff --git a/src/background/seelen_bar/mod.rs b/src/background/seelen_bar/mod.rs index 61bbdcf2..556e1f39 100644 --- a/src/background/seelen_bar/mod.rs +++ b/src/background/seelen_bar/mod.rs @@ -1,218 +1,205 @@ -pub mod cli; -pub mod hook; - -use crate::{ - error_handler::Result, - log_error, - modules::virtual_desk::get_vd_manager, - seelen::get_app_handle, - state::application::FULL_STATE, - utils::{ - are_overlaped, - constants::{OVERLAP_BLACK_LIST_BY_EXE, OVERLAP_BLACK_LIST_BY_TITLE}, - }, - windows_api::{AppBarData, AppBarDataEdge, WindowsApi}, -}; -use itertools::Itertools; -use seelen_core::state::HideMode; -use serde::Serialize; -use tauri::{Emitter, Listener, Manager, WebviewWindow}; -use windows::Win32::{ - Foundation::{HWND, RECT}, - Graphics::Gdi::HMONITOR, - UI::WindowsAndMessaging::{HWND_TOPMOST, SWP_NOACTIVATE, SW_HIDE, SW_SHOWNOACTIVATE}, -}; - -pub struct FancyToolbar { - window: WebviewWindow, - pub cached_monitor: HMONITOR, - last_focus: Option, - hidden: bool, - overlaped: bool, -} - -impl Drop for FancyToolbar { - fn drop(&mut self) { - log::info!("Dropping {}", self.window.label()); - if let Ok(hwnd) = self.window.hwnd() { - AppBarData::from_handle(hwnd).unregister_bar(); - } - log_error!(self.window.destroy()); - } -} - -impl FancyToolbar { - pub fn new(postfix: &str) -> Result { - log::info!("Creating {}/{}", Self::TARGET, postfix); - Ok(Self { - window: Self::create_window(postfix)?, - last_focus: None, - hidden: false, - cached_monitor: HMONITOR(-1), - overlaped: false, - }) - } - - pub fn emit(&self, event: &str, payload: S) -> Result<()> { - self.window.emit_to(self.window.label(), event, payload)?; - Ok(()) - } - - pub fn is_overlapping(&self, hwnd: HWND) -> Result { - let rect = WindowsApi::get_window_rect_without_shadow(hwnd); - let monitor_info = WindowsApi::monitor_info(self.cached_monitor)?; - let dpi = WindowsApi::get_device_pixel_ratio(self.cached_monitor)?; - let height = FULL_STATE.load().settings().fancy_toolbar.height; - - let mut hitbox_rect = monitor_info.monitorInfo.rcMonitor; - hitbox_rect.bottom = hitbox_rect.top + (height as f32 * dpi) as i32; - Ok(are_overlaped(&hitbox_rect, &rect)) - } - - pub fn set_overlaped_status(&mut self, is_overlaped: bool) -> Result<()> { - if self.overlaped == is_overlaped { - return Ok(()); - } - self.overlaped = is_overlaped; - self.emit("set-auto-hide", self.overlaped)?; - Ok(()) - } - - pub fn handle_overlaped_status(&mut self, hwnd: HWND) -> Result<()> { - let should_handle_hidden = WindowsApi::is_window_visible(hwnd) - && !OVERLAP_BLACK_LIST_BY_TITLE.contains(&WindowsApi::get_window_text(hwnd).as_str()) - && !OVERLAP_BLACK_LIST_BY_EXE - .contains(&WindowsApi::exe(hwnd).unwrap_or_default().as_str()); - - if !should_handle_hidden { - return Ok(()); - } - self.set_overlaped_status(self.is_overlapping(hwnd)?) - } - - pub fn hide(&mut self) -> Result<()> { - WindowsApi::show_window_async(self.window.hwnd()?, SW_HIDE)?; - self.hidden = true; - Ok(()) - } - - pub fn show(&mut self) -> Result<()> { - WindowsApi::show_window_async(self.window.hwnd()?, SW_SHOWNOACTIVATE)?; - self.hidden = false; - Ok(()) - } - - pub fn focus_changed(&mut self, hwnd: HWND) -> Result<()> { - self.last_focus = Some(hwnd.0); - Ok(()) - } - - pub fn ensure_hitbox_zorder(&self) -> Result<()> { - let hwnd = HWND(self.window.hwnd()?.0); - WindowsApi::bring_to(hwnd, HWND_TOPMOST)?; - self.set_positions(self.cached_monitor.0)?; - Ok(()) - } -} - -// statics -impl FancyToolbar { - pub const TITLE: &'static str = "Seelen Fancy Toolbar"; - const TARGET: &'static str = "fancy-toolbar"; - - /// Work area no works fine on multiple monitors - /// so we use this functions that only takes the toolbar in account - pub fn get_work_area_by_monitor(monitor: isize) -> Result { - let monitor_info = WindowsApi::monitor_info(HMONITOR(monitor))?; - - let dpi = WindowsApi::get_device_pixel_ratio(HMONITOR(monitor))?; - let mut rect = monitor_info.monitorInfo.rcMonitor; - - let state = FULL_STATE.load(); - if state.is_bar_enabled() { - let toolbar_height = state.settings().fancy_toolbar.height; - rect.top += (toolbar_height as f32 * dpi) as i32; - } - - Ok(rect) - } - - pub fn set_positions(&self, monitor: isize) -> Result<()> { - let hmonitor = HMONITOR(monitor); - if hmonitor.is_invalid() { - return Err("Invalid Monitor".into()); - } - - let monitor_info = WindowsApi::monitor_info(hmonitor)?; - let rc_monitor = monitor_info.monitorInfo.rcMonitor; - - let main_hwnd = HWND(self.window.hwnd()?.0); - - let state = FULL_STATE.load(); - let settings = &state.settings().fancy_toolbar; - - let mut abd = AppBarData::from_handle(main_hwnd); - let mut abd_rect = rc_monitor; - if settings.hide_mode == HideMode::Always || settings.hide_mode == HideMode::OnOverlap { - abd_rect.bottom = abd_rect.top + 1; - } else { - let dpi = WindowsApi::get_device_pixel_ratio(hmonitor)?; - abd_rect.bottom = abd_rect.top + (settings.height as f32 * dpi) as i32; - } - - abd.set_edge(AppBarDataEdge::Top); - abd.set_rect(abd_rect); - abd.register_as_new_bar(); - - // pre set position for resize in case of multiples dpi - WindowsApi::move_window(main_hwnd, &rc_monitor)?; - WindowsApi::set_position(main_hwnd, None, &rc_monitor, SWP_NOACTIVATE)?; - Ok(()) - } - - fn create_window(postfix: &str) -> Result { - let manager = get_app_handle(); - - let label = format!("{}/{}", Self::TARGET, postfix); - let window = match manager.get_webview_window(&label) { - Some(window) => window, - None => tauri::WebviewWindowBuilder::new( - &manager, - label, - tauri::WebviewUrl::App("toolbar/index.html".into()), - ) - .title(Self::TITLE) - .maximizable(false) - .minimizable(false) - .resizable(false) - .visible(false) - .decorations(false) - .transparent(true) - .shadow(false) - .skip_taskbar(true) - .always_on_top(true) - .drag_and_drop(false) - .build()?, - }; - - window.set_ignore_cursor_events(true)?; - window.once("store-events-ready", Self::on_store_events_ready); - Ok(window) - } - - fn on_store_events_ready(_: tauri::Event) { - // TODO refactor this implementation - std::thread::spawn(|| -> Result<()> { - let handler = get_app_handle(); - let vd = get_vd_manager(); - let desktops = vd - .get_all()? - .iter() - .map(|d| d.as_serializable()) - .collect_vec(); - handler.emit("workspaces-changed", &desktops)?; - handler.emit("active-workspace-changed", vd.get_current()?.id())?; - Ok(()) - }); - } -} +pub mod cli; +pub mod hook; + +use crate::{ + error_handler::Result, + log_error, + modules::virtual_desk::get_vd_manager, + seelen::get_app_handle, + state::application::FULL_STATE, + utils::{ + are_overlaped, + constants::{NATIVE_UI_POPUP_CLASSES, OVERLAP_BLACK_LIST_BY_EXE}, + }, + windows_api::{window::Window, AppBarData, AppBarDataEdge, WindowsApi}, +}; +use itertools::Itertools; +use seelen_core::{handlers::SeelenEvent, state::HideMode}; +use serde::Serialize; +use tauri::{Emitter, Listener, WebviewWindow}; +use windows::Win32::{ + Foundation::{HWND, RECT}, + Graphics::Gdi::HMONITOR, + UI::WindowsAndMessaging::{SWP_NOACTIVATE, SW_HIDE, SW_SHOWNOACTIVATE}, +}; + +pub struct FancyToolbar { + window: WebviewWindow, + /// Is the rect that the toolbar should have when it isn't hidden + pub theoretical_rect: RECT, + last_focus: Option, + overlaped: bool, +} + +impl Drop for FancyToolbar { + fn drop(&mut self) { + log::info!("Dropping {}", self.window.label()); + if let Ok(hwnd) = self.window.hwnd() { + AppBarData::from_handle(hwnd).unregister_bar(); + } + log_error!(self.window.destroy()); + } +} + +impl FancyToolbar { + pub fn new(postfix: &str) -> Result { + log::info!("Creating {}/{}", Self::TARGET, postfix); + Ok(Self { + window: Self::create_window(postfix)?, + last_focus: None, + theoretical_rect: RECT::default(), + overlaped: false, + }) + } + + pub fn emit(&self, event: &str, payload: S) -> Result<()> { + self.window.emit_to(self.window.label(), event, payload)?; + Ok(()) + } + + fn is_overlapping(&self, hwnd: HWND) -> Result { + let window_rect = WindowsApi::get_inner_window_rect(hwnd)?; + Ok(are_overlaped(&self.theoretical_rect, &window_rect)) + } + + fn set_overlaped_status(&mut self, is_overlaped: bool) -> Result<()> { + if self.overlaped == is_overlaped { + return Ok(()); + } + self.overlaped = is_overlaped; + self.emit(SeelenEvent::ToolbarOverlaped, self.overlaped)?; + Ok(()) + } + + pub fn handle_overlaped_status(&mut self, hwnd: HWND) -> Result<()> { + let window = Window::from(hwnd); + let is_overlaped = self.is_overlapping(hwnd)? + && !window.is_desktop() + && !window.is_seelen_overlay() + && !NATIVE_UI_POPUP_CLASSES.contains(&window.class().as_str()) + && !OVERLAP_BLACK_LIST_BY_EXE + .contains(&WindowsApi::exe(hwnd).unwrap_or_default().as_str()); + self.set_overlaped_status(is_overlaped) + } + + pub fn hide(&mut self) -> Result<()> { + WindowsApi::show_window_async(self.window.hwnd()?, SW_HIDE)?; + self.window.emit_to( + self.window.label(), + SeelenEvent::HandleLayeredHitboxes, + false, + )?; + Ok(()) + } + + pub fn show(&mut self) -> Result<()> { + WindowsApi::show_window_async(self.window.hwnd()?, SW_SHOWNOACTIVATE)?; + self.window.emit_to( + self.window.label(), + SeelenEvent::HandleLayeredHitboxes, + true, + )?; + Ok(()) + } + + pub fn focus_changed(&mut self, hwnd: HWND) -> Result<()> { + self.last_focus = Some(hwnd); + Ok(()) + } +} + +// statics +impl FancyToolbar { + pub const TITLE: &'static str = "Seelen Fancy Toolbar"; + const TARGET: &'static str = "fancy-toolbar"; + + /// Work area no works fine on multiple monitors + /// so we use this functions that only takes the toolbar in account + pub fn get_work_area_by_monitor(monitor: HMONITOR) -> Result { + let monitor_info = WindowsApi::monitor_info(monitor)?; + + let dpi = WindowsApi::get_device_pixel_ratio(monitor)?; + let mut rect = monitor_info.monitorInfo.rcMonitor; + + let state = FULL_STATE.load(); + if state.is_bar_enabled() { + let toolbar_height = state.settings().fancy_toolbar.height; + rect.top += (toolbar_height as f32 * dpi) as i32; + } + + Ok(rect) + } + + pub fn set_position(&mut self, monitor: HMONITOR) -> Result<()> { + let hwnd = HWND(self.window.hwnd()?.0); + + let state = FULL_STATE.load(); + let settings = &state.settings().fancy_toolbar; + + let monitor_info = WindowsApi::monitor_info(monitor)?; + let monitor_dpi = WindowsApi::get_device_pixel_ratio(monitor)?; + let rc_monitor = monitor_info.monitorInfo.rcMonitor; + self.theoretical_rect = RECT { + bottom: rc_monitor.top + (settings.height as f32 * monitor_dpi) as i32, + ..rc_monitor + }; + + let mut abd = AppBarData::from_handle(hwnd); + match settings.hide_mode { + HideMode::Never => { + abd.set_edge(AppBarDataEdge::Top); + abd.set_rect(self.theoretical_rect); + abd.register_as_new_bar(); + } + _ => abd.unregister_bar(), + }; + + // pre set position for resize in case of multiples dpi + WindowsApi::move_window(hwnd, &rc_monitor)?; + WindowsApi::set_position(hwnd, None, &rc_monitor, SWP_NOACTIVATE)?; + Ok(()) + } + + fn create_window(postfix: &str) -> Result { + let manager = get_app_handle(); + + let label = format!("{}/{}", Self::TARGET, postfix); + let window = tauri::WebviewWindowBuilder::new( + manager, + label, + tauri::WebviewUrl::App("toolbar/index.html".into()), + ) + .title(Self::TITLE) + .minimizable(false) + .maximizable(false) + .closable(false) + .resizable(false) + .visible(false) + .decorations(false) + .transparent(true) + .shadow(false) + .skip_taskbar(true) + .always_on_top(true) + .build()?; + + window.set_ignore_cursor_events(true)?; + window.listen("store-events-ready", Self::on_store_events_ready); + Ok(window) + } + + fn on_store_events_ready(_: tauri::Event) { + // TODO refactor this implementation + std::thread::spawn(|| -> Result<()> { + let handler = get_app_handle(); + let vd = get_vd_manager(); + let desktops = vd + .get_all()? + .iter() + .map(|d| d.as_serializable()) + .collect_vec(); + handler.emit(SeelenEvent::WorkspacesChanged, &desktops)?; + handler.emit(SeelenEvent::ActiveWorkspaceChanged, vd.get_current()?.id())?; + Ok(()) + }); + } +} diff --git a/src/background/seelen_rofi/cli.rs b/src/background/seelen_rofi/cli.rs new file mode 100644 index 00000000..a375e8cf --- /dev/null +++ b/src/background/seelen_rofi/cli.rs @@ -0,0 +1,35 @@ +use clap::Command; + +use crate::{error_handler::Result, get_subcommands}; + +use super::SeelenRofi; + +get_subcommands![ + /// Shows/Hides the App Launcher + Toggle, +]; + +impl SeelenRofi { + pub const CLI_IDENTIFIER: &'static str = "launcher"; + + pub fn get_cli() -> Command { + Command::new(Self::CLI_IDENTIFIER) + .about("Seelen's App Launcher") + .arg_required_else_help(true) + .subcommands(SubCommand::commands()) + } + + pub fn process(&mut self, matches: &clap::ArgMatches) -> Result<()> { + let subcommand = SubCommand::try_from(matches)?; + match subcommand { + SubCommand::Toggle => { + if self.window.is_visible()? { + self.hide()?; + } else { + self.show()?; + } + } + }; + Ok(()) + } +} diff --git a/src/background/seelen_rofi/handler.rs b/src/background/seelen_rofi/handler.rs new file mode 100644 index 00000000..0f58f9f0 --- /dev/null +++ b/src/background/seelen_rofi/handler.rs @@ -0,0 +1,11 @@ +use crate::{seelen::SEELEN, trace_lock}; + +use super::SeelenRofiApp; + +#[tauri::command(async)] +pub fn launcher_get_apps() -> Vec { + if let Some(rofi) = trace_lock!(SEELEN).rofi() { + return rofi.apps.clone(); + } + Vec::new() +} diff --git a/src/background/seelen_rofi/mod.rs b/src/background/seelen_rofi/mod.rs new file mode 100644 index 00000000..81fe1c1e --- /dev/null +++ b/src/background/seelen_rofi/mod.rs @@ -0,0 +1,132 @@ +pub mod cli; +pub mod handler; + +use std::{ffi::OsStr, path::PathBuf}; + +use serde::Serialize; +use tauri::{path::BaseDirectory, Manager, WebviewWindow}; +use windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE; + +use crate::{ + error_handler::Result, log_error, seelen::get_app_handle, + seelen_weg::icon_extractor::extract_and_save_icon_from_file, utils::constants::Icons, + windows_api::WindowsApi, +}; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SeelenRofiApp { + pub label: String, + pub icon: PathBuf, + pub path: PathBuf, +} + +pub struct SeelenRofi { + window: WebviewWindow, + pub apps: Vec, +} + +impl Drop for SeelenRofi { + fn drop(&mut self) { + log::info!("Dropping {}", self.window.label()); + log_error!(self.window.destroy()); + } +} + +impl SeelenRofi { + pub const TITLE: &str = "Seelen App Launcher"; + pub const TARGET: &str = "seelen-launcher"; + + pub fn new() -> Result { + log::info!("Creating {}", Self::TARGET); + Ok(Self { + // apps should be loaded first because it takes a long time on start and its needed by webview + apps: Self::load_apps()?, + window: Self::create_window()?, + }) + } + + fn load_dir(dir: PathBuf) -> Result> { + let mut apps = Vec::new(); + for entry in std::fs::read_dir(dir)?.flatten() { + let file_type = entry.file_type()?; + let path = entry.path(); + + if file_type.is_dir() { + match Self::load_dir(path) { + Ok(app) => apps.extend(app), + Err(e) => log::error!("{:?}", e), + } + continue; + } + + if file_type.is_file() && path.extension() != Some(OsStr::new("ini")) { + apps.push(SeelenRofiApp { + label: path.file_stem().unwrap().to_string_lossy().to_string(), + icon: extract_and_save_icon_from_file(&path) + .unwrap_or_else(|_| Icons::missing_app()), + path, + }) + } + } + Ok(apps) + } + + fn load_apps() -> Result> { + let mut result = Vec::new(); + + let apps = Self::load_dir(PathBuf::from( + r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs", + ))?; + result.extend(apps); + + let apps = Self::load_dir(get_app_handle().path().resolve( + r"Microsoft\Windows\Start Menu\Programs", + BaseDirectory::Data, + )?)?; + result.extend(apps); + + result.sort_by_key(|app| app.label.to_lowercase()); + Ok(result) + } + + pub fn show(&self) -> Result<()> { + let rc_monitor = WindowsApi::monitor_info(WindowsApi::monitor_from_cursor_point())? + .monitorInfo + .rcMonitor; + WindowsApi::move_window(self.window.hwnd()?, &rc_monitor)?; + WindowsApi::set_position(self.window.hwnd()?, None, &rc_monitor, SWP_NOACTIVATE)?; + std::thread::sleep(std::time::Duration::from_millis(100)); + self.window.show()?; + self.window.set_focus()?; + Ok(()) + } + + pub fn hide(&self) -> Result<()> { + self.window.hide()?; + Ok(()) + } + + fn create_window() -> Result { + let window = tauri::WebviewWindowBuilder::new( + get_app_handle(), + Self::TARGET, + tauri::WebviewUrl::App("seelen_rofi/index.html".into()), + ) + .title(Self::TITLE) + .minimizable(false) + .maximizable(false) + .closable(false) + .resizable(false) + .visible(false) // change to false after finish development + .transparent(true) + .shadow(false) + .decorations(false) + .skip_taskbar(true) + .drag_and_drop(false) + .always_on_top(true) + .build()?; + + Ok(window) + } +} diff --git a/src/background/seelen_wall/hook.rs b/src/background/seelen_wall/hook.rs new file mode 100644 index 00000000..30d52656 --- /dev/null +++ b/src/background/seelen_wall/hook.rs @@ -0,0 +1,23 @@ +use seelen_core::handlers::SeelenEvent; +use tauri::Emitter; + +use crate::{error_handler::Result, windows_api::window::Window, winevent::WinEvent}; + +use super::SeelenWall; + +impl SeelenWall { + pub fn process_win_event(&mut self, event: WinEvent, _origin: &Window) -> Result<()> { + match event { + WinEvent::SyntheticFullscreenStart(_) => { + self.window + .emit_to(self.window.label(), SeelenEvent::WallStop, true)?; + } + WinEvent::SyntheticFullscreenEnd(_) => { + self.window + .emit_to(self.window.label(), SeelenEvent::WallStop, false)?; + } + _ => {} + } + Ok(()) + } +} diff --git a/src/background/seelen_wall/mod.rs b/src/background/seelen_wall/mod.rs new file mode 100644 index 00000000..9198fd36 --- /dev/null +++ b/src/background/seelen_wall/mod.rs @@ -0,0 +1,155 @@ +mod hook; + +use tauri::WebviewWindow; +use windows::Win32::{ + Foundation::{HWND, LPARAM, RECT, WPARAM}, + Graphics::Gdi::{InvalidateRect, UpdateWindow}, + UI::WindowsAndMessaging::{ + FindWindowA, FindWindowExA, PostMessageW, SetParent, SWP_NOACTIVATE, + }, +}; + +use crate::{ + error_handler::Result, + log_error, pcstr, + seelen::get_app_handle, + windows_api::{WindowEnumerator, WindowsApi}, +}; + +pub struct SeelenWall { + window: WebviewWindow, +} + +impl Drop for SeelenWall { + fn drop(&mut self) { + log::info!("Dropping {}", self.window.label()); + log_error!(self.window.destroy()); + } +} + +impl SeelenWall { + pub const TITLE: &str = "Seelen Wall"; + const TARGET: &str = "seelen-wall"; + + pub fn new() -> Result { + log::info!("Creating {}", Self::TARGET); + Ok(Self { + window: Self::create_window()?, + }) + } + + fn create_window() -> Result { + let handle = get_app_handle(); + let window = tauri::WebviewWindowBuilder::new( + handle, + Self::TARGET, + tauri::WebviewUrl::App("seelen_wall/index.html".into()), + ) + .title(Self::TITLE) + .minimizable(false) + .maximizable(false) + .closable(false) + .resizable(false) + .decorations(false) + .shadow(false) + .visible(false) + .disable_drag_drop_handler() + .skip_taskbar(true) + // idk why I add this but lively wallpaper has it XD + // .additional_browser_args("--disk-cache-size=1 --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection") + .build()?; + + window.set_always_on_bottom(true)?; + Ok(window) + } + + pub fn update_position(&self) -> Result<()> { + let mut rect = WindowsApi::virtual_screen_rect()?; + let main_hwnd = HWND(self.window.hwnd()?.0); + + if Self::try_set_inside_workerw(main_hwnd).is_ok() { + // rect relative to the parent + rect = RECT { + top: 0, + left: 0, + right: rect.right - rect.left, + bottom: rect.bottom - rect.top, + }; + } + + // pre set position for resize in case of multiples dpi + WindowsApi::move_window(main_hwnd, &rect)?; + WindowsApi::set_position(main_hwnd, None, &rect, SWP_NOACTIVATE)?; + log_error!(Self::refresh_desktop().map_err(|e| format!("Failed to refresh desktop: {}", e))); + Ok(()) + } + + fn try_set_inside_workerw(hwnd: HWND) -> Result<()> { + let progman = unsafe { FindWindowA(pcstr!("Progman"), None)? }; + + // Send 0x052C to Progman. This message directs Progman to spawn a WorkerW + // behind the desktop icons. If it is already there, nothing happens. + unsafe { PostMessageW(progman, 0x052C, WPARAM(0xD), LPARAM(0x1))? }; + + // CASE 1: + // 0x00010190 "" WorkerW + // ... + // 0x000100EE "" SHELLDLL_DefView + // 0x000100F0 "FolderView" SysListView32 + // 0x00100B8A "" WorkerW <-- This is the WorkerW instance we are after! + // 0x000100EC "Program Manager" Progman + let mut worker = None; + + WindowEnumerator::new().for_each(|current| unsafe { + // check if current contains SHELLDLL_DefView + if FindWindowExA(current, None, pcstr!("SHELLDLL_DefView"), None).is_ok() { + // find next worker after the current one + if let Ok(_worker) = FindWindowExA(None, current, pcstr!("WorkerW"), None) { + worker = Some(_worker); + } + } + })?; + + // CASE 2: + // Some Windows 11 builds have a different Progman window layout. + // If the above code failed to find WorkerW, we should try this. + // 0x000100EC "Program Manager" Progman + // 0x000100EE "" SHELLDLL_DefView + // 0x000100F0 "FolderView" SysListView32 + // 0x00100B8A "" WorkerW <-- This is the WorkerW instance we are after! + if worker.is_none() { + let mut attempts = 0; + worker = + unsafe { FindWindowExA(progman, HWND::default(), pcstr!("WorkerW"), None).ok() }; + while worker.is_none() && attempts < 10 { + attempts += 1; + std::thread::sleep(std::time::Duration::from_millis(100)); + worker = unsafe { + FindWindowExA(progman, HWND::default(), pcstr!("WorkerW"), None).ok() + }; + } + } + + match worker { + Some(worker) => { + unsafe { SetParent(hwnd, worker)? }; + Ok(()) + } + None => Err("Failed to find/create progman worker window".into()), + } + } + + /// this is only needed on the case 2 of try_set_inside_workerw + fn refresh_desktop() -> Result<()> { + unsafe { + let progman = FindWindowA(pcstr!("Progman"), None)?; + if let Ok(shell_view) = + FindWindowExA(progman, HWND::default(), pcstr!("SHELLDLL_DefView"), None) + { + InvalidateRect(shell_view, None, true).ok()?; + UpdateWindow(shell_view).ok()?; + } + } + Ok(()) + } +} diff --git a/src/background/seelen_weg/cli.rs b/src/background/seelen_weg/cli.rs index 0409b9ca..82aedd1b 100644 --- a/src/background/seelen_weg/cli.rs +++ b/src/background/seelen_weg/cli.rs @@ -1,5 +1,4 @@ use clap::Command; -use tauri::Emitter; use crate::{error_handler::Result, get_subcommands}; @@ -8,8 +7,6 @@ use super::SeelenWeg; get_subcommands![ /** Open Dev Tools (only works if the app is running in dev mode) */ Debug, - /** Shows the invisible hitbox */ - DebugHitbox, ]; impl SeelenWeg { @@ -29,10 +26,6 @@ impl SeelenWeg { #[cfg(any(debug_assertions, feature = "devtools"))] self.window.open_devtools(); } - SubCommand::DebugHitbox => { - self.hitbox - .emit_to(self.hitbox.label(), "debug-hitbox", ())?; - } }; Ok(()) } diff --git a/src/background/seelen_weg/handler.rs b/src/background/seelen_weg/handler.rs index 14715f51..39031fd3 100644 --- a/src/background/seelen_weg/handler.rs +++ b/src/background/seelen_weg/handler.rs @@ -1,16 +1,17 @@ -use std::sync::atomic::Ordering; +use std::{ffi::OsStr, path::PathBuf, sync::atomic::Ordering}; use image::ImageFormat; +use seelen_core::state::WegItem; use tauri::Emitter; use tauri_plugin_shell::ShellExt; use crate::{ error_handler::Result, hook::LAST_ACTIVE_NOT_SEELEN, seelen::get_app_handle, - windows_api::WindowsApi, + state::application::FULL_STATE, windows_api::WindowsApi, }; use windows::Win32::{ - Foundation::{HWND, LPARAM, WPARAM}, - UI::WindowsAndMessaging::{PostMessageW, SW_MINIMIZE, SW_RESTORE, WM_CLOSE}, + Foundation::HWND, + UI::WindowsAndMessaging::{SW_MINIMIZE, SW_RESTORE, WM_CLOSE}, }; use super::SeelenWeg; @@ -19,8 +20,13 @@ use super::SeelenWeg; pub fn weg_request_update_previews(handles: Vec) -> Result<()> { let temp_dir = std::env::temp_dir(); - for hwnd in handles { - let hwnd: HWND = HWND(hwnd); + for addr in handles { + let hwnd: HWND = HWND(addr as _); + + if hwnd.is_invalid() || !WindowsApi::is_window_visible(hwnd) { + SeelenWeg::remove_hwnd(hwnd); + continue; + } if WindowsApi::is_iconic(hwnd) { continue; @@ -28,7 +34,7 @@ pub fn weg_request_update_previews(handles: Vec) -> Result<()> { let image = SeelenWeg::capture_window(hwnd); if let Some(image) = image { - let rect = WindowsApi::get_window_rect_without_shadow(hwnd); + let rect = WindowsApi::get_inner_window_rect(hwnd)?; let shadow = WindowsApi::shadow_rect(hwnd)?; let width = rect.right - rect.left; let height = rect.bottom - rect.top; @@ -40,30 +46,31 @@ pub fn weg_request_update_previews(handles: Vec) -> Result<()> { height as u32, ); - image.save_with_format(temp_dir.join(format!("{}.png", hwnd.0)), ImageFormat::Png)?; - get_app_handle().emit(format!("weg-preview-update-{}", hwnd.0).as_str(), ())?; + image.save_with_format(temp_dir.join(format!("{}.png", addr)), ImageFormat::Png)?; + get_app_handle().emit(format!("weg-preview-update-{}", addr).as_str(), ())?; } } Ok(()) } #[tauri::command(async)] -pub fn weg_close_app(hwnd: isize) -> Result<(), String> { - let hwnd = HWND(hwnd); - unsafe { - match PostMessageW(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)) { - Ok(()) => Ok(()), - Err(_) => Err("could not close window".to_owned()), - } +pub fn weg_close_app(hwnd: isize) -> Result<()> { + let hwnd = HWND(hwnd as _); + if !WindowsApi::is_window_visible(hwnd) { + SeelenWeg::remove_hwnd(hwnd); + } else { + WindowsApi::post_message(hwnd, WM_CLOSE, 0, 0)?; } + Ok(()) } #[tauri::command(async)] pub fn weg_toggle_window_state(hwnd: isize, exe_path: String) -> Result<()> { - let hwnd = HWND(hwnd); + let hwnd = HWND(hwnd as _); // If the window is not open, open it - if !WindowsApi::is_window(hwnd) { + if hwnd.is_invalid() || !WindowsApi::is_window_visible(hwnd) { + SeelenWeg::remove_hwnd(hwnd); get_app_handle() .shell() .command("explorer") @@ -73,15 +80,49 @@ pub fn weg_toggle_window_state(hwnd: isize, exe_path: String) -> Result<()> { } if WindowsApi::is_iconic(hwnd) { - WindowsApi::show_window(hwnd, SW_RESTORE)?; + WindowsApi::show_window_async(hwnd, SW_RESTORE)?; return Ok(()); } - if LAST_ACTIVE_NOT_SEELEN.load(Ordering::Acquire) == hwnd.0 { - WindowsApi::show_window(hwnd, SW_MINIMIZE)?; + if LAST_ACTIVE_NOT_SEELEN.load(Ordering::Acquire) == hwnd.0 as isize { + WindowsApi::show_window_async(hwnd, SW_MINIMIZE)?; } else { WindowsApi::async_force_set_foreground(hwnd) } Ok(()) } + +#[tauri::command(async)] +pub fn weg_pin_item(mut path: PathBuf) -> Result<()> { + let mut state = FULL_STATE.load().cloned(); + + if path.extension() == Some(OsStr::new("lnk")) { + path = WindowsApi::resolve_lnk_target(&path)?; + } + + let item = if path.extension() == Some(OsStr::new("exe")) { + // let execution_path = None; + // todo add support to UWP on seelen rofi + /* if let Some(package) = trace_lock!(UWP_MANAGER, 10).get_from_path(&path) { + if let Some(app) = path.file_name() { + execution_path = package.get_shell_path(app.to_string_lossy().as_ref()); + } + } */ + WegItem::PinnedApp { + exe: path.clone(), + execution_path: path.to_string_lossy().to_string(), + } + } else { + WegItem::Pinned { + is_dir: path.is_dir(), + path, + } + }; + + state.weg_items.center.insert(0, item); + state.emit_weg_items()?; + state.save_weg_items()?; + state.store(); + Ok(()) +} diff --git a/src/background/seelen_weg/hook.rs b/src/background/seelen_weg/hook.rs index 0a9e22a3..32643e3c 100644 --- a/src/background/seelen_weg/hook.rs +++ b/src/background/seelen_weg/hook.rs @@ -3,22 +3,32 @@ use windows::Win32::{ UI::WindowsAndMessaging::{FindWindowExA, EVENT_OBJECT_CREATE, EVENT_OBJECT_SHOW, SW_HIDE}, }; -use crate::{error_handler::Result, pcstr, windows_api::WindowsApi, winevent::WinEvent}; +use crate::{ + error_handler::Result, + pcstr, + windows_api::{window::Window, WindowsApi}, + winevent::WinEvent, +}; use super::{SeelenWeg, TASKBAR_CLASS}; impl SeelenWeg { - pub fn process_global_win_event(event: WinEvent, origin: HWND) -> Result<()> { + pub fn process_global_win_event(event: WinEvent, window: &Window) -> Result<()> { + let origin = window.hwnd(); match event { WinEvent::ObjectShow | WinEvent::ObjectCreate => { if Self::should_be_added(origin) { - Self::add_hwnd(origin); + Self::add_hwnd(origin)?; } } WinEvent::ObjectParentChange => { - let parent = WindowsApi::get_parent(origin); - if parent.0 != 0 && !Self::contains_app(parent) && Self::should_be_added(parent) { - Self::add_hwnd(parent); + if let Some(parent) = window.parent() { + if Self::contains_app(window.hwnd()) { + Self::remove_hwnd(origin); + } + if !Self::contains_app(parent.hwnd()) && Self::should_be_added(parent.hwnd()) { + Self::add_hwnd(parent.hwnd())?; + } } } WinEvent::ObjectDestroy | WinEvent::ObjectHide => { @@ -30,7 +40,7 @@ impl SeelenWeg { if Self::contains_app(origin) { Self::update_app(origin); } else if Self::should_be_added(origin) { - Self::add_hwnd(origin); + Self::add_hwnd(origin)?; } } WinEvent::SystemForeground | WinEvent::ObjectFocus => { @@ -42,11 +52,15 @@ impl SeelenWeg { } pub fn process_individual_win_event(&mut self, event: WinEvent, origin: HWND) -> Result<()> { + let window = Window::from(origin); match event { WinEvent::SystemForeground | WinEvent::ObjectFocus => { self.handle_overlaped_status(origin)?; } WinEvent::ObjectLocationChange => { + if window.hwnd() == self.window.hwnd()? { + self.set_position(window.monitor().raw())?; + } if origin == WindowsApi::get_foreground_window() { self.handle_overlaped_status(origin)?; } @@ -61,7 +75,6 @@ impl SeelenWeg { let monitor = WindowsApi::monitor_from_window(self.window.hwnd()?); if monitor == event_data.monitor { self.show()?; - self.set_overlaped_status(false)?; } } _ => {} @@ -90,22 +103,24 @@ impl SeelenWeg { let content_hwnd = unsafe { FindWindowExA( origin_hwnd, - HWND(0), + HWND::default(), pcstr!("Windows.UI.Composition.DesktopWindowContentBridge"), None, ) + .unwrap_or_default() }; - if content_hwnd.0 != 0 { + if !content_hwnd.is_invalid() { let input_hwnd = unsafe { FindWindowExA( content_hwnd, - HWND(0), + HWND::default(), pcstr!("Windows.UI.Input.InputSite.WindowClass"), None, ) + .unwrap_or_default() }; - if input_hwnd.0 != 0 { + if !input_hwnd.is_invalid() { // can fail on volume window island let _ = WindowsApi::show_window(input_hwnd, SW_HIDE); } diff --git a/src/background/seelen_weg/icon_extractor.rs b/src/background/seelen_weg/icon_extractor.rs index 2f83495f..ae8b93e3 100644 --- a/src/background/seelen_weg/icon_extractor.rs +++ b/src/background/seelen_weg/icon_extractor.rs @@ -1,37 +1,31 @@ -use color_eyre::eyre::eyre; -use image::ImageBuffer; -use image::RgbaImage; +use image::{GenericImageView, ImageBuffer, RgbaImage}; use itertools::Itertools; -use tauri::AppHandle; -use tauri::Manager; -use widestring::U16CString; use windows::core::PCWSTR; -use windows::Win32::Graphics::Gdi::CreateCompatibleDC; -use windows::Win32::Graphics::Gdi::DeleteDC; -use windows::Win32::Graphics::Gdi::DeleteObject; -use windows::Win32::Graphics::Gdi::GetDIBits; -use windows::Win32::Graphics::Gdi::SelectObject; -use windows::Win32::Graphics::Gdi::BITMAPINFO; -use windows::Win32::Graphics::Gdi::BITMAPINFOHEADER; -use windows::Win32::Graphics::Gdi::DIB_RGB_COLORS; -use windows::Win32::UI::Shell::ExtractIconExW; -use windows::Win32::UI::WindowsAndMessaging::DestroyIcon; -use windows::Win32::UI::WindowsAndMessaging::GetIconInfoExW; -use windows::Win32::UI::WindowsAndMessaging::HICON; -use windows::Win32::UI::WindowsAndMessaging::ICONINFOEXW; - -use std::arch::x86_64::__m128i; -use std::arch::x86_64::_mm_loadu_si128; -use std::arch::x86_64::_mm_setr_epi8; -#[cfg(target_arch = "x86_64")] -use std::arch::x86_64::_mm_shuffle_epi8; -use std::arch::x86_64::_mm_storeu_si128; +use windows::Win32::{ + Graphics::Gdi::{ + CreateCompatibleDC, DeleteDC, DeleteObject, GetDIBits, SelectObject, BITMAPINFO, + BITMAPINFOHEADER, DIB_RGB_COLORS, + }, + Storage::FileSystem::FILE_FLAGS_AND_ATTRIBUTES, + UI::{ + Controls::{IImageList, ILD_TRANSPARENT}, + Shell::{SHGetFileInfoW, SHGetImageList, SHFILEINFOW, SHGFI_SYSICONINDEX, SHIL_JUMBO}, + WindowsAndMessaging::{DestroyIcon, GetIconInfoExW, HICON, ICONINFOEXW}, + }, +}; + +use std::arch::x86_64::{ + __m128i, _mm_loadu_si128, _mm_setr_epi8, _mm_shuffle_epi8, _mm_storeu_si128, +}; +use std::ffi::OsStr; +use std::io::BufRead; +use std::os::windows::ffi::OsStrExt; use std::path::{Path, PathBuf}; use crate::error_handler::Result; -use crate::modules::uwp::UWP_MANAGER; -use crate::seelen::get_app_handle; -use crate::trace_lock; +use crate::modules::uwp::UwpManager; +use crate::state::application::FULL_STATE; +use crate::windows_api::WindowsApi; /// Convert BGRA to RGBA /// @@ -54,56 +48,6 @@ pub fn bgra_to_rgba(data: &mut [u8]) { } } -pub fn get_images_from_exe(executable_path: &str) -> Result> { - unsafe { - let path_cstr = U16CString::from_str(executable_path).map_err(|_| eyre!("Invalid path"))?; - let path_pcwstr = PCWSTR(path_cstr.as_ptr()); - let num_icons_total = ExtractIconExW(path_pcwstr, -1, None, None, 0); - if num_icons_total == 0 { - return Ok(Vec::new()); // No icons extracted - } - - let mut large_icons = vec![HICON::default(); num_icons_total as usize]; - let mut small_icons = vec![HICON::default(); num_icons_total as usize]; - let num_icons_fetched = ExtractIconExW( - path_pcwstr, - 0, - Some(large_icons.as_mut_ptr()), - Some(small_icons.as_mut_ptr()), - num_icons_total, - ); - - if num_icons_fetched == 0 { - return Ok(Vec::new()); // No icons extracted - } - - let images = large_icons - .iter() - .chain(small_icons.iter()) - .map(convert_hicon_to_rgba_image) - .filter_map(|r| match r { - Ok(img) => Some(img), - Err(e) => { - log::error!("Failed to convert HICON to RgbaImage: {:?}", e); - None - } - }) - .collect_vec(); - - large_icons - .iter() - .chain(small_icons.iter()) - .filter(|icon| !icon.is_invalid()) - .map(|icon| DestroyIcon(*icon)) - .filter_map(|r| r.err()) - .for_each(|e| { - log::error!("Failed to destroy icon: {:?}", e); - }); - - Ok(images) - } -} - pub fn convert_hicon_to_rgba_image(hicon: &HICON) -> Result { unsafe { let mut icon_info = ICONINFOEXW { @@ -112,7 +56,7 @@ pub fn convert_hicon_to_rgba_image(hicon: &HICON) -> Result { }; if !GetIconInfoExW(*hicon, &mut icon_info).as_bool() { - return Err(eyre!("Failed to get icon info").into()); + return Err("Failed to get icon info".into()); } let hdc_screen = CreateCompatibleDC(None); let hdc_mem = CreateCompatibleDC(hdc_screen); @@ -124,7 +68,7 @@ pub fn convert_hicon_to_rgba_image(hicon: &HICON) -> Result { biWidth: icon_info.xHotspot as i32 * 2, biHeight: -(icon_info.yHotspot as i32 * 2), biPlanes: 1, - biBitCount: 32, + biBitCount: 32, // 4 bytes per pixel biCompression: DIB_RGB_COLORS.0, ..Default::default() }, @@ -144,8 +88,9 @@ pub fn convert_hicon_to_rgba_image(hicon: &HICON) -> Result { DIB_RGB_COLORS, ) == 0 { - return Err(eyre!("Failed to get dibits").into()); + return Err("Failed to get dibits".into()); } + // Clean up SelectObject(hdc_mem, hbm_old); DeleteDC(hdc_mem).ok()?; @@ -153,6 +98,10 @@ pub fn convert_hicon_to_rgba_image(hicon: &HICON) -> Result { DeleteObject(icon_info.hbmColor).ok()?; DeleteObject(icon_info.hbmMask).ok()?; + if bmp_info.bmiHeader.biBitCount != 32 { + return Err("Icon is not 32 bit".into()); + } + bgra_to_rgba(buffer.as_mut_slice()); let image = ImageBuffer::from_raw(icon_info.xHotspot * 2, icon_info.yHotspot * 2, buffer) @@ -161,90 +110,201 @@ pub fn convert_hicon_to_rgba_image(hicon: &HICON) -> Result { } } -/// returns the path of the icon extracted from the executable or copied if is an UWP app. -/// -/// If the icon already exists, it returns the path instead overriding, this is needed for allow user custom icons. -pub fn extract_and_save_icon(handle: &AppHandle, exe_path: &str) -> Result { - let gen_icons_paths = handle.path().app_data_dir()?.join("icons"); - if !gen_icons_paths.exists() { - std::fs::create_dir_all(&gen_icons_paths)?; +/// this is the best solution having in consideration that a transparent image and have separated pixels +/// with transparent gaps, so search side by side and crop them is the best approach. +pub fn crop_transparent_borders(rgba_image: &RgbaImage) -> RgbaImage { + let (width, height) = rgba_image.dimensions(); + let mut top = None; + let mut bottom = None; + let mut left = None; + let mut right = None; + + 'outer: for y in 0..height { + for x in 0..width { + let pixel = rgba_image.get_pixel(x, y); + if pixel.0[3] != 0 { + top = Some(y); + break 'outer; + } + } } - let path = PathBuf::from(exe_path); - let filename = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - let saved_icon_path = gen_icons_paths.join(filename.replace(".exe", ".png")); + let top = match top { + Some(top) => top, + None => return RgbaImage::new(1, 1), + }; - if saved_icon_path.exists() { - return Ok(saved_icon_path); + 'outer: for y in (top..height).rev() { + for x in 0..width { + let pixel = rgba_image.get_pixel(x, y); + if pixel.0[3] != 0 { + bottom = Some(y); + break 'outer; + } + } } - log::trace!("Extracting icon for \"{}\"", filename); + let bottom = match bottom { + Some(bottom) => bottom, + None => return RgbaImage::new(1, 1), + }; - if let Some(package) = trace_lock!(UWP_MANAGER).get_from_path(&path) { - if let Some(uwp_icon_path) = package.get_light_icon(&filename) { - log::debug!("Copying UWP icon from \"{}\"", uwp_icon_path.display()); - std::fs::copy(uwp_icon_path, &saved_icon_path)?; - return Ok(saved_icon_path); + 'outer: for x in 0..width { + for y in top..bottom { + let pixel = rgba_image.get_pixel(x, y); + if pixel.0[3] != 0 { + left = Some(x); + break 'outer; + } } } - let images = get_images_from_exe(exe_path); - if let Ok(images) = images { - // icon on index 0 always is the app showed icon - if let Some(icon) = images.first() { - icon.save(&saved_icon_path)?; - return Ok(saved_icon_path); + let left = match left { + Some(left) => left, + None => return RgbaImage::new(1, 1), + }; + + 'outer: for x in (left..width).rev() { + for y in top..bottom { + let pixel = rgba_image.get_pixel(x, y); + if pixel.0[3] != 0 { + right = Some(x); + break 'outer; + } } } - log::trace!("No icon found for \"{}\"", filename); - Err("Failed to extract icon".into()) + let right = match right { + Some(right) => right, + None => return RgbaImage::new(1, 1), + }; + + rgba_image + .view(left, top, right - left + 1, bottom - top + 1) + .to_image() +} + +pub fn get_icon_from_file(path: &Path) -> Result { + unsafe { + let path_str = path.as_os_str().encode_wide().chain(Some(0)).collect_vec(); + + let mut file_info = SHFILEINFOW::default(); + let result = SHGetFileInfoW( + PCWSTR(path_str.as_ptr()), + FILE_FLAGS_AND_ATTRIBUTES(0), + Some(&mut file_info), + std::mem::size_of::() as u32, + SHGFI_SYSICONINDEX, + ); + + // file_info.iIcon = 0 is a valid icon but it is the default icon for files on Windows + // so we will handle this as no icon to avoid generate unnecessary artifacts + if result == 0 || file_info.iIcon == 0 { + return Err("Failed to get icon".into()); + } + + let image_list: IImageList = SHGetImageList(SHIL_JUMBO as i32)?; + // if 256x256 icon is not available, will use the icons with the most color depth and size + // this is useful for some icons where color depth is less than 32, + // example: icon of 124x124 16bits and other 64x64 32bits this will return the 32bits icon + // color depth is prioritized over size + let icon = image_list.GetIcon(file_info.iIcon, ILD_TRANSPARENT.0)?; + let image = crop_transparent_borders(&convert_hicon_to_rgba_image(&icon)?); + DestroyIcon(icon)?; + Ok(image) + } +} + +pub fn get_icon_from_url_file(path: &Path) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + + let mut path = None; + // in theory .url files are encoded in UTF-8 so we don't need to use OsString + for line in reader.lines() { + if let Some(stripped) = line?.strip_prefix("IconFile=") { + path = Some(PathBuf::from(stripped)); + break; + } + } + + let path = match path { + Some(icon_file) => icon_file, + None => return Err("Failed to get icon".into()), + }; + + get_icon_from_file(&path) } /// returns the path of the icon extracted from the executable or copied if is an UWP app. /// /// If the icon already exists, it returns the path instead overriding, this is needed for allow user custom icons. -pub fn extract_and_save_icon_v2>(path: T) -> Result { - let gen_icons_paths = get_app_handle().path().app_data_dir()?.join("icons"); - if !gen_icons_paths.exists() { - std::fs::create_dir_all(&gen_icons_paths)?; +pub fn extract_and_save_icon_from_file>(path: T) -> Result { + let path = path.as_ref(); + if !path.exists() || path.is_dir() { + return Err("Path is not a file".into()); } - let path = path.as_ref(); - let filename = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - let saved_icon_path = gen_icons_paths.join(filename.replace(".exe", ".png")); - - if saved_icon_path.exists() { - return Ok(saved_icon_path); + let state = FULL_STATE.load(); + if let Some(icon) = state.get_icon_by_key(&path.to_string_lossy()) { + return Ok(icon); } - log::trace!("Extracting icon for \"{}\"", filename); + let icon_filename = PathBuf::from(format!("{}.png", uuid::Uuid::new_v4())); + let icon_path = state + .icon_packs_folder() + .join("system") + .join(&icon_filename); - if let Some(package) = trace_lock!(UWP_MANAGER).get_from_path(path) { - if let Some(uwp_icon_path) = package.get_light_icon(&filename) { - log::debug!("Copying UWP icon from \"{}\"", uwp_icon_path.display()); - std::fs::copy(uwp_icon_path, &saved_icon_path)?; - return Ok(saved_icon_path); - } + let file_name = path.file_name().ok_or("Failed to get file name")?; + let ext = path.extension(); + + log::trace!( + "Extracting icon for \"{}\"", + file_name.to_string_lossy().to_string() + ); + + // try get icons for URLs + if ext == Some(OsStr::new("url")) { + let icon = get_icon_from_url_file(path)?; + icon.save(&icon_path)?; + state.push_and_save_system_icon(path.to_string_lossy().as_ref(), &icon_filename)?; + return Ok(icon_path); } - let images = get_images_from_exe(path.to_string_lossy().as_ref()); - if let Ok(images) = images { - // icon on index 0 always is the app showed icon - if let Some(icon) = images.first() { - icon.save(&saved_icon_path)?; - return Ok(saved_icon_path); - } + // try get the icon directly from the file + if let Ok(icon) = get_icon_from_file(path) { + icon.save(&icon_path)?; + state.push_and_save_system_icon(path.to_string_lossy().as_ref(), &icon_filename)?; + return Ok(icon_path); + } + + // if the lnk don't have an icon, try to extract it from the target + if ext == Some(OsStr::new("lnk")) { + let target = WindowsApi::resolve_lnk_target(path)?; + return extract_and_save_icon_from_file(&target); } - log::trace!("No icon found for \"{}\"", filename); Err("Failed to extract icon".into()) } + +/// returns the path of the icon extracted from the app with the specified user model id. +pub fn extract_and_save_icon_umid>(app_umid: T) -> Result { + let app_umid = app_umid.as_ref(); + + let state = FULL_STATE.load(); + if let Some(icon) = state.get_icon_by_key(app_umid) { + return Ok(icon); + } + + let app_icon = UwpManager::get_high_quality_icon_path(app_umid)?; + + let relative_path = PathBuf::from(format!("{}.png", uuid::Uuid::new_v4())); + let image_path = state + .icon_packs_folder() + .join("system") + .join(&relative_path); + std::fs::copy(app_icon, &image_path)?; + state.push_and_save_system_icon(app_umid, &relative_path)?; + Ok(image_path) +} diff --git a/src/background/seelen_weg/mod.rs b/src/background/seelen_weg/mod.rs index ddc36f94..8a2ad5be 100644 --- a/src/background/seelen_weg/mod.rs +++ b/src/background/seelen_weg/mod.rs @@ -1,451 +1,424 @@ -pub mod cli; -pub mod handler; -pub mod hook; -pub mod icon_extractor; - -use std::{collections::HashMap, thread::JoinHandle}; - -use getset::{Getters, MutGetters}; -use icon_extractor::extract_and_save_icon; -use image::{DynamicImage, RgbaImage}; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use seelen_core::state::AppExtraFlag; -use serde::Serialize; -use tauri::{path::BaseDirectory, Emitter, Listener, Manager, WebviewWindow, Wry}; -use win_screenshot::capture::capture_window; -use windows::Win32::{ - Foundation::{BOOL, HWND, LPARAM, RECT}, - UI::WindowsAndMessaging::{ - EnumWindows, HWND_TOPMOST, SWP_NOACTIVATE, SW_HIDE, SW_SHOWNOACTIVATE, SW_SHOWNORMAL, - WS_EX_APPWINDOW, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, - }, -}; - -use crate::{ - error_handler::Result, - log_error, - modules::uwp::UWP_MANAGER, - seelen::{get_app_handle, SEELEN}, - seelen_bar::FancyToolbar, - state::application::FULL_STATE, - trace_lock, - utils::{ - are_overlaped, - constants::{OVERLAP_BLACK_LIST_BY_EXE, OVERLAP_BLACK_LIST_BY_TITLE}, - sleep_millis, - }, - windows_api::{window::Window, AppBarData, AppBarDataState, WindowsApi}, -}; - -lazy_static! { - static ref TITLE_BLACK_LIST: Vec<&'static str> = Vec::from([ - "", - "Task Switching", - "DesktopWindowXamlSource", - "SeelenWeg", - "SeelenWeg Hitbox", - "Seelen Window Manager", - "Seelen Fancy Toolbar", - "Program Manager", - ]); - static ref OPEN_APPS: Mutex> = Mutex::new(Vec::new()); -} - -#[derive(Debug, Serialize, Clone)] -pub struct SeelenWegApp { - hwnd: isize, - exe: String, - title: String, - icon_path: String, - execution_path: String, - creator_hwnd: isize, -} - -#[derive(Getters, MutGetters)] -pub struct SeelenWeg { - window: WebviewWindow, - hitbox: WebviewWindow, - #[getset(get = "pub")] - ready: bool, - hidden: bool, - overlaped: bool, - last_hitbox_rect: Option, -} - -impl Drop for SeelenWeg { - fn drop(&mut self) { - log::info!("Dropping {}", self.window.label()); - log_error!(self.window.destroy()); - log_error!(self.hitbox.destroy()); - } -} - -// SINGLETON -impl SeelenWeg { - pub fn set_active_window(hwnd: HWND) -> Result<()> { - let handle = get_app_handle(); - handle.emit("set-focused-handle", hwnd.0)?; - handle.emit( - "set-focused-executable", - WindowsApi::exe(hwnd).unwrap_or_default(), - )?; - Ok(()) - } - - pub fn missing_icon() -> String { - get_app_handle() - .path() - .resolve("static/icons/missing.png", BaseDirectory::Resource) - .expect("Failed to resolve default icon path") - .to_string_lossy() - .to_uppercase() - } - - pub fn extract_icon(exe_path: &str) -> Result { - Ok(extract_and_save_icon(&get_app_handle(), exe_path)? - .to_string_lossy() - .trim_start_matches("\\\\?\\") - .to_string()) - } - - pub fn contains_app(hwnd: HWND) -> bool { - trace_lock!(OPEN_APPS) - .iter() - .any(|app| app.hwnd == hwnd.0 || app.creator_hwnd == hwnd.0) - } - - pub fn update_app(hwnd: HWND) { - let mut apps = trace_lock!(OPEN_APPS); - let app = apps.iter_mut().find(|app| app.hwnd == hwnd.0); - if let Some(app) = app { - app.title = WindowsApi::get_window_text(hwnd); - get_app_handle() - .emit("update-open-app-info", app.clone()) - .expect("Failed to emit"); - } - } - - pub fn add_hwnd(hwnd: HWND) { - if Self::contains_app(hwnd) { - return; - } - - let window = Window::from(hwnd); - let title = window.title(); - - let creator = match window.get_frame_creator() { - Ok(None) => return, - Ok(Some(creator)) => creator, - Err(_) => window, - }; - - let mut app = SeelenWegApp { - hwnd: hwnd.0, - exe: String::new(), - title, - icon_path: String::new(), - execution_path: String::new(), - creator_hwnd: creator.hwnd().0, - }; - - if let Ok(path) = creator.exe() { - app.exe = path.to_string_lossy().to_string(); - app.icon_path = Self::extract_icon(&app.exe).unwrap_or_else(|_| Self::missing_icon()); - - let exe = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - app.execution_path = match trace_lock!(UWP_MANAGER).get_from_path(&path) { - Some(package) => package - .get_shell_path(&exe) - .unwrap_or_else(|| app.exe.clone()), - None => app.exe.clone(), - }; - } else { - app.icon_path = Self::missing_icon(); - } - - get_app_handle() - .emit("add-open-app", app.clone()) - .expect("Failed to emit"); - - trace_lock!(OPEN_APPS).push(app); - } - - pub fn remove_hwnd(hwnd: HWND) { - trace_lock!(OPEN_APPS).retain(|app| app.hwnd != hwnd.0); - get_app_handle() - .emit("remove-open-app", hwnd.0) - .expect("Failed to emit"); - } - - pub fn should_be_added(hwnd: HWND) -> bool { - let window = Window::from(hwnd); - - if !window.is_visible() || window.parent().is_some() { - return false; - } - - let ex_style = WindowsApi::get_ex_styles(hwnd); - if (ex_style.contains(WS_EX_TOOLWINDOW) || ex_style.contains(WS_EX_NOACTIVATE)) - && !ex_style.contains(WS_EX_APPWINDOW) - { - return false; - } - - if let Ok(frame_creator) = window.get_frame_creator() { - if frame_creator.is_none() { - return false; - } - } - - if WindowsApi::window_is_uwp_suspended(hwnd).unwrap_or_default() { - return false; - } - - if let Ok(path) = window.exe() { - if path.starts_with("C:\\Windows\\SystemApps") { - return false; - } - } - - if let Some(config) = FULL_STATE.load().get_app_config_by_window(hwnd) { - if config.options.contains(&AppExtraFlag::Hidden) { - log::trace!("Skipping by config: {:?}", window); - return false; - } - } - - !TITLE_BLACK_LIST.contains(&window.title().as_str()) - } - - pub fn capture_window(hwnd: HWND) -> Option { - capture_window(hwnd.0).ok().map(|buf| { - let image = RgbaImage::from_raw(buf.width, buf.height, buf.pixels).unwrap_or_default(); - DynamicImage::ImageRgba8(image) - }) - } -} - -// INSTANCE -impl SeelenWeg { - pub fn new(postfix: &str) -> Result { - log::info!("Creating {}/{}", Self::TARGET, postfix); - let (window, hitbox) = Self::create_window(postfix)?; - - let weg = Self { - window, - hitbox, - ready: false, - hidden: false, - overlaped: false, - last_hitbox_rect: None, - }; - - Ok(weg) - } - - fn emit(&self, event: &str, payload: S) -> Result<()> { - self.window.emit_to(self.window.label(), event, payload)?; - Ok(()) - } - - fn is_overlapping(&self, hwnd: HWND) -> bool { - let rect = WindowsApi::get_window_rect_without_shadow(hwnd); - let hitbox_rect = self.last_hitbox_rect.unwrap_or_else(|| { - WindowsApi::get_window_rect_without_shadow(HWND( - self.hitbox.hwnd().expect("Failed to get hitbox handle").0, - )) - }); - are_overlaped(&hitbox_rect, &rect) - } - - pub fn set_overlaped_status(&mut self, is_overlaped: bool) -> Result<()> { - if self.overlaped == is_overlaped { - return Ok(()); - } - - self.overlaped = is_overlaped; - self.last_hitbox_rect = if self.overlaped { - Some(WindowsApi::get_window_rect_without_shadow(HWND( - self.hitbox.hwnd()?.0, - ))) - } else { - None - }; - - self.emit("set-auto-hide", self.overlaped)?; - Ok(()) - } - - pub fn handle_overlaped_status(&mut self, hwnd: HWND) -> Result<()> { - let should_handle_hidden = self.ready - && WindowsApi::is_window_visible(hwnd) - && !OVERLAP_BLACK_LIST_BY_TITLE.contains(&WindowsApi::get_window_text(hwnd).as_str()) - && !OVERLAP_BLACK_LIST_BY_EXE - .contains(&WindowsApi::exe(hwnd).unwrap_or_default().as_str()); - - if !should_handle_hidden { - return Ok(()); - } - - self.set_overlaped_status(self.is_overlapping(hwnd)) - } - - pub fn hide(&mut self) -> Result<()> { - WindowsApi::show_window_async(self.window.hwnd()?, SW_HIDE)?; - WindowsApi::show_window_async(self.hitbox.hwnd()?, SW_HIDE)?; - self.hidden = true; - Ok(()) - } - - pub fn show(&mut self) -> Result<()> { - WindowsApi::show_window_async(self.window.hwnd()?, SW_SHOWNOACTIVATE)?; - WindowsApi::show_window_async(self.hitbox.hwnd()?, SW_SHOWNOACTIVATE)?; - self.hidden = false; - Ok(()) - } - - pub fn ensure_hitbox_zorder(&self) -> Result<()> { - WindowsApi::bring_to(self.hitbox.hwnd()?, HWND_TOPMOST)?; - self.set_positions(WindowsApi::monitor_from_window(self.window.hwnd()?).0)?; - Ok(()) - } - - pub fn set_positions(&self, monitor_id: isize) -> Result<()> { - let rc_work = FancyToolbar::get_work_area_by_monitor(monitor_id)?; - let main_hwnd = HWND(self.window.hwnd()?.0); - // pre set position before resize in case of multiples dpi - WindowsApi::move_window(main_hwnd, &rc_work)?; - WindowsApi::set_position(main_hwnd, None, &rc_work, SWP_NOACTIVATE)?; - Ok(()) - } -} - -impl SeelenWeg { - pub const TITLE: &'static str = "SeelenWeg"; - pub const TITLE_HITBOX: &'static str = "SeelenWeg Hitbox"; - - const TARGET: &'static str = "seelenweg"; - const TARGET_HITBOX: &'static str = "seelenweg-hitbox"; - - fn create_window(postfix: &str) -> Result<(WebviewWindow, WebviewWindow)> { - let manager = get_app_handle(); - - let hitbox = tauri::WebviewWindowBuilder::new( - &manager, - format!("{}/{}", Self::TARGET_HITBOX, postfix), - tauri::WebviewUrl::App("seelenweg-hitbox/index.html".into()), - ) - .title(Self::TITLE_HITBOX) - .maximizable(false) - .minimizable(false) - .resizable(false) - .visible(false) - .decorations(false) - .transparent(true) - .shadow(false) - .skip_taskbar(true) - .always_on_top(true) - .drag_and_drop(false) - .build()?; - - let window = tauri::WebviewWindowBuilder::new( - &manager, - format!("{}/{}", Self::TARGET, postfix), - tauri::WebviewUrl::App("seelenweg/index.html".into()), - ) - .title(Self::TITLE) - .maximizable(false) - .minimizable(false) - .resizable(false) - .visible(false) - .decorations(false) - .transparent(true) - .shadow(false) - .skip_taskbar(true) - .always_on_top(true) - .drag_and_drop(false) - .owner(&hitbox)? - .build()?; - - window.set_ignore_cursor_events(true)?; - - let postfix = postfix.to_string(); - window.once("complete-setup", move |_event| { - std::thread::spawn(move || { - if let Some(monitor) = trace_lock!(SEELEN).monitor_by_name_mut(&postfix) { - if let Some(weg) = monitor.weg_mut() { - weg.ready = true; - } - } - }); - }); - - let label = window.label().to_string(); - window.listen("request-all-open-apps", move |_| { - let handler = get_app_handle(); - let apps = &*trace_lock!(OPEN_APPS); - log_error!(handler.emit_to(&label, "add-multiple-open-apps", apps)); - }); - Ok((window, hitbox)) - } - - pub fn hide_taskbar() -> JoinHandle<()> { - std::thread::spawn(move || match get_taskbars_handles() { - Ok(handles) => { - let mut attempts = 0; - while attempts < 10 && FULL_STATE.load().is_weg_enabled() { - for handle in &handles { - let app_bar = AppBarData::from_handle(*handle); - trace_lock!(TASKBAR_STATE_ON_INIT).insert(handle.0, app_bar.state()); - app_bar.set_state(AppBarDataState::AutoHide); - let _ = WindowsApi::show_window(*handle, SW_HIDE); - } - attempts += 1; - sleep_millis(50); - } - } - Err(err) => log::error!("Failed to get taskbars handles: {:?}", err), - }) - } - - pub fn restore_taskbar() -> Result<()> { - for hwnd in get_taskbars_handles()? { - AppBarData::from_handle(hwnd).set_state( - *trace_lock!(TASKBAR_STATE_ON_INIT) - .get(&hwnd.0) - .unwrap_or(&AppBarDataState::AlwaysOnTop), - ); - WindowsApi::show_window(hwnd, SW_SHOWNORMAL)?; - } - Ok(()) - } -} - -lazy_static! { - pub static ref TASKBAR_STATE_ON_INIT: Mutex> = - Mutex::new(HashMap::new()); - pub static ref FOUNDS: Mutex> = Mutex::new(Vec::new()); - pub static ref TASKBAR_CLASS: Vec<&'static str> = - Vec::from(["Shell_TrayWnd", "Shell_SecondaryTrayWnd",]); -} - -unsafe extern "system" fn enum_windows_proc(hwnd: HWND, _: LPARAM) -> BOOL { - let class = WindowsApi::get_class(hwnd).unwrap_or_default(); - if TASKBAR_CLASS.contains(&class.as_str()) { - trace_lock!(FOUNDS).push(hwnd); - } - true.into() -} - -pub fn get_taskbars_handles() -> Result> { - unsafe { EnumWindows(Some(enum_windows_proc), LPARAM(0))? }; - let mut found = trace_lock!(FOUNDS); - let result = found.clone(); - found.clear(); - Ok(result) -} +pub mod cli; +pub mod handler; +pub mod hook; +pub mod icon_extractor; + +use std::{collections::HashMap, path::PathBuf, thread::JoinHandle}; + +use getset::{Getters, MutGetters}; +use icon_extractor::{extract_and_save_icon_from_file, extract_and_save_icon_umid}; +use image::{DynamicImage, RgbaImage}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use seelen_core::{ + handlers::SeelenEvent, + state::{AppExtraFlag, HideMode, SeelenWegSide}, +}; +use serde::Serialize; +use tauri::{Emitter, Listener, WebviewWindow, Wry}; +use win_screenshot::capture::capture_window; +use windows::Win32::{ + Foundation::{HWND, RECT}, + Graphics::Gdi::HMONITOR, + UI::WindowsAndMessaging::{ + SWP_NOACTIVATE, SW_HIDE, SW_SHOWNOACTIVATE, SW_SHOWNORMAL, WS_EX_APPWINDOW, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, + }, +}; + +use crate::{ + error_handler::Result, + log_error, + seelen::get_app_handle, + seelen_bar::FancyToolbar, + state::application::FULL_STATE, + trace_lock, + utils::{ + are_overlaped, + constants::{Icons, NATIVE_UI_POPUP_CLASSES, OVERLAP_BLACK_LIST_BY_EXE}, + sleep_millis, + }, + windows_api::{window::Window, AppBarData, AppBarDataState, WindowEnumerator, WindowsApi}, +}; + +lazy_static! { + static ref TITLE_BLACK_LIST: Vec<&'static str> = Vec::from([ + "", + "Task Switching", + "DesktopWindowXamlSource", + "Program Manager", + ]); + static ref OPEN_APPS: Mutex> = Mutex::new(Vec::new()); +} + +#[derive(Debug, Serialize, Clone)] +pub struct SeelenWegApp { + hwnd: isize, + exe: PathBuf, + title: String, + icon_path: PathBuf, + execution_path: String, + creator_hwnd: isize, +} + +#[derive(Getters, MutGetters)] +pub struct SeelenWeg { + window: WebviewWindow, + overlaped: bool, + /// Is the rect that the dock should have when it isn't hidden + pub theoretical_rect: RECT, +} + +impl Drop for SeelenWeg { + fn drop(&mut self) { + log::info!("Dropping {}", self.window.label()); + if let Ok(hwnd) = self.window.hwnd() { + AppBarData::from_handle(hwnd).unregister_bar(); + } + log_error!(self.window.destroy()); + } +} + +// SINGLETON +impl SeelenWeg { + pub fn set_active_window(hwnd: HWND) -> Result<()> { + let handle = get_app_handle(); + handle.emit(SeelenEvent::WegSetFocusedHandle, hwnd.0 as isize)?; + handle.emit( + SeelenEvent::WegSetFocusedExecutable, + WindowsApi::exe(hwnd).unwrap_or_default(), + )?; + Ok(()) + } + + pub fn contains_app(hwnd: HWND) -> bool { + let addr = hwnd.0 as isize; + trace_lock!(OPEN_APPS) + .iter() + .any(|app| app.hwnd == addr || app.creator_hwnd == addr) + } + + pub fn update_app(hwnd: HWND) { + let addr = hwnd.0 as isize; + let mut apps = trace_lock!(OPEN_APPS); + let app = apps.iter_mut().find(|app| app.hwnd == addr); + if let Some(app) = app { + app.title = WindowsApi::get_window_text(hwnd); + get_app_handle() + .emit(SeelenEvent::WegUpdateOpenAppInfo, app.clone()) + .expect("Failed to emit"); + } + } + + pub fn enumerate_all_windows() -> Result<()> { + WindowEnumerator::new().for_each(|hwnd| { + if Self::should_be_added(hwnd) { + log_error!(Self::add_hwnd(hwnd)); + } + }) + } + + pub fn add_hwnd(hwnd: HWND) -> Result<()> { + if Self::contains_app(hwnd) { + return Ok(()); + } + + let window = Window::from(hwnd); + let creator = match window.get_frame_creator() { + Ok(None) => return Ok(()), + Ok(Some(creator)) => creator, + Err(_) => window, + }; + + let program_path = creator.exe()?; + let mut app = SeelenWegApp { + hwnd: hwnd.0 as isize, + title: creator.title(), + exe: program_path.clone(), + execution_path: program_path.to_string_lossy().to_string(), + icon_path: Default::default(), + creator_hwnd: creator.hwnd().0 as isize, + }; + + if let Some(umid) = creator.app_user_model_id() { + if umid.contains("!") { + app.execution_path = format!("shell:AppsFolder\\{umid}"); + app.icon_path = + extract_and_save_icon_umid(&umid).unwrap_or_else(|_| Icons::missing_app()) + } else { + app.icon_path = extract_and_save_icon_from_file(program_path) + .unwrap_or_else(|_| Icons::missing_app()); + } + } else { + app.icon_path = extract_and_save_icon_from_file(program_path) + .unwrap_or_else(|_| Icons::missing_app()); + } + + get_app_handle() + .emit(SeelenEvent::WegAddOpenApp, app.clone()) + .expect("Failed to emit"); + + trace_lock!(OPEN_APPS).push(app); + Ok(()) + } + + pub fn remove_hwnd(hwnd: HWND) { + let addr = hwnd.0 as isize; + trace_lock!(OPEN_APPS).retain(|app| app.hwnd != addr); + get_app_handle() + .emit(SeelenEvent::WegRemoveOpenApp, addr) + .expect("Failed to emit"); + } + + pub fn should_be_added(hwnd: HWND) -> bool { + let window = Window::from(hwnd); + let path = match window.process().program_path() { + Ok(path) => path, + Err(_) => return false, + }; + + if path.starts_with("C:\\Windows\\SystemApps") + || !window.is_visible() + || window.parent().is_some() + || window.is_seelen_overlay() + { + return false; + } + + // this class is used for edge tabs to be shown as independent windows on alt + tab + // this only applies when the new tab is created it is binded to explorer.exe for some reason + // maybe we can search/learn more about edge tabs later. + if window.class() == "Windows.Internal.Shell.TabProxyWindow" { + return false; + } + + let ex_style = WindowsApi::get_ex_styles(hwnd); + if (ex_style.contains(WS_EX_TOOLWINDOW) || ex_style.contains(WS_EX_NOACTIVATE)) + && !ex_style.contains(WS_EX_APPWINDOW) + { + return false; + } + + if let Ok(frame_creator) = window.get_frame_creator() { + if frame_creator.is_none() { + return false; + } + } + + if WindowsApi::window_is_uwp_suspended(hwnd).unwrap_or_default() { + return false; + } + + if let Some(config) = FULL_STATE.load().get_app_config_by_window(hwnd) { + if config.options.contains(&AppExtraFlag::Hidden) { + log::trace!("Skipping by config: {:?}", window); + return false; + } + } + + !TITLE_BLACK_LIST.contains(&window.title().as_str()) + } + + pub fn capture_window(hwnd: HWND) -> Option { + capture_window(hwnd.0 as isize).ok().map(|buf| { + let image = RgbaImage::from_raw(buf.width, buf.height, buf.pixels).unwrap_or_default(); + DynamicImage::ImageRgba8(image) + }) + } +} + +// INSTANCE +impl SeelenWeg { + pub fn new(postfix: &str) -> Result { + log::info!("Creating {}/{}", Self::TARGET, postfix); + let weg = Self { + window: Self::create_window(postfix)?, + overlaped: false, + theoretical_rect: RECT::default(), + }; + + Ok(weg) + } + + fn emit(&self, event: &str, payload: S) -> Result<()> { + self.window.emit_to(self.window.label(), event, payload)?; + Ok(()) + } + + fn is_overlapping(&self, hwnd: HWND) -> Result { + let window_rect = WindowsApi::get_inner_window_rect(hwnd)?; + Ok(are_overlaped(&self.theoretical_rect, &window_rect)) + } + + fn set_overlaped_status(&mut self, is_overlaped: bool) -> Result<()> { + if self.overlaped == is_overlaped { + return Ok(()); + } + self.overlaped = is_overlaped; + self.emit(SeelenEvent::WegOverlaped, self.overlaped)?; + Ok(()) + } + + pub fn handle_overlaped_status(&mut self, hwnd: HWND) -> Result<()> { + let window = Window::from(hwnd); + let is_overlaped = self.is_overlapping(hwnd)? + && !window.is_desktop() + && !window.is_seelen_overlay() + && !NATIVE_UI_POPUP_CLASSES.contains(&window.class().as_str()) + && !OVERLAP_BLACK_LIST_BY_EXE + .contains(&WindowsApi::exe(hwnd).unwrap_or_default().as_str()); + self.set_overlaped_status(is_overlaped) + } + + pub fn hide(&mut self) -> Result<()> { + WindowsApi::show_window_async(self.window.hwnd()?, SW_HIDE)?; + self.window.emit_to( + self.window.label(), + SeelenEvent::HandleLayeredHitboxes, + false, + )?; + Ok(()) + } + + pub fn show(&mut self) -> Result<()> { + WindowsApi::show_window_async(self.window.hwnd()?, SW_SHOWNOACTIVATE)?; + self.window.emit_to( + self.window.label(), + SeelenEvent::HandleLayeredHitboxes, + true, + )?; + Ok(()) + } + + pub fn set_position(&mut self, monitor: HMONITOR) -> Result<()> { + let rc_work = FancyToolbar::get_work_area_by_monitor(monitor)?; + let hwnd = HWND(self.window.hwnd()?.0); + + let state = FULL_STATE.load(); + let settings = &state.settings().seelenweg; + let monitor_dpi = WindowsApi::get_device_pixel_ratio(monitor)?; + let total_size = (settings.total_size() as f32 * monitor_dpi) as i32; + + self.theoretical_rect = rc_work; + let mut hidden_rect = rc_work; + match settings.position { + SeelenWegSide::Left => { + self.theoretical_rect.right = self.theoretical_rect.left + total_size; + hidden_rect.right = hidden_rect.left + 1; + } + SeelenWegSide::Right => { + self.theoretical_rect.left = self.theoretical_rect.right - total_size; + hidden_rect.left = hidden_rect.right - 1; + } + SeelenWegSide::Top => { + self.theoretical_rect.bottom = self.theoretical_rect.top + total_size; + hidden_rect.bottom = hidden_rect.top + 1; + } + SeelenWegSide::Bottom => { + self.theoretical_rect.top = self.theoretical_rect.bottom - total_size; + hidden_rect.top = hidden_rect.bottom - 1; + } + } + + let mut abd = AppBarData::from_handle(hwnd); + match settings.hide_mode { + HideMode::Never => { + abd.set_edge(settings.position.into()); + abd.set_rect(self.theoretical_rect); + abd.register_as_new_bar(); + } + _ => abd.unregister_bar(), + }; + + // pre set position for resize in case of multiples dpi + WindowsApi::move_window(hwnd, &rc_work)?; + WindowsApi::set_position(hwnd, None, &rc_work, SWP_NOACTIVATE)?; + Ok(()) + } +} + +impl SeelenWeg { + pub const TITLE: &'static str = "SeelenWeg"; + const TARGET: &'static str = "seelenweg"; + + fn create_window(postfix: &str) -> Result { + let manager = get_app_handle(); + + let window = tauri::WebviewWindowBuilder::new( + manager, + format!("{}/{}", Self::TARGET, postfix), + tauri::WebviewUrl::App("seelenweg/index.html".into()), + ) + .title(Self::TITLE) + .minimizable(false) + .maximizable(false) + .closable(false) + .resizable(false) + .visible(false) + .decorations(false) + .transparent(true) + .shadow(false) + .skip_taskbar(true) + .always_on_top(true) + .build()?; + + window.set_ignore_cursor_events(true)?; + let label = window.label().to_string(); + window.listen("request-all-open-apps", move |_| { + let handler = get_app_handle(); + let apps = &*trace_lock!(OPEN_APPS); + log_error!(handler.emit_to(&label, "add-multiple-open-apps", apps)); + }); + Ok(window) + } + + pub fn hide_taskbar() -> JoinHandle<()> { + std::thread::spawn(move || match get_taskbars_handles() { + Ok(handles) => { + let mut attempts = 0; + while attempts < 10 && FULL_STATE.load().is_weg_enabled() { + for handle in &handles { + let app_bar = AppBarData::from_handle(*handle); + trace_lock!(TASKBAR_STATE_ON_INIT) + .insert(handle.0 as isize, app_bar.state()); + app_bar.set_state(AppBarDataState::AutoHide); + let _ = WindowsApi::show_window(*handle, SW_HIDE); + } + attempts += 1; + sleep_millis(50); + } + } + Err(err) => log::error!("Failed to get taskbars handles: {:?}", err), + }) + } + + pub fn restore_taskbar() -> Result<()> { + for hwnd in get_taskbars_handles()? { + AppBarData::from_handle(hwnd).set_state( + *trace_lock!(TASKBAR_STATE_ON_INIT) + .get(&(hwnd.0 as isize)) + .unwrap_or(&AppBarDataState::AlwaysOnTop), + ); + WindowsApi::show_window(hwnd, SW_SHOWNORMAL)?; + } + Ok(()) + } +} + +lazy_static! { + pub static ref TASKBAR_STATE_ON_INIT: Mutex> = + Mutex::new(HashMap::new()); + pub static ref TASKBAR_CLASS: Vec<&'static str> = + Vec::from(["Shell_TrayWnd", "Shell_SecondaryTrayWnd",]); +} + +pub fn get_taskbars_handles() -> Result> { + let mut founds = Vec::new(); + WindowEnumerator::new().for_each(|hwnd| { + let class = WindowsApi::get_class(hwnd).unwrap_or_default(); + if TASKBAR_CLASS.contains(&class.as_str()) { + founds.push(hwnd); + } + })?; + Ok(founds) +} diff --git a/src/background/seelen_wm/cli.rs b/src/background/seelen_wm/cli.rs deleted file mode 100644 index 25a54090..00000000 --- a/src/background/seelen_wm/cli.rs +++ /dev/null @@ -1,151 +0,0 @@ -use clap::{Command, ValueEnum}; -use seelen_core::state::VirtualDesktopStrategy; -use serde::{Deserialize, Serialize}; -use windows::Win32::Foundation::HWND; - -use crate::error_handler::Result; -use crate::get_subcommands; -use crate::modules::virtual_desk::get_vd_manager; -use crate::seelen::Seelen; -use crate::state::application::FULL_STATE; -use crate::windows_api::WindowsApi; - -use super::WindowManager; - -#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] -pub enum AllowedReservations { - Left, - Right, - Top, - Bottom, - Stack, - Float, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] -pub enum AllowedFocus { - Left, - Right, - Up, - Down, - Latest, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] -pub enum Sizing { - Increase, - Decrease, -} - -get_subcommands![ - /** Open Dev Tools (only works if the app is running in dev mode) */ - Debug, - /** Pause the Seelen Window Manager. */ - Pause, - /** Resume the Seelen Window Manager. */ - Resume, - /** Reserve space for a incoming window. */ - Reserve(side: AllowedReservations => "The position of the new window."), - /** Cancels the current reservation */ - CancelReservation, - /** Switches to the specified workspace. */ - SwitchWorkspace(index: usize => "The index of the workspace to switch to."), - /** Moves the window to the specified workspace. */ - MoveToWorkspace(index: usize => "The index of the workspace to switch to."), - /** Sends the window to the specified workspace */ - SendToWorkspace(index: usize => "The index of the workspace to switch to."), - /** Increases or decreases the size of the window */ - Height(action: Sizing => "What to do with the height."), - /** Increases or decreases the size of the window */ - Width(action: Sizing => "What to do with the width."), - /** Resets the size of the containers in current workspace to the default size. */ - ResetWorkspaceSize, - /** Focuses the window in the specified position. */ - Focus(side: AllowedFocus => "The position of the window to focus."), -]; - -impl WindowManager { - pub const CLI_IDENTIFIER: &'static str = "manager"; - - pub fn get_cli() -> Command { - Command::new(Self::CLI_IDENTIFIER) - .about("Manage the Seelen Window Manager.") - .visible_alias("wm") - .arg_required_else_help(true) - .subcommands(SubCommand::commands()) - } - - pub fn reserve(&self, side: AllowedReservations) -> Result<()> { - self.emit("set-reservation", side)?; - Ok(()) - } - - pub fn discard_reservation(&self) -> Result<()> { - self.emit("set-reservation", ())?; - Ok(()) - } - - pub fn process(&mut self, matches: &clap::ArgMatches) -> Result<()> { - let subcommand = SubCommand::try_from(matches)?; - match subcommand { - SubCommand::Pause => { - self.pause(true, true)?; - } - SubCommand::Resume => { - self.pause(false, true)?; - Seelen::start_ahk_shortcuts()?; - } - SubCommand::SwitchWorkspace(index) => { - self.pseudo_pause()?; - get_vd_manager().switch_to(index)?; - if FULL_STATE.load().settings().virtual_desktop_strategy - == VirtualDesktopStrategy::Native - { - if let Some(next) = Self::get_next_by_order(HWND(0)) { - WindowsApi::async_force_set_foreground(next); - } - } - self.pseudo_resume()?; - } - SubCommand::SendToWorkspace(index) => { - let to_move = WindowsApi::get_foreground_window(); - get_vd_manager().send_to(index, to_move.0)?; - if FULL_STATE.load().settings().virtual_desktop_strategy - == VirtualDesktopStrategy::Native - { - if let Some(next) = Self::get_next_by_order(HWND(0)) { - WindowsApi::async_force_set_foreground(next); - } - } - } - SubCommand::MoveToWorkspace(index) => { - let to_move = WindowsApi::get_foreground_window(); - get_vd_manager().send_to(index, to_move.0)?; - get_vd_manager().switch_to(index)?; - } - SubCommand::Reserve(side) => { - self.reserve(side)?; - } - SubCommand::CancelReservation => { - self.discard_reservation()?; - } - SubCommand::Debug => { - #[cfg(any(debug_assertions, feature = "devtools"))] - self.window.open_devtools(); - } - SubCommand::Height(action) => { - self.emit("update-height", action)?; - } - SubCommand::Width(action) => { - self.emit("update-width", action)?; - } - SubCommand::ResetWorkspaceSize => { - self.emit("reset-workspace-size", ())?; - } - SubCommand::Focus(side) => { - self.emit("focus", side)?; - } - }; - Ok(()) - } -} diff --git a/src/background/seelen_wm/handler.rs b/src/background/seelen_wm/handler.rs deleted file mode 100644 index 4eabdebd..00000000 --- a/src/background/seelen_wm/handler.rs +++ /dev/null @@ -1,70 +0,0 @@ -use tauri::{Webview, Wry}; -use windows::Win32::{ - Foundation::{HWND, RECT}, - UI::WindowsAndMessaging::{ - SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOOWNERZORDER, SWP_NOSENDCHANGING, - SWP_NOZORDER, - }, -}; - -use crate::{seelen::SEELEN, trace_lock, windows_api::WindowsApi}; -use seelen_core::rect::Rect; - -#[tauri::command(async)] -pub fn set_window_position(hwnd: isize, rect: Rect) -> Result<(), String> { - let hwnd = HWND(hwnd); - - if !WindowsApi::is_window(hwnd) || WindowsApi::is_iconic(hwnd) { - return Ok(()); - } - - WindowsApi::unmaximize_window(hwnd)?; - let shadow = WindowsApi::shadow_rect(hwnd)?; - WindowsApi::set_position( - hwnd, - None, - &RECT { - top: rect.top + shadow.top, - left: rect.left + shadow.left, - right: rect.right + shadow.right, - bottom: rect.bottom + shadow.bottom, - }, - SWP_NOACTIVATE - | SWP_NOCOPYBITS - | SWP_NOZORDER - | SWP_NOOWNERZORDER - | SWP_ASYNCWINDOWPOS - | SWP_NOSENDCHANGING, - )?; - Ok(()) -} - -#[tauri::command(async)] -pub fn bounce_handle(webview: Webview, hwnd: isize) { - let monitor_id = webview.label().split("/").last().expect("No monitor ID"); - let monitor_id = monitor_id.parse::().expect("Invalid monitor ID"); - - if let Some(monitor) = trace_lock!(SEELEN).monitor_by_id_mut(monitor_id) { - if let Some(wm) = monitor.wm_mut() { - wm.bounce_handle(HWND(hwnd)); - } - } -} - -#[tauri::command(async)] -pub fn request_focus(hwnd: isize) -> Result<(), String> { - let hwnd = HWND(hwnd); - log::trace!( - "Requesting focus on {:?} - {} , {:?}", - hwnd, - WindowsApi::get_window_text(hwnd), - WindowsApi::exe(hwnd)?, - ); - - if !WindowsApi::is_window(hwnd) { - return Ok(()); - } - - WindowsApi::force_set_foreground(hwnd)?; - Ok(()) -} diff --git a/src/background/seelen_wm/hook.rs b/src/background/seelen_wm/hook.rs deleted file mode 100644 index c43598fe..00000000 --- a/src/background/seelen_wm/hook.rs +++ /dev/null @@ -1,120 +0,0 @@ -use windows::Win32::Foundation::HWND; - -use crate::{ - error_handler::Result, - modules::virtual_desk::VirtualDesktopEvent, - seelen::SEELEN, - trace_lock, - utils::{constants::FORCE_RETILING_AFTER_ADD, sleep_millis}, - windows_api::WindowsApi, - winevent::WinEvent, -}; - -use super::WindowManager; - -impl WindowManager { - pub fn process_vd_event(&mut self, event: &VirtualDesktopEvent) -> Result<()> { - match event { - VirtualDesktopEvent::DesktopChanged { new, old: _ } => { - self.discard_reservation()?; - self.set_active_workspace(new.id())?; - } - VirtualDesktopEvent::WindowChanged(window) => { - if self.is_managed(HWND(*window)) { - self.update_app(HWND(*window))?; - } - } - _ => {} - } - Ok(()) - } - - pub fn process_win_event(&mut self, event: WinEvent, origin: HWND) -> Result<()> { - match event { - WinEvent::SystemMoveSizeStart => { - if self.is_managed(origin) { - self.pseudo_pause()?; - } - } - WinEvent::SystemMoveSizeEnd => { - if self.is_managed(origin) { - self.force_retiling()?; - sleep_millis(35); - self.pseudo_resume()?; - } - } - WinEvent::SystemMinimizeEnd => { - if self.should_be_added(origin) { - self.add_hwnd(origin)?; - } - } - WinEvent::SystemMinimizeStart => { - if self.is_managed(origin) { - self.remove_hwnd(origin)?; - } - } - WinEvent::ObjectHide => { - if self.is_managed(origin) { - self.remove_hwnd(origin)?; - } - } - WinEvent::ObjectDestroy => { - let title = WindowsApi::get_window_text(origin); - if Self::VIRTUAL_PREVIEWS.contains(&title.as_str()) { - self.pseudo_resume()?; - } - if self.is_managed(origin) { - self.remove_hwnd(origin)?; - } - } - WinEvent::ObjectShow | WinEvent::ObjectCreate => { - let title = WindowsApi::get_window_text(origin); - if WindowManager::VIRTUAL_PREVIEWS.contains(&title.as_str()) { - self.pseudo_pause()?; - } - - if self.should_be_added(origin) { - self.set_active_window(origin)?; - if self.add_hwnd(origin)? && FORCE_RETILING_AFTER_ADD.contains(&title) { - // Todo search a better way to do this - std::thread::spawn(|| -> Result<()> { - sleep_millis(250); - if let Some(monitor) = trace_lock!(SEELEN).focused_monitor() { - monitor.wm().as_ref().unwrap().force_retiling()? - } - Ok(()) - }); - }; - } - } - WinEvent::ObjectNameChange => { - if self.should_be_added(origin) { - self.set_active_window(origin)?; - let title = WindowsApi::get_window_text(origin); - if self.add_hwnd(origin)? && FORCE_RETILING_AFTER_ADD.contains(&title) { - // Todo search a better way to do this - std::thread::spawn(|| -> Result<()> { - sleep_millis(250); - if let Some(monitor) = trace_lock!(SEELEN).focused_monitor() { - monitor.wm().as_ref().unwrap().force_retiling()? - } - Ok(()) - }); - }; - } - } - WinEvent::ObjectFocus | WinEvent::SystemForeground => { - self.set_active_window(origin)?; - } - WinEvent::ObjectLocationChange => { - if WindowsApi::is_maximized(origin) { - self.pseudo_pause()?; - } - } - WinEvent::SyntheticFullscreenStart(_) => self.pseudo_pause()?, - WinEvent::SyntheticFullscreenEnd(_) => self.pseudo_resume()?, - _ => {} - }; - Ok(()) - } -} diff --git a/src/background/seelen_wm/mod.rs b/src/background/seelen_wm/mod.rs deleted file mode 100644 index 76a85544..00000000 --- a/src/background/seelen_wm/mod.rs +++ /dev/null @@ -1,358 +0,0 @@ -pub mod cli; -pub mod handler; -pub mod hook; - -use std::sync::atomic::{AtomicIsize, Ordering}; - -use getset::{Getters, MutGetters}; -use serde::Serialize; -use tauri::{AppHandle, Emitter, Listener, WebviewWindow, Wry}; -use windows::Win32::{ - Foundation::{BOOL, HWND, LPARAM}, - Graphics::Gdi::HMONITOR, - UI::WindowsAndMessaging::{ - EnumWindows, HWND_BOTTOM, HWND_TOPMOST, SWP_NOACTIVATE, WS_CAPTION, WS_EX_TOPMOST, - }, -}; - -use crate::{ - error_handler::Result, - log_error, - modules::virtual_desk::get_vd_manager, - seelen::{get_app_handle, SEELEN}, - seelen_bar::FancyToolbar, - seelen_weg::SeelenWeg, - state::{application::FULL_STATE, domain::AppExtraFlag}, - trace_lock, - windows_api::WindowsApi, -}; - -#[derive(Serialize, Clone)] -pub struct ManagingApp { - hwnd: isize, - monitor: String, - desktop_id: String, - is_floating: bool, -} - -#[derive(Getters, MutGetters)] -pub struct WindowManager { - window: WebviewWindow, - monitor: HMONITOR, - apps: Vec, - pub current_virtual_desktop: String, - paused: bool, - #[getset(get = "pub")] - ready: bool, -} - -impl Drop for WindowManager { - fn drop(&mut self) { - log::info!("Dropping {}", self.window.label()); - log_error!(self.window.destroy()); - } -} - -impl WindowManager { - pub const TITLE: &'static str = "Seelen Window Manager"; - pub const TARGET: &'static str = "window-manager"; - pub const VIRTUAL_PREVIEWS: [&'static str; 2] = [ - "Virtual desktop switching preview", - "Virtual desktop hotkey switching preview", - ]; - - pub fn new(monitor: isize) -> Result { - log::info!("Creating Tiling Windows Manager / {}", monitor); - - let handle = get_app_handle(); - - Ok(Self { - window: Self::create_window(&handle, monitor)?, - monitor: HMONITOR(monitor), - apps: Vec::new(), - current_virtual_desktop: get_vd_manager().get_current()?.id(), - paused: true, // paused until complete-setup is called - ready: false, - }) - } - - pub fn emit(&self, event: &str, payload: S) -> Result<()> { - self.window.emit_to(self.window.label(), event, payload)?; - Ok(()) - } - - pub fn is_managed(&self, hwnd: HWND) -> bool { - self.get_app(hwnd).is_some() - } - - pub fn is_floating(&self, hwnd: HWND) -> bool { - self.get_app(hwnd) - .map(|app| app.is_floating) - .unwrap_or(false) - } - - pub fn get_app(&self, hwnd: HWND) -> Option<&ManagingApp> { - self.apps.iter().find(|app| app.hwnd == hwnd.0) - } - - pub fn get_app_mut(&mut self, hwnd: HWND) -> Option<&mut ManagingApp> { - self.apps.iter_mut().find(|app| app.hwnd == hwnd.0) - } - - pub fn set_active_window(&mut self, hwnd: HWND) -> Result<()> { - if WindowsApi::get_window_text(hwnd) == "Task Switching" { - return Ok(()); - } - - log::trace!( - "Setting active window to {} <=> {:?}", - hwnd.0, - WindowsApi::get_window_text(hwnd), - ); - - let hwnd = match self.is_managed(hwnd) - && !self.is_floating(hwnd) - && !WindowsApi::is_maximized(hwnd) - { - true => { - self.pseudo_resume()?; - hwnd - } - false => { - self.pseudo_pause()?; - HWND(0) - } - }; - self.emit("set-active-window", hwnd.0)?; - Ok(()) - } - - pub fn set_active_workspace(&mut self, virtual_desktop_id: String) -> Result<()> { - if virtual_desktop_id == self.current_virtual_desktop { - return Ok(()); - } - log::trace!("Setting active workspace to: {}", virtual_desktop_id); - self.current_virtual_desktop = virtual_desktop_id; - self.window - .emit("set-active-workspace", &self.current_virtual_desktop)?; - Ok(()) - } - - pub fn add_hwnd(&mut self, hwnd: HWND) -> Result { - if self.paused || self.is_managed(hwnd) { - return Ok(false); - } - - let desktop_to_add = if WindowsApi::is_cloaked(hwnd)? { - get_vd_manager().get_by_window(hwnd.0)?.id() - } else { - self.current_virtual_desktop.clone() - }; - - log::trace!( - "Adding {}({}) <=> {} on desktop: {}", - WindowsApi::exe(hwnd).unwrap_or_default(), - hwnd.0, - WindowsApi::get_window_text(hwnd), - desktop_to_add - ); - - let mut is_floating = false; - if let Some(config) = FULL_STATE.load().get_app_config_by_window(hwnd) { - is_floating = config.options.contains(&AppExtraFlag::Float); - } - - let app = ManagingApp { - hwnd: hwnd.0, - monitor: WindowsApi::monitor_name(WindowsApi::monitor_from_window(hwnd))?, - desktop_id: desktop_to_add, - is_floating, - }; - - self.emit("add-window", &app)?; - self.apps.push(app); - Ok(true) - } - - pub fn update_app(&mut self, hwnd: HWND) -> Result<()> { - if self.paused { - return Ok(()); - } - let app = { - let app = match self.get_app_mut(hwnd) { - Some(app) => app, - None => return Ok(()), - }; - - let current_desktop = get_vd_manager().get_by_window(app.hwnd)?.id(); - if app.desktop_id != current_desktop { - app.desktop_id = current_desktop; - } - app.clone() - }; - self.emit("update-window", app)?; - Ok(()) - } - - /** triggered when a window is bounced by the front-end on adding action */ - pub fn bounce_handle(&mut self, hwnd: HWND) { - if let Some(app) = self.get_app_mut(hwnd) { - app.is_floating = true; - } - } - - fn remove_hwnd_no_emit(&mut self, hwnd: HWND) -> bool { - if self.paused || !self.is_managed(hwnd) { - return false; - } - self.apps.retain(|x| x.hwnd != hwnd.0); - true - } - - pub fn remove_hwnd(&mut self, hwnd: HWND) -> Result { - if !self.remove_hwnd_no_emit(hwnd) { - return Ok(false); - } - log::trace!( - "Removing {} <=> {:?}", - hwnd.0, - WindowsApi::get_window_text(hwnd) - ); - self.emit("remove-window", hwnd.0)?; - Ok(true) - } - - pub fn force_retiling(&self) -> Result<()> { - log::trace!("Forcing retiling"); - self.emit("force-retiling", ())?; - Ok(()) - } - - pub fn pseudo_pause(&self) -> Result<()> { - WindowsApi::bring_to(self.window.hwnd()?, HWND_BOTTOM) - } - - pub fn pseudo_resume(&self) -> Result<()> { - WindowsApi::bring_to(self.window.hwnd()?, HWND_TOPMOST) - } - - pub fn pause(&mut self, action: bool, visuals: bool) -> Result<()> { - self.paused = action; - if visuals { - match action { - true => self.pseudo_pause()?, - false => self.pseudo_resume()?, - } - } - Ok(()) - } - - pub fn should_be_added(&self, hwnd: HWND) -> bool { - !self.is_managed(hwnd) - && self.monitor == WindowsApi::monitor_from_window(hwnd) - && Self::should_be_managed(hwnd) - } -} - -// UTILS AND STATICS -impl WindowManager { - fn should_be_managed(hwnd: HWND) -> bool { - if let Some(config) = FULL_STATE.load().get_app_config_by_window(hwnd) { - if config.options.contains(&AppExtraFlag::Force) { - return true; - } - - if config.options.contains(&AppExtraFlag::Unmanage) - || config.options.contains(&AppExtraFlag::Pinned) - { - return false; - } - } - Self::is_manageable_window(hwnd) - } - - pub fn is_manageable_window(hwnd: HWND) -> bool { - let exe = WindowsApi::exe(hwnd); - - if let Ok(exe) = &exe { - if exe.ends_with("ApplicationFrameHost.exe") && SeelenWeg::should_be_added(hwnd) { - return true; - } - } - - // Without admin some apps does not return the exe path so these should be unmanaged - exe.is_ok() - && SeelenWeg::should_be_added(hwnd) - // Ignore windows without a title bar, and top most windows normally are widgets or tools so they should not be managed - && (WindowsApi::get_styles(hwnd).contains(WS_CAPTION) && !WindowsApi::get_ex_styles(hwnd).contains(WS_EX_TOPMOST)) - && !WindowsApi::is_iconic(hwnd) - && (get_vd_manager().uses_cloak() || !WindowsApi::is_cloaked(hwnd).unwrap_or(false)) - } - - fn create_window(handle: &AppHandle, monitor_id: isize) -> Result { - let work_area = FancyToolbar::get_work_area_by_monitor(monitor_id)?; - - let window = tauri::WebviewWindowBuilder::>::new( - handle, - format!("{}/{}", Self::TARGET, monitor_id), - tauri::WebviewUrl::App("seelen_wm/index.html".into()), - ) - .title(Self::TITLE) - .maximizable(false) - .minimizable(false) - .resizable(false) - .visible(true) - .decorations(false) - .transparent(true) - .shadow(false) - .skip_taskbar(true) - .drag_and_drop(false) - .build()?; - - window.set_ignore_cursor_events(true)?; - - let main_hwnd = HWND(window.hwnd()?.0); - WindowsApi::move_window(main_hwnd, &work_area)?; - WindowsApi::set_position(main_hwnd, Some(HWND_TOPMOST), &work_area, SWP_NOACTIVATE)?; - - window.once("complete-setup", move |_event| { - std::thread::spawn(move || -> Result<()> { - if let Some(monitor) = trace_lock!(SEELEN).monitor_by_id_mut(monitor_id) { - if let Some(wm) = monitor.wm_mut() { - wm.paused = false; - wm.ready = true; - wm.window - .emit("set-active-workspace", &wm.current_virtual_desktop)?; - } - } - Ok(()) - }); - }); - - Ok(window) - } - - unsafe extern "system" fn get_next_by_order_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { - // TODO search a way to handle ApplicationFrameHost.exe as well on change of virtual desktop - if WindowManager::is_manageable_window(hwnd) - && hwnd.0 != lparam.0 - && !WindowsApi::exe(hwnd).is_ok_and(|exe| &exe == "ApplicationFrameHost.exe") - { - NEXT.store(hwnd.0, Ordering::SeqCst); - return false.into(); - } - true.into() - } - - pub fn get_next_by_order(hwnd: HWND) -> Option { - NEXT.store(0, Ordering::SeqCst); - unsafe { EnumWindows(Some(Self::get_next_by_order_proc), LPARAM(hwnd.0)) }.ok(); - let result = NEXT.load(Ordering::SeqCst); - if result == 0 { - return None; - } - Some(HWND(result)) - } -} - -static NEXT: AtomicIsize = AtomicIsize::new(0); diff --git a/src/background/seelen_wm_v2/cli.rs b/src/background/seelen_wm_v2/cli.rs new file mode 100644 index 00000000..b6acb7c3 --- /dev/null +++ b/src/background/seelen_wm_v2/cli.rs @@ -0,0 +1,157 @@ +use clap::{Command, ValueEnum}; +use seelen_core::handlers::SeelenEvent; +use serde::{Deserialize, Serialize}; +use tauri::Emitter; + +use crate::error_handler::Result; +use crate::seelen::{get_app_handle, SEELEN}; +use crate::state::application::FULL_STATE; +use crate::windows_api::window::Window; +use crate::windows_api::WindowsApi; +use crate::{get_subcommands, trace_lock}; + +use super::instance::WindowManagerV2; +use super::state::WM_STATE; + +#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] +pub enum AllowedReservations { + Left, + Right, + Top, + Bottom, + Stack, + Float, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] +pub enum AllowedFocus { + Left, + Right, + Up, + Down, + Latest, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] +pub enum Sizing { + Increase, + Decrease, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] +pub enum Axis { + Horizontal, + Vertical, + Top, + Bottom, + Left, + Right, +} + +get_subcommands![ + /** Open Dev Tools (only works if the app is running in dev mode) */ + Debug, + /** Pause the Seelen Window Manager. */ + Pause, + /** Resume the Seelen Window Manager. */ + Resume, + /** Reserve space for a incoming window. */ + Reserve(side: AllowedReservations => "The position of the new window."), + /** Cancels the current reservation */ + CancelReservation, + /** Increases or decreases the size of the window */ + Width(action: Sizing => "What to do with the width."), + /** Increases or decreases the size of the window */ + Height(action: Sizing => "What to do with the height."), + /** Resets the size of the containers in current workspace to the default size. */ + ResetWorkspaceSize, + /** Focuses the window in the specified position. */ + Focus(side: AllowedFocus => "The position of the window to focus."), +]; + +impl WindowManagerV2 { + pub const CLI_IDENTIFIER: &'static str = "manager"; + + pub fn get_cli() -> Command { + Command::new(Self::CLI_IDENTIFIER) + .about("Manage the Seelen Window Manager.") + .visible_alias("wm") + .arg_required_else_help(true) + .subcommands(SubCommand::commands()) + } + + pub fn reserve(&self, _side: AllowedReservations) -> Result<()> { + // self.emit(SeelenEvent::WMSetReservation, side)?; + Ok(()) + } + + pub fn discard_reservation(&self) -> Result<()> { + // self.emit(SeelenEvent::WMSetReservation, ())?; + Ok(()) + } + + pub fn process(matches: &clap::ArgMatches) -> Result<()> { + let subcommand = SubCommand::try_from(matches)?; + match subcommand { + SubCommand::Pause => { + // self.pause(true, true)?; + } + SubCommand::Resume => { + // self.pause(false, true)?; + // Seelen::start_ahk_shortcuts()?; + } + SubCommand::Reserve(_side) => { + // self.reserve(side)?; + } + SubCommand::CancelReservation => { + // self.discard_reservation()?; + } + SubCommand::Debug => + { + #[cfg(any(debug_assertions, feature = "devtools"))] + if let Some(monitor) = trace_lock!(SEELEN).focused_monitor_mut() { + if let Some(wm) = monitor.wm() { + wm.window.open_devtools(); + } + } + } + SubCommand::Width(action) => { + let foreground = Window::from(WindowsApi::get_foreground_window()); + let percentage = match action { + Sizing::Increase => FULL_STATE.load().settings.window_manager.resize_delta, + Sizing::Decrease => -FULL_STATE.load().settings.window_manager.resize_delta, + }; + + let state = trace_lock!(WM_STATE); + let (m, w) = state.update_size(&foreground, Axis::Horizontal, percentage, false)?; + get_app_handle().emit_to( + format!("{}/{}", Self::TARGET, m.id), + SeelenEvent::WMSetLayout, + w.get_root_node(), + )?; + } + SubCommand::Height(action) => { + let foreground = Window::from(WindowsApi::get_foreground_window()); + let percentage = match action { + Sizing::Increase => FULL_STATE.load().settings.window_manager.resize_delta, + Sizing::Decrease => -FULL_STATE.load().settings.window_manager.resize_delta, + }; + + let state = trace_lock!(WM_STATE); + let (m, w) = state.update_size(&foreground, Axis::Vertical, percentage, false)?; + get_app_handle().emit_to( + format!("{}/{}", Self::TARGET, m.id), + SeelenEvent::WMSetLayout, + w.get_root_node(), + )?; + } + SubCommand::ResetWorkspaceSize => { + // self.emit(SeelenEvent::WMResetWorkspaceSize, ())?; + } + SubCommand::Focus(_side) => { + // self.emit(SeelenEvent::WMFocus, side)?; + } + }; + Ok(()) + } +} diff --git a/src/background/seelen_wm_v2/handler.rs b/src/background/seelen_wm_v2/handler.rs new file mode 100644 index 00000000..6283bf3a --- /dev/null +++ b/src/background/seelen_wm_v2/handler.rs @@ -0,0 +1,55 @@ +use windows::Win32::{ + Foundation::{HWND, RECT}, + UI::WindowsAndMessaging::{ + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOSENDCHANGING, + }, +}; + +use crate::{error_handler::Result, windows_api::WindowsApi}; +use seelen_core::rect::Rect; + +#[tauri::command(async)] +pub fn set_window_position(hwnd: isize, rect: Rect) -> Result<()> { + let hwnd = HWND(hwnd as _); + + if !WindowsApi::is_window(hwnd) || WindowsApi::is_iconic(hwnd) { + return Ok(()); + } + + WindowsApi::unmaximize_window(hwnd)?; + + let shadow = WindowsApi::shadow_rect(hwnd)?; + let rect = RECT { + top: rect.top + shadow.top, + left: rect.left + shadow.left, + right: rect.right + shadow.right, + bottom: rect.bottom + shadow.bottom, + }; + + // WindowsApi::move_window(hwnd, &rect)?; + WindowsApi::set_position( + hwnd, + None, + &rect, + SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_ASYNCWINDOWPOS | SWP_NOSENDCHANGING, + )?; + Ok(()) +} + +#[tauri::command(async)] +pub fn request_focus(hwnd: isize) -> Result<()> { + let hwnd = HWND(hwnd as _); + log::trace!( + "Requesting focus on {:?} - {} , {:?}", + hwnd, + WindowsApi::get_window_text(hwnd), + WindowsApi::exe(hwnd)?, + ); + + if !WindowsApi::is_window(hwnd) { + return Ok(()); + } + + WindowsApi::async_force_set_foreground(hwnd); + Ok(()) +} diff --git a/src/background/seelen_wm_v2/hook.rs b/src/background/seelen_wm_v2/hook.rs new file mode 100644 index 00000000..5d5da96c --- /dev/null +++ b/src/background/seelen_wm_v2/hook.rs @@ -0,0 +1,140 @@ +use lazy_static::lazy_static; +use parking_lot::Mutex; +use seelen_core::rect::Rect; +use std::sync::Arc; + +use crate::{ + error_handler::Result, + modules::virtual_desk::VirtualDesktopEvent, + trace_lock, + windows_api::{monitor::Monitor, window::Window}, + winevent::WinEvent, +}; + +use super::{cli::Axis, state::WM_STATE, WindowManagerV2}; + +lazy_static! { + static ref SystemMoveSizeStartRect: Arc> = Arc::new(Mutex::new(Rect::default())); + static ref SystemMoveSizeStartMonitor: Arc> = + Arc::new(Mutex::new(Monitor::from(0))); +} + +impl WindowManagerV2 { + pub fn process_vd_event(event: &VirtualDesktopEvent) -> Result<()> { + match event { + VirtualDesktopEvent::DesktopChanged { new, old: _ } => { + // Self::discard_reservation()?; + Self::workspace_changed(new)?; + } + VirtualDesktopEvent::WindowChanged(window) => { + let window = &Window::from(*window); + if Self::is_managed(window) { + log::trace!("window changed: {:?}", window); + Self::remove(window)?; + Self::add(window)?; + } + } + _ => {} + } + Ok(()) + } + + fn system_move_size_end(window: &Window) -> Result<()> { + if !Self::is_managed(window) { + return Ok(()); + } + + if *SystemMoveSizeStartMonitor.lock() != window.monitor() { + log::trace!("window moved of monitor"); + Self::remove(window)?; + Self::add(window)?; + Self::set_overlay_visibility(true)?; + return Ok(()); + } + + let initial_rect = SystemMoveSizeStartRect.lock(); + let initial_width = (initial_rect.right - initial_rect.left) as f32; + let initial_height = (initial_rect.bottom - initial_rect.top) as f32; + + let rect = window.inner_rect()?; + let new_width = (rect.right - rect.left) as f32; + let new_height = (rect.bottom - rect.top) as f32; + + if initial_width != new_width { + let percentage_diff = (new_width - initial_width) / initial_width * 100.0; + let axis = if rect.left == initial_rect.left { + Axis::Right + } else { + Axis::Left + }; + log::trace!("window width changed by: {}%", percentage_diff); + let state = trace_lock!(WM_STATE); + let (m, w) = state.update_size(window, axis, percentage_diff, true)?; + Self::render_workspace(&m.id, w)?; + } + + if initial_height != new_height { + let percentage_diff = (new_height - initial_height) / initial_height * 100.0; + let axis = if rect.top == initial_rect.top { + Axis::Bottom + } else { + Axis::Top + }; + log::trace!("window height changed by: {}%", percentage_diff); + let state = trace_lock!(WM_STATE); + let (m, w) = state.update_size(window, axis, percentage_diff, true)?; + Self::render_workspace(&m.id, w)?; + } + + Self::force_retiling()?; + Self::set_overlay_visibility(true)?; + Ok(()) + } + + pub fn process_win_event(event: WinEvent, window: &Window) -> Result<()> { + match event { + WinEvent::SystemMoveSizeStart => { + if Self::is_managed(window) { + Self::set_overlay_visibility(false)?; + *SystemMoveSizeStartRect.lock() = window.inner_rect()?; + *SystemMoveSizeStartMonitor.lock() = window.monitor(); + } + } + WinEvent::SystemMoveSizeEnd => Self::system_move_size_end(window)?, + WinEvent::ObjectCreate | WinEvent::ObjectShow | WinEvent::SystemMinimizeEnd => { + if !Self::is_managed(window) && Self::should_be_managed(window.hwnd()) { + Self::add(window)?; + Self::set_overlay_visibility(true)?; + } + } + WinEvent::ObjectDestroy | WinEvent::ObjectHide | WinEvent::SystemMinimizeStart => { + if Self::is_managed(window) { + Self::remove(window)?; + } + } + WinEvent::ObjectFocus | WinEvent::SystemForeground => { + Self::set_active_window(window)?; + Self::set_overlay_visibility(Self::is_managed(window))?; + } + // apps like firefox doesn't launch ObjectCreate + WinEvent::ObjectNameChange => { + if window.is_foreground() + && !Self::is_managed(window) + && Self::should_be_managed(window.hwnd()) + { + Self::add(window)?; + Self::set_overlay_visibility(true)?; + } + } + WinEvent::ObjectLocationChange => { + if window.is_foreground() && window.is_maximized() { + Self::set_overlay_visibility(false)?; + } + } + WinEvent::SyntheticFullscreenStart(_) => Self::set_overlay_visibility(false)?, + WinEvent::SyntheticFullscreenEnd(_) => Self::set_overlay_visibility(true)?, + _ => {} + }; + Ok(()) + } +} diff --git a/src/background/seelen_wm_v2/instance.rs b/src/background/seelen_wm_v2/instance.rs new file mode 100644 index 00000000..d5351830 --- /dev/null +++ b/src/background/seelen_wm_v2/instance.rs @@ -0,0 +1,92 @@ +use getset::{Getters, MutGetters}; +use seelen_core::handlers::SeelenEvent; +use tauri::{Emitter, Listener, WebviewWindow}; +use windows::Win32::{Graphics::Gdi::HMONITOR, UI::WindowsAndMessaging::SWP_ASYNCWINDOWPOS}; + +use crate::{ + error_handler::Result, log_error, modules::virtual_desk::get_vd_manager, + seelen::get_app_handle, seelen_bar::FancyToolbar, seelen_wm_v2::state::WM_STATE, trace_lock, + windows_api::WindowsApi, +}; + +#[derive(Getters, MutGetters)] +pub struct WindowManagerV2 { + pub window: WebviewWindow, +} + +impl Drop for WindowManagerV2 { + fn drop(&mut self) { + log::info!("Dropping {}", self.window.label()); + log_error!(self.window.destroy()); + } +} + +impl WindowManagerV2 { + pub const TITLE: &'static str = "Seelen Window Manager"; + pub const TARGET: &'static str = "window-manager"; + + pub fn new(monitor_id: &str) -> Result { + log::info!("Creating Tiling Windows Manager"); + Ok(Self { + window: Self::create_window(monitor_id)?, + }) + } + + fn create_window(monitor_id: &str) -> Result { + let window = tauri::WebviewWindowBuilder::new( + get_app_handle(), + format!("{}/{}", Self::TARGET, monitor_id), + tauri::WebviewUrl::App("seelen_wm_v2/index.html".into()), + ) + .title(Self::TITLE) + .minimizable(false) + .maximizable(false) + .closable(false) + .resizable(false) + .visible(true) + .decorations(false) + .transparent(true) + .shadow(false) + .skip_taskbar(true) + .drag_and_drop(false) + .always_on_top(true) + .build()?; + + window.set_ignore_cursor_events(true)?; + + let monitor_id = monitor_id.to_owned(); + window.listen("complete-setup", move |_event| { + let monitor_id = monitor_id.clone(); + std::thread::spawn(move || -> Result<()> { + let app = get_app_handle(); + let mut state = trace_lock!(WM_STATE); + + if let Some(m) = state.get_monitor_mut(&monitor_id) { + let workspace_id = get_vd_manager().get_current()?.id(); + let w = m.get_workspace_mut(&workspace_id); + app.emit_to( + format!("{}/{}", Self::TARGET, monitor_id), + SeelenEvent::WMSetLayout, + w.get_root_node(), + )?; + } + + app.emit( + SeelenEvent::WMSetActiveWindow, + WindowsApi::get_foreground_window().0 as isize, + )?; + Ok(()) + }); + }); + + Ok(window) + } + + pub fn set_position(&self, monitor: HMONITOR) -> Result<()> { + let work_area = FancyToolbar::get_work_area_by_monitor(monitor)?; + let main_hwnd = self.window.hwnd()?; + WindowsApi::move_window(main_hwnd, &work_area)?; + WindowsApi::set_position(main_hwnd, None, &work_area, SWP_ASYNCWINDOWPOS)?; + Ok(()) + } +} diff --git a/src/background/seelen_wm_v2/mod.rs b/src/background/seelen_wm_v2/mod.rs new file mode 100644 index 00000000..853f3afc --- /dev/null +++ b/src/background/seelen_wm_v2/mod.rs @@ -0,0 +1,179 @@ +pub mod cli; +pub mod handler; +pub mod hook; +pub mod instance; +pub mod node_impl; +pub mod state; + +use instance::WindowManagerV2; +use seelen_core::{handlers::SeelenEvent, state::AppExtraFlag}; +use state::{WmV2StateWorkspace, WM_STATE}; +use tauri::Emitter; +use windows::Win32::{ + Foundation::HWND, + UI::WindowsAndMessaging::{WS_CAPTION, WS_EX_TOPMOST}, +}; + +use crate::{ + error_handler::Result, + log_error, + modules::virtual_desk::{get_vd_manager, VirtualDesktop}, + seelen::get_app_handle, + seelen_weg::SeelenWeg, + state::application::FULL_STATE, + trace_lock, + windows_api::{monitor::Monitor, window::Window, WindowEnumerator, WindowsApi}, +}; + +impl WindowManagerV2 { + fn is_manageable_window(hwnd: HWND) -> bool { + let exe = WindowsApi::exe(hwnd); + + if let Ok(exe) = &exe { + if exe.ends_with("ApplicationFrameHost.exe") && SeelenWeg::should_be_added(hwnd) { + return true; + } + } + + // Without admin some apps does not return the exe path so these should be unmanaged + exe.is_ok() + && SeelenWeg::should_be_added(hwnd) + // Ignore windows without a title bar, and top most windows normally are widgets or tools so they should not be managed + && (WindowsApi::get_styles(hwnd).contains(WS_CAPTION) && !WindowsApi::get_ex_styles(hwnd).contains(WS_EX_TOPMOST)) + && !WindowsApi::is_iconic(hwnd) + && (get_vd_manager().uses_cloak() || !WindowsApi::is_cloaked(hwnd).unwrap_or(false)) + } + + fn should_be_managed(hwnd: HWND) -> bool { + if let Some(config) = FULL_STATE.load().get_app_config_by_window(hwnd) { + if config.options.contains(&AppExtraFlag::Force) { + return true; + } + + if config.options.contains(&AppExtraFlag::Unmanage) + || config.options.contains(&AppExtraFlag::Pinned) + { + return false; + } + } + Self::is_manageable_window(hwnd) + } + + fn is_managed(window: &Window) -> bool { + trace_lock!(WM_STATE).contains(window) + } + + fn force_retiling() -> Result<()> { + get_app_handle().emit(SeelenEvent::WMForceRetiling, ())?; + Ok(()) + } + + fn render_workspace(monitor_id: &str, w: &WmV2StateWorkspace) -> Result<()> { + get_app_handle().emit_to( + format!("{}/{}", Self::TARGET, monitor_id), + SeelenEvent::WMSetLayout, + w.get_root_node(), + )?; + Ok(()) + } + + fn set_overlay_visibility(visible: bool) -> Result { + get_app_handle().emit(SeelenEvent::WMSetOverlayVisibility, visible)?; + Ok(()) + } + + fn set_active_window(window: &Window) -> Result { + get_app_handle().emit(SeelenEvent::WMSetActiveWindow, window.address())?; + Ok(()) + } + + fn add(window: &Window) -> Result<()> { + let mut state = trace_lock!(WM_STATE); + let vd_manager = get_vd_manager(); + let current_workspace_id = vd_manager.get_current()?.id(); + + let mut monitor_id = window.monitor().id()?; + let mut workspace_id = window.workspace()?.id(); + + if let Some(config) = FULL_STATE.load().get_app_config_by_window(window.hwnd()) { + if let Some(index) = config.bound_monitor { + if let Some(monitor) = Monitor::at(index) { + monitor_id = monitor.id()?; + } + } + if let Some(index) = config.bound_workspace { + let addr = window.address(); + vd_manager.send_to(index, addr)?; + std::thread::sleep(std::time::Duration::from_millis(20)); + vd_manager.switch_to(index)?; + if let Some(workspace) = vd_manager.get(index)? { + workspace_id = workspace.id(); + } + } + } + + if let Some(monitor) = state.get_monitor_mut(&monitor_id) { + let workspace = monitor.get_workspace_mut(&workspace_id); + workspace.add_window(window); + if workspace_id == current_workspace_id { + get_app_handle().emit_to( + format!("{}/{}", Self::TARGET, monitor_id), + SeelenEvent::WMSetLayout, + workspace.get_root_node(), + )?; + } + } + Ok(()) + } + + fn remove(window: &Window) -> Result<()> { + let mut state = trace_lock!(WM_STATE); + let current_workspace = get_vd_manager().get_current()?.id(); + + // TODO this can be optimized, later + for (monitor_id, monitor) in state.monitors.iter_mut() { + for (workspace_id, workspace) in monitor.workspaces.iter_mut() { + workspace.remove_window(window); + if workspace_id == ¤t_workspace { + get_app_handle().emit_to( + format!("{}/{}", Self::TARGET, monitor_id), + SeelenEvent::WMSetLayout, + workspace.get_root_node(), + )?; + } + } + } + Ok(()) + } + + fn workspace_changed(current: &VirtualDesktop) -> Result<()> { + let mut state = trace_lock!(WM_STATE); + let workspace_id = current.id(); + for (monitor_id, monitor) in state.monitors.iter_mut() { + let workspace = monitor.get_workspace_mut(&workspace_id); + get_app_handle().emit_to( + format!("{}/{}", Self::TARGET, monitor_id), + SeelenEvent::WMSetLayout, + workspace.get_root_node(), + )?; + } + Ok(()) + } + + pub fn clear_state() { + trace_lock!(WM_STATE).monitors.clear(); + } + + pub fn init_state() -> Result<()> { + trace_lock!(WM_STATE).init() + } + + pub fn enumerate_all_windows() -> Result<()> { + WindowEnumerator::new().for_each(|hwnd| { + let window = Window::from(hwnd); + if !Self::is_managed(&window) && Self::should_be_managed(hwnd) { + log_error!(Self::add(&window)); + } + }) + } +} diff --git a/src/background/seelen_wm_v2/node_impl.rs b/src/background/seelen_wm_v2/node_impl.rs new file mode 100644 index 00000000..4c2e140d --- /dev/null +++ b/src/background/seelen_wm_v2/node_impl.rs @@ -0,0 +1,279 @@ +use evalexpr::{context_map, eval_with_context, HashMapContext}; +use itertools::Itertools; +use seelen_core::state::WmNode; + +use crate::{error_handler::Result, modules::input::domain::Point, windows_api::window::Window}; + +#[derive(Debug)] +pub struct WmNodeImpl(WmNode); + +impl WmNodeImpl { + pub fn new(node: WmNode) -> Self { + Self(node) + } + + pub fn inner(&self) -> &WmNode { + &self.0 + } + + pub fn inner_mut(&mut self) -> &mut WmNode { + &mut self.0 + } + + fn is_node_enabled(condition: Option<&String>, context: &HashMapContext) -> bool { + match condition { + None => true, + Some(condition) => { + let result = eval_with_context(condition, context).and_then(|v| v.as_boolean()); + result.is_ok_and(|is_enabled| is_enabled) + } + } + } + + /// will fail if the node is full + fn _try_add_window(node: &mut WmNode, window: &Window, context: &HashMapContext) -> Result<()> { + let addr = window.address(); + + if !Self::is_node_enabled(node.condition(), context) { + return Err("DISABLED".into()); + } + + match node { + WmNode::Leaf(leaf) => { + if leaf.handle.is_some() { + return Err("FULL".into()); + } + + leaf.handle = Some(addr); + } + WmNode::Stack(_stack) => { + // a node of type stack only can add windows when the user uses the stack shortcut + return Err("FULL".into()); + } + WmNode::Fallback(fallback) => { + fallback.handles.push(addr); + fallback.active = Some(addr); + } + WmNode::Vertical(vertical) => { + for child in vertical + .children + .iter_mut() + .sorted_by(|a, b| a.priority().cmp(&b.priority())) + { + if Self::_try_add_window(child, window, context).is_ok() { + return Ok(()); + } + } + return Err("FULL".into()); + } + WmNode::Horizontal(horizontal) => { + for child in horizontal + .children + .iter_mut() + .sorted_by(|a, b| a.priority().cmp(&b.priority())) + { + if Self::_try_add_window(child, window, context).is_ok() { + return Ok(()); + } + } + return Err("FULL".into()); + } + } + Ok(()) + } + + /// will drain the node and return a list of window handles + fn _drain(root: &mut WmNode) -> Vec { + let mut handles = Vec::new(); + match root { + WmNode::Leaf(leaf) => { + if let Some(handle) = leaf.handle.take() { + handles.push(handle); + } + } + WmNode::Stack(stack) => { + handles.append(&mut stack.handles); + } + WmNode::Fallback(fallback) => { + handles.append(&mut fallback.handles); + } + WmNode::Vertical(vertical) => { + for child in vertical + .children + .iter_mut() + .sorted_by(|a, b| a.priority().cmp(&b.priority())) + { + handles.append(&mut Self::_drain(child)); + } + } + WmNode::Horizontal(horizontal) => { + for child in horizontal + .children + .iter_mut() + .sorted_by(|a, b| a.priority().cmp(&b.priority())) + { + handles.append(&mut Self::_drain(child)); + } + } + } + handles + } + + fn _trace<'a>(root: &'a WmNode, window: &Window) -> Vec<&'a WmNode> { + let mut nodes = Vec::new(); + match root { + WmNode::Leaf(leaf) => { + if leaf.handle == Some(window.address()) { + nodes.push(root); + } + } + WmNode::Stack(stack) => { + if stack.handles.contains(&window.address()) { + nodes.push(root); + } + } + WmNode::Fallback(fallback) => { + if fallback.handles.contains(&window.address()) { + nodes.push(root); + } + } + WmNode::Vertical(vertical) => { + for child in vertical.children.iter() { + let mut sub_trace = Self::_trace(child, window); + if !sub_trace.is_empty() { + nodes.push(root); + nodes.append(&mut sub_trace); + break; + } + } + } + WmNode::Horizontal(horizontal) => { + for child in horizontal.children.iter() { + let mut sub_trace = Self::_trace(child, window); + if !sub_trace.is_empty() { + nodes.push(root); + nodes.append(&mut sub_trace); + break; + } + } + } + } + nodes + } + + fn _get_node_at_point<'a>( + root: &'a mut WmNode, + point: &Point, + ) -> Result> { + match root { + WmNode::Leaf(leaf) => { + if let Some(handle) = leaf.handle { + let window = Window::from(handle); + if point.is_inside_rect(&window.inner_rect()?) { + return Ok(Some(root)); + } + } + } + WmNode::Stack(stack) => { + if let Some(handle) = stack.active { + let window = Window::from(handle); + if point.is_inside_rect(&window.inner_rect()?) { + return Ok(Some(root)); + } + } + } + WmNode::Fallback(fallback) => { + if let Some(handle) = fallback.active { + let window = Window::from(handle); + if point.is_inside_rect(&window.inner_rect()?) { + return Ok(Some(root)); + } + } + } + WmNode::Vertical(vertical) => { + for child in vertical.children.iter_mut() { + let node = Self::_get_node_at_point(child, point)?; + if node.is_some() { + return Ok(node); + } + } + } + WmNode::Horizontal(horizontal) => { + for child in horizontal.children.iter_mut() { + let node = Self::_get_node_at_point(child, point)?; + if node.is_some() { + return Ok(node); + } + } + } + }; + Ok(None) + } + + fn create_context(len: usize, is_reindexing: bool) -> HashMapContext { + context_map! { + "managed" => len as i64, + "is_reindexing" => is_reindexing + } + .expect("Failed to create context") + } + + /// If adding the new window is successful, a reindexing will be done. + /// + /// **Note:** Reindexing can fail on add some windows so it will return failed handles as residual + pub fn try_add_window(&mut self, window: &Window) -> Vec { + let len = self.inner().len(); + let context = Self::create_context(len, false); + if Self::_try_add_window(self.inner_mut(), window, &context).is_err() { + return vec![window.address()]; + } + + // reindexing to handle logical condition like `managed < 4` + let context = Self::create_context(len + 1, true); + let handles = Self::_drain(self.inner_mut()); + let mut residual = Vec::new(); + for handle in handles { + if Self::_try_add_window(self.inner_mut(), &Window::from(handle), &context).is_err() { + residual.push(handle); + } + } + residual + } + + /// Will make a reindexing ignoring the removed window. + /// + /// **Note:** Reindexing can fail on add some windows so it will return failed handles as residual + pub fn remove_window(&mut self, window: &Window) -> Vec { + let handles = Self::_drain(self.inner_mut()); + let context = Self::create_context( + if handles.contains(&window.address()) { + handles.len() - 1 + } else { + handles.len() + }, + true, + ); + + let mut residual = Vec::new(); + for handle in handles { + if handle != window.address() + && Self::_try_add_window(self.inner_mut(), &Window::from(handle), &context).is_err() + { + residual.push(handle); + } + } + residual + } + + pub fn contains(&self, window: &Window) -> bool { + !Self::_trace(self.inner(), window).is_empty() + } + + pub fn trace(&self, window: &Window) -> Vec<&WmNode> { + Self::_trace(self.inner(), window) + } + + pub fn get_node_at_point(&mut self, point: &Point) -> Result> { + Self::_get_node_at_point(self.inner_mut(), point) + } +} diff --git a/src/background/seelen_wm_v2/state.rs b/src/background/seelen_wm_v2/state.rs new file mode 100644 index 00000000..216a1fb7 --- /dev/null +++ b/src/background/seelen_wm_v2/state.rs @@ -0,0 +1,308 @@ +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::Arc, +}; + +use itertools::Itertools; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use seelen_core::state::{NoFallbackBehavior, WManagerLayoutInfo, WmNode}; + +use crate::{ + error_handler::Result, + modules::{input::domain::Point, virtual_desk::get_vd_manager}, + state::application::FULL_STATE, + windows_api::{monitor::Monitor, window::Window, MonitorEnumerator, WindowsApi}, +}; + +use super::{cli::Axis, node_impl::WmNodeImpl}; + +lazy_static! { + pub static ref WM_STATE: Arc> = Arc::new(Mutex::new({ + let mut state = WmV2State::default(); + state + .init() + .expect("Failed to initialize Window Manager State"); + state + })); +} + +#[derive(Debug)] +pub struct WmV2StateWorkspace { + root: Option, + layout_info: Option, + no_fallback_behavior: NoFallbackBehavior, +} + +#[derive(Debug, Default)] +pub struct WmV2StateMonitor { + pub id: String, + pub workspaces: HashMap, +} + +#[derive(Debug, Default)] +pub struct WmV2State { + pub monitors: HashMap, +} + +impl WmV2StateWorkspace { + pub fn new(monitor_idx: usize, workspace_idx: usize) -> Self { + let mut workspace = Self { + layout_info: None, + root: None, + no_fallback_behavior: NoFallbackBehavior::Float, + }; + + let settings = FULL_STATE.load(); + let layout_id = settings.get_wm_layout_id(monitor_idx, workspace_idx); + if let Some(l) = settings.layouts.get(&layout_id).cloned() { + workspace.layout_info = Some(l.info); + workspace.root = Some(WmNodeImpl::new(l.structure)); + workspace.no_fallback_behavior = l.no_fallback_behavior; + } + + workspace + } + + pub fn get_root_node(&self) -> Option<&WmNode> { + self.root.as_ref().map(|n| n.inner()) + } + + pub fn add_window(&mut self, window: &Window) { + if let Some(node) = &mut self.root { + let residual = node.try_add_window(window); + if !residual.is_empty() { + log::warn!("Current Layout is full, and fallback container was not found"); + // TODO + } + } + } + + pub fn remove_window(&mut self, window: &Window) { + if let Some(node) = &mut self.root { + let residual = node.remove_window(window); + if !residual.is_empty() { + log::warn!("Current Layout is full, and fallback container was not found"); + // TODO + } + } + } + + pub fn contains(&self, window: &Window) -> bool { + self.root.as_ref().map_or(false, |n| n.contains(window)) + } + + pub fn trace_to(&self, window: &Window) -> Vec<&WmNode> { + self.root.as_ref().map_or(vec![], |n| n.trace(window)) + } + + pub fn get_node_at_point(&mut self, point: &Point) -> Option<&mut WmNode> { + if let Some(root) = &mut self.root { + return root.get_node_at_point(point).ok()?; + } + None + } +} + +impl WmV2StateMonitor { + pub fn create_workspace(monitor_idx: usize, workspace_id: &str) -> Result { + for (workspace_idx, w) in get_vd_manager().get_all()?.iter().enumerate() { + if w.id() == workspace_id { + return Ok(WmV2StateWorkspace::new(monitor_idx, workspace_idx)); + } + } + Err("Invalid workspace id".into()) + } + + pub fn get_workspace_mut(&mut self, workspace_id: &str) -> &mut WmV2StateWorkspace { + match self.workspaces.entry(workspace_id.to_string()) { + Entry::Occupied(e) => e.into_mut(), + Entry::Vacant(e) => { + let monitor_idx = Monitor::by_id(&self.id) + .expect("Failed to get monitor") + .index() + .expect("Failed to get monitor index"); + let new_workspace = Self::create_workspace(monitor_idx, workspace_id) + .expect("Failed to ensure workspace"); + e.insert(new_workspace) + } + } + } + + pub fn contains(&self, window: &Window) -> bool { + self.workspaces.values().any(|w| w.contains(window)) + } + + pub fn trace_to(&self, window: &Window) -> Option<(&WmV2StateWorkspace, Vec<&WmNode>)> { + for w in self.workspaces.values() { + let trace = w.trace_to(window); + if !trace.is_empty() { + return Some((w, trace)); + } + } + None + } +} + +impl WmV2State { + /// will enumarate all monitors and workspaces + pub fn init(&mut self) -> Result<()> { + let workspaces = get_vd_manager().get_all()?; + for (monitor_idx, hmonitor) in MonitorEnumerator::get_all()?.into_iter().enumerate() { + let id = WindowsApi::monitor_name(hmonitor)?; + if self.monitors.contains_key(&id) { + continue; + } + + let mut monitor = WmV2StateMonitor::default(); + for (workspace_idx, w) in workspaces.iter().enumerate() { + if monitor.workspaces.contains_key(&w.id()) { + continue; + } + + monitor + .workspaces + .insert(w.id(), WmV2StateWorkspace::new(monitor_idx, workspace_idx)); + } + + monitor.id = id.clone(); + self.monitors.insert(id, monitor); + } + Ok(()) + } + + pub fn get_monitor_mut(&mut self, monitor_id: &str) -> Option<&mut WmV2StateMonitor> { + self.monitors.get_mut(monitor_id) + } + + pub fn contains(&self, window: &Window) -> bool { + self.trace_to(window).is_some() + } + + pub fn trace_to( + &self, + window: &Window, + ) -> Option<(&WmV2StateMonitor, &WmV2StateWorkspace, Vec<&WmNode>)> { + for m in self.monitors.values() { + if let Some((w, trace)) = m.trace_to(window) { + return Some((m, w, trace)); + } + } + None + } + + pub fn get_node_at_point(&mut self, point: &Point) -> Option<&mut WmNode> { + let monitor = Monitor::from(point); + if let Some(m) = self.monitors.get_mut(&monitor.id().ok()?) { + let current_workspace = get_vd_manager().get_current().ok()?.id(); + if let Some(w) = m.workspaces.get_mut(¤t_workspace) { + return w.get_node_at_point(point); + } + } + None + } + + /// # Parameters + /// + /// - `window`: A reference to the window whose size is being updated. + /// - `axis`: The axis along which the size update will occur (horizontal or vertical). + /// - `percentage`: The percentage by which the window size will be updated. Can be positive or negative. + /// - `relative`: Determines how the percentage is interpreted. If `true`, the percentage is relative to + /// the current window size. If `false`, it's relative to the total workspace size. + /// + pub fn update_size( + &self, + window: &Window, + axis: Axis, + percentage: f32, + relative: bool, + ) -> Result<(&WmV2StateMonitor, &WmV2StateWorkspace)> { + if let Some((m, w, trace)) = self.trace_to(window) { + let mut siblins_with_window_node = &Vec::new(); + + for n in trace.iter().rev() { + match n { + WmNode::Horizontal(inner) => match axis { + Axis::Horizontal | Axis::Left | Axis::Right => { + if inner + .children + .iter() + .filter(|n| !n.is_empty()) + .collect_vec() + .len() + >= 2 + { + siblins_with_window_node = &inner.children; + break; + } + } + _ => {} + }, + WmNode::Vertical(inner) => match axis { + Axis::Horizontal | Axis::Top | Axis::Bottom => { + if inner + .children + .iter() + .filter(|n| !n.is_empty()) + .collect_vec() + .len() + >= 2 + { + siblins_with_window_node = &inner.children; + break; + } + } + _ => {} + }, + _ => {} + } + } + + if siblins_with_window_node.is_empty() { + log::warn!("Can't change size if the window is alone on axis"); + return Ok((m, w)); + } + + let (node_of_window_idx, node_of_window) = siblins_with_window_node + .iter() + .find_position(|n| WmNodeImpl::new((*n).clone()).contains(window)) + .expect("The algorithm at the top of this function is wrong / broken"); + + let siblins = siblins_with_window_node + .iter() + .enumerate() + .filter(|(idx, n)| { + *idx != node_of_window_idx + && match axis { + Axis::Horizontal | Axis::Vertical => true, + Axis::Left | Axis::Top => *idx < node_of_window_idx, + Axis::Bottom | Axis::Right => *idx > node_of_window_idx, + } + && !n.is_empty() + }) + .collect_vec(); + + if siblins.is_empty() { + log::warn!( + "Can't change size at {:?} if there are no other windows on that side", + axis + ); + return Ok((m, w)); + } + + let total_pie: f32 = siblins.iter().map(|(_, n)| n.grow_factor().get()).sum(); + let window_portion = node_of_window.grow_factor().get(); + + let to_grow = if relative { window_portion } else { total_pie } * percentage / 100f32; + let to_reduce = to_grow / siblins.len() as f32; + + node_of_window.grow_factor().set(window_portion + to_grow); + for (_, n) in siblins { + n.grow_factor().set(n.grow_factor().get() - to_reduce); + } + return Ok((m, w)); + } + + Err("Trying to change size of an unmanaged window".into()) + } +} diff --git a/src/background/state/application/events.rs b/src/background/state/application/events.rs new file mode 100644 index 00000000..c664369d --- /dev/null +++ b/src/background/state/application/events.rs @@ -0,0 +1,69 @@ +use itertools::Itertools; +use seelen_core::handlers::SeelenEvent; +use tauri::Emitter; + +use crate::{ + error_handler::Result, + seelen::{get_app_handle, SEELEN}, + trace_lock, +}; + +use super::FullState; + +impl FullState { + pub(super) fn emit_settings(&self) -> Result<()> { + get_app_handle().emit(SeelenEvent::StateSettingsChanged, self.settings())?; + trace_lock!(SEELEN).on_settings_change()?; + Ok(()) + } + + pub fn emit_weg_items(&self) -> Result<()> { + get_app_handle().emit(SeelenEvent::StateWegItemsChanged, self.weg_items())?; + Ok(()) + } + + pub(super) fn emit_themes(&self) -> Result<()> { + get_app_handle().emit( + SeelenEvent::StateThemesChanged, + self.themes().values().collect_vec(), + )?; + Ok(()) + } + + pub(super) fn emit_placeholders(&self) -> Result<()> { + get_app_handle().emit( + SeelenEvent::StatePlaceholdersChanged, + self.placeholders().values().collect_vec(), + )?; + Ok(()) + } + + pub(super) fn emit_layouts(&self) -> Result<()> { + get_app_handle().emit( + SeelenEvent::StateLayoutsChanged, + self.layouts().values().collect_vec(), + )?; + Ok(()) + } + + pub(super) fn emit_settings_by_app(&self) -> Result<()> { + get_app_handle().emit( + SeelenEvent::StateSettingsByAppChanged, + self.settings_by_app(), + )?; + Ok(()) + } + + pub(super) fn emit_history(&self) -> Result<()> { + get_app_handle().emit(SeelenEvent::StateHistoryChanged, self.history())?; + Ok(()) + } + + pub(super) fn emit_icon_packs(&self) -> Result<()> { + get_app_handle().emit( + SeelenEvent::StateIconPacksChanged, + trace_lock!(self.icon_packs()).values().collect_vec(), + )?; + Ok(()) + } +} diff --git a/src/background/state/application/icons.rs b/src/background/state/application/icons.rs new file mode 100644 index 00000000..7d95e486 --- /dev/null +++ b/src/background/state/application/icons.rs @@ -0,0 +1,94 @@ +use std::path::{Path, PathBuf}; + +use seelen_core::state::IconPack; + +use crate::{error_handler::Result, trace_lock}; + +use super::FullState; + +impl FullState { + pub fn icon_packs_folder(&self) -> PathBuf { + self.data_dir.join("icons") + } + + fn load_icon_pack_from_dir(dir_path: &Path) -> Result { + let file = dir_path.join("metadata.yml"); + if !file.exists() { + return Err("metadata.yml not found".into()); + } + Ok(serde_yaml::from_str(&std::fs::read_to_string(&file)?)?) + } + + pub(super) fn load_icons_packs(&mut self) -> Result<()> { + let entries = std::fs::read_dir(self.icon_packs_folder())?; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let icon_pack = Self::load_icon_pack_from_dir(&path); + match icon_pack { + Ok(mut icon_pack) => { + icon_pack.info.filename = entry.file_name().to_string_lossy().to_string(); + trace_lock!(self.icon_packs) + .insert(icon_pack.info.filename.clone(), icon_pack); + } + Err(err) => { + log::error!("Failed to load icon pack ({:?}): {:?}", path, err) + } + } + } + } + + // add default icon pack if not exists + if trace_lock!(self.icon_packs).get("system").is_none() { + let mut icon_pack = IconPack::default(); + icon_pack.info.display_name = "System".to_string(); + icon_pack.info.author = "System".to_string(); + icon_pack.info.description = "Icons from Windows and Program Files".to_string(); + icon_pack.info.filename = "system".to_string(); + trace_lock!(self.icon_packs).insert(icon_pack.info.filename.clone(), icon_pack); + } + + Ok(()) + } + + pub fn push_and_save_system_icon(&self, key: &str, icon: &Path) -> Result<()> { + let mut icon_packs = trace_lock!(self.icon_packs); + let default_icon_pack = icon_packs.get_mut("system").unwrap(); + default_icon_pack.apps.insert( + key.trim_start_matches("\\\\?\\").to_string(), + icon.to_owned(), + ); + + let folder = self.icon_packs_folder().join("system"); + std::fs::create_dir_all(&folder)?; + std::fs::write( + folder.join("metadata.yml"), + serde_yaml::to_string(default_icon_pack)?, + )?; + Ok(()) + } + + /// Get icon pack by app user model id, filename or path + pub fn get_icon_by_key(&self, key: &str) -> Option { + let filename = PathBuf::from(key) + .file_name() + .map(|p| p.to_string_lossy().to_string()); + + for icon_pack in self.settings.icon_packs.iter().rev() { + if let Some(icon_pack) = trace_lock!(self.icon_packs).get(icon_pack) { + let maybe_icon = icon_pack.apps.get(key).or_else(|| match filename.as_ref() { + Some(filename) => icon_pack.apps.get(filename), + None => None, + }); + if let Some(icon) = maybe_icon { + return Some( + self.icon_packs_folder() + .join(&icon_pack.info.filename) + .join(icon), + ); + } + } + } + None + } +} diff --git a/src/background/state/application/mod.rs b/src/background/state/application/mod.rs index 56ee80a5..ae0b3d17 100644 --- a/src/background/state/application/mod.rs +++ b/src/background/state/application/mod.rs @@ -1,4 +1,6 @@ mod apps_config; +mod events; +mod icons; use arc_swap::ArcSwap; use getset::Getters; @@ -9,10 +11,12 @@ use notify_debouncer_full::{ notify::{ReadDirectoryChangesWatcher, RecursiveMode, Watcher}, DebounceEventResult, DebouncedEvent, Debouncer, FileIdMap, }; -use seelen_core::state::{VirtualDesktopStrategy, WegItems, WindowManagerLayout}; -use serde::Serialize; +use parking_lot::Mutex; +use seelen_core::state::{IconPack, VirtualDesktopStrategy, WegItems, WindowManagerLayout}; use std::{ collections::{HashMap, VecDeque}, + fs::{File, OpenOptions}, + io::{Seek, Write}, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering}, @@ -20,53 +24,58 @@ use std::{ }, time::Duration, }; -use tauri::{AppHandle, Emitter, Manager}; +use tauri::Manager; use crate::{ - error_handler::Result, - log_error, - modules::cli::domain::Resource, - seelen::{get_app_handle, SEELEN}, - trace_lock, - utils::is_virtual_desktop_supported, - windows_api::WindowsApi, + error_handler::Result, log_error, modules::cli::domain::Resource, seelen::get_app_handle, + trace_lock, utils::is_virtual_desktop_supported, windows_api::WindowsApi, }; use super::domain::{AppConfig, Placeholder, Settings, Theme}; lazy_static! { + static ref DATA_DIR: PathBuf = get_app_handle().path().app_data_dir().unwrap(); pub static ref FULL_STATE: Arc> = Arc::new(ArcSwap::from_pointee({ log::trace!("Creating new State Manager"); FullState::new().expect("Failed to create State Manager") })); + static ref OPEN_OPTIONS: OpenOptions = { + let mut options = OpenOptions::new(); + options.write(true).create(true); + options + }; + static ref USER_SETTINGS_PATH: PathBuf = DATA_DIR.join("settings.json"); + static ref USER_SETTINGS_FILE: Arc> = Arc::new(Mutex::new( + OPEN_OPTIONS.open(USER_SETTINGS_PATH.as_path()).unwrap() + )); + static ref WEG_ITEMS_PATH: PathBuf = DATA_DIR.join("seelenweg_items.yaml"); + static ref WEG_ITEMS_FILE: Arc> = Arc::new(Mutex::new( + OPEN_OPTIONS.open(WEG_ITEMS_PATH.as_path()).unwrap() + )); } -#[derive(Getters, Debug, Clone, Serialize)] +static FILE_LISTENER_PAUSED: AtomicBool = AtomicBool::new(false); + +pub type LauncherHistory = HashMap>; + +#[derive(Getters, Debug, Clone)] +#[getset(get = "pub")] pub struct FullState { - #[serde(skip)] - handle: AppHandle, - #[serde(skip)] data_dir: PathBuf, - #[serde(skip)] resources_dir: PathBuf, - #[serde(skip)] watcher: Arc>>, // ======== data ======== - #[getset(get = "pub")] - settings: Settings, - #[getset(get = "pub")] - settings_by_app: VecDeque, - #[getset(get = "pub")] - themes: HashMap, - #[getset(get = "pub")] - placeholders: HashMap, - #[getset(get = "pub")] - layouts: HashMap, - #[getset(get = "pub")] - weg_items: WegItems, + pub settings: Settings, + pub settings_by_app: VecDeque, + pub themes: HashMap, + pub icon_packs: Arc>>, + pub placeholders: HashMap, + pub layouts: HashMap, + pub weg_items: WegItems, + pub history: LauncherHistory, } -static FILE_LISTENER_PAUSED: AtomicBool = AtomicBool::new(false); +unsafe impl Sync for FullState {} impl FullState { fn new() -> Result { @@ -74,15 +83,16 @@ impl FullState { let mut manager = Self { data_dir: handle.path().app_data_dir()?, resources_dir: handle.path().resource_dir()?, - handle, watcher: Arc::new(None), // ======== data ======== settings: Settings::default(), settings_by_app: VecDeque::new(), themes: HashMap::new(), + icon_packs: Arc::new(Mutex::new(HashMap::new())), placeholders: HashMap::new(), layouts: HashMap::new(), weg_items: WegItems::default(), + history: HashMap::new(), }; manager.load_all()?; manager.start_listeners()?; @@ -99,18 +109,14 @@ impl FullState { FULL_STATE.store(Arc::new(self)); } - fn store_cloned(&self) { + pub fn store_cloned(&self) { FULL_STATE.store(Arc::new(self.cloned())); } - pub fn settings_path(&self) -> PathBuf { - self.data_dir.join("settings.json") - } - fn process_event(&mut self, event: DebouncedEvent) -> Result<()> { let event = event.event; - let weg_items_path = self.data_dir.join("seelenweg_items.yaml"); + let history_path = self.data_dir.join("history"); let user_themes = self.data_dir.join("themes"); let bundled_themes = self.resources_dir.join("static/themes"); @@ -124,14 +130,28 @@ impl FullState { let user_app_configs = self.data_dir.join("applications.yml"); let bundled_app_configs = self.resources_dir.join("static/apps_templates"); - if event.paths.contains(&weg_items_path) { + if event.paths.contains(&self.icon_packs_folder()) { + log::info!("Icons Packs changed"); + self.load_icons_packs()?; + self.store_cloned(); + self.emit_icon_packs()?; + } + + if event.paths.contains(&WEG_ITEMS_PATH) { log::info!("Weg Items changed"); self.load_weg_items()?; self.store_cloned(); self.emit_weg_items()?; } - if event.paths.contains(&self.settings_path()) { + if event.paths.contains(&history_path) { + log::info!("History changed"); + self.load_history()?; + self.store_cloned(); + self.emit_history()?; + } + + if event.paths.contains(&USER_SETTINGS_PATH) { log::info!("Seelen Settings changed"); self.load_settings()?; self.store_cloned(); @@ -192,7 +212,7 @@ impl FullState { None, |result: DebounceEventResult| match result { Ok(events) => { - log::info!("Seelen UI File Watcher events: {:?}", events); + // log::info!("Seelen UI File Watcher events: {:?}", events); if !FILE_LISTENER_PAUSED.load(Ordering::Acquire) { let mut state = FULL_STATE.load().cloned(); for event in events { @@ -206,52 +226,64 @@ impl FullState { }, )?; - debouncer - .watcher() - .watch(&self.data_dir, RecursiveMode::Recursive)?; - debouncer - .watcher() - .watch(&self.resources_dir, RecursiveMode::Recursive)?; + let paths: Vec = vec![ + // settings & user data + USER_SETTINGS_PATH.to_path_buf(), + WEG_ITEMS_PATH.to_path_buf(), + self.data_dir.join("applications.yml"), + self.data_dir.join("history"), + // resources + self.data_dir.join("themes"), + self.icon_packs_folder(), + self.data_dir.join("placeholders"), + self.data_dir.join("layouts"), + self.resources_dir.join("static/themes"), + self.resources_dir.join("static/placeholders"), + self.resources_dir.join("static/layouts"), + self.resources_dir.join("static/apps_templates"), + ]; + + for path in paths { + debouncer.watcher().watch(&path, RecursiveMode::Recursive)?; + } self.watcher = Arc::new(Some(debouncer)); Ok(()) } - pub fn get_settings_from_path(path: PathBuf) -> Result { + pub fn get_settings_from_path(path: &Path) -> Result { match path.extension() { Some(ext) if ext == "json" => { - let mut settings: Settings = - serde_json::from_str(&std::fs::read_to_string(&path)?)?; - settings.language = settings - .language - .or_else(|| Some(Settings::get_system_language())); - if settings.virtual_desktop_strategy == VirtualDesktopStrategy::Native - && !is_virtual_desktop_supported() - { - settings.virtual_desktop_strategy = VirtualDesktopStrategy::Seelen; - } - Ok(settings) + Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } _ => Err("Invalid settings file extension".into()), } } fn load_settings(&mut self) -> Result<()> { - let path = self.settings_path(); - if path.exists() { - self.settings = Self::get_settings_from_path(path)?; - } else { - // save current/default settings + let path_exists = USER_SETTINGS_PATH.exists(); + if path_exists { + self.settings = Self::get_settings_from_path(&USER_SETTINGS_PATH)?; + self.settings.sanitize(); + } + + if !is_virtual_desktop_supported() { + self.settings.virtual_desktop_strategy = VirtualDesktopStrategy::Seelen; + } + + if !path_exists { self.save_settings()?; } Ok(()) } fn load_weg_items(&mut self) -> Result<()> { - let path = self.data_dir.join("seelenweg_items.yaml"); - if path.exists() { - self.weg_items = serde_yaml::from_str(&std::fs::read_to_string(&path)?)?; - self.weg_items.clean_all_items(); + if WEG_ITEMS_PATH.exists() { + self.weg_items = + serde_yaml::from_str(&std::fs::read_to_string(WEG_ITEMS_PATH.as_path())?)?; + self.weg_items.sanitize(); + } else { + self.save_weg_items()?; } Ok(()) } @@ -285,6 +317,14 @@ impl FullState { theme.styles.wm = std::fs::read_to_string(path.join("theme.wm.css"))?; } + if path.join("theme.launcher.css").exists() { + theme.styles.launcher = std::fs::read_to_string(path.join("theme.launcher.css"))?; + } + + if path.join("theme.wall.css").exists() { + theme.styles.wall = std::fs::read_to_string(path.join("theme.wall.css"))?; + } + Ok(theme) } @@ -333,6 +373,7 @@ impl FullState { match placeholder { Ok(mut placeholder) => { + placeholder.sanitize(); placeholder.info.filename = entry.file_name().to_string_lossy().to_string(); self.placeholders .insert(placeholder.info.filename.clone(), placeholder); @@ -396,11 +437,29 @@ impl FullState { Ok(()) } + fn save_settings_by_app(&self) -> Result<()> { + let data = self + .settings_by_app + .iter() + .filter(|app| !app.is_bundled) + .cloned() + .collect_vec(); + std::fs::write( + self.data_dir.join("applications.yml"), + serde_yaml::to_string(&data)?, + )?; + Ok(()) + } + fn load_settings_by_app(&mut self) -> Result<()> { let user_apps_path = self.data_dir.join("applications.yml"); let apps_templates_path = self.resources_dir.join("static/apps_templates"); self.settings_by_app.clear(); + if !user_apps_path.exists() { + // save empty array on appdata dir + self.save_settings_by_app()?; + } for entry in apps_templates_path.read_dir()?.flatten() { let content = std::fs::read_to_string(entry.path())?; @@ -423,56 +482,39 @@ impl FullState { Ok(()) } + fn load_history(&mut self) -> Result<()> { + let history_path = self.data_dir.join("history"); + if history_path.exists() { + self.history = serde_yaml::from_str(&std::fs::read_to_string(&history_path)?)?; + } else { + std::fs::write(history_path, serde_yaml::to_string(&self.history)?)?; + } + Ok(()) + } + fn load_all(&mut self) -> Result<()> { self.load_settings()?; self.load_weg_items()?; self.load_themes()?; + self.load_icons_packs()?; self.load_placeholders()?; self.load_layouts()?; self.load_settings_by_app()?; + self.load_history()?; Ok(()) } - fn emit_settings(&self) -> Result<()> { - self.handle.emit("settings-changed", self.settings())?; - trace_lock!(SEELEN).on_state_changed()?; - Ok(()) - } - - fn emit_weg_items(&self) -> Result<()> { - self.handle.emit("weg-items", self.weg_items())?; - Ok(()) - } - - fn emit_themes(&self) -> Result<()> { - self.handle - .emit("themes", self.themes().values().collect_vec())?; - Ok(()) - } - - fn emit_placeholders(&self) -> Result<()> { - self.handle - .emit("placeholders", self.placeholders().values().collect_vec())?; - Ok(()) - } - - fn emit_layouts(&self) -> Result<()> { - self.handle - .emit("layouts", self.layouts().values().collect_vec())?; - Ok(()) - } - - fn emit_settings_by_app(&self) -> Result<()> { - self.handle - .emit("settings-by-app", self.settings_by_app())?; + pub fn save_settings(&self) -> Result<()> { + let mut file = trace_lock!(USER_SETTINGS_FILE); + file.rewind()?; + file.write_all(serde_json::to_string_pretty(&self.settings)?.as_bytes())?; Ok(()) } - pub fn save_settings(&self) -> Result<()> { - std::fs::write( - self.settings_path(), - serde_json::to_string_pretty(&self.settings)?, - )?; + pub fn save_weg_items(&self) -> Result<()> { + let mut file = trace_lock!(WEG_ITEMS_FILE); + file.rewind()?; + file.write_all(serde_yaml::to_string(&self.weg_items)?.as_bytes())?; Ok(()) } @@ -501,8 +543,8 @@ impl FullState { self.data_dir.join(format!("themes/{filename}")), serde_yaml::to_string(&theme)?, )?; - if !self.settings.selected_theme.contains(&filename) { - self.settings.selected_theme.push(filename); + if !self.settings.selected_themes.contains(&filename) { + self.settings.selected_themes.push(filename); } } diff --git a/src/background/state/infrastructure.rs b/src/background/state/infrastructure.rs index 5a4ead3d..c477b14c 100644 --- a/src/background/state/infrastructure.rs +++ b/src/background/state/infrastructure.rs @@ -6,7 +6,7 @@ use seelen_core::state::{WegItems, WindowManagerLayout}; use crate::{error_handler::Result, windows_api::WindowsApi}; use super::{ - application::{FullState, FULL_STATE}, + application::{FullState, LauncherHistory, FULL_STATE}, domain::{AppConfig, Placeholder, Settings, Theme}, }; @@ -35,10 +35,17 @@ pub fn state_get_weg_items() -> WegItems { FULL_STATE.load().weg_items().clone() } +#[tauri::command(async)] +pub fn state_get_history() -> LauncherHistory { + FULL_STATE.load().history().clone() +} + #[tauri::command(async)] pub fn state_get_settings(path: Option) -> Result { if let Some(path) = path { - FullState::get_settings_from_path(path) + let mut settings = FullState::get_settings_from_path(&path)?; + settings.sanitize(); + Ok(settings) } else { Ok(FULL_STATE.load().settings().clone()) } diff --git a/src/background/state/mod.rs b/src/background/state/mod.rs index 444eb7a3..9e925fd3 100644 --- a/src/background/state/mod.rs +++ b/src/background/state/mod.rs @@ -1,30 +1,65 @@ -pub mod application; -pub mod domain; -pub mod infrastructure; - -use std::collections::HashMap; - -use application::FullState; -use domain::AhkVar; - -impl FullState { - pub fn is_weg_enabled(&self) -> bool { - self.settings().seelenweg.enabled - } - - pub fn is_bar_enabled(&self) -> bool { - self.settings().fancy_toolbar.enabled - } - - pub fn is_window_manager_enabled(&self) -> bool { - self.settings().window_manager.enabled - } - - pub fn is_ahk_enabled(&self) -> bool { - self.settings().ahk_enabled - } - - pub fn get_ahk_variables(&self) -> HashMap { - self.settings().ahk_variables.as_hash_map() - } -} +pub mod application; +pub mod domain; +pub mod infrastructure; + +use std::collections::HashMap; + +use application::FullState; +use domain::AhkVar; + +impl FullState { + pub fn is_weg_enabled(&self) -> bool { + self.settings.seelenweg.enabled + } + + pub fn is_weg_enabled_on_monitor(&self, monitor_idx: usize) -> bool { + let is_global_enabled = self.is_weg_enabled(); + match self.settings.monitors.get(monitor_idx) { + Some(monitor) => is_global_enabled && monitor.weg.enabled, + None => is_global_enabled, + } + } + + pub fn is_bar_enabled(&self) -> bool { + self.settings.fancy_toolbar.enabled + } + + pub fn is_bar_enabled_on_monitor(&self, monitor_idx: usize) -> bool { + let is_global_enabled = self.is_bar_enabled(); + match self.settings.monitors.get(monitor_idx) { + Some(monitor) => is_global_enabled && monitor.tb.enabled, + None => is_global_enabled, + } + } + + pub fn is_window_manager_enabled(&self) -> bool { + self.settings.window_manager.enabled + } + + pub fn is_rofi_enabled(&self) -> bool { + self.settings.launcher.enabled + } + + pub fn is_wall_enabled(&self) -> bool { + self.settings.wall.enabled + } + + pub fn is_ahk_enabled(&self) -> bool { + self.settings.ahk_enabled + } + + pub fn get_ahk_variables(&self) -> HashMap { + self.settings.ahk_variables.as_hash_map() + } + + pub fn get_wm_layout_id(&self, monitor_idx: usize, workspace_idx: usize) -> String { + let default = self.settings.window_manager.default_layout.clone(); + match self.settings.monitors.get(monitor_idx) { + Some(monitor) => match monitor.workspaces_v2.get(workspace_idx) { + Some(workspace) => workspace.layout.clone().unwrap_or(default), + None => default, + }, + None => default, + } + } +} diff --git a/src/background/system/brightness.rs b/src/background/system/brightness.rs index 9ac13992..b3d1a88b 100644 --- a/src/background/system/brightness.rs +++ b/src/background/system/brightness.rs @@ -1,63 +1,63 @@ -use serde::Serialize; -use windows::Win32::Devices::Display::{ - GetMonitorBrightness, GetMonitorCapabilities, SetMonitorBrightness, -}; - -use crate::windows_api::WindowsApi; - -#[derive(Debug, Serialize)] -pub struct Brightness { - min: u32, - max: u32, - current: u32, -} - -#[tauri::command(async)] -pub fn get_main_monitor_brightness() -> Result { - let mut brightness = Brightness { - min: 0, - max: 0, - current: 0, - }; - - unsafe { - let hmonitor = WindowsApi::primary_physical_monitor()?; - - let mut pdwmonitorcapabilities: u32 = 0; - let mut pdwsupportedcolortemperatures: u32 = 0; - let mut result = GetMonitorCapabilities( - hmonitor.hPhysicalMonitor, - &mut pdwmonitorcapabilities, - &mut pdwsupportedcolortemperatures, - ); - - if result == 0 { - return Err("GetMonitorCapabilities failed".to_string()); - } - - result = GetMonitorBrightness( - hmonitor.hPhysicalMonitor, - &mut brightness.min, - &mut brightness.current, - &mut brightness.max, - ); - - if result == 0 { - return Err("GetMonitorBrightness failed".to_string()); - } - } - - Ok(brightness) -} - -#[tauri::command(async)] -pub fn set_main_monitor_brightness(brightness: u32) -> Result<(), String> { - let result = unsafe { - let hmonitor = WindowsApi::primary_physical_monitor()?; - SetMonitorBrightness(hmonitor.hPhysicalMonitor, brightness) - }; - if result == 0 { - return Err("SetMonitorBrightness failed".to_string()); - } - Ok(()) -} +use serde::Serialize; +use windows::Win32::Devices::Display::{ + GetMonitorBrightness, GetMonitorCapabilities, SetMonitorBrightness, +}; + +use crate::{error_handler::Result, windows_api::WindowsApi}; + +#[derive(Debug, Serialize)] +pub struct Brightness { + min: u32, + max: u32, + current: u32, +} + +#[tauri::command(async)] +pub fn get_main_monitor_brightness() -> Result { + let mut brightness = Brightness { + min: 0, + max: 0, + current: 0, + }; + + unsafe { + let hmonitor = WindowsApi::primary_physical_monitor()?; + + let mut pdwmonitorcapabilities: u32 = 0; + let mut pdwsupportedcolortemperatures: u32 = 0; + let mut result = GetMonitorCapabilities( + hmonitor.hPhysicalMonitor, + &mut pdwmonitorcapabilities, + &mut pdwsupportedcolortemperatures, + ); + + if result == 0 { + return Err("GetMonitorCapabilities failed".into()); + } + + result = GetMonitorBrightness( + hmonitor.hPhysicalMonitor, + &mut brightness.min, + &mut brightness.current, + &mut brightness.max, + ); + + if result == 0 { + return Err("GetMonitorBrightness failed".into()); + } + } + + Ok(brightness) +} + +#[tauri::command(async)] +pub fn set_main_monitor_brightness(brightness: u32) -> Result<()> { + let result = unsafe { + let hmonitor = WindowsApi::primary_physical_monitor()?; + SetMonitorBrightness(hmonitor.hPhysicalMonitor, brightness) + }; + if result == 0 { + return Err("SetMonitorBrightness failed".into()); + } + Ok(()) +} diff --git a/src/background/system/mod.rs b/src/background/system/mod.rs index df25aaf3..d0ddf99a 100644 --- a/src/background/system/mod.rs +++ b/src/background/system/mod.rs @@ -1,58 +1,55 @@ -pub mod brightness; - -use tauri::Listener; - -use crate::{ - error_handler::Result, - log_error, - modules::{ - media::infrastructure::{register_media_events, release_media_events}, - network::infrastructure::register_network_events, - notifications::infrastructure::{ - register_notification_events, release_notification_events, - }, - power::infrastructure::PowerManager, - system_settings::infrastructure::{register_colors_events, release_colors_events}, - tray::infrastructure::register_tray_events, - }, - seelen::get_app_handle, -}; - -pub fn declare_system_events_handlers() -> Result<()> { - let handle = get_app_handle(); - - handle.listen("register-power-events", move |_| { - log_error!(PowerManager::register_power_events()); - log_error!(PowerManager::emit_system_power_info()); - }); - - handle.listen("register-tray-events", move |_| register_tray_events()); - - handle.listen("register-network-events", move |_| { - log_error!(register_network_events()); - }); - - handle.listen("register-bluetooth-events", move |_| { - // todo - }); - - handle.listen("register-media-events", move |_| { - register_media_events(); - }); - - handle.listen("register-notifications-events", move |_| { - register_notification_events(); - }); - - handle.listen("register-colors-events", move |_| { - register_colors_events(); - }); - - Ok(()) -} - -pub fn release_system_events_handlers() { - release_media_events(); - release_notification_events(); - release_colors_events(); -} +pub mod brightness; + +use tauri::Listener; + +use crate::{ + error_handler::Result, + log_error, + modules::{ + media::infrastructure::{register_media_events, release_media_events}, + network::infrastructure::register_network_events, + notifications::infrastructure::{ + register_notification_events, release_notification_events, + }, + power::infrastructure::PowerManager, + system_settings::infrastructure::{register_colors_events, release_colors_events}, + tray::infrastructure::register_tray_events, + }, + seelen::get_app_handle, +}; + +pub fn declare_system_events_handlers() -> Result<()> { + let handle = get_app_handle(); + + handle.listen("register-power-events", move |_| { + log_error!(PowerManager::register_power_events()); + log_error!(PowerManager::emit_system_power_info()); + }); + + handle.listen("register-tray-events", move |_| register_tray_events()); + + handle.listen("register-network-events", move |_| { + log_error!(register_network_events()); + }); + + handle.listen("register-bluetooth-events", move |_| { + // todo + }); + + handle.listen("register-media-events", move |_| { + register_media_events(); + }); + + handle.listen("register-notifications-events", move |_| { + register_notification_events(); + }); + + register_colors_events(); + Ok(()) +} + +pub fn release_system_events_handlers() { + release_media_events(); + release_notification_events(); + release_colors_events(); +} diff --git a/src/background/utils/ahk/mocks/seelen.ahk b/src/background/utils/ahk/mocks/seelen.ahk index 73bce811..efe8988d 100644 --- a/src/background/utils/ahk/mocks/seelen.ahk +++ b/src/background/utils/ahk/mocks/seelen.ahk @@ -19,14 +19,12 @@ CloseIfNotRunning() ResumeWM() } -^#!h:: { - DebugHitboxes() -} - ^#!l:: { ToggleWinEventTracing() } ^#!t:: { ToggleMutexLockTracing() -} \ No newline at end of file +} + +LWin & Space::ToggleAppLauncher() \ No newline at end of file diff --git a/src/background/utils/ahk/mocks/seelen.lib.ahk b/src/background/utils/ahk/mocks/seelen.lib.ahk index 8cda09e9..b7b04cb3 100644 --- a/src/background/utils/ahk/mocks/seelen.lib.ahk +++ b/src/background/utils/ahk/mocks/seelen.lib.ahk @@ -16,10 +16,37 @@ CloseIfNotRunning() { SetTimer(CloseIfNotRunning, 1000) } -OpenSettings(){ +; ================= Main ================= +OpenSettings() { RunWait(seelen " settings", , "Hide") } +; ================= Debug ================= + +ToggleWinEventTracing() { + RunWait(seelen " debugger toggle-win-events", , "Hide") +} + +ToggleMutexLockTracing() { + RunWait(seelen " debugger toggle-trace-lock", , "Hide") +} + +; ================= Virtual desktop ================= + +SwitchWorkspace(idx) { + RunWait(seelen " vd switch-workspace " idx, , "Hide") +} + +MoveToWorkspace(idx) { + RunWait(seelen " vd move-to-workspace " idx, , "Hide") +} + +SendToWorkspace(idx) { + RunWait(seelen " vd send-to-workspace " idx, , "Hide") +} + +; ================= Tiling Window Manager ================= + Reserve(reservation) { RunWait(seelen " wm reserve " reservation, , "Hide") } @@ -48,18 +75,6 @@ focus(action) { RunWait(seelen " wm focus " action, , "Hide") } -SwitchWorkspace(idx) { - RunWait(seelen " wm switch-workspace " idx, , "Hide") -} - -MoveToWorkspace(idx) { - RunWait(seelen " wm move-to-workspace " idx, , "Hide") -} - -SendToWorkspace(idx) { - RunWait(seelen " wm send-to-workspace " idx, , "Hide") -} - PauseWM() { RunWait(seelen " wm pause", , "Hide") } @@ -72,14 +87,8 @@ ResumeWM() { RunWait(seelen " wm resume", , "Hide") } -DebugHitboxes() { - RunWait(seelen " weg debug-hitbox", , "Hide") -} +; ================= App Launcher ================= -ToggleWinEventTracing() { - RunWait(seelen " debugger toggle-win-events", , "Hide") -} - -ToggleMutexLockTracing() { - RunWait(seelen " debugger toggle-trace-lock", , "Hide") +ToggleAppLauncher() { + RunWait(seelen " launcher toggle", , "Hide") } \ No newline at end of file diff --git a/src/background/utils/ahk/mocks/seelen.vd.ahk b/src/background/utils/ahk/mocks/seelen.vd.ahk new file mode 100644 index 00000000..cd7fac98 --- /dev/null +++ b/src/background/utils/ahk/mocks/seelen.vd.ahk @@ -0,0 +1,78 @@ +/* + * This file is generated by Seelen UI and will be replaced on each update please don't modify manually. + * If you want to introduce your own implementation for shortcuts with AHK or any other scripting language + * just disable Seelen UI integrated shortcuts in the shortcuts tab, and this file will be ignored. +*/ +#Requires AutoHotkey v2.0 +#SingleInstance Force +#NoTrayIcon + +#Include seelen.lib.ahk + +CloseIfNotRunning() + +; Switch workspaces +;switch_workspace_0 +x:: SwitchWorkspace(0) +;switch_workspace_1 +x:: SwitchWorkspace(1) +;switch_workspace_2 +x:: SwitchWorkspace(2) +;switch_workspace_3 +x:: SwitchWorkspace(3) +;switch_workspace_4 +x:: SwitchWorkspace(4) +;switch_workspace_5 +x:: SwitchWorkspace(5) +;switch_workspace_6 +x:: SwitchWorkspace(6) +;switch_workspace_7 +x:: SwitchWorkspace(7) +;switch_workspace_8 +x:: SwitchWorkspace(8) +;switch_workspace_9 +x:: SwitchWorkspace(9) + +; Send the focused window across workspaces and switch specified workspace +;move_to_workspace_0 +x:: MoveToWorkspace(0) +;move_to_workspace_1 +x:: MoveToWorkspace(1) +;move_to_workspace_2 +x:: MoveToWorkspace(2) +;move_to_workspace_3 +x:: MoveToWorkspace(3) +;move_to_workspace_4 +x:: MoveToWorkspace(4) +;move_to_workspace_5 +x:: MoveToWorkspace(5) +;move_to_workspace_6 +x:: MoveToWorkspace(6) +;move_to_workspace_7 +x:: MoveToWorkspace(7) +;move_to_workspace_8 +x:: MoveToWorkspace(8) +;move_to_workspace_9 +x:: MoveToWorkspace(9) + +; Send the focused window across workspaces +;send_to_workspace_0 +x:: SendToWorkspace(0) +;send_to_workspace_1 +x:: SendToWorkspace(1) +;send_to_workspace_2 +x:: SendToWorkspace(2) +;send_to_workspace_3 +x:: SendToWorkspace(3) +;send_to_workspace_4 +x:: SendToWorkspace(4) +;send_to_workspace_5 +x:: SendToWorkspace(5) +;send_to_workspace_6 +x:: SendToWorkspace(6) +;send_to_workspace_7 +x:: SendToWorkspace(7) +;send_to_workspace_8 +x:: SendToWorkspace(8) +;send_to_workspace_9 +x:: SendToWorkspace(9) \ No newline at end of file diff --git a/src/background/utils/ahk/mocks/seelen.wm.ahk b/src/background/utils/ahk/mocks/seelen.wm.ahk index f0aa042d..fc065694 100644 --- a/src/background/utils/ahk/mocks/seelen.wm.ahk +++ b/src/background/utils/ahk/mocks/seelen.wm.ahk @@ -1,161 +1,95 @@ -/* - * This file is generated by Seelen UI and will be replaced on each update please don't modify manually. - * If you want to introduce your own implementation for shortcuts with AHK or any other scripting language - * just disable Seelen UI integrated shortcuts in the shortcuts tab, and this file will be ignored. -*/ -#Requires AutoHotkey v2.0 -#SingleInstance Force -#NoTrayIcon - -#Include seelen.lib.ahk - -CloseIfNotRunning() - -;debug_wm -^#!w:: WMDebug() - -^#!p:: { - PauseWM() - ExitApp() -} - -;reserve_top -x:: Reserve("top") -;reserve_bottom -x:: Reserve("bottom") -;reserve_left -x:: Reserve("left") -;reserve_right -x:: Reserve("right") - -;reserve_float -x:: Reserve("float") -;reserve_stack -x:: Reserve("stack") - -~Esc:: { - CancelReservation() -} - -;focusTop -x:: focus("up") -;focus_bottom -x:: focus("down") -;focus_left -x:: focus("left") -;focus_right -x:: focus("right") - -;focus_latest -x:: focus("latest") - -; Increase or decrease window size -;increase_width -x:: updateWindowWidth("increase") -;decrease_width -x:: updateWindowWidth("decrease") -;increase_height -x:: updateWindowHeight("increase") -;decrease_height -x:: updateWindowHeight("decrease") - -;restore_sizes -x:: resetWorkspaceSize() - -; Switch workspaces -;switch_workspace_0 -x:: SwitchWorkspace(0) -;switch_workspace_1 -x:: SwitchWorkspace(1) -;switch_workspace_2 -x:: SwitchWorkspace(2) -;switch_workspace_3 -x:: SwitchWorkspace(3) -;switch_workspace_4 -x:: SwitchWorkspace(4) -;switch_workspace_5 -x:: SwitchWorkspace(5) -;switch_workspace_6 -x:: SwitchWorkspace(6) -;switch_workspace_7 -x:: SwitchWorkspace(7) -;switch_workspace_8 -x:: SwitchWorkspace(8) -;switch_workspace_9 -x:: SwitchWorkspace(9) - -; Send the focused window across workspaces and switch specified workspace -;move_to_workspace_0 -x:: MoveToWorkspace(0) -;move_to_workspace_1 -x:: MoveToWorkspace(1) -;move_to_workspace_2 -x:: MoveToWorkspace(2) -;move_to_workspace_3 -x:: MoveToWorkspace(3) -;move_to_workspace_4 -x:: MoveToWorkspace(4) -;move_to_workspace_5 -x:: MoveToWorkspace(5) -;move_to_workspace_6 -x:: MoveToWorkspace(6) -;move_to_workspace_7 -x:: MoveToWorkspace(7) -;move_to_workspace_8 -x:: MoveToWorkspace(8) -;move_to_workspace_9 -x:: MoveToWorkspace(9) - -; Send the focused window across workspaces -;send_to_workspace_0 -x:: SendToWorkspace(0) -;send_to_workspace_1 -x:: SendToWorkspace(1) -;send_to_workspace_2 -x:: SendToWorkspace(2) -;send_to_workspace_3 -x:: SendToWorkspace(3) -;send_to_workspace_4 -x:: SendToWorkspace(4) -;send_to_workspace_5 -x:: SendToWorkspace(5) -;send_to_workspace_6 -x:: SendToWorkspace(6) -;send_to_workspace_7 -x:: SendToWorkspace(7) -;send_to_workspace_8 -x:: SendToWorkspace(8) -;send_to_workspace_9 -x:: SendToWorkspace(9) - -/* -TODO - -!+q:: CycleFocus("previous") -!q:: CycleFocus("next") - -; Move windows -#+a:: Move("left") -#+s:: Move("down") -#+w:: Move("up") -#+d:: Move("right") - -#+Enter:: Promote() - -#+x:: FlipLayout("horizontal") -#+z:: FlipLayout("vertical") - -; Stack windows -#a:: Stack("left") -#d:: Stack("right") -#w:: Stack("up") -#s:: Stack("down") -#;:: Unstack() - -#+q:: CycleStack("previous") -#q:: CycleStack("next") - -; Manipulate windows -#f:: ToggleFloat() -#m:: ToggleMonocle() -*/ +/* + * This file is generated by Seelen UI and will be replaced on each update please don't modify manually. + * If you want to introduce your own implementation for shortcuts with AHK or any other scripting language + * just disable Seelen UI integrated shortcuts in the shortcuts tab, and this file will be ignored. +*/ +#Requires AutoHotkey v2.0 +#SingleInstance Force +#NoTrayIcon + +#Include seelen.lib.ahk + +CloseIfNotRunning() + +;debug_wm +^#!w:: WMDebug() + +^#!p:: { + PauseWM() + ExitApp() +} + +;reserve_top +x:: Reserve("top") +;reserve_bottom +x:: Reserve("bottom") +;reserve_left +x:: Reserve("left") +;reserve_right +x:: Reserve("right") + +;reserve_float +x:: Reserve("float") +;reserve_stack +x:: Reserve("stack") + +~Esc:: { + CancelReservation() +} + +;focusTop +x:: focus("up") +;focus_bottom +x:: focus("down") +;focus_left +x:: focus("left") +;focus_right +x:: focus("right") + +;focus_latest +x:: focus("latest") + +; Increase or decrease window size +;increase_width +x:: updateWindowWidth("increase") +;decrease_width +x:: updateWindowWidth("decrease") +;increase_height +x:: updateWindowHeight("increase") +;decrease_height +x:: updateWindowHeight("decrease") + +;restore_sizes +x:: resetWorkspaceSize() + +/* +TODO + +!+q:: CycleFocus("previous") +!q:: CycleFocus("next") + +; Move windows +#+a:: Move("left") +#+s:: Move("down") +#+w:: Move("up") +#+d:: Move("right") + +#+Enter:: Promote() + +#+x:: FlipLayout("horizontal") +#+z:: FlipLayout("vertical") + +; Stack windows +#a:: Stack("left") +#d:: Stack("right") +#w:: Stack("up") +#s:: Stack("down") +#;:: Unstack() + +#+q:: CycleStack("previous") +#q:: CycleStack("next") + +; Manipulate windows +#f:: ToggleFloat() +#m:: ToggleMonocle() +*/ diff --git a/src/background/utils/constants.rs b/src/background/utils/constants.rs index 04ccf599..c025a7ab 100644 --- a/src/background/utils/constants.rs +++ b/src/background/utils/constants.rs @@ -1,30 +1,13 @@ +use std::path::PathBuf; + use itertools::Itertools; use lazy_static::lazy_static; +use tauri::{path::BaseDirectory, Manager}; -lazy_static! { - pub static ref IGNORE_FOCUS: Vec = [ - "Task Switching", - "Task View", - "Virtual desktop switching preview", - "Virtual desktop hotkey switching preview", - "Seelen Window Manager", // for some reason this sometimes is focused, maybe could be deleted - ] - .iter() - .map(|x| x.to_string()) - .collect_vec(); +use crate::{error_handler::Result, seelen::get_app_handle}; - pub static ref IGNORE_FULLSCREEN: Vec = [ - "Task Switching", - "Task View", - "Virtual desktop switching preview", - "Virtual desktop hotkey switching preview", - "Seelen Window Manager", - "Seelen Fancy Toolbar", - "SeelenWeg" - ] - .iter() - .map(|x| x.to_string()) - .collect_vec(); +lazy_static! { + static ref ICONS: Icons = Icons::instance().expect("Failed to load icons paths"); /** * Some UWP apps like WhatsApp are resized after be opened, @@ -36,13 +19,10 @@ lazy_static! { .collect_vec(); } -pub static OVERLAP_BLACK_LIST_BY_TITLE: [&str; 6] = [ - "", - "SeelenWeg", - "SeelenWeg Hitbox", - "Seelen Window Manager", - "Seelen Fancy Toolbar", - "Program Manager", +pub static NATIVE_UI_POPUP_CLASSES: [&str; 3] = [ + "ForegroundStaging", // Task Switching and Task View + "XamlExplorerHostIslandWindow", // Task Switching, Task View and other popups + "ControlCenterWindow", // Windows 11 right panel with quick settings ]; pub static OVERLAP_BLACK_LIST_BY_EXE: [&str; 4] = [ @@ -51,3 +31,22 @@ pub static OVERLAP_BLACK_LIST_BY_EXE: [&str; 4] = [ "StartMenuExperienceHost.exe", "ShellExperienceHost.exe", ]; + +pub struct Icons { + missing_app: PathBuf, +} + +impl Icons { + fn instance() -> Result { + let handle = get_app_handle(); + Ok(Self { + missing_app: handle + .path() + .resolve("static/icons/missing.png", BaseDirectory::Resource)?, + }) + } + + pub fn missing_app() -> PathBuf { + ICONS.missing_app.clone() + } +} diff --git a/src/background/utils/mod.rs b/src/background/utils/mod.rs index 22a0b80d..24c7cfe2 100644 --- a/src/background/utils/mod.rs +++ b/src/background/utils/mod.rs @@ -35,6 +35,10 @@ pub fn sleep_millis(millis: u64) { } pub fn are_overlaped(a: &RECT, b: &RECT) -> bool { + let zeroed = RECT::default(); + if a == &zeroed || b == &zeroed { + return false; + } if a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom { return false; } @@ -87,7 +91,7 @@ pub fn resolve_guid_path>(path: S) -> Result { let guid = part.trim_start_matches('{').trim_end_matches('}'); let rfid = GUID::from(guid); let string_path = unsafe { - SHGetKnownFolderPath(&rfid as _, KF_FLAG_DEFAULT, HANDLE(0))?.to_string()? + SHGetKnownFolderPath(&rfid as _, KF_FLAG_DEFAULT, HANDLE::default())?.to_string()? }; path_buf.push(string_path); @@ -102,24 +106,34 @@ pub fn resolve_guid_path>(path: S) -> Result { } pub static TRACE_LOCK_ENABLED: AtomicBool = AtomicBool::new(false); +lazy_static::lazy_static! { + pub static ref LAST_SUCCESSFUL_LOCK: Mutex> = Mutex::new(HashMap::new()); +} + #[macro_export] macro_rules! trace_lock { ($mutex:expr) => { trace_lock!($mutex, 5) }; ($mutex:expr, $duration:expr) => {{ - let guard = $mutex - .try_lock_for(std::time::Duration::from_secs($duration)) - .expect("Failed to lock"); + let guard = $mutex.try_lock_for(std::time::Duration::from_secs($duration)); + let guard_name = stringify!($mutex); + if guard.is_none() { + let mut panic_msg = format!("{} mutex is deadlocked", guard_name); + if let Some(path) = $crate::utils::LAST_SUCCESSFUL_LOCK.lock().get(guard_name) { + panic_msg = format!("{}, last successful aquire was at {}", panic_msg, path); + } + panic!("{}", panic_msg); + } + if $crate::utils::TRACE_LOCK_ENABLED.load(std::sync::atomic::Ordering::Relaxed) { - log::trace!( - "{} lock acquired at {}:{}", - stringify!($mutex), - file!(), - line!() - ); + let mut map = $crate::utils::LAST_SUCCESSFUL_LOCK.lock(); + let location = format!("{}:{}", file!(), line!()); + log::trace!("{} lock acquired at {}", guard_name, location); + map.insert(guard_name.to_owned(), location); } - guard + + guard.expect("Mutex deadlocked") }}; } @@ -135,12 +149,18 @@ pub struct PerformanceHelper { impl PerformanceHelper { pub fn start(&mut self, name: &str) { + log::debug!("{} start", name); self.time.insert(name.to_string(), Instant::now()); } pub fn elapsed(&self, name: &str) -> Duration { self.time.get(name).unwrap().elapsed() } + + pub fn end(&mut self, name: &str) { + log::debug!("{} end in: {:.2}s", name, self.elapsed(name).as_secs_f64()); + self.time.remove(name); + } } /// Useful when spawning threads that will allocate a loop or some other blocking operation diff --git a/src/background/utils/winver.rs b/src/background/utils/winver.rs index b7f86600..360a6454 100644 --- a/src/background/utils/winver.rs +++ b/src/background/utils/winver.rs @@ -9,5 +9,6 @@ pub fn is_windows_11() -> bool { /// this should be called before call any winvd function pub fn is_virtual_desktop_supported() -> bool { // disable virtual desktop for 24h2 - matches!(os_info::get().version(), os_info::Version::Semantic(_, _, x) if (&10240..&26000).contains(&x)) + // matches!(os_info::get().version(), os_info::Version::Semantic(_, _, x) if (&10240..&26000).contains(&x)) + false } diff --git a/src/background/windows_api/app_bar.rs b/src/background/windows_api/app_bar.rs index e310cf9e..eac29992 100644 --- a/src/background/windows_api/app_bar.rs +++ b/src/background/windows_api/app_bar.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use parking_lot::Mutex; +use seelen_core::state::SeelenWegSide; use windows::Win32::{ Foundation::{HWND, LPARAM, RECT}, UI::Shell::{ @@ -22,6 +23,17 @@ pub enum AppBarDataEdge { Bottom = ABE_BOTTOM as isize, } +impl From for AppBarDataEdge { + fn from(val: SeelenWegSide) -> Self { + match val { + SeelenWegSide::Left => AppBarDataEdge::Left, + SeelenWegSide::Top => AppBarDataEdge::Top, + SeelenWegSide::Right => AppBarDataEdge::Right, + SeelenWegSide::Bottom => AppBarDataEdge::Bottom, + } + } +} + /// https://learn.microsoft.com/en-us/windows/win32/shell/abm-setstate#parameters #[derive(Debug, Clone, Copy)] pub enum AppBarDataState { @@ -81,8 +93,9 @@ impl AppBarData { pub fn register_as_new_bar(&mut self) { let mut data = self.0; let mut registered = trace_lock!(RegisteredBars); - if !registered.contains(&data.hWnd.0) { - registered.push(data.hWnd.0); + let addr = data.hWnd.0 as isize; + if !registered.contains(&addr) { + registered.push(addr); unsafe { SHAppBarMessage(ABM_NEW, &mut data) }; } unsafe { SHAppBarMessage(ABM_SETPOS, &mut data) }; @@ -91,6 +104,6 @@ impl AppBarData { pub fn unregister_bar(&mut self) { let mut data = self.0; unsafe { SHAppBarMessage(ABM_REMOVE, &mut data) }; - trace_lock!(RegisteredBars).retain(|x| *x != data.hWnd.0); + trace_lock!(RegisteredBars).retain(|x| *x != data.hWnd.0 as isize); } } diff --git a/src/background/windows_api/iterator.rs b/src/background/windows_api/iterator.rs index 53da6999..235a4f13 100644 --- a/src/background/windows_api/iterator.rs +++ b/src/background/windows_api/iterator.rs @@ -1,5 +1,3 @@ -use std::slice::Iter; - use windows::Win32::{ Foundation::{BOOL, HWND, LPARAM, RECT}, Graphics::Gdi::{HDC, HMONITOR}, @@ -8,7 +6,7 @@ use windows::Win32::{ use crate::{error_handler::Result, windows_api::WindowsApi}; -use super::window::Window; +use super::{monitor::Monitor, window::Window}; #[derive(Debug, Clone)] pub struct WindowEnumerator { @@ -42,14 +40,14 @@ impl WindowEnumerator { /// If enumeration fails it will return error. pub fn for_each(&self, cb: F) -> Result<()> where - F: FnMut(HWND) + Sync, + F: FnMut(HWND), { type ForEachCallback<'a> = Box; let mut callback: ForEachCallback = Box::new(cb); unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { if let Some(boxed) = (lparam.0 as *mut ForEachCallback).as_mut() { - (*boxed)(hwnd) + (*boxed)(hwnd); } true.into() } @@ -61,8 +59,7 @@ impl WindowEnumerator { /// If enumeration fails it will return error. pub fn map(&self, cb: F) -> Result> where - F: FnMut(HWND) -> T + Sync, - T: Sync, + F: FnMut(HWND) -> T, { struct MapCallbackWrapper<'a, T> { cb: Box T + 'a>, @@ -89,7 +86,7 @@ impl WindowEnumerator { /// If no window matches the condition, it will return None. pub fn find(&self, cb: F) -> Result> where - F: FnMut(Window) -> bool + Sync, + F: FnMut(Window) -> bool, { struct FindCallbackWrapper<'a> { cb: Box bool + 'a>, @@ -118,28 +115,11 @@ impl WindowEnumerator { } #[derive(Debug, Clone, Default)] -pub struct MonitorEnumerator { - handles: Vec, -} - -impl IntoIterator for MonitorEnumerator { - type Item = HMONITOR; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.handles.into_iter() - } -} +pub struct MonitorEnumerator; impl MonitorEnumerator { - pub fn new_refreshed() -> Result { - let mut enumerator = Self::default(); - enumerator.refresh()?; - Ok(enumerator) - } - - pub fn refresh(&mut self) -> Result<()> { - self.handles.clear(); + pub fn get_all() -> Result> { + let mut handles = Vec::new(); unsafe extern "system" fn get_handles_proc( hmonitor: HMONITOR, @@ -154,13 +134,27 @@ impl MonitorEnumerator { true.into() } - WindowsApi::enum_display_monitors( - Some(get_handles_proc), - &mut self.handles as *mut _ as isize, - ) + WindowsApi::enum_display_monitors(Some(get_handles_proc), &mut handles as *mut _ as isize)?; + Ok(handles) } - pub fn iter(&self) -> Iter<'_, HMONITOR> { - self.handles.iter() + pub fn get_all_v2() -> Result> { + let mut handles = Vec::new(); + + unsafe extern "system" fn get_handles_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _rect_clip: *mut RECT, + lparam: LPARAM, + ) -> BOOL { + let data_ptr = lparam.0 as *mut Vec; + if let Some(data) = data_ptr.as_mut() { + data.push(Monitor::from(hmonitor)); + } + true.into() + } + + WindowsApi::enum_display_monitors(Some(get_handles_proc), &mut handles as *mut _ as isize)?; + Ok(handles) } } diff --git a/src/background/windows_api/mod.rs b/src/background/windows_api/mod.rs index 609a898f..700f774c 100644 --- a/src/background/windows_api/mod.rs +++ b/src/background/windows_api/mod.rs @@ -1,7 +1,9 @@ mod app_bar; mod com; mod iterator; +pub mod monitor; mod process; +mod string_utils; pub mod window; pub use app_bar::*; @@ -10,12 +12,18 @@ pub use iterator::*; use itertools::Itertools; use process::ProcessInformationFlag; use widestring::U16CStr; +use windows_core::Interface; + +use std::{ + ffi::{c_void, OsString}, + os::windows::ffi::{OsStrExt, OsStringExt}, + path::{Path, PathBuf}, + thread::sleep, + time::Duration, +}; -use std::{ffi::c_void, path::PathBuf, thread::sleep, time::Duration}; - -use color_eyre::eyre::eyre; use windows::{ - core::{GUID, PCWSTR, PWSTR}, + core::{BSTR, GUID, PCWSTR, PWSTR}, Storage::Streams::{ DataReader, IRandomAccessStreamReference, IRandomAccessStreamWithContentType, }, @@ -29,7 +37,8 @@ use windows::{ PHYSICAL_MONITOR, }, Foundation::{ - CloseHandle, FALSE, HANDLE, HMODULE, HWND, LPARAM, LUID, MAX_PATH, RECT, STATUS_SUCCESS, + CloseHandle, FALSE, HANDLE, HMODULE, HWND, LPARAM, LUID, MAX_PATH, RECT, + STATUS_SUCCESS, WPARAM, }, Graphics::{ Dwm::{ @@ -38,8 +47,8 @@ use windows::{ DWM_CLOAKED_INHERITED, DWM_CLOAKED_SHELL, }, Gdi::{ - EnumDisplayMonitors, GetMonitorInfoW, MonitorFromWindow, HDC, HMONITOR, - MONITORENUMPROC, MONITORINFOEXW, MONITOR_DEFAULTTOPRIMARY, + EnumDisplayMonitors, GetMonitorInfoW, MonitorFromPoint, MonitorFromWindow, HDC, + HMONITOR, MONITORENUMPROC, MONITORINFOEXW, MONITOR_DEFAULTTOPRIMARY, }, }, Security::{ @@ -48,9 +57,11 @@ use windows::{ TOKEN_QUERY, }, Storage::{ - EnhancedStorage::PKEY_FileDescription, Packaging::Appx::GetApplicationUserModelId, + EnhancedStorage::{PKEY_AppUserModel_ID, PKEY_FileDescription}, + FileSystem::WIN32_FIND_DATAW, }, System::{ + Com::{IPersistFile, STGM_READ}, LibraryLoader::GetModuleHandleW, Power::{GetSystemPowerStatus, SetSuspendState, SYSTEM_POWER_STATUS}, RemoteDesktop::ProcessIdToSessionId, @@ -64,29 +75,33 @@ use windows::{ UI::{ HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}, Shell::{ - IShellItem2, IVirtualDesktopManager, + IShellItem2, IShellLinkW, IVirtualDesktopManager, PropertiesSystem::{IPropertyStore, SHGetPropertyStoreForWindow}, - SHCreateItemFromParsingName, VirtualDesktopManager, SIGDN_NORMALDISPLAY, + SHCreateItemFromParsingName, SHQueryUserNotificationState, ShellLink, + VirtualDesktopManager, QUERY_USER_NOTIFICATION_STATE, QUNS_RUNNING_D3D_FULL_SCREEN, + SIGDN_NORMALDISPLAY, }, WindowsAndMessaging::{ EnumWindows, GetClassNameW, GetDesktopWindow, GetForegroundWindow, GetParent, - GetWindow, GetWindowLongW, GetWindowRect, GetWindowTextW, GetWindowThreadProcessId, - IsIconic, IsWindow, IsWindowVisible, IsZoomed, SetForegroundWindow, SetWindowPos, - ShowWindow, ShowWindowAsync, SystemParametersInfoW, ANIMATIONINFO, GWL_EXSTYLE, - GWL_STYLE, GW_OWNER, HWND_TOP, SET_WINDOW_POS_FLAGS, SHOW_WINDOW_CMD, - SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_GETANIMATION, SPI_GETDESKWALLPAPER, - SPI_SETANIMATION, SPI_SETDESKWALLPAPER, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, - SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SW_MINIMIZE, SW_NORMAL, SW_RESTORE, - SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, WINDOW_EX_STYLE, WINDOW_STYLE, WNDENUMPROC, + GetSystemMetrics, GetWindow, GetWindowLongW, GetWindowRect, GetWindowTextW, + GetWindowThreadProcessId, IsIconic, IsWindow, IsWindowVisible, IsZoomed, + PostMessageW, SetForegroundWindow, SetWindowPos, ShowWindow, ShowWindowAsync, + SystemParametersInfoW, ANIMATIONINFO, GWL_EXSTYLE, GWL_STYLE, GW_OWNER, HWND_TOP, + SET_WINDOW_POS_FLAGS, SHOW_WINDOW_CMD, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, + SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, + SPI_GETANIMATION, SPI_GETDESKWALLPAPER, SPI_SETANIMATION, SPI_SETDESKWALLPAPER, + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, + SW_MINIMIZE, SW_NORMAL, SW_RESTORE, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, + WINDOW_EX_STYLE, WINDOW_STYLE, WNDENUMPROC, WS_SIZEBOX, WS_THICKFRAME, }, }, }, }; use crate::{ - error_handler::{AppError, Result}, - hook::HOOK_MANAGER, - log_error, trace_lock, + error_handler::Result, + hook::HookManager, + modules::input::{domain::Point, Mouse}, utils::{is_virtual_desktop_supported, is_windows_11}, winevent::WinEvent, }; @@ -122,8 +137,15 @@ impl WindowsApi { callback: MONITORENUMPROC, callback_data_address: isize, ) -> Result<()> { - unsafe { EnumDisplayMonitors(HDC(0), None, callback, LPARAM(callback_data_address)) } - .ok()?; + unsafe { + EnumDisplayMonitors( + HDC::default(), + None, + callback, + LPARAM(callback_data_address), + ) + } + .ok()?; Ok(()) } @@ -132,6 +154,11 @@ impl WindowsApi { Ok(()) } + pub fn post_message(hwnd: HWND, message: u32, wparam: usize, lparam: isize) -> Result<()> { + unsafe { PostMessageW(hwnd, message, WPARAM(wparam), LPARAM(lparam))? }; + Ok(()) + } + pub fn get_device_pixel_ratio(hmonitor: HMONITOR) -> Result { let mut dpi_x: u32 = 0; let mut _dpi_y: u32 = 0; @@ -168,7 +195,7 @@ impl WindowsApi { if ProcessIdToSessionId(process_id, &mut session_id).is_ok() { Ok(session_id) } else { - Err(eyre!("could not determine current session id").into()) + Err("could not determine current session id".into()) } } } @@ -193,9 +220,17 @@ impl WindowsApi { unsafe { IsZoomed(hwnd) }.into() } + pub fn get_notification_state() -> Result { + Ok(unsafe { SHQueryUserNotificationState()? }) + } + + pub fn is_gaming_mode() -> Result { + Ok(Self::get_notification_state()? == QUNS_RUNNING_D3D_FULL_SCREEN) + } + pub fn is_fullscreen(hwnd: HWND) -> Result { let rc_monitor = WindowsApi::monitor_rect(WindowsApi::monitor_from_window(hwnd))?; - let window_rect = WindowsApi::get_window_rect_without_shadow(hwnd); + let window_rect = WindowsApi::get_inner_window_rect(hwnd)?; Ok(window_rect.left <= rc_monitor.left && window_rect.top <= rc_monitor.top && window_rect.right >= rc_monitor.right @@ -253,7 +288,7 @@ impl WindowsApi { rect: RECT, flags: SET_WINDOW_POS_FLAGS, ) -> Result<()> { - unsafe { + let result = unsafe { SetWindowPos( hwnd, order, @@ -262,7 +297,12 @@ impl WindowsApi { (rect.right - rect.left).abs(), (rect.bottom - rect.top).abs(), flags, - )?; + ) + }; + if let Err(error) = result { + if !error.code().is_ok() { + return Err(error.into()); + } } Ok(()) } @@ -277,15 +317,7 @@ impl WindowsApi { Some(_) => flags, None => SWP_NOZORDER | flags, }; - let order = order.unwrap_or(HWND(0)); - - if uflags.contains(SWP_ASYNCWINDOWPOS) { - let rect = *rect; - std::thread::spawn(move || Self::_set_position(hwnd, order, rect, uflags)); - return Ok(()); - } - - Self::_set_position(hwnd, order, *rect, uflags) + Self::_set_position(hwnd, order.unwrap_or_default(), *rect, uflags) } pub fn move_window(hwnd: HWND, rect: &RECT) -> Result<()> { @@ -315,25 +347,22 @@ impl WindowsApi { Ok(()) } - pub fn force_set_foreground(hwnd: HWND) -> Result<()> { - { - let mut hook_manager = trace_lock!(HOOK_MANAGER); - hook_manager.skip(WinEvent::SystemMinimizeStart, hwnd.0); - hook_manager.skip(WinEvent::SystemMinimizeEnd, hwnd.0); - } + pub fn async_force_set_foreground(hwnd: HWND) { + let hwnd = hwnd.0 as isize; + HookManager::run_with_async(move |hook_manager| { + let hwnd = HWND(hwnd as _); - Self::set_minimize_animation(false)?; - Self::show_window(hwnd, SW_MINIMIZE)?; - Self::show_window(hwnd, SW_RESTORE)?; - Self::set_minimize_animation(true)?; + hook_manager.skip(WinEvent::SystemMinimizeStart, hwnd); + hook_manager.skip(WinEvent::SystemMinimizeEnd, hwnd); - Self::bring_to(hwnd, HWND_TOP)?; - Self::set_foreground(hwnd)?; - Ok(()) - } + Self::set_minimize_animation(false)?; + Self::show_window(hwnd, SW_MINIMIZE)?; + Self::show_window(hwnd, SW_RESTORE)?; + Self::set_minimize_animation(true)?; - pub fn async_force_set_foreground(hwnd: HWND) { - std::thread::spawn(move || log_error!(Self::force_set_foreground(hwnd))); + Self::bring_to(hwnd, HWND_TOP)?; + Self::set_foreground(hwnd) + }); } fn open_process( @@ -345,7 +374,7 @@ impl WindowsApi { } pub fn open_current_process_token() -> Result { - let mut token_handle: HANDLE = HANDLE(0); + let mut token_handle = HANDLE::default(); unsafe { OpenProcessToken( Self::current_process(), @@ -353,6 +382,9 @@ impl WindowsApi { &mut token_handle, )?; } + if token_handle.is_invalid() { + return Err("OpenProcessToken failed".into()); + } Ok(token_handle) } @@ -376,7 +408,7 @@ impl WindowsApi { Ok(()) } - fn close_handle(handle: HANDLE) -> Result<()> { + pub fn close_handle(handle: HANDLE) -> Result<()> { unsafe { CloseHandle(handle)?; } @@ -388,31 +420,19 @@ impl WindowsApi { } pub fn get_parent(hwnd: HWND) -> HWND { - unsafe { GetParent(hwnd) } + // TODO change unwrap_or_default and return a result instead + unsafe { GetParent(hwnd).unwrap_or_default() } } pub fn get_owner(hwnd: HWND) -> HWND { - unsafe { GetWindow(hwnd, GW_OWNER) } + // TODO change unwrap_or_default and return a result instead + unsafe { GetWindow(hwnd, GW_OWNER).unwrap_or_default() } } pub fn get_desktop_window() -> HWND { unsafe { GetDesktopWindow() } } - pub fn exe_path_by_process(process_id: u32) -> Result { - let mut len = 512_u32; - let mut path: Vec = vec![0; len as usize]; - let text_ptr = path.as_mut_ptr(); - - let handle = Self::process_handle(process_id)?; - unsafe { - QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, PWSTR(text_ptr), &mut len)?; - } - Self::close_handle(handle)?; - - Ok(String::from_utf16(&path[..len as usize])?) - } - pub fn window_is_uwp_suspended(hwnd: HWND) -> Result { let (process_id, _) = Self::window_thread_process_id(hwnd); let handle = Self::open_process(PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)?; @@ -443,6 +463,20 @@ impl WindowsApi { Ok(is_frozen) } + pub fn exe_path_by_process(process_id: u32) -> Result { + let mut len = 512_u32; + let mut path: Vec = vec![0; len as usize]; + let text_ptr = path.as_mut_ptr(); + + let handle = Self::process_handle(process_id)?; + unsafe { + QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, PWSTR(text_ptr), &mut len)?; + } + Self::close_handle(handle)?; + + Ok(String::from_utf16(&path[..len as usize])?) + } + pub fn exe_path(hwnd: HWND) -> Result { let (process_id, _) = Self::window_thread_process_id(hwnd); Self::exe_path_by_process(process_id) @@ -461,7 +495,7 @@ impl WindowsApi { Ok(Self::exe_path(hwnd)? .split('\\') .last() - .ok_or_else(|| eyre!("there is no last element"))? + .ok_or("there is no last element")? .to_string()) } @@ -482,21 +516,38 @@ impl WindowsApi { Ok(unsafe { SHGetPropertyStoreForWindow(hwnd)? }) } - pub fn get_window_app_user_model_id(hwnd: HWND) -> Result { - let (process_id, _) = Self::window_thread_process_id(hwnd); - let handle = Self::process_handle(process_id)?; + /// this only works for exe apps + pub fn get_window_app_user_model_id_exe(hwnd: HWND) -> Result { + let store = Self::get_property_store_for_window(hwnd)?; + let value = unsafe { store.GetValue(&PKEY_AppUserModel_ID)? }; + if value.is_empty() { + return Err("No AppUserModel_ID".into()); + } + Ok(BSTR::try_from(&value)?.to_string()) + } - let mut buffer = vec![0u16; 1024]; - let mut size = buffer.len() as u32; - unsafe { GetApplicationUserModelId(handle, &mut size, PWSTR(buffer.as_mut_ptr())).ok()? }; + pub fn resolve_lnk_target(lnk_path: &Path) -> Result { + Com::run_with_context(|| { + let shell_link: IShellLinkW = Com::create_instance(&ShellLink)?; + let lnk_wide = lnk_path + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect_vec(); - Self::close_handle(handle)?; - Ok(String::from_utf16(&buffer[..size as usize])? - .trim_end_matches('\0') - .to_string()) + let persist_file: IPersistFile = shell_link.cast()?; + unsafe { persist_file.Load(PCWSTR(lnk_wide.as_ptr()), STGM_READ)? }; + + let mut target_path = vec![0u16; MAX_PATH as usize]; + let mut idk = WIN32_FIND_DATAW::default(); + unsafe { shell_link.GetPath(&mut target_path, &mut idk, 0)? }; + + target_path.retain(|x| *x != 0); + Ok(PathBuf::from(OsString::from_wide(&target_path))) + }) } - pub fn get_window_display_name(hwnd: HWND) -> Result { + pub fn get_executable_display_name(hwnd: HWND) -> Result { let shell_item = Self::get_shell_item(&Self::exe_path(hwnd)?)?; unsafe { match shell_item.GetString(&PKEY_FileDescription) { @@ -529,17 +580,17 @@ impl WindowsApi { u32::try_from(std::mem::size_of::())?, )?; } - Ok(()) } - pub fn get_window_rect(hwnd: HWND) -> RECT { + /// Get the window rect including drop shadow + pub fn get_outer_window_rect(hwnd: HWND) -> Result { let mut rect = RECT::default(); - unsafe { GetWindowRect(hwnd, &mut rect).ok() }; - rect + unsafe { GetWindowRect(hwnd, &mut rect)? }; + Ok(rect) } - pub fn get_window_thickness(hwnd: HWND) -> u32 { + fn get_window_thickness(hwnd: HWND) -> u32 { let mut thickness = 0u32; let _ = Self::dwm_get_window_attribute( hwnd, @@ -549,15 +600,24 @@ impl WindowsApi { thickness } - // some windows like explorer.exe have a shadow margin - pub fn get_window_rect_without_shadow(hwnd: HWND) -> RECT { + /// return the window rect excluding drop shadow & thick border + /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowrect#remarks + pub fn get_inner_window_rect(hwnd: HWND) -> Result { let mut rect = RECT::default(); - if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_ok() { - rect - } else { - unsafe { GetWindowRect(hwnd, &mut rect).ok() }; - rect + if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_err() { + rect = Self::get_outer_window_rect(hwnd)?; + } + + let styles = Self::get_styles(hwnd); + if styles.contains(WS_THICKFRAME) || styles.contains(WS_SIZEBOX) { + let thickness = Self::get_window_thickness(hwnd) as i32; + rect.left += thickness; + rect.top += thickness; + rect.right -= thickness; + rect.bottom -= thickness; } + + Ok(rect) } pub fn desktop_window() -> HWND { @@ -568,6 +628,17 @@ impl WindowsApi { unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY) } } + pub fn monitor_from_cursor_point() -> HMONITOR { + if let Ok(point) = Mouse::get_cursor_pos() { + return unsafe { MonitorFromPoint(*point.as_ref(), MONITOR_DEFAULTTOPRIMARY) }; + } + Self::primary_monitor() + } + + pub fn monitor_from_point(point: &Point) -> HMONITOR { + unsafe { MonitorFromPoint(*point.as_ref(), MONITOR_DEFAULTTOPRIMARY) } + } + pub fn primary_monitor() -> HMONITOR { unsafe { MonitorFromWindow(GetDesktopWindow(), MONITOR_DEFAULTTOPRIMARY) } } @@ -588,16 +659,35 @@ impl WindowsApi { Ok(p_physical_monitors[0]) } + pub fn monitor_index(hmonitor: HMONITOR) -> Result { + Ok(MonitorEnumerator::get_all()? + .into_iter() + .position(|m| m == hmonitor) + .ok_or("could not find monitor index")?) + } + pub fn monitor_name(hmonitor: HMONITOR) -> Result { let ex_info = Self::monitor_info(hmonitor)?; Ok(U16CStr::from_slice_truncate(&ex_info.szDevice) - .map_err(|_| AppError::Seelen("monitor name was not a valid u16 c string".to_owned()))? + .map_err(|_| "monitor name was not a valid u16 c string")? .to_ustring() .to_string_lossy() .trim_start_matches(r"\\.\") .to_string()) } + /// https://learn.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen + pub fn virtual_screen_rect() -> Result { + let mut rect = RECT::default(); + unsafe { + rect.left = GetSystemMetrics(SM_XVIRTUALSCREEN); + rect.top = GetSystemMetrics(SM_YVIRTUALSCREEN); + rect.right = rect.left + GetSystemMetrics(SM_CXVIRTUALSCREEN); + rect.bottom = rect.top + GetSystemMetrics(SM_CYVIRTUALSCREEN); + } + Ok(rect) + } + pub fn monitor_info(hmonitor: HMONITOR) -> Result { let mut ex_info = MONITORINFOEXW::default(); ex_info.monitorInfo.cbSize = u32::try_from(std::mem::size_of::())?; @@ -610,27 +700,14 @@ impl WindowsApi { } pub fn shadow_rect(hwnd: HWND) -> Result { - let rect_without_shadow = Self::get_window_rect_without_shadow(hwnd); - - let mut rect_with_shadow = Default::default(); - unsafe { GetWindowRect(hwnd, &mut rect_with_shadow)? }; - - let mut shadow_rect = RECT { - left: rect_with_shadow.left - rect_without_shadow.left, - top: rect_with_shadow.top - rect_without_shadow.top, - right: rect_with_shadow.right - rect_without_shadow.right, - bottom: rect_with_shadow.bottom - rect_without_shadow.bottom, - }; - - if !Self::is_maximized(hwnd) { - let thickness = Self::get_window_thickness(hwnd) as i32; - shadow_rect.left -= thickness; - shadow_rect.top -= thickness; - shadow_rect.right += thickness; - shadow_rect.bottom += thickness; - } - - Ok(shadow_rect) + let outer_rect = Self::get_outer_window_rect(hwnd)?; + let inner_rect = Self::get_inner_window_rect(hwnd)?; + Ok(RECT { + left: outer_rect.left - inner_rect.left, + top: outer_rect.top - inner_rect.top, + right: outer_rect.right - inner_rect.right, + bottom: outer_rect.bottom - inner_rect.bottom, + }) } pub fn _get_virtual_desktop_manager() -> Result { @@ -649,7 +726,7 @@ impl WindowsApi { } } if desktop_id.to_u128() == 0 { - return Err(eyre!("Failed to get desktop id for: {hwnd:?}").into()); + return Err(format!("Failed to get desktop id for: {hwnd:?}").into()); } Ok(desktop_id) } @@ -694,6 +771,11 @@ impl WindowsApi { Ok(()) } + pub fn refresh_desktop() -> Result<()> { + unsafe { SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, None, SPIF_UPDATEINIFILE)? }; + Ok(()) + } + pub fn get_min_animation_info() -> Result { let mut anim_info: ANIMATIONINFO = unsafe { core::mem::zeroed() }; anim_info.cbSize = core::mem::size_of::() as u32; @@ -733,7 +815,7 @@ impl WindowsApi { pub fn set_suspend_state() -> Result<()> { let success = unsafe { SetSuspendState(false, true, false).as_bool() }; if !success { - return Err(eyre!("Failed to set suspend state").into()); + return Err("Failed to set suspend state".into()); } Ok(()) } @@ -767,9 +849,9 @@ impl WindowsApi { Ok(power_status) } - pub fn extract_thumbnail_from_stream( + pub fn stream_to_dynamic_image( stream: IRandomAccessStreamWithContentType, - ) -> Result { + ) -> Result { let size = stream.Size()?; let mut buffer = vec![0u8; size as usize]; @@ -780,9 +862,15 @@ impl WindowsApi { data_reader.ReadBytes(&mut buffer)?; let image = image::load_from_memory_with_format(&buffer, image::ImageFormat::Png)?; + Ok(image) + } + + pub fn extract_thumbnail_from_stream( + stream: IRandomAccessStreamWithContentType, + ) -> Result { + let image = Self::stream_to_dynamic_image(stream)?; let image_path = std::env::temp_dir().join(format!("{}.png", uuid::Uuid::new_v4())); image.save(&image_path)?; - Ok(image_path) } diff --git a/src/background/windows_api/monitor.rs b/src/background/windows_api/monitor.rs new file mode 100644 index 00000000..150deb0c --- /dev/null +++ b/src/background/windows_api/monitor.rs @@ -0,0 +1,59 @@ +use windows::Win32::Graphics::Gdi::HMONITOR; + +use crate::{error_handler::Result, modules::input::domain::Point}; + +use super::{MonitorEnumerator, WindowsApi}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Monitor(HMONITOR); +unsafe impl Send for Monitor {} +unsafe impl Sync for Monitor {} + +impl From for Monitor { + fn from(hmonitor: HMONITOR) -> Self { + Self(hmonitor) + } +} + +impl From for Monitor { + fn from(hmonitor: isize) -> Self { + Self(HMONITOR(hmonitor as _)) + } +} + +impl From<&Point> for Monitor { + fn from(point: &Point) -> Self { + let hmonitor = WindowsApi::monitor_from_point(point); + Self(hmonitor) + } +} + +impl Monitor { + pub fn raw(&self) -> HMONITOR { + self.0 + } + + pub fn id(&self) -> Result { + WindowsApi::monitor_name(self.0) + } + + pub fn index(&self) -> Result { + WindowsApi::monitor_index(self.0) + } + + pub fn at(index: usize) -> Option { + let monitors = MonitorEnumerator::get_all().ok()?; + monitors.get(index).map(|m| Self::from(*m)) + } + + pub fn by_id(id: &str) -> Option { + for m in MonitorEnumerator::get_all().ok()? { + if let Ok(name) = WindowsApi::monitor_name(m) { + if name == id { + return Some(Self::from(m)); + } + } + } + None + } +} diff --git a/src/background/windows_api/process.rs b/src/background/windows_api/process.rs index cab60ff4..ba2ec9e7 100644 --- a/src/background/windows_api/process.rs +++ b/src/background/windows_api/process.rs @@ -1,3 +1,20 @@ +use std::path::PathBuf; + +use windows::{ + ApplicationModel::AppInfo, + Win32::{ + Foundation::HANDLE, + Storage::Packaging::Appx::{ + GetApplicationUserModelId, GetPackageFamilyName, GetPackageFullName, + }, + System::Threading::PROCESS_QUERY_INFORMATION, + }, +}; + +use crate::error_handler::Result; + +use super::{string_utils::WindowsString, window::Window, WindowsApi}; + // https://stackoverflow.com/questions/47300622/meaning-of-flags-in-process-extended-basic-information-struct #[allow(dead_code)] pub enum ProcessInformationFlag { @@ -11,3 +28,67 @@ pub enum ProcessInformationFlag { IsSecureProcess = 0x80, IsSubsystemProcess = 0x100, } + +pub struct Process(u32); + +impl Process { + pub fn from_window(window: &Window) -> Self { + let (process_id, _) = WindowsApi::window_thread_process_id(window.hwnd()); + Self(process_id) + } + + pub fn id(&self) -> u32 { + self.0 + } + + fn with_handle(&self, f: F) -> Result + where + F: FnOnce(HANDLE) -> T, + { + let handle = WindowsApi::open_process(PROCESS_QUERY_INFORMATION, false, self.0)?; + let result = f(handle); + WindowsApi::close_handle(handle)?; + Ok(result) + } + + pub fn package_family_name(&self) -> Result { + self.with_handle(|hprocess| { + let mut len = 1024_u32; + let mut family_name = WindowsString::new_to_fill(len as usize); + unsafe { GetPackageFamilyName(hprocess, &mut len, family_name.as_pwstr()).ok()? }; + Ok(family_name.to_string()) + })? + } + + pub fn package_full_name(&self) -> Result { + self.with_handle(|hprocess| { + let mut len = 1024_u32; + let mut family_name = WindowsString::new_to_fill(len as usize); + unsafe { GetPackageFullName(hprocess, &mut len, family_name.as_pwstr()).ok()? }; + Ok(family_name.to_string()) + })? + } + + /// package app user model id + pub fn package_app_user_model_id(&self) -> Result { + self.with_handle(|hprocess| { + let mut len = 1024_u32; + let mut id = WindowsString::new_to_fill(len as usize); + unsafe { GetApplicationUserModelId(hprocess, &mut len, id.as_pwstr()).ok()? }; + Ok(id.to_string()) + })? + } + + pub fn package_app_info(&self) -> Result { + let app_info = AppInfo::GetFromAppUserModelId(&self.package_app_user_model_id()?.into())?; + Ok(app_info) + } + + pub fn program_path(&self) -> Result { + let path_string = WindowsApi::exe_path_by_process(self.0)?; + if path_string.is_empty() { + return Err("exe path is empty".into()); + } + Ok(PathBuf::from(path_string)) + } +} diff --git a/src/background/windows_api/string_utils.rs b/src/background/windows_api/string_utils.rs new file mode 100644 index 00000000..bddabbd2 --- /dev/null +++ b/src/background/windows_api/string_utils.rs @@ -0,0 +1,31 @@ +use windows_core::{PCWSTR, PWSTR}; + +pub struct WindowsString { + pub inner: Vec, +} + +impl WindowsString { + pub fn new_to_fill(len: usize) -> Self { + Self { + inner: vec![0; len], + } + } + + pub fn as_pcwstr(&self) -> PCWSTR { + PCWSTR(self.inner.as_ptr()) + } + + pub fn as_pwstr(&mut self) -> PWSTR { + PWSTR(self.inner.as_mut_ptr()) + } +} + +impl std::fmt::Display for WindowsString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + String::from_utf16_lossy(&self.inner).trim_end_matches("\0") + ) + } +} diff --git a/src/background/windows_api/window.rs b/src/background/windows_api/window.rs index 4b554766..fed4e7ce 100644 --- a/src/background/windows_api/window.rs +++ b/src/background/windows_api/window.rs @@ -1,125 +1,202 @@ -use std::{ - fmt::{Debug, Display}, - path::PathBuf, -}; - -use windows::Win32::Foundation::HWND; - -use crate::{ - error_handler::Result, seelen_bar::FancyToolbar, seelen_weg::SeelenWeg, - seelen_wm::WindowManager, -}; - -use super::{WindowEnumerator, WindowsApi}; - -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct Window(HWND); - -impl From for Window { - fn from(hwnd: HWND) -> Self { - Self(hwnd) - } -} - -impl Debug for Window { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Window") - .field("handle", &self.0 .0) - .field("title", &self.title()) - .field("class", &self.class()) - .field("exe", &self.exe()) - .finish() - } -} - -impl Display for Window { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Window({:x})", self.0 .0) - } -} - -pub const APP_FRAME_HOST_PATH: &str = "C:\\Windows\\System32\\ApplicationFrameHost.exe"; -impl Window { - pub fn hwnd(self) -> HWND { - self.0 - } - - pub fn app_user_model_id(&self) -> Option { - WindowsApi::get_window_app_user_model_id(self.0).ok() - } - - pub fn title(&self) -> String { - WindowsApi::get_window_text(self.0) - } - - pub fn class(&self) -> String { - WindowsApi::get_class(self.0).unwrap_or_default() - } - - pub fn exe(&self) -> Result { - WindowsApi::exe_path_v2(self.0) - } - - pub fn app_display_name(&self) -> Result { - WindowsApi::get_window_display_name(self.0) - } - - pub fn parent(&self) -> Option { - let parent = WindowsApi::get_parent(self.0); - if parent.0 != 0 { - Some(Window(parent)) - } else { - None - } - } - - pub fn children(&self) -> Result> { - WindowEnumerator::new() - .with_parent(self.0) - .map(Window::from) - } - - pub fn is_window(&self) -> bool { - WindowsApi::is_window(self.0) - } - - pub fn is_visible(&self) -> bool { - WindowsApi::is_window_visible(self.0) - } - - /// is the window an Application Frame Host - pub fn is_frame(&self) -> Result { - Ok(self.exe()? == PathBuf::from(APP_FRAME_HOST_PATH)) - } - - /// will fail if the window is not a frame - pub fn get_frame_creator(&self) -> Result> { - if !self.is_frame()? { - return Err("Window is not a frame".into()); - } - for window in self.children()? { - if !window.class().starts_with("ApplicationFrame") { - return Ok(Some(window)); - } - } - Ok(None) - } - - pub fn is_desktop(&self) -> bool { - WindowsApi::get_desktop_window() == self.0 || self.class() == "Progman" - } - - pub fn is_seelen_overlay(&self) -> bool { - if let Ok(exe) = self.exe() { - return exe.ends_with("seelen-ui.exe") - && [ - FancyToolbar::TITLE, - WindowManager::TITLE, - SeelenWeg::TITLE, - SeelenWeg::TITLE_HITBOX, - ] - .contains(&self.title().as_str()); - } - false - } -} +use seelen_core::rect::Rect; +use std::{ + fmt::{Debug, Display}, + path::PathBuf, +}; + +use windows::Win32::Foundation::HWND; + +use crate::{ + error_handler::Result, + modules::virtual_desk::{get_vd_manager, VirtualDesktop}, + seelen_bar::FancyToolbar, + seelen_rofi::SeelenRofi, + seelen_wall::SeelenWall, + seelen_weg::SeelenWeg, + seelen_wm_v2::instance::WindowManagerV2, +}; + +use super::{monitor::Monitor, process::Process, WindowEnumerator, WindowsApi}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Window(HWND); +unsafe impl Send for Window {} +unsafe impl Sync for Window {} + +impl From for Window { + fn from(hwnd: HWND) -> Self { + Self(hwnd) + } +} + +impl From for Window { + fn from(addr: isize) -> Self { + Self(HWND(addr as _)) + } +} + +impl Debug for Window { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Window") + .field("handle", &self.0 .0) + .field("title", &self.title()) + .field("class", &self.class()) + .field("exe", &self.exe()) + .finish() + } +} + +impl Display for Window { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Window({:?})", self.0) + } +} + +pub const APP_FRAME_HOST_PATH: &str = "C:\\Windows\\System32\\ApplicationFrameHost.exe"; +impl Window { + pub fn hwnd(&self) -> HWND { + self.0 + } + + pub fn address(&self) -> isize { + self.0 .0 as isize + } + + /// this could return the process user model id if it is a uwp + /// or the app user model id asigned to the window via property-store + pub fn app_user_model_id(&self) -> Option { + if let Ok(id) = self.process().package_app_user_model_id() { + return Some(id); + } + WindowsApi::get_window_app_user_model_id_exe(self.0).ok() + } + + pub fn title(&self) -> String { + WindowsApi::get_window_text(self.0) + } + + pub fn class(&self) -> String { + WindowsApi::get_class(self.0).unwrap_or_default() + } + + pub fn process(&self) -> Process { + Process::from_window(self) + } + + /// will fail if process is restricted and the invoker is not running as admin + pub fn exe(&self) -> Result { + WindowsApi::exe_path_v2(self.0) + } + + pub fn app_display_name(&self) -> Result { + if let Ok(info) = self.process().package_app_info() { + return Ok(info.DisplayInfo()?.DisplayName()?.to_string_lossy()); + } + WindowsApi::get_executable_display_name(self.0) + } + + pub fn outer_rect(&self) -> Result { + Ok(WindowsApi::get_outer_window_rect(self.hwnd())?.into()) + } + + pub fn inner_rect(&self) -> Result { + Ok(WindowsApi::get_inner_window_rect(self.hwnd())?.into()) + } + + pub fn parent(&self) -> Option { + let parent = WindowsApi::get_parent(self.0); + if !parent.is_invalid() { + Some(Window(parent)) + } else { + None + } + } + + pub fn children(&self) -> Result> { + WindowEnumerator::new() + .with_parent(self.0) + .map(Window::from) + } + + pub fn monitor(&self) -> Monitor { + Monitor::from(WindowsApi::monitor_from_window(self.0)) + } + + pub fn workspace(&self) -> Result { + get_vd_manager().get_by_window(self.address()) + } + + pub fn is_window(&self) -> bool { + WindowsApi::is_window(self.0) + } + + pub fn is_visible(&self) -> bool { + WindowsApi::is_window_visible(self.0) + } + + pub fn is_minimized(&self) -> bool { + WindowsApi::is_iconic(self.0) + } + + pub fn is_maximized(&self) -> bool { + WindowsApi::is_maximized(self.0) + } + + pub fn is_cloaked(&self) -> bool { + WindowsApi::is_cloaked(self.0).unwrap_or(false) + } + + pub fn is_foreground(&self) -> bool { + WindowsApi::get_foreground_window() == self.0 + } + + pub fn is_fullscreen(&self) -> bool { + WindowsApi::is_fullscreen(self.0).unwrap_or(false) + } + + /// is the window an Application Frame Host + pub fn is_frame(&self) -> Result { + Ok(self.exe()? == PathBuf::from(APP_FRAME_HOST_PATH)) + } + + /// will fail if the window is not a frame + pub fn get_frame_creator(&self) -> Result> { + if !self.is_frame()? { + return Err("Window is not a frame".into()); + } + for window in self.children()? { + if !window.class().starts_with("ApplicationFrame") { + return Ok(Some(window)); + } + } + Ok(None) + } + + /// this means all windows that are part of the UI desktop not the real desktop window + pub fn is_desktop(&self) -> bool { + let class = self.class(); + WindowsApi::get_desktop_window() == self.0 + || class == "Progman" + || (class == "WorkerW" + && self.children().is_ok_and(|children| { + children + .iter() + .any(|child| child.class() == "SHELLDLL_DefView") + })) + } + + pub fn is_seelen_overlay(&self) -> bool { + if let Ok(exe) = self.exe() { + return exe.ends_with("seelen-ui.exe") + && [ + FancyToolbar::TITLE, + WindowManagerV2::TITLE, + SeelenWeg::TITLE, + SeelenRofi::TITLE, + SeelenWall::TITLE, + ] + .contains(&self.title().as_str()); + } + false + } +} diff --git a/src/background/winevent.rs b/src/background/winevent.rs index 31d19d34..b20657b4 100644 --- a/src/background/winevent.rs +++ b/src/background/winevent.rs @@ -91,14 +91,24 @@ use windows::Win32::UI::WindowsAndMessaging::{ EVENT_SYSTEM_SWITCHEND, EVENT_SYSTEM_SWITCHER_APPDROPPED, }; +use crate::error_handler::Result; use crate::trace_lock; -use crate::utils::constants::IGNORE_FULLSCREEN; +use crate::utils::constants::NATIVE_UI_POPUP_CLASSES; +use crate::windows_api::window::Window; use crate::windows_api::WindowsApi; lazy_static! { - static ref FULLSCREENED: Mutex> = Mutex::new(Vec::new()); + static ref FULLSCREENED: Mutex> = Mutex::new(None); } +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct SyntheticFullscreenData { + pub handle: HWND, + pub monitor: HMONITOR, +} + +unsafe impl Send for SyntheticFullscreenData {} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[repr(u32)] #[allow(dead_code)] @@ -187,184 +197,149 @@ pub enum WinEvent { UiaEventIdStart = EVENT_UIA_EVENTID_START, UiaPropIdSEnd = EVENT_UIA_PROPID_END, UiaPropIdStart = EVENT_UIA_PROPID_START, + /// Fallback for unknown/missing Win32 events + Unknown(u32), // ================== Synthetic events ================== SyntheticFullscreenStart(SyntheticFullscreenData), SyntheticFullscreenEnd(SyntheticFullscreenData), } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct SyntheticFullscreenData { - pub handle: HWND, - pub monitor: HMONITOR, -} - -impl TryFrom for WinEvent { - type Error = (); - - fn try_from(value: u32) -> Result { +impl From for WinEvent { + fn from(value: u32) -> Self { match value { - EVENT_AIA_END => Ok(Self::AiaEnd), - EVENT_AIA_START => Ok(Self::AiaStart), - EVENT_CONSOLE_CARET => Ok(Self::ConsoleCaret), - EVENT_CONSOLE_END => Ok(Self::ConsoleEnd), - EVENT_CONSOLE_END_APPLICATION => Ok(Self::ConsoleEndApplication), - EVENT_CONSOLE_LAYOUT => Ok(Self::ConsoleLayout), - EVENT_CONSOLE_START_APPLICATION => Ok(Self::ConsoleStartApplication), - EVENT_CONSOLE_UPDATE_REGION => Ok(Self::ConsoleUpdateRegion), - EVENT_CONSOLE_UPDATE_SCROLL => Ok(Self::ConsoleUpdateScroll), - EVENT_CONSOLE_UPDATE_SIMPLE => Ok(Self::ConsoleUpdateSimple), - EVENT_OBJECT_ACCELERATORCHANGE => Ok(Self::ObjectAcceleratorChange), - EVENT_OBJECT_CLOAKED => Ok(Self::ObjectCloaked), - EVENT_OBJECT_CONTENTSCROLLED => Ok(Self::ObjectContentScrolled), - EVENT_OBJECT_CREATE => Ok(Self::ObjectCreate), - EVENT_OBJECT_DEFACTIONCHANGE => Ok(Self::ObjectDefActionChange), - EVENT_OBJECT_DESCRIPTIONCHANGE => Ok(Self::ObjectDescriptionChange), - EVENT_OBJECT_DESTROY => Ok(Self::ObjectDestroy), - EVENT_OBJECT_DRAGCANCEL => Ok(Self::ObjectDragCancel), - EVENT_OBJECT_DRAGCOMPLETE => Ok(Self::ObjectDragComplete), - EVENT_OBJECT_DRAGDROPPED => Ok(Self::ObjectDragDropped), - EVENT_OBJECT_DRAGENTER => Ok(Self::ObjectDragEnter), - EVENT_OBJECT_DRAGLEAVE => Ok(Self::ObjectDragLeave), - EVENT_OBJECT_DRAGSTART => Ok(Self::ObjectDragStart), - EVENT_OBJECT_END => Ok(Self::ObjectEnd), - EVENT_OBJECT_FOCUS => Ok(Self::ObjectFocus), - EVENT_OBJECT_HELPCHANGE => Ok(Self::ObjectHelpChange), - EVENT_OBJECT_HIDE => Ok(Self::ObjectHide), - EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED => Ok(Self::ObjectHostedObjectsInvalidated), - EVENT_OBJECT_IME_CHANGE => Ok(Self::ObjectImeChange), - EVENT_OBJECT_IME_HIDE => Ok(Self::ObjectImeHide), - EVENT_OBJECT_IME_SHOW => Ok(Self::ObjectImeShow), - EVENT_OBJECT_INVOKED => Ok(Self::ObjectInvoked), - EVENT_OBJECT_LIVEREGIONCHANGED => Ok(Self::ObjectLiveRegionChanged), - EVENT_OBJECT_LOCATIONCHANGE => Ok(Self::ObjectLocationChange), - EVENT_OBJECT_NAMECHANGE => Ok(Self::ObjectNameChange), - EVENT_OBJECT_PARENTCHANGE => Ok(Self::ObjectParentChange), - EVENT_OBJECT_REORDER => Ok(Self::ObjectReorder), - EVENT_OBJECT_SELECTION => Ok(Self::ObjectSelection), - EVENT_OBJECT_SELECTIONADD => Ok(Self::ObjectSelectionAdd), - EVENT_OBJECT_SELECTIONREMOVE => Ok(Self::ObjectSelectionRemove), - EVENT_OBJECT_SELECTIONWITHIN => Ok(Self::ObjectSelectionWithin), - EVENT_OBJECT_SHOW => Ok(Self::ObjectShow), - EVENT_OBJECT_STATECHANGE => Ok(Self::ObjectStateChange), + EVENT_AIA_END => Self::AiaEnd, + EVENT_AIA_START => Self::AiaStart, + EVENT_CONSOLE_CARET => Self::ConsoleCaret, + EVENT_CONSOLE_END => Self::ConsoleEnd, + EVENT_CONSOLE_END_APPLICATION => Self::ConsoleEndApplication, + EVENT_CONSOLE_LAYOUT => Self::ConsoleLayout, + EVENT_CONSOLE_START_APPLICATION => Self::ConsoleStartApplication, + EVENT_CONSOLE_UPDATE_REGION => Self::ConsoleUpdateRegion, + EVENT_CONSOLE_UPDATE_SCROLL => Self::ConsoleUpdateScroll, + EVENT_CONSOLE_UPDATE_SIMPLE => Self::ConsoleUpdateSimple, + EVENT_OBJECT_ACCELERATORCHANGE => Self::ObjectAcceleratorChange, + EVENT_OBJECT_CLOAKED => Self::ObjectCloaked, + EVENT_OBJECT_CONTENTSCROLLED => Self::ObjectContentScrolled, + EVENT_OBJECT_CREATE => Self::ObjectCreate, + EVENT_OBJECT_DEFACTIONCHANGE => Self::ObjectDefActionChange, + EVENT_OBJECT_DESCRIPTIONCHANGE => Self::ObjectDescriptionChange, + EVENT_OBJECT_DESTROY => Self::ObjectDestroy, + EVENT_OBJECT_DRAGCANCEL => Self::ObjectDragCancel, + EVENT_OBJECT_DRAGCOMPLETE => Self::ObjectDragComplete, + EVENT_OBJECT_DRAGDROPPED => Self::ObjectDragDropped, + EVENT_OBJECT_DRAGENTER => Self::ObjectDragEnter, + EVENT_OBJECT_DRAGLEAVE => Self::ObjectDragLeave, + EVENT_OBJECT_DRAGSTART => Self::ObjectDragStart, + EVENT_OBJECT_END => Self::ObjectEnd, + EVENT_OBJECT_FOCUS => Self::ObjectFocus, + EVENT_OBJECT_HELPCHANGE => Self::ObjectHelpChange, + EVENT_OBJECT_HIDE => Self::ObjectHide, + EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED => Self::ObjectHostedObjectsInvalidated, + EVENT_OBJECT_IME_CHANGE => Self::ObjectImeChange, + EVENT_OBJECT_IME_HIDE => Self::ObjectImeHide, + EVENT_OBJECT_IME_SHOW => Self::ObjectImeShow, + EVENT_OBJECT_INVOKED => Self::ObjectInvoked, + EVENT_OBJECT_LIVEREGIONCHANGED => Self::ObjectLiveRegionChanged, + EVENT_OBJECT_LOCATIONCHANGE => Self::ObjectLocationChange, + EVENT_OBJECT_NAMECHANGE => Self::ObjectNameChange, + EVENT_OBJECT_PARENTCHANGE => Self::ObjectParentChange, + EVENT_OBJECT_REORDER => Self::ObjectReorder, + EVENT_OBJECT_SELECTION => Self::ObjectSelection, + EVENT_OBJECT_SELECTIONADD => Self::ObjectSelectionAdd, + EVENT_OBJECT_SELECTIONREMOVE => Self::ObjectSelectionRemove, + EVENT_OBJECT_SELECTIONWITHIN => Self::ObjectSelectionWithin, + EVENT_OBJECT_SHOW => Self::ObjectShow, + EVENT_OBJECT_STATECHANGE => Self::ObjectStateChange, EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED => { - Ok(Self::ObjectTextEditConversionTargetChanged) + Self::ObjectTextEditConversionTargetChanged } - EVENT_OBJECT_TEXTSELECTIONCHANGED => Ok(Self::ObjectTextSelectionChanged), - EVENT_OBJECT_UNCLOAKED => Ok(Self::ObjectUncloaked), - EVENT_OBJECT_VALUECHANGE => Ok(Self::ObjectValueChange), - EVENT_OEM_DEFINED_END => Ok(Self::OemDefinedEnd), - EVENT_OEM_DEFINED_START => Ok(Self::OemDefinedStart), - EVENT_SYSTEM_ALERT => Ok(Self::SystemAlert), - EVENT_SYSTEM_ARRANGMENTPREVIEW => Ok(Self::SystemArrangementPreview), - EVENT_SYSTEM_CAPTUREEND => Ok(Self::SystemCaptureEnd), - EVENT_SYSTEM_CAPTURESTART => Ok(Self::SystemCaptureStart), - EVENT_SYSTEM_CONTEXTHELPEND => Ok(Self::SystemContextHelpEnd), - EVENT_SYSTEM_CONTEXTHELPSTART => Ok(Self::SystemContextHelpStart), - EVENT_SYSTEM_DESKTOPSWITCH => Ok(Self::SystemDesktopSwitch), - EVENT_SYSTEM_DIALOGEND => Ok(Self::SystemDialogEnd), - EVENT_SYSTEM_DIALOGSTART => Ok(Self::SystemDialogStart), - EVENT_SYSTEM_DRAGDROPEND => Ok(Self::SystemDragDropEnd), - EVENT_SYSTEM_DRAGDROPSTART => Ok(Self::SystemDragDropStart), - EVENT_SYSTEM_END => Ok(Self::SystemEnd), - EVENT_SYSTEM_FOREGROUND => Ok(Self::SystemForeground), - EVENT_SYSTEM_IME_KEY_NOTIFICATION => Ok(Self::SystemImeKeyNotification), - EVENT_SYSTEM_MENUEND => Ok(Self::SystemMenuEnd), - EVENT_SYSTEM_MENUPOPUPEND => Ok(Self::SystemMenuPopupEnd), - EVENT_SYSTEM_MENUPOPUPSTART => Ok(Self::SystemMenuPopupStart), - EVENT_SYSTEM_MENUSTART => Ok(Self::SystemMenuStart), - EVENT_SYSTEM_MINIMIZEEND => Ok(Self::SystemMinimizeEnd), - EVENT_SYSTEM_MINIMIZESTART => Ok(Self::SystemMinimizeStart), - EVENT_SYSTEM_MOVESIZEEND => Ok(Self::SystemMoveSizeEnd), - EVENT_SYSTEM_MOVESIZESTART => Ok(Self::SystemMoveSizeStart), - EVENT_SYSTEM_SCROLLINGEND => Ok(Self::SystemScrollingEnd), - EVENT_SYSTEM_SCROLLINGSTART => Ok(Self::SystemScrollingStart), - EVENT_SYSTEM_SOUND => Ok(Self::SystemSound), - EVENT_SYSTEM_SWITCHEND => Ok(Self::SystemSwitchEnd), - EVENT_SYSTEM_SWITCHER_APPDROPPED => Ok(Self::SystemSwitcherAppDropped), - EVENT_SYSTEM_SWITCHER_APPGRABBED => Ok(Self::SystemSwitcherAppGrabbed), - EVENT_SYSTEM_SWITCHER_APPOVERTARGET => Ok(Self::SystemSwitcherAppOverTarget), - EVENT_SYSTEM_SWITCHER_CANCELLED => Ok(Self::SystemSwitcherCancelled), - EVENT_SYSTEM_SWITCHSTART => Ok(Self::SystemSwitchStart), - EVENT_UIA_EVENTID_END => Ok(Self::UiaEventIdSEnd), - EVENT_UIA_EVENTID_START => Ok(Self::UiaEventIdStart), - EVENT_UIA_PROPID_END => Ok(Self::UiaPropIdSEnd), - EVENT_UIA_PROPID_START => Ok(Self::UiaPropIdStart), - - _ => Err(()), + EVENT_OBJECT_TEXTSELECTIONCHANGED => Self::ObjectTextSelectionChanged, + EVENT_OBJECT_UNCLOAKED => Self::ObjectUncloaked, + EVENT_OBJECT_VALUECHANGE => Self::ObjectValueChange, + EVENT_OEM_DEFINED_END => Self::OemDefinedEnd, + EVENT_OEM_DEFINED_START => Self::OemDefinedStart, + EVENT_SYSTEM_ALERT => Self::SystemAlert, + EVENT_SYSTEM_ARRANGMENTPREVIEW => Self::SystemArrangementPreview, + EVENT_SYSTEM_CAPTUREEND => Self::SystemCaptureEnd, + EVENT_SYSTEM_CAPTURESTART => Self::SystemCaptureStart, + EVENT_SYSTEM_CONTEXTHELPEND => Self::SystemContextHelpEnd, + EVENT_SYSTEM_CONTEXTHELPSTART => Self::SystemContextHelpStart, + EVENT_SYSTEM_DESKTOPSWITCH => Self::SystemDesktopSwitch, + EVENT_SYSTEM_DIALOGEND => Self::SystemDialogEnd, + EVENT_SYSTEM_DIALOGSTART => Self::SystemDialogStart, + EVENT_SYSTEM_DRAGDROPEND => Self::SystemDragDropEnd, + EVENT_SYSTEM_DRAGDROPSTART => Self::SystemDragDropStart, + EVENT_SYSTEM_END => Self::SystemEnd, + EVENT_SYSTEM_FOREGROUND => Self::SystemForeground, + EVENT_SYSTEM_IME_KEY_NOTIFICATION => Self::SystemImeKeyNotification, + EVENT_SYSTEM_MENUEND => Self::SystemMenuEnd, + EVENT_SYSTEM_MENUPOPUPEND => Self::SystemMenuPopupEnd, + EVENT_SYSTEM_MENUPOPUPSTART => Self::SystemMenuPopupStart, + EVENT_SYSTEM_MENUSTART => Self::SystemMenuStart, + EVENT_SYSTEM_MINIMIZEEND => Self::SystemMinimizeEnd, + EVENT_SYSTEM_MINIMIZESTART => Self::SystemMinimizeStart, + EVENT_SYSTEM_MOVESIZEEND => Self::SystemMoveSizeEnd, + EVENT_SYSTEM_MOVESIZESTART => Self::SystemMoveSizeStart, + EVENT_SYSTEM_SCROLLINGEND => Self::SystemScrollingEnd, + EVENT_SYSTEM_SCROLLINGSTART => Self::SystemScrollingStart, + EVENT_SYSTEM_SOUND => Self::SystemSound, + EVENT_SYSTEM_SWITCHEND => Self::SystemSwitchEnd, + EVENT_SYSTEM_SWITCHER_APPDROPPED => Self::SystemSwitcherAppDropped, + EVENT_SYSTEM_SWITCHER_APPGRABBED => Self::SystemSwitcherAppGrabbed, + EVENT_SYSTEM_SWITCHER_APPOVERTARGET => Self::SystemSwitcherAppOverTarget, + EVENT_SYSTEM_SWITCHER_CANCELLED => Self::SystemSwitcherCancelled, + EVENT_SYSTEM_SWITCHSTART => Self::SystemSwitchStart, + EVENT_UIA_EVENTID_END => Self::UiaEventIdSEnd, + EVENT_UIA_EVENTID_START => Self::UiaEventIdStart, + EVENT_UIA_PROPID_END => Self::UiaPropIdSEnd, + EVENT_UIA_PROPID_START => Self::UiaPropIdStart, + _ => Self::Unknown(value), } } } impl WinEvent { - pub fn should_handle_fullscreen_events(&self, hwnd: HWND) -> bool { - hwnd == WindowsApi::get_foreground_window() - && WindowsApi::is_window_visible(hwnd) - && !IGNORE_FULLSCREEN.contains(&WindowsApi::get_window_text(hwnd)) - } - - pub fn get_synthetic(&self, origin: HWND) -> Option { + pub fn get_synthetics(&self, origin: HWND) -> Result> { + let mut synthetics = Vec::new(); match self { - Self::ObjectShow - | Self::ObjectCreate - | Self::ObjectUncloaked - | Self::SystemMinimizeEnd => { - if !self.should_handle_fullscreen_events(origin) { - return None; - } - - let mut fullscreened = trace_lock!(FULLSCREENED); - if WindowsApi::is_fullscreen(origin).ok()? - && !fullscreened.iter().any(|x| x.handle == origin) - { - let data = SyntheticFullscreenData { - handle: origin, - monitor: WindowsApi::monitor_from_window(origin), - }; - fullscreened.push(data); - Some(Self::SyntheticFullscreenStart(data)) - } else { - None - } - } - Self::ObjectHide - | Self::ObjectDestroy - | Self::ObjectCloaked - | Self::SystemMinimizeStart => { - let mut fullscreened = trace_lock!(FULLSCREENED); - if let Some(index) = fullscreened.iter().position(|x| x.handle == origin) { - let data = fullscreened.remove(index); - Some(Self::SyntheticFullscreenEnd(data)) - } else { - None - } - } - Self::ObjectLocationChange => { - if !self.should_handle_fullscreen_events(origin) { - return None; - } - - let mut fullscreened = trace_lock!(FULLSCREENED); - let was_fullscreen = fullscreened.iter().position(|x| x.handle == origin); - let is_fullscreen = WindowsApi::is_fullscreen(origin).ok()?; + Self::SystemForeground | Self::ObjectLocationChange => { + if origin == WindowsApi::get_foreground_window() { + let mut latest_fullscreened = trace_lock!(FULLSCREENED); + let window = Window::from(origin); + let is_origin_fullscreen = window.is_fullscreen() + && !window.is_desktop() + && !window.is_seelen_overlay() + && !NATIVE_UI_POPUP_CLASSES.contains(&window.class().as_str()); - // state no changed - if was_fullscreen.is_some() == is_fullscreen { - return None; - } - - if let Some(index) = was_fullscreen { - let data = fullscreened.remove(index); - Some(Self::SyntheticFullscreenEnd(data)) - } else { - let data = SyntheticFullscreenData { - handle: origin, - monitor: WindowsApi::monitor_from_window(origin), - }; - fullscreened.push(data); - Some(Self::SyntheticFullscreenStart(data)) + match *latest_fullscreened { + Some(latest) if latest.handle == origin => { + // exiting fullscreen + if !is_origin_fullscreen { + *latest_fullscreened = None; + synthetics.push(Self::SyntheticFullscreenEnd(latest)); + } + } + _ => { + // remove fullscreen of latest when foregrounding another window + if let Some(old) = latest_fullscreened.take() { + synthetics.push(Self::SyntheticFullscreenEnd(old)); + } + // if new foregrounded window is fullscreen emit it + if is_origin_fullscreen { + log::trace!("Fullscreened: {:?}", window); + let data = SyntheticFullscreenData { + handle: origin, + monitor: WindowsApi::monitor_from_window(origin), + }; + *latest_fullscreened = Some(data); + synthetics.push(Self::SyntheticFullscreenStart(data)); + } + } + } } } - _ => None, - } + _ => {} + }; + Ok(synthetics) } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 8bddd370..a1846586 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,12 +1,12 @@ -declare module '*.module.css' { - const classnames: Record; - export default classnames; -} - -declare module '*.yml' { - export default string; -} - -interface ObjectConstructor { - keys(o: T): (keyof T)[]; -} +declare module '*.module.css' { + const classnames: Record; + export default classnames; +} + +declare module '*.yml' { + export default string; +} + +interface ObjectConstructor { + keys(o: T): (keyof T)[]; +} diff --git a/src/shared.interfaces.ts b/src/shared.interfaces.ts index b9df4db3..d0a7904c 100644 --- a/src/shared.interfaces.ts +++ b/src/shared.interfaces.ts @@ -1,31 +1,23 @@ -import { Layout, LayoutSchema, NoFallbackBehavior } from './apps/shared/schemas/Layout'; -import { Placeholder } from './apps/shared/schemas/Placeholders'; -import { ISettings } from './apps/shared/schemas/Settings'; -import { Theme } from './apps/shared/schemas/Theme'; - -import { AppConfiguration } from './apps/settings/modules/appsConfigurations/domain'; - +import { + AppConfiguration, + Placeholder, + Settings, + Theme, + UIColors, + WindowManagerLayout, +} from 'seelen-core'; export interface IRootState { settings: T; + colors: UIColors; } export interface UserSettings { - jsonSettings: ISettings; + jsonSettings: Settings; yamlSettings: AppConfiguration[]; themes: Theme[]; - layouts: Layout[]; + layouts: WindowManagerLayout[]; placeholders: Placeholder[]; env: Record; /** wallpaper url */ wallpaper: string | null; } - -const _defaultLayout = LayoutSchema.parse({}); -export const defaultLayout: Layout = { - ..._defaultLayout, - info: { - ..._defaultLayout.info, - filename: 'unknown', - }, - noFallbackBehavior: NoFallbackBehavior.Unmanaged, -}; diff --git a/static/icons/folder.svg b/static/icons/folder.svg new file mode 100644 index 00000000..775ef4c1 --- /dev/null +++ b/static/icons/folder.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/static/layouts/Grid.json b/static/layouts/Grid.json index 5c3fc511..2491faae 100644 --- a/static/layouts/Grid.json +++ b/static/layouts/Grid.json @@ -1,74 +1,69 @@ -{ - "$schema": "https://raw.githubusercontent.com/eythaann/Seelen-UI/master/documentation/schemas/layout.schema.json", - "info": { - "displayName": "Grid", - "author": "eythaann", - "description": "Grid Layout useful for big monitors" - }, - "no_fallback_behavior": "Float", - "structure": { - "type": "Horizontal", - "children": [ - { - "type": "Vertical", - "priority": 3, - "condition": "total >= 5", - "children": [ - { - "type": "Leaf", - "condition": "total >= 8" - }, - { - "type": "Leaf" - }, - { - "type": "Leaf" - } - ] - }, - { - "type": "Leaf", - "priority": 1, - "condition": "total == 5" - }, - { - "type": "Vertical", - "priority": 1, - "condition": "total != 5", - "children": [ - { - "type": "Leaf", - "priority": 3, - "condition": "total >= 9" - }, - { - "type": "Leaf", - "priority": 1 - }, - { - "type": "Leaf", - "condition": "total == 4 or total >= 6", - "priority": 2 - } - ] - }, - { - "type": "Vertical", - "priority": 2, - "condition": "total > 1", - "children": [ - { - "type": "Leaf", - "condition": "total >= 7" - }, - { - "type": "Leaf" - }, - { - "type": "Leaf" - } - ] - } - ] - } +{ + "$schema": "https://raw.githubusercontent.com/eythaann/Seelen-UI/master/documentation/schemas/layout.schema.json", + "info": { + "displayName": "Grid", + "author": "eythaann", + "description": "Grid Layout useful for big monitors" + }, + "no_fallback_behavior": "Float", + "structure": { + "type": "Horizontal", + "children": [ + { + "type": "Vertical", + "priority": 3, + "condition": "managed >= 4", + "children": [ + { + "type": "Leaf", + "condition": "managed >= 7", + "priority": 3 + }, + { + "type": "Leaf" + }, + { + "type": "Leaf" + } + ] + }, + { + "type": "Vertical", + "priority": 1, + "children": [ + { + "type": "Leaf", + "priority": 3, + "condition": "if(is_reindexing, managed == 9, managed == 8)" + }, + { + "type": "Leaf", + "priority": 1 + }, + { + "type": "Leaf", + "condition": "if(is_reindexing, managed == 4 || managed >= 6, managed == 3 || managed >= 5)", + "priority": 2 + } + ] + }, + { + "type": "Vertical", + "priority": 2, + "children": [ + { + "type": "Leaf", + "condition": "if(is_reindexing, managed >= 7, managed >= 6)", + "priority": 3 + }, + { + "type": "Leaf" + }, + { + "type": "Leaf" + } + ] + } + ] + } } \ No newline at end of file diff --git a/static/themes/default/theme.launcher.css b/static/themes/default/theme.launcher.css new file mode 100644 index 00000000..c156fab1 --- /dev/null +++ b/static/themes/default/theme.launcher.css @@ -0,0 +1,112 @@ +body { + overflow: hidden; + background: transparent; + width: 100vw; + height: 100vh; +} + +:root { + --accent-color-by-scheme: var(--config-accent-dark-color); +} + +@media (prefers-color-scheme: dark) { + :root { + --accent-color-by-scheme: var(--config-accent-light-color); + } +} + +#root { + position: absolute; + top: 50%; + left: 50%; + translate: -50% -50%; + height: 50lvh; + aspect-ratio: 1 / 1; +} + +.launcher { + --padding: 14px; + width: 100%; + height: 100%; + background-color: var(--color-gray-50); + border-radius: 20px; + box-shadow: 0 4px 10px 4px #0001; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.launcher-header { + display: flex; + align-items: center; + padding: var(--padding); + gap: 10px; + border-bottom: 1px solid var(--color-gray-300); + + .launcher-header-runner-selector { + width: 120px; + } + + .launcher-header-command-input { + flex: 1; + } +} + +.launcher-body { + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow: auto; + height: 100%; +} + +.launcher-footer { + display: flex; + align-items: center; + gap: 10px; + padding: var(--padding); + border-top: 1px solid var(--color-gray-300); + justify-content: flex-end; +} + +.launcher-item { + width: 100%; + display: flex; + align-items: center; + gap: 20px; + padding: 8px var(--padding); + position: relative; + + .launcher-item-icon { + width: 40px; + height: 40px; + object-fit: contain; + } + + .launcher-item-label { + font-weight: 600; + } + + .launcher-item-path { + color: var(--color-gray-500); + font-size: 0.8rem; + font-weight: 500; + } + + &:nth-child(2n) { + background-color: var(--color-gray-100); + } + + &:hover { + background-color: var(--color-gray-200); + } + + &:focus, + &:focus-visible { + outline: 2px solid rgba(var(--config-accent-dark-color-rgb), 0.8); + z-index: 2; + } + + &:active { + background-color: var(--color-gray-300); + } +} diff --git a/static/themes/default/theme.toolbar.css b/static/themes/default/theme.toolbar.css index 3ab112eb..c8aaa2d5 100644 --- a/static/themes/default/theme.toolbar.css +++ b/static/themes/default/theme.toolbar.css @@ -65,7 +65,7 @@ } .ft-bar-item-clickable { - padding: 4px; + padding: 1px 4px; border-radius: 6px; &:hover { @@ -312,6 +312,7 @@ aspect-ratio: 1/1; right: 5%; bottom: 5%; + object-fit: contain; } .media-session-thumbnail { @@ -449,3 +450,15 @@ font-weight: 600; } } + +.ft-bar-item-context-menu-container { + .bg-layer-1 { + background-color: var(--color-gray-100); + border-radius: 10px; + } + + .ft-bar-item-context-menu { + .ft-bar-item-context-menu-item { + } + } +} diff --git a/static/themes/default/theme.wall.css b/static/themes/default/theme.wall.css new file mode 100644 index 00000000..fcb703d1 --- /dev/null +++ b/static/themes/default/theme.wall.css @@ -0,0 +1,26 @@ +.wallpaper-empty { + width: 100%; + height: 100%; + background: linear-gradient( + to bottom, + var(--config-accent-darkest-color), + var(--config-accent-color), + var(--config-accent-darkest-color) + ); + + &::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient( + to bottom, + var(--config-accent-dark-color), + var(--config-accent-lightest-color) + ); + mask-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' id='svg' viewBox='0 0 1440 790' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 0,800 C 0,800 0,160 0,160 C 104.43062200956939,139.54066985645932 208.86124401913878,119.08133971291866 297,128 C 385.1387559808612,136.91866028708134 456.9856459330142,175.2153110047847 540,178 C 623.0143540669858,180.7846889952153 717.1961722488038,148.05741626794256 816,150 C 914.8038277511962,151.94258373205744 1018.2296650717703,188.555023923445 1123,196 C 1227.7703349282297,203.444976076555 1333.8851674641148,181.7224880382775 1440,160 C 1440,160 1440,800 1440,800 Z' stroke='none' stroke-width='0' fill='%23FFFFFF' fill-opacity='0.265' class='path-0' style='transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 300ms; transition-delay: 150ms;'%3E%3C/path%3E%3Cpath d='M 0,800 C 0,800 0,320 0,320 C 120.88995215311004,343.9138755980861 241.7799043062201,367.82775119617224 335,373 C 428.2200956937799,378.17224880382776 493.77033492822966,364.60287081339715 576,343 C 658.2296650717703,321.39712918660285 757.1387559808612,291.76076555023917 869,287 C 980.8612440191388,282.23923444976083 1105.6746411483252,302.354066985646 1203,312 C 1300.3253588516748,321.645933014354 1370.1626794258373,320.822966507177 1440,320 C 1440,320 1440,800 1440,800 Z' stroke='none' stroke-width='0' fill='%23FFFFFF' fill-opacity='0.4' class='path-1' style='transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 300ms; transition-delay: 150ms;'%3E%3C/path%3E%3Cpath d='M 0,800 C 0,800 0,480 0,480 C 116.71770334928229,477.07177033492826 233.43540669856458,474.14354066985646 330,462 C 426.5645933014354,449.85645933014354 502.97607655502395,428.49760765550246 593,421 C 683.023923444976,413.50239234449754 786.6602870813399,419.8660287081339 891,432 C 995.3397129186601,444.1339712918661 1100.382775119617,462.0382775119617 1192,471 C 1283.617224880383,479.9617224880383 1361.8086124401916,479.98086124401914 1440,480 C 1440,480 1440,800 1440,800 Z' stroke='none' stroke-width='0' fill='%23FFFFFF' fill-opacity='0.53' class='path-2' style='transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 300ms; transition-delay: 150ms;'%3E%3C/path%3E%3Cpath d='M 0,800 C 0,800 0,640 0,640 C 117.33014354066987,611.6746411483253 234.66028708133973,583.3492822966507 339,587 C 443.33971291866027,590.6507177033493 534.688995215311,626.2775119617224 621,629 C 707.311004784689,631.7224880382776 788.5837320574162,601.5406698564593 883,614 C 977.4162679425838,626.4593301435407 1084.9760765550238,681.5598086124402 1180,693 C 1275.0239234449762,704.4401913875598 1357.5119617224882,672.22009569378 1440,640 C 1440,640 1440,800 1440,800 Z' stroke='none' stroke-width='0' fill='%23FFFFFF' fill-opacity='1' class='path-3' style='transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 300ms; transition-delay: 150ms;'%3E%3C/path%3E%3C/svg%3E"); + mask-repeat: no-repeat; + mask-position: bottom; + mask-size: contain; + } +} diff --git a/static/themes/default/theme.weg.css b/static/themes/default/theme.weg.css index f24b38cf..bf5295f4 100644 --- a/static/themes/default/theme.weg.css +++ b/static/themes/default/theme.weg.css @@ -1,154 +1,267 @@ -.taskbar { - .taskbar-bg-layer-1 { - opacity: 0.3; - filter: saturate(0); - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='10' numOctaves='3' stitchTiles='stitch '/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); - background-size: cover; - border-radius: 15px; - } - - .taskbar-bg-layer-2 { - opacity: 0.8; - background-color: var(--color-gray-100); - border-radius: 15px; - } -} - -.weg-separator { - .horizontal & { - &.weg-separator-1 { - border-left: 1px solid var(--color-gray-400); - } - &.weg-separator-2 { - border-right: 1px solid var(--color-gray-400); - } - } - - .vertical & { - &.weg-separator-1 { - border-top: 1px solid var(--color-gray-400); - } - &.weg-separator-2 { - border-bottom: 1px solid var(--color-gray-400); - } - } -} - -.weg-item { - .item-bg-layer-1 { - background-color: var(--color-gray-100); - border-radius: 25%; - box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5); - transition: background-color 0.2s ease-in-out; - } - - &:hover { - .item-bg-layer-1 { - background-color: var(--color-gray-400); - } - } - - &:active { - filter: brightness(0.4); - } - - &:not(:active) { - transition-property: filter; - transition-duration: 0.2s; - transition-timing-function: ease-in-out; - } -} - -.weg-item-icon { - width: 65%; - height: 65%; - filter: drop-shadow(0px 0px 1px #0000009a); - object-fit: contain; -} - -.weg-item-icon-start { - width: 100%; - height: 100%; - filter: brightness(1.2); - background: linear-gradient(150deg, var(--config-accent-color) 10%, #000 150%); - mask-image: url('data:image/svg+xml;charset=utf-8,'); - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; -} - -.weg-item-open-sign { - transition-property: width, height, opacity, background-color, border-radius; - transition-duration: 0.2s; - transition-timing-function: linear; - border-radius: 4px; - - &.weg-item-open-sign-active { - --empty-rule: "delete me on use"; - } - - &.weg-item-open-sign-focused { - .vertical & { - height: 50%; - } - - .horizontal & { - width: 50%; - } - } -} - -.weg-context-menu-container { - padding: 6px; - - .menu-bg-layer-1 { - background-color: var(--color-gray-100); - border-radius: 10px; - } - - .weg-context-menu { - --empty-rule: "delete me on use"; - } -} - -.weg-item-preview-container { - padding: 10px; - border-radius: 10px; - - .preview-bg-layer-1 { - background-color: var(--color-gray-100); - border-radius: 10px; - } -} - -.weg-item-preview { - padding: 6px 10px 10px 10px; - border-radius: 10px; -} - -.weg-item-preview-topbar { - margin: 0 0 8px 0; -} - -.weg-item-preview-title { - font-size: 14px; - font-weight: 600; - color: var(--color-gray-900); -} - -.weg-item-preview-close { - --empty-rule: "delete me on use"; -} - -.weg-item-preview-image-container { - border-radius: 10px; - border: 1px solid var(--color-gray-300); -} - -.weg-item-preview-image { - --empty-rule: "delete me on use"; -} - -.weg-item-preview-spin { - --empty-rule: "delete me on use"; -} +.taskbar { + .taskbar-bg-layer-1 { + opacity: 0.3; + filter: saturate(0); + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='10' numOctaves='3' stitchTiles='stitch '/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); + background-size: cover; + border-radius: 15px; + } + + .taskbar-bg-layer-2 { + opacity: 0.8; + background-color: var(--color-gray-100); + border-radius: 15px; + } +} + +.weg-separator { + .horizontal & { + &.weg-separator-1 { + border-left: 1px solid var(--color-gray-400); + } + &.weg-separator-2 { + border-right: 1px solid var(--color-gray-400); + } + } + + .vertical & { + &.weg-separator-1 { + border-top: 1px solid var(--color-gray-400); + } + &.weg-separator-2 { + border-bottom: 1px solid var(--color-gray-400); + } + } +} + +/* This will act like a hitbox for items */ +.weg-item::before { + content: ""; + position: absolute; + /* border: solid 1px red; */ +} + +.vertical .weg-item::before { + top: 50%; + transform: translateY(-50%); + width: calc(100% + var(--config-padding) + var(--config-margin)); + height: calc(100% + var(--config-space-between-items)); +} + +.horizontal .weg-item::before { + left: 50%; + transform: translateX(-50%); + width: calc(100% + var(--config-space-between-items)); + height: calc(100% + var(--config-padding) + var(--config-margin)); +} + +.bottom .weg-item::before { + top: 0; +} + +.top .weg-item::before { + bottom: 0; +} + +.left .weg-item::before { + right: 0; +} + +.right .weg-item::before { + left: 0; +} + +.weg-item-drag-container:not(.dragging) { + --item-size-diff: calc(var(--config-item-zoom-size) - var(--config-item-size)); + + &:has(+ .weg-item-drag-container + .weg-item-drag-container:hover) .weg-item { + width: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.7)); + height: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.7)); + } + + &:has(+ .weg-item-drag-container:hover) .weg-item { + width: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.3)); + height: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.3)); + } + + &:hover { + .weg-item { + width: var(--config-item-zoom-size); + height: var(--config-item-zoom-size); + } + + + .weg-item-drag-container > .weg-item { + width: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.3)); + height: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.3)); + } + + + .weg-item-drag-container + .weg-item-drag-container .weg-item { + width: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.7)); + height: calc(var(--config-item-zoom-size) - (var(--item-size-diff) * 0.7)); + } + } +} + +.weg-item { + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: var(--config-item-size); + height: var(--config-item-size); + transition: width, height, 100ms cubic-bezier(0.25, 1, 0.5, 1); + + .bg-layer-1 { + background-color: var(--color-gray-100); + border-radius: 25%; + box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5); + } + + &:active { + .bg-layer-1 { + filter: brightness(0.8); + } + + .weg-item-icon, + .weg-item-icon-start { + transform: scale(0.8); + } + } + + &:not(:active) { + .bg-layer-1 { + transition: filter 0.2s linear; + } + + .weg-item-icon, + .weg-item-icon-start { + transition: transform 0.2s linear; + } + } +} + +.weg-item-icon { + width: 65%; + height: 65%; + filter: drop-shadow(0px 0px 1px #0000009a); + object-fit: contain; + fill: var(--config-accent-lighter-color); +} + +.weg-item-icon-start { + width: 100%; + height: 100%; + filter: brightness(1.2); + background: linear-gradient(150deg, var(--config-accent-color) 10%, #000 150%); + mask-image: url('data:image/svg+xml;charset=utf-8,'); + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; +} + +.weg-item-open-sign { + position: absolute; + width: 3px; + height: 3px; + border-radius: 6px; + background-color: var(--color-gray-600); + opacity: 0; + transition-property: width, height, transform, opacity, background-color, border-radius; + transition-duration: 0.2s; + transition-timing-function: linear; + + .vertical & { + transform: translateX(-50%); + } + + .horizontal & { + transform: translateY(-50%); + } + + .top & { + bottom: calc(100% + var(--config-padding) / 2); + } + + .bottom & { + top: calc(100% + var(--config-padding) / 2); + } + + .left & { + right: calc(100% + var(--config-padding) / 2); + } + + .right & { + left: calc(100% + var(--config-padding) / 2); + } + + &.weg-item-open-sign-active { + opacity: 1; + } + + &.weg-item-open-sign-focused { + background-color: var(--config-accent-color); + + .vertical & { + height: 50%; + } + + .horizontal & { + width: 50%; + } + } +} + +.weg-context-menu-container { + padding: 3px; + + .menu-bg-layer-1 { + background-color: var(--color-gray-100); + border-radius: 10px; + } + + .weg-context-menu { + --empty-rule: "delete me on use"; + } +} + +.weg-item-preview-container { + padding: 10px; + border-radius: 10px; + + .preview-bg-layer-1 { + background-color: var(--color-gray-100); + border-radius: 10px; + } +} + +.weg-item-preview { + padding: 6px 10px 10px 10px; + border-radius: 10px; +} + +.weg-item-preview-topbar { + margin: 0 0 8px 0; +} + +.weg-item-preview-title { + font-size: 14px; + font-weight: 600; + color: var(--color-gray-900); +} + +.weg-item-preview-close { + --empty-rule: "delete me on use"; +} + +.weg-item-preview-image-container { + border-radius: 10px; + border: 1px solid var(--color-gray-300); +} + +.weg-item-preview-image { + --empty-rule: "delete me on use"; +} + +.weg-item-preview-spin { + --empty-rule: "delete me on use"; +} diff --git a/static/themes/default/theme.wm.css b/static/themes/default/theme.wm.css index 6c7c8dca..6bec3546 100644 --- a/static/themes/default/theme.wm.css +++ b/static/themes/default/theme.wm.css @@ -1,28 +1,28 @@ -.wm-leaf { - &.wm-leaf-with-borders { - border-style: solid; - border-width: var(--config-border-width); - border-color: var(--config-accent-dark-color); - - &.wm-leaf-focused { - border-color: var(--config-accent-lighter-color); - } - } -} - -.wm-stack { - .wm-stack-bar { - background-color: #222222; - color: #fefefe; - - &.wm-stack-bar-with-borders { - border-style: solid; - border-width: var(--config-border-width); - border-color: var(--config-accent-dark-color); - } - } -} - -.wm-reserved { - background-color: var(--color-accent-lighter-color); -} +.wm-leaf { + &.wm-leaf-with-borders { + border-style: solid; + border-width: var(--config-border-width); + border-color: var(--config-accent-light-color); + + &.wm-leaf-focused { + border-color: var(--config-accent-lighter-color); + } + } +} + +.wm-stack { + .wm-stack-bar { + background-color: #222222; + color: #fefefe; + + &.wm-stack-bar-with-borders { + border-style: solid; + border-width: var(--config-border-width); + border-color: var(--config-accent-dark-color); + } + } +} + +.wm-reserved { + background-color: var(--color-accent-lighter-color); +} diff --git a/tauri.conf.json b/tauri.conf.json index ec095afd..cf275858 100644 --- a/tauri.conf.json +++ b/tauri.conf.json @@ -1,5 +1,5 @@ { - "$schema": "node_modules/@tauri-apps/cli/schema.json", + "$schema": "node_modules/@tauri-apps/cli/config.schema.json", "productName": "Seelen UI", "identifier": "com.seelen.seelen-ui", "version": "package.json", @@ -14,25 +14,25 @@ "**/*.jpg", "**/*.jpeg", "**/*.png", + "**/*.webp", "**/*.gif", - "**/*.bmp", - "**/*.tif", - "**/*.tiff" + "**/*.mp4", + "**/*.mkv", + "**/*.wav" ] } } }, "build": { - "beforeBuildCommand": "npm run build:ui", + "beforeBuildCommand": "npm run build:ui -- --production", "frontendDist": "dist", "features": [] }, "bundle": { "active": true, - "createUpdaterArtifacts": "v1Compatible", - "resources": [ - "static/**/*" - ], + "resources": { + "static/": "static/" + }, "targets": [ "nsis" ], @@ -50,16 +50,21 @@ "startMenuFolder": "Seelen", "displayLanguageSelector": true, "languages": [ - "English", - "German", - "Spanish", - "French", - "Hindi", - "Portuguese", - "Japanese", - "Korean", - "SimpChinese", - "TradChinese" + "arabic", + "bulgarian", + "dutch", + "english", + "german", + "japanese", + "korean", + "portuguesebr", + "russian", + "tradchinese", + "simpchinese", + "french", + "spanish", + "turkish", + "swedish" ] } }, diff --git a/templates/installer.nsi b/templates/installer.nsi index 1f29d2fe..ff75e708 100644 --- a/templates/installer.nsi +++ b/templates/installer.nsi @@ -1,880 +1,881 @@ -Unicode true -ManifestDPIAware true -; Add in `dpiAwareness` `PerMonitorV2` to manifest for Windows 10 1607+ (note this should not affect lower versions since they should be able to ignore this and pick up `dpiAware` `true` set by `ManifestDPIAware true`) -; Currently undocumented on NSIS's website but is in the Docs folder of source tree, see -; https://github.com/kichik/nsis/blob/5fc0b87b819a9eec006df4967d08e522ddd651c9/Docs/src/attributes.but#L286-L300 -; https://github.com/tauri-apps/tauri/pull/10106 -ManifestDPIAwareness PerMonitorV2 - -!if "{{compression}}" == "none" - SetCompress off -!else - ; Set the compression algorithm. We default to LZMA. - SetCompressor /SOLID "{{compression}}" -!endif - -!include MUI2.nsh -!include FileFunc.nsh -!include x64.nsh -!include WordFunc.nsh -!include "utils.nsh" -!include "FileAssociation.nsh" -!include "Win\COM.nsh" -!include "Win\Propkey.nsh" -!include "StrFunc.nsh" -${StrCase} -${StrLoc} - -{{#if installer_hooks}} -!include "{{installer_hooks}}" -{{/if}} - -!define MANUFACTURER "{{manufacturer}}" -!define PRODUCTNAME "{{product_name}}" -!define VERSION "{{version}}" -!define VERSIONWITHBUILD "{{version_with_build}}" -!define SHORTDESCRIPTION "{{short_description}}" -!define HOMEPAGE "{{homepage}}" -!define INSTALLMODE "{{install_mode}}" -!define LICENSE "{{license}}" -!define INSTALLERICON "{{installer_icon}}" -!define SIDEBARIMAGE "{{sidebar_image}}" -!define HEADERIMAGE "{{header_image}}" -!define MAINBINARYNAME "{{main_binary_name}}" -!define MAINBINARYSRCPATH "{{main_binary_path}}" -!define BUNDLEID "{{bundle_id}}" -!define COPYRIGHT "{{copyright}}" -!define OUTFILE "{{out_file}}" -!define ARCH "{{arch}}" -!define PLUGINSPATH "{{additional_plugins_path}}" -!define ALLOWDOWNGRADES "{{allow_downgrades}}" -!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}" -!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}" -!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}" -!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}" -!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}" -!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}" -!define APPPATHKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAINBINARYNAME}.exe" -!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}" -!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}" -!define ESTIMATEDSIZE "{{estimated_size}}" -!define STARTMENUFOLDER "{{start_menu_folder}}" - -Var PassiveMode -Var UpdateMode -Var NoShortcutMode - -Name "${PRODUCTNAME}" -BrandingText "${COPYRIGHT}" -OutFile "${OUTFILE}" - -; We don't actually use this value as default install path, -; it's just for nsis to append the product name folder in the directory selector -; https://nsis.sourceforge.io/Reference/InstallDir -!define PLACEHOLDER_INSTALL_DIR "placeholder\${MANUFACTURER}\${PRODUCTNAME}" -InstallDir "${PLACEHOLDER_INSTALL_DIR}" - -VIProductVersion "${VERSIONWITHBUILD}" -VIAddVersionKey "ProductName" "${PRODUCTNAME}" -VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}" -VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" -VIAddVersionKey "FileVersion" "${VERSION}" -VIAddVersionKey "ProductVersion" "${VERSION}" - -; Plugins path, currently exists for linux only -!if "${PLUGINSPATH}" != "" - !addplugindir "${PLUGINSPATH}" -!endif - -; Uninstaller signing command -!if "${UNINSTALLERSIGNCOMMAND}" != "" - !uninstfinalize '${UNINSTALLERSIGNCOMMAND}' -!endif - -; Handle install mode, `perUser`, `perMachine` or `both` -!if "${INSTALLMODE}" == "perMachine" - RequestExecutionLevel highest -!endif - -!if "${INSTALLMODE}" == "currentUser" - RequestExecutionLevel user -!endif - -!if "${INSTALLMODE}" == "both" - !define MULTIUSER_MUI - !define MULTIUSER_INSTALLMODE_INSTDIR "${MANUFACTURER}\${PRODUCTNAME}" - !define MULTIUSER_INSTALLMODE_COMMANDLINE - !if "${ARCH}" == "x64" - !define MULTIUSER_USE_PROGRAMFILES64 - !else if "${ARCH}" == "arm64" - !define MULTIUSER_USE_PROGRAMFILES64 - !endif - !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}" - !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser" - !define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME - !define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation - !define MULTIUSER_EXECUTIONLEVEL Highest - !include MultiUser.nsh -!endif - -; Installer & Unistaller icon -!if "${INSTALLERICON}" != "" - !define MUI_ICON "${INSTALLERICON}" - !define MUI_UNICON "${INSTALLERICON}" -!endif - -; Installer sidebar image -!if "${SIDEBARIMAGE}" != "" - !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" -!endif - -; Installer header image -!if "${HEADERIMAGE}" != "" - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" -!endif - -; Define registry key to store installer language -!define MUI_LANGDLL_REGISTRY_ROOT "HKCU" -!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}" -!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" - -; =============================================================================================== -; ====================================== INSTALLER PAGES ======================================== -; =============================================================================================== -!define MUI_BGCOLOR 222228 -!define MUI_TEXTCOLOR fdfdfd -!define MUI_FINISHPAGE_TEXT_COLOR fdfdfd - -; 1. Welcome Page -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive -!insertmacro MUI_PAGE_WELCOME - -; 2. License Page (if defined) -!if "${LICENSE}" != "" - !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive - !insertmacro MUI_PAGE_LICENSE "${LICENSE}" -!endif - -; 3. Install mode (if it is set to `both`) -!if "${INSTALLMODE}" == "both" - !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive - !insertmacro MULTIUSER_PAGE_INSTALLMODE -!endif - -; 4. Custom page to ask user if he wants to reinstall/uninstall -; only if a previous installation was detected -Var ReinstallPageCheck -Page custom PageReinstall PageLeaveReinstall -Function PageReinstall - ; Uninstall previous WiX installation if exists. - ; - ; A WiX installer stores the installation info in registry - ; using a UUID and so we have to loop through all keys under - ; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` - ; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER} - ; - ; This has a potential issue that there maybe another installation that matches - ; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer, - ; however, this should be fine since the user will have to confirm the uninstallation - ; and they can chose to abort it if doesn't make sense. - StrCpy $0 0 - wix_loop: - EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0 - StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on - IntOp $0 $0 + 1 - ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName" - ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher" - StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop - ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString" - ${StrCase} $R1 $R0 "L" - ${StrLoc} $R0 $R1 "msiexec" ">" - StrCmp $R0 0 0 wix_done - StrCpy $R7 "wix" - StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" - Goto compare_version - wix_done: - - ; Check if there is an existing installation, if not, abort the reinstall page - ReadRegStr $R0 SHCTX "${UNINSTKEY}" "" - ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" - ${IfThen} "$R0$R1" == "" ${|} Abort ${|} - - ; Compare this installar version with the existing installation - ; and modify the messages presented to the user accordingly - compare_version: - StrCpy $R4 "$(older)" - ${If} $R7 == "wix" - ReadRegStr $R0 HKLM "$R6" "DisplayVersion" - ${Else} - ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion" - ${EndIf} - ${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|} - - nsis_tauri_utils::SemverCompare "${VERSION}" $R0 - Pop $R0 - ; Reinstalling the same version - ${If} $R0 = 0 - StrCpy $R1 "$(alreadyInstalledLong)" - StrCpy $R2 "$(addOrReinstall)" - StrCpy $R3 "$(uninstallApp)" - !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)" - StrCpy $R5 "2" - ; Upgrading - ${ElseIf} $R0 = 1 - StrCpy $R1 "$(olderOrUnknownVersionInstalled)" - StrCpy $R2 "$(uninstallBeforeInstalling)" - StrCpy $R3 "$(dontUninstall)" - !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" - StrCpy $R5 "1" - ; Downgrading - ${ElseIf} $R0 = -1 - StrCpy $R1 "$(newerVersionInstalled)" - StrCpy $R2 "$(uninstallBeforeInstalling)" - !if "${ALLOWDOWNGRADES}" == "true" - StrCpy $R3 "$(dontUninstall)" - !else - StrCpy $R3 "$(dontUninstallDowngrade)" - !endif - !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" - StrCpy $R5 "1" - ${Else} - Abort - ${EndIf} - - ; Skip showing the page if passive - ; - ; Note that we don't call this earlier at the begining - ; of this function because we need to populate some variables - ; related to current installed version if detected and whether - ; we are downgrading or not. - Call SkipIfPassive - - nsDialogs::Create 1018 - Pop $R4 - ${IfThen} $(^RTL) = 1 ${|} nsDialogs::SetRTL $(^RTL) ${|} - - ${NSD_CreateLabel} 0 0 100% 24u $R1 - Pop $R1 - - ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2 - Pop $R2 - ${NSD_OnClick} $R2 PageReinstallUpdateSelection - - ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 - Pop $R3 - ; Disable this radio button if downgrading and downgrades are disabled - !if "${ALLOWDOWNGRADES}" == "false" - ${IfThen} $R0 = -1 ${|} EnableWindow $R3 0 ${|} - !endif - ${NSD_OnClick} $R3 PageReinstallUpdateSelection - - ; Check the first radio button if this the first time - ; we enter this page or if the second button wasn't - ; selected the last time we were on this page - ${If} $ReinstallPageCheck <> 2 - SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0 - ${Else} - SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0 - ${EndIf} - - ${NSD_SetFocus} $R2 - nsDialogs::Show -FunctionEnd -Function PageReinstallUpdateSelection - ${NSD_GetState} $R2 $R1 - ${If} $R1 == ${BST_CHECKED} - StrCpy $ReinstallPageCheck 1 - ${Else} - StrCpy $ReinstallPageCheck 2 - ${EndIf} -FunctionEnd -Function PageLeaveReinstall - ${NSD_GetState} $R2 $R1 - - ; $R5 holds whether we are reinstalling the same version or not - ; $R5 == "1" -> different versions - ; $R5 == "2" -> same version - ; - ; $R1 holds the radio buttons state. its meaning is dependent on the context - StrCmp $R5 "1" 0 +2 ; Existing install is not the same version? - StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling - StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling - - reinst_uninstall: - HideWindow - ClearErrors - - ${If} $R7 == "wix" - ReadRegStr $R1 HKLM "$R6" "UninstallString" - ExecWait '$R1' $0 - ${Else} - ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" - ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" - ${If} $UpdateMode = 1 - ExecWait '$R1 /UPDATE /P _?=$4' $0 - ${Else} - ExecWait '$R1 /P _?=$4' $0 - ${EndIf} - ${EndIf} - - BringToFront - - ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code - - ${If} $0 <> 0 - ${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe" - ${If} $0 = 1 ; User aborted uninstaller? - StrCmp $R5 "2" 0 +2 ; Is the existing install the same version? - Quit ; ...yes, already installed, we are done - Abort - ${EndIf} - MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)" - Abort - ${Else} - StrCpy $0 $R1 1 - ${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString - Delete $R1 - RMDir $INSTDIR - ${EndIf} - reinst_done: -FunctionEnd - -; 5. Choose install directory page -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive -!insertmacro MUI_PAGE_DIRECTORY - -; 6. Start menu shortcut page -Var AppStartMenuFolder -!if "${STARTMENUFOLDER}" != "" - !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive - !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${STARTMENUFOLDER}" -!else - !define MUI_PAGE_CUSTOMFUNCTION_PRE Skip -!endif -!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder - -; 7. Installation page -!insertmacro MUI_PAGE_INSTFILES - -; 8. Finish page -; -; Don't auto jump to finish page after installation page, -; because the installation page has useful info that can be used debug any issues with the installer. -!define MUI_FINISHPAGE_NOAUTOCLOSE -; Show sponsor link -!define MUI_FINISHPAGE_LINK_COLOR 59a7f6 -!define MUI_FINISHPAGE_LINK "Join us on Discord! 🤍" -!define MUI_FINISHPAGE_LINK_LOCATION "https://discord.gg/ABfASx5ZAJ" - -Function RunMainBinary - Exec '"$INSTDIR\${MAINBINARYNAME}.exe"' -FunctionEnd - -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive -!define MUI_PAGE_CUSTOMFUNCTION_LEAVE RunMainBinary -!insertmacro MUI_PAGE_FINISH - -; Uninstaller Pages -; 1. Confirm uninstall page -Var DeleteAppDataCheckbox -Var DeleteAppDataCheckboxState -!define /ifndef WS_EX_LAYOUTRTL 0x00400000 -!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow -Function un.ConfirmShow ; Add add a `Delete app data` check box - ; $1 inner dialog HWND - ; $2 window DPI - ; $3 style - ; $4 x - ; $5 y - ; $6 width - ; $7 height - FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog - System::Call "user32::GetDpiForWindow(p r1) i .r2" - ${If} $(^RTL) = 1 - StrCpy $3 "${__NSD_CheckBox_EXSTYLE} | ${WS_EX_LAYOUTRTL}" - IntOp $4 50 * $2 - ${Else} - StrCpy $3 "${__NSD_CheckBox_EXSTYLE}" - IntOp $4 0 * $2 - ${EndIf} - IntOp $5 100 * $2 - IntOp $6 400 * $2 - IntOp $7 25 * $2 - IntOp $4 $4 / 96 - IntOp $5 $5 / 96 - IntOp $6 $6 / 96 - IntOp $7 $7 / 96 - System::Call 'user32::CreateWindowEx(i r3, w "${__NSD_CheckBox_CLASS}", w "$(deleteAppData)", i ${__NSD_CheckBox_STYLE}, i r4, i r5, i r6, i r7, p r1, i0, i0, i0) i .s' - Pop $DeleteAppDataCheckbox - SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1 - SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1 -FunctionEnd -!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave -Function un.ConfirmLeave - SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState -FunctionEnd -!insertmacro MUI_UNPAGE_CONFIRM - -; 2. Uninstalling Page -!insertmacro MUI_UNPAGE_INSTFILES - -;Languages -{{#each languages}} -!insertmacro MUI_LANGUAGE "{{this}}" -{{/each}} -!insertmacro MUI_RESERVEFILE_LANGDLL -{{#each language_files}} - !include "{{this}}" -{{/each}} - -Function .onInit - ${GetOptions} $CMDLINE "/P" $PassiveMode - ${IfNot} ${Errors} - StrCpy $PassiveMode 1 - ${EndIf} - - ${GetOptions} $CMDLINE "/NS" $NoShortcutMode - ${IfNot} ${Errors} - StrCpy $NoShortcutMode 1 - ${EndIf} - - ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode - ${IfNot} ${Errors} - StrCpy $UpdateMode 1 - ${EndIf} - - !if "${DISPLAYLANGUAGESELECTOR}" == "true" - !insertmacro MUI_LANGDLL_DISPLAY - !endif - - !insertmacro SetContext - - ${If} $INSTDIR == "${PLACEHOLDER_INSTALL_DIR}" - ; Set default install location - !if "${INSTALLMODE}" == "perMachine" - ${If} ${RunningX64} - !if "${ARCH}" == "x64" - StrCpy $INSTDIR "$PROGRAMFILES64\${MANUFACTURER}\${PRODUCTNAME}" - !else if "${ARCH}" == "arm64" - StrCpy $INSTDIR "$PROGRAMFILES64\${MANUFACTURER}\${PRODUCTNAME}" - !else - StrCpy $INSTDIR "$PROGRAMFILES\${MANUFACTURER}\${PRODUCTNAME}" - !endif - ${Else} - StrCpy $INSTDIR "$PROGRAMFILES\${MANUFACTURER}\${PRODUCTNAME}" - ${EndIf} - !else if "${INSTALLMODE}" == "currentUser" - StrCpy $INSTDIR "$LOCALAPPDATA\${MANUFACTURER}\${PRODUCTNAME}" - !endif - - Call RestorePreviousInstallLocation - ${EndIf} - - - !if "${INSTALLMODE}" == "both" - !insertmacro MULTIUSER_INIT - !endif -FunctionEnd - - -Section EarlyChecks - ; Abort silent installer if downgrades is disabled - !if "${ALLOWDOWNGRADES}" == "false" - ${If} ${Silent} - ; If downgrading - ${If} $R0 = -1 - System::Call 'kernel32::AttachConsole(i -1)i.r0' - ${If} $0 <> 0 - System::Call 'kernel32::GetStdHandle(i -11)i.r0' - System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color - FileWrite $0 "$(silentDowngrades)" - ${EndIf} - Abort - ${EndIf} - ${EndIf} - !endif - -SectionEnd - -Section WebView2 - ; Check if Webview2 is already installed and skip this section - ${If} ${RunningX64} - ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" - ${Else} - ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" - ${EndIf} - ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" - - StrCmp $4 "" 0 webview2_done - StrCmp $5 "" 0 webview2_done - - ; Webview2 installation - ; - ; Skip if updating - ${If} $UpdateMode <> 1 - !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" - Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" - DetailPrint "$(webview2Downloading)" - NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Pop $0 - ${If} $0 = 0 - DetailPrint "$(webview2DownloadSuccess)" - ${Else} - DetailPrint "$(webview2DownloadError)" - Abort "$(webview2AbortError)" - ${EndIf} - StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Goto install_webview2 - !endif - - !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" - Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" - File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" - DetailPrint "$(installingWebview2)" - StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Goto install_webview2 - !endif - - !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" - Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" - File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" - DetailPrint "$(installingWebview2)" - StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" - Goto install_webview2 - !endif - - Goto webview2_done - - install_webview2: - DetailPrint "$(installingWebview2)" - ; $6 holds the path to the webview2 installer - ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 - ${If} $1 = 0 - DetailPrint "$(webview2InstallSuccess)" - ${Else} - DetailPrint "$(webview2InstallError)" - Abort "$(webview2AbortError)" - ${EndIf} - webview2_done: - ${EndIf} -SectionEnd - -Section Install - SetOutPath $INSTDIR - - !ifmacrodef NSIS_HOOK_PREINSTALL - !insertmacro NSIS_HOOK_PREINSTALL - !endif - - !insertmacro CheckIfAppIsRunning - - ; Copy main executable - File "${MAINBINARYSRCPATH}" - - ; Copy resources - {{#each resources_dirs}} - CreateDirectory "$INSTDIR\\{{this}}" - {{/each}} - {{#each resources}} - File /a "/oname={{this.[1]}}" "{{@key}}" - {{/each}} - - ; Copy external binaries - {{#each binaries}} - File /a "/oname={{this}}" "{{@key}}" - {{/each}} - - ; Create file associations - {{#each file_associations as |association| ~}} - {{#each association.ext as |ext| ~}} - !insertmacro APP_ASSOCIATE "{{ext}}" "{{or association.name ext}}" "{{association-description association.description ext}}" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0" "Open with ${PRODUCTNAME}" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" - {{/each}} - {{/each}} - - ; Register deep links - {{#each deep_link_protocols as |protocol| ~}} - WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "URL Protocol" "" - WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "" "URL:${BUNDLEID} protocol" - WriteRegStr SHCTX "Software\Classes\\{{protocol}}\DefaultIcon" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0" - WriteRegStr SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" - {{/each}} - - ; Refresh file associations icons - !insertmacro UPDATEFILEASSOC - - ; Create uninstaller - WriteUninstaller "$INSTDIR\uninstall.exe" - - ; Save $INSTDIR in registry for future installations - WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR - - !if "${INSTALLMODE}" == "both" - ; Save install mode to be selected by default for the next installation such as updating - ; or when uninstalling - WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1 - !endif - - ; Registry information for add/remove programs - WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}" - WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" - WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}" - WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}" - WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\"" - WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1" - WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1" - - ${GetSize} "$INSTDIR" "/M=uninstall.exe /S=0K /G=0" $0 $1 $2 - IntOp $0 $0 + ${ESTIMATEDSIZE} - IntFmt $0 "0x%08X" $0 - WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "$0" - - !if "${HOMEPAGE}" != "" - WriteRegStr SHCTX "${UNINSTKEY}" "URLInfoAbout" "${HOMEPAGE}" - WriteRegStr SHCTX "${UNINSTKEY}" "URLUpdateInfo" "${HOMEPAGE}" - WriteRegStr SHCTX "${UNINSTKEY}" "HelpLink" "${HOMEPAGE}" - !endif - - ; Register Main Binary path to Apps Paths - WriteRegStr SHCTX "${APPPATHKEY}" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" - WriteRegStr SHCTX "${APPPATHKEY}" "Path" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" - - ; Create start menu shortcut - !insertmacro MUI_STARTMENU_WRITE_BEGIN Application - Call CreateOrUpdateStartMenuShortcut - !insertmacro MUI_STARTMENU_WRITE_END - - ; Create desktop shortcut and run Executable for silent and passive installers - ; because finish page will be skipped - ${If} $PassiveMode = 1 - ${OrIf} ${Silent} - Call CreateOrUpdateDesktopShortcut - Call RunMainBinary - ${EndIf} - - !ifmacrodef NSIS_HOOK_POSTINSTALL - !insertmacro NSIS_HOOK_POSTINSTALL - !endif - - ; Auto close this page for passive mode - ${If} $PassiveMode = 1 - SetAutoClose true - ${EndIf} -SectionEnd - -Function .onInstSuccess - ; Check for `/R` flag only in silent and passive installers because - ; GUI installer has a toggle for the user to (re)start the app - ${If} $PassiveMode = 1 - ${OrIf} ${Silent} - ${GetOptions} $CMDLINE "/R" $R0 - ${IfNot} ${Errors} - ${GetOptions} $CMDLINE "/ARGS" $R0 - nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0" - ${EndIf} - ${EndIf} -FunctionEnd - -Function un.onInit - !insertmacro SetContext - - !if "${INSTALLMODE}" == "both" - !insertmacro MULTIUSER_UNINIT - !endif - - !insertmacro MUI_UNGETLANGUAGE - - ${GetOptions} $CMDLINE "/P" $PassiveMode - ${IfNot} ${Errors} - StrCpy $PassiveMode 1 - ${EndIf} - - ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode - ${IfNot} ${Errors} - StrCpy $UpdateMode 1 - ${EndIf} -FunctionEnd - -Section Uninstall - - !ifmacrodef NSIS_HOOK_PREUNINSTALL - !insertmacro NSIS_HOOK_PREUNINSTALL - !endif - - !insertmacro CheckIfAppIsRunning - - ; Delete the app directory and its content from disk - ; Copy main executable - Delete "$INSTDIR\${MAINBINARYNAME}.exe" - - ; Delete resources - {{#each resources}} - Delete "$INSTDIR\\{{this.[1]}}" - {{/each}} - - ; Delete external binaries - {{#each binaries}} - Delete "$INSTDIR\\{{this}}" - {{/each}} - - ; Delete app associations - {{#each file_associations as |association| ~}} - {{#each association.ext as |ext| ~}} - !insertmacro APP_UNASSOCIATE "{{ext}}" "{{or association.name ext}}" - {{/each}} - {{/each}} - - ; Delete deep links - {{#each deep_link_protocols as |protocol| ~}} - ReadRegStr $R7 SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" - ${If} $R7 == "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" - DeleteRegKey SHCTX "Software\Classes\\{{protocol}}" - ${EndIf} - {{/each}} - - ; Refresh file associations icons - !insertmacro UPDATEFILEASSOC - - ; Delete uninstaller - Delete "$INSTDIR\uninstall.exe" - - {{#each resources_ancestors}} - RMDir /REBOOTOK "$INSTDIR\\{{this}}" - {{/each}} - RMDir "$INSTDIR" - - ; Remove shortcuts if not updating - ${If} $UpdateMode <> 1 - !insertmacro DeleteAppUserModelId - - ; Remove start menu shortcut - !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder - !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" - Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" - RMDir "$SMPROGRAMS\$AppStartMenuFolder" - ${EndIf} - !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" - Delete "$SMPROGRAMS\${PRODUCTNAME}.lnk" - ${EndIf} - - ; Remove desktop shortcuts - !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk" - Delete "$DESKTOP\${PRODUCTNAME}.lnk" - ${EndIf} - ${EndIf} - - ; Remove registry information for add/remove programs - !if "${INSTALLMODE}" == "both" - DeleteRegKey SHCTX "${UNINSTKEY}" - DeleteRegKey SHCTX "${APPPATHKEY}" - !else if "${INSTALLMODE}" == "perMachine" - DeleteRegKey HKLM "${UNINSTKEY}" - DeleteRegKey HKLM "${APPPATHKEY}" - !else - DeleteRegKey HKCU "${UNINSTKEY}" - DeleteRegKey HKCU "${APPPATHKEY}" - !endif - - DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" - - ; Delete app data if the checkbox is selected - ; and if not updating - ${If} $DeleteAppDataCheckboxState = 1 - ${AndIf} $UpdateMode <> 1 - SetShellVarContext current - RmDir /r "$APPDATA\${BUNDLEID}" - RmDir /r "$LOCALAPPDATA\${BUNDLEID}" - ${EndIf} - - !ifmacrodef NSIS_HOOK_POSTUNINSTALL - !insertmacro NSIS_HOOK_POSTUNINSTALL - !endif - - ; Auto close if passive mode - ${If} $PassiveMode = 1 - SetAutoClose true - ${EndIf} -SectionEnd - -Function RestorePreviousInstallLocation - ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" - StrCmp $4 "" +2 0 - StrCpy $INSTDIR $4 -FunctionEnd - -Function Skip - Abort -FunctionEnd - -Function SkipIfPassive - ${IfThen} $PassiveMode = 1 ${|} Abort ${|} -FunctionEnd - -Function CreateOrUpdateStartMenuShortcut - ; We used to use product name as MAINBINARYNAME - ; migrate old shortcuts to target the new MAINBINARYNAME - StrCpy $R0 0 - - !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCTNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro SetShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - StrCpy $R0 1 - ${EndIf} - - !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCTNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro SetShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - StrCpy $R0 1 - ${EndIf} - - ${If} $R0 = 1 - Return - ${EndIf} - - ; Skip creating shortcut if in update mode or no shortcut mode - ${If} $UpdateMode = 1 - ${OrIf} $NoShortcutMode = 1 - Return - ${EndIf} - - !if "${STARTMENUFOLDER}" != "" - CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder" - CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" - !else - CreateShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk" - !endif -FunctionEnd - -Function CreateOrUpdateDesktopShortcut - ; We used to use product name as MAINBINARYNAME - ; migrate old shortcuts to target the new MAINBINARYNAME - !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCTNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro SetShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Return - ${EndIf} - - ; Skip creating shortcut if in update mode or no shortcut mode - ${If} $UpdateMode = 1 - ${OrIf} $NoShortcutMode = 1 - Return - ${EndIf} - - CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - !insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk" +Unicode true +ManifestDPIAware true +; Add in `dpiAwareness` `PerMonitorV2` to manifest for Windows 10 1607+ (note this should not affect lower versions since they should be able to ignore this and pick up `dpiAware` `true` set by `ManifestDPIAware true`) +; Currently undocumented on NSIS's website but is in the Docs folder of source tree, see +; https://github.com/kichik/nsis/blob/5fc0b87b819a9eec006df4967d08e522ddd651c9/Docs/src/attributes.but#L286-L300 +; https://github.com/tauri-apps/tauri/pull/10106 +ManifestDPIAwareness PerMonitorV2 + +!if "{{compression}}" == "none" + SetCompress off +!else + ; Set the compression algorithm. We default to LZMA. + SetCompressor /SOLID "{{compression}}" +!endif + +!include MUI2.nsh +!include FileFunc.nsh +!include x64.nsh +!include WordFunc.nsh +!include "utils.nsh" +!include "FileAssociation.nsh" +!include "Win\COM.nsh" +!include "Win\Propkey.nsh" +!include "StrFunc.nsh" +${StrCase} +${StrLoc} + +{{#if installer_hooks}} +!include "{{installer_hooks}}" +{{/if}} + +!define MANUFACTURER "{{manufacturer}}" +!define PRODUCTNAME "{{product_name}}" +!define VERSION "{{version}}" +!define VERSIONWITHBUILD "{{version_with_build}}" +!define SHORTDESCRIPTION "{{short_description}}" +!define HOMEPAGE "{{homepage}}" +!define INSTALLMODE "{{install_mode}}" +!define LICENSE "{{license}}" +!define INSTALLERICON "{{installer_icon}}" +!define SIDEBARIMAGE "{{sidebar_image}}" +!define HEADERIMAGE "{{header_image}}" +!define MAINBINARYNAME "{{main_binary_name}}" +!define MAINBINARYSRCPATH "{{main_binary_path}}" +!define BUNDLEID "{{bundle_id}}" +!define COPYRIGHT "{{copyright}}" +!define OUTFILE "{{out_file}}" +!define ARCH "{{arch}}" +!define PLUGINSPATH "{{additional_plugins_path}}" +!define ALLOWDOWNGRADES "{{allow_downgrades}}" +!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}" +!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}" +!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}" +!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}" +!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}" +!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}" +!define APPPATHKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAINBINARYNAME}.exe" +!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}" +!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}" +!define ESTIMATEDSIZE "{{estimated_size}}" +!define STARTMENUFOLDER "{{start_menu_folder}}" + +Var PassiveMode +Var UpdateMode +Var NoShortcutMode + +Name "${PRODUCTNAME}" +BrandingText "${COPYRIGHT}" +OutFile "${OUTFILE}" + +; We don't actually use this value as default install path, +; it's just for nsis to append the product name folder in the directory selector +; https://nsis.sourceforge.io/Reference/InstallDir +!define PLACEHOLDER_INSTALL_DIR "placeholder\${MANUFACTURER}\${PRODUCTNAME}" +InstallDir "${PLACEHOLDER_INSTALL_DIR}" + +VIProductVersion "${VERSIONWITHBUILD}" +VIAddVersionKey "ProductName" "${PRODUCTNAME}" +VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileVersion" "${VERSION}" +VIAddVersionKey "ProductVersion" "${VERSION}" + +; Plugins path, currently exists for linux only +!if "${PLUGINSPATH}" != "" + !addplugindir "${PLUGINSPATH}" +!endif + +; Uninstaller signing command +!if "${UNINSTALLERSIGNCOMMAND}" != "" + !uninstfinalize '${UNINSTALLERSIGNCOMMAND}' +!endif + +; Handle install mode, `perUser`, `perMachine` or `both` +!if "${INSTALLMODE}" == "perMachine" + RequestExecutionLevel highest +!endif + +!if "${INSTALLMODE}" == "currentUser" + RequestExecutionLevel user +!endif + +!if "${INSTALLMODE}" == "both" + !define MULTIUSER_MUI + !define MULTIUSER_INSTALLMODE_INSTDIR "${MANUFACTURER}\${PRODUCTNAME}" + !define MULTIUSER_INSTALLMODE_COMMANDLINE + !if "${ARCH}" == "x64" + !define MULTIUSER_USE_PROGRAMFILES64 + !else if "${ARCH}" == "arm64" + !define MULTIUSER_USE_PROGRAMFILES64 + !endif + !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}" + !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser" + !define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME + !define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation + !define MULTIUSER_EXECUTIONLEVEL Highest + !include MultiUser.nsh +!endif + +; Installer & Unistaller icon +!if "${INSTALLERICON}" != "" + !define MUI_ICON "${INSTALLERICON}" + !define MUI_UNICON "${INSTALLERICON}" +!endif + +; Installer sidebar image +!if "${SIDEBARIMAGE}" != "" + !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" +!endif + +; Installer header image +!if "${HEADERIMAGE}" != "" + !define MUI_HEADERIMAGE + !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" +!endif + +; Define registry key to store installer language +!define MUI_LANGDLL_REGISTRY_ROOT "HKCU" +!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}" +!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" + +; =============================================================================================== +; ====================================== INSTALLER PAGES ======================================== +; =============================================================================================== +!define MUI_BGCOLOR 222228 +!define MUI_TEXTCOLOR fdfdfd +!define MUI_FINISHPAGE_TEXT_COLOR fdfdfd + +; 1. Welcome Page +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive +!insertmacro MUI_PAGE_WELCOME + +; 2. License Page (if defined) +!if "${LICENSE}" != "" + !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive + !insertmacro MUI_PAGE_LICENSE "${LICENSE}" +!endif + +; 3. Install mode (if it is set to `both`) +!if "${INSTALLMODE}" == "both" + !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive + !insertmacro MULTIUSER_PAGE_INSTALLMODE +!endif + +; 4. Custom page to ask user if he wants to reinstall/uninstall +; only if a previous installation was detected +Var ReinstallPageCheck +Page custom PageReinstall PageLeaveReinstall +Function PageReinstall + ; Uninstall previous WiX installation if exists. + ; + ; A WiX installer stores the installation info in registry + ; using a UUID and so we have to loop through all keys under + ; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` + ; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER} + ; + ; This has a potential issue that there maybe another installation that matches + ; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer, + ; however, this should be fine since the user will have to confirm the uninstallation + ; and they can chose to abort it if doesn't make sense. + StrCpy $0 0 + wix_loop: + EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0 + StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on + IntOp $0 $0 + 1 + ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName" + ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher" + StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop + ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString" + ${StrCase} $R1 $R0 "L" + ${StrLoc} $R0 $R1 "msiexec" ">" + StrCmp $R0 0 0 wix_done + StrCpy $R7 "wix" + StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" + Goto compare_version + wix_done: + + ; Check if there is an existing installation, if not, abort the reinstall page + ReadRegStr $R0 SHCTX "${UNINSTKEY}" "" + ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" + ${IfThen} "$R0$R1" == "" ${|} Abort ${|} + + ; Compare this installar version with the existing installation + ; and modify the messages presented to the user accordingly + compare_version: + StrCpy $R4 "$(older)" + ${If} $R7 == "wix" + ReadRegStr $R0 HKLM "$R6" "DisplayVersion" + ${Else} + ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion" + ${EndIf} + ${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|} + + nsis_tauri_utils::SemverCompare "${VERSION}" $R0 + Pop $R0 + ; Reinstalling the same version + ${If} $R0 = 0 + StrCpy $R1 "$(alreadyInstalledLong)" + StrCpy $R2 "$(addOrReinstall)" + StrCpy $R3 "$(uninstallApp)" + !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)" + StrCpy $R5 "2" + ; Upgrading + ${ElseIf} $R0 = 1 + StrCpy $R1 "$(olderOrUnknownVersionInstalled)" + StrCpy $R2 "$(uninstallBeforeInstalling)" + StrCpy $R3 "$(dontUninstall)" + !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" + StrCpy $R5 "1" + ; Downgrading + ${ElseIf} $R0 = -1 + StrCpy $R1 "$(newerVersionInstalled)" + StrCpy $R2 "$(uninstallBeforeInstalling)" + !if "${ALLOWDOWNGRADES}" == "true" + StrCpy $R3 "$(dontUninstall)" + !else + StrCpy $R3 "$(dontUninstallDowngrade)" + !endif + !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" + StrCpy $R5 "1" + ${Else} + Abort + ${EndIf} + + ; Skip showing the page if passive + ; + ; Note that we don't call this earlier at the begining + ; of this function because we need to populate some variables + ; related to current installed version if detected and whether + ; we are downgrading or not. + Call SkipIfPassive + + nsDialogs::Create 1018 + Pop $R4 + ${IfThen} $(^RTL) = 1 ${|} nsDialogs::SetRTL $(^RTL) ${|} + + ${NSD_CreateLabel} 0 0 100% 24u $R1 + Pop $R1 + + ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2 + Pop $R2 + ${NSD_OnClick} $R2 PageReinstallUpdateSelection + + ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 + Pop $R3 + ; Disable this radio button if downgrading and downgrades are disabled + !if "${ALLOWDOWNGRADES}" == "false" + ${IfThen} $R0 = -1 ${|} EnableWindow $R3 0 ${|} + !endif + ${NSD_OnClick} $R3 PageReinstallUpdateSelection + + ; Check the first radio button if this the first time + ; we enter this page or if the second button wasn't + ; selected the last time we were on this page + ${If} $ReinstallPageCheck <> 2 + SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0 + ${Else} + SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0 + ${EndIf} + + ${NSD_SetFocus} $R2 + nsDialogs::Show +FunctionEnd +Function PageReinstallUpdateSelection + ${NSD_GetState} $R2 $R1 + ${If} $R1 == ${BST_CHECKED} + StrCpy $ReinstallPageCheck 1 + ${Else} + StrCpy $ReinstallPageCheck 2 + ${EndIf} +FunctionEnd +Function PageLeaveReinstall + ${NSD_GetState} $R2 $R1 + + ; $R5 holds whether we are reinstalling the same version or not + ; $R5 == "1" -> different versions + ; $R5 == "2" -> same version + ; + ; $R1 holds the radio buttons state. its meaning is dependent on the context + StrCmp $R5 "1" 0 +2 ; Existing install is not the same version? + StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling + StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling + + reinst_uninstall: + HideWindow + ClearErrors + + ${If} $R7 == "wix" + ReadRegStr $R1 HKLM "$R6" "UninstallString" + ExecWait '$R1' $0 + ${Else} + ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" + ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" + ${If} $UpdateMode = 1 + ExecWait '$R1 /UPDATE /P _?=$4' $0 + ${Else} + ExecWait '$R1 /P _?=$4' $0 + ${EndIf} + ${EndIf} + + BringToFront + + ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code + + ${If} $0 <> 0 + ${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe" + ${If} $0 = 1 ; User aborted uninstaller? + StrCmp $R5 "2" 0 +2 ; Is the existing install the same version? + Quit ; ...yes, already installed, we are done + Abort + ${EndIf} + MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)" + Abort + ${Else} + StrCpy $0 $R1 1 + ${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString + Delete $R1 + RMDir $INSTDIR + ${EndIf} + reinst_done: +FunctionEnd + +; 5. Choose install directory page +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive +!insertmacro MUI_PAGE_DIRECTORY + +; 6. Start menu shortcut page +Var AppStartMenuFolder +!if "${STARTMENUFOLDER}" != "" + !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive + !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${STARTMENUFOLDER}" +!else + !define MUI_PAGE_CUSTOMFUNCTION_PRE Skip +!endif +!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder + +; 7. Installation page +!insertmacro MUI_PAGE_INSTFILES + +; 8. Finish page +; +; Don't auto jump to finish page after installation page, +; because the installation page has useful info that can be used debug any issues with the installer. +!define MUI_FINISHPAGE_NOAUTOCLOSE +; Show sponsor link +!define MUI_FINISHPAGE_LINK_COLOR 59a7f6 +!define MUI_FINISHPAGE_LINK "Join us on Discord! 🤍" +!define MUI_FINISHPAGE_LINK_LOCATION "https://discord.gg/ABfASx5ZAJ" + +Function RunMainBinary + Exec '"$INSTDIR\${MAINBINARYNAME}.exe"' +FunctionEnd + +!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE RunMainBinary +!insertmacro MUI_PAGE_FINISH + +; Uninstaller Pages +; 1. Confirm uninstall page +Var DeleteAppDataCheckbox +Var DeleteAppDataCheckboxState +!define /ifndef WS_EX_LAYOUTRTL 0x00400000 +!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow +Function un.ConfirmShow ; Add add a `Delete app data` check box + ; $1 inner dialog HWND + ; $2 window DPI + ; $3 style + ; $4 x + ; $5 y + ; $6 width + ; $7 height + FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog + System::Call "user32::GetDpiForWindow(p r1) i .r2" + ${If} $(^RTL) = 1 + StrCpy $3 "${__NSD_CheckBox_EXSTYLE} | ${WS_EX_LAYOUTRTL}" + IntOp $4 50 * $2 + ${Else} + StrCpy $3 "${__NSD_CheckBox_EXSTYLE}" + IntOp $4 0 * $2 + ${EndIf} + IntOp $5 100 * $2 + IntOp $6 400 * $2 + IntOp $7 25 * $2 + IntOp $4 $4 / 96 + IntOp $5 $5 / 96 + IntOp $6 $6 / 96 + IntOp $7 $7 / 96 + System::Call 'user32::CreateWindowEx(i r3, w "${__NSD_CheckBox_CLASS}", w "$(deleteAppData)", i ${__NSD_CheckBox_STYLE}, i r4, i r5, i r6, i r7, p r1, i0, i0, i0) i .s' + Pop $DeleteAppDataCheckbox + SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1 + SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1 +FunctionEnd +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave +Function un.ConfirmLeave + SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState +FunctionEnd +!insertmacro MUI_UNPAGE_CONFIRM + +; 2. Uninstalling Page +!insertmacro MUI_UNPAGE_INSTFILES + +;Languages +{{#each languages}} +!insertmacro MUI_LANGUAGE "{{this}}" +{{/each}} +!insertmacro MUI_RESERVEFILE_LANGDLL +{{#each language_files}} + !include "{{this}}" +{{/each}} + +Function .onInit + ${GetOptions} $CMDLINE "/P" $PassiveMode + ${IfNot} ${Errors} + StrCpy $PassiveMode 1 + ${EndIf} + + ${GetOptions} $CMDLINE "/NS" $NoShortcutMode + ${IfNot} ${Errors} + StrCpy $NoShortcutMode 1 + ${EndIf} + + ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode + ${IfNot} ${Errors} + StrCpy $UpdateMode 1 + ${EndIf} + + !if "${DISPLAYLANGUAGESELECTOR}" == "true" + !insertmacro MUI_LANGDLL_DISPLAY + !endif + + !insertmacro SetContext + + ${If} $INSTDIR == "${PLACEHOLDER_INSTALL_DIR}" + ; Set default install location + !if "${INSTALLMODE}" == "perMachine" + ${If} ${RunningX64} + !if "${ARCH}" == "x64" + StrCpy $INSTDIR "$PROGRAMFILES64\${MANUFACTURER}\${PRODUCTNAME}" + !else if "${ARCH}" == "arm64" + StrCpy $INSTDIR "$PROGRAMFILES64\${MANUFACTURER}\${PRODUCTNAME}" + !else + StrCpy $INSTDIR "$PROGRAMFILES\${MANUFACTURER}\${PRODUCTNAME}" + !endif + ${Else} + StrCpy $INSTDIR "$PROGRAMFILES\${MANUFACTURER}\${PRODUCTNAME}" + ${EndIf} + !else if "${INSTALLMODE}" == "currentUser" + StrCpy $INSTDIR "$LOCALAPPDATA\${MANUFACTURER}\${PRODUCTNAME}" + !endif + + Call RestorePreviousInstallLocation + ${EndIf} + + + !if "${INSTALLMODE}" == "both" + !insertmacro MULTIUSER_INIT + !endif +FunctionEnd + + +Section EarlyChecks + ; Abort silent installer if downgrades is disabled + !if "${ALLOWDOWNGRADES}" == "false" + ${If} ${Silent} + ; If downgrading + ${If} $R0 = -1 + System::Call 'kernel32::AttachConsole(i -1)i.r0' + ${If} $0 <> 0 + System::Call 'kernel32::GetStdHandle(i -11)i.r0' + System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color + FileWrite $0 "$(silentDowngrades)" + ${EndIf} + Abort + ${EndIf} + ${EndIf} + !endif + +SectionEnd + +Section WebView2 + ; Check if Webview2 is already installed and skip this section + ${If} ${RunningX64} + ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${Else} + ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${EndIf} + ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + + StrCmp $4 "" 0 webview2_done + StrCmp $5 "" 0 webview2_done + + ; Webview2 installation + ; + ; Skip if updating + ${If} $UpdateMode <> 1 + !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + DetailPrint "$(webview2Downloading)" + NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Pop $0 + ${If} $0 = 0 + DetailPrint "$(webview2DownloadSuccess)" + ${Else} + DetailPrint "$(webview2DownloadError)" + Abort "$(webview2AbortError)" + ${EndIf} + StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Goto install_webview2 + !endif + + !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" + Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" + File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" + DetailPrint "$(installingWebview2)" + StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" + Goto install_webview2 + !endif + + !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" + Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" + File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" + DetailPrint "$(installingWebview2)" + StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" + Goto install_webview2 + !endif + + Goto webview2_done + + install_webview2: + DetailPrint "$(installingWebview2)" + ; $6 holds the path to the webview2 installer + ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 + ${If} $1 = 0 + DetailPrint "$(webview2InstallSuccess)" + ${Else} + DetailPrint "$(webview2InstallError)" + Abort "$(webview2AbortError)" + ${EndIf} + webview2_done: + ${EndIf} +SectionEnd + +Section Install + SetOutPath $INSTDIR + + !ifmacrodef NSIS_HOOK_PREINSTALL + !insertmacro NSIS_HOOK_PREINSTALL + !endif + + !insertmacro CheckIfAppIsRunning + + ; Copy main executable + File "${MAINBINARYSRCPATH}" + FILE "${__FILEDIR__}\..\..\seelen_ui.pdb" + + ; Copy resources + {{#each resources_dirs}} + CreateDirectory "$INSTDIR\\{{this}}" + {{/each}} + {{#each resources}} + File /a "/oname={{this.[1]}}" "{{@key}}" + {{/each}} + + ; Copy external binaries + {{#each binaries}} + File /a "/oname={{this}}" "{{@key}}" + {{/each}} + + ; Create file associations + {{#each file_associations as |association| ~}} + {{#each association.ext as |ext| ~}} + !insertmacro APP_ASSOCIATE "{{ext}}" "{{or association.name ext}}" "{{association-description association.description ext}}" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0" "Open with ${PRODUCTNAME}" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" + {{/each}} + {{/each}} + + ; Register deep links + {{#each deep_link_protocols as |protocol| ~}} + WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "URL Protocol" "" + WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "" "URL:${BUNDLEID} protocol" + WriteRegStr SHCTX "Software\Classes\\{{protocol}}\DefaultIcon" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0" + WriteRegStr SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" + {{/each}} + + ; Refresh file associations icons + !insertmacro UPDATEFILEASSOC + + ; Create uninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + ; Save $INSTDIR in registry for future installations + WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR + + !if "${INSTALLMODE}" == "both" + ; Save install mode to be selected by default for the next installation such as updating + ; or when uninstalling + WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1 + !endif + + ; Registry information for add/remove programs + WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}" + WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" + WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}" + WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}" + WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\"" + WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1" + WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1" + + ${GetSize} "$INSTDIR" "/M=uninstall.exe /S=0K /G=0" $0 $1 $2 + IntOp $0 $0 + ${ESTIMATEDSIZE} + IntFmt $0 "0x%08X" $0 + WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "$0" + + !if "${HOMEPAGE}" != "" + WriteRegStr SHCTX "${UNINSTKEY}" "URLInfoAbout" "${HOMEPAGE}" + WriteRegStr SHCTX "${UNINSTKEY}" "URLUpdateInfo" "${HOMEPAGE}" + WriteRegStr SHCTX "${UNINSTKEY}" "HelpLink" "${HOMEPAGE}" + !endif + + ; Register Main Binary path to Apps Paths + WriteRegStr SHCTX "${APPPATHKEY}" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" + WriteRegStr SHCTX "${APPPATHKEY}" "Path" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" + + ; Create start menu shortcut + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + Call CreateOrUpdateStartMenuShortcut + !insertmacro MUI_STARTMENU_WRITE_END + + ; Create desktop shortcut and run Executable for silent and passive installers + ; because finish page will be skipped + ${If} $PassiveMode = 1 + ${OrIf} ${Silent} + Call CreateOrUpdateDesktopShortcut + Call RunMainBinary + ${EndIf} + + !ifmacrodef NSIS_HOOK_POSTINSTALL + !insertmacro NSIS_HOOK_POSTINSTALL + !endif + + ; Auto close this page for passive mode + ${If} $PassiveMode = 1 + SetAutoClose true + ${EndIf} +SectionEnd + +Function .onInstSuccess + ; Check for `/R` flag only in silent and passive installers because + ; GUI installer has a toggle for the user to (re)start the app + ${If} $PassiveMode = 1 + ${OrIf} ${Silent} + ${GetOptions} $CMDLINE "/R" $R0 + ${IfNot} ${Errors} + ${GetOptions} $CMDLINE "/ARGS" $R0 + nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0" + ${EndIf} + ${EndIf} +FunctionEnd + +Function un.onInit + !insertmacro SetContext + + !if "${INSTALLMODE}" == "both" + !insertmacro MULTIUSER_UNINIT + !endif + + !insertmacro MUI_UNGETLANGUAGE + + ${GetOptions} $CMDLINE "/P" $PassiveMode + ${IfNot} ${Errors} + StrCpy $PassiveMode 1 + ${EndIf} + + ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode + ${IfNot} ${Errors} + StrCpy $UpdateMode 1 + ${EndIf} +FunctionEnd + +Section Uninstall + + !ifmacrodef NSIS_HOOK_PREUNINSTALL + !insertmacro NSIS_HOOK_PREUNINSTALL + !endif + + !insertmacro CheckIfAppIsRunning + + ; Delete the app directory and its content from disk + ; Copy main executable + Delete "$INSTDIR\${MAINBINARYNAME}.exe" + + ; Delete resources + {{#each resources}} + Delete "$INSTDIR\\{{this.[1]}}" + {{/each}} + + ; Delete external binaries + {{#each binaries}} + Delete "$INSTDIR\\{{this}}" + {{/each}} + + ; Delete app associations + {{#each file_associations as |association| ~}} + {{#each association.ext as |ext| ~}} + !insertmacro APP_UNASSOCIATE "{{ext}}" "{{or association.name ext}}" + {{/each}} + {{/each}} + + ; Delete deep links + {{#each deep_link_protocols as |protocol| ~}} + ReadRegStr $R7 SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" + ${If} $R7 == "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" + DeleteRegKey SHCTX "Software\Classes\\{{protocol}}" + ${EndIf} + {{/each}} + + ; Refresh file associations icons + !insertmacro UPDATEFILEASSOC + + ; Delete uninstaller + Delete "$INSTDIR\uninstall.exe" + + {{#each resources_ancestors}} + RMDir /REBOOTOK "$INSTDIR\\{{this}}" + {{/each}} + RMDir "$INSTDIR" + + ; Remove shortcuts if not updating + ${If} $UpdateMode <> 1 + !insertmacro DeleteAppUserModelId + + ; Remove start menu shortcut + !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder + !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" + Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" + RMDir "$SMPROGRAMS\$AppStartMenuFolder" + ${EndIf} + !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro UnpinShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" + Delete "$SMPROGRAMS\${PRODUCTNAME}.lnk" + ${EndIf} + + ; Remove desktop shortcuts + !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk" + Delete "$DESKTOP\${PRODUCTNAME}.lnk" + ${EndIf} + ${EndIf} + + ; Remove registry information for add/remove programs + !if "${INSTALLMODE}" == "both" + DeleteRegKey SHCTX "${UNINSTKEY}" + DeleteRegKey SHCTX "${APPPATHKEY}" + !else if "${INSTALLMODE}" == "perMachine" + DeleteRegKey HKLM "${UNINSTKEY}" + DeleteRegKey HKLM "${APPPATHKEY}" + !else + DeleteRegKey HKCU "${UNINSTKEY}" + DeleteRegKey HKCU "${APPPATHKEY}" + !endif + + DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" + + ; Delete app data if the checkbox is selected + ; and if not updating + ${If} $DeleteAppDataCheckboxState = 1 + ${AndIf} $UpdateMode <> 1 + SetShellVarContext current + RmDir /r "$APPDATA\${BUNDLEID}" + RmDir /r "$LOCALAPPDATA\${BUNDLEID}" + ${EndIf} + + !ifmacrodef NSIS_HOOK_POSTUNINSTALL + !insertmacro NSIS_HOOK_POSTUNINSTALL + !endif + + ; Auto close if passive mode + ${If} $PassiveMode = 1 + SetAutoClose true + ${EndIf} +SectionEnd + +Function RestorePreviousInstallLocation + ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" + StrCmp $4 "" +2 0 + StrCpy $INSTDIR $4 +FunctionEnd + +Function Skip + Abort +FunctionEnd + +Function SkipIfPassive + ${IfThen} $PassiveMode = 1 ${|} Abort ${|} +FunctionEnd + +Function CreateOrUpdateStartMenuShortcut + ; We used to use product name as MAINBINARYNAME + ; migrate old shortcuts to target the new MAINBINARYNAME + StrCpy $R0 0 + + !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCTNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro SetShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + StrCpy $R0 1 + ${EndIf} + + !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCTNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro SetShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + StrCpy $R0 1 + ${EndIf} + + ${If} $R0 = 1 + Return + ${EndIf} + + ; Skip creating shortcut if in update mode or no shortcut mode + ${If} $UpdateMode = 1 + ${OrIf} $NoShortcutMode = 1 + Return + ${EndIf} + + !if "${STARTMENUFOLDER}" != "" + CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder" + CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" + !else + CreateShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk" + !endif +FunctionEnd + +Function CreateOrUpdateDesktopShortcut + ; We used to use product name as MAINBINARYNAME + ; migrate old shortcuts to target the new MAINBINARYNAME + !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCTNAME}.exe" + Pop $0 + ${If} $0 = 1 + !insertmacro SetShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + Return + ${EndIf} + + ; Skip creating shortcut if in update mode or no shortcut mode + ${If} $UpdateMode = 1 + ${OrIf} $NoShortcutMode = 1 + Return + ${EndIf} + + CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" + !insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk" FunctionEnd \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 07ea163f..6a83b29d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,31 +1,35 @@ -{ - "compilerOptions": { - "lib": [ - "ESNext", - "DOM" - ], - "target": "ES2020", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUncheckedIndexedAccess": true, - "noEmit": true, - "jsx": "react-jsx", - "types": [ - "react", - "readable-types", - "node", - "jest" - ], - "skipLibCheck": true, - }, - "exclude": [ - "node_modules", - "dist" - ] +{ + "compilerOptions": { + "lib": [ + "ESNext", + "DOM" + ], + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "jsx": "react-jsx", + "types": [ + "react", + "readable-types", + "node", + "jest" + ], + "skipLibCheck": true, + "paths": { + "seelen-core": ["./lib/src/lib.ts"], + }, + "baseUrl": "." + }, + "exclude": [ + "node_modules", + "dist" + ] } \ No newline at end of file