ruby-clash-systray 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +14 -0
- data/.ruby-version +1 -0
- data/LICENSE +674 -0
- data/README.md +34 -0
- data/Rakefile +12 -0
- data/exe/clash-systray +6 -0
- data/lib/clash-systray/app/assets/README.md +5 -0
- data/lib/clash-systray/app/assets/app.svg +1 -0
- data/lib/clash-systray/app/services/export_env_service.rb +14 -0
- data/lib/clash-systray/app/services/open_dashboard_service.rb +11 -0
- data/lib/clash-systray/app/services.rb +2 -0
- data/lib/clash-systray/app/views/systray/helpers.rb +11 -0
- data/lib/clash-systray/app/views/systray.rb +120 -0
- data/lib/clash-systray/app/views.rb +1 -0
- data/lib/clash-systray/app.rb +25 -0
- data/lib/clash-systray/config/initializers/qt6.rb +8 -0
- data/lib/clash-systray/config/settings.rb +18 -0
- data/lib/clash-systray/config.rb +8 -0
- data/lib/clash-systray/lib/clash/config.rb +23 -0
- data/lib/clash-systray/lib/clash/web/api.rb +29 -0
- data/lib/clash-systray/lib/clash/web/client.rb +20 -0
- data/lib/clash-systray/lib/clash/web/http.rb +66 -0
- data/lib/clash-systray/lib/clash/web.rb +3 -0
- data/lib/clash-systray/lib/clash.rb +15 -0
- data/lib/clash-systray/lib/contrib/sighandler.rb +24 -0
- data/lib/clash-systray/lib/contrib.rb +1 -0
- data/lib/clash-systray/lib.rb +2 -0
- data/lib/clash-systray/version.rb +3 -0
- data/lib/clash-systray.rb +10 -0
- data/misc/screenshots/systray.png +0 -0
- metadata +112 -0
data/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ClashSystray
|
|
2
|
+
|
|
3
|
+
A simple system tray for Clash on Linux.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Ruby](https://www.ruby-lang.org/)
|
|
8
|
+
- [Qt](https://www.qt.io/)
|
|
9
|
+
- [Clash](https://github.com/MetaCubeX/mihomo)
|
|
10
|
+
- **the config file should be located at one of the following paths**
|
|
11
|
+
- ~/.local/share/clash/config.yaml
|
|
12
|
+
- ~/.config/clash/config.yaml
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install ruby-clash-systray
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
To launch the GUI:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
clash-systray
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Screenshot
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
Licensed under the [GPL-3.0-only](./LICENSE).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
_ = Gem::Specification.load("clash-systray.gemspec")
|
|
5
|
+
|
|
6
|
+
require "rubocop/rake_task"
|
|
7
|
+
RuboCop::RakeTask.new
|
|
8
|
+
|
|
9
|
+
require "rspec/core/rake_task"
|
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
11
|
+
|
|
12
|
+
task default: %i[spec]
|
data/exe/clash-systray
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64" class="icon" viewBox="0 0 1024 1024"><image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPsAAAD7CAYAAACscuKmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABENSURB VHic7Z3tdqO4EgDbTt7/ie8k+2MuE6XdX8JgA111jg8gsCSyFN0SePYmvbm9uwPwcr7f3YF3ceWL /crnBvtyyRvCVYTY6jyu8veAH7YS9/Q3gDNf3JW+n/n84D1UpD6l+GeTIerv2n1bfgeOwRoZo+9k 9Z1C/jNc0DMSW8d63z/DucM+eHJa5brsmZvCWznyBV+V9FZYn6kPrktV8u/C+kx9h+BoF/qs4Jno kfxRe3BdsuhtrWdlXr1R+cs5ysVekTySubr02jrK3wH2oyp5dZmVVdp/KUe4yDP5IoG9da8sWoce VKK4JXi2P6o7KnsZ77zYZySP1rNtXZfVNtJfHy/troidbVvrXrte2e686yKvTLJ5IkeSR/t13VF/ 4HpUZLekjoSP9us2Z2b1d+HVF3kUzWckHz93p1x/32rL61NlHxyT6qMxT/ZM6q9gXzXdr/Z3U155 MWcz6pngWupsm+gOC89GdS14tj0r/UuEf8UFPhPNI8GtdW9/JL21rPQZzkU2Kx5F9kh2a93b78nv 9W9X6T/3rFxq0TwT3Vp6+/R3dRtWH7y+wnWoRvZx24ve4/L+/+VtKLsZ39X1e327Ocdswp6yz4qu o7SWuyJ7NZXX/UP06+NNls2k8F/yW/ZR+FF6LfyX0ZebWi7sJvxeskeij9J5qbn+ZOVZGq/7YPXR K4Nzk0XTmUk5Lbq1vUg/Cu/VvZS/RPg9ZK+IriN6JHT02SKqW9twPaJJsTXRXcv+R+zrT6f2Osov be4u/NayWxJZsnvj8LuIfIgt9odxrJX+Z2N1q59eGVyDSnRfltbY/S627OP2TR5vBH+Gct2OJb3u 16bCbyn7GtEtoT3pPdm9qC7B0uszXJ/KZN2y/JZH0ccxuif7GOUt6ccbyHgjGK/HpR+bCb+V7BXR s1T9w1lGKbwlutW+1UevDK5NFOWjWfpFfC38XX7LPIo/Sq/TfD3Bt7RnTdxtIvwWsj8juiV4Jnsl dRdjafV15rzgPERi6P+uY0T1ZB/XF/H0LLyeoFuWi+RW2yKPUd4av4tTNsWzsmeiW4/TdKqeif4h j5Jnk3Fe32bOBc6LJXR07BhBx+OtCH9T68v1qFN4K6r/GdrRKfsY5a0bkO7rNM/IXhV9XHqR3BJd R/ZIcsbmEFG50VtS6aX+3NQxltxjmaj1hVH03YTfY8w+nvQipxXNLcm3EF2vR2UAIo+TYyMV4bXs Vhqvpddt/JEf0bXkbx2zW2JZkutxuiW6F9krouv2vT4CRETjeOtY7wUZ7/q0BNcsqfw4LND9eUr+ NbJXRbcm4fTnU+L03RNdt231Les7gIgtjR5Pe5mijvDj23PWdarH6h56ln7sx2rhZ2X3Tlp/ItE/ pRbRtexjW2Isq/0FGLGierTf+r6etdf7reie1ftlHPO22XhrnG49YstS9y0iutcvgFl0VPf2WSyP 0JZjs0iePfO3nsGPx0xF9xnZs/TdGqPPjNWtF2Ys2XVfrG2ALfDEHyflLEbprbo8rFd1R9csycvC PxvZI8mjMXom+ii8GEvdD4C9saLqsq2FHKXNGOX+MMpFHlP6XR+9WSm794mE12XRa7CW7F6fAF6F jqTRdXgv1Je9ohsNB6ai+0d2wFCZXuoxupb501nOpO9jm2NfEB3eiXUNRtdrhvduvt6v16c8qMge RXUrdR+l/hRf9rWiAxyFtcJXJu088at9eSCTPZqUy8bokejWizOR6ERzOCre5PFsoLKidyXSRxPX v5iVPRubj3J7abz345ZIdICjUxHeO86Sem2Ed6nKPi6tN+S8GXcrskfp+9iWXgc4Opnw1fTde0nH i/AlTyLZK1Hde25eGatHj9jKJwBwMJ5J6S2ZPcG9SO96U5Hdi+qjuJH00ThdvwqbdhjgBFRTeo33 +E2Mdf2d1RN0s2P12VdidZ2IDlcju6b1eF2GbU92ccqitv+RvVQTSR/NymdvxXmiA1wZK51fXq0d l/ozvidvuVN6uSaL7NWxuvWMXaf51qSc/gMgPlyJKJ2Pxuc6Ta/M1qdYr/N5wnlR3Yru2Yy71Rai wxXxrvEsQ9ZlUXactSsi8bu7XnSPOmndBEjdAX5TdcrzK3LKdUvLnj0iiO5E1R+1WHUjP1yZ6Fqf daqaKT9sV36VoztkRexM8iiyIzp0IErhs4zZ8m06U/Zk91L4sSySe1VnAJpS8Sea4C6l8pUxu+5Q FtVnxuvcBKATlegeTXp7NwSvjV/cCwdFd5HZlB0AbKqpfZRte/WKSP7oLZJcz75bEwdRZ7gJQEe8 jFnE9q3impW+P/gVjdmjDnh3m0h4swMAjckCa+RWFFhNstn4TPS1KTzSQ2fWDJlnPDPrrzx603ef aAxhNcS4HSBGO+JJHz3lSj27DwdaHdAVVe461vFeGwDdeYVnN5HaG3RZA9XUPeoMQDcqnqzxz20j e6nG64DXiN4HAHNEPln+eXU8UPnVW3ViIBqzA0CM5000ZzY1SZe9QZfdOaJUw2wQAFwqgdY6Th9v Uv0hjG5YjKUE5UgP4BNN0uljosm5kOqjt0rDALAPlQCbOnivHKQqfKZBbgoAP1Rn5K3vzEb2W+Un rtl+Zt4B9mVmzO7un/nHKyplY8cAYB3Tj9UqVP8Nuk0aA4DNsCb1dPkvKr9ntyq0GuUmALA9kV9T 82Qzj94qHSALAFhP1adVXs38u/ERSA2wH5v4tTayVzrADQBgnq28mnpdFgAuBLIDNGGN7KTnAO9n 2kMiO0ATkB2gCcgO0ARkB2gCsgM0AdkBmoDsAE1AdoAmIDtAE5AdoAnIDtAEZAdoArIDNAHZAZqA 7ABNQHaAJiA7QBOQHaAJyA7QBGQHaAKyAzQB2QGagOwATUB2gCYgO0ATkB2gCcgO0ARkB2gCsgM0 AdkBmoDsAE1AdoAmIDtAE5AdoAnIDtAEZAdoArIDNAHZAZqA7ABNQHaAJiA7QBOQHaAJyA7QBGQH aAKyAzQB2QGagOwATUB2gCYgO0ATkB2gCcgO0ARkB2gCsgM0AdkBmoDsAE1AdoAmIDtAE5AdoAnI DtAEZAdoArIDNAHZAZqA7ABNQHaAJiA7QBOQHaAJyA7QBGQHaAKyAzQB2QGagOwATUB2gCYgO0AT kB2gCcgO0ARkB2gCsgM0AdkBmoDsAE1AdoAmIDtAE5AdoAnIDtAEZAdoArIDNAHZAZqA7ABNQHaA JiA7QBOQHaAJyA7QBGQHaAKyAzQB2QGagOwATUB2gCYgO0ATkB2gCcgO0ARkB2gCsgM0AdkBmoDs AE1AdoAmIDtAE5AdoAnIDtAEZAdowhrZvzfvBQDMMu0hkR2gCcgO0ARL9mp6kB1Hug8wz1ZePRy3 VWRHbID92MSvtbJ/q6Uu97YBwKfq04xX/469W4XBl6xj1nQAAGpEfnlOmkSRfWzk2ygHgPehnUyD 7jNjdq/SqbsNADwQObTaLU/27C6hoz5yA+yH9qzi5QP34IuzDSI9wLZYTq0NtN+VND5rUIz9UV0A 8JdZb54KtDNjdi+yVx4PIDmAjzXZVgmwm87Gz4zZKzcCAPDxInZlCC2SyF95XdZq0LqrVF+0AYBH qhny7A3gH9lsvFXuRXXdQSQHmCfyyfLPq+MBLXsW1XWZdUwmOTcBgJona/xz27hbhUaZ18BXsUNe GwDdqXhm+WYdr+v7VZbNxlsVZeN3Lzt4aBwARMT3bFyvzJmFXlXH7FmDlc4AwF/WzInNeFYas1tf yBr/ctatSI/8AD9YblTd8jxzHcsevUVjB92RSHRSeYC/RMNcT/jMtVJgrfyePUotorsNqTxAnYpH 2YS4V6+I1NJ43Rnd4JfxqaQdVhsAV0Zf+5HQnleWh14bv6j8xNW7i3idiO5AAPBIxR/LN+u7Io5v 1R/CZJ2J7kBEd4BaVK86tSqYVt6g09tR+p6lHl7dCA9XJrrW93LqYXvm36CbuftUojtAV6pRPRsu l1N4kbn/ScSM9Flab7XFDQCuSHS9Z4GzOumdtSsiIh/OgTe1tMpuw+eutnW5Pr7SDsDZsUS3JtdG mf84n69h6U3ShXwWOnsb1seK9R3pJnZUvw37bkankBu64GXFWXacDY3H+l28yC4yH92tjz6mUrde BzgjWfpupetjBPei+ij+WJ/X9j8qsi/r1vaM6JUbAcLDFYjS92isruUet7PZ+DSNj2QXWRfddZm1 Lc621xbAWaiIbqXqnuRa9mys7kpflX1Z97az6O0db7XhlQEcnUx0/XjaEt2K8DNPtVxmZdf7PKnF 2WeVW/VmbQMcjWrqHqXtM6KXZuBHMtlF/LG6t62XUbpvtWHV4x0H8G68NHrmXZS1EV23v3o2fqQi XyR89t1oG+HhqKwV3XuW7ok+K73JrOzLeiRydb8lbVV4rwzgFVhirYno3nh97bvwIVXZReajejbx Zm2vKUd6eCVRNF+W0csyUVS3Zt6ffplmYa3sa1PyNd9dUwfA1mRj5Wwyrpq+e4/aVk3KjczILuIL V5mgs74XlWfHzWYFALN4YnnR3Ivq3ptx/xM/qlcftZXln5VdJH72rvdX6qge651UVBfiwyyRPJbk yzJL3bXsWnQtvE7hRWLpU7aQfeb42fpnyb6L/KDJhPGeny/LakQfZbdE934A83T6vrBGdpFc+C3H 1NWTZBwPW1GdhItEj9L36s9Xteiro7rIetlFasJXs4DsJGZPjFl7mCG75iLJK6JHwlvR3Hrc9pTo ItvJHm17wmfPKr19HtF+RIeMNdF8jejem3JPP1rLeEZ2EVvwNe+0R3dRq7xah7dtwQ3huswGihnR Ky/JeI/WrN+oL8ux3ZlzcXlWdpHnx+xemlKRPnscMXN33HQyBN7KzH9L67qzIncmePVHLdEjtt1E F8n/WaoK32Kn57dh/Uv+/nt0X873v+XvjWdZvw/bd+OztHlT694PcHR/RO3zzguuR2X4GEV07/Ha 7CuwM4/YvH5PsYXsInXhx2fm1mcUXIv+IT83jeVzG8puYktvLcXZhutTSdmXZSb6uG6l55HoVuZg 9c8rm2Yr2UVqwov8ln45zhJey77Ur2W/D3VG/5rt7ONCuA6ZQJbgy7YlZia7V2aNzXeP6Atbyi7i C2+xnOx9ONaSfPnoqD7KrqWP0noRInxHIoks4byIrlP4b4nTdOsGYX2yPj7N1rKL2MLr6D5G6VF6 7w9hiW7JrkWfSem9Mjg3z47RM9mzMbwVxV8uusg+sovUhBf5HYllOMaSXG9bonvCi8TRHcmvj5W2 L+uVqO6l71akj8blluC7iy6yn+wideEXRvG/1fHjJFw1qi/DgyzCi7MN18GTyRurW3J6Y+7KePzt oou85gKP0mVrMk0LG617+59J5aNyOA+eNLMpvBXdvbJMdKvdSp834ZUXtRdJ14gfRXMvfSey96Ua 2cd1T/bKtlWf1a7Xv1149QU+E+XH9ehTGadHokd/A24A5yMSxxq3e6l8JcpnEfzt0XzkXRdzFFVn pI/2efVk7cM1sUQf1yuiZ/uteqw2re3deedFHkX5cT0St7Kt66q0DdciiqjVlD7attYrbb+UI1zg M9LrZSZ2JW0/wt8AXkMUXbP0u5KeH1LyhaNc6JVZ8eoNIFp6bR3l7wD7kcmXCVwV2xP6raKLHO8i r0g/bldvBl7dRzt/2J9Z6Sv7vXqj8pdz5It9VvxofaY+uC5VIatR+/CCj5zhQp95NDYTvc9w7rAP M5LOzKIfUvKFs13wa5+JrznPs/1t4Ic10j0j8aElXzjzBV3p+5nPD95DRdxTyK25igxbncdV/h7w w1ZinlLwkStf3Fc+N9iX04tt0V2I7uffkUuKXOE/2+1WpyDcm1gAAAAASUVORK5CYII=" width="974.11" height="992.02" x="24.949" y="32" preserveAspectRatio="none"/><rect width="896.03" height="896.03" x="64" y="63.498" fill="#f2f2f2" rx="208.03" ry="208.03"/><path d="M63.968 735.97v16c0 115.25 92.81 208.06 208.06 208.06h479.94c115.25 0 208.06-92.81 208.06-208.06v-16c0 115.25-92.811 208.03-208.06 208.03h-479.94c-115.25 0-208.06-92.781-208.06-208.03" opacity=".1"/><path fill="#fff" d="M63.968 288.06v-16c0-115.25 92.81-208.06 208.06-208.06h479.94c115.25 0 208.06 92.81 208.06 208.06v16c0-115.25-92.811-208.03-208.06-208.03h-479.94c-115.25 0-208.06 92.781-208.06 208.03" opacity=".5"/><path fill="#333" d="M415.585 241.36c10.23-1.827 17.797 6.607 24.242 13.18 20.876 23.377 35.723 51.243 51.307 78.243 50.921-12.09 104.697-10.486 155.779 0 13.885-27.546 26.936-55.86 45.63-80.52 4.779-5.098 9.941-12.666 17.67-12.153 8.305 10.614 8.497 24.98 12.024 37.518 5.42 20.555 7.664 41.687 12.314 62.402 22.094 131.152 43.258 262.624 56.116 395.06.962 11.607 2.149 23.376.545 34.984-3.142 4.553-8.85 4.65-13.724 5.643-14.783 2.95-29.918 2.213-44.861 3.335-35.145 4.233-70.675.93-105.852 4.458-42.744.577-85.49.448-128.234.16-53.294-4.457-107.84 1.25-160.14-11.704-28.475-1.283-59.515-6.221-80.551-27.257-22.607-20.17-30.239-53.551-23.473-82.41 5.836-26.969 27.962-48.582 53.648-57.304 8.786-3.206 20.939-3.335 26.326 5.644 2.47 8.722-6.381 13.692-12.185 18.085-12.698 8.562-26.583 17.765-32.067 32.837-5.13 17.668-1.122 38.768 13.276 51.114 15.969 14.27 38.576 15.937 58.97 17.155 8.273-94.853 20.459-189.353 33.317-283.66 8.434-58.233 16.547-116.594 29.149-174.09 2.854-10.389 4.265-21.805 10.774-30.72m54.706 186.147c-11.063 1.764-20.074 10.806-22.447 21.645-3.207 14.141 7.15 29.597 21.58 31.65 17.766 4.104 35.498-13.95 31.65-31.554-2.18-14.077-16.867-24.402-30.783-21.741m189.449.77c-19.625 6.766-22.832 37.036-5.484 48.131 16.13 12.346 42.905.225 42.937-20.458 2.598-19.913-19.24-35.273-37.453-27.673m-95.206 72.534c-4.329 3.784-9.043 7.055-13.853 10.23-4.329-1.412-8.658-2.95-12.89-4.65l-.385 9.78c10.421 5.868 21.74 2.982 31.617-2.34 9.877 5.322 21.228 8.112 31.65 2.34-.096-2.437-.256-7.343-.353-9.78a187 187 0 0 1-12.89 4.617c-4.81-3.11-9.524-6.413-13.853-10.197a425 425 0 0 0-9.043 0"/></svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class ExportEnvService
|
|
2
|
+
def perform
|
|
3
|
+
config = Clash.config
|
|
4
|
+
http_proxy = "http://127.0.0.1:#{config.port}"
|
|
5
|
+
socks5_proxy = "socks5h://127.0.0.1:#{config.socks_port}"
|
|
6
|
+
text = "export HTTP_PROXY=#{http_proxy} HTTPS_PROXY=#{http_proxy} ALL_PROXY=#{socks5_proxy}"
|
|
7
|
+
|
|
8
|
+
if ENV.include?("WAYLAND_DISPLAY")
|
|
9
|
+
QProcess.execute("wl-copy", QStringList.new << text)
|
|
10
|
+
else
|
|
11
|
+
QApplication.clipboard.set_text(text)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class OpenDashboardService
|
|
2
|
+
def perform
|
|
3
|
+
config = Clash.config
|
|
4
|
+
port = config.external_controller.split(":")[1]
|
|
5
|
+
url = "http://127.0.0.1:#{port}/ui/?secret=#{config.secret}"
|
|
6
|
+
view = QWebEngineView.new
|
|
7
|
+
view.set_attribute(Qt::WA_DeleteOnClose)
|
|
8
|
+
view.load(QUrl.new(url))
|
|
9
|
+
view.show_maximized
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative "systray/helpers"
|
|
2
|
+
|
|
3
|
+
class Systray < RubyQt6::Bando::QWidget
|
|
4
|
+
q_object do
|
|
5
|
+
slot "_on_systray_menu_about_to_show()"
|
|
6
|
+
slot "_on_open_dashboard_action_triggered()"
|
|
7
|
+
slot "_on_export_env_action_triggered()"
|
|
8
|
+
slot "_on_proxy_action_toggled(bool)"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
super
|
|
13
|
+
|
|
14
|
+
initialize_actions
|
|
15
|
+
initialize_menus
|
|
16
|
+
|
|
17
|
+
@systray = QSystemTrayIcon.new(self)
|
|
18
|
+
@systray.set_icon(QApplication.window_icon)
|
|
19
|
+
@systray.set_context_menu(@menu)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
@systray.show
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def initialize_actions
|
|
29
|
+
@open_dashboard_action = QAction.new("Dashboard", self)
|
|
30
|
+
@open_dashboard_action.triggered.connect(self, :_on_open_dashboard_action_triggered)
|
|
31
|
+
|
|
32
|
+
@export_env_action = QAction.new("Export Env", self)
|
|
33
|
+
@export_env_action.triggered.connect(self, :_on_export_env_action_triggered)
|
|
34
|
+
|
|
35
|
+
@quit_action = QAction.new("Quit", self)
|
|
36
|
+
@quit_action.triggered.connect($qApp, :quit)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize_menus
|
|
40
|
+
@menu = QMenu.new("", self)
|
|
41
|
+
@menu.add_action(@open_dashboard_action)
|
|
42
|
+
|
|
43
|
+
initialize_menus_selectors
|
|
44
|
+
|
|
45
|
+
@separator = @menu.add_separator
|
|
46
|
+
@menu.add_action(@export_env_action)
|
|
47
|
+
|
|
48
|
+
@menu.add_separator
|
|
49
|
+
@menu.add_action(@quit_action)
|
|
50
|
+
|
|
51
|
+
@menu.about_to_show.connect(self, :_on_systray_menu_about_to_show)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize_menus_selectors
|
|
55
|
+
@selectors_actions = []
|
|
56
|
+
@proxies_actions = []
|
|
57
|
+
|
|
58
|
+
Clash.api.GET_proxies(on_success: ->(data) {
|
|
59
|
+
selectors = data["proxies"].filter { |_, v| v["hidden"] == false }
|
|
60
|
+
selectors.each do |_, selector_data|
|
|
61
|
+
action = QAction.new(selector_data["name"])
|
|
62
|
+
@selectors_actions << action
|
|
63
|
+
|
|
64
|
+
action_menu = QMenu.new("", self)
|
|
65
|
+
action.set_menu(action_menu)
|
|
66
|
+
|
|
67
|
+
action_group = QActionGroup.new(self)
|
|
68
|
+
selector_data["all"].each do |proxy_name|
|
|
69
|
+
proxy_action = action_menu.add_action(proxy_name)
|
|
70
|
+
proxy_action.set_checkable(true)
|
|
71
|
+
proxy_action.set_checked(selector_data["now"] == proxy_name)
|
|
72
|
+
proxy_action.toggled.connect(self, :_on_proxy_action_toggled)
|
|
73
|
+
action_group.add_action(proxy_action)
|
|
74
|
+
@proxies_actions << proxy_action
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@menu.insert_separator(@separator)
|
|
79
|
+
@selectors_actions.each { |action| @menu.insert_action(@separator, action) }
|
|
80
|
+
})
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def _on_systray_menu_about_to_show
|
|
84
|
+
@proxies_actions.each do |proxy_action|
|
|
85
|
+
proxy_name = Helpers.extract_proxy_name(proxy_action.text)
|
|
86
|
+
proxy_action.set_text(proxy_name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@selectors_actions.each do |action|
|
|
90
|
+
Clash.api.GET_group_delay(action.text, on_success: ->(data) {
|
|
91
|
+
@proxies_actions.each do |proxy_action|
|
|
92
|
+
proxy_name = Helpers.extract_proxy_name(proxy_action.text)
|
|
93
|
+
proxy_delay = data[proxy_name]
|
|
94
|
+
next unless proxy_delay
|
|
95
|
+
|
|
96
|
+
text = Helpers.inject_proxy_name(proxy_name, "#{proxy_delay} ms")
|
|
97
|
+
proxy_action.set_text(text)
|
|
98
|
+
end
|
|
99
|
+
})
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def _on_open_dashboard_action_triggered
|
|
104
|
+
srv = OpenDashboardService.new
|
|
105
|
+
srv.perform
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def _on_export_env_action_triggered
|
|
109
|
+
srv = ExportEnvService.new
|
|
110
|
+
srv.perform
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def _on_proxy_action_toggled(checked)
|
|
114
|
+
return unless checked
|
|
115
|
+
|
|
116
|
+
group = sender.parent.menu_action.text
|
|
117
|
+
proxy = Helpers.extract_proxy_name(sender.text)
|
|
118
|
+
Clash.api.PUT_proxies(group, proxy, on_success: ->(_) {})
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "views/systray"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "config"
|
|
2
|
+
require_relative "lib"
|
|
3
|
+
|
|
4
|
+
require_relative "app/services"
|
|
5
|
+
require_relative "app/views"
|
|
6
|
+
|
|
7
|
+
module ClashSystray
|
|
8
|
+
class Application
|
|
9
|
+
def self.run
|
|
10
|
+
new.exec
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@app = QApplication.new
|
|
15
|
+
@systray = Systray.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exec
|
|
19
|
+
Contrib::SigHandler.new(@app)
|
|
20
|
+
|
|
21
|
+
@systray.show
|
|
22
|
+
@app.exec
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
QApplication.set_organization_name("souk4711")
|
|
2
|
+
QApplication.set_application_name("ClashSystray")
|
|
3
|
+
|
|
4
|
+
root_path = File.expand_path("../..", __dir__)
|
|
5
|
+
QDir.add_search_path("assets", File.join(root_path, "app/assets"))
|
|
6
|
+
|
|
7
|
+
icon = QIcon.new("assets:/app.svg")
|
|
8
|
+
QApplication.set_window_icon(icon)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module ClashSystray
|
|
2
|
+
class Settings
|
|
3
|
+
def GET_clash_config_path
|
|
4
|
+
default_clash_config_path
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def default_clash_config_path
|
|
10
|
+
candidates = [
|
|
11
|
+
QDir.home.file_path(".local/share/clash/config.yaml"),
|
|
12
|
+
QDir.home.file_path(".config/clash/config.yaml")
|
|
13
|
+
]
|
|
14
|
+
candidates.each { |filepath| return filepath if File.exist?(filepath) }
|
|
15
|
+
candidates[0]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Clash
|
|
2
|
+
class Config
|
|
3
|
+
attr_reader :port, :socks_port
|
|
4
|
+
attr_reader :external_controller, :secret
|
|
5
|
+
|
|
6
|
+
def self.load(file)
|
|
7
|
+
new.tap { |c| c.__send__(:parse, file) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def parse(file)
|
|
13
|
+
data = YAML.load_file(file)
|
|
14
|
+
|
|
15
|
+
%w[
|
|
16
|
+
port socks_port
|
|
17
|
+
external_controller secret
|
|
18
|
+
].each do |key|
|
|
19
|
+
instance_variable_set("@#{key}", data[key.tr("_", "-")])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Clash
|
|
2
|
+
module Web
|
|
3
|
+
class API
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def GET_proxies(on_success:)
|
|
9
|
+
url = "/proxies"
|
|
10
|
+
@client.get(url, on_success:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def GET_group_delay(group, on_success:)
|
|
14
|
+
url = "/group/#{group}/delay"
|
|
15
|
+
@client.get(url, params: {
|
|
16
|
+
url: "https://www.gstatic.com/generate_204",
|
|
17
|
+
timeout: 2000
|
|
18
|
+
}, on_success:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def PUT_proxies(group, proxy, on_success:)
|
|
22
|
+
url = "/proxies/#{group}"
|
|
23
|
+
@client.put(url, json: {
|
|
24
|
+
name: proxy
|
|
25
|
+
}, on_success:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Clash
|
|
2
|
+
module Web
|
|
3
|
+
class Client
|
|
4
|
+
extend Forwardable
|
|
5
|
+
|
|
6
|
+
attr_reader :secret
|
|
7
|
+
|
|
8
|
+
def_delegators :@http, :get, :put
|
|
9
|
+
|
|
10
|
+
def initialize(options)
|
|
11
|
+
@secret = options.fetch(:secret)
|
|
12
|
+
@http = Http.new(self, host: options.fetch(:host), port: options.fetch(:port))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def api
|
|
16
|
+
@api ||= API.new(self)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Clash
|
|
2
|
+
module Web
|
|
3
|
+
class Http < RubyQt6::Bando::QObject
|
|
4
|
+
q_object do
|
|
5
|
+
slot "_on_reply_finished(QNetworkReply*)"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(client, options)
|
|
9
|
+
super($qApp)
|
|
10
|
+
|
|
11
|
+
@client = client
|
|
12
|
+
@host = options.fetch(:host)
|
|
13
|
+
@port = options.fetch(:port)
|
|
14
|
+
|
|
15
|
+
@on_reply_success = {}
|
|
16
|
+
@manager = QNetworkAccessManager.new
|
|
17
|
+
@manager.finished.connect(self, :_on_reply_finished)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(path, options = {})
|
|
21
|
+
url = QUrl.new("http://#{@host}:#{@port}#{path}")
|
|
22
|
+
set_url_query(url, options[:params])
|
|
23
|
+
|
|
24
|
+
request = QNetworkRequest.new
|
|
25
|
+
request.set_url(url)
|
|
26
|
+
request.set_raw_header("Authorization", "Bearer #{@client.secret}")
|
|
27
|
+
|
|
28
|
+
reply = @manager.get(request)
|
|
29
|
+
@on_reply_success[reply._qobject_ptr] = options[:on_success]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def put(path, options = {})
|
|
33
|
+
url = QUrl.new("http://#{@host}:#{@port}#{path}")
|
|
34
|
+
|
|
35
|
+
request = QNetworkRequest.new
|
|
36
|
+
request.set_url(url)
|
|
37
|
+
request.set_raw_header("Authorization", "Bearer #{@client.secret}")
|
|
38
|
+
|
|
39
|
+
data = QByteArray.new(options[:json].to_json)
|
|
40
|
+
reply = @manager.put(request, data)
|
|
41
|
+
@on_reply_success[reply._qobject_ptr] = options[:on_success]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def set_url_query(url, params)
|
|
47
|
+
return if params.nil?
|
|
48
|
+
|
|
49
|
+
query = QUrlQuery.new
|
|
50
|
+
params.each { |k, v| query.add_query_item(k.to_qstr, v.to_s) }
|
|
51
|
+
url.set_query(query)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def _on_reply_finished(reply)
|
|
55
|
+
on_success = @on_reply_success.delete(reply._qobject_ptr)
|
|
56
|
+
return if reply.error != QNetworkReply::NoError
|
|
57
|
+
|
|
58
|
+
body = reply.read_all.to_s
|
|
59
|
+
data = body.empty? ? body : JSON.parse(body)
|
|
60
|
+
on_success.call(data)
|
|
61
|
+
ensure
|
|
62
|
+
reply.delete_later
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require_relative "clash/config"
|
|
2
|
+
require_relative "clash/web"
|
|
3
|
+
|
|
4
|
+
module Clash
|
|
5
|
+
def self.api
|
|
6
|
+
@api ||= Web::Client.new(
|
|
7
|
+
host: "127.0.0.1", port: config.external_controller.split(":")[1],
|
|
8
|
+
secret: config.secret
|
|
9
|
+
).api
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.config
|
|
13
|
+
@config ||= Config.load(ClashSystray.settings.GET_clash_config_path)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Contrib
|
|
2
|
+
class SigHandler < RubyQt6::Bando::QObject
|
|
3
|
+
q_object do
|
|
4
|
+
slot "_noop()"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def initialize(parent)
|
|
8
|
+
super
|
|
9
|
+
|
|
10
|
+
["INT", "TERM"].each do |sig|
|
|
11
|
+
Signal.trap(sig) { |_| QApplication.quit }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
timer = QTimer.new(self)
|
|
15
|
+
timer.timeout.connect(self, :_noop)
|
|
16
|
+
timer.start(1_000)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def _noop
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "contrib/sighandler"
|
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-clash-systray
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.6
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Doe
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruby-qt6-qtwidgets
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby-qt6-qtnetwork
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ruby-qt6-qtwebenginewidgets
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.0'
|
|
54
|
+
email:
|
|
55
|
+
- johndoe@example.com
|
|
56
|
+
executables:
|
|
57
|
+
- clash-systray
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- ".rubocop.yml"
|
|
62
|
+
- ".ruby-version"
|
|
63
|
+
- LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- Rakefile
|
|
66
|
+
- exe/clash-systray
|
|
67
|
+
- lib/clash-systray.rb
|
|
68
|
+
- lib/clash-systray/app.rb
|
|
69
|
+
- lib/clash-systray/app/assets/README.md
|
|
70
|
+
- lib/clash-systray/app/assets/app.svg
|
|
71
|
+
- lib/clash-systray/app/services.rb
|
|
72
|
+
- lib/clash-systray/app/services/export_env_service.rb
|
|
73
|
+
- lib/clash-systray/app/services/open_dashboard_service.rb
|
|
74
|
+
- lib/clash-systray/app/views.rb
|
|
75
|
+
- lib/clash-systray/app/views/systray.rb
|
|
76
|
+
- lib/clash-systray/app/views/systray/helpers.rb
|
|
77
|
+
- lib/clash-systray/config.rb
|
|
78
|
+
- lib/clash-systray/config/initializers/qt6.rb
|
|
79
|
+
- lib/clash-systray/config/settings.rb
|
|
80
|
+
- lib/clash-systray/lib.rb
|
|
81
|
+
- lib/clash-systray/lib/clash.rb
|
|
82
|
+
- lib/clash-systray/lib/clash/config.rb
|
|
83
|
+
- lib/clash-systray/lib/clash/web.rb
|
|
84
|
+
- lib/clash-systray/lib/clash/web/api.rb
|
|
85
|
+
- lib/clash-systray/lib/clash/web/client.rb
|
|
86
|
+
- lib/clash-systray/lib/clash/web/http.rb
|
|
87
|
+
- lib/clash-systray/lib/contrib.rb
|
|
88
|
+
- lib/clash-systray/lib/contrib/sighandler.rb
|
|
89
|
+
- lib/clash-systray/version.rb
|
|
90
|
+
- misc/screenshots/systray.png
|
|
91
|
+
homepage: https://github.com/souk4711/ruby-clash-systray
|
|
92
|
+
licenses:
|
|
93
|
+
- GPL-3.0-only
|
|
94
|
+
metadata: {}
|
|
95
|
+
rdoc_options: []
|
|
96
|
+
require_paths:
|
|
97
|
+
- lib
|
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 3.3.0
|
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0'
|
|
108
|
+
requirements: []
|
|
109
|
+
rubygems_version: 3.6.9
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: A simple system tray for Clash on Linux.
|
|
112
|
+
test_files: []
|