Compare commits
720 Commits
av1
..
slint-client
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c20599800 | |||
| f5c4e2ba4a | |||
| 59f77c2820 | |||
| eeb6a2a1ae | |||
| 1f86c43458 | |||
| c487d3a62c | |||
| aee5be5ee6 | |||
| 6ff2783105 | |||
| d6d30b6842 | |||
| 5db309243b | |||
| 0681f6540d | |||
| d843901550 | |||
| b41630ebb4 | |||
| a815eecc73 | |||
| 7e0984fe9c | |||
| 9c28cd2ab2 | |||
| 3c4000bdbb | |||
| d8e9fa5bba | |||
| e026491b9f | |||
| 009699b375 | |||
| 3677588a3d | |||
| 3d280053a7 | |||
| fbde3f6a47 | |||
| 1c1a33fdce | |||
| fea238722d | |||
| 178d958c08 | |||
| f9633f366b | |||
| 7a81f3e767 | |||
| bbbbbf7927 | |||
| 5735f84008 | |||
| fe0cf42e5d | |||
| 04100584ce | |||
| 9d3a422916 | |||
| 65d8284fb8 | |||
| eea107db66 | |||
| 67812957db | |||
| 69d77e59cc | |||
| efcebfd82c | |||
| 32345f93bf | |||
| 193e4bd5bf | |||
| 53edf3d57e | |||
| 895e297771 | |||
| 8f3959e6c6 | |||
| 5ff6b601c7 | |||
| 4895ac9c23 | |||
| f121aa47f7 | |||
| 00a8d59284 | |||
| a30489e05b | |||
| dfbeb3ed20 | |||
| 2eed1c974e | |||
| 63a79a90ac | |||
| a61e74d97b | |||
| 54438a4aa1 | |||
| 7682ad63e4 | |||
| 06c53fdc9c | |||
| 665f4e684c | |||
| 52b894fe0e | |||
| 82c0cbbad4 | |||
| ce004af379 | |||
| 15bd9e9fdc | |||
| 37aabeaf72 | |||
| 473737ac9b | |||
| 1e29ec708f | |||
| 515d517a99 | |||
| a3aedcb624 | |||
| 98b7c6c966 | |||
| b1d956af2c | |||
| b7a031bb7f | |||
| 15cce07b6e | |||
| 1d5d6f5121 | |||
| 5f541f5c8b | |||
| 71bce08549 | |||
| 37b9badb2a | |||
| 4089e80fe8 | |||
| 2be6e727ce | |||
| d3b886c3f6 | |||
| 97e48bfe71 | |||
| a8769dee06 | |||
| ffa94986d5 | |||
| e4dfb61509 | |||
| d42b6e3261 | |||
| 855b15025c | |||
| 3701b2c0d9 | |||
| ecbec4d301 | |||
| 21425c7132 | |||
| 3e95a7ba29 | |||
| c1394db285 | |||
| eee6c588bd | |||
| eca68f6c7a | |||
| f4e28d8774 | |||
| 21b179e01c | |||
| 83cacf6f51 | |||
| 13c37f01b1 | |||
| 511831ced3 | |||
| 518e1afa58 | |||
| 43d03ac081 | |||
| f7f62c5fe0 | |||
| 2bbddbca6b | |||
| f0f8f27f4c | |||
| 262af263f2 | |||
| 38b7775b1b | |||
| 56c0bca62f | |||
| 4b1b09fd5b | |||
| 1d6425bbf4 | |||
| 5ec6552d25 | |||
| 79e4a0790a | |||
| 1d3cac54ab | |||
| 2f26334775 | |||
| 9270d528e3 | |||
| 91db3a7e34 | |||
| d017561e54 | |||
| 8e8a85bae3 | |||
| bea89e9111 | |||
| 499ce0190a | |||
| 91bde91238 | |||
| 3e31ba102d | |||
| 263c5eefd3 | |||
| b230b851e4 | |||
| ff32477ffe | |||
| c6c60decdb | |||
| 7505adeca8 | |||
| 754f1fba88 | |||
| 8be46b870a | |||
| 81cb1d6c0b | |||
| 319416f1b7 | |||
| d679c6251b | |||
| a14baafda7 | |||
| cfdc7d3106 | |||
| 33d51b8ce5 | |||
| b13dac2093 | |||
| a605c95e5a | |||
| 9a5553a636 | |||
| ef02403da6 | |||
| adfab363c1 | |||
| 123d4cf595 | |||
| 19feb8ff49 | |||
| 9223bf9d2d | |||
| 11b5f87841 | |||
| cea59fb453 | |||
| 3a179bf480 | |||
| b9c53024f1 | |||
| 62b37ad698 | |||
| de56cd5d3b | |||
| 8d9d78185a | |||
| b10a6512fe | |||
| a94da8802f | |||
| 4e6f82d00c | |||
| 5e2ad99ec0 | |||
| 8ab50ea362 | |||
| 25e9958a69 | |||
| 410ea8b96b | |||
| e656664cad | |||
| 0e6cee0961 | |||
| 42506b8c1d | |||
| e35365d162 | |||
| bf1c0f796d | |||
| 547532b28c | |||
| a91e23abf6 | |||
| 2b324f636b | |||
| 103b8372e4 | |||
| f7f1724bf1 | |||
| 5d70e11f17 | |||
| fb7ae90d46 | |||
| 957792a7a0 | |||
| 2e8ce6a2f0 | |||
| 9927a56b78 | |||
| db3da52f83 | |||
| 19a7c6978a | |||
| b5e9ba03a1 | |||
| cb5f8b91ad | |||
| f627f60f1a | |||
| e9fce5b8b8 | |||
| a7820a79db | |||
| b6a52dbcd4 | |||
| 7bbd10a50c | |||
| ee08b231db | |||
| 619e54dc0e | |||
| 9b69596af1 | |||
| f6e169c013 | |||
| fd242d50c1 | |||
| d6d8ecd6c5 | |||
| 669fac7f50 | |||
| 92d670916e | |||
| 0155413c12 | |||
| 8468be6532 | |||
| 78c82f778a | |||
| ab89a3d41a | |||
| 5f320af6e6 | |||
| 17b7ba6b72 | |||
| c70ebdfe15 | |||
| a3e564f160 | |||
| eea37424c9 | |||
| b322181853 | |||
| 3ad66f5e0b | |||
| 4035e0dd13 | |||
| 832b820096 | |||
| d337971de0 | |||
| a967dc72d7 | |||
| 5066fcda48 | |||
| e7bdf42694 | |||
| 875fea88ee | |||
| b2654ea9db | |||
| 8f8e415262 | |||
| 5ff624f7b2 | |||
| e09243f1ec | |||
| f5941c7eda | |||
| 3c2ebe602e | |||
| 2f64172ead | |||
| a83206a346 | |||
| 46e769976f | |||
| 58c24b798e | |||
| 5cc31e5ba3 | |||
| 74fe9bebf5 | |||
| 1f6a2182be | |||
| 1a883f0d6c | |||
| a560b4ca70 | |||
| 46f45ed216 | |||
| 5c23f1c5e8 | |||
| 70ae02549f | |||
| 68de995c64 | |||
| ed5ddb96fd | |||
| 436dfafc2a | |||
| 5221b193e5 | |||
| fafced23c2 | |||
| 1e48b645ca | |||
| 49ed0200e7 | |||
| 24873afe64 | |||
| d21e1bd422 | |||
| be044c248b | |||
| 49cbbc3363 | |||
| 1e20cb806b | |||
| 2e52818f6f | |||
| b50f386713 | |||
| 280e011ae4 | |||
| 8d09bf53c3 | |||
| 131b4f1795 | |||
| 7d3ecf789d | |||
| 37797bf873 | |||
| 91d42b6561 | |||
| feb9f2f460 | |||
| 9c1753c78c | |||
| 7370ff5b30 | |||
| f6eda34dbd | |||
| 5d9a0a3ea5 | |||
| 3d8249bffa | |||
| 82f32cbe8f | |||
| 56da2f99f3 | |||
| e6c72fe558 | |||
| a964c6bbf5 | |||
| 239da373d0 | |||
| 217cfb091d | |||
| 3c3c7b9ae0 | |||
| f14bdb7fe8 | |||
| c0a98f97c3 | |||
| 0ab6686eb8 | |||
| 76b475450b | |||
| 5d1e1b5667 | |||
| c3b8b1374a | |||
| 7c940d6b15 | |||
| 86501b05dd | |||
| 01ebed9b37 | |||
| 2188adb1f1 | |||
| 51409e16c8 | |||
| 6ca3b58ae2 | |||
| 692e176e34 | |||
| 4fb7acaa61 | |||
| c0d6429a54 | |||
| 07c7c7f179 | |||
| c5ceeb0d80 | |||
| 5ce0a891df | |||
| f94ef49210 | |||
| 5d0a4d1385 | |||
| dd482cee60 | |||
| e3c2edfb1c | |||
| f3901d09ea | |||
| 2b12749477 | |||
| 759488f675 | |||
| 4bb4240a9e | |||
| 1457247a6a | |||
| 97ab9bfca5 | |||
| 4dd3c3e073 | |||
| 4ba4f17a6b | |||
| f5d0291b5a | |||
| 1a64c1afef | |||
| 18f4973d0a | |||
| 37ede5861c | |||
| 497454ac51 | |||
| 4ebb7a6a4d | |||
| 7d1910df71 | |||
| 52e70a26f3 | |||
| adb6cee326 | |||
| 941b5e5cdc | |||
| c602dea58f | |||
| e9ced9fa4f | |||
| 6ab5e7487f | |||
| e3143f3e7a | |||
| 538c17d182 | |||
| 2ad32ec2b4 | |||
| b28f1dca81 | |||
| 6947f7e1c3 | |||
| b1df10c0de | |||
| 3817b222fd | |||
| 910cc9b587 | |||
| 2ee3e93afe | |||
| 8875c6a6a1 | |||
| 28062f5574 | |||
| 017af3eea4 | |||
| 78eb069cc8 | |||
| 0d591f769d | |||
| d4726355a7 | |||
| b78c9cf7d1 | |||
| b3132db785 | |||
| 43db021326 | |||
| 2c622bc76e | |||
| b790c7d08e | |||
| 0ca90d2516 | |||
| 401bfe4483 | |||
| 3b34c26555 | |||
| b668b3c936 | |||
| cc19ec125a | |||
| ffa77fdf44 | |||
| 47cf806532 | |||
| 911dce2e71 | |||
| 9f80d4f69d | |||
| cba644f055 | |||
| f733fe9e49 | |||
| 27263fe1db | |||
| 698bf72a6c | |||
| 0bd27d0b17 | |||
| ee5612da8b | |||
| c7a2023c88 | |||
| c031a8c145 | |||
| 0bf83f07ad | |||
| 3638b712bd | |||
| b2ab940f20 | |||
| 17f9536476 | |||
| 0ef51e3faf | |||
| cccf5dadb2 | |||
| 2f0b0ffc22 | |||
| c7411b59f1 | |||
| 8222782522 | |||
| 5fed09c1aa | |||
| 8e499772f9 | |||
| 0da812e7e9 | |||
| 3d67b6e9d6 | |||
| 506b2893c6 | |||
| 56abe9e690 | |||
| ab13fa582d | |||
| b9e8192eee | |||
| 5590aaeb1e | |||
| adfe14809f | |||
| a21dbc8d69 | |||
| 9d45e497f4 | |||
| e5891eb397 | |||
| b9c70f54d3 | |||
| 9cd617d078 | |||
| 92fd7f2e89 | |||
| b8535fff6f | |||
| 09f34a81ad | |||
| 22cc552e85 | |||
| 69a4dfcbb9 | |||
| e1390ca2d3 | |||
| dccdcd1b6f | |||
| 276c3a336f | |||
| 08518a9409 | |||
| f6b0767bb1 | |||
| 641fc84430 | |||
| 4ce79b87a3 | |||
| 477b204913 | |||
| 98f349ea2f | |||
| 47b1e15eef | |||
| 590bf50924 | |||
| 097633e47d | |||
| d9ba88107d | |||
| f6a6ef0b08 | |||
| a93ee0c19e | |||
| 7e73553aca | |||
| b858bd78c0 | |||
| f10947edf9 | |||
| c723cca8f9 | |||
| b8f8b07ebe | |||
| f9c6e5a6ef | |||
| 15bf8ef8c0 | |||
| 6565816c0e | |||
| 2aa67ccd57 | |||
| 88c75f94e4 | |||
| aea9505c4c | |||
| 646740aab6 | |||
| a458836e6e | |||
| 5fe7df8ea8 | |||
| fda3743f86 | |||
| 9e4170b3a8 | |||
| 43338aaf02 | |||
| 274b7fcedc | |||
| 01a5660984 | |||
| a45118f785 | |||
| b6631c3db0 | |||
| 84d164c3af | |||
| 32815ce25a | |||
| 7c2b3f8c8d | |||
| 309d9d6ed6 | |||
| 91ab21c5f2 | |||
| b8369c4f0a | |||
| a068a32303 | |||
| b6fe0da581 | |||
| 6e7e2697b5 | |||
| 22084072b0 | |||
| d5457373f4 | |||
| 01ae299cde | |||
| fbcd4c21cf | |||
| cf9dc0a9a5 | |||
| 1b4f41af46 | |||
| 7f3425519b | |||
| 434c92deb6 | |||
| e5eae1feb4 | |||
| 37e37d7d20 | |||
| 062568dc96 | |||
| d60fdf9050 | |||
| 9912a88a13 | |||
| 8a8f2cd5d7 | |||
| e77e16d9c2 | |||
| 1616d0ec33 | |||
| 374453775f | |||
| 6f8fd6a030 | |||
| c7166975b3 | |||
| 2ae5e5a969 | |||
| 85cdc995c5 | |||
| b051c8a059 | |||
| 508b7dc7a1 | |||
| f203566b81 | |||
| 8360c1725f | |||
| 3b00fdef71 | |||
| f2664daaec | |||
| 1a1bbe3e4d | |||
| 4c898c5f07 | |||
| 5d704edbc8 | |||
| b23038bac6 | |||
| 147b9b2ddc | |||
| 1327b995f2 | |||
| e8d7ec8daf | |||
| 3bf68a396f | |||
| 29077fad5e | |||
| 69367bdadf | |||
| f650b5a6ef | |||
| 75021b74ef | |||
| 205421621b | |||
| 4bb5607702 | |||
| 78c54136e2 | |||
| 8fe8f4fd7e | |||
| c13e2613b6 | |||
| e52ec6cde2 | |||
| ad6a24b230 | |||
| ad60815d83 | |||
| c47a90bf9c | |||
| 3901e26c37 | |||
| 41c27139a0 | |||
| a31ca21ef4 | |||
| 57939ccd00 | |||
| fc14f7b9c6 | |||
| cfcf6dd9cb | |||
| 854c3a72c7 | |||
| 8004d25ca3 | |||
| b04dfd188a | |||
| 383ace2493 | |||
| 094204361c | |||
| d72c6d9df7 | |||
| 05d73ebe9a | |||
| a8b5e934b8 | |||
| 920677a433 | |||
| b86d3d42ee | |||
| 818dab764f | |||
| 7ea26854af | |||
| f34b55b88a | |||
| 67168f7735 | |||
| 6e69c37056 | |||
| 8f5dd21e75 | |||
| e2dfeb1186 | |||
| 96c7d3174b | |||
| 97f6c18296 | |||
| 9138ca771f | |||
| abc6b17a3b | |||
| 8e0524bf60 | |||
| 6ebf583a13 | |||
| eca4f614c8 | |||
| 11358e0b60 | |||
| 0a1014dded | |||
| a6beb48b1f | |||
| 5eff530ab9 | |||
| e6e237279c | |||
| 6fe46f6181 | |||
| 9b5023645c | |||
| c2da4ebcb7 | |||
| dc05f2405f | |||
| e2a469ed65 | |||
| e89d385f99 | |||
| 0a61dcc2be | |||
| 250fd49406 | |||
| 93bd5b2660 | |||
| d05ff9f905 | |||
| f3451a5fa1 | |||
| a9084ba98d | |||
| 893051f9b3 | |||
| dfbd4317b7 | |||
| 532ad0eb51 | |||
| 184983d857 | |||
| c33e4bbe0e | |||
| 4b9e86c424 | |||
| f7f8ddd925 | |||
| 188b1758f2 | |||
| 9705374b9a | |||
| 22aed6ea53 | |||
| 44c5fde086 | |||
| 2cd5a9dd76 | |||
| fa73447f88 | |||
| 536fe17055 | |||
| 662cbbc3cc | |||
| 69a8503ee1 | |||
| b906bfcafd | |||
| f891a047d6 | |||
| dce32672a6 | |||
| 2774991107 | |||
| b7ce8b6299 | |||
| 700fb2ec14 | |||
| 62d14587cd | |||
| 824d9243cc | |||
| 5351b4da0e | |||
| fcd0488624 | |||
| 193e905b28 | |||
| f48085c69a | |||
| cf078be53f | |||
| badcd6a05c | |||
| d828bd736d | |||
| 1e014bdae3 | |||
| 334ab182db | |||
| f52de76bfc | |||
| 6c7d0e8cab | |||
| 7229ae856e | |||
| 597d760d24 | |||
| bcf61e1ada | |||
| 08e596714b | |||
| bff577ba34 | |||
| 4df935b9d2 | |||
| 9bb560314b | |||
| 1a0c5e8b42 | |||
| 011919d0e7 | |||
| 418ab7a1d2 | |||
| 6a2c9af316 | |||
| eaabf478cc | |||
| ffe3ca76af | |||
| c5a6302220 | |||
| 9b2f81690f | |||
| 9621e6b570 | |||
| ce3ae03bef | |||
| e0457213ea | |||
| 10cdc440a0 | |||
| ddc62c90bb | |||
| 9e70d0e8fc | |||
| 4533d53ba8 | |||
| 0caa243006 | |||
| 1e58abdfdd | |||
| 370ac08d09 | |||
| 31b6b2736c | |||
| abd22ab7f1 | |||
| 5ab68988aa | |||
| ba3edcc02a | |||
| 8414a57a5b | |||
| 3f777e4662 | |||
| 52828183a1 | |||
| df7489f8e2 | |||
| 4fea7d86e1 | |||
| cb17b7c8db | |||
| cf7ef89bf2 | |||
| 2d2a578800 | |||
| 0ba12f3ccf | |||
| 5ac603977d | |||
| 25d5a80bee | |||
| c9d452a025 | |||
| 8132d62c02 | |||
| ca32ebeefe | |||
| 5bf5e9ee25 | |||
| bf097008e7 | |||
| 59b1208321 | |||
| 84194188f8 | |||
| a067441fb9 | |||
| 6eac8380b6 | |||
| 5aed8317ca | |||
| 9aed7a19cf | |||
| c0154be1aa | |||
| f9d024e971 | |||
| 526eb4bb31 | |||
| f9347cbd49 | |||
| b6671bdbe7 | |||
| edcf5d408c | |||
| 8c8731909e | |||
| de721ac6e3 | |||
| 963f1da1d8 | |||
| 4c6159e4d4 | |||
| e3c2e9ec6d | |||
| 02022bdcdf | |||
| 19ea426efc | |||
| 863070a8a7 | |||
| 44f9e6a8c9 | |||
| 087d5d7e52 | |||
| 26fa53f867 | |||
| d18af6cbc6 | |||
| b5bb62bd22 | |||
| 9ed3ab9929 | |||
| dca18762e0 | |||
| fed7c3b103 | |||
| d246b7a04d | |||
| a49ca813e0 | |||
| 0c688efaee | |||
| be3561d46f | |||
| c3af40a3f0 | |||
| d493b9a131 | |||
| 4e4e84ae4d | |||
| fea545e5e7 | |||
| 9096769a85 | |||
| 04ab157ecb | |||
| 2331f08283 | |||
| 9f8f99f21b | |||
| 56dadb6a49 | |||
| 59c9ca8d53 | |||
| f16a4e8aa2 | |||
| 890615e13a | |||
| 2f72e3957e | |||
| 1292018f51 | |||
| 8ae9513104 | |||
| c1efe2f4ac | |||
| 1210a0b631 | |||
| 39863c597e | |||
| 8a964f0030 | |||
| 74e29f25bf | |||
| 1e5bea2b1e | |||
| d8297ebb74 | |||
| 93d7f71cf2 | |||
| 887a217828 | |||
| 89b12136e4 | |||
| def7025abf | |||
| 35af5aab43 | |||
| 9ea67df0fd | |||
| 72fda8a728 | |||
| 070b48d7a7 | |||
| 6168009cef | |||
| 06a7243ac1 | |||
| c8602b0d89 | |||
| e3c730fd5f | |||
| b252cb6ddd | |||
| 8ca1e8e5a1 | |||
| a7d45b78c8 | |||
| 018231eee4 | |||
| 4704d494ec | |||
| 65927c2091 | |||
| 574b9d10ab | |||
| ff510a3b44 | |||
| 4d3c864950 | |||
| 2cde54cf30 | |||
| d1f3d11318 | |||
| 436228946b | |||
| 2b4083ee10 | |||
| e3abb4e3de | |||
| 4b7cd1005b | |||
| 03ea96096d | |||
| 0ea8916426 | |||
| 43b36eb893 | |||
| 03b6a187b3 | |||
| 664412dd4e | |||
| b37e08a202 | |||
| a05d72ec67 | |||
| f77e9fe6a8 | |||
| 1f9614e060 | |||
| 50d92a763a | |||
| ec23656334 | |||
| 880c2949c3 | |||
| 07f5fe81c8 | |||
| 5a992b6589 | |||
| 8e03e8e79b | |||
| ceb3d9fe20 | |||
| 0dc0b87bc4 | |||
| 3a4284fece | |||
| 502a90f121 | |||
| 88cd4aca4a | |||
| 3395004f93 | |||
| e4c05e1f4d | |||
| d17c70c2c8 | |||
| 7b42923418 | |||
| 5b6bdee25a | |||
| 05deb73c29 | |||
| 3685acc549 | |||
| 8f5a53937a | |||
| b9c5db41ab | |||
| a99a4230af | |||
| f446154747 | |||
| 5a1e2c5ed9 | |||
| ff6f295fac | |||
| 3111b3a641 | |||
| 20bb13ce85 | |||
| 5aa05f3a13 | |||
| c911aa2eb1 | |||
| d0cd2fe9ab | |||
| 9702805331 | |||
| 872152f1be | |||
| b822221d7f | |||
| 95ad605b36 | |||
| af32e25149 | |||
| e63b384d1e | |||
| 7f25f7426c | |||
| eed93ea953 | |||
| b5f8e92526 | |||
| af04b0571e | |||
| 75452a3e76 | |||
| 3f717f1df2 | |||
| ad6f2c2c70 | |||
| 8076e7f662 | |||
| be78496992 | |||
| a3f745d441 | |||
| e693d920d3 | |||
| 0f1b89eda9 | |||
| 172b8836fd | |||
| 71178ffa33 |
@@ -0,0 +1,20 @@
|
||||
**
|
||||
|
||||
!.dockerignore
|
||||
!docker/
|
||||
!docker/linux-build/
|
||||
!docker/linux-build/**
|
||||
|
||||
# Dependency manifests and local xmake package recipes are the only project
|
||||
# files needed while pre-warming the build image.
|
||||
!xmake.lua
|
||||
!xmake/
|
||||
!xmake/**
|
||||
!thirdparty/
|
||||
!thirdparty/**
|
||||
!submodules/
|
||||
!submodules/xmake.lua
|
||||
!submodules/minirtc/
|
||||
!submodules/minirtc/xmake.lua
|
||||
!submodules/minirtc/thirdparty/
|
||||
!submodules/minirtc/thirdparty/**
|
||||
@@ -0,0 +1,3 @@
|
||||
*.h linguist-language=C++
|
||||
*.cpp linguist-language=C++
|
||||
*.lua linguist-language=Xmake
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: 问题反馈
|
||||
about: 请在此提交问题报告,以便持续优化产品。
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: kunkundi
|
||||
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
清晰简洁地描述遇到的错误。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
1. 前往 '...'
|
||||
2. 点击 '....'
|
||||
3. 出现错误
|
||||
|
||||
**预期行为**
|
||||
清晰简洁地描述你期望发生的行为。
|
||||
|
||||
**截图**
|
||||
如果适用,请添加截图以帮助说明问题。
|
||||
|
||||
**桌面端信息(请填写以下内容):**
|
||||
- 操作系统: [例如 Windows 11]
|
||||
- 版本: [例如 v1.1.10]
|
||||
|
||||
**移动端信息(请填写以下内容):**
|
||||
- 设备: [例如 iPhone 17]
|
||||
- 操作系统: [例如 iOS 26.1]
|
||||
- 浏览器: [例如 系统浏览器、Safari]
|
||||
|
||||
**补充信息**
|
||||
在此添加与问题相关的其他上下文内容。
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 请在此提交功能需求或改进建议,以便后续迭代参考。
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: kunkundi
|
||||
|
||||
---
|
||||
|
||||
**功能/改进建议描述**
|
||||
清晰简洁地描述希望新增的功能或改进的内容。
|
||||
|
||||
**使用场景 / 背景**
|
||||
说明该功能或改进的使用场景,以及解决后带来的价值。
|
||||
|
||||
**预期效果**
|
||||
描述你认为最理想的功能表现或改进效果。
|
||||
|
||||
**参考示例(可选)**
|
||||
提供类似功能截图、参考链接或其他说明,帮助更好理解需求。
|
||||
|
||||
**优先级(可选)**
|
||||
- [ ] 高
|
||||
- [ ] 中
|
||||
- [ ] 低
|
||||
|
||||
**补充信息(可选)**
|
||||
其他相关信息或特殊要求。
|
||||
@@ -0,0 +1,155 @@
|
||||
name: Build Linux Build Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- ci/linux-build-image
|
||||
paths:
|
||||
- .dockerignore
|
||||
- .gitmodules
|
||||
- .github/workflows/build-linux-image.yml
|
||||
- docker/linux-build/**
|
||||
- xmake.lua
|
||||
- xmake/**
|
||||
- thirdparty/**
|
||||
- submodules/xmake.lua
|
||||
- submodules/minirtc
|
||||
- submodules/minirtc/xmake.lua
|
||||
- submodules/minirtc/thirdparty/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: linux-build-image-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_NAME: crossdesk/ubuntu22.04
|
||||
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
XMAKE_VERSION: 3.0.9
|
||||
RUST_VERSION: 1.92.0
|
||||
RUSTUP_VERSION: 1.28.2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build candidate (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04
|
||||
- arch: arm64
|
||||
platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
base_image: ubuntu:22.04
|
||||
steps:
|
||||
- name: Set candidate tag
|
||||
id: image
|
||||
run: |
|
||||
CANDIDATE_TAG="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-${{ matrix.arch }}"
|
||||
echo "CANDIDATE_TAG=${CANDIDATE_TAG}" >> "${GITHUB_ENV}"
|
||||
echo "candidate=${CANDIDATE_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push candidate
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/linux-build/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
pull: true
|
||||
tags: ${{ steps.image.outputs.candidate }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:${{ env.STACK_TAG }}
|
||||
cache-to: type=inline
|
||||
provenance: false
|
||||
sbom: false
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.base_image }}
|
||||
XMAKE_VERSION=${{ env.XMAKE_VERSION }}
|
||||
RUST_VERSION=${{ env.RUST_VERSION }}
|
||||
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
|
||||
|
||||
verify:
|
||||
name: Verify candidate (${{ matrix.arch }})
|
||||
needs: build
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Set candidate tag
|
||||
run: |
|
||||
CANDIDATE_TAG="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-${{ matrix.arch }}"
|
||||
echo "CANDIDATE_TAG=${CANDIDATE_TAG}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Verify tools, architecture, and Ubuntu baseline
|
||||
run: |
|
||||
docker pull "${CANDIDATE_TAG}"
|
||||
docker run --rm \
|
||||
--env EXPECTED_ARCH="${{ matrix.arch }}" \
|
||||
"${CANDIDATE_TAG}" \
|
||||
verify-crossdesk-build-image
|
||||
|
||||
publish:
|
||||
name: Publish multi-arch image
|
||||
needs: verify
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Promote verified candidates
|
||||
run: |
|
||||
AMD64_IMAGE="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-amd64"
|
||||
ARM64_IMAGE="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-arm64"
|
||||
|
||||
docker buildx imagetools create \
|
||||
--tag "${IMAGE_NAME}:${STACK_TAG}" \
|
||||
--tag "${IMAGE_NAME}:sha-${GITHUB_SHA}" \
|
||||
--tag "${IMAGE_NAME}:latest" \
|
||||
"${AMD64_IMAGE}" \
|
||||
"${ARM64_IMAGE}"
|
||||
|
||||
- name: Inspect published manifest
|
||||
run: |
|
||||
docker buildx imagetools inspect "${IMAGE_NAME}:${STACK_TAG}"
|
||||
@@ -0,0 +1,585 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- "!ci/linux-build-image"
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
patch:
|
||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
name: Build Linux (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_amd64.sh
|
||||
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_arm64.sh
|
||||
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
BUILD_DATE_OVERRIDE=""
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set legal Debian version
|
||||
shell: bash
|
||||
run: |
|
||||
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
|
||||
if [[ -z "${BUILD_DATE}" ]]; then
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
|
||||
VERSION_BASE="0.0.0-${VERSION_BASE}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Verify Linux build image
|
||||
run: verify-crossdesk-build-image
|
||||
|
||||
- name: Build CrossDesk
|
||||
env:
|
||||
CUDA_PATH: /usr/local/cuda
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
chmod +x ${{ matrix.package_script }}
|
||||
${{ matrix.package_script }} ${LEGAL_VERSION}
|
||||
|
||||
- name: Verify packaged Ubuntu 22.04 glibc baseline
|
||||
run: |
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
|
||||
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
|
||||
install -d "${VERIFY_DIR}"
|
||||
dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}"
|
||||
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
|
||||
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
|
||||
|
||||
# macOS
|
||||
build-macos:
|
||||
name: Build on macOS
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
binary_arch: x86_64
|
||||
runner: macos-15-intel
|
||||
cache-key: intel-macos14
|
||||
out-dir: ./build/macosx/x86_64/release/crossdesk
|
||||
package_script: ./scripts/macosx/pkg_x64.sh
|
||||
- arch: arm64
|
||||
binary_arch: arm64
|
||||
runner: macos-14
|
||||
cache-key: arm-macos14
|
||||
out-dir: ./build/macosx/arm64/release/crossdesk
|
||||
package_script: ./scripts/macosx/pkg_arm64.sh
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
echo "VERSION_NUM=${VERSION_NUM}"
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.xmake/packages
|
||||
key: "${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-
|
||||
|
||||
- name: Install xmake
|
||||
run: brew install xmake
|
||||
|
||||
- name: Update xmake repositories
|
||||
run: xmake repo -u
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package CrossDesk app
|
||||
run: |
|
||||
chmod +x ${{ matrix.package_script }}
|
||||
${{ matrix.package_script }} ${VERSION_NUM}
|
||||
|
||||
- name: Verify packaged app
|
||||
shell: bash
|
||||
run: |
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-macos-${{ matrix.arch }}-${VERSION_NUM}.pkg"
|
||||
VERIFY_DIR="$(mktemp -d "${RUNNER_TEMP}/crossdesk-macos-pkg.XXXXXX")"
|
||||
trap 'rm -rf "${VERIFY_DIR}"' EXIT
|
||||
|
||||
pkgutil --expand-full "${PACKAGE_FILE}" "${VERIFY_DIR}/expanded"
|
||||
APP_PATH="$(find "${VERIFY_DIR}/expanded" -type d -name 'CrossDesk.app' -print -quit)"
|
||||
if [[ -z "${APP_PATH}" ]]; then
|
||||
echo "CrossDesk.app is missing from ${PACKAGE_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
|
||||
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
|
||||
|
||||
- name: Move files to release dir
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg release/
|
||||
|
||||
# Windows
|
||||
build-windows-x64:
|
||||
name: Build on Windows x64
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
XMAKE_GLOBALDIR: D:\xmake_global
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ref = $env:GITHUB_REF
|
||||
$version = $ref -replace '^refs/(tags|heads)/', ''
|
||||
$version = $version -replace '^v', ''
|
||||
$version = $version -replace '/', '-'
|
||||
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
|
||||
$PATCH_NUMBER = "${{ github.event.inputs.patch }}"
|
||||
|
||||
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
|
||||
$PATCH_NUMBER = "0"
|
||||
}
|
||||
|
||||
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
$BUILD_DATE = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
$PATCH_NUMBER = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
}
|
||||
|
||||
if ($PATCH_NUMBER -ne "0") {
|
||||
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
|
||||
} else {
|
||||
$VERSION_NUM = "v$version-$BUILD_DATE"
|
||||
}
|
||||
|
||||
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: D:\xmake_global\.xmake\packages
|
||||
key: "${{ runner.os }}-xmake-deps-intel-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake-deps-intel-
|
||||
|
||||
- name: Install xmake
|
||||
run: |
|
||||
Invoke-Expression (Invoke-Webrequest 'https://raw.githubusercontent.com/tboox/xmake/master/scripts/get.ps1' -UseBasicParsing).Content
|
||||
echo "C:\Users\runneradmin\xmake" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
xmake repo -u
|
||||
xmake create cuda
|
||||
Set-Location cuda
|
||||
xmake g --theme=plain
|
||||
$cudaPath = ""
|
||||
$packagesPath = "D:\xmake_global\.xmake\packages"
|
||||
|
||||
if (Test-Path $packagesPath) {
|
||||
Write-Host "Packages directory exists: $packagesPath"
|
||||
try {
|
||||
$info = xmake require --info "cuda 12.6.3" 2>$null
|
||||
if ($null -ne $info -and $info -ne "") {
|
||||
$cudaPath = (($info | Select-String installdir).ToString() -replace '.*installdir:\s*','').Trim()
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
Write-Host "Packages directory not found: $packagesPath"
|
||||
Write-Host "Installing CUDA package..."
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
$info = xmake require --info "cuda 12.6.3"
|
||||
$cudaPath = (($info | Select-String installdir).ToString() -replace '.*installdir:\s*','').Trim()
|
||||
}
|
||||
|
||||
echo "CUDA_PATH=$cudaPath" >> $env:GITHUB_ENV
|
||||
Write-Host "Resolved CUDA_PATH = $cudaPath"
|
||||
Pop-Location
|
||||
|
||||
- name: Download INetC plugin
|
||||
shell: powershell
|
||||
run: |
|
||||
$url = "https://github.com/DigitalMediaServer/NSIS-INetC-plugin/releases/download/v1.0.5.7/InetC.zip"
|
||||
$out = "$env:RUNNER_TEMP\InetC.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $out
|
||||
Expand-Archive $out -DestinationPath "$env:RUNNER_TEMP\InetC" -Force
|
||||
|
||||
$source = "$env:RUNNER_TEMP\InetC\x86-unicode\INetC.dll"
|
||||
$target = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode\INetC.dll"
|
||||
|
||||
Write-Host "Copying $source to $target"
|
||||
Copy-Item $source $target -Force
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: Copy nsProcess plugin to NSIS folder
|
||||
run: |
|
||||
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||
copy "${{ github.workspace }}\scripts\windows\nsProcess.dll" $nsisPluginDir
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package Portable
|
||||
shell: pwsh
|
||||
run: |
|
||||
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
|
||||
$portableDir = "${{ github.workspace }}\portable"
|
||||
New-Item -ItemType Directory -Force -Path $portableDir
|
||||
|
||||
$portableFiles = @(
|
||||
@("crossdesk.exe", "CrossDesk.exe"),
|
||||
@("crossdesk_service.exe", "crossdesk_service.exe"),
|
||||
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
|
||||
)
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
$source = Join-Path $buildDir $file[0]
|
||||
$destination = Join-Path $portableDir $file[1]
|
||||
if (!(Test-Path $source)) {
|
||||
throw "Missing portable package file: $source"
|
||||
}
|
||||
Copy-Item $source $destination -Force
|
||||
}
|
||||
|
||||
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
$packagedFile = Join-Path $portableDir $file[1]
|
||||
if (!(Test-Path $packagedFile)) {
|
||||
throw "Portable package is missing: $packagedFile"
|
||||
}
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
|
||||
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
|
||||
- name: Upload portable artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
|
||||
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
|
||||
|
||||
release:
|
||||
name: Publish Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
[build-linux, build-macos, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
VERSION_WITH_V="v${VERSION_NUM}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
||||
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
||||
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
||||
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||
|
||||
- name: List release files
|
||||
run: ls -lh release/
|
||||
|
||||
- name: Upload to Versioned GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: release/*
|
||||
generate_release_notes: false
|
||||
body: |
|
||||
Binary release only. Source code is not included.
|
||||
|
||||
- name: Create or update 'latest' tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -f latest
|
||||
git push origin latest --force
|
||||
|
||||
- name: Upload to GitHub Release (latest)
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: latest
|
||||
name: Latest Release
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: release/*
|
||||
generate_release_notes: false
|
||||
|
||||
- name: Upload artifacts to server
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
with:
|
||||
switches: -avzr --progress --delete
|
||||
path: release/*
|
||||
remote_path: /var/www/html/downloads/
|
||||
remote_host: ${{ secrets.SERVER_HOST }}
|
||||
remote_user: ${{ secrets.SERVER_USER }}
|
||||
remote_key: ${{ secrets.SERVER_KEY }}
|
||||
|
||||
- name: Generate version.json
|
||||
run: |
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": "",
|
||||
"releaseNotes": "",
|
||||
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
|
||||
"downloads": {
|
||||
"windows-x64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe",
|
||||
"filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe"
|
||||
},
|
||||
"windows-x64-portable": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip",
|
||||
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip"
|
||||
},
|
||||
"macos-x64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
|
||||
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
|
||||
},
|
||||
"macos-arm64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
|
||||
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
|
||||
},
|
||||
"linux-amd64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb",
|
||||
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb",
|
||||
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat version.json
|
||||
|
||||
- name: Upload version.json to server
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
with:
|
||||
switches: -avzr --delete
|
||||
path: version.json
|
||||
remote_path: /var/www/html/version/
|
||||
remote_host: ${{ secrets.SERVER_HOST }}
|
||||
remote_user: ${{ secrets.SERVER_USER }}
|
||||
remote_key: ${{ secrets.SERVER_KEY }}
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Close Inactive Issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every day at midnight
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
close_inactive_issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check inactive issues and close them
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const now = Date.now();
|
||||
|
||||
// paginate through all open issues (listForRepo also returns PRs)
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
});
|
||||
|
||||
for (const issue of issues) {
|
||||
// skip pull requests (they are also returned by listForRepo)
|
||||
if (issue.pull_request) continue;
|
||||
|
||||
// skip labeled issues
|
||||
if (issue.labels.length > 0) {
|
||||
console.log(`Skipping issue #${issue.number} (Has labels).`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// last activity time = the issue's own updated_at, which is
|
||||
// refreshed on comments, labels, etc. This avoids relying on
|
||||
// fetching comments and is accurate even when comments are edited.
|
||||
const lastActivityTime = new Date(issue.updated_at).getTime();
|
||||
|
||||
// check inactivity
|
||||
if (now - lastActivityTime > inactivePeriod) {
|
||||
console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: "This issue has been automatically closed due to inactivity for 7 days."
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
});
|
||||
} else {
|
||||
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
name: Update version.json from Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published, edited]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-version-json:
|
||||
name: Update version.json with release information
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
TAG_NAME="${{ github.event.release.tag_name }}"
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
VERSION_FULL="${TAG_VERSION}"
|
||||
VERSION_BASE="${TAG_VERSION}"
|
||||
PATCH_NUMBER=0
|
||||
|
||||
# Extract date and patch from tags such as v1.2.3-1-20251113.
|
||||
if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
DATE_STR="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
else
|
||||
# Use release published date
|
||||
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
|
||||
fi
|
||||
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
|
||||
- name: Get release information
|
||||
id: release_info
|
||||
run: |
|
||||
# Use jq to properly escape JSON
|
||||
RELEASE_BODY="${{ github.event.release.body }}"
|
||||
RELEASE_NAME="${{ github.event.release.name }}"
|
||||
|
||||
# Handle empty values
|
||||
if [ -z "$RELEASE_BODY" ]; then
|
||||
RELEASE_BODY=""
|
||||
fi
|
||||
if [ -z "$RELEASE_NAME" ]; then
|
||||
RELEASE_NAME=""
|
||||
fi
|
||||
|
||||
# Save to temporary files for proper handling
|
||||
echo -n "$RELEASE_BODY" > /tmp/release_body.txt
|
||||
echo -n "$RELEASE_NAME" > /tmp/release_name.txt
|
||||
|
||||
# Use jq to escape JSON strings
|
||||
RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt)
|
||||
RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt)
|
||||
|
||||
echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT
|
||||
echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download current version.json from server
|
||||
id: download_version
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Try to download current version.json from server
|
||||
curl -f -s "https://version.crossdesk.cn/version.json" -o version.json || echo "Failed to download, will create new one"
|
||||
|
||||
- name: Generate or update version.json
|
||||
run: |
|
||||
# If version.json exists, try to preserve downloads section
|
||||
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
|
||||
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
|
||||
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
|
||||
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
|
||||
else
|
||||
DOWNLOADS_JSON=""
|
||||
fi
|
||||
else
|
||||
DOWNLOADS_JSON=""
|
||||
fi
|
||||
|
||||
# If downloads is empty, use default structure
|
||||
if [ -z "$DOWNLOADS_JSON" ]; then
|
||||
DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF
|
||||
{
|
||||
"windows-x64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe",
|
||||
"filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe"
|
||||
},
|
||||
"windows-x64-portable": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip",
|
||||
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip"
|
||||
},
|
||||
"macos-x64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg",
|
||||
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg"
|
||||
},
|
||||
"macos-arm64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg",
|
||||
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg"
|
||||
},
|
||||
"linux-amd64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb",
|
||||
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb",
|
||||
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb"
|
||||
}
|
||||
}
|
||||
DOWNLOADS_EOF
|
||||
)
|
||||
fi
|
||||
|
||||
# Generate version.json using cat and heredoc
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
|
||||
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
|
||||
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
|
||||
"downloads": ${DOWNLOADS_JSON}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat version.json
|
||||
|
||||
- name: Upload version.json to server
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
with:
|
||||
switches: -avzr --delete
|
||||
path: version.json
|
||||
remote_path: /var/www/html/version/
|
||||
remote_host: ${{ secrets.SERVER_HOST }}
|
||||
remote_user: ${{ secrets.SERVER_USER }}
|
||||
remote_key: ${{ secrets.SERVER_KEY }}
|
||||
@@ -6,5 +6,4 @@ build/
|
||||
.DS_Store
|
||||
|
||||
# VSCode cache
|
||||
.vscode
|
||||
continuous-desk.code-workspace
|
||||
.vscode
|
||||
@@ -1,3 +1,3 @@
|
||||
[submodule "thirdparty/projectx"]
|
||||
path = thirdparty/projectx
|
||||
url = https://github.com/dijunkun/projectx.git
|
||||
[submodule "submodules/minirtc"]
|
||||
path = submodules/minirtc
|
||||
url = https://github.com/kunkundi/minirtc.git
|
||||
|
||||
@@ -1,20 +1,674 @@
|
||||
The MIT License (MIT)
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2023 The Continuous Desk Authors
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
@@ -1,67 +1,335 @@
|
||||
# Continuous Desk
|
||||
# CrossDesk
|
||||
|
||||
#### More than remote desktop
|
||||
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||
[]()
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||
[](https://github.com/kunkundi/crossdesk/actions)
|
||||
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
----
|
||||
[中文](README_CN.md) / [English](README.md)
|
||||
[ [English](README_EN.md) / 中文 ]
|
||||
|
||||

|
||||
PC 客户端
|
||||

|
||||
|
||||
# Intro
|
||||
Web 客户端
|
||||
<p align="center">
|
||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||
</p>
|
||||
|
||||
Continuous Desk is a lightweight cross-platform remote desktop. It allows multiple users to remotely control the same computer at the same time. In addition to desktop image transmission, it also supports end-to-end voice transmission, providing collaboration capabilities on the basis of remote desktop.
|
||||
## 简介
|
||||
|
||||
Continuous Desk is an experimental application of [Projectx](https://github.com/dijunkun/projectx) real-time communications library. Projectx is a lightweight cross-platform real-time communications library. It has basic capabilities such as network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding/decoding (H264), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, and network congestion control ([TCP over UDP](https://libnice.freedesktop.org/)).
|
||||
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
|
||||
|
||||
## Usage
|
||||
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264/AV1),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
|
||||
|
||||
Enter the remote desktop ID in the 'REMOTE ID' field on the menu bar, and click 'Connect' button to initiate the remote connection.
|
||||
## 系统要求
|
||||
|
||||

|
||||
| 平台 | 最低版本 |
|
||||
|----------------|---------------------------|
|
||||
| **Windows** | Windows 10 及以上 (64 位) |
|
||||
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
|
||||
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) |
|
||||
|
||||
If the remote desktop is set with a connection password, the local end needs to enter the correct password to initiate the remote connection. If the password is incorrect, an "Incorrect password" alert will appear in the status bar.
|
||||
## 使用
|
||||
|
||||

|
||||
在菜单栏“对端ID”处输入远端桌面的ID,点击“→”即可发起远程连接。
|
||||
|
||||
After connection successfully established, the status bar will display the message "ClientConnected."
|
||||

|
||||
|
||||

|
||||
如果远端桌面设置了连接密码,则本端需填写正确的连接密码才能成功发起远程连接。
|
||||
|
||||
## How to build
|
||||

|
||||
|
||||
Requirements:
|
||||
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
|
||||

|
||||
|
||||
### Web 客户端
|
||||
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
|
||||
输入 **远程设备 ID** 与 **密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**:
|
||||
|
||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||
|
||||
### Windows 服务(CrossDesk Service)
|
||||
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
||||
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
||||
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
||||
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||
|
||||
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||
|
||||
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||
```
|
||||
# 安装
|
||||
.\CrossDesk.exe --service-install
|
||||
# 启动
|
||||
.\CrossDesk.exe --service-start
|
||||
# 查看状态
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-stop
|
||||
# 卸载
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||
|
||||
## 如何编译
|
||||
|
||||
依赖:
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
- [vcpkg](https://vcpkg.io/en/getting-started)
|
||||
|
||||
Following packages need to be installed on Linux:
|
||||
### Linux
|
||||
|
||||
```
|
||||
sudo apt-get install -y nvidia-cuda-toolkit libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev libxv-dev libasound2-dev libsndio-dev libasound2-dev libpulse-dev
|
||||
Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
```
|
||||
|
||||
Commands:
|
||||
```
|
||||
git clone https://github.com/dijunkun/continuous-desk
|
||||
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
|
||||
|
||||
cd continuous-desk
|
||||
|
||||
git submodule init
|
||||
|
||||
git submodule update
|
||||
|
||||
xmake b remote_desk
|
||||
```
|
||||
Run:
|
||||
```
|
||||
# Windows/MacOS
|
||||
xmake r remote_desk
|
||||
|
||||
# root privileges are required on Linux
|
||||
./remote_desk
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
下载子模块并编译 Release 版本:
|
||||
|
||||
Continuous Desk is licenced under MIT, and some third-party libraries are distributed under their licenses.
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
|
||||
cd crossdesk
|
||||
|
||||
# 已经克隆过仓库时执行这一行
|
||||
git submodule update --init --recursive
|
||||
|
||||
xmake f -m release --USE_CUDA=false -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
|
||||
|
||||
启用 Wayland 和 DRM 的配置示例:
|
||||
|
||||
```bash
|
||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
生成 Debian 安装包(必须在对应架构上先完成 Release 编译):
|
||||
|
||||
```bash
|
||||
# amd64
|
||||
./scripts/linux/pkg_amd64.sh 1.0.0
|
||||
|
||||
# arm64
|
||||
./scripts/linux/pkg_arm64.sh 1.0.0
|
||||
```
|
||||
|
||||
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
||||
|
||||
### 通用编译选项
|
||||
|
||||
```text
|
||||
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
|
||||
--USE_WAYLAND=true/false: 在 Linux 上启用 Wayland/PipeWire 捕获,默认不启用
|
||||
--USE_DRM=true/false: 在 Linux 上启用 DRM 捕获,默认不启用
|
||||
--CROSSDESK_PORTABLE=true/false: 构建便携版本,默认不启用
|
||||
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
|
||||
|
||||
# 示例
|
||||
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
xmake r crossdesk
|
||||
```
|
||||
|
||||
### 无 CUDA 环境下的开发支持
|
||||
|
||||
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
|
||||
|
||||
进入容器,下载工程后执行:
|
||||
```
|
||||
export CUDA_PATH=/usr/local/cuda
|
||||
export XMAKE_GLOBALDIR=/data
|
||||
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b --root -vy crossdesk
|
||||
```
|
||||
|
||||
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
|
||||
```
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
```
|
||||
安装完成后执行:
|
||||
```
|
||||
xmake require --info "cuda 12.6.3"
|
||||
```
|
||||
输出如下:
|
||||
|
||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||
|
||||
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
|
||||
```
|
||||
set CUDA_PATH=path_to_cuda_installdir
|
||||
```
|
||||
重新执行:
|
||||
```
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 注意
|
||||
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
|
||||
|
||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||
|
||||
## 关于 Xmake
|
||||
|
||||
#### 安装 Xmake
|
||||
使用 curl:
|
||||
```
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
```
|
||||
使用 wget:
|
||||
```
|
||||
wget https://xmake.io/shget.text -O - | bash
|
||||
```
|
||||
使用 powershell:
|
||||
```
|
||||
irm https://xmake.io/psget.text | iex
|
||||
```
|
||||
|
||||
#### 编译选项
|
||||
```
|
||||
# 切换编译模式
|
||||
xmake f -m debug/release
|
||||
|
||||
# 可选编译参数
|
||||
-r :重新构建目标
|
||||
-v :显示详细的构建日志
|
||||
-y :自动确认提示
|
||||
|
||||
# 示例
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 运行选项
|
||||
```
|
||||
# 使用调试模式运行
|
||||
xmake r -d crossdesk
|
||||
```
|
||||
更多使用方法可参考 [Xmake官方文档](https://xmake.io/guide/quick-start.html) 。
|
||||
|
||||
## 自托管服务器
|
||||
推荐使用Docker部署CrossDesk Server。
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e CROSSDESK_SERVER_PORT=xxxx \
|
||||
-e COTURN_PORT=xxxx \
|
||||
-e MIN_PORT=xxxxx \
|
||||
-e MAX_PORT=xxxxx \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
上述命令中,用户需注意的参数如下:
|
||||
|
||||
**参数**
|
||||
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
|
||||
- INTERNAL_IP:服务器内网 IP
|
||||
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
|
||||
- COTURN_PORT: COTURN 服务使用的端口, 对应 CrossDesk 客户端**自托管服务器配置**中填写的**中继服务端口**
|
||||
- MIN_PORT/MAX_PORT:COTURN 服务使用的端口范围,例如:MIN_PORT=50000, MAX_PORT=60000,范围可根据客户端数量调整。
|
||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
|
||||
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=114.114.114.114 \
|
||||
-e INTERNAL_IP=10.0.0.1 \
|
||||
-e CROSSDESK_SERVER_PORT=9099 \
|
||||
-e COTURN_PORT=3478 \
|
||||
-e MIN_PORT=50000 \
|
||||
-e MAX_PORT=60000 \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
|
||||
- 如果不挂载 volume,容器删除后数据会丢失
|
||||
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
|
||||
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
|
||||
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
|
||||
|
||||
**权限注意**:如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
|
||||
- 证书生成失败,容器启动脚本会报错退出
|
||||
- 数据库目录创建失败,程序会抛出异常并崩溃
|
||||
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
|
||||
|
||||
**解决方案**:在启动容器前手动设置权限:
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
|
||||
### 客户端
|
||||
1. 点击右上角设置进入设置页面。<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||
|
||||
2. 点击`自托管服务器配置`按钮。<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
|
||||
|
||||
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
|
||||
|
||||
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
|
||||
|
||||
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
|
||||
|
||||
Windows 平台使用**管理员权限**打开 PowerShell 执行
|
||||
```
|
||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||
```
|
||||
Linux
|
||||
```
|
||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
macOS
|
||||
```
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||
```
|
||||
|
||||
### Web 客户端
|
||||
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
# 常见问题
|
||||
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
|
||||
|
||||
# 致谢
|
||||
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
|
||||
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
|
||||
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# Continuous Desk
|
||||
|
||||
#### 不止远程桌面
|
||||
|
||||
----
|
||||
[English](README.md) / [中文](README_CN.md)
|
||||
|
||||

|
||||
|
||||
## 简介
|
||||
|
||||
Continuous Desk 是一个轻量级的跨平台远程桌面软件。它允许多个用户在同一时间远程操控同一台电脑。除桌面图像传输外,它还支持端到端的语音传输,在远程桌面基础上提供额外的协作能力。
|
||||
|
||||
Continuous Desk 是 [Projectx](https://github.com/dijunkun/projectx) 实时音视频传输库的实验性应用。Projectx 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制([TCP over UDP](https://libnice.freedesktop.org/))等基础能力。
|
||||
|
||||
|
||||
## 使用
|
||||
|
||||
在菜单栏“REMOTE ID”处输入远端桌面的ID,点击“Connect”即可发起远程连接。
|
||||
|
||||

|
||||
|
||||
如果远端桌面设置了连接密码,则本端需填写正确的连接密码才能成功发起远程连接。密码错误时,状态栏会出现“Incorrect password”告警提示。
|
||||
|
||||

|
||||
|
||||
连接成功建立后,状态栏会有“ClientConnected”相关字样。
|
||||
|
||||

|
||||
|
||||
## 编译
|
||||
|
||||
依赖:
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
- [vcpkg](https://vcpkg.io/en/getting-started)
|
||||
|
||||
Linux环境下需安装以下包:
|
||||
|
||||
```
|
||||
sudo apt-get install -y nvidia-cuda-toolkit libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev libxv-dev libasound2-dev libsndio-dev libasound2-dev libpulse-dev
|
||||
```
|
||||
|
||||
编译命令
|
||||
```
|
||||
git clone https://github.com/dijunkun/continuous-desk
|
||||
|
||||
cd continuous-desk
|
||||
|
||||
git submodule init
|
||||
|
||||
git submodule update
|
||||
|
||||
xmake b remote_desk
|
||||
```
|
||||
运行
|
||||
```
|
||||
# Windows/MacOS
|
||||
xmake r remote_desk
|
||||
|
||||
# Linux下需使用root权限运行
|
||||
./remote_desk
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
Continuous Desk 使用 MIT 许可证,其中使用到的第三方库根据自身许可证进行分发。
|
||||
@@ -0,0 +1,349 @@
|
||||
# CrossDesk
|
||||
|
||||
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||
[]()
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||
[](https://github.com/kunkundi/crossdesk/actions)
|
||||
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
[ [中文](README.md) / English ]
|
||||
|
||||
PC Client
|
||||

|
||||
|
||||
Web Client
|
||||
<p align="center">
|
||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||
</p>
|
||||
|
||||
# Intro
|
||||
|
||||
CrossDesk is a lightweight cross-platform remote desktop software.
|
||||
|
||||
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
|
||||
|
||||
## System Requirements
|
||||
|
||||
| Platform | Minimum Version |
|
||||
|-----------|-----------------|
|
||||
| **Windows** | Windows 10 or later (64-bit) |
|
||||
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
|
||||
| **Linux** | Ubuntu 22.04 or later *(older versions can be built manually for compatibility)* |
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Enter the remote desktop ID in the menu bar’s “Remote ID” field and click “→” to initiate a remote connection.
|
||||
|
||||

|
||||
|
||||
If the remote desktop requires a connection password, you must enter the correct password on your side to successfully establish the connection.
|
||||
|
||||

|
||||
|
||||
Before connecting, you can customize configuration options in the settings, such as language and video encoding format.
|
||||
|
||||

|
||||
|
||||
### Web Client
|
||||
|
||||
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
|
||||
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
|
||||
|
||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||
|
||||
### Windows Service (CrossDesk Service)
|
||||
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
|
||||
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
||||
- Remote `Ctrl+Alt+Del` (SAS) delivery.
|
||||
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
|
||||
|
||||
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
|
||||
|
||||
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
|
||||
```
|
||||
# install
|
||||
.\CrossDesk.exe --service-install
|
||||
# start
|
||||
.\CrossDesk.exe --service-start
|
||||
# check status
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# stop
|
||||
.\CrossDesk.exe --service-stop
|
||||
# uninstall
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
|
||||
|
||||
## How to build
|
||||
|
||||
Requirements:
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
|
||||
### Linux
|
||||
|
||||
Linux builds currently support Ubuntu 22.04 or later on amd64 and arm64. Install the base build dependencies first:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
```
|
||||
|
||||
Install the optional dependencies when Wayland capture or DRM capture is enabled:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
|
||||
```
|
||||
|
||||
Clone the submodules and build a release binary:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
|
||||
cd crossdesk
|
||||
|
||||
# Run this when the repository has already been cloned
|
||||
git submodule update --init --recursive
|
||||
|
||||
xmake f -m release --USE_CUDA=false -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
|
||||
|
||||
Example configuration with Wayland and DRM capture enabled:
|
||||
|
||||
```bash
|
||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
Build a Debian package after compiling a release binary on the matching architecture:
|
||||
|
||||
```bash
|
||||
# amd64
|
||||
./scripts/linux/pkg_amd64.sh 1.0.0
|
||||
|
||||
# arm64
|
||||
./scripts/linux/pkg_arm64.sh 1.0.0
|
||||
```
|
||||
|
||||
The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
|
||||
|
||||
### Common build options
|
||||
|
||||
```text
|
||||
--USE_CUDA=true/false: Enable CUDA hardware codec acceleration, disabled by default
|
||||
--USE_WAYLAND=true/false: Enable Wayland/PipeWire capture on Linux, disabled by default
|
||||
--USE_DRM=true/false: Enable DRM capture on Linux, disabled by default
|
||||
--CROSSDESK_PORTABLE=true/false: Build the portable variant, disabled by default
|
||||
--CROSSDESK_VERSION=xxx: Set the CrossDesk version
|
||||
|
||||
# Example
|
||||
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
xmake r crossdesk
|
||||
```
|
||||
|
||||
#### Development Without CUDA Environment
|
||||
|
||||
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
|
||||
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
|
||||
|
||||
After entering the container, download the project and run:
|
||||
```
|
||||
export CUDA_PATH=/usr/local/cuda
|
||||
export XMAKE_GLOBALDIR=/data
|
||||
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b --root -vy crossdesk
|
||||
```
|
||||
|
||||
For **Windows developers without a CUDA environment** installed, run the following command to install the CUDA build environment:
|
||||
```
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
```
|
||||
After the installation is complete, execute:
|
||||
```
|
||||
xmake require --info "cuda 12.6.3"
|
||||
```
|
||||
The output will look like this:
|
||||
|
||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||
|
||||
From the output above, locate the CUDA installation directory — this is the path pointed to by installdir.
|
||||
Add this path to your system environment variable CUDA_PATH, or set it in the terminal using:
|
||||
```
|
||||
set CUDA_PATH=path_to_cuda_installdir:
|
||||
```
|
||||
Then re-run:
|
||||
```
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### Notice
|
||||
If the client status bar shows **Disconnected** during runtime, please first install the client from the [CrossDesk official website](https://www.crossdesk.cn/) to ensure the required certificate files are available in the environment.
|
||||
|
||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||
|
||||
## About Xmake
|
||||
#### Installing Xmake
|
||||
|
||||
You can install Xmake using one of the following methods:
|
||||
|
||||
Using curl:
|
||||
```
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
```
|
||||
Using wget:
|
||||
```
|
||||
wget https://xmake.io/shget.text -O - | bash
|
||||
```
|
||||
Using powershell:
|
||||
```
|
||||
irm https://xmake.io/psget.text | iex
|
||||
```
|
||||
|
||||
#### Build Options
|
||||
```
|
||||
# Switch build mode
|
||||
xmake f -m debug/release
|
||||
|
||||
# Optional build parameters
|
||||
-r : Rebuild the target
|
||||
-v : Show detailed build logs
|
||||
-y : Automatically confirm prompts
|
||||
|
||||
# Example
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### Run Options
|
||||
```
|
||||
# Run in debug mode
|
||||
xmake r -d crossdesk
|
||||
```
|
||||
|
||||
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html) .
|
||||
|
||||
## Self-Hosted Server
|
||||
It is recommended to deploy CrossDesk Server using Docker.
|
||||
```
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e CROSSDESK_SERVER_PORT=xxxx \
|
||||
-e COTURN_PORT=xxxx \
|
||||
-e MIN_PORT=xxxxx \
|
||||
-e MAX_PORT=xxxxx \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
The parameters you need to pay attention to are as follows:
|
||||
|
||||
**Parameters**
|
||||
- **EXTERNAL_IP**: The server’s public IP. This corresponds to **Server Address** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||
- **INTERNAL_IP**: The server’s internal IP.
|
||||
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||
- **COTURN_PORT**: The port used by the COTURN service. This corresponds to **Relay Service Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||
- **MIN_PORT / MAX_PORT**: The port range used by the COTURN service. Example: `MIN_PORT=50000`, `MAX_PORT=60000`. Adjust the range depending on the number of clients.
|
||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
|
||||
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=114.114.114.114 \
|
||||
-e INTERNAL_IP=10.0.0.1 \
|
||||
-e CROSSDESK_SERVER_PORT=9099 \
|
||||
-e COTURN_PORT=3478 \
|
||||
-e MIN_PORT=50000 \
|
||||
-e MAX_PORT=60000 \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**Notes**
|
||||
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
|
||||
- If you don’t mount volumes, all data will be lost when the container is removed.
|
||||
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
|
||||
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
|
||||
- Log files will be created and stored at `/var/log/crossdesk/`.
|
||||
|
||||
**Permission Notice**
|
||||
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
|
||||
- Certificate generation failure, leading to startup script errors and container exit.
|
||||
- Database directory creation failure, causing the program to throw exceptions and crash.
|
||||
- Log directory creation failure, preventing logs from being written (though the program may continue running).
|
||||
|
||||
**Solution:** Manually set permissions before starting the container:
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
### Server Side
|
||||
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
|
||||
|
||||
### Client Side
|
||||
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||
|
||||
2. Click `Self-Hosted Server Configuration` button.<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
|
||||
|
||||
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
|
||||
|
||||
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
|
||||
|
||||
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
|
||||
|
||||
On Windows, open PowerShell with **administrator privileges** and execute:
|
||||
```
|
||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||
```
|
||||
Linux
|
||||
```
|
||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
macOS
|
||||
```
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||
```
|
||||
|
||||
|
||||
### Web Client
|
||||
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
# FAQ
|
||||
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
|
||||
|
||||
# Acknowledgements
|
||||
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
|
||||
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
|
||||
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
|
||||
@@ -1,19 +0,0 @@
|
||||
[signal server]
|
||||
ip = 150.158.81.30
|
||||
port = 9099
|
||||
|
||||
[stun server]
|
||||
ip = 150.158.81.30
|
||||
port = 3478
|
||||
|
||||
[turn server]
|
||||
ip = 150.158.81.30
|
||||
port = 3478
|
||||
username = dijunkun
|
||||
password = dijunkunpw
|
||||
|
||||
[hardware acceleration]
|
||||
turn_on = false
|
||||
|
||||
[av1 encoding]
|
||||
turn_on = true
|
||||
@@ -0,0 +1,153 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG XMAKE_VERSION=3.0.9
|
||||
ARG RUST_VERSION=1.92.0
|
||||
ARG RUSTUP_VERSION=1.28.2
|
||||
|
||||
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \
|
||||
org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \
|
||||
org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \
|
||||
org.opencontainers.image.licenses="GPL-3.0-only"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
TZ=Etc/UTC \
|
||||
LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
RUSTUP_HOME=/opt/rustup \
|
||||
CARGO_HOME=/opt/cargo \
|
||||
XMAKE_GLOBALDIR=/data \
|
||||
XMAKE_ROOT=y \
|
||||
CUDA_PATH=/usr/local/cuda \
|
||||
PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
automake \
|
||||
binutils \
|
||||
bison \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
curl \
|
||||
dbus \
|
||||
dpkg-dev \
|
||||
file \
|
||||
flex \
|
||||
gettext \
|
||||
git \
|
||||
libasound2-dev \
|
||||
libdbus-1-dev \
|
||||
libdrm-dev \
|
||||
libegl1-mesa-dev \
|
||||
libffi-dev \
|
||||
libfontconfig1-dev \
|
||||
libfreetype6-dev \
|
||||
libgl1-mesa-dev \
|
||||
libmount-dev \
|
||||
libpulse-dev \
|
||||
libreadline-dev \
|
||||
libsndio-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
libx11-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-shm0-dev \
|
||||
libxcb-xinerama0-dev \
|
||||
libxcb-xkb-dev \
|
||||
libxcb-xtest0-dev \
|
||||
libxcursor-dev \
|
||||
libxext-dev \
|
||||
libxfixes-dev \
|
||||
libxft-dev \
|
||||
libxi-dev \
|
||||
libxinerama-dev \
|
||||
libxkbcommon-dev \
|
||||
libxrandr-dev \
|
||||
libxrender-dev \
|
||||
libxtst-dev \
|
||||
libxv-dev \
|
||||
make \
|
||||
nasm \
|
||||
ninja-build \
|
||||
patch \
|
||||
patchelf \
|
||||
perl \
|
||||
pkg-config \
|
||||
python3 \
|
||||
python3-pip \
|
||||
unzip \
|
||||
xutils-dev \
|
||||
xz-utils \
|
||||
yasm \
|
||||
zip \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN export GITHUB_ACTIONS=true \
|
||||
&& curl -fsSL https://xmake.io/shget.text \
|
||||
| bash -s -- "v${XMAKE_VERSION}" \
|
||||
&& source ~/.xmake/profile \
|
||||
&& xmake --version | grep -F "v${XMAKE_VERSION}"
|
||||
|
||||
RUN case "${TARGETARCH}" in \
|
||||
amd64) \
|
||||
rustup_arch="x86_64-unknown-linux-gnu"; \
|
||||
rustup_sha256="20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" \
|
||||
;; \
|
||||
arm64) \
|
||||
rustup_arch="aarch64-unknown-linux-gnu"; \
|
||||
rustup_sha256="e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c" \
|
||||
;; \
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& curl --proto '=https' --tlsv1.2 --fail --show-error --location \
|
||||
"https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustup_arch}/rustup-init" \
|
||||
-o /tmp/rustup-init \
|
||||
&& echo "${rustup_sha256} /tmp/rustup-init" | sha256sum --check - \
|
||||
&& chmod 0755 /tmp/rustup-init \
|
||||
&& /tmp/rustup-init -y --no-modify-path --profile minimal \
|
||||
--default-toolchain "${RUST_VERSION}" \
|
||||
&& rm -f /tmp/rustup-init \
|
||||
&& rustc --version \
|
||||
&& cargo --version
|
||||
|
||||
WORKDIR /tmp/crossdesk-dependencies
|
||||
|
||||
COPY xmake.lua ./xmake.lua
|
||||
COPY xmake ./xmake
|
||||
COPY thirdparty ./thirdparty
|
||||
COPY submodules/xmake.lua ./submodules/xmake.lua
|
||||
COPY submodules/minirtc/xmake.lua ./submodules/minirtc/xmake.lua
|
||||
COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
|
||||
|
||||
# Configure the real project manifests so the image cache stays aligned with
|
||||
# CrossDesk's exact dependency graph. Source files are intentionally excluded
|
||||
# from the Docker context; source-only changes therefore reuse this layer.
|
||||
RUN xmake repo -u --root \
|
||||
&& xmake f -m release --USE_CUDA=true --root -y \
|
||||
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
|
||||
&& mkdir -p /workspace
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY docker/linux-build/verify-build-image.sh \
|
||||
/usr/local/bin/verify-crossdesk-build-image
|
||||
COPY docker/linux-build/verify-glibc-baseline.sh \
|
||||
/usr/local/bin/verify-crossdesk-glibc-baseline
|
||||
|
||||
RUN chmod 0755 \
|
||||
/usr/local/bin/verify-crossdesk-build-image \
|
||||
/usr/local/bin/verify-crossdesk-glibc-baseline \
|
||||
&& verify-crossdesk-build-image
|
||||
|
||||
CMD ["bash"]
|
||||
@@ -0,0 +1,41 @@
|
||||
# CrossDesk Linux 构建镜像
|
||||
|
||||
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、
|
||||
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
|
||||
|
||||
## 发布方式
|
||||
|
||||
推送影响镜像的文件到 `ci/linux-build-image` 分支时,
|
||||
`build-linux-image.yml` 会在 GitHub 原生 amd64/arm64 runner 上分别构建和验证,
|
||||
全部通过后再合并为同一个 multi-arch 镜像:
|
||||
|
||||
```text
|
||||
crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
```
|
||||
|
||||
Docker 会根据运行机器自动选择正确架构。amd64 变体以
|
||||
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建
|
||||
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。
|
||||
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容
|
||||
Ubuntu 20.04。
|
||||
|
||||
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
|
||||
工具链版本 tag 只会在两个架构都验证成功后更新。
|
||||
|
||||
## GitHub 配置
|
||||
|
||||
在仓库的 Actions secrets 中配置:
|
||||
|
||||
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名
|
||||
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
|
||||
|
||||
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
|
||||
新 tag 时应用构建先于镜像完成,常规 `build.yml` 不响应
|
||||
`ci/linux-build-image` 分支的 push。第一次启用时,应先推送该分支并等待镜像
|
||||
发布成功,再把应用构建工作流的修改合并到 `main`。
|
||||
|
||||
## 更新依赖
|
||||
|
||||
修改 Dockerfile、xmake 清单、本地包配方或子模块中的包配方后,推送该分支即可。
|
||||
升级工具链时应同时修改 Dockerfile 的版本参数、工作流中的 `STACK_TAG`,以及
|
||||
常规构建工作流引用的镜像 tag。
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED_ARCH="${EXPECTED_ARCH:-}"
|
||||
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
|
||||
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then
|
||||
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
|
||||
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then
|
||||
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTUAL_ARCH="$(dpkg --print-architecture)"
|
||||
if [[ -n "${EXPECTED_ARCH}" && "${ACTUAL_ARCH}" != "${EXPECTED_ARCH}" ]]; then
|
||||
echo "Expected architecture ${EXPECTED_ARCH}, found ${ACTUAL_ARCH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
|
||||
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
|
||||
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
|
||||
cargo --version >/dev/null
|
||||
cmake --version >/dev/null
|
||||
g++ --version >/dev/null
|
||||
pkg-config --exists xft
|
||||
|
||||
for package_path in \
|
||||
"s/slint" \
|
||||
"l/libdatachannel" \
|
||||
"o/openssl3" \
|
||||
"s/spdlog"; do
|
||||
if [[ ! -d "${XMAKE_GLOBALDIR}/.xmake/packages/${package_path}" ]]; then
|
||||
echo "Prebuilt xmake package is missing: ${package_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! find "${XMAKE_GLOBALDIR}/.xmake/packages/s/slint" \
|
||||
-type f -name 'slint-compiler*' -print -quit | grep -q .; then
|
||||
echo "The prebuilt Slint compiler is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${ACTUAL_ARCH}" == "amd64" && ! -f "${CUDA_PATH}/include/cuda.h" ]]; then
|
||||
echo "The amd64 image is missing CUDA headers under ${CUDA_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Verified CrossDesk build image: Ubuntu ${VERSION_ID}, ${ACTUAL_ARCH}, glibc ${GLIBC_VERSION}"
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
echo "Usage: $0 <ELF-file-or-directory> [maximum-glibc-version]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
INPUT_PATH="$1"
|
||||
MAXIMUM_VERSION="${2:-2.35}"
|
||||
|
||||
verify_elf() {
|
||||
local elf_file="$1"
|
||||
local version=""
|
||||
local highest_version="none"
|
||||
local -a required_versions=()
|
||||
|
||||
mapfile -t required_versions < <(
|
||||
readelf --version-info "${elf_file}" \
|
||||
| grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' \
|
||||
| sed 's/^GLIBC_//' \
|
||||
| sort -Vu \
|
||||
|| true
|
||||
)
|
||||
|
||||
for version in "${required_versions[@]}"; do
|
||||
if dpkg --compare-versions "${version}" gt "${MAXIMUM_VERSION}"; then
|
||||
echo "${elf_file} requires GLIBC_${version}; maximum allowed is GLIBC_${MAXIMUM_VERSION}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#required_versions[@]} -gt 0 ]]; then
|
||||
highest_version="${required_versions[-1]}"
|
||||
fi
|
||||
echo "Verified ${elf_file}: highest required glibc version is ${highest_version}"
|
||||
}
|
||||
|
||||
if [[ -f "${INPUT_PATH}" ]]; then
|
||||
if ! readelf -h "${INPUT_PATH}" >/dev/null 2>&1; then
|
||||
echo "Not an ELF file: ${INPUT_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
verify_elf "${INPUT_PATH}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "${INPUT_PATH}" ]]; then
|
||||
echo "File or directory does not exist: ${INPUT_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ELF_COUNT=0
|
||||
while IFS= read -r -d '' candidate; do
|
||||
if readelf -h "${candidate}" >/dev/null 2>&1; then
|
||||
verify_elf "${candidate}"
|
||||
ELF_COUNT=$((ELF_COUNT + 1))
|
||||
fi
|
||||
done < <(find "${INPUT_PATH}" -type f -print0)
|
||||
|
||||
if [[ ${ELF_COUNT} -eq 0 ]]; then
|
||||
echo "No ELF files found under ${INPUT_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,33 @@
|
||||
# 常见问题(FAQ)
|
||||
|
||||
欢迎来到 **CrossDesk 常见问题** 页面!
|
||||
这里整理了用户和开发者最常见的一些疑问。如果你没有找到答案,欢迎在 [Issues](https://github.com/kunkundi/crossdesk/issues) 中反馈。
|
||||
|
||||
---
|
||||
|
||||
### Q1. 对等连接失败
|
||||
**A:**
|
||||
打开设置,勾选 **启用中继服务** 选项,尝试重新发起连接。
|
||||
|
||||
<img width="396" height="306" alt="Image" src="https://github.com/user-attachments/assets/fd8db148-c782-4f4d-b874-8f1b2a7ec7d6" />
|
||||
|
||||
由于公共中继服务器带宽较小,连接的清晰度流畅度可能会下降,建议自建服务器。 [Issue #8](https://github.com/kunkundi/crossdesk/issues/8)
|
||||
|
||||
### Q2. Windows 无 CUDA 环境下编译
|
||||
**A:**
|
||||
运行下面的命令安装 CUDA 编译环境。
|
||||
```
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
```
|
||||
安装完成后执行
|
||||
```
|
||||
xmake require --info "cuda 12.6.3"
|
||||
```
|
||||
输出如下
|
||||
|
||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||
|
||||
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入 set CUDA_PATH=path_to_cuda_installdir,重新执行 xmake b -vy crossdesk 即可。
|
||||
[Issue #6](https://github.com/kunkundi/crossdesk/issues/6)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,207 @@
|
||||
# GUI 目录结构与职责说明
|
||||
|
||||
本文档说明 `src/gui` 的目录结构、核心类型、依赖方向和代码归属规则,用于避免 `Render`、`GuiApplication` 或 `GuiRuntime` 再次演变成职责混杂的大类。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
src/gui/
|
||||
├── render.h # 对外稳定入口 Render
|
||||
├── render.cpp # 创建 GuiApplication 并转发 Run
|
||||
├── application/ # SDL/ImGui 应用外壳
|
||||
│ ├── gui_application.h # GuiApplication 声明
|
||||
│ ├── gui_application.cpp # 初始化、主循环和清理
|
||||
│ ├── application_state.h # 窗口、交互和 UI 状态
|
||||
│ ├── sdl_event_dispatch.cpp # SDL 窗口及应用事件分发
|
||||
│ ├── sdl_events.cpp # 键盘和鼠标事件转换
|
||||
│ ├── window_lifecycle.cpp # 原生窗口及 ImGui 上下文生命周期
|
||||
│ └── window_rendering.cpp # 三类窗口的渲染流程
|
||||
├── runtime/ # 非界面的 GUI 运行时
|
||||
│ ├── gui_runtime.h/.cpp # 运行时协调接口与公共实现
|
||||
│ ├── connection_runtime.cpp # 连接、在线探测、超时及会话清理
|
||||
│ ├── windows_service_runtime.cpp # Windows 服务和安全桌面集成
|
||||
│ ├── mac_permission_runtime.mm # macOS 权限检查和系统设置调用
|
||||
│ ├── gui_state.h # ApplicationState 与 RuntimeState 组合点
|
||||
│ ├── runtime_state.h # 配置、连接、平台和通信状态
|
||||
│ ├── remote_session.h # 单个远端会话 RemoteSession
|
||||
│ ├── device_presence_cache.h # 设备在线状态缓存
|
||||
│ ├── peer_event_handler.h/.cpp # 信令和连接状态回调
|
||||
│ ├── peer_media_callbacks.cpp # 视频和音频回调
|
||||
│ ├── peer_data_callbacks.cpp # 控制、剪贴板和文件数据回调
|
||||
│ └── remote_action_codec.h/.cpp # RemoteAction 编解码
|
||||
├── features/ # 可独立演进的功能模块
|
||||
│ ├── clipboard/ # 本地与远端剪贴板同步
|
||||
│ ├── devices/ # 媒体及输入设备生命周期
|
||||
│ ├── file_transfer/ # 文件队列、发送、确认和进度
|
||||
│ ├── input/ # 键盘状态、命令和超时处理
|
||||
│ └── settings/ # 配置缓存和最近连接别名
|
||||
├── views/ # ImGui 视图实现
|
||||
│ ├── panels/ # 主窗口内嵌面板
|
||||
│ ├── toolbars/ # 标题栏、状态栏和控制栏
|
||||
│ └── windows/ # 独立窗口和模态对话框
|
||||
├── platform/ # 原生桌面平台适配
|
||||
│ └── tray/ # Windows、macOS 和 Linux 托盘
|
||||
└── assets/ # 字体、图标、布局和本地化资源
|
||||
```
|
||||
|
||||
## 分层关系
|
||||
|
||||
```text
|
||||
Render
|
||||
└── GuiApplication
|
||||
├── SDL/ImGui 生命周期
|
||||
├── views/panels / views/toolbars / views/windows
|
||||
└── GuiRuntime
|
||||
├── SessionDeviceManager
|
||||
├── KeyboardController
|
||||
├── ClipboardController
|
||||
├── FileTransferManager
|
||||
├── SettingsManager
|
||||
└── PeerEventHandler
|
||||
```
|
||||
|
||||
依赖方向应保持从上向下:
|
||||
|
||||
```text
|
||||
公开入口 → 应用层 → 运行时协调层 → 功能模块 → 底层库
|
||||
↘ 视图层
|
||||
```
|
||||
|
||||
底层功能模块不应反向依赖具体面板、工具栏或窗口。
|
||||
|
||||
## 核心类型
|
||||
|
||||
### Render
|
||||
|
||||
`Render` 是应用其他部分可见的稳定入口,只负责:
|
||||
|
||||
- 管理 `GuiApplication` 生命周期;
|
||||
- 将 `Run()` 转发给 `GuiApplication`;
|
||||
- 隔离 GUI 内部类型,避免实现细节扩散到其他模块。
|
||||
|
||||
不要向 `Render` 添加窗口状态、连接状态或业务方法。
|
||||
|
||||
### GuiApplication
|
||||
|
||||
`GuiApplication` 是 SDL/ImGui 应用外壳,负责:
|
||||
|
||||
- 初始化日志、配置、SDL 和功能模块;
|
||||
- 创建、销毁原生窗口及 ImGui 上下文;
|
||||
- 执行主事件循环;
|
||||
- 分发 SDL 事件;
|
||||
- 调用面板、工具栏和窗口的绘制方法;
|
||||
- 在程序退出时按顺序清理资源。
|
||||
|
||||
它不应实现连接协议、文件传输、剪贴板或设备控制细节。
|
||||
|
||||
### GuiRuntime
|
||||
|
||||
`GuiRuntime` 是 GUI 进程的非界面协调层,负责:
|
||||
|
||||
- 初始化 MiniRTC Peer 及回调参数;
|
||||
- 协调连接、远端会话和在线状态;
|
||||
- 持有各功能 Manager/Controller;
|
||||
- 处理跨功能模块的调用顺序;
|
||||
- 提供平台集成功能所需的运行时上下文。
|
||||
|
||||
`GuiRuntime` 应保持为协调器。新的独立功能应优先创建 Manager 或 Controller,不应直接继续堆积到 `GuiRuntime`。
|
||||
|
||||
### RemoteSession
|
||||
|
||||
`RemoteSession` 表示一个连接中或已连接的远端端点,其生命周期覆盖:
|
||||
|
||||
- MiniRTC Peer 和连接状态;
|
||||
- 视频帧、纹理和渲染区域;
|
||||
- 远端显示器和控制栏状态;
|
||||
- 音频、鼠标和键盘控制状态;
|
||||
- 文件传输状态;
|
||||
- Windows 服务及安全桌面状态。
|
||||
|
||||
`remote_sessions_` 是按远端 ID 索引的会话表。会话查找、连接和清理统一由运行时处理。
|
||||
|
||||
## 状态划分
|
||||
|
||||
状态定义按生命周期和使用范围划分:
|
||||
|
||||
| 文件 | 状态范围 |
|
||||
| --- | --- |
|
||||
| `application/application_state.h` | SDL 窗口、渲染上下文、交互标志和 UI 显示状态 |
|
||||
| `runtime/runtime_state.h` | 配置、Peer、连接表、在线探测和平台集成状态 |
|
||||
| `runtime/remote_session.h` | 单个远端会话独占的连接、媒体、控制和文件状态 |
|
||||
| `runtime/gui_state.h` | 仅组合 `ApplicationState` 与 `RuntimeState` |
|
||||
|
||||
添加状态前应先判断其生命周期:
|
||||
|
||||
- 只属于某个远端连接:放入 `RemoteSession`;
|
||||
- 只属于窗口或 UI:放入 `ApplicationState` 对应子状态;
|
||||
- 属于整个 GUI 运行期:放入 `RuntimeState` 对应子状态;
|
||||
- 只属于某个功能模块:优先作为 Manager/Controller 的私有成员。
|
||||
|
||||
## 主要运行流程
|
||||
|
||||
### 启动
|
||||
|
||||
```text
|
||||
Render::Run
|
||||
→ GuiApplication::Run
|
||||
→ 初始化路径、日志和配置
|
||||
→ 初始化 SDL 和功能模块
|
||||
→ GuiRuntime::CreateConnectionPeer
|
||||
→ 创建主窗口
|
||||
→ GuiApplication::MainLoop
|
||||
```
|
||||
|
||||
### 建立远端连接
|
||||
|
||||
```text
|
||||
remote_peer_panel
|
||||
→ GuiRuntime::ConnectTo
|
||||
→ 在线状态探测
|
||||
→ 创建或复用 RemoteSession
|
||||
→ MiniRTC JoinConnection
|
||||
→ PeerEventHandler 接收连接状态
|
||||
→ 创建串流窗口并开始渲染
|
||||
```
|
||||
|
||||
### 接收远端数据
|
||||
|
||||
```text
|
||||
MiniRTC callback
|
||||
→ PeerEventHandler
|
||||
├── peer_media_callbacks:视频、音频
|
||||
├── peer_data_callbacks:控制、文件、剪贴板
|
||||
└── peer_event_handler:信令、连接状态、网络统计
|
||||
→ 对应 Manager/Controller
|
||||
→ 更新 RemoteSession 或运行时状态
|
||||
```
|
||||
|
||||
### 退出和清理
|
||||
|
||||
```text
|
||||
SDL quit / tray exit
|
||||
→ GuiApplication::Cleanup
|
||||
→ CloseAllRemoteSessions
|
||||
→ 停止设备及后台任务
|
||||
→ 销毁 Peer
|
||||
→ 销毁 ImGui 上下文和 SDL 窗口
|
||||
→ SDL_Quit
|
||||
```
|
||||
|
||||
## 新代码归属规则
|
||||
|
||||
新增代码时按以下规则选择目录:
|
||||
|
||||
| 功能 | 目录 |
|
||||
| --- | --- |
|
||||
| SDL 初始化、事件循环、窗口生命周期 | `application/` |
|
||||
| 连接、会话、Peer 回调和平台运行时 | `runtime/` |
|
||||
| 视频、音频、鼠标、键盘设备生命周期 | `features/devices/` |
|
||||
| 键盘协议和按键状态 | `features/input/` |
|
||||
| 剪贴板同步 | `features/clipboard/` |
|
||||
| 文件传输 | `features/file_transfer/` |
|
||||
| 配置持久化 | `features/settings/` |
|
||||
| 主窗口内嵌区域 | `views/panels/` |
|
||||
| 标题栏、状态栏、控制栏 | `views/toolbars/` |
|
||||
| 独立窗口和对话框 | `views/windows/` |
|
||||
| 系统托盘 | `platform/tray/` |
|
||||
| 字体、图标、布局和本地化数据 | `assets/` |
|
||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 746 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
||||
echo "Usage: $0 <debian-arch> <xmake-arch> <version> [extra-recommendation]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
DEBIAN_ARCH="$1"
|
||||
XMAKE_ARCH="$2"
|
||||
INPUT_VERSION="$3"
|
||||
EXTRA_RECOMMENDATION="${4:-}"
|
||||
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
find_slint_runtime() {
|
||||
local needed="$1"
|
||||
local resolved=""
|
||||
|
||||
resolved="$(ldd "$BUILD_BINARY" 2>/dev/null | awk -v needed="$needed" \
|
||||
'$1 == needed && $2 == "=>" && $3 ~ /^\// { print $3; exit }')"
|
||||
if [[ -n "$resolved" && -f "$resolved" ]]; then
|
||||
echo "$resolved"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local package_roots=()
|
||||
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
|
||||
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
|
||||
fi
|
||||
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
|
||||
|
||||
local package_root=""
|
||||
local candidate=""
|
||||
for package_root in "${package_roots[@]}"; do
|
||||
[[ -d "$package_root/s/slint" ]] || continue
|
||||
candidate="$(find "$package_root/s/slint" -path "*/lib/$needed" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$INPUT_VERSION")"
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
if [[ ! "$DEB_VERSION" =~ ^[0-9][0-9A-Za-z.+:~_-]*$ ]]; then
|
||||
echo "Invalid Debian version: $DEB_VERSION" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
BUILD_BINARY="$PROJECT_ROOT/build/linux/$XMAKE_ARCH/release/crossdesk"
|
||||
if [[ ! -x "$BUILD_BINARY" ]]; then
|
||||
echo "Missing release binary: $BUILD_BINARY" >&2
|
||||
echo "Build it first with: xmake f -m release -y && xmake b -vy crossdesk" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command_name in readelf ldd dpkg-deb; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Required packaging command is missing: $command_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-deb.XXXXXX")"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
DEB_DIR="$WORK_DIR/${PKG_NAME}-${DEB_VERSION}"
|
||||
DEBIAN_DIR="$DEB_DIR/DEBIAN"
|
||||
BIN_DIR="$DEB_DIR/usr/bin"
|
||||
PRIVATE_DIR="$DEB_DIR/usr/lib/$PKG_NAME"
|
||||
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
|
||||
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
|
||||
OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
|
||||
|
||||
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
|
||||
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
|
||||
|
||||
# Slint is intentionally built as a shared library. Keep that runtime private
|
||||
# to CrossDesk instead of relying on a non-existent distribution package.
|
||||
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
|
||||
's/.*Shared library: \[\(libslint_cpp[^]]*\)\].*/\1/p' | head -n 1)"
|
||||
if [[ -n "$SLINT_NEEDED" ]]; then
|
||||
SLINT_LIBRARY="$(find_slint_runtime "$SLINT_NEEDED" || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "Unable to locate the required Slint runtime: $SLINT_NEEDED" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
|
||||
fi
|
||||
|
||||
cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
|
||||
#!/bin/sh
|
||||
private_lib_dir="/usr/lib/crossdesk"
|
||||
if [ -n "${LD_LIBRARY_PATH:-}" ]; then
|
||||
export LD_LIBRARY_PATH="$private_lib_dir:$LD_LIBRARY_PATH"
|
||||
else
|
||||
export LD_LIBRARY_PATH="$private_lib_dir"
|
||||
fi
|
||||
exec /usr/lib/crossdesk/crossdesk-bin "$@"
|
||||
EOF
|
||||
chmod 0755 "$BIN_DIR/$PKG_NAME"
|
||||
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
|
||||
|
||||
for size in 16 24 32 48 64 96 128 256; do
|
||||
install -d "$ICON_BASE_DIR/${size}x${size}/apps"
|
||||
install -m 0644 "$PROJECT_ROOT/icons/linux/crossdesk_${size}x${size}.png" \
|
||||
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
|
||||
done
|
||||
|
||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS"
|
||||
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
|
||||
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
|
||||
fi
|
||||
|
||||
cat > "$DEBIAN_DIR/control" <<EOF
|
||||
Package: $PKG_NAME
|
||||
Version: $DEB_VERSION
|
||||
Architecture: $DEBIAN_ARCH
|
||||
Maintainer: $MAINTAINER
|
||||
Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.35), libstdc++6 (>= 9), libx11-6, libxext6,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
|
||||
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
||||
Recommends: $RECOMMENDS
|
||||
Priority: optional
|
||||
Section: utils
|
||||
EOF
|
||||
|
||||
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Version=$DEB_VERSION
|
||||
Name=$APP_NAME
|
||||
Comment=$DESCRIPTION
|
||||
Exec=/usr/bin/$PKG_NAME
|
||||
Icon=$PKG_NAME
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
EOF
|
||||
|
||||
rm -f "$OUTPUT_FILE"
|
||||
dpkg-deb --root-owner-group --build "$DEB_DIR" "$OUTPUT_FILE"
|
||||
dpkg-deb --info "$OUTPUT_FILE" >/dev/null
|
||||
dpkg-deb --contents "$OUTPUT_FILE" > "$WORK_DIR/package-contents.txt"
|
||||
|
||||
if [[ -n "$SLINT_NEEDED" ]] && \
|
||||
! grep -q "usr/lib/$PKG_NAME/$SLINT_NEEDED" "$WORK_DIR/package-contents.txt"; then
|
||||
echo "Packaged artifact is missing $SLINT_NEEDED" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deb package created: $OUTPUT_FILE"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec bash "$SCRIPT_DIR/package_deb.sh" amd64 x86_64 "$1" nvidia-cuda-toolkit
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec bash "$SCRIPT_DIR/package_deb.sh" arm64 arm64 "$1"
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
APP_BUNDLE="$1"
|
||||
EXPECTED_ARCH="$2"
|
||||
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
|
||||
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
|
||||
|
||||
for command_name in codesign install_name_tool lipo otool; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Required bundling command is missing: $command_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -x "$APP_EXECUTABLE" ]]; then
|
||||
echo "Missing app executable: $APP_EXECUTABLE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "App executable does not contain the expected architecture: $EXPECTED_ARCH" >&2
|
||||
lipo -archs "$APP_EXECUTABLE" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLINT_REFERENCE="$(otool -L "$APP_EXECUTABLE" | awk \
|
||||
'$1 ~ /(^|\/)libslint_cpp[^\/[:space:]]*\.dylib$/ { print $1; exit }')"
|
||||
if [[ -z "$SLINT_REFERENCE" ]]; then
|
||||
echo "The app executable does not reference the Slint runtime" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLINT_NAME="${SLINT_REFERENCE##*/}"
|
||||
SLINT_CURRENT_VERSION="$(otool -L "$APP_EXECUTABLE" | awk -v dependency="$SLINT_REFERENCE" '
|
||||
$1 == dependency {
|
||||
for (i = 1; i <= NF; ++i) {
|
||||
if ($i == "current") {
|
||||
version = $(i + 2)
|
||||
gsub(/[),]/, "", version)
|
||||
print version
|
||||
exit
|
||||
}
|
||||
}
|
||||
}')"
|
||||
|
||||
slint_runtime_version() {
|
||||
local library="$1"
|
||||
otool -L "$library" | awk -v name="$SLINT_NAME" '
|
||||
index($1, "/" name) || $1 == "@rpath/" name {
|
||||
for (i = 1; i <= NF; ++i) {
|
||||
if ($i == "current") {
|
||||
version = $(i + 2)
|
||||
gsub(/[),]/, "", version)
|
||||
print version
|
||||
exit
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
find_slint_runtime() {
|
||||
local candidate=""
|
||||
local candidate_version=""
|
||||
local package_root=""
|
||||
local package_roots=()
|
||||
|
||||
if [[ "$SLINT_REFERENCE" == /* && -f "$SLINT_REFERENCE" ]]; then
|
||||
if lipo "$SLINT_REFERENCE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "$SLINT_REFERENCE"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
|
||||
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
|
||||
fi
|
||||
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
|
||||
|
||||
for package_root in "${package_roots[@]}"; do
|
||||
[[ -d "$package_root/s/slint" ]] || continue
|
||||
while IFS= read -r candidate; do
|
||||
if ! lipo "$candidate" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
|
||||
candidate_version="$(slint_runtime_version "$candidate")"
|
||||
if [[ -n "$SLINT_CURRENT_VERSION" && "$candidate_version" != "$SLINT_CURRENT_VERSION" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$candidate"
|
||||
return 0
|
||||
done < <(find "$package_root/s/slint" -type f -path "*/lib/$SLINT_NAME" -print 2>/dev/null)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
SLINT_LIBRARY="$(find_slint_runtime || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "Unable to locate $SLINT_NAME for $EXPECTED_ARCH (current version $SLINT_CURRENT_VERSION)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d "$FRAMEWORKS_DIR"
|
||||
install -m 0755 "$SLINT_LIBRARY" "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
|
||||
install_name_tool -id "@rpath/$SLINT_NAME" "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
if [[ "$SLINT_REFERENCE" != "@rpath/$SLINT_NAME" ]]; then
|
||||
install_name_tool -change "$SLINT_REFERENCE" "@rpath/$SLINT_NAME" "$APP_EXECUTABLE"
|
||||
fi
|
||||
|
||||
if ! otool -l "$APP_EXECUTABLE" | awk '
|
||||
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
|
||||
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
|
||||
END { exit(found ? 0 : 1) }'; then
|
||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
|
||||
fi
|
||||
|
||||
# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code
|
||||
# first, then seal the complete app bundle so local packages remain launchable.
|
||||
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
codesign --force --deep --sign - "$APP_BUNDLE"
|
||||
|
||||
echo "Bundled Slint runtime: $SLINT_LIBRARY"
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
|
||||
PLATFORM="macos"
|
||||
ARCH="arm64"
|
||||
BINARY_ARCH="arm64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="14.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||
|
||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
VOL_NAME="Install ${APP_NAME_UPPER}"
|
||||
|
||||
echo "delete old files"
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
|
||||
|
||||
mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
|
||||
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
cp "${ICON_PATH}" "${RESOURCES_DIR}/crossedesk.icns"
|
||||
ICON_KEY="<key>CFBundleIconFile</key><string>crossedesk.icns</string>"
|
||||
else
|
||||
ICON_KEY=""
|
||||
fi
|
||||
|
||||
echo "generate Info.plist"
|
||||
cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${IDENTIFIER}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
${ICON_KEY}
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOS_MIN_VERSION}</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>应用需要访问摄像头</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>应用需要访问麦克风</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>应用需要发送 Apple 事件</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>应用需要录屏权限以捕获屏幕内容</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||
find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
cat > build_pkg_resources/welcome.html <<EOF
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
echo "building pkg..."
|
||||
pkgbuild \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>${INSTALLER_TITLE}</title>
|
||||
<welcome file="welcome.html" mime-type="text/html"/>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="${IDENTIFIER}"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||
<pkg-ref id="${IDENTIFIER}"/>
|
||||
</choice>
|
||||
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
EOF
|
||||
|
||||
productbuild \
|
||||
--distribution build_pkg_temp/Distribution \
|
||||
--package-path build_pkg_temp \
|
||||
--resources build_pkg_resources \
|
||||
"${PKG_NAME}"
|
||||
|
||||
echo "PKG package created: ${PKG_NAME}"
|
||||
|
||||
# Set custom icon for PKG file
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
echo "Setting custom icon for PKG file..."
|
||||
# Create a temporary iconset from icns
|
||||
TEMP_ICON_DIR=$(mktemp -d)
|
||||
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
|
||||
|
||||
# Use sips to create a png from icns for the icon
|
||||
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
|
||||
|
||||
# Method: Use osascript to set file icon (works on macOS)
|
||||
osascript <<APPLESCRIPT
|
||||
use framework "Foundation"
|
||||
use framework "AppKit"
|
||||
|
||||
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
|
||||
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
|
||||
|
||||
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
|
||||
set workspace to current application's NSWorkspace's sharedWorkspace()
|
||||
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
|
||||
APPLESCRIPT
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Custom icon set successfully for ${PKG_NAME}"
|
||||
else
|
||||
echo "Warning: Failed to set custom icon (this is optional)"
|
||||
fi
|
||||
|
||||
rm -rf "${TEMP_ICON_DIR}"
|
||||
fi
|
||||
echo "Set icon finished"
|
||||
|
||||
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||
|
||||
echo "PKG package created successfully."
|
||||
echo "package ${APP_BUNDLE}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
|
||||
PLATFORM="macos"
|
||||
ARCH="x64"
|
||||
BINARY_ARCH="x86_64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="14.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||
|
||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
VOL_NAME="Install ${APP_NAME_UPPER}"
|
||||
|
||||
echo "delete old files"
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
|
||||
|
||||
mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
|
||||
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
cp "${ICON_PATH}" "${RESOURCES_DIR}/crossedesk.icns"
|
||||
ICON_KEY="<key>CFBundleIconFile</key><string>crossedesk.icns</string>"
|
||||
else
|
||||
ICON_KEY=""
|
||||
fi
|
||||
|
||||
echo "generate Info.plist"
|
||||
cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${IDENTIFIER}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
${ICON_KEY}
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOS_MIN_VERSION}</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>应用需要访问摄像头</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>应用需要访问麦克风</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>应用需要发送 Apple 事件</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>应用需要录屏权限以捕获屏幕内容</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||
find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
cat > build_pkg_resources/welcome.html <<EOF
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
echo "building pkg..."
|
||||
pkgbuild \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>${INSTALLER_TITLE}</title>
|
||||
<welcome file="welcome.html" mime-type="text/html"/>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="${IDENTIFIER}"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||
<pkg-ref id="${IDENTIFIER}"/>
|
||||
</choice>
|
||||
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
EOF
|
||||
|
||||
productbuild \
|
||||
--distribution build_pkg_temp/Distribution \
|
||||
--package-path build_pkg_temp \
|
||||
--resources build_pkg_resources \
|
||||
"${PKG_NAME}"
|
||||
|
||||
echo "PKG package created: ${PKG_NAME}"
|
||||
|
||||
# Set custom icon for PKG file
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
echo "Setting custom icon for PKG file..."
|
||||
# Create a temporary iconset from icns
|
||||
TEMP_ICON_DIR=$(mktemp -d)
|
||||
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
|
||||
|
||||
# Use sips to create a png from icns for the icon
|
||||
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
|
||||
|
||||
# Method: Use osascript to set file icon (works on macOS)
|
||||
osascript <<APPLESCRIPT
|
||||
use framework "Foundation"
|
||||
use framework "AppKit"
|
||||
|
||||
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
|
||||
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
|
||||
|
||||
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
|
||||
set workspace to current application's NSWorkspace's sharedWorkspace()
|
||||
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
|
||||
APPLESCRIPT
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Custom icon set successfully for ${PKG_NAME}"
|
||||
else
|
||||
echo "Warning: Failed to set custom icon (this is optional)"
|
||||
fi
|
||||
|
||||
rm -rf "${TEMP_ICON_DIR}"
|
||||
fi
|
||||
echo "Set icon finished"
|
||||
|
||||
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||
|
||||
echo "PKG package created successfully."
|
||||
echo "package ${APP_BUNDLE}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
APP_IDENTIFIER="cn.crossdesk.app"
|
||||
|
||||
# Keep known historical identifiers here. tccutil only resets identifiers that
|
||||
# Launch Services can currently resolve, so path/db cleanup below remains a
|
||||
# best-effort fallback for stale entries from unsigned or removed builds.
|
||||
BUNDLE_IDENTIFIERS=(
|
||||
"cn.crossdesk.app"
|
||||
"cn.crossdesk.CrossDesk"
|
||||
"com.crossdesk.app"
|
||||
"com.crossdesk.CrossDesk"
|
||||
"com.kunkundi.crossdesk"
|
||||
"com.kunkundi.CrossDesk"
|
||||
)
|
||||
|
||||
TCC_SERVICES=(
|
||||
"ScreenCapture"
|
||||
"Accessibility"
|
||||
"Microphone"
|
||||
"AudioCapture"
|
||||
)
|
||||
|
||||
run_tccutil() {
|
||||
local user_name="$1"
|
||||
local user_id="$2"
|
||||
local service="$3"
|
||||
local bundle_id="$4"
|
||||
|
||||
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
|
||||
/bin/launchctl asuser "$user_id" \
|
||||
/usr/bin/sudo -u "$user_name" \
|
||||
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||
else
|
||||
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
reset_bundle_tcc() {
|
||||
local user_name="$1"
|
||||
local user_id="$2"
|
||||
local bundle_id
|
||||
local service
|
||||
|
||||
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
|
||||
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for service in "${TCC_SERVICES[@]}"; do
|
||||
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
cleanup_tcc_db() {
|
||||
local db_path="$1"
|
||||
|
||||
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
|
||||
DELETE FROM access
|
||||
WHERE service IN (
|
||||
'kTCCServiceScreenCapture',
|
||||
'kTCCServiceAccessibility',
|
||||
'kTCCServiceMicrophone',
|
||||
'kTCCServiceAudioCapture'
|
||||
)
|
||||
AND (
|
||||
client IN (
|
||||
'cn.crossdesk.app',
|
||||
'cn.crossdesk.CrossDesk',
|
||||
'com.crossdesk.app',
|
||||
'com.crossdesk.CrossDesk',
|
||||
'com.kunkundi.crossdesk',
|
||||
'com.kunkundi.CrossDesk'
|
||||
)
|
||||
OR lower(client) LIKE '%crossdesk%'
|
||||
);
|
||||
SQL
|
||||
}
|
||||
|
||||
cleanup_user_tcc_db() {
|
||||
local user_name="$1"
|
||||
local home_dir
|
||||
|
||||
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
|
||||
/usr/bin/awk '{print $2}')
|
||||
if [ -z "$home_dir" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
}
|
||||
|
||||
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||
if [ -n "$CONSOLE_USER" ] &&
|
||||
[ "$CONSOLE_USER" != "root" ] &&
|
||||
[ "$CONSOLE_USER" != "loginwindow" ]; then
|
||||
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
|
||||
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
|
||||
cleanup_user_tcc_db "$CONSOLE_USER"
|
||||
fi
|
||||
|
||||
# Also clear any system/root-scoped decisions as a harmless fallback.
|
||||
reset_bundle_tcc "" ""
|
||||
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
APP_BUNDLE="$1"
|
||||
EXPECTED_ARCH="$2"
|
||||
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
|
||||
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
|
||||
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
|
||||
|
||||
if [[ ! -x "$APP_EXECUTABLE" ]]; then
|
||||
echo "Missing app executable: $APP_EXECUTABLE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "Unexpected app architecture; expected $EXPECTED_ARCH" >&2
|
||||
lipo -archs "$APP_EXECUTABLE" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! otool -l "$APP_EXECUTABLE" | awk '
|
||||
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
|
||||
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
|
||||
END { exit(found ? 0 : 1) }'; then
|
||||
echo "Missing app rpath: @executable_path/../Frameworks" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_dependencies() {
|
||||
local macho="$1"
|
||||
local dependency=""
|
||||
local relative_path=""
|
||||
|
||||
while IFS= read -r dependency; do
|
||||
case "$dependency" in
|
||||
/System/Library/*|/usr/lib/*)
|
||||
;;
|
||||
@rpath/*)
|
||||
relative_path="${dependency#@rpath/}"
|
||||
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
|
||||
echo "Unbundled dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
@executable_path/../Frameworks/*)
|
||||
relative_path="${dependency#@executable_path/../Frameworks/}"
|
||||
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
|
||||
echo "Unbundled dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
@loader_path/*)
|
||||
relative_path="${dependency#@loader_path/}"
|
||||
if [[ ! -f "$(dirname "$macho")/$relative_path" ]]; then
|
||||
echo "Unbundled dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "External non-system dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done < <(otool -L "$macho" | tail -n +2 | awk '{ print $1 }')
|
||||
}
|
||||
|
||||
verify_dependencies "$APP_EXECUTABLE"
|
||||
|
||||
SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "The packaged app does not contain libslint_cpp.dylib" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! lipo "$SLINT_LIBRARY" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "Unexpected Slint runtime architecture; expected $EXPECTED_ARCH" >&2
|
||||
lipo -archs "$SLINT_LIBRARY" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
verify_dependencies "$SLINT_LIBRARY"
|
||||
|
||||
PLIST_MIN_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST")"
|
||||
BINARY_MIN_VERSION="$(otool -l "$APP_EXECUTABLE" | awk '
|
||||
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); version_min = ($2 == "LC_VERSION_MIN_MACOSX"); next }
|
||||
build_version && $1 == "minos" { print $2; exit }
|
||||
version_min && $1 == "version" { print $2; exit }')"
|
||||
if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION" ]]; then
|
||||
echo "Minimum macOS version mismatch: plist=$PLIST_MIN_VERSION binary=${BINARY_MIN_VERSION:-unknown}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
|
||||
|
||||
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,11 @@
|
||||
// Application icon resource; load by the resource name IDI_ICON1.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Portable Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,8 @@
|
||||
// Portable build resource. The app itself runs as the current user; service
|
||||
// installation raises a separate UAC prompt only when the user chooses it.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
@@ -0,0 +1,237 @@
|
||||
; Set search path
|
||||
!addincludedir "${__FILEDIR__}"
|
||||
|
||||
; Installer initial constants
|
||||
!define PRODUCT_NAME "CrossDesk"
|
||||
!define PRODUCT_VERSION "${VERSION}"
|
||||
!define PRODUCT_PUBLISHER "CrossDesk"
|
||||
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
|
||||
!define APP_NAME "CrossDesk"
|
||||
!define UNINSTALL_REG_KEY "CrossDesk"
|
||||
!define PRODUCT_SERVICE_NAME "CrossDeskService"
|
||||
|
||||
; Installer icon path
|
||||
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
|
||||
|
||||
; Compression settings
|
||||
SetCompressor /FINAL lzma
|
||||
|
||||
; Request admin privileges (needed to write HKLM)
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; ------ MUI Modern UI Definition ------
|
||||
!include "MUI.nsh"
|
||||
!define MUI_ABORTWARNING
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
; Add run-after-install option
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Run ${PRODUCT_NAME}"
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION LaunchApp
|
||||
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
|
||||
; ------ End of MUI Definition ------
|
||||
|
||||
; Include LogicLib for process handling
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||
InstallDir "$PROGRAMFILES\CrossDesk"
|
||||
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
|
||||
ShowInstDetails show
|
||||
|
||||
Section "MainSection"
|
||||
; Check if CrossDesk is running
|
||||
StrCpy $1 "CrossDesk.exe"
|
||||
|
||||
nsProcess::_FindProcess "$1"
|
||||
Pop $R0
|
||||
${If} $R0 = 0 ;
|
||||
MessageBox MB_ICONQUESTION|MB_YESNO "CrossDesk is running. Do you want to close it and continue the installation?" IDYES closeApp IDNO cancelInstall
|
||||
${Else}
|
||||
Goto installApp
|
||||
${EndIf}
|
||||
|
||||
closeApp:
|
||||
nsProcess::_KillProcess "$1"
|
||||
Pop $R0
|
||||
Sleep 500
|
||||
Goto installApp
|
||||
|
||||
cancelInstall:
|
||||
SetDetailsPrint both
|
||||
MessageBox MB_ICONEXCLAMATION|MB_OK "Installation has been aborted."
|
||||
Abort
|
||||
|
||||
installApp:
|
||||
Call StopInstalledService
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
SetOverwrite ifnewer
|
||||
|
||||
; Main application executable path
|
||||
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
|
||||
; Bundle service-side binaries required by the Windows service flow
|
||||
File "..\..\build\windows\x64\release\crossdesk_service.exe"
|
||||
File "..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
||||
; Bundle runtime DLLs from the release output directory
|
||||
File "..\..\build\windows\x64\release\*.dll"
|
||||
|
||||
Call RegisterInstalledService
|
||||
|
||||
; Write uninstall information
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
|
||||
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
|
||||
SectionEnd
|
||||
|
||||
Section -AdditionalIcons
|
||||
; Desktop shortcut
|
||||
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
|
||||
|
||||
; Start menu shortcut
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
; Check if CrossDesk is running
|
||||
StrCpy $1 "CrossDesk.exe"
|
||||
|
||||
nsProcess::_FindProcess "$1"
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
MessageBox MB_ICONQUESTION|MB_YESNO "CrossDesk is running. Do you want to close it and uninstall?" IDYES closeApp IDNO cancelUninstall
|
||||
${Else}
|
||||
Goto uninstallApp
|
||||
${EndIf}
|
||||
|
||||
closeApp:
|
||||
nsProcess::_KillProcess "$1"
|
||||
Pop $R0
|
||||
Sleep 500
|
||||
Goto uninstallApp
|
||||
|
||||
cancelUninstall:
|
||||
SetDetailsPrint both
|
||||
MessageBox MB_ICONEXCLAMATION|MB_OK "Uninstallation has been aborted."
|
||||
Abort
|
||||
|
||||
uninstallApp:
|
||||
Call un.UnregisterInstalledService
|
||||
|
||||
; Delete main executable and uninstaller
|
||||
Delete "$INSTDIR\CrossDesk.exe"
|
||||
Delete "$INSTDIR\crossdesk_service.exe"
|
||||
Delete "$INSTDIR\crossdesk_session_helper.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Recursively delete installation directory
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
; Delete desktop and start menu shortcuts
|
||||
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
|
||||
|
||||
; Delete registry uninstall entry
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}"
|
||||
|
||||
; Delete remembered install dir
|
||||
DeleteRegKey HKCU "Software\${PRODUCT_NAME}"
|
||||
|
||||
; Recursively delete CrossDesk folder in user AppData
|
||||
RMDir /r "$APPDATA\CrossDesk"
|
||||
RMDir /r "$LOCALAPPDATA\CrossDesk"
|
||||
SectionEnd
|
||||
|
||||
; ------ Functions ------
|
||||
Function LaunchApp
|
||||
Exec "$INSTDIR\CrossDesk.exe"
|
||||
FunctionEnd
|
||||
|
||||
Function StopInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc
|
||||
|
||||
DetailPrint "Stopping existing CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
|
||||
${If} $0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
stop_with_sc:
|
||||
DetailPrint "Stopping existing CrossDesk service via Service Control Manager"
|
||||
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
${AndIf} $0 != 1062
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the existing CrossDesk service. The installation will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
Sleep 1500
|
||||
FunctionEnd
|
||||
|
||||
Function RegisterInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 missing_service_binary
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 missing_service_binary
|
||||
IfFileExists "$INSTDIR\crossdesk_session_helper.exe" 0 missing_service_binary
|
||||
|
||||
DetailPrint "Registering CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-install' $0
|
||||
${If} $0 != 0
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to register the CrossDesk service. The installation will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
DetailPrint "CrossDesk service registered for on-demand start"
|
||||
|
||||
Return
|
||||
|
||||
missing_service_binary:
|
||||
MessageBox MB_ICONSTOP|MB_OK "CrossDesk service files are missing from the installer package. The installation will be aborted."
|
||||
Abort
|
||||
FunctionEnd
|
||||
|
||||
Function un.UnregisterInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc
|
||||
|
||||
DetailPrint "Stopping CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
|
||||
${If} $0 = 0
|
||||
DetailPrint "Removing CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $0
|
||||
${If} $0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
unregister_with_sc:
|
||||
DetailPrint "Removing CrossDesk service via Service Control Manager"
|
||||
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
${AndIf} $0 != 1062
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the CrossDesk service. Uninstall will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
Sleep 1500
|
||||
|
||||
ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to remove the CrossDesk service. Uninstall will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
@@ -0,0 +1,40 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Normalize-AppVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputVersion
|
||||
)
|
||||
|
||||
$prefix = ""
|
||||
$body = $InputVersion
|
||||
|
||||
if ($body.StartsWith("v")) {
|
||||
$prefix = "v"
|
||||
$body = $body.Substring(1)
|
||||
}
|
||||
|
||||
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
|
||||
}
|
||||
|
||||
return $InputVersion
|
||||
}
|
||||
|
||||
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Push-Location $scriptDir
|
||||
try {
|
||||
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
#include "daemon.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#include <tchar.h>
|
||||
#include <windows.h>
|
||||
#elif __APPLE__
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr int kRestartDelayMs = 1000;
|
||||
#ifndef _WIN32
|
||||
constexpr int kWaitPollIntervalMs = 200;
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
// get executable file path
|
||||
static std::string GetExecutablePath() {
|
||||
#ifdef _WIN32
|
||||
char path[32768];
|
||||
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
|
||||
if (length > 0 && length < sizeof(path)) {
|
||||
return std::string(path);
|
||||
}
|
||||
#elif __APPLE__
|
||||
char path[PATH_MAX];
|
||||
uint32_t size = sizeof(path);
|
||||
if (_NSGetExecutablePath(path, &size) == 0) {
|
||||
char resolved_path[PATH_MAX];
|
||||
if (realpath(path, resolved_path) != nullptr) {
|
||||
return std::string(resolved_path);
|
||||
}
|
||||
return std::string(path);
|
||||
}
|
||||
#else
|
||||
char path[PATH_MAX];
|
||||
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
if (count != -1) {
|
||||
path[count] = '\0';
|
||||
return std::string(path);
|
||||
}
|
||||
#endif
|
||||
return "";
|
||||
}
|
||||
|
||||
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
|
||||
|
||||
void Daemon::stop() {
|
||||
running_.store(false);
|
||||
#ifndef _WIN32
|
||||
stop_requested_ = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Daemon::isRunning() const {
|
||||
#ifndef _WIN32
|
||||
return running_.load() && (stop_requested_ == 0);
|
||||
#else
|
||||
return running_.load();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Daemon::start(MainLoopFunc loop) {
|
||||
#ifdef _WIN32
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#elif __APPLE__
|
||||
// macOS: Use child process monitoring (like Windows) to preserve GUI
|
||||
stop_requested_ = 0;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#else
|
||||
// linux: Daemonize first, then run with restart monitoring
|
||||
stop_requested_ = 0;
|
||||
|
||||
// check if running from terminal before fork
|
||||
bool from_terminal =
|
||||
(isatty(STDIN_FILENO) != 0) || (isatty(STDOUT_FILENO) != 0);
|
||||
|
||||
// first fork: detach from terminal
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
std::cerr << "Failed to fork daemon process" << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (pid > 0) _exit(0);
|
||||
|
||||
if (setsid() < 0) {
|
||||
std::cerr << "Failed to create new session" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
std::cerr << "Failed to fork daemon process (second fork)" << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (pid > 0) _exit(0);
|
||||
|
||||
umask(0);
|
||||
if (chdir("/") != 0) {
|
||||
std::cerr << "Failed to change daemon working directory to /: "
|
||||
<< std::strerror(errno) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// redirect file descriptors: keep stdout/stderr if from terminal, else
|
||||
// redirect to /dev/null
|
||||
int fd = open("/dev/null", O_RDWR);
|
||||
if (fd >= 0) {
|
||||
dup2(fd, STDIN_FILENO);
|
||||
if (!from_terminal) {
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
dup2(fd, STDERR_FILENO);
|
||||
}
|
||||
if (fd > 2) close(fd);
|
||||
}
|
||||
|
||||
// set up signal handlers
|
||||
signal(SIGTERM, [](int) { stop_requested_ = 1; });
|
||||
signal(SIGINT, [](int) { stop_requested_ = 1; });
|
||||
|
||||
// ignore SIGPIPE
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) {
|
||||
try {
|
||||
loop();
|
||||
return 0; // normal exit
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Exception caught: " << e.what() << std::endl;
|
||||
return 1; // c++ exception
|
||||
} catch (...) {
|
||||
std::cerr << "Unknown exception caught" << std::endl;
|
||||
return 1; // other exception
|
||||
}
|
||||
}
|
||||
|
||||
static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) {
|
||||
__try {
|
||||
return RunLoopCatchCpp(loop);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
// catch system-level crashes (access violation, divide by zero, etc.)
|
||||
DWORD code = GetExceptionCode();
|
||||
std::cerr << "System crash detected (SEH exception code: 0x" << std::hex
|
||||
<< code << std::dec << ")" << std::endl;
|
||||
return 2; // System crash
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// run with restart logic: parent monitors child process and restarts on crash
|
||||
bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
int restart_count = 0;
|
||||
std::string exe_path = GetExecutablePath();
|
||||
if (exe_path.empty()) {
|
||||
std::cerr
|
||||
<< "Failed to get executable path, falling back to direct execution"
|
||||
<< std::endl;
|
||||
while (isRunning()) {
|
||||
try {
|
||||
loop();
|
||||
break;
|
||||
} catch (...) {
|
||||
restart_count++;
|
||||
std::cerr << "Exception caught, restarting... (attempt "
|
||||
<< restart_count << ")" << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
while (isRunning()) {
|
||||
#ifdef _WIN32
|
||||
// windows: use CreateProcess to create child process
|
||||
STARTUPINFOA si = {sizeof(si)};
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
|
||||
std::string cmd_line = "\"" + exe_path + "\" --child";
|
||||
std::vector<char> cmd_line_buf(cmd_line.begin(), cmd_line.end());
|
||||
cmd_line_buf.push_back('\0');
|
||||
|
||||
BOOL success = CreateProcessA(
|
||||
nullptr, // executable file path (specified in command line)
|
||||
cmd_line_buf.data(), // command line arguments
|
||||
nullptr, // process security attributes
|
||||
nullptr, // thread security attributes
|
||||
FALSE, // don't inherit handles
|
||||
0, // creation flags
|
||||
nullptr, // environment variables (inherit from parent)
|
||||
nullptr, // current directory
|
||||
&si, // startup info
|
||||
&pi // process information
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
std::cerr << "Failed to create child process, error: " << GetLastError()
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
restart_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (isRunning()) {
|
||||
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
|
||||
if (wait_result == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
if (wait_result == WAIT_FAILED) {
|
||||
std::cerr << "Failed waiting child process, error: " << GetLastError()
|
||||
<< std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRunning()) {
|
||||
TerminateProcess(pi.hProcess, 1);
|
||||
WaitForSingleObject(pi.hProcess, 3000);
|
||||
}
|
||||
|
||||
DWORD exit_code = 0;
|
||||
GetExitCodeProcess(pi.hProcess, &exit_code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
if (!isRunning() || exit_code == 0) {
|
||||
break; // normal exit
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "Child process exited with code " << exit_code
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
#else
|
||||
// linux: use fork + exec to create child process
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
execl(exe_path.c_str(), exe_path.c_str(), "--child", nullptr);
|
||||
_exit(1); // exec failed
|
||||
} else if (pid > 0) {
|
||||
int status = 0;
|
||||
pid_t waited_pid = -1;
|
||||
while (isRunning()) {
|
||||
waited_pid = waitpid(pid, &status, WNOHANG);
|
||||
if (waited_pid == pid) {
|
||||
break;
|
||||
}
|
||||
if (waited_pid < 0 && errno != EINTR) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(kWaitPollIntervalMs));
|
||||
}
|
||||
|
||||
if (!isRunning() && waited_pid != pid) {
|
||||
kill(pid, SIGTERM);
|
||||
waited_pid = waitpid(pid, &status, 0);
|
||||
}
|
||||
|
||||
if (waited_pid < 0) {
|
||||
if (!isRunning()) {
|
||||
break;
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "waitpid failed, errno: " << errno
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
if (!isRunning() || exit_code == 0) {
|
||||
break; // normal exit
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "Child process exited with code " << exit_code
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
if (!isRunning()) {
|
||||
break;
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
} else {
|
||||
restart_count++;
|
||||
std::cerr << "Child process exited with unknown status, restarting... "
|
||||
"(attempt "
|
||||
<< restart_count << ")" << std::endl;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
} else {
|
||||
std::cerr << "Failed to fork child process" << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
restart_count++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-11-19
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DAEMON_H_
|
||||
#define _DAEMON_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
class Daemon {
|
||||
public:
|
||||
using MainLoopFunc = std::function<void()>;
|
||||
|
||||
Daemon(const std::string& name);
|
||||
|
||||
bool start(MainLoopFunc loop);
|
||||
|
||||
void stop();
|
||||
|
||||
bool isRunning() const;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
bool runWithRestart(MainLoopFunc loop);
|
||||
|
||||
#ifndef _WIN32
|
||||
static volatile std::sig_atomic_t stop_requested_;
|
||||
#endif
|
||||
std::atomic<bool> running_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,231 @@
|
||||
#ifdef _WIN32
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
#pragma comment(linker, "/subsystem:\"console\"")
|
||||
#else
|
||||
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <cstdio>
|
||||
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
#include "config_center.h"
|
||||
#include "daemon.h"
|
||||
#include "path_manager.h"
|
||||
#include "render.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
|
||||
void EnsureConsoleForCli() {
|
||||
static bool console_ready = false;
|
||||
if (console_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
DWORD error = GetLastError();
|
||||
if (error != ERROR_ACCESS_DENIED) {
|
||||
AllocConsole();
|
||||
}
|
||||
}
|
||||
|
||||
FILE* stream = nullptr;
|
||||
freopen_s(&stream, "CONOUT$", "w", stdout);
|
||||
freopen_s(&stream, "CONOUT$", "w", stderr);
|
||||
freopen_s(&stream, "CONIN$", "r", stdin);
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
console_ready = true;
|
||||
}
|
||||
|
||||
void PrintServiceCliUsage() {
|
||||
std::cout
|
||||
<< "CrossDesk service management commands\n"
|
||||
<< " --service-install Install the sibling crossdesk_service.exe\n"
|
||||
<< " --service-uninstall Remove the installed Windows service\n"
|
||||
<< " --service-start Start the Windows service\n"
|
||||
<< " --service-stop Stop the Windows service\n"
|
||||
<< " --service-sas Ask the service to send Secure Attention "
|
||||
"Sequence\n"
|
||||
<< " --service-ping Ping the service over named pipe IPC\n"
|
||||
<< " --service-status Query service runtime status\n"
|
||||
<< " --service-help Show this help\n";
|
||||
}
|
||||
|
||||
std::wstring GetCurrentExecutablePathW() {
|
||||
wchar_t path[MAX_PATH] = {0};
|
||||
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (length == 0 || length >= MAX_PATH) {
|
||||
return L"";
|
||||
}
|
||||
return std::wstring(path, length);
|
||||
}
|
||||
|
||||
std::filesystem::path GetSiblingServiceExecutablePath() {
|
||||
std::wstring current_executable = GetCurrentExecutablePathW();
|
||||
if (current_executable.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::filesystem::path(current_executable).parent_path() /
|
||||
L"crossdesk_service.exe";
|
||||
}
|
||||
|
||||
bool IsServiceCliCommand(const char* arg) {
|
||||
if (arg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::strcmp(arg, "--service-install") == 0 ||
|
||||
std::strcmp(arg, "--service-uninstall") == 0 ||
|
||||
std::strcmp(arg, "--service-start") == 0 ||
|
||||
std::strcmp(arg, "--service-stop") == 0 ||
|
||||
std::strcmp(arg, "--service-sas") == 0 ||
|
||||
std::strcmp(arg, "--service-ping") == 0 ||
|
||||
std::strcmp(arg, "--service-status") == 0 ||
|
||||
std::strcmp(arg, "--service-help") == 0;
|
||||
}
|
||||
|
||||
void TryStartManagedWindowsService() {
|
||||
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
|
||||
if (service_path.empty() || !std::filesystem::exists(service_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!crossdesk::IsCrossDeskServiceInstalled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
crossdesk::StartCrossDeskService();
|
||||
}
|
||||
|
||||
int HandleServiceCliCommand(const std::string& command) {
|
||||
EnsureConsoleForCli();
|
||||
|
||||
if (command == "--service-help") {
|
||||
PrintServiceCliUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-install") {
|
||||
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
|
||||
if (service_path.empty()) {
|
||||
std::cerr << "Failed to locate crossdesk_service.exe" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (!std::filesystem::exists(service_path)) {
|
||||
std::cerr << "Service binary not found: " << service_path.string()
|
||||
<< std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool success = crossdesk::InstallCrossDeskService(service_path.wstring());
|
||||
std::cout << (success ? "install ok" : "install failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-uninstall") {
|
||||
bool success = crossdesk::UninstallCrossDeskService();
|
||||
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-start") {
|
||||
bool success = crossdesk::StartCrossDeskService();
|
||||
std::cout << (success ? "start ok" : "start failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-stop") {
|
||||
bool success = crossdesk::StopCrossDeskService();
|
||||
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-sas") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-ping") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-status") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
PrintServiceCliUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
#endif
|
||||
|
||||
// check if running as child process
|
||||
bool is_child = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--child") == 0) {
|
||||
is_child = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_child) {
|
||||
// child process: run render directly
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
TryStartManagedWindowsService();
|
||||
#endif
|
||||
|
||||
bool enable_daemon = false;
|
||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||
if (path_manager) {
|
||||
std::string cache_path = path_manager->GetCachePath().string();
|
||||
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
|
||||
enable_daemon = config_center.IsEnableDaemon();
|
||||
}
|
||||
|
||||
if (enable_daemon) {
|
||||
// start daemon with restart monitoring
|
||||
Daemon daemon("CrossDesk");
|
||||
|
||||
// define main loop function: run render and stop daemon on normal exit
|
||||
Daemon::MainLoopFunc main_loop = [&daemon]() {
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
daemon.stop();
|
||||
};
|
||||
|
||||
// start daemon and return result
|
||||
bool success = daemon.start(main_loop);
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
// run without daemon: direct execution
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
#include "autostart.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <limits.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <unistd.h>
|
||||
#elif defined(__linux__)
|
||||
#include <linux/limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static std::string get_home_dir() {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) {
|
||||
return "";
|
||||
}
|
||||
return std::string(home);
|
||||
}
|
||||
|
||||
static bool file_exists(const std::string& path) {
|
||||
return std::filesystem::exists(path) &&
|
||||
std::filesystem::is_regular_file(path);
|
||||
}
|
||||
|
||||
static std::string GetExecutablePath() {
|
||||
#ifdef _WIN32
|
||||
char path[32768];
|
||||
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
|
||||
if (length > 0 && length < sizeof(path)) {
|
||||
return std::string(path);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
char path[1024];
|
||||
uint32_t size = sizeof(path);
|
||||
if (_NSGetExecutablePath(path, &size) == 0) {
|
||||
char resolved_path[PATH_MAX];
|
||||
if (realpath(path, resolved_path) != nullptr) {
|
||||
return std::string(resolved_path);
|
||||
}
|
||||
return std::string(path);
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
char path[PATH_MAX];
|
||||
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
if (count != -1) {
|
||||
path[count] = '\0';
|
||||
return std::string(path);
|
||||
}
|
||||
#endif
|
||||
return "";
|
||||
}
|
||||
|
||||
// Windows
|
||||
#ifdef _WIN32
|
||||
static constexpr const char* WINDOWS_RUN_KEY =
|
||||
"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||
|
||||
static bool windows_enable(const std::string& appName,
|
||||
const std::string& exePath) {
|
||||
if (exePath.empty() || !std::filesystem::exists(exePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HKEY hKey = nullptr;
|
||||
// Use KEY_WRITE to ensure we have write permission
|
||||
LONG result =
|
||||
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_WRITE, &hKey);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string regValue = exePath;
|
||||
if (!exePath.empty() && exePath.find(' ') != std::string::npos) {
|
||||
if (exePath.front() != '"' || exePath.back() != '"') {
|
||||
regValue = "\"" + exePath + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we close the key even if RegSetValueExA fails
|
||||
result = RegSetValueExA(hKey, appName.c_str(), 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(regValue.c_str()),
|
||||
static_cast<DWORD>(regValue.size() + 1));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
static bool windows_disable(const std::string& appName) {
|
||||
HKEY hKey = nullptr;
|
||||
LONG result =
|
||||
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_WRITE, &hKey);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result = RegDeleteValueA(hKey, appName.c_str());
|
||||
RegCloseKey(hKey);
|
||||
|
||||
// Return true even if the value doesn't exist (already disabled)
|
||||
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
static bool windows_exists(const std::string& appName) {
|
||||
HKEY hKey = nullptr;
|
||||
LONG result =
|
||||
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_READ, &hKey);
|
||||
if (result != ERROR_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result = RegQueryValueExA(hKey, appName.c_str(), nullptr, nullptr, nullptr,
|
||||
nullptr);
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Linux
|
||||
#if defined(__linux__)
|
||||
static std::string linux_desktop_path(const std::string& appName) {
|
||||
std::string home = get_home_dir();
|
||||
if (home.empty()) {
|
||||
return "";
|
||||
}
|
||||
return home + "/.config/autostart/" + appName + ".desktop";
|
||||
}
|
||||
|
||||
static bool linux_enable(const std::string& appName,
|
||||
const std::string& exePath) {
|
||||
std::string home = get_home_dir();
|
||||
if (home.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path dir =
|
||||
std::filesystem::path(home) / ".config" / "autostart";
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string path = linux_desktop_path(appName);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file << "[Desktop Entry]\n";
|
||||
file << "Type=Application\n";
|
||||
file << "Exec=" << exePath << "\n";
|
||||
file << "Hidden=false\n";
|
||||
file << "NoDisplay=false\n";
|
||||
file << "X-GNOME-Autostart-enabled=true\n";
|
||||
file << "Terminal=false\n";
|
||||
file << "StartupNotify=false\n";
|
||||
file << "Name=" << appName << "\n";
|
||||
file.close();
|
||||
|
||||
return file.good();
|
||||
}
|
||||
|
||||
static bool linux_disable(const std::string& appName) {
|
||||
std::string path = linux_desktop_path(appName);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
return std::filesystem::remove(path, ec) && !ec;
|
||||
}
|
||||
|
||||
static bool linux_exists(const std::string& appName) {
|
||||
std::string path = linux_desktop_path(appName);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
return file_exists(path);
|
||||
}
|
||||
#endif
|
||||
|
||||
// macOS
|
||||
#ifdef __APPLE__
|
||||
static std::string mac_plist_path(const std::string& appName) {
|
||||
std::string home = get_home_dir();
|
||||
if (home.empty()) {
|
||||
return "";
|
||||
}
|
||||
return home + "/Library/LaunchAgents/" + appName + ".plist";
|
||||
}
|
||||
|
||||
static bool mac_enable(const std::string& appName, const std::string& exePath) {
|
||||
std::string path = mac_plist_path(appName);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure LaunchAgents directory exists
|
||||
std::filesystem::path dir =
|
||||
std::filesystem::path(get_home_dir()) / "Library" / "LaunchAgents";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file << R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>)"
|
||||
<< appName << R"(</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>)"
|
||||
<< exePath << R"(</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>)";
|
||||
file.close();
|
||||
|
||||
return file.good();
|
||||
}
|
||||
|
||||
static bool mac_disable(const std::string& appName) {
|
||||
std::string path = mac_plist_path(appName);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
return std::filesystem::remove(path, ec) && !ec;
|
||||
}
|
||||
|
||||
static bool mac_exists(const std::string& appName) {
|
||||
std::string path = mac_plist_path(appName);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
return file_exists(path);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool EnableAutostart(const std::string& appName) {
|
||||
std::string exePath = GetExecutablePath();
|
||||
if (exePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
return windows_enable(appName, exePath);
|
||||
#elif __APPLE__
|
||||
return mac_enable(appName, exePath);
|
||||
#else
|
||||
return linux_enable(appName, exePath);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DisableAutostart(const std::string& appName) {
|
||||
#ifdef _WIN32
|
||||
return windows_disable(appName);
|
||||
#elif __APPLE__
|
||||
return mac_disable(appName);
|
||||
#else
|
||||
return linux_disable(appName);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsAutostartEnabled(const std::string& appName) {
|
||||
#ifdef _WIN32
|
||||
return windows_exists(appName);
|
||||
#elif __APPLE__
|
||||
return mac_exists(appName);
|
||||
#else
|
||||
return linux_exists(appName);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-11-18
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _AUTOSTART_H_
|
||||
#define _AUTOSTART_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool EnableAutostart(const std::string& appName);
|
||||
|
||||
bool DisableAutostart(const std::string& appName);
|
||||
|
||||
bool IsAutostartEnabled(const std::string& appName);
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-05-15
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DISPLAY_INFO_H_
|
||||
#define _DISPLAY_INFO_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class DisplayInfo {
|
||||
public:
|
||||
DisplayInfo(std::string name, int left, int top, int right, int bottom)
|
||||
: name(name), left(left), top(top), right(right), bottom(bottom) {
|
||||
width = right - left;
|
||||
height = bottom - top;
|
||||
}
|
||||
DisplayInfo(void* handle, std::string name, bool is_primary, int left,
|
||||
int top, int right, int bottom)
|
||||
: handle(handle),
|
||||
name(name),
|
||||
is_primary(is_primary),
|
||||
left(left),
|
||||
top(top),
|
||||
right(right),
|
||||
bottom(bottom) {
|
||||
width = right - left;
|
||||
height = bottom - top;
|
||||
}
|
||||
~DisplayInfo() {}
|
||||
|
||||
void* handle = nullptr;
|
||||
std::string name = "";
|
||||
bool is_primary = false;
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
int right = 0;
|
||||
int bottom = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
|
||||
#define CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline std::filesystem::path PathFromUtf8(std::string_view value) {
|
||||
#if defined(__cpp_char8_t)
|
||||
const auto *begin = reinterpret_cast<const char8_t *>(value.data());
|
||||
return std::filesystem::path(std::u8string(begin, begin + value.size()));
|
||||
#else
|
||||
return std::filesystem::u8path(value.begin(), value.end());
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "platform.h"
|
||||
|
||||
#include "log.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Winsock2.h>
|
||||
@@ -19,19 +22,21 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string GetMac() {
|
||||
char mac_addr[16];
|
||||
int len = 0;
|
||||
#ifdef _WIN32
|
||||
IP_ADAPTER_INFO adapterInfo[16];
|
||||
DWORD bufferSize = sizeof(adapterInfo);
|
||||
|
||||
DWORD result = GetAdaptersInfo(adapterInfo, &bufferSize);
|
||||
if (result == ERROR_SUCCESS) {
|
||||
PIP_ADAPTER_INFO adapter = adapterInfo;
|
||||
while (adapter) {
|
||||
for (UINT i = 0; i < adapter->AddressLength; i++) {
|
||||
len += sprintf(mac_addr + len, "%.2X", adapter->Address[i]);
|
||||
len += sprintf_s(mac_addr + len, sizeof(mac_addr) - len, "%.2X",
|
||||
adapter->Address[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -39,23 +44,24 @@ std::string GetMac() {
|
||||
#elif __APPLE__
|
||||
std::string if_name = "en0";
|
||||
|
||||
struct ifaddrs *addrs;
|
||||
struct ifaddrs *cursor;
|
||||
const struct sockaddr_dl *dlAddr;
|
||||
struct ifaddrs* addrs;
|
||||
struct ifaddrs* cursor;
|
||||
const struct sockaddr_dl* dlAddr;
|
||||
|
||||
if (!getifaddrs(&addrs)) {
|
||||
cursor = addrs;
|
||||
while (cursor != 0) {
|
||||
const struct sockaddr_dl *socAddr =
|
||||
(const struct sockaddr_dl *)cursor->ifa_addr;
|
||||
const struct sockaddr_dl* socAddr =
|
||||
(const struct sockaddr_dl*)cursor->ifa_addr;
|
||||
if ((cursor->ifa_addr->sa_family == AF_LINK) &&
|
||||
(socAddr->sdl_type == IFT_ETHER) &&
|
||||
strcmp(if_name.c_str(), cursor->ifa_name) == 0) {
|
||||
dlAddr = (const struct sockaddr_dl *)cursor->ifa_addr;
|
||||
const unsigned char *base =
|
||||
(const unsigned char *)&dlAddr->sdl_data[dlAddr->sdl_nlen];
|
||||
dlAddr = (const struct sockaddr_dl*)cursor->ifa_addr;
|
||||
const unsigned char* base =
|
||||
(const unsigned char*)&dlAddr->sdl_data[dlAddr->sdl_nlen];
|
||||
for (int i = 0; i < dlAddr->sdl_alen; i++) {
|
||||
len += sprintf(mac_addr + len, "%.2X", base[i]);
|
||||
len +=
|
||||
snprintf(mac_addr + len, sizeof(mac_addr) - len, "%.2X", base[i]);
|
||||
}
|
||||
}
|
||||
cursor = cursor->ifa_next;
|
||||
@@ -76,8 +82,8 @@ std::string GetMac() {
|
||||
close(sock);
|
||||
return "";
|
||||
}
|
||||
struct ifreq *it = ifc.ifc_req;
|
||||
const struct ifreq *const end = it + (ifc.ifc_len / sizeof(struct ifreq));
|
||||
struct ifreq* it = ifc.ifc_req;
|
||||
const struct ifreq* const end = it + (ifc.ifc_len / sizeof(struct ifreq));
|
||||
for (; it != end; ++it) {
|
||||
std::strcpy(ifr.ifr_name, it->ifr_name);
|
||||
if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) {
|
||||
@@ -98,4 +104,49 @@ std::string GetMac() {
|
||||
close(sock);
|
||||
#endif
|
||||
return mac_addr;
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetHostName() {
|
||||
char hostname[256];
|
||||
#ifdef _WIN32
|
||||
WSADATA wsaData;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
||||
LOG_ERROR("WSAStartup failed");
|
||||
return "";
|
||||
}
|
||||
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
|
||||
LOG_ERROR("gethostname failed: {}", WSAGetLastError());
|
||||
WSACleanup();
|
||||
return "";
|
||||
}
|
||||
WSACleanup();
|
||||
#else
|
||||
if (gethostname(hostname, sizeof(hostname)) == -1) {
|
||||
LOG_ERROR("gethostname failed");
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
return hostname;
|
||||
}
|
||||
|
||||
bool IsWaylandSession() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
const char* session_type = std::getenv("XDG_SESSION_TYPE");
|
||||
if (session_type) {
|
||||
if (std::strcmp(session_type, "wayland") == 0 ||
|
||||
std::strcmp(session_type, "Wayland") == 0) {
|
||||
return true;
|
||||
}
|
||||
if (std::strcmp(session_type, "x11") == 0 ||
|
||||
std::strcmp(session_type, "X11") == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const char* wayland_display = std::getenv("WAYLAND_DISPLAY");
|
||||
return wayland_display && wayland_display[0] != '\0';
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
#include <iostream>
|
||||
|
||||
std::string GetMac();
|
||||
namespace crossdesk {
|
||||
|
||||
#endif
|
||||
std::string GetMac();
|
||||
std::string GetHostName();
|
||||
bool IsWaylandSession();
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "rounded_corner_button.h"
|
||||
|
||||
namespace crossdesk {
|
||||
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
|
||||
ImDrawFlags round_flags, bool enabled,
|
||||
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
|
||||
ImU32 border_col) {
|
||||
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
|
||||
if (current_window->SkipItems) return false;
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
|
||||
ImGuiID button_id = current_window->GetID(label);
|
||||
ImVec2 cursor_pos = current_window->DC.CursorPos;
|
||||
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
|
||||
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
|
||||
cursor_pos.y + button_size.y));
|
||||
ImGui::ItemSize(button_rect);
|
||||
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
|
||||
|
||||
bool is_hovered = false, is_held = false;
|
||||
bool is_pressed = false;
|
||||
if (enabled) {
|
||||
is_pressed =
|
||||
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
|
||||
}
|
||||
|
||||
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
|
||||
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
|
||||
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
|
||||
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
|
||||
|
||||
ImU32 fill_color = normal_col;
|
||||
if (is_held && is_hovered)
|
||||
fill_color = active_col;
|
||||
else if (is_hovered)
|
||||
fill_color = hover_col;
|
||||
|
||||
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
|
||||
|
||||
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
|
||||
rounding, round_flags);
|
||||
|
||||
if (style.FrameBorderSize > 0.0f) {
|
||||
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
|
||||
rounding, round_flags, style.FrameBorderSize);
|
||||
}
|
||||
|
||||
ImU32 text_color =
|
||||
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
|
||||
|
||||
const char* label_end = ImGui::FindRenderedTextEnd(label);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(text_color));
|
||||
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
|
||||
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
return is_pressed;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-26
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _ROUNDED_CORNER_BUTTON_H_
|
||||
#define _ROUNDED_CORNER_BUTTON_H_
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
namespace crossdesk {
|
||||
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
|
||||
ImDrawFlags round_flags, bool enabled = true,
|
||||
ImU32 normal_col = 0, ImU32 hover_col = 0,
|
||||
ImU32 active_col = 0, ImU32 border_col = 0);
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
#include <dbus/dbus.h>
|
||||
#endif
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex& SharedSessionMutex() {
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
SharedWaylandPortalSessionInfo& SharedSessionInfo() {
|
||||
static SharedWaylandPortalSessionInfo info;
|
||||
return info;
|
||||
}
|
||||
|
||||
bool& SharedSessionActive() {
|
||||
static bool active = false;
|
||||
return active;
|
||||
}
|
||||
|
||||
int& SharedSessionRefs() {
|
||||
static int refs = 0;
|
||||
return refs;
|
||||
}
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
|
||||
constexpr const char* kPortalSessionInterface =
|
||||
"org.freedesktop.portal.Session";
|
||||
constexpr int kPortalCloseWaitMs = 100;
|
||||
|
||||
void LogCloseDbusError(const char* action, DBusError* error) {
|
||||
if (error && dbus_error_is_set(error)) {
|
||||
LOG_ERROR("{} failed: {} ({})", action,
|
||||
error->message ? error->message : "unknown",
|
||||
error->name ? error->name : "unknown");
|
||||
} else {
|
||||
LOG_ERROR("{} failed", action);
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionClosedState {
|
||||
std::string session_handle;
|
||||
bool received = false;
|
||||
};
|
||||
|
||||
DBusHandlerResult HandleSessionClosedSignal(DBusConnection* connection,
|
||||
DBusMessage* message,
|
||||
void* user_data) {
|
||||
(void)connection;
|
||||
auto* state = static_cast<SessionClosedState*>(user_data);
|
||||
if (!state || !message) {
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
if (!dbus_message_is_signal(message, kPortalSessionInterface, "Closed")) {
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
const char* path = dbus_message_get_path(message);
|
||||
if (!path || state->session_handle != path) {
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
state->received = true;
|
||||
return DBUS_HANDLER_RESULT_HANDLED;
|
||||
}
|
||||
|
||||
bool BeginSessionClosedWatch(DBusConnection* connection,
|
||||
const std::string& session_handle,
|
||||
SessionClosedState* state,
|
||||
std::string* match_rule_out) {
|
||||
if (!connection || session_handle.empty() || !state || !match_rule_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->session_handle = session_handle;
|
||||
state->received = false;
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
const std::string match_rule =
|
||||
"type='signal',interface='" + std::string(kPortalSessionInterface) +
|
||||
"',member='Closed',path='" + session_handle + "'";
|
||||
dbus_bus_add_match(connection, match_rule.c_str(), &error);
|
||||
if (dbus_error_is_set(&error)) {
|
||||
LogCloseDbusError("dbus_bus_add_match(Session.Closed)", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
dbus_connection_add_filter(connection, HandleSessionClosedSignal, state,
|
||||
nullptr);
|
||||
*match_rule_out = match_rule;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EndSessionClosedWatch(DBusConnection* connection, SessionClosedState* state,
|
||||
const std::string& match_rule) {
|
||||
if (!connection || !state || match_rule.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_connection_remove_filter(connection, HandleSessionClosedSignal, state);
|
||||
|
||||
DBusError remove_error;
|
||||
dbus_error_init(&remove_error);
|
||||
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
|
||||
if (dbus_error_is_set(&remove_error)) {
|
||||
dbus_error_free(&remove_error);
|
||||
}
|
||||
}
|
||||
|
||||
void WaitForSessionClosed(DBusConnection* connection, SessionClosedState* state,
|
||||
int timeout_ms = kPortalCloseWaitMs) {
|
||||
if (!connection || !state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
|
||||
while (!state->received && std::chrono::steady_clock::now() < deadline) {
|
||||
dbus_connection_read_write(connection, 100);
|
||||
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PublishSharedWaylandPortalSession(
|
||||
const SharedWaylandPortalSessionInfo& info) {
|
||||
if (!info.connection || info.session_handle.empty() || info.stream_id == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(SharedSessionMutex());
|
||||
if (SharedSessionActive()) {
|
||||
const auto& active_info = SharedSessionInfo();
|
||||
if (active_info.session_handle != info.session_handle &&
|
||||
SharedSessionRefs() > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const bool same_session =
|
||||
SharedSessionActive() &&
|
||||
SharedSessionInfo().session_handle == info.session_handle;
|
||||
SharedSessionInfo() = info;
|
||||
SharedSessionActive() = true;
|
||||
if (!same_session || SharedSessionRefs() <= 0) {
|
||||
SharedSessionRefs() = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AcquireSharedWaylandPortalSession(bool require_pointer,
|
||||
SharedWaylandPortalSessionInfo* out) {
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(SharedSessionMutex());
|
||||
if (!SharedSessionActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& info = SharedSessionInfo();
|
||||
if (require_pointer && !info.pointer_granted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
++SharedSessionRefs();
|
||||
*out = info;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
|
||||
std::string* session_handle_out) {
|
||||
if (connection_out) {
|
||||
*connection_out = nullptr;
|
||||
}
|
||||
if (session_handle_out) {
|
||||
session_handle_out->clear();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(SharedSessionMutex());
|
||||
if (!SharedSessionActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SharedSessionRefs() > 0) {
|
||||
--SharedSessionRefs();
|
||||
}
|
||||
|
||||
if (SharedSessionRefs() > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (connection_out) {
|
||||
*connection_out = SharedSessionInfo().connection;
|
||||
}
|
||||
if (session_handle_out) {
|
||||
*session_handle_out = SharedSessionInfo().session_handle;
|
||||
}
|
||||
|
||||
SharedSessionInfo() = SharedWaylandPortalSessionInfo{};
|
||||
SharedSessionActive() = false;
|
||||
SharedSessionRefs() = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
|
||||
const std::string& session_handle,
|
||||
const char* close_action) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session_handle.empty()) {
|
||||
SessionClosedState close_state;
|
||||
std::string close_match_rule;
|
||||
const bool watching_closed = BeginSessionClosedWatch(
|
||||
connection, session_handle, &close_state, &close_match_rule);
|
||||
|
||||
DBusMessage* message = dbus_message_new_method_call(
|
||||
kPortalBusName, session_handle.c_str(), kPortalSessionInterface,
|
||||
"Close");
|
||||
if (message) {
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
|
||||
connection, message, 1000, &error);
|
||||
if (!reply && dbus_error_is_set(&error)) {
|
||||
LogCloseDbusError(close_action, &error);
|
||||
dbus_error_free(&error);
|
||||
}
|
||||
if (reply) {
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
dbus_message_unref(message);
|
||||
}
|
||||
|
||||
if (watching_closed) {
|
||||
WaitForSessionClosed(connection, &close_state);
|
||||
if (!close_state.received) {
|
||||
LOG_WARN("Timed out waiting for portal session to close: {}",
|
||||
session_handle);
|
||||
LOG_WARN("Forcing local teardown without waiting for Session.Closed: {}",
|
||||
session_handle);
|
||||
EndSessionClosedWatch(connection, &close_state, close_match_rule);
|
||||
} else {
|
||||
EndSessionClosedWatch(connection, &close_state, close_match_rule);
|
||||
LOG_INFO("Portal session closed: {}", session_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbus_connection_close(connection);
|
||||
dbus_connection_unref(connection);
|
||||
#else
|
||||
(void)connection;
|
||||
(void)session_handle;
|
||||
(void)close_action;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Shared Wayland portal session state used by the Linux Wayland capturer and
|
||||
* mouse controller so they can reuse one RemoteDesktop session.
|
||||
*/
|
||||
|
||||
#ifndef _WAYLAND_PORTAL_SHARED_H_
|
||||
#define _WAYLAND_PORTAL_SHARED_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct DBusConnection;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct SharedWaylandPortalSessionInfo {
|
||||
DBusConnection* connection = nullptr;
|
||||
std::string session_handle;
|
||||
uint32_t stream_id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool pointer_granted = false;
|
||||
};
|
||||
|
||||
bool PublishSharedWaylandPortalSession(
|
||||
const SharedWaylandPortalSessionInfo& info);
|
||||
bool AcquireSharedWaylandPortalSession(bool require_pointer,
|
||||
SharedWaylandPortalSessionInfo* out);
|
||||
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
|
||||
std::string* session_handle_out);
|
||||
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
|
||||
const std::string& session_handle,
|
||||
const char* close_action);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-01-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_UTIL_MAC_H_
|
||||
#define _WINDOW_UTIL_MAC_H_
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Best-effort: keep an SDL window above normal windows on macOS.
|
||||
// No-op on non-macOS builds.
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
|
||||
|
||||
// Best-effort: exclude an SDL window from the Window menu and window cycling.
|
||||
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
|
||||
// affects the Window menu and Cmd-` window cycling.
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "window_util_mac.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
|
||||
if (!window) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
|
||||
return nil;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
void* cocoa_window_ptr =
|
||||
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
|
||||
if (!cocoa_window_ptr) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge NSWindow*)cocoa_window_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)always_on_top;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep above normal windows.
|
||||
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
||||
[ns_window setLevel:level];
|
||||
|
||||
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
|
||||
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)excluded;
|
||||
return;
|
||||
}
|
||||
|
||||
[ns_window setExcludedFromWindowsMenu:excluded];
|
||||
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
|
||||
behavior |= NSWindowCollectionBehaviorTransient;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -1,38 +1,473 @@
|
||||
#include "config_center.h"
|
||||
|
||||
ConfigCenter::ConfigCenter() {}
|
||||
#include "autostart.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsValidTurnModeValue(long value) {
|
||||
return value >= static_cast<long>(ConfigCenter::TURN_MODE::DISABLED) &&
|
||||
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCenter::ConfigCenter(const std::string& config_path)
|
||||
: config_path_(config_path) {
|
||||
ini_.SetUnicode(true);
|
||||
Load();
|
||||
}
|
||||
|
||||
ConfigCenter::~ConfigCenter() {}
|
||||
|
||||
int ConfigCenter::Load() {
|
||||
SI_Error rc = ini_.LoadFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
Save();
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool persist_turn_mode_migration = false;
|
||||
|
||||
const long language_value =
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
|
||||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
|
||||
language_ = LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
language_ = static_cast<LANGUAGE>(language_value);
|
||||
}
|
||||
|
||||
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
|
||||
section_, "video_quality", static_cast<long>(video_quality_)));
|
||||
|
||||
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
|
||||
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
|
||||
|
||||
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
|
||||
ini_.GetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_)));
|
||||
|
||||
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
|
||||
hardware_video_codec_);
|
||||
|
||||
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
|
||||
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
|
||||
const long parsed_turn_mode = ini_.GetLongValue(
|
||||
section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
if (IsValidTurnModeValue(parsed_turn_mode)) {
|
||||
turn_mode_ = static_cast<TURN_MODE>(parsed_turn_mode);
|
||||
} else {
|
||||
LOG_WARN("Invalid TURN mode [{}], using auto UDP/TCP",
|
||||
parsed_turn_mode);
|
||||
turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
}
|
||||
} else {
|
||||
const bool legacy_enable_turn = ini_.GetBoolValue(
|
||||
section_, "enable_turn", turn_mode_ != TURN_MODE::DISABLED);
|
||||
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
|
||||
: TURN_MODE::DISABLED;
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
persist_turn_mode_migration = true;
|
||||
}
|
||||
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
enable_self_hosted_ =
|
||||
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
const char* signal_server_host_value =
|
||||
ini_.GetValue(section_, "signal_server_host", nullptr);
|
||||
if (signal_server_host_value != nullptr &&
|
||||
strlen(signal_server_host_value) > 0) {
|
||||
signal_server_host_ = signal_server_host_value;
|
||||
} else {
|
||||
signal_server_host_ = "";
|
||||
}
|
||||
const char* signal_server_port_value =
|
||||
ini_.GetValue(section_, "signal_server_port", nullptr);
|
||||
if (signal_server_port_value != nullptr &&
|
||||
strlen(signal_server_port_value) > 0) {
|
||||
signal_server_port_ =
|
||||
static_cast<int>(ini_.GetLongValue(section_, "signal_server_port", 0));
|
||||
} else {
|
||||
signal_server_port_ = 0;
|
||||
}
|
||||
const char* coturn_server_port_value =
|
||||
ini_.GetValue(section_, "coturn_server_port", nullptr);
|
||||
if (coturn_server_port_value != nullptr &&
|
||||
strlen(coturn_server_port_value) > 0) {
|
||||
coturn_server_port_ =
|
||||
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
|
||||
} else {
|
||||
coturn_server_port_ = 0;
|
||||
}
|
||||
|
||||
enable_autostart_ =
|
||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_minimize_to_tray_ = ini_.GetBoolValue(
|
||||
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
const char* file_transfer_save_path_value =
|
||||
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
|
||||
if (file_transfer_save_path_value != nullptr &&
|
||||
strlen(file_transfer_save_path_value) > 0) {
|
||||
file_transfer_save_path_ = file_transfer_save_path_value;
|
||||
} else {
|
||||
file_transfer_save_path_ = "";
|
||||
}
|
||||
|
||||
if (persist_turn_mode_migration &&
|
||||
ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::Save() {
|
||||
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
|
||||
ini_.SetLongValue(section_, "video_quality",
|
||||
static_cast<long>(video_quality_));
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
// only save when self hosted
|
||||
if (enable_self_hosted_) {
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
}
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// setters
|
||||
|
||||
int ConfigCenter::SetLanguage(LANGUAGE language) {
|
||||
language_ = language;
|
||||
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoQuality(VIDEO_QUALITY video_quality) {
|
||||
video_quality_ = video_quality;
|
||||
ini_.SetLongValue(section_, "video_quality",
|
||||
static_cast<long>(video_quality_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
|
||||
video_frame_rate_ = video_frame_rate;
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoEncodeFormat(
|
||||
VIDEO_ENCODE_FORMAT video_encode_format) {
|
||||
video_encode_format_ = video_encode_format;
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
|
||||
hardware_video_codec_ = hardware_video_codec;
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() { return language_; }
|
||||
int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
|
||||
if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() {
|
||||
turn_mode_ = turn_mode;
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurn(bool enable_turn) {
|
||||
if (!enable_turn) {
|
||||
return SetTurnMode(TURN_MODE::DISABLED);
|
||||
}
|
||||
if (turn_mode_ == TURN_MODE::DISABLED) {
|
||||
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
|
||||
}
|
||||
return SetTurnMode(turn_mode_);
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSrtp(bool enable_srtp) {
|
||||
enable_srtp_ = enable_srtp;
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
|
||||
signal_server_host_ = signal_server_host;
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetServerPort(int signal_server_port) {
|
||||
signal_server_port_ = signal_server_port;
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
|
||||
coturn_server_port_ = coturn_server_port;
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
enable_self_hosted_ = enable_self_hosted;
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
// load from config if self hosted is enabled
|
||||
if (enable_self_hosted_) {
|
||||
const char* signal_server_host_value =
|
||||
ini_.GetValue(section_, "signal_server_host", nullptr);
|
||||
if (signal_server_host_value != nullptr &&
|
||||
strlen(signal_server_host_value) > 0) {
|
||||
signal_server_host_ = signal_server_host_value;
|
||||
}
|
||||
const char* signal_server_port_value =
|
||||
ini_.GetValue(section_, "signal_server_port", nullptr);
|
||||
if (signal_server_port_value != nullptr &&
|
||||
strlen(signal_server_port_value) > 0) {
|
||||
signal_server_port_ = static_cast<int>(
|
||||
ini_.GetLongValue(section_, "signal_server_port", 0));
|
||||
}
|
||||
const char* coturn_server_port_value =
|
||||
ini_.GetValue(section_, "coturn_server_port", nullptr);
|
||||
if (coturn_server_port_value != nullptr &&
|
||||
strlen(coturn_server_port_value) > 0) {
|
||||
coturn_server_port_ = static_cast<int>(
|
||||
ini_.GetLongValue(section_, "coturn_server_port", 0));
|
||||
}
|
||||
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
}
|
||||
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
|
||||
enable_minimize_to_tray_ = enable_minimize_to_tray;
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetAutostart(bool enable_autostart) {
|
||||
enable_autostart_ = enable_autostart;
|
||||
bool success = false;
|
||||
if (enable_autostart) {
|
||||
success = EnableAutostart("CrossDesk");
|
||||
} else {
|
||||
success = DisableAutostart("CrossDesk");
|
||||
}
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR("SetAutostart failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetDaemon(bool enable_daemon) {
|
||||
enable_daemon_ = enable_daemon;
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
portable_service_prompt_suppressed_ = suppressed;
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// getters
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
|
||||
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() const {
|
||||
return video_quality_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() {
|
||||
ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
|
||||
return video_frame_rate_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsHardwareVideoCodec() { return hardware_video_codec_; }
|
||||
bool ConfigCenter::IsHardwareVideoCodec() const {
|
||||
return hardware_video_codec_;
|
||||
}
|
||||
|
||||
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
|
||||
return turn_mode_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableTurn() const {
|
||||
return turn_mode_ != TURN_MODE::DISABLED;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
|
||||
|
||||
std::string ConfigCenter::GetSignalServerHost() const {
|
||||
return signal_server_host_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
|
||||
|
||||
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
|
||||
|
||||
std::string ConfigCenter::GetDefaultServerHost() const {
|
||||
return signal_server_host_default_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetDefaultSignalServerPort() const {
|
||||
return server_port_default_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetDefaultCoturnServerPort() const {
|
||||
return coturn_server_port_default_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
|
||||
|
||||
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
|
||||
|
||||
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
file_transfer_save_path_ = path;
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string ConfigCenter::GetFileTransferSavePath() const {
|
||||
return file_transfer_save_path_;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,34 +7,98 @@
|
||||
#ifndef _CONFIG_CENTER_H_
|
||||
#define _CONFIG_CENTER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "SimpleIni.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ConfigCenter {
|
||||
public:
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 };
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
|
||||
enum class VIDEO_ENCODE_FORMAT { AV1 = 0, H264 = 1 };
|
||||
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
AUTO_UDP_TCP = 1,
|
||||
FORCE_UDP = 2,
|
||||
FORCE_TCP = 3
|
||||
};
|
||||
|
||||
public:
|
||||
ConfigCenter();
|
||||
explicit ConfigCenter(const std::string& config_path = "config.ini");
|
||||
~ConfigCenter();
|
||||
|
||||
public:
|
||||
// write config
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetVideoQuality(VIDEO_QUALITY video_quality);
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
int SetTurn(bool enable_turn);
|
||||
int SetSrtp(bool enable_srtp);
|
||||
int SetServerHost(const std::string& signal_server_host);
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetCoturnServerPort(int coturn_server_port);
|
||||
int SetSelfHosted(bool enable_self_hosted);
|
||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
public:
|
||||
LANGUAGE GetLanguage();
|
||||
VIDEO_QUALITY GetVideoQuality();
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat();
|
||||
bool IsHardwareVideoCodec();
|
||||
// read config
|
||||
|
||||
LANGUAGE GetLanguage() const;
|
||||
VIDEO_QUALITY GetVideoQuality() const;
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
bool IsEnableTurn() const;
|
||||
bool IsEnableSrtp() const;
|
||||
std::string GetSignalServerHost() const;
|
||||
int GetSignalServerPort() const;
|
||||
int GetCoturnServerPort() const;
|
||||
std::string GetDefaultServerHost() const;
|
||||
int GetDefaultSignalServerPort() const;
|
||||
int GetDefaultCoturnServerPort() const;
|
||||
bool IsSelfHosted() const;
|
||||
bool IsMinimizeToTray() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
int Load();
|
||||
int Save();
|
||||
|
||||
private:
|
||||
// Default value should be same with parameters in localization.h
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::MEDIUM;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::AV1;
|
||||
bool hardware_video_codec_ = false;
|
||||
};
|
||||
std::string config_path_;
|
||||
CSimpleIniA ini_;
|
||||
const char* section_ = "Settings";
|
||||
|
||||
#endif
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
bool enable_srtp_ = false;
|
||||
std::string signal_server_host_ = "";
|
||||
std::string signal_server_host_default_ = "api.crossdesk.cn";
|
||||
int signal_server_port_ = 0;
|
||||
int server_port_default_ = 9099;
|
||||
int coturn_server_port_ = 0;
|
||||
int coturn_server_port_default_ = 3478;
|
||||
bool enable_self_hosted_ = false;
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -9,36 +9,287 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
typedef enum { mouse = 0, keyboard } ControlType;
|
||||
typedef enum { move = 0, left_down, left_up, right_down, right_up } MouseFlag;
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "display_info.h"
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
typedef enum {
|
||||
mouse = 0,
|
||||
keyboard = 1,
|
||||
audio_capture = 2,
|
||||
host_infomation = 3,
|
||||
display_id = 4,
|
||||
service_status = 5,
|
||||
service_command = 6,
|
||||
keyboard_state = 7,
|
||||
} ControlType;
|
||||
typedef enum {
|
||||
move = 0,
|
||||
left_down,
|
||||
left_up,
|
||||
right_down,
|
||||
right_up,
|
||||
middle_down,
|
||||
middle_up,
|
||||
wheel_vertical,
|
||||
wheel_horizontal
|
||||
} MouseFlag;
|
||||
typedef enum { key_down = 0, key_up } KeyFlag;
|
||||
typedef enum { send_sas = 0, lock_workstation } ServiceCommandFlag;
|
||||
typedef struct {
|
||||
size_t x;
|
||||
size_t y;
|
||||
float x;
|
||||
float y;
|
||||
int s;
|
||||
MouseFlag flag;
|
||||
} Mouse;
|
||||
|
||||
typedef struct {
|
||||
size_t key_value;
|
||||
uint32_t scan_code;
|
||||
bool extended;
|
||||
KeyFlag flag;
|
||||
} Key;
|
||||
|
||||
inline constexpr size_t kMaxKeyboardStateKeys = 32;
|
||||
|
||||
typedef struct {
|
||||
size_t key_value;
|
||||
uint32_t scan_code;
|
||||
bool extended;
|
||||
} KeyboardStateKey;
|
||||
|
||||
typedef struct {
|
||||
uint32_t seq;
|
||||
size_t pressed_count;
|
||||
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
||||
} KeyboardState;
|
||||
|
||||
typedef struct {
|
||||
char host_name[64];
|
||||
size_t host_name_size;
|
||||
char** display_list;
|
||||
size_t display_num;
|
||||
int* left;
|
||||
int* top;
|
||||
int* right;
|
||||
int* bottom;
|
||||
} HostInfo;
|
||||
|
||||
typedef struct {
|
||||
bool available;
|
||||
char interactive_stage[32];
|
||||
} ServiceStatus;
|
||||
|
||||
typedef struct {
|
||||
ServiceCommandFlag flag;
|
||||
} ServiceCommand;
|
||||
|
||||
struct RemoteAction {
|
||||
ControlType type;
|
||||
union {
|
||||
Mouse m;
|
||||
Key k;
|
||||
KeyboardState ks;
|
||||
HostInfo i;
|
||||
bool a;
|
||||
int d;
|
||||
ServiceStatus ss;
|
||||
ServiceCommand c;
|
||||
};
|
||||
} RemoteAction;
|
||||
|
||||
// parse
|
||||
std::string to_json() const { return ToJson(*this); }
|
||||
|
||||
bool from_json(const std::string& json_str) {
|
||||
RemoteAction temp;
|
||||
if (!FromJson(json_str, temp)) return false;
|
||||
*this = temp;
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string ToJson(const RemoteAction& a) {
|
||||
json j;
|
||||
j["type"] = a.type;
|
||||
switch (a.type) {
|
||||
case ControlType::mouse:
|
||||
j["mouse"] = {
|
||||
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
|
||||
break;
|
||||
case ControlType::keyboard:
|
||||
j["keyboard"] = {{"key_value", a.k.key_value},
|
||||
{"scan_code", a.k.scan_code},
|
||||
{"extended", a.k.extended},
|
||||
{"flag", a.k.flag}};
|
||||
break;
|
||||
case ControlType::keyboard_state: {
|
||||
json keys = json::array();
|
||||
const size_t pressed_count =
|
||||
a.ks.pressed_count < kMaxKeyboardStateKeys
|
||||
? a.ks.pressed_count
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < pressed_count; ++idx) {
|
||||
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
|
||||
{"scan_code", a.ks.pressed_keys[idx].scan_code},
|
||||
{"extended", a.ks.pressed_keys[idx].extended}});
|
||||
}
|
||||
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
j["audio_capture"] = a.a;
|
||||
break;
|
||||
case ControlType::display_id:
|
||||
j["display_id"] = a.d;
|
||||
break;
|
||||
case ControlType::service_status:
|
||||
j["service_status"] = {{"available", a.ss.available},
|
||||
{"interactive_stage", a.ss.interactive_stage}};
|
||||
break;
|
||||
case ControlType::service_command:
|
||||
j["service_command"] = {{"flag", a.c.flag}};
|
||||
break;
|
||||
case ControlType::host_infomation: {
|
||||
json displays = json::array();
|
||||
for (size_t idx = 0; idx < a.i.display_num; idx++) {
|
||||
displays.push_back(
|
||||
{{"name", a.i.display_list ? a.i.display_list[idx] : ""},
|
||||
{"left", a.i.left ? a.i.left[idx] : 0},
|
||||
{"top", a.i.top ? a.i.top[idx] : 0},
|
||||
{"right", a.i.right ? a.i.right[idx] : 0},
|
||||
{"bottom", a.i.bottom ? a.i.bottom[idx] : 0}});
|
||||
}
|
||||
|
||||
j["host_info"] = {{"host_name", a.i.host_name},
|
||||
{"display_num", a.i.display_num},
|
||||
{"displays", displays}};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return j.dump();
|
||||
}
|
||||
|
||||
static bool FromJson(const std::string& json_str, RemoteAction& out) {
|
||||
try {
|
||||
json j = json::parse(json_str);
|
||||
out.type = (ControlType)j.at("type").get<int>();
|
||||
switch (out.type) {
|
||||
case ControlType::mouse:
|
||||
out.m.x = j.at("mouse").at("x").get<float>();
|
||||
out.m.y = j.at("mouse").at("y").get<float>();
|
||||
out.m.s = j.at("mouse").at("s").get<int>();
|
||||
out.m.flag = (MouseFlag)j.at("mouse").at("flag").get<int>();
|
||||
break;
|
||||
case ControlType::keyboard:
|
||||
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
|
||||
out.k.scan_code =
|
||||
j.at("keyboard").value("scan_code", static_cast<uint32_t>(0));
|
||||
out.k.extended = j.at("keyboard").value("extended", false);
|
||||
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
||||
break;
|
||||
case ControlType::keyboard_state: {
|
||||
const auto& keyboard_state_json = j.at("keyboard_state");
|
||||
out.ks.seq = keyboard_state_json.value("seq", 0u);
|
||||
out.ks.pressed_count = 0;
|
||||
|
||||
const auto keys_json =
|
||||
keyboard_state_json.value("pressed_keys", json::array());
|
||||
if (!keys_json.is_array()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t count =
|
||||
keys_json.size() < kMaxKeyboardStateKeys
|
||||
? keys_json.size()
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < count; ++idx) {
|
||||
const auto& key_json = keys_json[idx];
|
||||
out.ks.pressed_keys[idx].key_value =
|
||||
key_json.at("key_value").get<size_t>();
|
||||
out.ks.pressed_keys[idx].scan_code =
|
||||
key_json.value("scan_code", static_cast<uint32_t>(0));
|
||||
out.ks.pressed_keys[idx].extended =
|
||||
key_json.value("extended", false);
|
||||
}
|
||||
out.ks.pressed_count = count;
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
out.a = j.at("audio_capture").get<bool>();
|
||||
break;
|
||||
case ControlType::display_id:
|
||||
out.d = j.at("display_id").get<int>();
|
||||
break;
|
||||
case ControlType::service_status: {
|
||||
const auto& service_status_json = j.at("service_status");
|
||||
out.ss.available = service_status_json.value("available", false);
|
||||
std::string interactive_stage =
|
||||
service_status_json.value("interactive_stage", std::string());
|
||||
std::strncpy(out.ss.interactive_stage, interactive_stage.c_str(),
|
||||
sizeof(out.ss.interactive_stage) - 1);
|
||||
out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] = '\0';
|
||||
break;
|
||||
}
|
||||
case ControlType::service_command:
|
||||
out.c.flag = static_cast<ServiceCommandFlag>(
|
||||
j.at("service_command").at("flag").get<int>());
|
||||
break;
|
||||
case ControlType::host_infomation: {
|
||||
std::string host_name =
|
||||
j.at("host_info").at("host_name").get<std::string>();
|
||||
strncpy(out.i.host_name, host_name.c_str(), sizeof(out.i.host_name));
|
||||
out.i.host_name[sizeof(out.i.host_name) - 1] = '\0';
|
||||
out.i.host_name_size = host_name.size();
|
||||
|
||||
out.i.display_num = j.at("host_info").at("display_num").get<size_t>();
|
||||
auto displays = j.at("host_info").at("displays");
|
||||
|
||||
out.i.display_list =
|
||||
(char**)malloc(out.i.display_num * sizeof(char*));
|
||||
out.i.left = (int*)malloc(out.i.display_num * sizeof(int));
|
||||
out.i.top = (int*)malloc(out.i.display_num * sizeof(int));
|
||||
out.i.right = (int*)malloc(out.i.display_num * sizeof(int));
|
||||
out.i.bottom = (int*)malloc(out.i.display_num * sizeof(int));
|
||||
|
||||
for (size_t idx = 0; idx < out.i.display_num; idx++) {
|
||||
std::string name = displays[idx].at("name").get<std::string>();
|
||||
out.i.display_list[idx] = (char*)malloc(name.size() + 1);
|
||||
strcpy(out.i.display_list[idx], name.c_str());
|
||||
out.i.left[idx] = displays[idx].at("left").get<int>();
|
||||
out.i.top[idx] = displays[idx].at("top").get<int>();
|
||||
out.i.right[idx] = displays[idx].at("right").get<int>();
|
||||
out.i.bottom[idx] = displays[idx].at("bottom").get<int>();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
printf("Failed to parse RemoteAction JSON: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// int key_code, bool is_down, uint32_t scan_code, bool extended
|
||||
typedef void (*OnKeyAction)(int, bool, uint32_t, bool, void*);
|
||||
|
||||
class DeviceController {
|
||||
public:
|
||||
virtual ~DeviceController() {}
|
||||
|
||||
public:
|
||||
virtual int Init(int screen_width, int screen_height) = 0;
|
||||
virtual int Destroy() = 0;
|
||||
virtual int SendCommand(RemoteAction remote_action) = 0;
|
||||
};
|
||||
// virtual int Init(int screen_width, int screen_height);
|
||||
// virtual int Destroy();
|
||||
// virtual int SendMouseCommand(RemoteAction remote_action);
|
||||
|
||||
#endif
|
||||
// virtual int Hook();
|
||||
// virtual int Unhook();
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
#define _DEVICE_CONTROLLER_FACTORY_H_
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "keyboard_capturer.h"
|
||||
#include "mouse_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class DeviceControllerFactory {
|
||||
public:
|
||||
enum Device { Mouse = 0, Keyboard };
|
||||
@@ -23,11 +26,11 @@ class DeviceControllerFactory {
|
||||
case Mouse:
|
||||
return new MouseController();
|
||||
case Keyboard:
|
||||
return nullptr;
|
||||
return new KeyboardCapturer();
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_key_metadata.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static KeySym NormalizeKeySym(KeySym key_sym) {
|
||||
if (key_sym >= XK_a && key_sym <= XK_z) {
|
||||
return key_sym - XK_a + XK_A;
|
||||
}
|
||||
return key_sym;
|
||||
}
|
||||
|
||||
static int KeyboardEventHandler(Display* display, XEvent* event) {
|
||||
(void)display;
|
||||
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
|
||||
KeySym key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 0));
|
||||
auto key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
|
||||
if (key_it == x11KeySymToVkCode.end()) {
|
||||
key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 1));
|
||||
key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
|
||||
}
|
||||
|
||||
if (key_it == x11KeySymToVkCode.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int key_code = key_it->second;
|
||||
bool is_key_down = (event->xkey.type == KeyPress);
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
LookupWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
|
||||
if (g_on_key_action) {
|
||||
g_on_key_action(key_code, is_key_down, scan_code, extended, g_user_ptr);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer()
|
||||
: display_(nullptr),
|
||||
root_(0),
|
||||
running_(false),
|
||||
use_wayland_portal_(false),
|
||||
wayland_init_attempted_(false),
|
||||
dbus_connection_(nullptr) {
|
||||
XInitThreads();
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Failed to open X display.");
|
||||
}
|
||||
}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {
|
||||
Unhook();
|
||||
CleanupWaylandPortal();
|
||||
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display not initialized.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
|
||||
if (running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
root_ = DefaultRootWindow(display_);
|
||||
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
|
||||
XFlush(display_);
|
||||
|
||||
running_ = true;
|
||||
const int x11_fd = ConnectionNumber(display_);
|
||||
event_thread_ = std::thread([this, x11_fd]() {
|
||||
while (running_) {
|
||||
while (running_ && XPending(display_) > 0) {
|
||||
XEvent event;
|
||||
XNextEvent(display_, &event);
|
||||
KeyboardEventHandler(display_, &event);
|
||||
}
|
||||
|
||||
if (!running_) {
|
||||
break;
|
||||
}
|
||||
|
||||
struct pollfd pfd = {x11_fd, POLLIN, 0};
|
||||
int poll_ret = poll(&pfd, 1, 50);
|
||||
if (poll_ret < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
LOG_ERROR("poll for X11 events failed.");
|
||||
running_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (poll_ret == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
|
||||
LOG_ERROR("poll got invalid X11 event fd state.");
|
||||
running_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((pfd.revents & POLLIN) == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
running_ = false;
|
||||
|
||||
if (event_thread_.joinable()) {
|
||||
event_thread_.join();
|
||||
}
|
||||
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
|
||||
if (display_ && root_ != 0) {
|
||||
XSelectInput(display_, root_, 0);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (IsWaylandSession()) {
|
||||
if (!use_wayland_portal_ && !wayland_init_attempted_) {
|
||||
wayland_init_attempted_ = true;
|
||||
if (InitWaylandPortal()) {
|
||||
use_wayland_portal_ = true;
|
||||
LOG_INFO("Keyboard controller initialized with Wayland portal backend");
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Wayland keyboard control init failed, falling back to X11/XTest "
|
||||
"backend");
|
||||
}
|
||||
}
|
||||
|
||||
if (use_wayland_portal_) {
|
||||
return SendWaylandKeyboardCommand(key_code, is_down, scan_code, extended);
|
||||
}
|
||||
}
|
||||
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display not initialized.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vkCodeToX11KeySym.find(key_code) != vkCodeToX11KeySym.end()) {
|
||||
int x11_key_code = vkCodeToX11KeySym[key_code];
|
||||
KeyCode keycode = XKeysymToKeycode(display_, x11_key_code);
|
||||
XTestFakeKeyEvent(display_, keycode, is_down, CurrentTime);
|
||||
XFlush(display_);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-11-22
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KEYBOARD_CAPTURER_H_
|
||||
#define _KEYBOARD_CAPTURER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessageIter;
|
||||
struct _XDisplay;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
KeyboardCapturer();
|
||||
virtual ~KeyboardCapturer();
|
||||
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
bool InitWaylandPortal();
|
||||
void CleanupWaylandPortal();
|
||||
int SendWaylandKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
bool NotifyWaylandKeyboardKeysym(int keysym, uint32_t state);
|
||||
bool NotifyWaylandKeyboardKeycode(int keycode, uint32_t state);
|
||||
bool SendWaylandPortalVoidCall(const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>&
|
||||
append_args);
|
||||
|
||||
private:
|
||||
_XDisplay* display_;
|
||||
unsigned long root_;
|
||||
std::atomic<bool> running_;
|
||||
std::thread event_thread_;
|
||||
bool use_wayland_portal_ = false;
|
||||
bool wayland_init_attempted_ = false;
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
std::string wayland_session_handle_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,731 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
#include <dbus/dbus.h>
|
||||
#endif
|
||||
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "rd_log.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
extern std::map<int, int> vkCodeToX11KeySym;
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
namespace {
|
||||
|
||||
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
|
||||
constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop";
|
||||
constexpr const char* kPortalRemoteDesktopInterface =
|
||||
"org.freedesktop.portal.RemoteDesktop";
|
||||
constexpr const char* kPortalRequestInterface =
|
||||
"org.freedesktop.portal.Request";
|
||||
constexpr const char* kPortalRequestPathPrefix =
|
||||
"/org/freedesktop/portal/desktop/request/";
|
||||
constexpr const char* kPortalSessionPathPrefix =
|
||||
"/org/freedesktop/portal/desktop/session/";
|
||||
|
||||
constexpr uint32_t kRemoteDesktopDeviceKeyboard = 1u;
|
||||
constexpr uint32_t kKeyboardReleased = 0u;
|
||||
constexpr uint32_t kKeyboardPressed = 1u;
|
||||
|
||||
int NormalizeFallbackKeysym(int keysym) {
|
||||
if (keysym >= XK_A && keysym <= XK_Z) {
|
||||
return keysym - XK_A + XK_a;
|
||||
}
|
||||
return keysym;
|
||||
}
|
||||
|
||||
std::string MakeToken(const char* prefix) {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
return std::string(prefix) + "_" + std::to_string(now);
|
||||
}
|
||||
|
||||
void LogDbusError(const char* action, DBusError* error) {
|
||||
if (error && dbus_error_is_set(error)) {
|
||||
LOG_ERROR("{} failed: {} ({})", action,
|
||||
error->message ? error->message : "unknown",
|
||||
error->name ? error->name : "unknown");
|
||||
} else {
|
||||
LOG_ERROR("{} failed", action);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendDictEntryString(DBusMessageIter* dict, const char* key,
|
||||
const std::string& value) {
|
||||
DBusMessageIter entry;
|
||||
DBusMessageIter variant;
|
||||
const char* key_cstr = key;
|
||||
const char* value_cstr = value.c_str();
|
||||
|
||||
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
|
||||
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
|
||||
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant);
|
||||
dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value_cstr);
|
||||
dbus_message_iter_close_container(&entry, &variant);
|
||||
dbus_message_iter_close_container(dict, &entry);
|
||||
}
|
||||
|
||||
void AppendDictEntryUint32(DBusMessageIter* dict, const char* key,
|
||||
uint32_t value) {
|
||||
DBusMessageIter entry;
|
||||
DBusMessageIter variant;
|
||||
const char* key_cstr = key;
|
||||
|
||||
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
|
||||
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
|
||||
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant);
|
||||
dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &value);
|
||||
dbus_message_iter_close_container(&entry, &variant);
|
||||
dbus_message_iter_close_container(dict, &entry);
|
||||
}
|
||||
|
||||
void AppendEmptyOptionsDict(DBusMessageIter* iter) {
|
||||
DBusMessageIter options;
|
||||
dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &options);
|
||||
dbus_message_iter_close_container(iter, &options);
|
||||
}
|
||||
|
||||
bool ReadPathLikeVariant(DBusMessageIter* variant, std::string* value) {
|
||||
if (!variant || !value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int type = dbus_message_iter_get_arg_type(variant);
|
||||
if (type == DBUS_TYPE_OBJECT_PATH || type == DBUS_TYPE_STRING) {
|
||||
const char* temp = nullptr;
|
||||
dbus_message_iter_get_basic(variant, &temp);
|
||||
if (temp && temp[0] != '\0') {
|
||||
*value = temp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ReadUint32Like(DBusMessageIter* iter, uint32_t* value) {
|
||||
if (!iter || !value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int type = dbus_message_iter_get_arg_type(iter);
|
||||
if (type == DBUS_TYPE_UINT32) {
|
||||
uint32_t temp = 0;
|
||||
dbus_message_iter_get_basic(iter, &temp);
|
||||
*value = temp;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == DBUS_TYPE_INT32) {
|
||||
int32_t temp = 0;
|
||||
dbus_message_iter_get_basic(iter, &temp);
|
||||
if (temp < 0) {
|
||||
return false;
|
||||
}
|
||||
*value = static_cast<uint32_t>(temp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string BuildSessionHandleFromRequestPath(
|
||||
const std::string& request_path, const std::string& session_handle_token) {
|
||||
if (request_path.rfind(kPortalRequestPathPrefix, 0) != 0 ||
|
||||
session_handle_token.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const size_t sender_start = strlen(kPortalRequestPathPrefix);
|
||||
const size_t token_sep = request_path.find('/', sender_start);
|
||||
if (token_sep == std::string::npos || token_sep <= sender_start) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string sender =
|
||||
request_path.substr(sender_start, token_sep - sender_start);
|
||||
if (sender.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return std::string(kPortalSessionPathPrefix) + sender + "/" +
|
||||
session_handle_token;
|
||||
}
|
||||
|
||||
struct PortalResponseState {
|
||||
std::string request_path;
|
||||
bool received = false;
|
||||
DBusMessage* message = nullptr;
|
||||
};
|
||||
|
||||
DBusHandlerResult HandlePortalResponseSignal(DBusConnection* connection,
|
||||
DBusMessage* message,
|
||||
void* user_data) {
|
||||
(void)connection;
|
||||
auto* state = static_cast<PortalResponseState*>(user_data);
|
||||
if (!state || !message) {
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
if (!dbus_message_is_signal(message, kPortalRequestInterface, "Response")) {
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
const char* path = dbus_message_get_path(message);
|
||||
if (!path || state->request_path != path) {
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
if (state->message) {
|
||||
dbus_message_unref(state->message);
|
||||
state->message = nullptr;
|
||||
}
|
||||
|
||||
state->message = dbus_message_ref(message);
|
||||
state->received = true;
|
||||
return DBUS_HANDLER_RESULT_HANDLED;
|
||||
}
|
||||
|
||||
DBusMessage* WaitForPortalResponse(DBusConnection* connection,
|
||||
const std::string& request_path,
|
||||
int timeout_ms = 120000) {
|
||||
if (!connection || request_path.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PortalResponseState state;
|
||||
state.request_path = request_path;
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
|
||||
const std::string match_rule =
|
||||
"type='signal',interface='" + std::string(kPortalRequestInterface) +
|
||||
"',member='Response',path='" + request_path + "'";
|
||||
dbus_bus_add_match(connection, match_rule.c_str(), &error);
|
||||
if (dbus_error_is_set(&error)) {
|
||||
LogDbusError("dbus_bus_add_match", &error);
|
||||
dbus_error_free(&error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dbus_connection_add_filter(connection, HandlePortalResponseSignal, &state,
|
||||
nullptr);
|
||||
|
||||
auto deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
|
||||
while (!state.received && std::chrono::steady_clock::now() < deadline) {
|
||||
dbus_connection_read_write(connection, 100);
|
||||
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
|
||||
}
|
||||
}
|
||||
|
||||
dbus_connection_remove_filter(connection, HandlePortalResponseSignal, &state);
|
||||
|
||||
DBusError remove_error;
|
||||
dbus_error_init(&remove_error);
|
||||
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
|
||||
if (dbus_error_is_set(&remove_error)) {
|
||||
dbus_error_free(&remove_error);
|
||||
}
|
||||
|
||||
return state.message;
|
||||
}
|
||||
|
||||
bool ExtractRequestPath(DBusMessage* reply, std::string* request_path) {
|
||||
if (!reply || !request_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* path = nullptr;
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
const dbus_bool_t ok = dbus_message_get_args(
|
||||
reply, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
|
||||
if (!ok || !path) {
|
||||
LogDbusError("dbus_message_get_args(request_path)", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
*request_path = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code,
|
||||
DBusMessageIter* results_array) {
|
||||
if (!message || !response_code || !results_array) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter iter;
|
||||
if (!dbus_message_iter_init(message, &iter) ||
|
||||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dbus_message_iter_get_basic(&iter, response_code);
|
||||
if (!dbus_message_iter_next(&iter) ||
|
||||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*results_array = iter;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendPortalRequestAndHandleResponse(
|
||||
DBusConnection* connection, const char* interface_name,
|
||||
const char* method_name, const char* action_name,
|
||||
const std::function<bool(DBusMessage*)>& append_message_args,
|
||||
const std::function<bool(uint32_t, DBusMessageIter*)>& handle_results,
|
||||
std::string* request_path_out = nullptr) {
|
||||
if (!connection || !interface_name || interface_name[0] == '\0' ||
|
||||
!method_name || method_name[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessage* message = dbus_message_new_method_call(
|
||||
kPortalBusName, kPortalObjectPath, interface_name, method_name);
|
||||
if (!message) {
|
||||
LOG_ERROR("Failed to allocate {} message", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (append_message_args && !append_message_args(message)) {
|
||||
dbus_message_unref(message);
|
||||
LOG_ERROR("{} arguments are malformed", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
DBusMessage* reply =
|
||||
dbus_connection_send_with_reply_and_block(connection, message, -1, &error);
|
||||
dbus_message_unref(message);
|
||||
if (!reply) {
|
||||
LogDbusError(action_name ? action_name : method_name, &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string request_path;
|
||||
const bool got_request_path = ExtractRequestPath(reply, &request_path);
|
||||
dbus_message_unref(reply);
|
||||
if (!got_request_path) {
|
||||
return false;
|
||||
}
|
||||
if (request_path_out) {
|
||||
*request_path_out = request_path;
|
||||
}
|
||||
|
||||
DBusMessage* response = WaitForPortalResponse(connection, request_path);
|
||||
if (!response) {
|
||||
LOG_ERROR("Timed out waiting for {} response", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t response_code = 1;
|
||||
DBusMessageIter results;
|
||||
const bool parsed = ExtractPortalResponse(response, &response_code, &results);
|
||||
if (!parsed) {
|
||||
dbus_message_unref(response);
|
||||
LOG_ERROR("{} response was malformed", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool ok = handle_results ? handle_results(response_code, &results)
|
||||
: (response_code == 0);
|
||||
dbus_message_unref(response);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
bool KeyboardCapturer::InitWaylandPortal() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
CleanupWaylandPortal();
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
|
||||
DBusConnection* check_connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
|
||||
if (!check_connection) {
|
||||
LogDbusError("dbus_bus_get", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbus_bool_t has_owner =
|
||||
dbus_bus_name_has_owner(check_connection, kPortalBusName, &error);
|
||||
if (dbus_error_is_set(&error)) {
|
||||
LogDbusError("dbus_bus_name_has_owner", &error);
|
||||
dbus_error_free(&error);
|
||||
dbus_connection_unref(check_connection);
|
||||
return false;
|
||||
}
|
||||
dbus_connection_unref(check_connection);
|
||||
|
||||
if (!has_owner) {
|
||||
LOG_ERROR("xdg-desktop-portal is not available on session bus");
|
||||
return false;
|
||||
}
|
||||
|
||||
dbus_connection_ = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
|
||||
if (!dbus_connection_) {
|
||||
LogDbusError("dbus_bus_get_private", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
dbus_connection_set_exit_on_disconnect(dbus_connection_, FALSE);
|
||||
|
||||
const std::string session_handle_token =
|
||||
MakeToken("crossdesk_keyboard_session");
|
||||
std::string request_path;
|
||||
const bool create_ok = SendPortalRequestAndHandleResponse(
|
||||
dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession",
|
||||
"CreateSession",
|
||||
[&](DBusMessage* message) {
|
||||
DBusMessageIter iter;
|
||||
DBusMessageIter options;
|
||||
dbus_message_iter_init_append(message, &iter);
|
||||
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
|
||||
&options);
|
||||
AppendDictEntryString(&options, "session_handle_token",
|
||||
session_handle_token);
|
||||
AppendDictEntryString(&options, "handle_token",
|
||||
MakeToken("crossdesk_keyboard_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
[&](uint32_t response_code, DBusMessageIter* results) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("RemoteDesktop.CreateSession denied, response={}",
|
||||
response_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter dict;
|
||||
dbus_message_iter_recurse(results, &dict);
|
||||
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
|
||||
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
|
||||
DBusMessageIter entry;
|
||||
dbus_message_iter_recurse(&dict, &entry);
|
||||
|
||||
const char* key = nullptr;
|
||||
dbus_message_iter_get_basic(&entry, &key);
|
||||
if (key && dbus_message_iter_next(&entry) &&
|
||||
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT &&
|
||||
strcmp(key, "session_handle") == 0) {
|
||||
DBusMessageIter variant;
|
||||
std::string parsed_handle;
|
||||
dbus_message_iter_recurse(&entry, &variant);
|
||||
if (ReadPathLikeVariant(&variant, &parsed_handle) &&
|
||||
!parsed_handle.empty()) {
|
||||
wayland_session_handle_ = parsed_handle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
dbus_message_iter_next(&dict);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
&request_path);
|
||||
|
||||
if (!create_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wayland_session_handle_.empty()) {
|
||||
wayland_session_handle_ =
|
||||
BuildSessionHandleFromRequestPath(request_path, session_handle_token);
|
||||
}
|
||||
|
||||
if (wayland_session_handle_.empty()) {
|
||||
LOG_ERROR("RemoteDesktop.CreateSession did not return session handle");
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
const bool select_ok = SendPortalRequestAndHandleResponse(
|
||||
dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices",
|
||||
"SelectDevices",
|
||||
[&](DBusMessage* message) {
|
||||
DBusMessageIter iter;
|
||||
DBusMessageIter options;
|
||||
dbus_message_iter_init_append(message, &iter);
|
||||
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
|
||||
&session_handle);
|
||||
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
|
||||
&options);
|
||||
AppendDictEntryUint32(&options, "types", kRemoteDesktopDeviceKeyboard);
|
||||
AppendDictEntryString(&options, "handle_token",
|
||||
MakeToken("crossdesk_keyboard_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
[](uint32_t response_code, DBusMessageIter*) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("RemoteDesktop.SelectDevices denied, response={}",
|
||||
response_code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!select_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* parent_window = "";
|
||||
bool keyboard_granted = false;
|
||||
const bool start_ok = SendPortalRequestAndHandleResponse(
|
||||
dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start",
|
||||
[&](DBusMessage* message) {
|
||||
DBusMessageIter iter;
|
||||
DBusMessageIter options;
|
||||
dbus_message_iter_init_append(message, &iter);
|
||||
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
|
||||
&session_handle);
|
||||
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &parent_window);
|
||||
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
|
||||
&options);
|
||||
AppendDictEntryString(&options, "handle_token",
|
||||
MakeToken("crossdesk_keyboard_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
[&](uint32_t response_code, DBusMessageIter* results) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("RemoteDesktop.Start denied, response={}", response_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t granted_devices = 0;
|
||||
DBusMessageIter dict;
|
||||
dbus_message_iter_recurse(results, &dict);
|
||||
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
|
||||
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
|
||||
DBusMessageIter entry;
|
||||
dbus_message_iter_recurse(&dict, &entry);
|
||||
|
||||
const char* key = nullptr;
|
||||
dbus_message_iter_get_basic(&entry, &key);
|
||||
if (key && dbus_message_iter_next(&entry) &&
|
||||
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) {
|
||||
DBusMessageIter variant;
|
||||
dbus_message_iter_recurse(&entry, &variant);
|
||||
if (strcmp(key, "devices") == 0) {
|
||||
ReadUint32Like(&variant, &granted_devices);
|
||||
}
|
||||
}
|
||||
}
|
||||
dbus_message_iter_next(&dict);
|
||||
}
|
||||
|
||||
keyboard_granted =
|
||||
(granted_devices & kRemoteDesktopDeviceKeyboard) != 0;
|
||||
if (!keyboard_granted) {
|
||||
LOG_ERROR(
|
||||
"RemoteDesktop.Start granted devices mask={}, keyboard not allowed",
|
||||
granted_devices);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!start_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!keyboard_granted) {
|
||||
LOG_ERROR("RemoteDesktop session started without keyboard permission");
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void KeyboardCapturer::CleanupWaylandPortal() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (dbus_connection_) {
|
||||
CloseWaylandPortalSessionAndConnection(dbus_connection_,
|
||||
wayland_session_handle_,
|
||||
"RemoteDesktop.Session.Close");
|
||||
dbus_connection_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
use_wayland_portal_ = false;
|
||||
wayland_session_handle_.clear();
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode >= 0 &&
|
||||
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
if (evdev_keycode >= 0) {
|
||||
LOG_ERROR(
|
||||
"Failed to send Wayland keyboard keycode event, vk_code={}, "
|
||||
"evdev_keycode={}, is_down={}",
|
||||
key_code, evdev_keycode, is_down);
|
||||
return -3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
const int keysym = key_it->second;
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
if (x11_keycode > 8) {
|
||||
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int keysym = key_it->second;
|
||||
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
||||
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Failed to send Wayland keyboard event, vk_code={}, is_down={}",
|
||||
key_code, is_down);
|
||||
return -3;
|
||||
#else
|
||||
(void)key_code;
|
||||
(void)is_down;
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::NotifyWaylandKeyboardKeysym(int keysym, uint32_t state) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
return SendWaylandPortalVoidCall(
|
||||
"NotifyKeyboardKeysym", [&](DBusMessageIter* iter) {
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
int32_t key_sym = keysym;
|
||||
uint32_t key_state = state;
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
|
||||
&session_handle);
|
||||
AppendEmptyOptionsDict(iter);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_sym);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
|
||||
});
|
||||
#else
|
||||
(void)keysym;
|
||||
(void)state;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::NotifyWaylandKeyboardKeycode(int keycode,
|
||||
uint32_t state) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
return SendWaylandPortalVoidCall(
|
||||
"NotifyKeyboardKeycode", [&](DBusMessageIter* iter) {
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
int32_t key_code = keycode;
|
||||
uint32_t key_state = state;
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
|
||||
&session_handle);
|
||||
AppendEmptyOptionsDict(iter);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_code);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
|
||||
});
|
||||
#else
|
||||
(void)keycode;
|
||||
(void)state;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::SendWaylandPortalVoidCall(
|
||||
const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>& append_args) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (!dbus_connection_ || !method_name || method_name[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessage* message = dbus_message_new_method_call(
|
||||
kPortalBusName, kPortalObjectPath, kPortalRemoteDesktopInterface,
|
||||
method_name);
|
||||
if (!message) {
|
||||
LOG_ERROR("Failed to allocate {} message", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter iter;
|
||||
dbus_message_iter_init_append(message, &iter);
|
||||
if (append_args) {
|
||||
append_args(&iter);
|
||||
}
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
|
||||
dbus_connection_, message, 5000, &error);
|
||||
dbus_message_unref(message);
|
||||
if (!reply) {
|
||||
LogDbusError(method_name, &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
|
||||
const char* error_name = dbus_message_get_error_name(reply);
|
||||
LOG_ERROR("{} returned DBus error: {}", method_name,
|
||||
error_name ? error_name : "unknown");
|
||||
dbus_message_unref(reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
dbus_message_unref(reply);
|
||||
return true;
|
||||
#else
|
||||
(void)method_name;
|
||||
(void)append_args;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,324 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
|
||||
|
||||
static int VkCodeFromUnicode(UniChar ch) {
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
return static_cast<int>(ch - 'a' + 'A');
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
return static_cast<int>(ch);
|
||||
}
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return static_cast<int>(ch);
|
||||
}
|
||||
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
return 0x20; // VK_SPACE
|
||||
case '-':
|
||||
case '_':
|
||||
return 0xBD; // VK_OEM_MINUS
|
||||
case '=':
|
||||
case '+':
|
||||
return 0xBB; // VK_OEM_PLUS
|
||||
case '[':
|
||||
case '{':
|
||||
return 0xDB; // VK_OEM_4
|
||||
case ']':
|
||||
case '}':
|
||||
return 0xDD; // VK_OEM_6
|
||||
case '\\':
|
||||
case '|':
|
||||
return 0xDC; // VK_OEM_5
|
||||
case ';':
|
||||
case ':':
|
||||
return 0xBA; // VK_OEM_1
|
||||
case '\'':
|
||||
case '"':
|
||||
return 0xDE; // VK_OEM_7
|
||||
case ',':
|
||||
case '<':
|
||||
return 0xBC; // VK_OEM_COMMA
|
||||
case '.':
|
||||
case '>':
|
||||
return 0xBE; // VK_OEM_PERIOD
|
||||
case '/':
|
||||
case '?':
|
||||
return 0xBF; // VK_OEM_2
|
||||
case '`':
|
||||
case '~':
|
||||
return 0xC0; // VK_OEM_3
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
|
||||
bool is_key_down) {
|
||||
auto key_it = CGKeyCodeToVkCode.find(key_code);
|
||||
if (key_it != CGKeyCodeToVkCode.end()) {
|
||||
if (is_key_down) {
|
||||
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
|
||||
}
|
||||
return key_it->second;
|
||||
}
|
||||
|
||||
int vk_code = -1;
|
||||
UniChar chars[4] = {0};
|
||||
UniCharCount char_count = 0;
|
||||
CGEventKeyboardGetUnicodeString(event, 4, &char_count, chars);
|
||||
if (char_count > 0) {
|
||||
vk_code = VkCodeFromUnicode(chars[0]);
|
||||
}
|
||||
|
||||
if (vk_code < 0) {
|
||||
auto fallback_it =
|
||||
g_unmapped_keycode_to_vk.find(static_cast<int>(key_code));
|
||||
if (fallback_it != g_unmapped_keycode_to_vk.end()) {
|
||||
vk_code = fallback_it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (vk_code >= 0) {
|
||||
if (is_key_down) {
|
||||
g_unmapped_keycode_to_vk[static_cast<int>(key_code)] = vk_code;
|
||||
} else {
|
||||
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
|
||||
}
|
||||
}
|
||||
|
||||
return vk_code;
|
||||
}
|
||||
|
||||
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
CGEventRef event, void* userInfo) {
|
||||
(void)proxy;
|
||||
if (!g_on_key_action) {
|
||||
return event;
|
||||
}
|
||||
|
||||
KeyboardCapturer* keyboard_capturer = (KeyboardCapturer*)userInfo;
|
||||
if (!keyboard_capturer) {
|
||||
LOG_ERROR("keyboard_capturer is nullptr");
|
||||
return event;
|
||||
}
|
||||
|
||||
if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
|
||||
const bool is_key_down = (type == kCGEventKeyDown);
|
||||
CGKeyCode key_code = static_cast<CGKeyCode>(
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
|
||||
int vk_code = ResolveVkCodeFromMacEvent(event, key_code, is_key_down);
|
||||
if (vk_code >= 0) {
|
||||
g_on_key_action(vk_code, is_key_down, 0, false, g_user_ptr);
|
||||
}
|
||||
} else if (type == kCGEventFlagsChanged) {
|
||||
CGEventFlags current_flags = CGEventGetFlags(event);
|
||||
CGKeyCode key_code = static_cast<CGKeyCode>(
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
|
||||
auto key_it = CGKeyCodeToVkCode.find(key_code);
|
||||
if (key_it == CGKeyCodeToVkCode.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
const int vk_code = key_it->second;
|
||||
|
||||
// caps lock
|
||||
bool caps_lock_state = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
|
||||
if (caps_lock_state != keyboard_capturer->caps_lock_flag_) {
|
||||
keyboard_capturer->caps_lock_flag_ = caps_lock_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->caps_lock_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// shift
|
||||
bool shift_state = (current_flags & kCGEventFlagMaskShift) != 0;
|
||||
if (shift_state != keyboard_capturer->shift_flag_) {
|
||||
keyboard_capturer->shift_flag_ = shift_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->shift_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// control
|
||||
bool control_state = (current_flags & kCGEventFlagMaskControl) != 0;
|
||||
if (control_state != keyboard_capturer->control_flag_) {
|
||||
keyboard_capturer->control_flag_ = control_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->control_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// option
|
||||
bool option_state = (current_flags & kCGEventFlagMaskAlternate) != 0;
|
||||
if (option_state != keyboard_capturer->option_flag_) {
|
||||
keyboard_capturer->option_flag_ = option_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->option_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// command
|
||||
bool command_state = (current_flags & kCGEventFlagMaskCommand) != 0;
|
||||
if (command_state != keyboard_capturer->command_flag_) {
|
||||
keyboard_capturer->command_flag_ = command_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->command_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer()
|
||||
: event_tap_(nullptr), run_loop_source_(nullptr) {}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
if (event_tap_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_unmapped_keycode_to_vk.clear();
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
|
||||
CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) |
|
||||
(1 << kCGEventFlagsChanged);
|
||||
|
||||
event_tap_ = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
|
||||
kCGEventTapOptionDefault, eventMask,
|
||||
eventCallback, this);
|
||||
|
||||
if (!event_tap_) {
|
||||
LOG_ERROR("CGEventTapCreate failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
run_loop_source_ =
|
||||
CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event_tap_, 0);
|
||||
if (!run_loop_source_) {
|
||||
LOG_ERROR("CFMachPortCreateRunLoopSource failed");
|
||||
CFRelease(event_tap_);
|
||||
event_tap_ = nullptr;
|
||||
return -1;
|
||||
}
|
||||
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source_,
|
||||
kCFRunLoopCommonModes);
|
||||
|
||||
const CGEventFlags current_flags =
|
||||
CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
|
||||
caps_lock_flag_ = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
|
||||
shift_flag_ = (current_flags & kCGEventFlagMaskShift) != 0;
|
||||
control_flag_ = (current_flags & kCGEventFlagMaskControl) != 0;
|
||||
option_flag_ = (current_flags & kCGEventFlagMaskAlternate) != 0;
|
||||
command_flag_ = (current_flags & kCGEventFlagMaskCommand) != 0;
|
||||
|
||||
CGEventTapEnable(event_tap_, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
g_unmapped_keycode_to_vk.clear();
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
|
||||
if (event_tap_) {
|
||||
CGEventTapEnable(event_tap_, false);
|
||||
}
|
||||
|
||||
if (run_loop_source_) {
|
||||
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source_,
|
||||
kCFRunLoopCommonModes);
|
||||
CFRelease(run_loop_source_);
|
||||
run_loop_source_ = nullptr;
|
||||
}
|
||||
|
||||
if (event_tap_) {
|
||||
CFRelease(event_tap_);
|
||||
event_tap_ = nullptr;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline bool IsFunctionKey(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0x7A:
|
||||
case 0x78:
|
||||
case 0x63:
|
||||
case 0x76:
|
||||
case 0x60:
|
||||
case 0x61:
|
||||
case 0x62:
|
||||
case 0x64:
|
||||
case 0x65:
|
||||
case 0x6D:
|
||||
case 0x67:
|
||||
case 0x6F:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CGEventFlags ToCGEventFlags(uint32_t injected_flags) {
|
||||
CGEventFlags flags = 0;
|
||||
if ((injected_flags & kMacInjectedModifierShift) != 0) {
|
||||
flags |= kCGEventFlagMaskShift;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierControl) != 0) {
|
||||
flags |= kCGEventFlagMaskControl;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierOption) != 0) {
|
||||
flags |= kCGEventFlagMaskAlternate;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierCommand) != 0) {
|
||||
flags |= kCGEventFlagMaskCommand;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
const uint32_t injected_flags =
|
||||
injected_modifier_state_.Update(key_code, is_down);
|
||||
|
||||
if (vkCodeToCGKeyCode.find(key_code) != vkCodeToCGKeyCode.end()) {
|
||||
CGKeyCode cg_key_code = vkCodeToCGKeyCode[key_code];
|
||||
CGEventRef event = CGEventCreateKeyboardEvent(NULL, cg_key_code, is_down);
|
||||
if (!event) {
|
||||
LOG_ERROR("CGEventCreateKeyboardEvent failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
|
||||
// F1-F12 keys often require the FN key to be pressed on Mac keyboards, so
|
||||
// we simulate the FN key release when an F1-F12 key is released.
|
||||
if (IsFunctionKey(cg_key_code) && !is_down) {
|
||||
CGEventRef fn_release_event =
|
||||
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
|
||||
if (!fn_release_event) {
|
||||
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
|
||||
return -1;
|
||||
}
|
||||
CGEventPost(kCGHIDEventTap, fn_release_event);
|
||||
CFRelease(fn_release_event);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-11-22
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KEYBOARD_CAPTURER_H_
|
||||
#define _KEYBOARD_CAPTURER_H_
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "macos_keyboard_modifier_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
KeyboardCapturer();
|
||||
virtual ~KeyboardCapturer();
|
||||
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
CFMachPortRef event_tap_ = nullptr;
|
||||
CFRunLoopSourceRef run_loop_source_ = nullptr;
|
||||
|
||||
public:
|
||||
bool caps_lock_flag_ = false;
|
||||
bool shift_flag_ = false;
|
||||
bool control_flag_ = false;
|
||||
bool option_flag_ = false;
|
||||
bool command_flag_ = false;
|
||||
int fn_key_code_ = 0x3F;
|
||||
MacKeyboardModifierState injected_modifier_state_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static int NormalizeModifierVkCode(const KBDLLHOOKSTRUCT* kb_data) {
|
||||
if (kb_data == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (kb_data->vkCode != VK_SHIFT && kb_data->vkCode != VK_CONTROL &&
|
||||
kb_data->vkCode != VK_MENU) {
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
UINT scan_code = static_cast<UINT>(kb_data->scanCode & 0xFF);
|
||||
if ((kb_data->flags & LLKHF_EXTENDED) != 0) {
|
||||
scan_code |= 0xE000;
|
||||
}
|
||||
|
||||
const UINT normalized_vk = MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized_vk != 0) {
|
||||
return static_cast<int>(normalized_vk);
|
||||
}
|
||||
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
case VK_LCONTROL:
|
||||
case VK_RCONTROL:
|
||||
case VK_LMENU:
|
||||
case VK_RMENU:
|
||||
case VK_LWIN:
|
||||
case VK_RWIN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
g_on_key_action(key_code, true, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
|
||||
g_on_key_action(key_code, false, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() {}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {}
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
|
||||
keyboard_hook_ = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
|
||||
if (!keyboard_hook_) {
|
||||
LOG_ERROR("Failed to install keyboard hook");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
if (keyboard_hook_) {
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
UnhookWindowsHookEx(keyboard_hook_);
|
||||
keyboard_hook_ = nullptr;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// apply remote keyboard commands to the local machine
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
scan_code != 0
|
||||
? static_cast<UINT>(scan_code & 0xFF) | (extended ? 0xE000u : 0u)
|
||||
: MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
|
||||
if (scan_code != 0 && !prefer_vk) {
|
||||
input.ki.wVk = 0;
|
||||
input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
|
||||
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
|
||||
if (extended) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else {
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
|
||||
if (prefer_vk && resolved_scan_code != 0) {
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
if ((resolved_scan_code & 0xFF00) != 0) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else if (resolved_scan_code != 0) {
|
||||
input.ki.wVk = 0;
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
|
||||
if ((resolved_scan_code & 0xFF00) != 0) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_down) {
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
|
||||
is_down, GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-11-22
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KEYBOARD_CAPTURER_H_
|
||||
#define _KEYBOARD_CAPTURER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
KeyboardCapturer();
|
||||
virtual ~KeyboardCapturer();
|
||||
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
HHOOK keyboard_hook_ = nullptr;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,748 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-11-25
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KEYBOARD_CONVERTER_H_
|
||||
#define _KEYBOARD_CONVERTER_H_
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Windows vkCode to macOS CGKeyCode (104 keys)
|
||||
std::map<int, int> vkCodeToCGKeyCode = {
|
||||
// A-Z
|
||||
{0x41, 0x00}, // A
|
||||
{0x42, 0x0B}, // B
|
||||
{0x43, 0x08}, // C
|
||||
{0x44, 0x02}, // D
|
||||
{0x45, 0x0E}, // E
|
||||
{0x46, 0x03}, // F
|
||||
{0x47, 0x05}, // G
|
||||
{0x48, 0x04}, // H
|
||||
{0x49, 0x22}, // I
|
||||
{0x4A, 0x26}, // J
|
||||
{0x4B, 0x28}, // K
|
||||
{0x4C, 0x25}, // L
|
||||
{0x4D, 0x2E}, // M
|
||||
{0x4E, 0x2D}, // N
|
||||
{0x4F, 0x1F}, // O
|
||||
{0x50, 0x23}, // P
|
||||
{0x51, 0x0C}, // Q
|
||||
{0x52, 0x0F}, // R
|
||||
{0x53, 0x01}, // S
|
||||
{0x54, 0x11}, // T
|
||||
{0x55, 0x20}, // U
|
||||
{0x56, 0x09}, // V
|
||||
{0x57, 0x0D}, // W
|
||||
{0x58, 0x07}, // X
|
||||
{0x59, 0x10}, // Y
|
||||
{0x5A, 0x06}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x30, 0x1D}, // 0
|
||||
{0x31, 0x12}, // 1
|
||||
{0x32, 0x13}, // 2
|
||||
{0x33, 0x14}, // 3
|
||||
{0x34, 0x15}, // 4
|
||||
{0x35, 0x17}, // 5
|
||||
{0x36, 0x16}, // 6
|
||||
{0x37, 0x1A}, // 7
|
||||
{0x38, 0x1C}, // 8
|
||||
{0x39, 0x19}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x70, 0x7A}, // F1
|
||||
{0x71, 0x78}, // F2
|
||||
{0x72, 0x63}, // F3
|
||||
{0x73, 0x76}, // F4
|
||||
{0x74, 0x60}, // F5
|
||||
{0x75, 0x61}, // F6
|
||||
{0x76, 0x62}, // F7
|
||||
{0x77, 0x64}, // F8
|
||||
{0x78, 0x65}, // F9
|
||||
{0x79, 0x6D}, // F10
|
||||
{0x7A, 0x67}, // F11
|
||||
{0x7B, 0x6F}, // F12
|
||||
|
||||
// control keys
|
||||
{0x1B, 0x35}, // Escape
|
||||
{0x0D, 0x24}, // Enter
|
||||
{0x20, 0x31}, // Space
|
||||
{0x08, 0x33}, // Backspace
|
||||
{0x09, 0x30}, // Tab
|
||||
{0x2C, 0x69}, // Print Screen(F13)
|
||||
{0x2D, 0x72}, // Insert
|
||||
{0x2E, 0x75}, // Delete
|
||||
{0x24, 0x73}, // Home
|
||||
{0x23, 0x77}, // End
|
||||
{0x21, 0x74}, // Page Up
|
||||
{0x22, 0x79}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x25, 0x7B}, // Left Arrow
|
||||
{0x27, 0x7C}, // Right Arrow
|
||||
{0x26, 0x7E}, // Up Arrow
|
||||
{0x28, 0x7D}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x60, 0x52}, // Numpad 0
|
||||
{0x61, 0x53}, // Numpad 1
|
||||
{0x62, 0x54}, // Numpad 2
|
||||
{0x63, 0x55}, // Numpad 3
|
||||
{0x64, 0x56}, // Numpad 4
|
||||
{0x65, 0x57}, // Numpad 5
|
||||
{0x66, 0x58}, // Numpad 6
|
||||
{0x67, 0x59}, // Numpad 7
|
||||
{0x68, 0x5B}, // Numpad 8
|
||||
{0x69, 0x5C}, // Numpad 9
|
||||
{0x90, 0x47}, // Num Lock / Keypad Clear
|
||||
{0x6E, 0x41}, // Numpad .
|
||||
{0x6F, 0x4B}, // Numpad /
|
||||
{0x6A, 0x43}, // Numpad *
|
||||
{0x6D, 0x4E}, // Numpad -
|
||||
{0x6B, 0x45}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0xBA, 0x29}, // ; (Semicolon)
|
||||
{0xDE, 0x27}, // ' (Quote)
|
||||
{0xC0, 0x32}, // ` (Backtick)
|
||||
{0xBC, 0x2B}, // , (Comma)
|
||||
{0xBE, 0x2F}, // . (Period)
|
||||
{0xBF, 0x2C}, // / (Slash)
|
||||
{0xDC, 0x2A}, // \ (Backslash)
|
||||
{0xDB, 0x21}, // [ (Left Bracket)
|
||||
{0xDD, 0x1E}, // ] (Right Bracket)
|
||||
{0xBD, 0x1B}, // - (Minus)
|
||||
{0xBB, 0x18}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x14, 0x39}, // Caps Lock
|
||||
{0xA0, 0x38}, // Shift (Left)
|
||||
{0xA1, 0x3C}, // Shift (Right)
|
||||
{0xA2, 0x3B}, // Ctrl (Left)
|
||||
{0xA3, 0x3E}, // Ctrl (Right)
|
||||
{0xA4, 0x3A}, // Alt (Left)
|
||||
{0xA5, 0x3D}, // Alt (Right)
|
||||
{0x5B, 0x37}, // Left Command (Windows key)
|
||||
{0x5C, 0x36}, // Right Command
|
||||
};
|
||||
|
||||
// macOS CGKeyCode to Windows vkCode
|
||||
std::map<int, int> CGKeyCodeToVkCode = {
|
||||
// A-Z
|
||||
{0x00, 0x41}, // A
|
||||
{0x0B, 0x42}, // B
|
||||
{0x08, 0x43}, // C
|
||||
{0x02, 0x44}, // D
|
||||
{0x0E, 0x45}, // E
|
||||
{0x03, 0x46}, // F
|
||||
{0x05, 0x47}, // G
|
||||
{0x04, 0x48}, // H
|
||||
{0x22, 0x49}, // I
|
||||
{0x26, 0x4A}, // J
|
||||
{0x28, 0x4B}, // K
|
||||
{0x25, 0x4C}, // L
|
||||
{0x2E, 0x4D}, // M
|
||||
{0x2D, 0x4E}, // N
|
||||
{0x1F, 0x4F}, // O
|
||||
{0x23, 0x50}, // P
|
||||
{0x0C, 0x51}, // Q
|
||||
{0x0F, 0x52}, // R
|
||||
{0x01, 0x53}, // S
|
||||
{0x11, 0x54}, // T
|
||||
{0x20, 0x55}, // U
|
||||
{0x09, 0x56}, // V
|
||||
{0x0D, 0x57}, // W
|
||||
{0x07, 0x58}, // X
|
||||
{0x10, 0x59}, // Y
|
||||
{0x06, 0x5A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x1D, 0x30}, // 0
|
||||
{0x12, 0x31}, // 1
|
||||
{0x13, 0x32}, // 2
|
||||
{0x14, 0x33}, // 3
|
||||
{0x15, 0x34}, // 4
|
||||
{0x17, 0x35}, // 5
|
||||
{0x16, 0x36}, // 6
|
||||
{0x1A, 0x37}, // 7
|
||||
{0x1C, 0x38}, // 8
|
||||
{0x19, 0x39}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x7A, 0x70}, // F1
|
||||
{0x78, 0x71}, // F2
|
||||
{0x63, 0x72}, // F3
|
||||
{0x76, 0x73}, // F4
|
||||
{0x60, 0x74}, // F5
|
||||
{0x61, 0x75}, // F6
|
||||
{0x62, 0x76}, // F7
|
||||
{0x64, 0x77}, // F8
|
||||
{0x65, 0x78}, // F9
|
||||
{0x6D, 0x79}, // F10
|
||||
{0x67, 0x7A}, // F11
|
||||
{0x6F, 0x7B}, // F12
|
||||
|
||||
// control keys
|
||||
{0x35, 0x1B}, // Escape
|
||||
{0x24, 0x0D}, // Enter
|
||||
{0x31, 0x20}, // Space
|
||||
{0x33, 0x08}, // Backspace
|
||||
{0x30, 0x09}, // Tab
|
||||
{0x69, 0x2C}, // Print Screen(F13)
|
||||
{0x72, 0x2D}, // Insert
|
||||
{0x75, 0x2E}, // Delete
|
||||
{0x73, 0x24}, // Home
|
||||
{0x77, 0x23}, // End
|
||||
{0x74, 0x21}, // Page Up
|
||||
{0x79, 0x22}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x7B, 0x25}, // Left Arrow
|
||||
{0x7C, 0x27}, // Right Arrow
|
||||
{0x7E, 0x26}, // Up Arrow
|
||||
{0x7D, 0x28}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x52, 0x60}, // Numpad 0
|
||||
{0x53, 0x61}, // Numpad 1
|
||||
{0x54, 0x62}, // Numpad 2
|
||||
{0x55, 0x63}, // Numpad 3
|
||||
{0x56, 0x64}, // Numpad 4
|
||||
{0x57, 0x65}, // Numpad 5
|
||||
{0x58, 0x66}, // Numpad 6
|
||||
{0x59, 0x67}, // Numpad 7
|
||||
{0x5B, 0x68}, // Numpad 8
|
||||
{0x5C, 0x69}, // Numpad 9
|
||||
{0x47, 0x90}, // Num Lock / Keypad Clear
|
||||
{0x41, 0x6E}, // Numpad .
|
||||
{0x4B, 0x6F}, // Numpad /
|
||||
{0x43, 0x6A}, // Numpad *
|
||||
{0x4E, 0x6D}, // Numpad -
|
||||
{0x45, 0x6B}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x29, 0xBA}, // ; (Semicolon)
|
||||
{0x27, 0xDE}, // ' (Quote)
|
||||
{0x32, 0xC0}, // ` (Backtick)
|
||||
{0x2B, 0xBC}, // , (Comma)
|
||||
{0x2F, 0xBE}, // . (Period)
|
||||
{0x2C, 0xBF}, // / (Slash)
|
||||
{0x2A, 0xDC}, // \ (Backslash)
|
||||
{0x21, 0xDB}, // [ (Left Bracket)
|
||||
{0x1E, 0xDD}, // ] (Right Bracket)
|
||||
{0x1B, 0xBD}, // - (Minus)
|
||||
{0x18, 0xBB}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x39, 0x14}, // Caps Lock
|
||||
{0x38, 0xA0}, // Shift (Left)
|
||||
{0x3C, 0xA1}, // Shift (Right)
|
||||
{0x3B, 0xA2}, // Control (Left)
|
||||
{0x3E, 0xA3}, // Control (Right)
|
||||
{0x3A, 0xA4}, // Alt (Left)
|
||||
{0x3D, 0xA5}, // Alt (Right)
|
||||
{0x37, 0x5B}, // Left Command (Windows key)
|
||||
{0x36, 0x5C}, // Right Command
|
||||
};
|
||||
|
||||
// Windows vkCode to X11 KeySym
|
||||
std::map<int, int> vkCodeToX11KeySym = {
|
||||
// A-Z
|
||||
{0x41, 0x0041}, // A
|
||||
{0x42, 0x0042}, // B
|
||||
{0x43, 0x0043}, // C
|
||||
{0x44, 0x0044}, // D
|
||||
{0x45, 0x0045}, // E
|
||||
{0x46, 0x0046}, // F
|
||||
{0x47, 0x0047}, // G
|
||||
{0x48, 0x0048}, // H
|
||||
{0x49, 0x0049}, // I
|
||||
{0x4A, 0x004A}, // J
|
||||
{0x4B, 0x004B}, // K
|
||||
{0x4C, 0x004C}, // L
|
||||
{0x4D, 0x004D}, // M
|
||||
{0x4E, 0x004E}, // N
|
||||
{0x4F, 0x004F}, // O
|
||||
{0x50, 0x0050}, // P
|
||||
{0x51, 0x0051}, // Q
|
||||
{0x52, 0x0052}, // R
|
||||
{0x53, 0x0053}, // S
|
||||
{0x54, 0x0054}, // T
|
||||
{0x55, 0x0055}, // U
|
||||
{0x56, 0x0056}, // V
|
||||
{0x57, 0x0057}, // W
|
||||
{0x58, 0x0058}, // X
|
||||
{0x59, 0x0059}, // Y
|
||||
{0x5A, 0x005A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x30, 0x0030}, // 0
|
||||
{0x31, 0x0031}, // 1
|
||||
{0x32, 0x0032}, // 2
|
||||
{0x33, 0x0033}, // 3
|
||||
{0x34, 0x0034}, // 4
|
||||
{0x35, 0x0035}, // 5
|
||||
{0x36, 0x0036}, // 6
|
||||
{0x37, 0x0037}, // 7
|
||||
{0x38, 0x0038}, // 8
|
||||
{0x39, 0x0039}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x70, 0xFFBE}, // F1
|
||||
{0x71, 0xFFBF}, // F2
|
||||
{0x72, 0xFFC0}, // F3
|
||||
{0x73, 0xFFC1}, // F4
|
||||
{0x74, 0xFFC2}, // F5
|
||||
{0x75, 0xFFC3}, // F6
|
||||
{0x76, 0xFFC4}, // F7
|
||||
{0x77, 0xFFC5}, // F8
|
||||
{0x78, 0xFFC6}, // F9
|
||||
{0x79, 0xFFC7}, // F10
|
||||
{0x7A, 0xFFC8}, // F11
|
||||
{0x7B, 0xFFC9}, // F12
|
||||
|
||||
// control keys
|
||||
{0x1B, 0xFF1B}, // Escape
|
||||
{0x0D, 0xFF0D}, // Enter
|
||||
{0x20, 0x0020}, // Space
|
||||
{0x08, 0xFF08}, // Backspace
|
||||
{0x09, 0xFF09}, // Tab
|
||||
{0x2C, 0xFF15}, // Print Screen
|
||||
{0x91, 0xFF14}, // Scroll Lock
|
||||
{0x13, 0xFF13}, // Pause/Break
|
||||
{0x2D, 0xFF63}, // Insert
|
||||
{0x2E, 0xFFFF}, // Delete
|
||||
{0x24, 0xFF50}, // Home
|
||||
{0x23, 0xFF57}, // End
|
||||
{0x21, 0xFF55}, // Page Up
|
||||
{0x22, 0xFF56}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x25, 0xFF51}, // Left Arrow
|
||||
{0x27, 0xFF53}, // Right Arrow
|
||||
{0x26, 0xFF52}, // Up Arrow
|
||||
{0x28, 0xFF54}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x60, 0xFFB0}, // Numpad 0
|
||||
{0x61, 0xFFB1}, // Numpad 1
|
||||
{0x62, 0xFFB2}, // Numpad 2
|
||||
{0x63, 0xFFB3}, // Numpad 3
|
||||
{0x64, 0xFFB4}, // Numpad 4
|
||||
{0x65, 0xFFB5}, // Numpad 5
|
||||
{0x66, 0xFFB6}, // Numpad 6
|
||||
{0x67, 0xFFB7}, // Numpad 7
|
||||
{0x68, 0xFFB8}, // Numpad 8
|
||||
{0x69, 0xFFB9}, // Numpad 9
|
||||
{0x90, 0xFF7F}, // Num Lock
|
||||
{0x6E, 0xFFAE}, // Numpad .
|
||||
{0x6F, 0xFFAF}, // Numpad /
|
||||
{0x6A, 0xFFAA}, // Numpad *
|
||||
{0x6D, 0xFFAD}, // Numpad -
|
||||
{0x6B, 0xFFAB}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0xBA, 0x003B}, // ; (Semicolon)
|
||||
{0xDE, 0x0027}, // ' (Quote)
|
||||
{0xC0, 0x007E}, // ` (Grave)
|
||||
{0xBC, 0x002C}, // , (Comma)
|
||||
{0xBE, 0x002E}, // . (Period)
|
||||
{0xBF, 0x002F}, // / (Slash)
|
||||
{0xDC, 0x005C}, // \ (Backslash)
|
||||
{0xDB, 0x005B}, // [ (Left Bracket)
|
||||
{0xDD, 0x005D}, // ] (Right Bracket)
|
||||
{0xBD, 0x002D}, // - (Minus)
|
||||
{0xBB, 0x003D}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x14, 0xFFE5}, // Caps Lock
|
||||
{0xA0, 0xFFE1}, // Shift (Left)
|
||||
{0xA1, 0xFFE2}, // Shift (Right)
|
||||
{0xA2, 0xFFE3}, // Ctrl (Left)
|
||||
{0xA3, 0xFFE4}, // Ctrl (Right)
|
||||
{0xA4, 0xFFE9}, // Alt (Left)
|
||||
{0xA5, 0xFFEA}, // Alt (Right)
|
||||
{0x5B, 0xFFEB}, // Left Command (Windows key)
|
||||
{0x5C, 0xFFEC}, // Right Command
|
||||
};
|
||||
|
||||
// X11 KeySym to Windows vkCode
|
||||
// std::map<int, int> x11KeySymToVkCode = []() {
|
||||
// std::map<int, int> result;
|
||||
// for (const auto& pair : vkCodeToX11KeySym) {
|
||||
// result[pair.second] = pair.first;
|
||||
// }
|
||||
// return result;
|
||||
// }();
|
||||
|
||||
std::map<int, int> x11KeySymToVkCode = {
|
||||
// A-Z
|
||||
{0x0041, 0x41}, // A
|
||||
{0x0042, 0x42}, // B
|
||||
{0x0043, 0x43}, // C
|
||||
{0x0044, 0x44}, // D
|
||||
{0x0045, 0x45}, // E
|
||||
{0x0046, 0x46}, // F
|
||||
{0x0047, 0x47}, // G
|
||||
{0x0048, 0x48}, // H
|
||||
{0x0049, 0x49}, // I
|
||||
{0x004A, 0x4A}, // J
|
||||
{0x004B, 0x4B}, // K
|
||||
{0x004C, 0x4C}, // L
|
||||
{0x004D, 0x4D}, // M
|
||||
{0x004E, 0x4E}, // N
|
||||
{0x004F, 0x4F}, // O
|
||||
{0x0050, 0x50}, // P
|
||||
{0x0051, 0x51}, // Q
|
||||
{0x0052, 0x52}, // R
|
||||
{0x0053, 0x53}, // S
|
||||
{0x0054, 0x54}, // T
|
||||
{0x0055, 0x55}, // U
|
||||
{0x0056, 0x56}, // V
|
||||
{0x0057, 0x57}, // W
|
||||
{0x0058, 0x58}, // X
|
||||
{0x0059, 0x59}, // Y
|
||||
{0x005A, 0x5A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x0030, 0x30}, // 0
|
||||
{0x0031, 0x31}, // 1
|
||||
{0x0032, 0x32}, // 2
|
||||
{0x0033, 0x33}, // 3
|
||||
{0x0034, 0x34}, // 4
|
||||
{0x0035, 0x35}, // 5
|
||||
{0x0036, 0x36}, // 6
|
||||
{0x0037, 0x37}, // 7
|
||||
{0x0038, 0x38}, // 8
|
||||
{0x0039, 0x39}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0xFFBE, 0x70}, // F1
|
||||
{0xFFBF, 0x71}, // F2
|
||||
{0xFFC0, 0x72}, // F3
|
||||
{0xFFC1, 0x73}, // F4
|
||||
{0xFFC2, 0x74}, // F5
|
||||
{0xFFC3, 0x75}, // F6
|
||||
{0xFFC4, 0x76}, // F7
|
||||
{0xFFC5, 0x77}, // F8
|
||||
{0xFFC6, 0x78}, // F9
|
||||
{0xFFC7, 0x79}, // F10
|
||||
{0xFFC8, 0x7A}, // F11
|
||||
{0xFFC9, 0x7B}, // F12
|
||||
|
||||
// control keys
|
||||
{0xFF1B, 0x1B}, // Escape
|
||||
{0xFF0D, 0x0D}, // Enter
|
||||
{0x0020, 0x20}, // Space
|
||||
{0xFF08, 0x08}, // Backspace
|
||||
{0xFF09, 0x09}, // Tab
|
||||
{0xFF15, 0x2C}, // Print Screen
|
||||
{0xFF14, 0x91}, // Scroll Lock
|
||||
{0xFF13, 0x13}, // Pause/Break
|
||||
{0xFF63, 0x2D}, // Insert
|
||||
{0xFFFF, 0x2E}, // Delete
|
||||
{0xFF50, 0x24}, // Home
|
||||
{0xFF57, 0x23}, // End
|
||||
{0xFF55, 0x21}, // Page Up
|
||||
{0xFF56, 0x22}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0xFF51, 0x25}, // Left Arrow
|
||||
{0xFF53, 0x27}, // Right Arrow
|
||||
{0xFF52, 0x26}, // Up Arrow
|
||||
{0xFF54, 0x28}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0xFFB0, 0x60}, // Numpad 0
|
||||
{0xFFB1, 0x61}, // Numpad 1
|
||||
{0xFFB2, 0x62}, // Numpad 2
|
||||
{0xFFB3, 0x63}, // Numpad 3
|
||||
{0xFFB4, 0x64}, // Numpad 4
|
||||
{0xFFB5, 0x65}, // Numpad 5
|
||||
{0xFFB6, 0x66}, // Numpad 6
|
||||
{0xFFB7, 0x67}, // Numpad 7
|
||||
{0xFFB8, 0x68}, // Numpad 8
|
||||
{0xFFB9, 0x69}, // Numpad 9
|
||||
{0xFF7F, 0x90}, // Num Lock
|
||||
{0xFFAE, 0x6E}, // Numpad .
|
||||
{0xFFAF, 0x6F}, // Numpad /
|
||||
{0xFFAA, 0x6A}, // Numpad *
|
||||
{0xFFAD, 0x6D}, // Numpad -
|
||||
{0xFFAB, 0x6B}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x003B, 0xBA}, // ; (Semicolon)
|
||||
{0x0027, 0xDE}, // ' (Quote)
|
||||
{0x007E, 0xC0}, // ` (Grave)
|
||||
{0x002C, 0xBC}, // , (Comma)
|
||||
{0x002E, 0xBE}, // . (Period)
|
||||
{0x002F, 0xBF}, // / (Slash)
|
||||
{0x005C, 0xDC}, // \ (Backslash)
|
||||
{0x005B, 0xDB}, // [ (Left Bracket)
|
||||
{0x005D, 0xDD}, // ] (Right Bracket)
|
||||
{0x002D, 0xBD}, // - (Minus)
|
||||
{0x003D, 0xBB}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0xFFE5, 0x14}, // Caps Lock
|
||||
{0xFFE1, 0xA0}, // Shift (Left)
|
||||
{0xFFE2, 0xA1}, // Shift (Right)
|
||||
{0xFFE3, 0xA2}, // Ctrl (Left)
|
||||
{0xFFE4, 0xA3}, // Ctrl (Right)
|
||||
{0xFFE9, 0xA4}, // Alt (Left)
|
||||
{0xFFEA, 0xA5}, // Alt (Right)
|
||||
{0xFFEB, 0x5B}, // Left Command (Windows key)
|
||||
{0xFFEC, 0x5C}, // Right Command
|
||||
};
|
||||
|
||||
// macOS CGKeyCode to X11 KeySym
|
||||
std::map<int, int> cgKeyCodeToX11KeySym = {
|
||||
// A-Z
|
||||
{0x00, 0x0041}, // A
|
||||
{0x0B, 0x0042}, // B
|
||||
{0x08, 0x0043}, // C
|
||||
{0x02, 0x0044}, // D
|
||||
{0x0E, 0x0045}, // E
|
||||
{0x03, 0x0046}, // F
|
||||
{0x05, 0x0047}, // G
|
||||
{0x04, 0x0048}, // H
|
||||
{0x22, 0x0049}, // I
|
||||
{0x26, 0x004A}, // J
|
||||
{0x28, 0x004B}, // K
|
||||
{0x25, 0x004C}, // L
|
||||
{0x2E, 0x004D}, // M
|
||||
{0x2D, 0x004E}, // N
|
||||
{0x1F, 0x004F}, // O
|
||||
{0x23, 0x0050}, // P
|
||||
{0x0C, 0x0051}, // Q
|
||||
{0x0F, 0x0052}, // R
|
||||
{0x01, 0x0053}, // S
|
||||
{0x11, 0x0054}, // T
|
||||
{0x20, 0x0055}, // U
|
||||
{0x09, 0x0056}, // V
|
||||
{0x0D, 0x0057}, // W
|
||||
{0x07, 0x0058}, // X
|
||||
{0x10, 0x0059}, // Y
|
||||
{0x06, 0x005A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x1D, 0x0030}, // 0
|
||||
{0x12, 0x0031}, // 1
|
||||
{0x13, 0x0032}, // 2
|
||||
{0x14, 0x0033}, // 3
|
||||
{0x15, 0x0034}, // 4
|
||||
{0x17, 0x0035}, // 5
|
||||
{0x16, 0x0036}, // 6
|
||||
{0x1A, 0x0037}, // 7
|
||||
{0x1C, 0x0038}, // 8
|
||||
{0x19, 0x0039}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x7A, 0xFFBE}, // F1
|
||||
{0x78, 0xFFBF}, // F2
|
||||
{0x63, 0xFFC0}, // F3
|
||||
{0x76, 0xFFC1}, // F4
|
||||
{0x60, 0xFFC2}, // F5
|
||||
{0x61, 0xFFC3}, // F6
|
||||
{0x62, 0xFFC4}, // F7
|
||||
{0x64, 0xFFC5}, // F8
|
||||
{0x65, 0xFFC6}, // F9
|
||||
{0x6D, 0xFFC7}, // F10
|
||||
{0x67, 0xFFC8}, // F11
|
||||
{0x6F, 0xFFC9}, // F12
|
||||
|
||||
// control keys
|
||||
{0x35, 0xFF1B}, // Escape
|
||||
{0x24, 0xFF0D}, // Enter
|
||||
{0x31, 0x0020}, // Space
|
||||
{0x33, 0xFF08}, // Backspace
|
||||
{0x30, 0xFF09}, // Tab
|
||||
{0x69, 0xFF15}, // Print Screen(F13)
|
||||
{0x72, 0xFF63}, // Insert
|
||||
{0x75, 0xFFFF}, // Delete
|
||||
{0x73, 0xFF50}, // Home
|
||||
{0x77, 0xFF57}, // End
|
||||
{0x74, 0xFF55}, // Page Up
|
||||
{0x79, 0xFF56}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x7B, 0xFF51}, // Left Arrow
|
||||
{0x7C, 0xFF53}, // Right Arrow
|
||||
{0x7E, 0xFF52}, // Up Arrow
|
||||
{0x7D, 0xFF54}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x52, 0xFFB0}, // Numpad 0
|
||||
{0x53, 0xFFB1}, // Numpad 1
|
||||
{0x54, 0xFFB2}, // Numpad 2
|
||||
{0x55, 0xFFB3}, // Numpad 3
|
||||
{0x56, 0xFFB4}, // Numpad 4
|
||||
{0x57, 0xFFB5}, // Numpad 5
|
||||
{0x58, 0xFFB6}, // Numpad 6
|
||||
{0x59, 0xFFB7}, // Numpad 7
|
||||
{0x5B, 0xFFB8}, // Numpad 8
|
||||
{0x5C, 0xFFB9}, // Numpad 9
|
||||
{0x47, 0xFF7F}, // Num Lock / Keypad Clear
|
||||
{0x41, 0xFFAE}, // Numpad .
|
||||
{0x4B, 0xFFAF}, // Numpad /
|
||||
{0x43, 0xFFAA}, // Numpad *
|
||||
{0x4E, 0xFFAD}, // Numpad -
|
||||
{0x45, 0xFFAB}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x29, 0x003B}, // ; (Semicolon)
|
||||
{0x27, 0x0027}, // ' (Quote)
|
||||
{0x32, 0x007E}, // ` (Backtick)
|
||||
{0x2B, 0x002C}, // , (Comma)
|
||||
{0x2F, 0x002E}, // . (Period)
|
||||
{0x2C, 0x002F}, // / (Slash)
|
||||
{0x2A, 0x005C}, // \ (Backslash)
|
||||
{0x21, 0x005B}, // [ (Left Bracket)
|
||||
{0x1E, 0x005D}, // ] (Right Bracket)
|
||||
{0x1B, 0x002D}, // - (Minus)
|
||||
{0x18, 0x003D}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x39, 0xFFE5}, // Caps Lock
|
||||
{0x38, 0xFFE1}, // Shift (Left)
|
||||
{0x3C, 0xFFE2}, // Shift (Right)
|
||||
{0x3B, 0xFFE3}, // Control (Left)
|
||||
{0x3E, 0xFFE4}, // Control (Right)
|
||||
{0x3A, 0xFFE9}, // Alt (Left)
|
||||
{0x3D, 0xFFEA}, // Alt (Right)
|
||||
{0x37, 0xFFEB}, // Left Command (Windows key)
|
||||
{0x36, 0xFFEC}, // Right Command
|
||||
};
|
||||
|
||||
// X11 KeySym to macOS CGKeyCode
|
||||
// std::map<int, int> x11KeySymToCgKeyCode = []() {
|
||||
// std::map<int, int> result;
|
||||
// for (const auto& pair : cgKeyCodeToX11KeySym) {
|
||||
// result[pair.second] = pair.first;
|
||||
// }
|
||||
// return result;
|
||||
// }();
|
||||
|
||||
std::map<int, int> x11KeySymToCgKeyCode = {
|
||||
// A-Z
|
||||
{0x0041, 0x00}, // A
|
||||
{0x0042, 0x0B}, // B
|
||||
{0x0043, 0x08}, // C
|
||||
{0x0044, 0x02}, // D
|
||||
{0x0045, 0x0E}, // E
|
||||
{0x0046, 0x03}, // F
|
||||
{0x0047, 0x05}, // G
|
||||
{0x0048, 0x04}, // H
|
||||
{0x0049, 0x22}, // I
|
||||
{0x004A, 0x26}, // J
|
||||
{0x004B, 0x28}, // K
|
||||
{0x004C, 0x25}, // L
|
||||
{0x004D, 0x2E}, // M
|
||||
{0x004E, 0x2D}, // N
|
||||
{0x004F, 0x1F}, // O
|
||||
{0x0050, 0x23}, // P
|
||||
{0x0051, 0x0C}, // Q
|
||||
{0x0052, 0x0F}, // R
|
||||
{0x0053, 0x01}, // S
|
||||
{0x0054, 0x11}, // T
|
||||
{0x0055, 0x20}, // U
|
||||
{0x0056, 0x09}, // V
|
||||
{0x0057, 0x0D}, // W
|
||||
{0x0058, 0x07}, // X
|
||||
{0x0059, 0x10}, // Y
|
||||
{0x005A, 0x06}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x0030, 0x1D}, // 0
|
||||
{0x0031, 0x12}, // 1
|
||||
{0x0032, 0x13}, // 2
|
||||
{0x0033, 0x14}, // 3
|
||||
{0x0034, 0x15}, // 4
|
||||
{0x0035, 0x17}, // 5
|
||||
{0x0036, 0x16}, // 6
|
||||
{0x0037, 0x1A}, // 7
|
||||
{0x0038, 0x1C}, // 8
|
||||
{0x0039, 0x19}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0xFFBE, 0x7A}, // F1
|
||||
{0xFFBF, 0x78}, // F2
|
||||
{0xFFC0, 0x63}, // F3
|
||||
{0xFFC1, 0x76}, // F4
|
||||
{0xFFC2, 0x60}, // F5
|
||||
{0xFFC3, 0x61}, // F6
|
||||
{0xFFC4, 0x62}, // F7
|
||||
{0xFFC5, 0x64}, // F8
|
||||
{0xFFC6, 0x65}, // F9
|
||||
{0xFFC7, 0x6D}, // F10
|
||||
{0xFFC8, 0x67}, // F11
|
||||
{0xFFC9, 0x6F}, // F12
|
||||
|
||||
// control keys
|
||||
{0xFF1B, 0x35}, // Escape
|
||||
{0xFF0D, 0x24}, // Enter
|
||||
{0x0020, 0x31}, // Space
|
||||
{0xFF08, 0x33}, // Backspace
|
||||
{0xFF09, 0x30}, // Tab
|
||||
{0xFF15, 0x69}, // Print Screen(F13)
|
||||
{0xFF63, 0x72}, // Insert
|
||||
{0xFFFF, 0x75}, // Delete
|
||||
{0xFF50, 0x73}, // Home
|
||||
{0xFF57, 0x77}, // End
|
||||
{0xFF55, 0x74}, // Page Up
|
||||
{0xFF56, 0x79}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0xFF51, 0x7B}, // Left Arrow
|
||||
{0xFF53, 0x7C}, // Right Arrow
|
||||
{0xFF52, 0x7E}, // Up Arrow
|
||||
{0xFF54, 0x7D}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0xFFB0, 0x52}, // Numpad 0
|
||||
{0xFFB1, 0x53}, // Numpad 1
|
||||
{0xFFB2, 0x54}, // Numpad 2
|
||||
{0xFFB3, 0x55}, // Numpad 3
|
||||
{0xFFB4, 0x56}, // Numpad 4
|
||||
{0xFFB5, 0x57}, // Numpad 5
|
||||
{0xFFB6, 0x58}, // Numpad 6
|
||||
{0xFFB7, 0x59}, // Numpad 7
|
||||
{0xFFB8, 0x5B}, // Numpad 8
|
||||
{0xFFB9, 0x5C}, // Numpad 9
|
||||
{0xFF7F, 0x47}, // Num Lock / Keypad Clear
|
||||
{0xFFAE, 0x41}, // Numpad .
|
||||
{0xFFAF, 0x4B}, // Numpad /
|
||||
{0xFFAA, 0x43}, // Numpad *
|
||||
{0xFFAD, 0x4E}, // Numpad -
|
||||
{0xFFAB, 0x45}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x003B, 0x29}, // ; (Semicolon)
|
||||
{0x0027, 0x27}, // ' (Quote)
|
||||
{0x007E, 0x32}, // ` (Backtick)
|
||||
{0x002C, 0x2B}, // , (Comma)
|
||||
{0x002E, 0x2F}, // . (Period)
|
||||
{0x002F, 0x2C}, // / (Slash)
|
||||
{0x005C, 0x2A}, // \ (Backslash)
|
||||
{0x005B, 0x21}, // [ (Left Bracket)
|
||||
{0x005D, 0x1E}, // ] (Right Bracket)
|
||||
{0x002D, 0x1B}, // - (Minus)
|
||||
{0x003D, 0x18}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0xFFE5, 0x39}, // Caps Lock
|
||||
{0xFFE1, 0x38}, // Shift (Left)
|
||||
{0xFFE2, 0x3C}, // Shift (Right)
|
||||
{0xFFE3, 0x3B}, // Control (Left)
|
||||
{0xFFE4, 0x3E}, // Control (Right)
|
||||
{0xFFE9, 0x3A}, // Alt (Left)
|
||||
{0xFFEA, 0x3D}, // Alt (Right)
|
||||
{0xFFEB, 0x37}, // Left Command
|
||||
{0xFFEC, 0x36}, // Right Command
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,295 @@
|
||||
#ifndef _LINUX_EVDEV_KEYCODE_H_
|
||||
#define _LINUX_EVDEV_KEYCODE_H_
|
||||
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
|
||||
bool extended) {
|
||||
const uint32_t base_scan_code = scan_code & 0xFFu;
|
||||
if (base_scan_code == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (extended) {
|
||||
switch (base_scan_code) {
|
||||
case 0x1C:
|
||||
return KEY_KPENTER;
|
||||
case 0x1D:
|
||||
return KEY_RIGHTCTRL;
|
||||
case 0x35:
|
||||
return KEY_KPSLASH;
|
||||
case 0x38:
|
||||
return KEY_RIGHTALT;
|
||||
case 0x47:
|
||||
return KEY_HOME;
|
||||
case 0x48:
|
||||
return KEY_UP;
|
||||
case 0x49:
|
||||
return KEY_PAGEUP;
|
||||
case 0x4B:
|
||||
return KEY_LEFT;
|
||||
case 0x4D:
|
||||
return KEY_RIGHT;
|
||||
case 0x4F:
|
||||
return KEY_END;
|
||||
case 0x50:
|
||||
return KEY_DOWN;
|
||||
case 0x51:
|
||||
return KEY_PAGEDOWN;
|
||||
case 0x52:
|
||||
return KEY_INSERT;
|
||||
case 0x53:
|
||||
return KEY_DELETE;
|
||||
case 0x5B:
|
||||
return KEY_LEFTMETA;
|
||||
case 0x5C:
|
||||
return KEY_RIGHTMETA;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
|
||||
// with the low byte of the Windows scan code.
|
||||
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
|
||||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
|
||||
base_scan_code == 0x58) {
|
||||
return static_cast<int>(base_scan_code);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0x08:
|
||||
return KEY_BACKSPACE;
|
||||
case 0x09:
|
||||
return KEY_TAB;
|
||||
case 0x0D:
|
||||
return KEY_ENTER;
|
||||
case 0x10:
|
||||
case 0xA0:
|
||||
return KEY_LEFTSHIFT;
|
||||
case 0x11:
|
||||
case 0xA2:
|
||||
return KEY_LEFTCTRL;
|
||||
case 0x12:
|
||||
case 0xA4:
|
||||
return KEY_LEFTALT;
|
||||
case 0x13:
|
||||
return KEY_PAUSE;
|
||||
case 0x14:
|
||||
return KEY_CAPSLOCK;
|
||||
case 0x1B:
|
||||
return KEY_ESC;
|
||||
case 0x20:
|
||||
return KEY_SPACE;
|
||||
case 0x21:
|
||||
return KEY_PAGEUP;
|
||||
case 0x22:
|
||||
return KEY_PAGEDOWN;
|
||||
case 0x23:
|
||||
return KEY_END;
|
||||
case 0x24:
|
||||
return KEY_HOME;
|
||||
case 0x25:
|
||||
return KEY_LEFT;
|
||||
case 0x26:
|
||||
return KEY_UP;
|
||||
case 0x27:
|
||||
return KEY_RIGHT;
|
||||
case 0x28:
|
||||
return KEY_DOWN;
|
||||
case 0x2C:
|
||||
return KEY_SYSRQ;
|
||||
case 0x2D:
|
||||
return KEY_INSERT;
|
||||
case 0x2E:
|
||||
return KEY_DELETE;
|
||||
case 0x30:
|
||||
return KEY_0;
|
||||
case 0x31:
|
||||
return KEY_1;
|
||||
case 0x32:
|
||||
return KEY_2;
|
||||
case 0x33:
|
||||
return KEY_3;
|
||||
case 0x34:
|
||||
return KEY_4;
|
||||
case 0x35:
|
||||
return KEY_5;
|
||||
case 0x36:
|
||||
return KEY_6;
|
||||
case 0x37:
|
||||
return KEY_7;
|
||||
case 0x38:
|
||||
return KEY_8;
|
||||
case 0x39:
|
||||
return KEY_9;
|
||||
case 0x41:
|
||||
return KEY_A;
|
||||
case 0x42:
|
||||
return KEY_B;
|
||||
case 0x43:
|
||||
return KEY_C;
|
||||
case 0x44:
|
||||
return KEY_D;
|
||||
case 0x45:
|
||||
return KEY_E;
|
||||
case 0x46:
|
||||
return KEY_F;
|
||||
case 0x47:
|
||||
return KEY_G;
|
||||
case 0x48:
|
||||
return KEY_H;
|
||||
case 0x49:
|
||||
return KEY_I;
|
||||
case 0x4A:
|
||||
return KEY_J;
|
||||
case 0x4B:
|
||||
return KEY_K;
|
||||
case 0x4C:
|
||||
return KEY_L;
|
||||
case 0x4D:
|
||||
return KEY_M;
|
||||
case 0x4E:
|
||||
return KEY_N;
|
||||
case 0x4F:
|
||||
return KEY_O;
|
||||
case 0x50:
|
||||
return KEY_P;
|
||||
case 0x51:
|
||||
return KEY_Q;
|
||||
case 0x52:
|
||||
return KEY_R;
|
||||
case 0x53:
|
||||
return KEY_S;
|
||||
case 0x54:
|
||||
return KEY_T;
|
||||
case 0x55:
|
||||
return KEY_U;
|
||||
case 0x56:
|
||||
return KEY_V;
|
||||
case 0x57:
|
||||
return KEY_W;
|
||||
case 0x58:
|
||||
return KEY_X;
|
||||
case 0x59:
|
||||
return KEY_Y;
|
||||
case 0x5A:
|
||||
return KEY_Z;
|
||||
case 0x5B:
|
||||
return KEY_LEFTMETA;
|
||||
case 0x5C:
|
||||
return KEY_RIGHTMETA;
|
||||
case 0x60:
|
||||
return KEY_KP0;
|
||||
case 0x61:
|
||||
return KEY_KP1;
|
||||
case 0x62:
|
||||
return KEY_KP2;
|
||||
case 0x63:
|
||||
return KEY_KP3;
|
||||
case 0x64:
|
||||
return KEY_KP4;
|
||||
case 0x65:
|
||||
return KEY_KP5;
|
||||
case 0x66:
|
||||
return KEY_KP6;
|
||||
case 0x67:
|
||||
return KEY_KP7;
|
||||
case 0x68:
|
||||
return KEY_KP8;
|
||||
case 0x69:
|
||||
return KEY_KP9;
|
||||
case 0x6A:
|
||||
return KEY_KPASTERISK;
|
||||
case 0x6B:
|
||||
return KEY_KPPLUS;
|
||||
case 0x6D:
|
||||
return KEY_KPMINUS;
|
||||
case 0x6E:
|
||||
return KEY_KPDOT;
|
||||
case 0x6F:
|
||||
return KEY_KPSLASH;
|
||||
case 0x70:
|
||||
return KEY_F1;
|
||||
case 0x71:
|
||||
return KEY_F2;
|
||||
case 0x72:
|
||||
return KEY_F3;
|
||||
case 0x73:
|
||||
return KEY_F4;
|
||||
case 0x74:
|
||||
return KEY_F5;
|
||||
case 0x75:
|
||||
return KEY_F6;
|
||||
case 0x76:
|
||||
return KEY_F7;
|
||||
case 0x77:
|
||||
return KEY_F8;
|
||||
case 0x78:
|
||||
return KEY_F9;
|
||||
case 0x79:
|
||||
return KEY_F10;
|
||||
case 0x7A:
|
||||
return KEY_F11;
|
||||
case 0x7B:
|
||||
return KEY_F12;
|
||||
case 0x90:
|
||||
return KEY_NUMLOCK;
|
||||
case 0x91:
|
||||
return KEY_SCROLLLOCK;
|
||||
case 0xA1:
|
||||
return KEY_RIGHTSHIFT;
|
||||
case 0xA3:
|
||||
return KEY_RIGHTCTRL;
|
||||
case 0xA5:
|
||||
return KEY_RIGHTALT;
|
||||
case 0xBA:
|
||||
return KEY_SEMICOLON;
|
||||
case 0xBB:
|
||||
return KEY_EQUAL;
|
||||
case 0xBC:
|
||||
return KEY_COMMA;
|
||||
case 0xBD:
|
||||
return KEY_MINUS;
|
||||
case 0xBE:
|
||||
return KEY_DOT;
|
||||
case 0xBF:
|
||||
return KEY_SLASH;
|
||||
case 0xC0:
|
||||
return KEY_GRAVE;
|
||||
case 0xDB:
|
||||
return KEY_LEFTBRACE;
|
||||
case 0xDC:
|
||||
return KEY_BACKSLASH;
|
||||
case 0xDD:
|
||||
return KEY_RIGHTBRACE;
|
||||
case 0xDE:
|
||||
return KEY_APOSTROPHE;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
const int scan_keycode =
|
||||
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
|
||||
if (scan_keycode >= 0) {
|
||||
return scan_keycode;
|
||||
}
|
||||
|
||||
return LinuxEvdevKeycodeFromWindowsVk(key_code);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-05-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MACOS_KEYBOARD_MODIFIER_STATE_H_
|
||||
#define _MACOS_KEYBOARD_MODIFIER_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr uint32_t kMacInjectedModifierShift = 1u << 0;
|
||||
inline constexpr uint32_t kMacInjectedModifierControl = 1u << 1;
|
||||
inline constexpr uint32_t kMacInjectedModifierOption = 1u << 2;
|
||||
inline constexpr uint32_t kMacInjectedModifierCommand = 1u << 3;
|
||||
|
||||
class MacKeyboardModifierState {
|
||||
public:
|
||||
uint32_t Update(int key_code, bool is_down) {
|
||||
bool* state = MutableStateForVk(key_code);
|
||||
if (state != nullptr) {
|
||||
*state = is_down;
|
||||
}
|
||||
return flags();
|
||||
}
|
||||
|
||||
uint32_t flags() const {
|
||||
uint32_t result = 0;
|
||||
if (left_shift_down_ || right_shift_down_) {
|
||||
result |= kMacInjectedModifierShift;
|
||||
}
|
||||
if (left_control_down_ || right_control_down_) {
|
||||
result |= kMacInjectedModifierControl;
|
||||
}
|
||||
if (left_option_down_ || right_option_down_) {
|
||||
result |= kMacInjectedModifierOption;
|
||||
}
|
||||
if (left_command_down_ || right_command_down_) {
|
||||
result |= kMacInjectedModifierCommand;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
left_shift_down_ = false;
|
||||
right_shift_down_ = false;
|
||||
left_control_down_ = false;
|
||||
right_control_down_ = false;
|
||||
left_option_down_ = false;
|
||||
right_option_down_ = false;
|
||||
left_command_down_ = false;
|
||||
right_command_down_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool* MutableStateForVk(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0xA0: // VK_LSHIFT
|
||||
return &left_shift_down_;
|
||||
case 0xA1: // VK_RSHIFT
|
||||
return &right_shift_down_;
|
||||
case 0xA2: // VK_LCONTROL
|
||||
return &left_control_down_;
|
||||
case 0xA3: // VK_RCONTROL
|
||||
return &right_control_down_;
|
||||
case 0xA4: // VK_LMENU / left Option
|
||||
return &left_option_down_;
|
||||
case 0xA5: // VK_RMENU / right Option
|
||||
return &right_option_down_;
|
||||
case 0x5B: // VK_LWIN / left Command
|
||||
return &left_command_down_;
|
||||
case 0x5C: // VK_RWIN / right Command
|
||||
return &right_command_down_;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool left_shift_down_ = false;
|
||||
bool right_shift_down_ = false;
|
||||
bool left_control_down_ = false;
|
||||
bool right_control_down_ = false;
|
||||
bool left_option_down_ = false;
|
||||
bool right_option_down_ = false;
|
||||
bool left_command_down_ = false;
|
||||
bool right_command_down_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,861 @@
|
||||
/*
|
||||
File: HIToolbox/Events.h
|
||||
|
||||
Contains: Event Manager Interfaces.
|
||||
|
||||
Copyright: 锟� 1985-2008 by Apple Computer, Inc., all rights reserved
|
||||
|
||||
Bugs?: For bug reports, consult the following page on
|
||||
the World Wide Web:
|
||||
|
||||
http://developer.apple.com/bugreporter/
|
||||
|
||||
*/
|
||||
#ifndef __EVENTS__
|
||||
#define __EVENTS__
|
||||
|
||||
#ifndef __APPLICATIONSERVICES__
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
#include <AvailabilityMacros.h>
|
||||
|
||||
#if PRAGMA_ONCE
|
||||
#pragma once
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#pragma pack(push, 2)
|
||||
|
||||
typedef UInt16 EventKind;
|
||||
typedef UInt16 EventMask;
|
||||
enum {
|
||||
nullEvent = 0,
|
||||
mouseDown = 1,
|
||||
mouseUp = 2,
|
||||
keyDown = 3,
|
||||
keyUp = 4,
|
||||
autoKey = 5,
|
||||
updateEvt = 6,
|
||||
diskEvt = 7, /* Not sent in Carbon. See kEventClassVolume in CarbonEvents.h*/
|
||||
activateEvt = 8,
|
||||
osEvt = 15,
|
||||
kHighLevelEvent = 23
|
||||
};
|
||||
|
||||
enum {
|
||||
mDownMask = 1 << mouseDown, /* mouse button pressed*/
|
||||
mUpMask = 1 << mouseUp, /* mouse button released*/
|
||||
keyDownMask = 1 << keyDown, /* key pressed*/
|
||||
keyUpMask = 1 << keyUp, /* key released*/
|
||||
autoKeyMask = 1 << autoKey, /* key repeatedly held down*/
|
||||
updateMask = 1 << updateEvt, /* window needs updating*/
|
||||
diskMask = 1 << diskEvt, /* disk inserted*/
|
||||
activMask = 1 << activateEvt, /* activate/deactivate window*/
|
||||
highLevelEventMask = 0x0400, /* high-level events (includes AppleEvents)*/
|
||||
osMask = 1 << osEvt, /* operating system events (suspend, resume)*/
|
||||
everyEvent = 0xFFFF /* all of the above*/
|
||||
};
|
||||
|
||||
enum {
|
||||
charCodeMask = 0x000000FF,
|
||||
keyCodeMask = 0x0000FF00,
|
||||
adbAddrMask = 0x00FF0000,
|
||||
osEvtMessageMask = (UInt32)0xFF000000
|
||||
};
|
||||
|
||||
enum {
|
||||
/* OS event messages. Event (sub)code is in the high byte of the message
|
||||
field.*/
|
||||
mouseMovedMessage = 0x00FA,
|
||||
suspendResumeMessage = 0x0001
|
||||
};
|
||||
|
||||
enum {
|
||||
resumeFlag = 1 /* Bit 0 of message indicates resume vs suspend*/
|
||||
};
|
||||
|
||||
#if CALL_NOT_IN_CARBON
|
||||
/* convertClipboardFlag is not ever set under Carbon. This is because scrap
|
||||
* conversion is */
|
||||
/* not tied to suspend/resume events any longer. Your application should
|
||||
* instead use the */
|
||||
/* scrap promise mechanism and fulfill scrap requests only when your promise
|
||||
* keeper proc */
|
||||
/* is called. If you need to know if the scrap has changed, you can cache the
|
||||
* last */
|
||||
/* ScrapRef you received and compare it with the current ScrapRef */
|
||||
enum {
|
||||
convertClipboardFlag =
|
||||
2 /* Bit 1 in resume message indicates clipboard change*/
|
||||
};
|
||||
|
||||
#endif /* CALL_NOT_IN_CARBON */
|
||||
|
||||
/*
|
||||
CARBON ALERT! BATTLESTATIONS!
|
||||
|
||||
The EventModifiers bits defined here are also used in the newer Carbon Event
|
||||
key modifiers parameters. There are two main differences:
|
||||
|
||||
1) The Carbon key modifiers parameter is a UInt32, not a UInt16. Never try
|
||||
to extract the key modifiers parameter from a Carbon Event into an
|
||||
EventModifiers type. You will probably get your stack trashed. 2) The Carbon
|
||||
key modifiers is just that: key modifiers. That parameter will never contain
|
||||
the button state bit.
|
||||
*/
|
||||
typedef UInt16 EventModifiers;
|
||||
enum {
|
||||
/* modifiers */
|
||||
activeFlagBit = 0, /* activate? (activateEvt and mouseDown)*/
|
||||
btnStateBit = 7, /* state of button?*/
|
||||
cmdKeyBit = 8, /* command key down?*/
|
||||
shiftKeyBit = 9, /* shift key down?*/
|
||||
alphaLockBit = 10, /* alpha lock down?*/
|
||||
optionKeyBit = 11, /* option key down?*/
|
||||
controlKeyBit = 12, /* control key down?*/
|
||||
rightShiftKeyBit = 13, /* right shift key down? Not supported on Mac OS X.*/
|
||||
rightOptionKeyBit = 14, /* right Option key down? Not supported on Mac OS X.*/
|
||||
rightControlKeyBit =
|
||||
15 /* right Control key down? Not supported on Mac OS X.*/
|
||||
};
|
||||
|
||||
enum {
|
||||
activeFlag = 1 << activeFlagBit,
|
||||
btnState = 1 << btnStateBit,
|
||||
cmdKey = 1 << cmdKeyBit,
|
||||
shiftKey = 1 << shiftKeyBit,
|
||||
alphaLock = 1 << alphaLockBit,
|
||||
optionKey = 1 << optionKeyBit,
|
||||
controlKey = 1 << controlKeyBit,
|
||||
rightShiftKey = 1 << rightShiftKeyBit, /* Not supported on Mac OS X.*/
|
||||
rightOptionKey = 1 << rightOptionKeyBit, /* Not supported on Mac OS X.*/
|
||||
rightControlKey = 1 << rightControlKeyBit /* Not supported on Mac OS X.*/
|
||||
};
|
||||
|
||||
/* MacRoman character codes*/
|
||||
enum {
|
||||
kNullCharCode = 0,
|
||||
kHomeCharCode = 1,
|
||||
kEnterCharCode = 3,
|
||||
kEndCharCode = 4,
|
||||
kHelpCharCode = 5,
|
||||
kBellCharCode = 7,
|
||||
kBackspaceCharCode = 8,
|
||||
kTabCharCode = 9,
|
||||
kLineFeedCharCode = 10,
|
||||
kVerticalTabCharCode = 11,
|
||||
kPageUpCharCode = 11,
|
||||
kFormFeedCharCode = 12,
|
||||
kPageDownCharCode = 12,
|
||||
kReturnCharCode = 13,
|
||||
kFunctionKeyCharCode = 16,
|
||||
kCommandCharCode = 17, /* glyph available only in system fonts*/
|
||||
kCheckCharCode = 18, /* glyph available only in system fonts*/
|
||||
kDiamondCharCode = 19, /* glyph available only in system fonts*/
|
||||
kAppleLogoCharCode = 20, /* glyph available only in system fonts*/
|
||||
kEscapeCharCode = 27,
|
||||
kClearCharCode = 27,
|
||||
kLeftArrowCharCode = 28,
|
||||
kRightArrowCharCode = 29,
|
||||
kUpArrowCharCode = 30,
|
||||
kDownArrowCharCode = 31,
|
||||
kSpaceCharCode = 32,
|
||||
kDeleteCharCode = 127,
|
||||
kBulletCharCode = 165,
|
||||
kNonBreakingSpaceCharCode = 202
|
||||
};
|
||||
|
||||
/* useful Unicode code points*/
|
||||
enum {
|
||||
kShiftUnicode = 0x21E7, /* Unicode UPWARDS WHITE ARROW*/
|
||||
kControlUnicode = 0x2303, /* Unicode UP ARROWHEAD*/
|
||||
kOptionUnicode = 0x2325, /* Unicode OPTION KEY*/
|
||||
kCommandUnicode = 0x2318, /* Unicode PLACE OF INTEREST SIGN*/
|
||||
kPencilUnicode = 0x270E, /* Unicode LOWER RIGHT PENCIL; actually pointed left
|
||||
until Mac OS X 10.3*/
|
||||
kPencilLeftUnicode = 0xF802, /* Unicode LOWER LEFT PENCIL; available in Mac OS
|
||||
X 10.3 and later*/
|
||||
kCheckUnicode = 0x2713, /* Unicode CHECK MARK*/
|
||||
kDiamondUnicode = 0x25C6, /* Unicode BLACK DIAMOND*/
|
||||
kBulletUnicode = 0x2022, /* Unicode BULLET*/
|
||||
kAppleLogoUnicode = 0xF8FF /* Unicode APPLE LOGO*/
|
||||
};
|
||||
|
||||
/*
|
||||
* Summary:
|
||||
* Virtual keycodes
|
||||
*
|
||||
* Discussion:
|
||||
* These constants are the virtual keycodes defined originally in
|
||||
* Inside Mac Volume V, pg. V-191. They identify physical keys on a
|
||||
* keyboard. Those constants with "ANSI" in the name are labeled
|
||||
* according to the key position on an ANSI-standard US keyboard.
|
||||
* For example, kVK_ANSI_A indicates the virtual keycode for the key
|
||||
* with the letter 'A' in the US keyboard layout. Other keyboard
|
||||
* layouts may have the 'A' key label on a different physical key;
|
||||
* in this case, pressing 'A' will generate a different virtual
|
||||
* keycode.
|
||||
*/
|
||||
enum {
|
||||
kVK_ANSI_A = 0x00,
|
||||
kVK_ANSI_S = 0x01,
|
||||
kVK_ANSI_D = 0x02,
|
||||
kVK_ANSI_F = 0x03,
|
||||
kVK_ANSI_H = 0x04,
|
||||
kVK_ANSI_G = 0x05,
|
||||
kVK_ANSI_Z = 0x06,
|
||||
kVK_ANSI_X = 0x07,
|
||||
kVK_ANSI_C = 0x08,
|
||||
kVK_ANSI_V = 0x09,
|
||||
kVK_ANSI_B = 0x0B,
|
||||
kVK_ANSI_Q = 0x0C,
|
||||
kVK_ANSI_W = 0x0D,
|
||||
kVK_ANSI_E = 0x0E,
|
||||
kVK_ANSI_R = 0x0F,
|
||||
kVK_ANSI_Y = 0x10,
|
||||
kVK_ANSI_T = 0x11,
|
||||
kVK_ANSI_1 = 0x12,
|
||||
kVK_ANSI_2 = 0x13,
|
||||
kVK_ANSI_3 = 0x14,
|
||||
kVK_ANSI_4 = 0x15,
|
||||
kVK_ANSI_6 = 0x16,
|
||||
kVK_ANSI_5 = 0x17,
|
||||
kVK_ANSI_Equal = 0x18,
|
||||
kVK_ANSI_9 = 0x19,
|
||||
kVK_ANSI_7 = 0x1A,
|
||||
kVK_ANSI_Minus = 0x1B,
|
||||
kVK_ANSI_8 = 0x1C,
|
||||
kVK_ANSI_0 = 0x1D,
|
||||
kVK_ANSI_RightBracket = 0x1E,
|
||||
kVK_ANSI_O = 0x1F,
|
||||
kVK_ANSI_U = 0x20,
|
||||
kVK_ANSI_LeftBracket = 0x21,
|
||||
kVK_ANSI_I = 0x22,
|
||||
kVK_ANSI_P = 0x23,
|
||||
kVK_ANSI_L = 0x25,
|
||||
kVK_ANSI_J = 0x26,
|
||||
kVK_ANSI_Quote = 0x27,
|
||||
kVK_ANSI_K = 0x28,
|
||||
kVK_ANSI_Semicolon = 0x29,
|
||||
kVK_ANSI_Backslash = 0x2A,
|
||||
kVK_ANSI_Comma = 0x2B,
|
||||
kVK_ANSI_Slash = 0x2C,
|
||||
kVK_ANSI_N = 0x2D,
|
||||
kVK_ANSI_M = 0x2E,
|
||||
kVK_ANSI_Period = 0x2F,
|
||||
kVK_ANSI_Grave = 0x32,
|
||||
kVK_ANSI_KeypadDecimal = 0x41,
|
||||
kVK_ANSI_KeypadMultiply = 0x43,
|
||||
kVK_ANSI_KeypadPlus = 0x45,
|
||||
kVK_ANSI_KeypadClear = 0x47,
|
||||
kVK_ANSI_KeypadDivide = 0x4B,
|
||||
kVK_ANSI_KeypadEnter = 0x4C,
|
||||
kVK_ANSI_KeypadMinus = 0x4E,
|
||||
kVK_ANSI_KeypadEquals = 0x51,
|
||||
kVK_ANSI_Keypad0 = 0x52,
|
||||
kVK_ANSI_Keypad1 = 0x53,
|
||||
kVK_ANSI_Keypad2 = 0x54,
|
||||
kVK_ANSI_Keypad3 = 0x55,
|
||||
kVK_ANSI_Keypad4 = 0x56,
|
||||
kVK_ANSI_Keypad5 = 0x57,
|
||||
kVK_ANSI_Keypad6 = 0x58,
|
||||
kVK_ANSI_Keypad7 = 0x59,
|
||||
kVK_ANSI_Keypad8 = 0x5B,
|
||||
kVK_ANSI_Keypad9 = 0x5C
|
||||
};
|
||||
|
||||
/* keycodes for keys that are independent of keyboard layout*/
|
||||
enum {
|
||||
kVK_Return = 0x24,
|
||||
kVK_Tab = 0x30,
|
||||
kVK_Space = 0x31,
|
||||
kVK_Delete = 0x33,
|
||||
kVK_Escape = 0x35,
|
||||
kVK_Command = 0x37,
|
||||
kVK_Shift = 0x38,
|
||||
kVK_CapsLock = 0x39,
|
||||
kVK_Option = 0x3A,
|
||||
kVK_Control = 0x3B,
|
||||
kVK_RightCommand = 0x36,
|
||||
kVK_RightShift = 0x3C,
|
||||
kVK_RightOption = 0x3D,
|
||||
kVK_RightControl = 0x3E,
|
||||
kVK_Function = 0x3F,
|
||||
kVK_F17 = 0x40,
|
||||
kVK_VolumeUp = 0x48,
|
||||
kVK_VolumeDown = 0x49,
|
||||
kVK_Mute = 0x4A,
|
||||
kVK_F18 = 0x4F,
|
||||
kVK_F19 = 0x50,
|
||||
kVK_F20 = 0x5A,
|
||||
kVK_F5 = 0x60,
|
||||
kVK_F6 = 0x61,
|
||||
kVK_F7 = 0x62,
|
||||
kVK_F3 = 0x63,
|
||||
kVK_F8 = 0x64,
|
||||
kVK_F9 = 0x65,
|
||||
kVK_F11 = 0x67,
|
||||
kVK_F13 = 0x69,
|
||||
kVK_F16 = 0x6A,
|
||||
kVK_F14 = 0x6B,
|
||||
kVK_F10 = 0x6D,
|
||||
kVK_ContextualMenu = 0x6E,
|
||||
kVK_F12 = 0x6F,
|
||||
kVK_F15 = 0x71,
|
||||
kVK_Help = 0x72,
|
||||
kVK_Home = 0x73,
|
||||
kVK_PageUp = 0x74,
|
||||
kVK_ForwardDelete = 0x75,
|
||||
kVK_F4 = 0x76,
|
||||
kVK_End = 0x77,
|
||||
kVK_F2 = 0x78,
|
||||
kVK_PageDown = 0x79,
|
||||
kVK_F1 = 0x7A,
|
||||
kVK_LeftArrow = 0x7B,
|
||||
kVK_RightArrow = 0x7C,
|
||||
kVK_DownArrow = 0x7D,
|
||||
kVK_UpArrow = 0x7E
|
||||
};
|
||||
|
||||
/* ISO keyboards only*/
|
||||
enum { kVK_ISO_Section = 0x0A };
|
||||
|
||||
/* JIS keyboards only*/
|
||||
enum {
|
||||
kVK_JIS_Yen = 0x5D,
|
||||
kVK_JIS_Underscore = 0x5E,
|
||||
kVK_JIS_KeypadComma = 0x5F,
|
||||
kVK_JIS_Eisu = 0x66,
|
||||
kVK_JIS_Kana = 0x68
|
||||
};
|
||||
|
||||
struct EventRecord {
|
||||
EventKind what;
|
||||
unsigned long message;
|
||||
UInt32 when;
|
||||
Point where;
|
||||
EventModifiers modifiers;
|
||||
};
|
||||
typedef struct EventRecord EventRecord;
|
||||
typedef CALLBACK_API(void, FKEYProcPtr)(void);
|
||||
typedef STACK_UPP_TYPE(FKEYProcPtr) FKEYUPP;
|
||||
/*
|
||||
* NewFKEYUPP()
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: not available
|
||||
* CarbonLib: not available
|
||||
* Non-Carbon CFM: available as macro/inline
|
||||
*/
|
||||
|
||||
/*
|
||||
* DisposeFKEYUPP()
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: not available
|
||||
* CarbonLib: not available
|
||||
* Non-Carbon CFM: available as macro/inline
|
||||
*/
|
||||
|
||||
/*
|
||||
* InvokeFKEYUPP()
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: not available
|
||||
* CarbonLib: not available
|
||||
* Non-Carbon CFM: available as macro/inline
|
||||
*/
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* GetMouse() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use HIGetMousePosition instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.5 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void GetMouse(Point *mouseLoc)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_5;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
* Button() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use GetCurrentButtonState or GetCurrentEventButtonState instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework but
|
||||
* deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later Non-Carbon
|
||||
* CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean Button(void)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* StillDown()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean StillDown(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
/*
|
||||
* WaitMouseUp()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean WaitMouseUp(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
/*
|
||||
* KeyTranslate() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use UCKeyTranslate instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern UInt32 KeyTranslate(const void *transData, UInt16 keycode, UInt32 *state)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
/*
|
||||
* GetCaretTime()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern UInt32 GetCaretTime(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
QuickTime 3.0 supports GetKeys() on unix and win32
|
||||
But, on little endian machines you will have to be
|
||||
careful about bit numberings and/or use a KeyMapByteArray
|
||||
instead.
|
||||
*/
|
||||
#if TARGET_API_MAC_OS8
|
||||
|
||||
typedef UInt32 KeyMap[4];
|
||||
#else
|
||||
typedef BigEndianUInt32 KeyMap[4];
|
||||
#endif /* TARGET_API_MAC_OS8 */
|
||||
|
||||
typedef UInt8 KeyMapByteArray[16];
|
||||
/*
|
||||
* GetKeys()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework
|
||||
* CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void GetKeys(KeyMap theKeys) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
/* Obsolete event types & masks */
|
||||
enum {
|
||||
networkEvt = 10,
|
||||
driverEvt = 11,
|
||||
app1Evt = 12,
|
||||
app2Evt = 13,
|
||||
app3Evt = 14,
|
||||
app4Evt = 15,
|
||||
networkMask = 0x0400,
|
||||
driverMask = 0x0800,
|
||||
app1Mask = 0x1000,
|
||||
app2Mask = 0x2000,
|
||||
app3Mask = 0x4000,
|
||||
app4Mask = 0x8000
|
||||
};
|
||||
|
||||
struct EvQEl {
|
||||
QElemPtr qLink;
|
||||
SInt16 qType;
|
||||
EventKind
|
||||
evtQWhat; /* this part is identical to the EventRecord as defined above */
|
||||
unsigned long evtQMessage;
|
||||
UInt32 evtQWhen;
|
||||
Point evtQWhere;
|
||||
EventModifiers evtQModifiers;
|
||||
};
|
||||
typedef struct EvQEl EvQEl;
|
||||
typedef EvQEl *EvQElPtr;
|
||||
typedef CALLBACK_API(void, GetNextEventFilterProcPtr)(EventRecord *theEvent,
|
||||
Boolean *result);
|
||||
typedef STACK_UPP_TYPE(GetNextEventFilterProcPtr) GetNextEventFilterUPP;
|
||||
/*
|
||||
* NewGetNextEventFilterUPP()
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: not available
|
||||
* CarbonLib: not available
|
||||
* Non-Carbon CFM: available as macro/inline
|
||||
*/
|
||||
|
||||
/*
|
||||
* DisposeGetNextEventFilterUPP()
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: not available
|
||||
* CarbonLib: not available
|
||||
* Non-Carbon CFM: available as macro/inline
|
||||
*/
|
||||
|
||||
/*
|
||||
* InvokeGetNextEventFilterUPP()
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: not available
|
||||
* CarbonLib: not available
|
||||
* Non-Carbon CFM: available as macro/inline
|
||||
*/
|
||||
|
||||
typedef GetNextEventFilterUPP GNEFilterUPP;
|
||||
#if !__LP64__
|
||||
/*
|
||||
* GetDblTime()
|
||||
*
|
||||
* Summary:
|
||||
* Returns the maximum time (in units of 1/60th of a second) allowed
|
||||
* between two consecutive mouse-down events in order for the second
|
||||
* click to be considered a double-click.
|
||||
*
|
||||
* Discussion:
|
||||
* In 64-bit applications, you may replace calls to this API with
|
||||
* calls to NXClickTime (declared in
|
||||
* <IOKit/hidsystem/event_status_driver.h>) or with +[NSEvent
|
||||
* doubleClickInterval] (available in Mac OS X 10.6 and later).
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Result:
|
||||
* The maximum time between mouse-downs allowed for a double-click.
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern UInt32 GetDblTime(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
/*
|
||||
* SetEventMask()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void SetEventMask(EventMask value)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
/*
|
||||
* GetNextEvent() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use ReceiveNextEvent instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean GetNextEvent(EventMask eventMask, EventRecord *theEvent)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
/*
|
||||
* WaitNextEvent() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use ReceiveNextEvent instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean WaitNextEvent(EventMask eventMask, EventRecord *theEvent,
|
||||
UInt32 sleep, RgnHandle mouseRgn) /* can be NULL */
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
/*
|
||||
* EventAvail() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use FindSpecificEventInQueue instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean EventAvail(EventMask eventMask, EventRecord *theEvent)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
/*
|
||||
* PostEvent() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use PostEventToQueue or CGEventPost instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern OSErr PostEvent(EventKind eventNum, UInt32 eventMsg)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
* FlushEvents() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use FlushEventsMatchingListFromQueue,
|
||||
* FlushSpecificEventsFromQueue, or FlushEventQueue instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework but
|
||||
* deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later Non-Carbon
|
||||
* CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void FlushEvents(EventMask whichMask, EventMask stopMask)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
#if OLDROUTINENAMES
|
||||
#define KeyTrans(transData, keycode, state) \
|
||||
KeyTranslate(transData, keycode, state)
|
||||
#endif /* OLDROUTINENAMES */
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* KeyScript() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use TISSelectInputSource API for positive verbs (ScriptCode).
|
||||
* Use TSMDocument properties to restrict input sources:
|
||||
* kTSMDocumentEnabledInputSourcesPropertyTag
|
||||
* kTSMDocumentInputSourceOverridePropertyTag
|
||||
*
|
||||
* Summary:
|
||||
* Switch to the specified script's default (last used) input source.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] but deprecated in 10.5 CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void KeyScript(short code)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_5;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
* IsCmdChar() *** DEPRECATED ***
|
||||
*
|
||||
* Deprecated:
|
||||
* Use IsUserCancelEventRef or CheckEventQueueForUserCancel instead.
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework but
|
||||
* deprecated in 10.6 CarbonLib: in CarbonLib 1.0 and later Non-Carbon
|
||||
* CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern Boolean IsCmdChar(const EventRecord *event, short test)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER_BUT_DEPRECATED_IN_MAC_OS_X_VERSION_10_6;
|
||||
|
||||
/*
|
||||
LowMem accessor functions previously in LowMem.h
|
||||
*/
|
||||
/*
|
||||
* LMGetKeyThresh()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework
|
||||
* CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern SInt16 LMGetKeyThresh(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* LMSetKeyThresh()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void LMSetKeyThresh(SInt16 value)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
* LMGetKeyRepThresh()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework
|
||||
* CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern SInt16 LMGetKeyRepThresh(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* LMSetKeyRepThresh()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void LMSetKeyRepThresh(SInt16 value)
|
||||
AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
* LMGetKbdLast()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework
|
||||
* CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern UInt8 LMGetKbdLast(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* LMSetKbdLast()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void LMSetKbdLast(UInt8 value) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
/*
|
||||
* LMGetKbdType()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework
|
||||
* CarbonLib: in CarbonLib 1.0 and later
|
||||
* Non-Carbon CFM: in InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern UInt8 LMGetKbdType(void) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#if !__LP64__
|
||||
/*
|
||||
* LMSetKbdType()
|
||||
*
|
||||
* Mac OS X threading:
|
||||
* Not thread safe
|
||||
*
|
||||
* Availability:
|
||||
* Mac OS X: in version 10.0 and later in Carbon.framework [32-bit
|
||||
* only] CarbonLib: in CarbonLib 1.0 and later Non-Carbon CFM: in
|
||||
* InterfaceLib 7.1 and later
|
||||
*/
|
||||
extern void LMSetKbdType(UInt8 value) AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;
|
||||
|
||||
#endif /* !__LP64__ */
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __EVENTS__ */
|
||||
@@ -1,127 +1,199 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include "log.h"
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
MouseController::MouseController() {}
|
||||
|
||||
MouseController::~MouseController() {
|
||||
if (uinput_fd_) {
|
||||
ioctl(uinput_fd_, UI_DEV_DESTROY);
|
||||
close(uinput_fd_);
|
||||
MouseController::~MouseController() { Destroy(); }
|
||||
|
||||
int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
display_info_list_ = display_info_list;
|
||||
|
||||
if (IsWaylandSession()) {
|
||||
if (InitWaylandPortal()) {
|
||||
use_wayland_portal_ = true;
|
||||
LOG_INFO("Mouse controller initialized with Wayland portal backend");
|
||||
return 0;
|
||||
}
|
||||
LOG_WARN(
|
||||
"Wayland mouse control init failed, falling back to X11/XTest backend");
|
||||
}
|
||||
}
|
||||
|
||||
int MouseController::Init(int screen_width, int screen_height) {
|
||||
screen_width_ = screen_width;
|
||||
screen_height_ = screen_height;
|
||||
|
||||
uinput_fd_ = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
|
||||
if (uinput_fd_ < 0) {
|
||||
LOG_ERROR("Cannot open device: /dev/uinput");
|
||||
display_ = XOpenDisplay(NULL);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Cannot connect to X server");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ioctl(uinput_fd_, UI_SET_EVBIT, EV_KEY);
|
||||
ioctl(uinput_fd_, UI_SET_KEYBIT, BTN_RIGHT);
|
||||
ioctl(uinput_fd_, UI_SET_KEYBIT, BTN_LEFT);
|
||||
ioctl(uinput_fd_, UI_SET_EVBIT, EV_ABS);
|
||||
ioctl(uinput_fd_, UI_SET_ABSBIT, ABS_X);
|
||||
ioctl(uinput_fd_, UI_SET_ABSBIT, ABS_Y);
|
||||
ioctl(uinput_fd_, UI_SET_EVBIT, EV_REL);
|
||||
root_ = DefaultRootWindow(display_);
|
||||
|
||||
struct uinput_user_dev uidev;
|
||||
memset(&uidev, 0, sizeof(uidev));
|
||||
snprintf(uidev.name, UINPUT_MAX_NAME_SIZE, "VirtualMouse");
|
||||
uidev.id.bustype = BUS_USB;
|
||||
uidev.id.version = 1;
|
||||
uidev.id.vendor = 0x1;
|
||||
uidev.id.product = 0x1;
|
||||
uidev.absmin[ABS_X] = 0;
|
||||
uidev.absmax[ABS_X] = screen_width_;
|
||||
uidev.absmin[ABS_Y] = 0;
|
||||
uidev.absmax[ABS_Y] = screen_height_;
|
||||
|
||||
int res_uidev = write(uinput_fd_, &uidev, sizeof(uidev));
|
||||
ioctl(uinput_fd_, UI_DEV_CREATE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::SendCommand(RemoteAction remote_action) {
|
||||
int mouse_pos_x = remote_action.m.x * screen_width_ / 1280;
|
||||
int mouse_pos_y = remote_action.m.y * screen_height_ / 720;
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
struct input_event event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
gettimeofday(&event.time, NULL);
|
||||
|
||||
if (remote_action.m.flag == MouseFlag::left_down) {
|
||||
SimulateKeyDown(uinput_fd_, BTN_LEFT);
|
||||
} else if (remote_action.m.flag == MouseFlag::left_up) {
|
||||
SimulateKeyUp(uinput_fd_, BTN_LEFT);
|
||||
} else if (remote_action.m.flag == MouseFlag::right_down) {
|
||||
SimulateKeyDown(uinput_fd_, BTN_RIGHT);
|
||||
} else if (remote_action.m.flag == MouseFlag::right_up) {
|
||||
SimulateKeyUp(uinput_fd_, BTN_RIGHT);
|
||||
} else {
|
||||
SetMousePosition(uinput_fd_, mouse_pos_x, mouse_pos_y);
|
||||
}
|
||||
int event_base, error_base, major_version, minor_version;
|
||||
if (!XTestQueryExtension(display_, &event_base, &error_base, &major_version,
|
||||
&minor_version)) {
|
||||
LOG_ERROR("XTest extension not available");
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
return -2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MouseController::SimulateKeyDown(int fd, int kval) {
|
||||
int res_ev = 0;
|
||||
struct input_event event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
gettimeofday(&event.time, 0);
|
||||
void MouseController::UpdateDisplayInfoList(
|
||||
const std::vector<DisplayInfo>& display_info_list) {
|
||||
if (display_info_list.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.type = EV_KEY;
|
||||
event.value = 1;
|
||||
event.code = kval;
|
||||
res_ev = write(fd, &event, sizeof(event));
|
||||
display_info_list_ = display_info_list;
|
||||
if (use_wayland_portal_) {
|
||||
OnWaylandDisplayInfoListUpdated();
|
||||
}
|
||||
|
||||
event.type = EV_SYN;
|
||||
event.value = 0;
|
||||
event.code = SYN_REPORT;
|
||||
res_ev = write(fd, &event, sizeof(event));
|
||||
if (last_display_index_ < 0 ||
|
||||
last_display_index_ >= static_cast<int>(display_info_list_.size())) {
|
||||
last_display_index_ = -1;
|
||||
last_norm_x_ = -1.0;
|
||||
last_norm_y_ = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
void MouseController::SimulateKeyUp(int fd, int kval) {
|
||||
int res_ev = 0;
|
||||
struct input_event event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
gettimeofday(&event.time, 0);
|
||||
int MouseController::Destroy() {
|
||||
CleanupWaylandPortal();
|
||||
|
||||
event.type = EV_KEY;
|
||||
event.value = 0;
|
||||
event.code = kval;
|
||||
res_ev = write(fd, &event, sizeof(event));
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
|
||||
event.type = EV_SYN;
|
||||
event.value = 0;
|
||||
event.code = SYN_REPORT;
|
||||
res_ev = write(fd, &event, sizeof(event));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MouseController::SetMousePosition(int fd, int x, int y) {
|
||||
struct input_event ev[2], ev_sync;
|
||||
memset(ev, 0, sizeof(ev));
|
||||
memset(&ev_sync, 0, sizeof(ev_sync));
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
if (remote_action.type != ControlType::mouse) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ev[0].type = EV_ABS;
|
||||
ev[0].code = ABS_X;
|
||||
ev[0].value = x;
|
||||
ev[1].type = EV_ABS;
|
||||
ev[1].code = ABS_Y;
|
||||
ev[1].value = y;
|
||||
int res_w = write(fd, ev, sizeof(ev));
|
||||
if (use_wayland_portal_) {
|
||||
return SendWaylandMouseCommand(remote_action, display_index);
|
||||
}
|
||||
|
||||
ev_sync.type = EV_SYN;
|
||||
ev_sync.value = 0;
|
||||
ev_sync.code = 0;
|
||||
int res_ev_sync = write(fd, &ev_sync, sizeof(ev_sync));
|
||||
}
|
||||
if (!display_) {
|
||||
LOG_ERROR("X11 display not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (remote_action.type) {
|
||||
case mouse:
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::move: {
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("Invalid display index: {}", display_index);
|
||||
return -2;
|
||||
}
|
||||
|
||||
SetMousePosition(
|
||||
static_cast<int>(remote_action.m.x *
|
||||
display_info_list_[display_index].width +
|
||||
display_info_list_[display_index].left),
|
||||
static_cast<int>(remote_action.m.y *
|
||||
display_info_list_[display_index].height +
|
||||
display_info_list_[display_index].top));
|
||||
break;
|
||||
}
|
||||
case MouseFlag::left_down:
|
||||
XTestFakeButtonEvent(display_, 1, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
XTestFakeButtonEvent(display_, 1, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
XTestFakeButtonEvent(display_, 3, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
XTestFakeButtonEvent(display_, 3, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
XTestFakeButtonEvent(display_, 2, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
XTestFakeButtonEvent(display_, 2, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical: {
|
||||
if (remote_action.m.s > 0) {
|
||||
SimulateMouseWheel(4, remote_action.m.s);
|
||||
} else if (remote_action.m.s < 0) {
|
||||
SimulateMouseWheel(5, -remote_action.m.s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MouseFlag::wheel_horizontal: {
|
||||
if (remote_action.m.s > 0) {
|
||||
SimulateMouseWheel(6, remote_action.m.s);
|
||||
} else if (remote_action.m.s < 0) {
|
||||
SimulateMouseWheel(7, -remote_action.m.s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MouseController::SetMousePosition(int x, int y) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
XWarpPointer(display_, None, root_, 0, 0, 0, 0, x, y);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
void MouseController::SimulateKeyDown(int kval) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
XTestFakeKeyEvent(display_, kval, True, CurrentTime);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
void MouseController::SimulateKeyUp(int kval) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
XTestFakeKeyEvent(display_, kval, False, CurrentTime);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
void MouseController::SimulateMouseWheel(int direction_button, int count) {
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
XTestFakeButtonEvent(display_, direction_button, True, CurrentTime);
|
||||
XTestFakeButtonEvent(display_, direction_button, False, CurrentTime);
|
||||
}
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,41 +1,77 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2023-12-14
|
||||
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
|
||||
* @Date: 2025-05-07
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MOUSE_CONTROLLER_H_
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/uinput.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessageIter;
|
||||
struct _XDisplay;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class MouseController : public DeviceController {
|
||||
public:
|
||||
MouseController();
|
||||
virtual ~MouseController();
|
||||
|
||||
public:
|
||||
virtual int Init(int screen_width, int screen_height);
|
||||
virtual int Init(std::vector<DisplayInfo> display_info_list);
|
||||
virtual int Destroy();
|
||||
virtual int SendCommand(RemoteAction remote_action);
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
void UpdateDisplayInfoList(const std::vector<DisplayInfo>& display_info_list);
|
||||
|
||||
private:
|
||||
void SimulateKeyDown(int fd, int kval);
|
||||
void SimulateKeyUp(int fd, int kval);
|
||||
void SetMousePosition(int fd, int x, int y);
|
||||
void SimulateKeyDown(int kval);
|
||||
void SimulateKeyUp(int kval);
|
||||
void SetMousePosition(int x, int y);
|
||||
void SimulateMouseWheel(int direction_button, int count);
|
||||
bool InitWaylandPortal();
|
||||
void CleanupWaylandPortal();
|
||||
int SendWaylandMouseCommand(RemoteAction remote_action, int display_index);
|
||||
void OnWaylandDisplayInfoListUpdated();
|
||||
bool NotifyWaylandPointerMotion(double dx, double dy);
|
||||
bool NotifyWaylandPointerMotionAbsolute(uint32_t stream, double x, double y);
|
||||
bool NotifyWaylandPointerButton(int button, uint32_t state);
|
||||
bool NotifyWaylandPointerAxisDiscrete(uint32_t axis, int32_t steps);
|
||||
bool SendWaylandPortalVoidCall(
|
||||
const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>& append_args);
|
||||
|
||||
private:
|
||||
int uinput_fd_;
|
||||
struct uinput_user_dev uinput_dev_;
|
||||
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
|
||||
|
||||
_XDisplay* display_ = nullptr;
|
||||
unsigned long root_ = 0;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
int screen_width_ = 0;
|
||||
int screen_height_ = 0;
|
||||
};
|
||||
bool use_wayland_portal_ = false;
|
||||
|
||||
#endif
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
std::string wayland_session_handle_;
|
||||
int last_display_index_ = -1;
|
||||
double last_norm_x_ = -1.0;
|
||||
double last_norm_y_ = -1.0;
|
||||
bool logged_wayland_display_info_ = false;
|
||||
uintptr_t last_logged_wayland_stream_ = 0;
|
||||
int last_logged_wayland_width_ = 0;
|
||||
int last_logged_wayland_height_ = 0;
|
||||
WaylandAbsoluteMode wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown;
|
||||
bool wayland_absolute_disabled_logged_ = false;
|
||||
uint32_t wayland_absolute_stream_id_ = 0;
|
||||
int wayland_portal_space_width_ = 0;
|
||||
int wayland_portal_space_height_ = 0;
|
||||
bool using_shared_wayland_session_ = false;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -2,48 +2,186 @@
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include "log.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDoubleClickInterval = std::chrono::milliseconds(500);
|
||||
constexpr int kDoubleClickMaxDistance = 8;
|
||||
constexpr int kMaxClickState = 3;
|
||||
|
||||
bool IsWithinClickDistance(int x1, int y1, int x2, int y2) {
|
||||
return std::abs(x1 - x2) <= kDoubleClickMaxDistance &&
|
||||
std::abs(y1 - y2) <= kDoubleClickMaxDistance;
|
||||
}
|
||||
|
||||
void SetClickState(CGEventRef event, int click_state) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGEventSetIntegerValueField(
|
||||
event, kCGMouseEventClickState,
|
||||
std::max(1, std::min(click_state, kMaxClickState)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MouseController::MouseController() {}
|
||||
|
||||
MouseController::~MouseController() {}
|
||||
|
||||
int MouseController::Init(int screen_width, int screen_height) {
|
||||
screen_width_ = screen_width;
|
||||
screen_height_ = screen_height;
|
||||
int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
display_info_list_ = display_info_list;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::SendCommand(RemoteAction remote_action) {
|
||||
int mouse_pos_x = remote_action.m.x * screen_width_ / 1280;
|
||||
int mouse_pos_y = remote_action.m.y * screen_height_ / 720;
|
||||
int MouseController::BeginClick(ClickTracker& tracker, int x, int y) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool continues_previous_click =
|
||||
tracker.has_last_down &&
|
||||
now - tracker.last_down_time <= kDoubleClickInterval &&
|
||||
IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y);
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
CGEventRef mouse_event;
|
||||
CGEventType mouse_type;
|
||||
tracker.click_state = continues_previous_click
|
||||
? std::min(tracker.click_state + 1, kMaxClickState)
|
||||
: 1;
|
||||
tracker.active_click_state = tracker.click_state;
|
||||
tracker.has_last_down = true;
|
||||
tracker.last_down_time = now;
|
||||
tracker.last_down_x = x;
|
||||
tracker.last_down_y = y;
|
||||
|
||||
if (remote_action.m.flag == MouseFlag::left_down) {
|
||||
return tracker.active_click_state;
|
||||
}
|
||||
|
||||
int MouseController::EndClick(ClickTracker& tracker, int x, int y) {
|
||||
const int click_state = tracker.active_click_state;
|
||||
if (!IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y)) {
|
||||
tracker.has_last_down = false;
|
||||
tracker.click_state = 0;
|
||||
tracker.active_click_state = 1;
|
||||
}
|
||||
|
||||
return click_state;
|
||||
}
|
||||
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
if (remote_action.type != ControlType::mouse) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
display_index, display_info_list_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
const DisplayInfo& display_info = display_info_list_[display_index];
|
||||
if (display_info.width <= 0 || display_info.height <= 0) {
|
||||
LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
|
||||
display_info.width, display_info.height);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
int mouse_pos_x = normalized_x * display_info.width + display_info.left;
|
||||
int mouse_pos_y = normalized_y * display_info.height + display_info.top;
|
||||
|
||||
CGEventRef mouse_event = nullptr;
|
||||
CGEventType mouse_type;
|
||||
CGMouseButton mouse_button;
|
||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||
int click_state = 1;
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
mouse_type = kCGEventLeftMouseDown;
|
||||
} else if (remote_action.m.flag == MouseFlag::left_up) {
|
||||
left_dragging_ = true;
|
||||
click_state = BeginClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
mouse_type = kCGEventLeftMouseUp;
|
||||
} else if (remote_action.m.flag == MouseFlag::right_down) {
|
||||
left_dragging_ = false;
|
||||
click_state = EndClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
mouse_type = kCGEventRightMouseDown;
|
||||
} else if (remote_action.m.flag == MouseFlag::right_up) {
|
||||
right_dragging_ = true;
|
||||
click_state = BeginClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
mouse_type = kCGEventRightMouseUp;
|
||||
} else {
|
||||
mouse_type = kCGEventMouseMoved;
|
||||
}
|
||||
right_dragging_ = false;
|
||||
click_state = EndClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
mouse_type = kCGEventOtherMouseDown;
|
||||
click_state = BeginClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
mouse_type = kCGEventOtherMouseUp;
|
||||
click_state = EndClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
|
||||
2, remote_action.m.s, 0);
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
|
||||
2, 0, remote_action.m.s);
|
||||
break;
|
||||
default:
|
||||
if (left_dragging_) {
|
||||
mouse_type = kCGEventLeftMouseDragged;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
} else if (right_dragging_) {
|
||||
mouse_type = kCGEventRightMouseDragged;
|
||||
mouse_button = kCGMouseButtonRight;
|
||||
} else {
|
||||
mouse_type = kCGEventMouseMoved;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
}
|
||||
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type,
|
||||
CGPointMake(mouse_pos_x, mouse_pos_y),
|
||||
kCGMouseButtonLeft);
|
||||
mouse_event =
|
||||
CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, mouse_button);
|
||||
break;
|
||||
}
|
||||
|
||||
if (mouse_event) {
|
||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||
CFRelease(mouse_event);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,21 +7,42 @@
|
||||
#ifndef _MOUSE_CONTROLLER_H_
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class MouseController : public DeviceController {
|
||||
public:
|
||||
MouseController();
|
||||
virtual ~MouseController();
|
||||
|
||||
public:
|
||||
virtual int Init(int screen_width, int screen_height);
|
||||
virtual int Init(std::vector<DisplayInfo> display_info_list);
|
||||
virtual int Destroy();
|
||||
virtual int SendCommand(RemoteAction remote_action);
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
|
||||
private:
|
||||
int screen_width_ = 0;
|
||||
int screen_height_ = 0;
|
||||
};
|
||||
struct ClickTracker {
|
||||
bool has_last_down = false;
|
||||
std::chrono::steady_clock::time_point last_down_time{};
|
||||
int last_down_x = 0;
|
||||
int last_down_y = 0;
|
||||
int click_state = 0;
|
||||
int active_click_state = 1;
|
||||
};
|
||||
|
||||
#endif
|
||||
int BeginClick(ClickTracker& tracker, int x, int y);
|
||||
int EndClick(ClickTracker& tracker, int x, int y);
|
||||
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
bool left_dragging_ = false;
|
||||
bool right_dragging_ = false;
|
||||
ClickTracker left_click_tracker_;
|
||||
ClickTracker right_click_tracker_;
|
||||
ClickTracker middle_click_tracker_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -1,47 +1,96 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include "log.h"
|
||||
#include <Windows.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
MouseController::MouseController() {}
|
||||
|
||||
MouseController::~MouseController() {}
|
||||
|
||||
int MouseController::Init(int screen_width, int screen_height) {
|
||||
screen_width_ = screen_width;
|
||||
screen_height_ = screen_height;
|
||||
int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
display_info_list_ = display_info_list;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::SendCommand(RemoteAction remote_action) {
|
||||
INPUT ip;
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
display_index, display_info_list_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
INPUT ip = {0};
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
ip.type = INPUT_MOUSE;
|
||||
ip.mi.dx = remote_action.m.x * screen_width_ / 1280;
|
||||
ip.mi.dy = remote_action.m.y * screen_height_ / 720;
|
||||
if (remote_action.m.flag == MouseFlag::left_down) {
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
} else if (remote_action.m.flag == MouseFlag::left_up) {
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE;
|
||||
} else if (remote_action.m.flag == MouseFlag::right_down) {
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
} else if (remote_action.m.flag == MouseFlag::right_up) {
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_ABSOLUTE;
|
||||
} else {
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MOVE;
|
||||
ip.mi.dx =
|
||||
(LONG)(remote_action.m.x * display_info_list_[display_index].width) +
|
||||
display_info_list_[display_index].left;
|
||||
ip.mi.dy =
|
||||
(LONG)(remote_action.m.y * display_info_list_[display_index].height) +
|
||||
display_info_list_[display_index].top;
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEUP | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_WHEEL;
|
||||
ip.mi.mouseData = remote_action.m.s * 120;
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_HWHEEL;
|
||||
ip.mi.mouseData = remote_action.m.s * 120;
|
||||
break;
|
||||
default:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MOVE;
|
||||
break;
|
||||
}
|
||||
ip.mi.mouseData = 0;
|
||||
|
||||
ip.mi.time = 0;
|
||||
|
||||
SetCursorPos(ip.mi.dx, ip.mi.dy);
|
||||
if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
|
||||
LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, static_cast<int>(remote_action.m.flag),
|
||||
GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
|
||||
SendInput(1, &ip, sizeof(INPUT));
|
||||
UINT sent = SendInput(1, &ip, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN(
|
||||
"SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), GetLastError());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,21 +7,24 @@
|
||||
#ifndef _MOUSE_CONTROLLER_H_
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class MouseController : public DeviceController {
|
||||
public:
|
||||
MouseController();
|
||||
virtual ~MouseController();
|
||||
|
||||
public:
|
||||
virtual int Init(int screen_width, int screen_height);
|
||||
virtual int Init(std::vector<DisplayInfo> display_info_list);
|
||||
virtual int Destroy();
|
||||
virtual int SendCommand(RemoteAction remote_action);
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
|
||||
private:
|
||||
int screen_width_ = 0;
|
||||
int screen_height_ = 0;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-05-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_KEY_METADATA_H_
|
||||
#define _WINDOWS_KEY_METADATA_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool LookupWindowsKeyMetadataFromVk(int key_code,
|
||||
uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (scan_code_out == nullptr || extended_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (key_code) {
|
||||
case 0x21: // Page Up
|
||||
*scan_code_out = 0x49;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x22: // Page Down
|
||||
*scan_code_out = 0x51;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x23: // End
|
||||
*scan_code_out = 0x4F;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x24: // Home
|
||||
*scan_code_out = 0x47;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x25: // Left Arrow
|
||||
*scan_code_out = 0x4B;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x26: // Up Arrow
|
||||
*scan_code_out = 0x48;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x27: // Right Arrow
|
||||
*scan_code_out = 0x4D;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x28: // Down Arrow
|
||||
*scan_code_out = 0x50;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x2D: // Insert
|
||||
*scan_code_out = 0x52;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x2E: // Delete
|
||||
*scan_code_out = 0x53;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x6F: // Numpad /
|
||||
*scan_code_out = 0x35;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0xA3: // Right Ctrl
|
||||
*scan_code_out = 0x1D;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0xA5: // Right Alt
|
||||
*scan_code_out = 0x38;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x5B: // Left Win
|
||||
*scan_code_out = 0x5B;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x5C: // Right Win
|
||||
*scan_code_out = 0x5C;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,206 @@
|
||||
// virtual_key_codes.h
|
||||
#ifndef VIRTUAL_KEY_CODES_H
|
||||
#define VIRTUAL_KEY_CODES_H
|
||||
|
||||
#define VK_LBUTTON 0x01 // Left mouse button
|
||||
#define VK_RBUTTON 0x02 // Right mouse button
|
||||
#define VK_CANCEL 0x03 // Control-break processing
|
||||
#define VK_MBUTTON 0x04 // Middle mouse button
|
||||
#define VK_XBUTTON1 0x05 // X1 mouse button
|
||||
#define VK_XBUTTON2 0x06 // X2 mouse button
|
||||
// 0x07 Reserved
|
||||
#define VK_BACK 0x08 // Backspace key
|
||||
#define VK_TAB 0x09 // Tab key
|
||||
// 0x0A-0B Reserved
|
||||
#define VK_CLEAR 0x0C // Clear key
|
||||
#define VK_RETURN 0x0D // Enter key
|
||||
// 0x0E-0F Unassigned
|
||||
#define VK_SHIFT 0x10 // Shift key
|
||||
#define VK_CONTROL 0x11 // Ctrl key
|
||||
#define VK_MENU 0x12 // Alt key
|
||||
#define VK_PAUSE 0x13 // Pause key
|
||||
#define VK_CAPITAL 0x14 // Caps lock key
|
||||
#define VK_KANA 0x15 // IME Kana mode
|
||||
#define VK_HANGUL 0x15 // IME Hangul mode
|
||||
#define VK_IME_ON 0x16 // IME On
|
||||
#define VK_JUNJA 0x17 // IME Junja mode
|
||||
#define VK_FINAL 0x18 // IME final mode
|
||||
#define VK_HANJA 0x19 // IME Hanja mode
|
||||
#define VK_KANJI 0x19 // IME Kanji mode
|
||||
#define VK_IME_OFF 0x1A // IME Off
|
||||
#define VK_ESCAPE 0x1B // Esc key
|
||||
#define VK_CONVERT 0x1C // IME convert
|
||||
#define VK_NONCONVERT 0x1D // IME nonconvert
|
||||
#define VK_ACCEPT 0x1E // IME accept
|
||||
#define VK_MODECHANGE 0x1F // IME mode change request
|
||||
#define VK_SPACE 0x20 // Spacebar key
|
||||
#define VK_PRIOR 0x21 // Page up key
|
||||
#define VK_NEXT 0x22 // Page down key
|
||||
#define VK_END 0x23 // End key
|
||||
#define VK_HOME 0x24 // Home key
|
||||
#define VK_LEFT 0x25 // Left arrow key
|
||||
#define VK_UP 0x26 // Up arrow key
|
||||
#define VK_RIGHT 0x27 // Right arrow key
|
||||
#define VK_DOWN 0x28 // Down arrow key
|
||||
#define VK_SELECT 0x29 // Select key
|
||||
#define VK_PRINT 0x2A // Print key
|
||||
#define VK_EXECUTE 0x2B // Execute key
|
||||
#define VK_SNAPSHOT 0x2C // Print screen key
|
||||
#define VK_INSERT 0x2D // Insert key
|
||||
#define VK_DELETE 0x2E // Delete key
|
||||
#define VK_HELP 0x2F // Help key
|
||||
|
||||
#define VK_0 0x30 // 0 key
|
||||
#define VK_1 0x31 // 1 key
|
||||
#define VK_2 0x32 // 2 key
|
||||
#define VK_3 0x33 // 3 key
|
||||
#define VK_4 0x34 // 4 key
|
||||
#define VK_5 0x35 // 5 key
|
||||
#define VK_6 0x36 // 6 key
|
||||
#define VK_7 0x37 // 7 key
|
||||
#define VK_8 0x38 // 8 key
|
||||
#define VK_9 0x39 // 9 key
|
||||
// 0x3A-40 Undefined
|
||||
|
||||
#define VK_A 0x41 // A key
|
||||
#define VK_B 0x42 // B key
|
||||
#define VK_C 0x43 // C key
|
||||
#define VK_D 0x44 // D key
|
||||
#define VK_E 0x45 // E key
|
||||
#define VK_F 0x46 // F key
|
||||
#define VK_G 0x47 // G key
|
||||
#define VK_H 0x48 // H key
|
||||
#define VK_I 0x49 // I key
|
||||
#define VK_J 0x4A // J key
|
||||
#define VK_K 0x4B // K key
|
||||
#define VK_L 0x4C // L key
|
||||
#define VK_M 0x4D // M key
|
||||
#define VK_N 0x4E // N key
|
||||
#define VK_O 0x4F // O key
|
||||
#define VK_P 0x50 // P key
|
||||
#define VK_Q 0x51 // Q key
|
||||
#define VK_R 0x52 // R key
|
||||
#define VK_S 0x53 // S key
|
||||
#define VK_T 0x54 // T key
|
||||
#define VK_U 0x55 // U key
|
||||
#define VK_V 0x56 // V key
|
||||
#define VK_W 0x57 // W key
|
||||
#define VK_X 0x58 // X key
|
||||
#define VK_Y 0x59 // Y key
|
||||
#define VK_Z 0x5A // Z key
|
||||
|
||||
#define VK_LWIN 0x5B // Left Windows logo key
|
||||
#define VK_RWIN 0x5C // Right Windows logo key
|
||||
#define VK_APPS 0x5D // Application key
|
||||
// 0x5E Reserved
|
||||
#define VK_SLEEP 0x5F // Computer Sleep key
|
||||
|
||||
#define VK_NUMPAD0 0x60 // Numeric keypad 0 key
|
||||
#define VK_NUMPAD1 0x61 // Numeric keypad 1 key
|
||||
#define VK_NUMPAD2 0x62 // Numeric keypad 2 key
|
||||
#define VK_NUMPAD3 0x63 // Numeric keypad 3 key
|
||||
#define VK_NUMPAD4 0x64 // Numeric keypad 4 key
|
||||
#define VK_NUMPAD5 0x65 // Numeric keypad 5 key
|
||||
#define VK_NUMPAD6 0x66 // Numeric keypad 6 key
|
||||
#define VK_NUMPAD7 0x67 // Numeric keypad 7 key
|
||||
#define VK_NUMPAD8 0x68 // Numeric keypad 8 key
|
||||
#define VK_NUMPAD9 0x69 // Numeric keypad 9 key
|
||||
#define VK_MULTIPLY 0x6A // Multiply key
|
||||
#define VK_ADD 0x6B // Add key
|
||||
#define VK_SEPARATOR 0x6C // Separator key
|
||||
#define VK_SUBTRACT 0x6D // Subtract key
|
||||
#define VK_DECIMAL 0x6E // Decimal key
|
||||
#define VK_DIVIDE 0x6F // Divide key
|
||||
|
||||
#define VK_F1 0x70 // F1 key
|
||||
#define VK_F2 0x71 // F2 key
|
||||
#define VK_F3 0x72 // F3 key
|
||||
#define VK_F4 0x73 // F4 key
|
||||
#define VK_F5 0x74 // F5 key
|
||||
#define VK_F6 0x75 // F6 key
|
||||
#define VK_F7 0x76 // F7 key
|
||||
#define VK_F8 0x77 // F8 key
|
||||
#define VK_F9 0x78 // F9 key
|
||||
#define VK_F10 0x79 // F10 key
|
||||
#define VK_F11 0x7A // F11 key
|
||||
#define VK_F12 0x7B // F12 key
|
||||
#define VK_F13 0x7C // F13 key
|
||||
#define VK_F14 0x7D // F14 key
|
||||
#define VK_F15 0x7E // F15 key
|
||||
#define VK_F16 0x7F // F16 key
|
||||
#define VK_F17 0x80 // F17 key
|
||||
#define VK_F18 0x81 // F18 key
|
||||
#define VK_F19 0x82 // F19 key
|
||||
#define VK_F20 0x83 // F20 key
|
||||
#define VK_F21 0x84 // F21 key
|
||||
#define VK_F22 0x85 // F22 key
|
||||
#define VK_F23 0x86 // F23 key
|
||||
#define VK_F24 0x87 // F24 key
|
||||
// 0x88–0x8F Reserved
|
||||
|
||||
#define VK_NUMLOCK 0x90 // Num lock key
|
||||
#define VK_SCROLL 0x91 // Scroll lock key
|
||||
// 0x92–0x96 OEM specific
|
||||
// 0x97–0x9F Unassigned
|
||||
|
||||
#define VK_LSHIFT 0xA0 // Left Shift key
|
||||
#define VK_RSHIFT 0xA1 // Right Shift key
|
||||
#define VK_LCONTROL 0xA2 // Left Ctrl key
|
||||
#define VK_RCONTROL 0xA3 // Right Ctrl key
|
||||
#define VK_LMENU 0xA4 // Left Alt key
|
||||
#define VK_RMENU 0xA5 // Right Alt key
|
||||
|
||||
#define VK_BROWSER_BACK 0xA6 // Browser Back key
|
||||
#define VK_BROWSER_FORWARD 0xA7 // Browser Forward key
|
||||
#define VK_BROWSER_REFRESH 0xA8 // Browser Refresh key
|
||||
#define VK_BROWSER_STOP 0xA9 // Browser Stop key
|
||||
#define VK_BROWSER_SEARCH 0xAA // Browser Search key
|
||||
#define VK_BROWSER_FAVORITES 0xAB // Browser Favorites key
|
||||
#define VK_BROWSER_HOME 0xAC // Browser Start and Home key
|
||||
#define VK_VOLUME_MUTE 0xAD // Volume Mute key
|
||||
#define VK_VOLUME_DOWN 0xAE // Volume Down key
|
||||
#define VK_VOLUME_UP 0xAF // Volume Up key
|
||||
|
||||
#define VK_MEDIA_NEXT_TRACK 0xB0 // Next Track key
|
||||
#define VK_MEDIA_PREV_TRACK 0xB1 // Previous Track key
|
||||
#define VK_MEDIA_STOP 0xB2 // Stop Media key
|
||||
#define VK_MEDIA_PLAY_PAUSE 0xB3 // Play/Pause Media key
|
||||
#define VK_LAUNCH_MAIL 0xB4 // Start Mail key
|
||||
#define VK_LAUNCH_MEDIA_SELECT 0xB5 // Select Media key
|
||||
#define VK_LAUNCH_APP1 0xB6 // Start Application 1 key
|
||||
#define VK_LAUNCH_APP2 0xB7 // Start Application 2 key
|
||||
// 0xB8–0xB9 Reserved
|
||||
|
||||
#define VK_OEM_1 0xBA // For US: Semicolon/Colon key
|
||||
#define VK_OEM_PLUS 0xBB // Equals/Plus key
|
||||
#define VK_OEM_COMMA 0xBC // Comma/Less Than key
|
||||
#define VK_OEM_MINUS 0xBD // Dash/Underscore key
|
||||
#define VK_OEM_PERIOD 0xBE // Period/Greater Than key
|
||||
#define VK_OEM_2 0xBF // Slash/Question Mark key
|
||||
#define VK_OEM_3 0xC0 // Grave Accent/Tilde key
|
||||
// 0xC1–0xDA Reserved
|
||||
|
||||
#define VK_OEM_4 0xDB // Left Brace key
|
||||
#define VK_OEM_5 0xDC // Backslash/Pipe key
|
||||
#define VK_OEM_6 0xDD // Right Brace key
|
||||
#define VK_OEM_7 0xDE // Apostrophe/Quote key
|
||||
#define VK_OEM_8 0xDF // (Canadian CSA: Right Ctrl key)
|
||||
// 0xE0 Reserved
|
||||
#define VK_OEM_102 0xE2 // (European ISO: Backslash/Pipe key)
|
||||
// 0xE3–E4 OEM specific
|
||||
#define VK_PROCESSKEY 0xE5 // IME PROCESS key
|
||||
// 0xE6 OEM specific
|
||||
#define VK_PACKET 0xE7 // Unicode characters as keystrokes
|
||||
// 0xE8 Unassigned
|
||||
// 0xE9–F5 OEM specific
|
||||
#define VK_ATTN 0xF6 // Attn key
|
||||
#define VK_CRSEL 0xF7 // CrSel key
|
||||
#define VK_EXSEL 0xF8 // ExSel key
|
||||
#define VK_EREOF 0xF9 // Erase EOF key
|
||||
#define VK_PLAY 0xFA // Play key
|
||||
#define VK_ZOOM 0xFB // Zoom key
|
||||
#define VK_NONAME 0xFC // Reserved
|
||||
#define VK_PA1 0xFD // PA1 key
|
||||
#define VK_OEM_CLEAR 0xFE // Clear key
|
||||
|
||||
#endif // VIRTUAL_KEY_CODES_H
|
||||
@@ -0,0 +1,191 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct MainWindowState {
|
||||
float title_bar_width_ = 640;
|
||||
float title_bar_height_ = 30;
|
||||
float title_bar_button_width_ = 30;
|
||||
float title_bar_button_height_ = 30;
|
||||
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16;
|
||||
|
||||
bool main_window_minimized_ = false;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
float main_window_dpi_scaling_w_ = 1.0f;
|
||||
float main_window_dpi_scaling_h_ = 1.0f;
|
||||
float dpi_scale_ = 1.0f;
|
||||
float main_window_width_default_ = 640;
|
||||
float main_window_height_default_ = 480;
|
||||
float main_window_width_ = 640;
|
||||
float main_window_height_ = 480;
|
||||
float main_window_width_last_ = 640;
|
||||
float main_window_height_last_ = 480;
|
||||
float local_window_width_ = 320;
|
||||
float local_window_height_ = 235;
|
||||
float remote_window_width_ = 320;
|
||||
float remote_window_height_ = 235;
|
||||
float local_child_window_width_ = 266;
|
||||
float local_child_window_height_ = 180;
|
||||
float remote_child_window_width_ = 266;
|
||||
float remote_child_window_height_ = 180;
|
||||
float main_window_text_y_padding_ = 10;
|
||||
float main_child_window_x_padding_ = 27;
|
||||
float main_child_window_y_padding_ = 45;
|
||||
float status_bar_height_ = 22;
|
||||
float connection_status_window_width_ = 200;
|
||||
float connection_status_window_height_ = 150;
|
||||
float notification_window_width_ = 200;
|
||||
float notification_window_height_ = 80;
|
||||
float about_window_width_ = 300;
|
||||
float about_window_height_ = 170;
|
||||
float update_notification_window_width_ = 400;
|
||||
float update_notification_window_height_ = 320;
|
||||
};
|
||||
|
||||
// Application-global interaction flags that coordinate SDL events with media
|
||||
// and remote-control devices.
|
||||
struct InteractionState {
|
||||
bool start_mouse_controller_ = false;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool start_screen_capturer_ = false;
|
||||
bool screen_capturer_is_started_ = false;
|
||||
bool start_speaker_capturer_ = false;
|
||||
bool speaker_capturer_is_started_ = false;
|
||||
bool start_keyboard_capturer_ = false;
|
||||
bool show_cursor_ = false;
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool keyboard_capturer_uses_sdl_events_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool focus_on_stream_window_ = false;
|
||||
bool audio_capture_ = false;
|
||||
int screen_width_ = 1280;
|
||||
int screen_height_ = 720;
|
||||
int selected_display_ = 0;
|
||||
std::string connect_button_label_ = "Connect";
|
||||
char input_password_tmp_[7] = "";
|
||||
char input_password_[7] = "";
|
||||
std::string random_password_;
|
||||
char new_password_[7] = "";
|
||||
char remote_id_display_[12] = "";
|
||||
unsigned char audio_buffer_[720]{};
|
||||
int audio_len_ = 0;
|
||||
bool audio_buffer_fresh_ = false;
|
||||
bool need_to_rejoin_ = false;
|
||||
std::chrono::steady_clock::time_point last_rejoin_check_time_ =
|
||||
std::chrono::steady_clock::now();
|
||||
bool just_created_ = false;
|
||||
std::string controlled_remote_id_;
|
||||
std::string focused_remote_id_;
|
||||
std::string remote_client_id_;
|
||||
};
|
||||
|
||||
struct UpdateState {
|
||||
nlohmann::json latest_version_info_ = nlohmann::json{};
|
||||
bool update_available_ = false;
|
||||
std::string latest_version_;
|
||||
std::string release_name_;
|
||||
std::string release_notes_;
|
||||
std::string release_date_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
};
|
||||
|
||||
struct StreamWindowState {
|
||||
bool need_to_create_stream_window_ = false;
|
||||
bool stream_window_created_ = false;
|
||||
bool stream_window_inited_ = false;
|
||||
bool window_maximized_ = false;
|
||||
bool control_mouse_ = false;
|
||||
int stream_window_width_default_ = 1280;
|
||||
int stream_window_height_default_ = 720;
|
||||
float stream_window_width_ = 1280;
|
||||
float stream_window_height_ = 720;
|
||||
int stream_window_width_real_ = 1280;
|
||||
int stream_window_height_real_ = 720;
|
||||
float stream_window_dpi_scaling_w_ = 1.0f;
|
||||
float stream_window_dpi_scaling_h_ = 1.0f;
|
||||
};
|
||||
|
||||
struct ServerWindowState {
|
||||
bool need_to_create_server_window_ = false;
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
float window_rounding_ = 6.0f;
|
||||
float window_rounding_default_ = 6.0f;
|
||||
bool server_window_collapsed_ = false;
|
||||
bool server_window_collapsed_dragging_ = false;
|
||||
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_collapsed_drag_start_win_x_ = 0;
|
||||
int server_window_collapsed_drag_start_win_y_ = 0;
|
||||
bool server_window_dragging_ = false;
|
||||
float server_window_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_drag_start_win_x_ = 0;
|
||||
int server_window_drag_start_win_y_ = 0;
|
||||
};
|
||||
|
||||
struct UiState {
|
||||
bool label_inited_ = false;
|
||||
bool connect_button_pressed_ = false;
|
||||
bool password_validating_ = false;
|
||||
uint32_t password_validating_time_ = 0;
|
||||
bool show_settings_window_ = false;
|
||||
bool show_self_hosted_server_config_window_ = false;
|
||||
bool rejoin_ = false;
|
||||
bool local_id_copied_ = false;
|
||||
bool show_password_ = true;
|
||||
bool show_about_window_ = false;
|
||||
bool show_connection_status_window_ = false;
|
||||
bool show_reset_password_window_ = false;
|
||||
bool show_update_notification_window_ = false;
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool focus_on_input_widget_ = true;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool show_edit_connection_alias_window_ = false;
|
||||
bool show_offline_warning_window_ = false;
|
||||
bool delete_connection_ = false;
|
||||
bool is_tab_bar_hovered_ = false;
|
||||
std::string delete_connection_name_;
|
||||
std::string edit_connection_alias_remote_id_;
|
||||
char edit_connection_alias_[128] = "";
|
||||
std::string offline_warning_text_;
|
||||
bool re_enter_remote_id_ = false;
|
||||
double copy_start_time_ = 0;
|
||||
};
|
||||
|
||||
struct ApplicationState : MainWindowState,
|
||||
InteractionState,
|
||||
UpdateState,
|
||||
StreamWindowState,
|
||||
ServerWindowState,
|
||||
UiState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Slint application shell. GuiRuntime remains the owner of transport, media,
|
||||
// device, clipboard, transfer and settings behavior.
|
||||
class GuiApplication final : private GuiRuntime {
|
||||
public:
|
||||
GuiApplication();
|
||||
~GuiApplication();
|
||||
|
||||
int Run();
|
||||
|
||||
private:
|
||||
struct SlintUi;
|
||||
|
||||
void InitializeLogger();
|
||||
void InitializeSettings();
|
||||
bool InitializeSDL();
|
||||
void InitializeModules();
|
||||
void InitializeUi();
|
||||
void InitializeSystemTray();
|
||||
bool MinimizeMainWindowToTray();
|
||||
void BindMainCallbacks();
|
||||
void BindStreamCallbacks();
|
||||
void BindServerCallbacks();
|
||||
void Tick();
|
||||
void SyncMainWindow();
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
void SyncServerWindow();
|
||||
void UpdateLocalization();
|
||||
void ResetSettingsUi();
|
||||
void SaveSettingsFromUi();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
void CheckPortableWindowsService();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
void ConnectFromUi(const std::string &remote_id);
|
||||
void SelectStreamTab(int index);
|
||||
void ReorderStreamTab(int from, float drop_x, float tab_width);
|
||||
void CloseStreamTab(const std::string &remote_id);
|
||||
void SendPointerInput(int button, int kind, float x, float y);
|
||||
void SendScrollInput(float delta_x, float delta_y, float x, float y);
|
||||
void SendKeyInput(const std::string &text, bool pressed, bool control,
|
||||
bool alt, bool shift, bool meta);
|
||||
void Cleanup();
|
||||
|
||||
std::shared_ptr<RemoteSession> SelectedSession();
|
||||
std::string OpenFileDialog(const std::string &title);
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_H_
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "service_host.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetCurrentExecutablePath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD length = GetModuleFileNameW(
|
||||
nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < buffer.size()) {
|
||||
return std::filesystem::path(buffer.data(), buffer.data() + length);
|
||||
}
|
||||
if (buffer.size() >= 32768) {
|
||||
return {};
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool InstallServiceWithElevation() {
|
||||
const std::filesystem::path executable_path = GetCurrentExecutablePath();
|
||||
if (executable_path.empty()) {
|
||||
LOG_ERROR("Portable service install failed: current executable not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path service_path =
|
||||
executable_path.parent_path() / L"crossdesk_service.exe";
|
||||
const std::filesystem::path helper_path =
|
||||
executable_path.parent_path() / L"crossdesk_session_helper.exe";
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR("Portable service install failed: service binaries missing, "
|
||||
"service={}, helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring executable = executable_path.wstring();
|
||||
const std::wstring working_dir = executable_path.parent_path().wstring();
|
||||
constexpr wchar_t parameters[] = L"--service-install";
|
||||
|
||||
SHELLEXECUTEINFOW execute_info{};
|
||||
execute_info.cbSize = sizeof(execute_info);
|
||||
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
execute_info.lpVerb = L"runas";
|
||||
execute_info.lpFile = executable.c_str();
|
||||
execute_info.lpParameters = parameters;
|
||||
execute_info.lpDirectory = working_dir.c_str();
|
||||
execute_info.nShow = SW_HIDE;
|
||||
|
||||
if (!ShellExecuteExW(&execute_info)) {
|
||||
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const DWORD wait_result =
|
||||
WaitForSingleObject(execute_info.hProcess, INFINITE);
|
||||
DWORD exit_code = 1;
|
||||
if (wait_result == WAIT_OBJECT_0) {
|
||||
GetExitCodeProcess(execute_info.hProcess, &exit_code);
|
||||
} else {
|
||||
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
|
||||
}
|
||||
CloseHandle(execute_info.hProcess);
|
||||
|
||||
if (exit_code != 0) {
|
||||
LOG_ERROR("Portable service install command failed, exit_code={}",
|
||||
exit_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool started = StartCrossDeskService();
|
||||
if (!started) {
|
||||
LOG_WARN("Portable service installed but start failed");
|
||||
}
|
||||
return IsCrossDeskServiceInstalled() && started;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GuiApplication::CheckPortableWindowsService() {
|
||||
if (portable_service_prompt_checked_) {
|
||||
return;
|
||||
}
|
||||
portable_service_prompt_checked_ = true;
|
||||
portable_service_installed_ = IsCrossDeskServiceInstalled();
|
||||
if (portable_service_installed_ || portable_service_prompt_suppressed_) {
|
||||
return;
|
||||
}
|
||||
|
||||
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||
std::memory_order_relaxed);
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void GuiApplication::StartPortableWindowsServiceInstall() {
|
||||
portable_service_do_not_remind_ = false;
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
expected, PortableServiceInstallState::installing,
|
||||
std::memory_order_acq_rel)) {
|
||||
if (expected != PortableServiceInstallState::failed) {
|
||||
return;
|
||||
}
|
||||
portable_service_install_state_.store(
|
||||
PortableServiceInstallState::installing, std::memory_order_release);
|
||||
}
|
||||
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
portable_service_install_thread_ = std::thread([this]() {
|
||||
const bool installed = InstallServiceWithElevation();
|
||||
portable_service_install_state_.store(
|
||||
installed ? PortableServiceInstallState::succeeded
|
||||
: PortableServiceInstallState::failed,
|
||||
std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiApplication::JoinPortableWindowsServiceInstallThread() {
|
||||
if (portable_service_install_thread_.joinable()) {
|
||||
portable_service_install_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,320 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
|
||||
if (main_ctx_) {
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
} else {
|
||||
LOG_ERROR("Main context is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream_window_inited_) {
|
||||
if (stream_ctx_) {
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
} else {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (server_window_inited_) {
|
||||
if (server_ctx_) {
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
} else {
|
||||
LOG_ERROR("Server context is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
|
||||
LOG_INFO("Quit program from system tray");
|
||||
if (stream_window_) {
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
}
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (tray_) {
|
||||
tray_->RemoveTrayIcon();
|
||||
}
|
||||
#endif
|
||||
exit_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (clipboard_.event_type() != 0 && event.type == clipboard_.event_type()) {
|
||||
clipboard_.ApplyPendingRemoteText();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_QUIT:
|
||||
if (stream_window_inited_) {
|
||||
LOG_INFO("Destroy stream window");
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
DestroyStreamWindow();
|
||||
DestroyStreamWindowContext();
|
||||
|
||||
{
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &[host_name, props] : remote_sessions_) {
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
thumbnail_->SaveToThumbnail(
|
||||
(char *)frame_snapshot->data(), video_width, video_height,
|
||||
host_name, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
}
|
||||
|
||||
if (props->peer_) {
|
||||
std::string client_id = (host_name == client_id_)
|
||||
? "C-" + std::string(client_id_)
|
||||
: client_id_;
|
||||
LOG_INFO("[{}] Leave connection [{}]", client_id, host_name);
|
||||
LeaveConnection(props->peer_, host_name.c_str());
|
||||
LOG_INFO("Destroy peer [{}]", client_id);
|
||||
DestroyPeer(&props->peer_);
|
||||
}
|
||||
|
||||
props->streaming_ = false;
|
||||
props->remember_password_ = false;
|
||||
props->connection_established_ = false;
|
||||
props->audio_capture_button_pressed_ = false;
|
||||
|
||||
memset(&props->net_traffic_stats_, 0,
|
||||
sizeof(props->net_traffic_stats_));
|
||||
SDL_SetWindowFullscreen(main_window_, false);
|
||||
SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT);
|
||||
memset(audio_buffer_, 0, 720);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
remote_sessions_.clear();
|
||||
}
|
||||
|
||||
rejoin_ = false;
|
||||
is_client_mode_ = false;
|
||||
reload_recent_connections_ = true;
|
||||
fullscreen_button_pressed_ = false;
|
||||
start_keyboard_capturer_ = false;
|
||||
just_created_ = false;
|
||||
recent_connection_image_save_time_ = SDL_GetTicks();
|
||||
} else {
|
||||
LOG_INFO("Quit program");
|
||||
exit_ = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
if (stream_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(stream_window_)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (main_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(main_window_) &&
|
||||
MinimizeMainWindowToTray()) {
|
||||
break;
|
||||
}
|
||||
|
||||
exit_ = true;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
if (stream_window_created_ &&
|
||||
event.window.windowID == SDL_GetWindowID(stream_window_)) {
|
||||
UpdateRenderRect();
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
if (stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
focus_on_stream_window_ = true;
|
||||
} else if (main_window_ &&
|
||||
SDL_GetWindowID(main_window_) == event.window.windowID) {
|
||||
foucs_on_main_window_ = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
if (stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
focus_on_stream_window_ = false;
|
||||
} else if (main_window_ &&
|
||||
SDL_GetWindowID(main_window_) == event.window.windowID) {
|
||||
foucs_on_main_window_ = false;
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
transfers_.HandleDropEvent(event);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_CLIPBOARD_UPDATE:
|
||||
clipboard_.HandleLocalUpdate();
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
case SDL_EVENT_MOUSE_WHEEL: {
|
||||
Uint32 mouse_window_id = 0;
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
mouse_window_id = event.motion.windowID;
|
||||
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
mouse_window_id = event.button.windowID;
|
||||
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
mouse_window_id = event.wheel.windowID;
|
||||
}
|
||||
|
||||
if (focus_on_stream_window_ && stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == mouse_window_id) {
|
||||
ProcessMouseEvent(event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ &&
|
||||
focus_on_stream_window_ && stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.key.windowID) {
|
||||
ProcessKeyboardEvent(event);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (event.type == STREAM_REFRESH_EVENT) {
|
||||
auto *props = static_cast<RemoteSession *>(event.user.data1);
|
||||
if (!props) {
|
||||
break;
|
||||
}
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
bool render_rect_dirty = false;
|
||||
bool cleanup_pending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
cleanup_pending = props->stream_cleanup_pending_;
|
||||
if (!cleanup_pending) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
render_rect_dirty = props->render_rect_dirty_;
|
||||
}
|
||||
|
||||
if (cleanup_pending) {
|
||||
if (props->stream_texture_) {
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (video_width <= 0 || video_height <= 0) {
|
||||
break;
|
||||
}
|
||||
if (!frame_snapshot || frame_snapshot->empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (props->stream_texture_) {
|
||||
if (video_width != props->texture_width_ ||
|
||||
video_height != props->texture_height_) {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
// props->texture_height_);
|
||||
|
||||
SDL_PropertiesID nvProps = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
props->texture_width_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
props->texture_height_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_NV12);
|
||||
SDL_SetNumberProperty(nvProps,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
SDL_COLORSPACE_BT601_LIMITED);
|
||||
props->stream_texture_ =
|
||||
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
} else {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
// props->texture_height_);
|
||||
|
||||
SDL_PropertiesID nvProps = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
props->texture_width_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
props->texture_height_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_NV12);
|
||||
SDL_SetNumberProperty(nvProps,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
SDL_COLORSPACE_BT601_LIMITED);
|
||||
props->stream_texture_ =
|
||||
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
|
||||
props->texture_width_);
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,415 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent &event) {
|
||||
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||
return 0x90;
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_KP_0:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2D;
|
||||
}
|
||||
return 0x60;
|
||||
case SDL_SCANCODE_KP_1:
|
||||
if (!numlock_enabled) {
|
||||
return 0x23;
|
||||
}
|
||||
return 0x61;
|
||||
case SDL_SCANCODE_KP_2:
|
||||
if (!numlock_enabled) {
|
||||
return 0x28;
|
||||
}
|
||||
return 0x62;
|
||||
case SDL_SCANCODE_KP_3:
|
||||
if (!numlock_enabled) {
|
||||
return 0x22;
|
||||
}
|
||||
return 0x63;
|
||||
case SDL_SCANCODE_KP_4:
|
||||
if (!numlock_enabled) {
|
||||
return 0x25;
|
||||
}
|
||||
return 0x64;
|
||||
case SDL_SCANCODE_KP_5:
|
||||
return 0x65;
|
||||
case SDL_SCANCODE_KP_6:
|
||||
if (!numlock_enabled) {
|
||||
return 0x27;
|
||||
}
|
||||
return 0x66;
|
||||
case SDL_SCANCODE_KP_7:
|
||||
if (!numlock_enabled) {
|
||||
return 0x24;
|
||||
}
|
||||
return 0x67;
|
||||
case SDL_SCANCODE_KP_8:
|
||||
if (!numlock_enabled) {
|
||||
return 0x26;
|
||||
}
|
||||
return 0x68;
|
||||
case SDL_SCANCODE_KP_9:
|
||||
if (!numlock_enabled) {
|
||||
return 0x21;
|
||||
}
|
||||
return 0x69;
|
||||
case SDL_SCANCODE_KP_PERIOD:
|
||||
case SDL_SCANCODE_KP_COMMA:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2E;
|
||||
}
|
||||
return 0x6E;
|
||||
case SDL_SCANCODE_KP_DIVIDE:
|
||||
return 0x6F;
|
||||
case SDL_SCANCODE_KP_MULTIPLY:
|
||||
return 0x6A;
|
||||
case SDL_SCANCODE_KP_MINUS:
|
||||
return 0x6D;
|
||||
case SDL_SCANCODE_KP_PLUS:
|
||||
return 0x6B;
|
||||
case SDL_SCANCODE_KP_EQUALS:
|
||||
return 0xBB;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent &event) {
|
||||
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
|
||||
if (keypad_key_code >= 0) {
|
||||
return keypad_key_code;
|
||||
}
|
||||
|
||||
const int key = static_cast<int>(event.key);
|
||||
if (key >= 'a' && key <= 'z') {
|
||||
return key - 'a' + 0x41;
|
||||
}
|
||||
if (key >= 'A' && key <= 'Z') {
|
||||
return key;
|
||||
}
|
||||
if (key >= '0' && key <= '9') {
|
||||
return key;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case ';':
|
||||
return 0xBA;
|
||||
case '\'':
|
||||
return 0xDE;
|
||||
case '`':
|
||||
return 0xC0;
|
||||
case ',':
|
||||
return 0xBC;
|
||||
case '.':
|
||||
return 0xBE;
|
||||
case '/':
|
||||
return 0xBF;
|
||||
case '\\':
|
||||
return 0xDC;
|
||||
case '[':
|
||||
return 0xDB;
|
||||
case ']':
|
||||
return 0xDD;
|
||||
case '-':
|
||||
return 0xBD;
|
||||
case '=':
|
||||
return 0xBB;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
return 0x1B;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_SPACE:
|
||||
return 0x20;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
return 0x08;
|
||||
case SDL_SCANCODE_TAB:
|
||||
return 0x09;
|
||||
case SDL_SCANCODE_PRINTSCREEN:
|
||||
return 0x2C;
|
||||
case SDL_SCANCODE_SCROLLLOCK:
|
||||
return 0x91;
|
||||
case SDL_SCANCODE_PAUSE:
|
||||
return 0x13;
|
||||
case SDL_SCANCODE_INSERT:
|
||||
return 0x2D;
|
||||
case SDL_SCANCODE_DELETE:
|
||||
return 0x2E;
|
||||
case SDL_SCANCODE_HOME:
|
||||
return 0x24;
|
||||
case SDL_SCANCODE_END:
|
||||
return 0x23;
|
||||
case SDL_SCANCODE_PAGEUP:
|
||||
return 0x21;
|
||||
case SDL_SCANCODE_PAGEDOWN:
|
||||
return 0x22;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
return 0x25;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
return 0x27;
|
||||
case SDL_SCANCODE_UP:
|
||||
return 0x26;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
return 0x28;
|
||||
case SDL_SCANCODE_F1:
|
||||
return 0x70;
|
||||
case SDL_SCANCODE_F2:
|
||||
return 0x71;
|
||||
case SDL_SCANCODE_F3:
|
||||
return 0x72;
|
||||
case SDL_SCANCODE_F4:
|
||||
return 0x73;
|
||||
case SDL_SCANCODE_F5:
|
||||
return 0x74;
|
||||
case SDL_SCANCODE_F6:
|
||||
return 0x75;
|
||||
case SDL_SCANCODE_F7:
|
||||
return 0x76;
|
||||
case SDL_SCANCODE_F8:
|
||||
return 0x77;
|
||||
case SDL_SCANCODE_F9:
|
||||
return 0x78;
|
||||
case SDL_SCANCODE_F10:
|
||||
return 0x79;
|
||||
case SDL_SCANCODE_F11:
|
||||
return 0x7A;
|
||||
case SDL_SCANCODE_F12:
|
||||
return 0x7B;
|
||||
case SDL_SCANCODE_CAPSLOCK:
|
||||
return 0x14;
|
||||
case SDL_SCANCODE_LSHIFT:
|
||||
return 0xA0;
|
||||
case SDL_SCANCODE_RSHIFT:
|
||||
return 0xA1;
|
||||
case SDL_SCANCODE_LCTRL:
|
||||
return 0xA2;
|
||||
case SDL_SCANCODE_RCTRL:
|
||||
return 0xA3;
|
||||
case SDL_SCANCODE_LALT:
|
||||
return 0xA4;
|
||||
case SDL_SCANCODE_RALT:
|
||||
return 0xA5;
|
||||
case SDL_SCANCODE_LGUI:
|
||||
return 0x5B;
|
||||
case SDL_SCANCODE_RGUI:
|
||||
return 0x5C;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
||||
if (key_code < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return keyboard_.SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
|
||||
}
|
||||
|
||||
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
|
||||
controlled_remote_id_ = "";
|
||||
RemoteAction remote_action{};
|
||||
float cursor_x = last_mouse_event.motion.x;
|
||||
float cursor_y = last_mouse_event.motion.y;
|
||||
|
||||
auto normalize_cursor_to_window_space = [&](float *x, float *y) {
|
||||
if (!x || !y || !stream_window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
int window_width = 0;
|
||||
int window_height = 0;
|
||||
int pixel_width = 0;
|
||||
int pixel_height = 0;
|
||||
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
|
||||
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
|
||||
|
||||
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
|
||||
pixel_height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((window_width != pixel_width || window_height != pixel_height) &&
|
||||
(*x > static_cast<float>(window_width) + 1.0f ||
|
||||
*y > static_cast<float>(window_height) + 1.0f)) {
|
||||
const float scale_x =
|
||||
static_cast<float>(window_width) / static_cast<float>(pixel_width);
|
||||
const float scale_y =
|
||||
static_cast<float>(window_height) / static_cast<float>(pixel_height);
|
||||
*x *= scale_x;
|
||||
*y *= scale_y;
|
||||
|
||||
static bool logged_pixel_to_window_conversion = false;
|
||||
if (!logged_pixel_to_window_conversion) {
|
||||
LOG_INFO(
|
||||
"Mouse coordinate space converted from pixels to window units: "
|
||||
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
|
||||
window_width, window_height, pixel_width, pixel_height, scale_x,
|
||||
scale_y);
|
||||
logged_pixel_to_window_conversion = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
cursor_x = event.motion.x;
|
||||
cursor_y = event.motion.y;
|
||||
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
|
||||
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
cursor_x = event.button.x;
|
||||
cursor_y = event.button.y;
|
||||
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
|
||||
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
cursor_x = last_mouse_event.motion.x;
|
||||
cursor_y = last_mouse_event.motion.y;
|
||||
}
|
||||
|
||||
const bool is_pointer_position_event =
|
||||
(event.type == SDL_EVENT_MOUSE_MOTION ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
|
||||
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (!props->control_mouse_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool file_transfer_window_hovered =
|
||||
props->file_transfer_.file_transfer_window_hovered_;
|
||||
const bool overlay_hovered =
|
||||
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
|
||||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
|
||||
|
||||
const SDL_FRect render_rect = props->stream_render_rect_f_;
|
||||
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_pointer_position_event && cursor_x >= render_rect.x &&
|
||||
cursor_x <= render_rect.x + render_rect.w &&
|
||||
cursor_y >= render_rect.y &&
|
||||
cursor_y <= render_rect.y + render_rect.h) {
|
||||
controlled_remote_id_ = it.first;
|
||||
last_mouse_event.motion.x = cursor_x;
|
||||
last_mouse_event.motion.y = cursor_y;
|
||||
last_mouse_event.button.x = cursor_x;
|
||||
last_mouse_event.button.y = cursor_y;
|
||||
|
||||
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
|
||||
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
if (SDL_BUTTON_LEFT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::left_down;
|
||||
} else if (SDL_BUTTON_RIGHT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::right_down;
|
||||
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::middle_down;
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
if (SDL_BUTTON_LEFT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::left_up;
|
||||
} else if (SDL_BUTTON_RIGHT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::right_up;
|
||||
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::middle_up;
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
remote_action.m.flag = MouseFlag::move;
|
||||
}
|
||||
|
||||
if (overlay_hovered) {
|
||||
break;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
|
||||
last_mouse_event.button.x >= render_rect.x &&
|
||||
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
|
||||
last_mouse_event.button.y >= render_rect.y &&
|
||||
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
|
||||
float scroll_x = event.wheel.x;
|
||||
float scroll_y = event.wheel.y;
|
||||
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
scroll_x = -scroll_x;
|
||||
scroll_y = -scroll_y;
|
||||
}
|
||||
|
||||
remote_action.type = ControlType::mouse;
|
||||
|
||||
auto roundUp = [](float value) -> int {
|
||||
if (value > 0) {
|
||||
return static_cast<int>(std::ceil(value));
|
||||
} else if (value < 0) {
|
||||
return static_cast<int>(std::floor(value));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
|
||||
remote_action.m.flag = MouseFlag::wheel_vertical;
|
||||
remote_action.m.s = roundUp(scroll_y);
|
||||
} else {
|
||||
remote_action.m.flag = MouseFlag::wheel_horizontal;
|
||||
remote_action.m.s = roundUp(scroll_x);
|
||||
}
|
||||
|
||||
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
|
||||
(std::max)(render_rect.w, 1.0f);
|
||||
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
|
||||
(std::max)(render_rect.h, 1.0f);
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (overlay_hovered) {
|
||||
continue;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,687 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <libyuv.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "fa_regular_400.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "window_util_mac.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
const ImWchar *GetMultilingualGlyphRanges() {
|
||||
static std::vector<ImWchar> glyph_ranges;
|
||||
if (glyph_ranges.empty()) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImFontGlyphRangesBuilder builder;
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
|
||||
|
||||
ImVector<ImWchar> built_ranges;
|
||||
builder.BuildRanges(&built_ranges);
|
||||
glyph_ranges.assign(built_ranges.Data,
|
||||
built_ranges.Data + built_ranges.Size);
|
||||
}
|
||||
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
|
||||
}
|
||||
|
||||
bool CanReadFontFile(const char *font_path) {
|
||||
if (!font_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream font_file(font_path, std::ios::binary);
|
||||
return font_file.good();
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
HICON LoadTrayIcon() {
|
||||
HMODULE module = GetModuleHandleW(nullptr);
|
||||
HICON icon = reinterpret_cast<HICON>(
|
||||
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
return LoadIconW(nullptr, IDI_APPLICATION);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
inline bool X11GetDisplayAndWindow(SDL_Window *window, Display **display_out,
|
||||
::Window *x11_window_out) {
|
||||
if (!window || !display_out || !x11_window_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
|
||||
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
|
||||
// SDL build does not expose X11 window properties.
|
||||
return false;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
Display *display = (Display *)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
|
||||
const Sint64 x11_window_num =
|
||||
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
const ::Window x11_window = (::Window)x11_window_num;
|
||||
|
||||
if (!display || !x11_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*display_out = display;
|
||||
*x11_window_out = x11_window;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void X11SendNetWmState(Display *display, ::Window x11_window,
|
||||
long action, Atom state1, Atom state2 = 0) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
|
||||
XEvent event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.xclient.type = ClientMessage;
|
||||
event.xclient.serial = 0;
|
||||
event.xclient.send_event = True;
|
||||
event.xclient.message_type = wm_state;
|
||||
event.xclient.window = x11_window;
|
||||
event.xclient.format = 32;
|
||||
event.xclient.data.l[0] = action;
|
||||
event.xclient.data.l[1] = (long)state1;
|
||||
event.xclient.data.l[2] = (long)state2;
|
||||
event.xclient.data.l[3] = 1; // normal source indication
|
||||
event.xclient.data.l[4] = 0;
|
||||
|
||||
XSendEvent(display, DefaultRootWindow(display), False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &event);
|
||||
}
|
||||
|
||||
inline void X11SetWindowTypeUtility(Display *display, ::Window x11_window) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_window_type =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom wm_window_type_utility =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
|
||||
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char *)&wm_window_type_utility, 1);
|
||||
}
|
||||
|
||||
inline void X11SetWindowAlwaysOnTop(SDL_Window *window) {
|
||||
Display *display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
|
||||
const Atom state_stays_on_top =
|
||||
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
|
||||
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
inline void X11SetWindowSkipTaskbar(SDL_Window *window) {
|
||||
Display *display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom skip_taskbar =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
|
||||
const Atom skip_pager =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
|
||||
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
|
||||
|
||||
// Hint the WM that this is an auxiliary/utility window.
|
||||
X11SetWindowTypeUtility(display, x11_window);
|
||||
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window,
|
||||
const SDL_Point *area,
|
||||
void *data) {
|
||||
GuiApplication *application = (GuiApplication *)data;
|
||||
if (!application) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
if (application->fullscreen_button_pressed_) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
// Server window: OS-level dragging for the title bar, but keep the left-side
|
||||
// collapse/expand button clickable.
|
||||
if (application->server_window_ && window == application->server_window_) {
|
||||
const float title_h = application->server_window_title_bar_height_;
|
||||
const float button_w = title_h;
|
||||
if (area->y >= 0 && area->y < title_h) {
|
||||
if (area->x >= 0 && area->x < button_w) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int window_width, window_height;
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
|
||||
// check if curosor is in tab bar
|
||||
if (application->stream_window_inited_ && application->stream_window_created_ &&
|
||||
!application->fullscreen_button_pressed_ && application->stream_ctx_) {
|
||||
ImGuiContext *prev_ctx = ImGui::GetCurrentContext();
|
||||
ImGui::SetCurrentContext(application->stream_ctx_);
|
||||
|
||||
ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar");
|
||||
if (tab_bar_window && tab_bar_window->Active) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float scale_x = io.DisplayFramebufferScale.x;
|
||||
float scale_y = io.DisplayFramebufferScale.y;
|
||||
|
||||
float tab_bar_x = tab_bar_window->Pos.x * scale_x;
|
||||
float tab_bar_y = tab_bar_window->Pos.y * scale_y;
|
||||
float tab_bar_width = tab_bar_window->Size.x * scale_x;
|
||||
float tab_bar_height = tab_bar_window->Size.y * scale_y;
|
||||
|
||||
ImGui::SetCurrentContext(prev_ctx);
|
||||
|
||||
if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width &&
|
||||
area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
} else {
|
||||
ImGui::SetCurrentContext(prev_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
float mouse_grab_padding = application->title_bar_button_width_ * 0.16f;
|
||||
if (area->y < application->title_bar_button_width_ &&
|
||||
area->y > mouse_grab_padding &&
|
||||
area->x < window_width - application->title_bar_button_width_ * 3.0f &&
|
||||
area->x > mouse_grab_padding) {
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
|
||||
// if (!application->streaming_) {
|
||||
// return SDL_HITTEST_NORMAL;
|
||||
// }
|
||||
|
||||
if (area->y < mouse_grab_padding) {
|
||||
if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_TOPLEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_TOPRIGHT;
|
||||
} else {
|
||||
return SDL_HITTEST_RESIZE_TOP;
|
||||
}
|
||||
} else if (area->y > window_height - mouse_grab_padding) {
|
||||
if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_BOTTOMLEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
|
||||
} else {
|
||||
return SDL_HITTEST_RESIZE_BOTTOM;
|
||||
}
|
||||
} else if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_LEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_RIGHT;
|
||||
}
|
||||
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateMainWindow() {
|
||||
main_ctx_ = ImGui::CreateContext();
|
||||
if (!main_ctx_) {
|
||||
LOG_ERROR("Main context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Main Window", (int)main_window_width_,
|
||||
(int)main_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
|
||||
&main_window_, &main_renderer_)) {
|
||||
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
|
||||
if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) {
|
||||
dpi_scale_ = dpi_scale;
|
||||
|
||||
main_window_width_ = (int)(main_window_width_default_ * dpi_scale_);
|
||||
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
|
||||
server_window_normal_width_ =
|
||||
(int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_normal_height_ =
|
||||
(int)(server_window_height_default_ * dpi_scale_);
|
||||
window_rounding_ = window_rounding_default_ * dpi_scale_;
|
||||
|
||||
SDL_SetWindowSize(main_window_, (int)main_window_width_,
|
||||
(int)main_window_height_);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(main_window_, false);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
SetupFontAndStyle(&main_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
#if _WIN32
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(main_window_);
|
||||
HWND main_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
HICON tray_icon = LoadTrayIcon();
|
||||
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__APPLE__)
|
||||
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
tray_ = std::make_unique<LinuxTray>(
|
||||
main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT);
|
||||
#endif
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(main_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyMainWindow() {
|
||||
if (main_ctx_) {
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
}
|
||||
|
||||
if (main_renderer_) {
|
||||
SDL_DestroyRenderer(main_renderer_);
|
||||
}
|
||||
|
||||
if (main_window_) {
|
||||
SDL_DestroyWindow(main_window_);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateStreamWindow() {
|
||||
if (stream_window_created_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
|
||||
stream_ctx_ = ImGui::CreateContext();
|
||||
if (!stream_ctx_) {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Stream Window", (int)stream_window_width_,
|
||||
(int)stream_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_TRANSPARENT,
|
||||
&stream_window_, &stream_renderer_)) {
|
||||
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
stream_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
|
||||
SDL_SetWindowResizable(stream_window_, true);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
SetupFontAndStyle(&stream_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(stream_renderer_);
|
||||
|
||||
// change props->stream_render_rect_
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
|
||||
event.window.windowID = SDL_GetWindowID(stream_window_);
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
stream_window_created_ = true;
|
||||
just_created_ = true;
|
||||
|
||||
stream_window_inited_ = true;
|
||||
LOG_INFO("Stream window inited");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyStreamWindow() {
|
||||
stream_window_width_ = (float)stream_window_width_default_;
|
||||
stream_window_height_ = (float)stream_window_height_default_;
|
||||
|
||||
if (stream_ctx_) {
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
}
|
||||
|
||||
if (stream_renderer_) {
|
||||
SDL_DestroyRenderer(stream_renderer_);
|
||||
stream_renderer_ = nullptr;
|
||||
}
|
||||
|
||||
if (stream_window_) {
|
||||
SDL_DestroyWindow(stream_window_);
|
||||
stream_window_ = nullptr;
|
||||
}
|
||||
|
||||
stream_window_created_ = false;
|
||||
focus_on_stream_window_ = false;
|
||||
stream_window_grabbed_ = false;
|
||||
control_mouse_ = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateServerWindow() {
|
||||
if (server_window_created_) {
|
||||
return 0;
|
||||
}
|
||||
server_ctx_ = ImGui::CreateContext();
|
||||
if (!server_ctx_) {
|
||||
LOG_ERROR("Server context is null");
|
||||
return -1;
|
||||
}
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Server Window", (int)server_window_width_,
|
||||
(int)server_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_TRANSPARENT,
|
||||
&server_window_, &server_renderer_)) {
|
||||
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
// Hide server window from the taskbar by making it a tool window.
|
||||
{
|
||||
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
|
||||
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
if (server_hwnd) {
|
||||
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
|
||||
ex_style |= WS_EX_TOOLWINDOW;
|
||||
ex_style &= ~WS_EX_APPWINDOW;
|
||||
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
|
||||
|
||||
// Keep the server window above normal windows.
|
||||
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
// Best-effort keep above other windows on X11.
|
||||
X11SetWindowAlwaysOnTop(server_window_);
|
||||
// Best-effort hide from taskbar on X11.
|
||||
X11SetWindowSkipTaskbar(server_window_);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Best-effort keep above other windows on macOS.
|
||||
MacSetWindowAlwaysOnTop(server_window_, true);
|
||||
// Best-effort exclude from Window menu / window cycling.
|
||||
MacSetWindowExcludedFromWindowMenu(server_window_, true);
|
||||
#endif
|
||||
|
||||
// Set window position to bottom-right corner
|
||||
SDL_Rect display_bounds;
|
||||
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
|
||||
&display_bounds)) {
|
||||
int window_x =
|
||||
display_bounds.x + display_bounds.w - (int)server_window_width_;
|
||||
int window_y =
|
||||
display_bounds.y + display_bounds.h - (int)server_window_height_;
|
||||
SDL_SetWindowPosition(server_window_, window_x, window_y);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(server_window_, false);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(server_window_, HitTestCallback, this);
|
||||
|
||||
SetupFontAndStyle(&server_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(server_renderer_);
|
||||
|
||||
server_window_created_ = true;
|
||||
server_window_inited_ = true;
|
||||
|
||||
LOG_INFO("Server window inited");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyServerWindow() {
|
||||
if (server_ctx_) {
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
}
|
||||
|
||||
if (server_renderer_) {
|
||||
SDL_DestroyRenderer(server_renderer_);
|
||||
server_renderer_ = nullptr;
|
||||
}
|
||||
|
||||
if (server_window_) {
|
||||
SDL_DestroyWindow(server_window_);
|
||||
server_window_ = nullptr;
|
||||
}
|
||||
|
||||
server_window_created_ = false;
|
||||
server_window_inited_ = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) {
|
||||
float font_size = 32.0f;
|
||||
|
||||
// Setup Dear ImGui style
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
io.IniFilename = NULL; // disable imgui.ini
|
||||
|
||||
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
|
||||
ImFontConfig config;
|
||||
config.FontDataOwnedByAtlas = false;
|
||||
config.MergeMode = false;
|
||||
|
||||
if (system_chinese_font_out) {
|
||||
*system_chinese_font_out = nullptr;
|
||||
}
|
||||
|
||||
ImFont *ui_font = nullptr;
|
||||
const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges();
|
||||
|
||||
#if defined(_WIN32)
|
||||
const char *base_font_paths[] = {
|
||||
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
|
||||
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc", nullptr};
|
||||
#elif defined(__APPLE__)
|
||||
const char *base_font_paths[] = {
|
||||
"/System/Library/Fonts/PingFang.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/System/Library/Fonts/SFNS.ttf", nullptr};
|
||||
#else
|
||||
const char *base_font_paths[] = {
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
nullptr};
|
||||
#endif
|
||||
|
||||
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
|
||||
if (!CanReadFontFile(base_font_paths[i])) {
|
||||
continue;
|
||||
}
|
||||
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
|
||||
&config, multilingual_ranges);
|
||||
if (ui_font != nullptr) {
|
||||
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
|
||||
}
|
||||
}
|
||||
if (!ui_font) {
|
||||
ui_font = io.Fonts->AddFontDefault(&config);
|
||||
}
|
||||
|
||||
if (!ui_font) {
|
||||
LOG_WARN("Failed to initialize base UI font");
|
||||
ImGui::StyleColorsLight();
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImFontConfig icon_config = config;
|
||||
icon_config.MergeMode = true;
|
||||
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
|
||||
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
|
||||
font_size, &icon_config, icon_ranges);
|
||||
|
||||
io.FontDefault = ui_font;
|
||||
if (system_chinese_font_out) {
|
||||
*system_chinese_font_out = ui_font;
|
||||
}
|
||||
|
||||
ImGui::StyleColorsLight();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyMainWindowContext() {
|
||||
if (!main_ctx_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(main_ctx_);
|
||||
main_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyStreamWindowContext() {
|
||||
if (!stream_ctx_) {
|
||||
stream_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(stream_ctx_);
|
||||
stream_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyServerWindowContext() {
|
||||
if (!server_ctx_) {
|
||||
server_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
server_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(server_ctx_);
|
||||
server_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int GuiApplication::DrawMainWindow() {
|
||||
if (!main_ctx_) {
|
||||
LOG_ERROR("Main context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("MainRender", nullptr,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoDocking);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
TitleBar(true);
|
||||
|
||||
MainWindow();
|
||||
|
||||
UpdateNotificationWindow();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
PortableServiceInstallWindow();
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (show_request_permission_window_) {
|
||||
RequestPermissionWindow();
|
||||
}
|
||||
#endif
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Rendering
|
||||
(void)io;
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(main_renderer_);
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
|
||||
SDL_RenderPresent(main_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DrawStreamWindow() {
|
||||
if (!stream_ctx_) {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
StreamWindow();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float stream_title_window_height =
|
||||
fullscreen_button_pressed_ ? 0 : title_bar_height_;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
// Set minimum window size to 0 to allow exact height control
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("StreamTitleWindow", nullptr,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoDocking);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (!fullscreen_button_pressed_) {
|
||||
TitleBar(false);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Rendering
|
||||
(void)io;
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
|
||||
SDL_RenderClear(stream_renderer_);
|
||||
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->tab_selected_) {
|
||||
SDL_FRect render_rect_f = {
|
||||
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
|
||||
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
|
||||
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
|
||||
&render_rect_f);
|
||||
}
|
||||
}
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_);
|
||||
SDL_RenderPresent(stream_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DrawServerWindow() {
|
||||
if (!server_ctx_) {
|
||||
LOG_ERROR("Server context is null");
|
||||
return -1;
|
||||
}
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
server_window_width_ = io.DisplaySize.x;
|
||||
server_window_height_ = io.DisplaySize.y;
|
||||
|
||||
ServerWindow();
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(server_renderer_);
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
|
||||
SDL_RenderPresent(server_renderer_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GuiApplication::MinimizeMainWindowToTray() {
|
||||
if (!enable_minimize_to_tray_ || !main_window_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
if (!tray_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tray_->MinimizeToTray();
|
||||
return true;
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
if (!tray_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tray_->MinimizeToTray();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuiApplication::UpdateRenderRect() {
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &[_, props] : remote_sessions_) {
|
||||
if (!props->reset_control_bar_pos_) {
|
||||
props->mouse_diff_control_bar_pos_x_ = 0;
|
||||
props->mouse_diff_control_bar_pos_y_ = 0;
|
||||
}
|
||||
|
||||
if (!just_created_) {
|
||||
props->reset_control_bar_pos_ = true;
|
||||
}
|
||||
|
||||
int stream_window_width, stream_window_height;
|
||||
SDL_GetWindowSize(stream_window_, &stream_window_width,
|
||||
&stream_window_height);
|
||||
stream_window_width_ = (float)stream_window_width;
|
||||
stream_window_height_ = (float)stream_window_height;
|
||||
|
||||
float video_ratio =
|
||||
(float)props->video_width_ / (float)props->video_height_;
|
||||
float video_ratio_reverse =
|
||||
(float)props->video_height_ / (float)props->video_width_;
|
||||
|
||||
float render_area_width = props->render_window_width_;
|
||||
float render_area_height = props->render_window_height_;
|
||||
|
||||
props->stream_render_rect_last_ = props->stream_render_rect_;
|
||||
|
||||
SDL_FRect rect_f{props->render_window_x_, props->render_window_y_,
|
||||
render_area_width, render_area_height};
|
||||
if (render_area_width < render_area_height * video_ratio) {
|
||||
rect_f.x = props->render_window_x_;
|
||||
rect_f.y = std::abs(render_area_height -
|
||||
render_area_width * video_ratio_reverse) /
|
||||
2.0f +
|
||||
props->render_window_y_;
|
||||
rect_f.w = render_area_width;
|
||||
rect_f.h = render_area_width * video_ratio_reverse;
|
||||
} else if (render_area_width > render_area_height * video_ratio) {
|
||||
rect_f.x =
|
||||
std::abs(render_area_width - render_area_height * video_ratio) /
|
||||
2.0f +
|
||||
props->render_window_x_;
|
||||
rect_f.y = props->render_window_y_;
|
||||
rect_f.w = render_area_height * video_ratio;
|
||||
rect_f.h = render_area_height;
|
||||
} else {
|
||||
rect_f.x = props->render_window_x_;
|
||||
rect_f.y = props->render_window_y_;
|
||||
rect_f.w = render_area_width;
|
||||
rect_f.h = render_area_height;
|
||||
}
|
||||
|
||||
props->stream_render_rect_f_ = rect_f;
|
||||
props->stream_render_rect_ = {static_cast<int>(std::lround(rect_f.x)),
|
||||
static_cast<int>(std::lround(rect_f.y)),
|
||||
static_cast<int>(std::lround(rect_f.w)),
|
||||
static_cast<int>(std::lround(rect_f.h))};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||