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.
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
+ ![screenshot](https://github.com/souk4711/ruby-clash-systray/raw/main/misc/screenshots/systray.png)
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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "clash-systray"
5
+
6
+ ClashSystray::Application.run
@@ -0,0 +1,5 @@
1
+ # Assets
2
+
3
+ ## License
4
+
5
+ - [app.svg](./app.svg) come from [vinceliuice/Colloid-icon-theme](https://github.com/vinceliuice/Colloid-icon-theme)
@@ -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,2 @@
1
+ require_relative "services/export_env_service"
2
+ require_relative "services/open_dashboard_service"
@@ -0,0 +1,11 @@
1
+ class Systray < RubyQt6::Bando::QWidget
2
+ module Helpers
3
+ def self.extract_proxy_name(str)
4
+ str.to_s.split(" | ")[0].strip
5
+ end
6
+
7
+ def self.inject_proxy_name(*arr)
8
+ arr.join(" | ")
9
+ end
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,8 @@
1
+ require_relative "config/initializers/qt6"
2
+ require_relative "config/settings"
3
+
4
+ module ClashSystray
5
+ def self.settings
6
+ @settings ||= Settings.new
7
+ end
8
+ 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,3 @@
1
+ require_relative "web/api"
2
+ require_relative "web/client"
3
+ require_relative "web/http"
@@ -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"
@@ -0,0 +1,2 @@
1
+ require_relative "lib/clash"
2
+ require_relative "lib/contrib"
@@ -0,0 +1,3 @@
1
+ module ClashSystray
2
+ VERSION = "1.0.6"
3
+ end
@@ -0,0 +1,10 @@
1
+ require "forwardable"
2
+ require "json"
3
+ require "yaml"
4
+
5
+ require "qt6/qtwidgets"
6
+ require "qt6/qtnetwork"
7
+ require "qt6/qtwebenginewidgets"
8
+
9
+ require_relative "clash-systray/app"
10
+ require_relative "clash-systray/version"
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: []