├── .DS_Store ├── .github └── workflows │ └── github-actions.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── app.go ├── backend ├── client │ └── box.go ├── config │ ├── config.go │ └── config_test.go └── data │ └── data.go ├── build.sh ├── build ├── .DS_Store ├── README.md ├── appicon.png ├── darwin │ ├── Info.dev.plist │ └── Info.plist └── windows │ ├── icon.ico │ ├── info.json │ ├── installer │ ├── project.nsi │ └── wails_tools.nsh │ └── wails.exe.manifest ├── cmd └── gpp │ └── main.go ├── frontend ├── READ-THIS.md ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── package-lock.json ├── package.json ├── package.json.md5 ├── src │ ├── App.vue │ ├── assets │ │ ├── fonts │ │ │ ├── OFL.txt │ │ │ └── nunito-v16-latin-regular.woff2 │ │ └── images │ │ │ └── logo-universal.png │ ├── components │ │ ├── HelloWorld.vue │ │ └── Layout.vue │ ├── main.ts │ ├── views │ │ └── Index.vue │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── wailsjs │ ├── go │ ├── main │ │ ├── App.d.ts │ │ └── App.js │ └── models.ts │ └── runtime │ ├── package.json │ ├── runtime.d.ts │ └── runtime.js ├── go.mod ├── go.sum ├── main.go ├── server ├── core │ ├── config.go │ └── server.go ├── export │ └── expoet.go └── install.sh ├── systray ├── internal │ ├── DbusMenu.xml │ ├── StatusNotifierItem.xml │ └── generated │ │ ├── menu │ │ └── dbus_menu.go │ │ └── notifier │ │ └── status_notifier_item.go ├── systray.go ├── systray.h ├── systray_darwin.go ├── systray_darwin.m ├── systray_menu_unix.go ├── systray_other.go ├── systray_unix.go └── systray_windows.go └── wails.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbai225/gpp/7880e370c04d4305132488611fec55504a263afd/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: releaser 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | env: 7 | NODE_OPTIONS: "--max-old-space-size=4096" # 增加 Node.js 可用的最大内存,防止构建失败 8 | jobs: 9 | build-server: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | - name: Set up dependencies 17 | run: sudo apt-get update && sudo apt-get install gcc musl-dev -y 18 | - name: Run GoReleaser 19 | uses: goreleaser/goreleaser-action@v6 20 | with: 21 | distribution: goreleaser 22 | version: latest 23 | args: release --clean 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | CGO_ENABLED: 0 27 | GOOS: linux 28 | GOARCH: amd64 29 | LDFLAGS: '-extldflags "-static"' 30 | build-client: 31 | permissions: write-all 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | build: 36 | - os: windows-latest 37 | GOOS: windows 38 | GOARCH: amd64 39 | - os: macos-latest 40 | GOOS: darwin 41 | GOARCH: arm64 42 | - os: ubuntu-latest 43 | GOOS: linux 44 | GOARCH: amd64 45 | runs-on: ${{ matrix.build.os }} 46 | env: 47 | APP_NAME: gpp 48 | ZIP_FILE: gpp-${{ matrix.build.GOOS }}-${{ matrix.build.GOARCH }}.zip 49 | steps: 50 | - uses: actions/checkout@v4 51 | - name: Set up Go 52 | uses: actions/setup-go@v5 53 | with: 54 | go-version-file: ./go.mod 55 | - run: go version 56 | - run: node -v 57 | - name: Install dependencies 58 | run: | 59 | cd frontend && npm install 60 | go install github.com/wailsapp/wails/v2/cmd/wails@latest 61 | 62 | # Dependencies: GNU/Linux 63 | - name: Update system and dependencies 64 | if: runner.os == 'Linux' 65 | run: | 66 | sudo apt-get update 67 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev 68 | 69 | # Build 70 | - name: Build for ${{ matrix.build.GOOS }}-${{ matrix.build.GOARCH }} 71 | env: 72 | GOOS: ${{ matrix.build.GOOS }} 73 | GOARCH: ${{ matrix.build.GOARCH }} 74 | run: ~/go/bin/wails build -m -trimpath -tags webkit2_41,with_quic -webview2 embed -o ${{ env.APP_NAME }}.exe 75 | 76 | # Compress: macOS 77 | - name: Create a compressed file for macOS 78 | if: runner.os == 'macOS' 79 | run: | 80 | cd ./build/bin 81 | # The compiled product name of macos is always "${{ env.APP_NAME }}.app" 82 | zip -q -r ${{ env.ZIP_FILE }} ${{ env.APP_NAME }}.app 83 | 84 | # Compress: Windows 85 | - name: Create a compressed file for Windows 86 | if: runner.os == 'Windows' 87 | run: | 88 | cd ./build/bin 89 | Compress-Archive -Path ${{ env.APP_NAME }}.exe -DestinationPath ${{ env.ZIP_FILE }} 90 | 91 | # Compress: GNU/Linux 92 | - name: Create a compressed file for GNU/Linux 93 | if: runner.os == 'Linux' 94 | run: | 95 | cd ./build/bin 96 | mv ${{ env.APP_NAME }}.exe ${{ env.APP_NAME }} 97 | zip ${{ env.ZIP_FILE }} ${{ env.APP_NAME }} 98 | 99 | # Artifact 100 | # - name: Upload Artifact ${{ env.ZIP_FILE }} 101 | # uses: actions/upload-artifact@v3 102 | # with: 103 | # name: ${{ env.ZIP_FILE }} 104 | # path: ./build/bin/${{ env.ZIP_FILE }} 105 | 106 | - name: Create Release and Upload Assets 107 | uses: svenstaro/upload-release-action@v2 108 | with: 109 | repo_token: ${{ secrets.GITHUB_TOKEN }} 110 | file: ./build/bin/${{ env.ZIP_FILE }} 111 | asset_name: ${{ env.ZIP_FILE }} 112 | tag: ${{ github.ref }} 113 | release_name: ${{ github.ref_name }} 114 | overwrite: true 115 | draft: false 116 | prerelease: false 117 | body: | 118 | Auto-generated release from GitHub Actions. 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | /.idea/ 23 | build/bin 24 | node_modules 25 | frontend/dist 26 | run.log 27 | dist/ 28 | config.json 29 | .dev -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - id: box 4 | dir: . 5 | main: ./cmd/gpp/main.go 6 | binary: gpp-server 7 | ldflags: -s -w 8 | flags: 9 | - -tags=with_quic 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - amd64 16 | - arm64 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 danbai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gpp 2 | 3 | 基于[sing-box](https://github.com/SagerNet/sing-box)+[wails](https://github.com/wailsapp/wails)的加速器,使用golang编写,支持windows、linux、macos 4 | 5 | - http分流 6 | - gui客户端 7 | - 基于tun代理 8 | - 自定义规则 9 | - 使用简单 10 | 11 | [qq交流群936204503](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=syMCYJm6Isz_yAxUfrQetpNGioUdpdjO&authKey=lkUyXpKkdAzUwOZYq0m%2BH5Y%2FvAU3XegyxWTm5fM1%2BxOZDdBHJUF%2BODVeNg9MraDl&noverify=0&group_code=936204503) [TG交流群](https://t.me/+3cX2FOX_owA1ODM1) 12 | # 截图 13 | 14 | | | | 15 | |---------------------------------------------------------|-------------------------------------------------------| 16 | | ![界面截图](https://imgc.cc/2024/07/06/66888d266d829.png) | ![英雄联盟](https://imgc.cc/2024/07/06/66888d3c49609.png) | 17 | | ![战地2042](https://imgc.cc/2024/07/06/66888d4ea1807.png) | ![绝地求生](https://imgc.cc/2024/07/06/66888d51e610d.png) | 18 | 19 | 20 | # 使用教程 21 | 22 | ## 服务的搭建 23 | 24 | 在优质线路服务器上运行安装脚本 25 | 快速安装服务端脚本(仅支持linux) 26 | ```bash 27 | bash <(curl -sL https://raw.githubusercontent.com/danbai225/gpp/main/server/install.sh) 28 | ``` 29 | 然后执行/usr/local/gpp/run.sh start启动服务端 30 | 31 | 根据提示安装完成后会输出导入链接 32 | 33 | # 运行客户端 34 | 35 | [从releases下载](https://github.com/danbai225/gpp/releases)下载对应系统的客户端以管理员身份运行 36 | 37 | 点击页面上的`Game`或`Http`字样弹出节点列表窗口,在下方粘贴服务端的链接完成节点导入。 38 | 在节点列表选择你的加速节点,如何开始加速。 39 | 40 | ## mac修复损坏 41 | 安装后命令行执行 42 | ```bash 43 | sudo xattr -r -d com.apple.quarantine /Applications/gpp.app 44 | ``` 45 | 46 | # 编译 47 | 48 | ## 编译服务端 49 | 50 | 使用`golang`编译 `cmd/gpp/main.go`获得服务端可执行文件。 51 | 52 | ## 编译GUI客户端 53 | 54 | gui的客户端需要自建构建,需要安装`wails`、`npm`和`golang`,安装方法如下 55 | 56 | - 安装`golang`,[下载地址](https://golang.org/dl/) 57 | - 安装`npm` [下载地址](https://nodejs.org/en/download/) 58 | - 安装`wails`,`go install github.com/wailsapp/wails/v2/cmd/wails@latest` 59 | 60 | 使用`wails`编译 61 | 62 | ``` 63 | wails build 64 | ``` 65 | 66 | # config解释 67 | 68 | ## 服务端 69 | 70 | 配置存放为服务端二进制文件当前目录的`config.json` 71 | 72 | - protocol 协议 73 | - port 端口 74 | - addr 绑定地址 75 | - uuid 认证用途 76 | 77 | ```json 78 | { 79 | "protocol": "vless", 80 | "port": 5123, 81 | "addr": "0.0.0.0", 82 | "uuid":"xxx-xx-xx-xx-xxx" 83 | } 84 | ``` 85 | 86 | ## 客户端 87 | 88 | 配置存放为客户端二进制文件当前目录的`config.json`或者用户目录下`/.gpp/config.json` 89 | 90 | - peer_list 节点列表 91 | - proxy_dns 代理dns 92 | - local_dns 直连dns 93 | - sub_addr 订阅地址 94 | - rules [代理规则](https://sing-box.sagernet.org/zh/configuration/route/rule) 95 | 96 | ```json 97 | { 98 | "peer_list": [ 99 | { 100 | "name": "直连", 101 | "protocol": "direct", 102 | "port": 0, 103 | "addr": "direct", 104 | "uuid": "" 105 | }, 106 | { 107 | "name": "hk", 108 | "protocol": "vless", 109 | "port": 5123, 110 | "addr": "xxx.xx.xx.xx", 111 | "uuid": "xxx-xxx-xx-xxx-xxx" 112 | } 113 | ], 114 | "proxy_dns": "8.8.8.8", 115 | "local_dns": "223.5.5.5", 116 | "sub_addr": "https://sub.com", 117 | "rules": [ 118 | { 119 | "process_name": "C://1.exe", 120 | "outbound": "direct" 121 | }, 122 | { 123 | "domain": "ipv4.ip.sb", 124 | "outbound": "proxy" 125 | } 126 | ] 127 | } 128 | ``` 129 | 130 | # 支持 131 | 132 | - [独角鲸](https://fuckip.me/) 133 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloverstd/tcping/ping" 7 | "github.com/danbai225/gpp/backend/client" 8 | "github.com/danbai225/gpp/backend/config" 9 | "github.com/danbai225/gpp/backend/data" 10 | "github.com/danbai225/gpp/systray" 11 | box "github.com/sagernet/sing-box" 12 | netutils "github.com/shirou/gopsutil/v3/net" 13 | "github.com/wailsapp/wails/v2/pkg/runtime" 14 | "io" 15 | "net" 16 | "net/http" 17 | "os" 18 | "sort" 19 | "strings" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | // App struct 25 | type App struct { 26 | ctx context.Context 27 | conf *config.Config 28 | gamePeer *config.Peer 29 | httpPeer *config.Peer 30 | box *box.Box 31 | lock sync.Mutex 32 | } 33 | 34 | // NewApp creates a new App application struct 35 | func NewApp() *App { 36 | conf := config.Config{} 37 | app := App{ 38 | conf: &conf, 39 | } 40 | return &app 41 | } 42 | func (a *App) systemTray() { 43 | systray.SetIcon(logo) // read the icon from a file 44 | show := systray.AddMenuItem("显示窗口", "显示窗口") 45 | systray.AddSeparator() 46 | exit := systray.AddMenuItem("退出加速器", "退出加速器") 47 | show.Click(func() { runtime.WindowShow(a.ctx) }) 48 | exit.Click(func() { 49 | a.Stop() 50 | runtime.Quit(a.ctx) 51 | systray.Quit() 52 | time.Sleep(time.Second) 53 | os.Exit(0) 54 | }) 55 | systray.SetOnClick(func(menu systray.IMenu) { runtime.WindowShow(a.ctx) }) 56 | go func() { 57 | listener, err := net.Listen("tcp", "127.0.0.1:54713") 58 | if err != nil { 59 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 60 | Type: runtime.ErrorDialog, 61 | Title: "监听错误", 62 | Message: fmt.Sprintln("Error listening0:", err), 63 | }) 64 | } 65 | var conn net.Conn 66 | for { 67 | conn, err = listener.Accept() 68 | if err != nil { 69 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 70 | Type: runtime.ErrorDialog, 71 | Title: "监听错误", 72 | Message: fmt.Sprintln("Error listening1:", err), 73 | }) 74 | continue 75 | } 76 | // 读取指令 77 | buffer := make([]byte, 1024) 78 | n, err := conn.Read(buffer) 79 | if err != nil { 80 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 81 | Type: runtime.ErrorDialog, 82 | Title: "监听错误", 83 | Message: fmt.Sprintln("Error read:", err), 84 | }) 85 | continue 86 | } 87 | command := string(buffer[:n]) 88 | // 如果收到显示窗口的命令,则显示窗口 89 | if command == "SHOW_WINDOW" { 90 | // 展示窗口的代码 91 | runtime.WindowShow(a.ctx) 92 | } 93 | _ = conn.Close() 94 | } 95 | }() 96 | } 97 | 98 | func (a *App) testPing() { 99 | for { 100 | a.PingAll() 101 | time.Sleep(time.Second * 5) 102 | } 103 | } 104 | func (a *App) startup(ctx context.Context) { 105 | a.ctx = ctx 106 | go systray.Run(a.systemTray, func() {}) 107 | loadConfig, err := config.LoadConfig() 108 | if err != nil { 109 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 110 | Type: runtime.WarningDialog, 111 | Title: "配置加载错误", 112 | Message: err.Error(), 113 | }) 114 | } else { 115 | a.conf = loadConfig 116 | } 117 | if len(a.conf.PeerList) > 0 { 118 | if a.conf.GamePeer == "" { 119 | a.conf.GamePeer = a.conf.PeerList[0].Name 120 | } else { 121 | for _, peer := range a.conf.PeerList { 122 | if peer.Name == a.conf.GamePeer { 123 | a.gamePeer = peer 124 | } 125 | } 126 | } 127 | if a.conf.HTTPPeer == "" { 128 | a.conf.HTTPPeer = a.conf.PeerList[0].Name 129 | } else { 130 | for _, peer := range a.conf.PeerList { 131 | if peer.Name == a.conf.HTTPPeer { 132 | a.httpPeer = peer 133 | } 134 | } 135 | } 136 | } 137 | go a.testPing() 138 | } 139 | func (a *App) PingAll() { 140 | a.lock.Lock() 141 | if a.box != nil { 142 | a.lock.Unlock() 143 | return 144 | } 145 | a.lock.Unlock() 146 | group := sync.WaitGroup{} 147 | for i := range a.conf.PeerList { 148 | if a.conf.PeerList[i].Protocol == "direct" { 149 | continue 150 | } 151 | group.Add(1) 152 | peer := a.conf.PeerList[i] 153 | go func() { 154 | defer group.Done() 155 | peer.Ping = pingPort(peer.Addr, peer.Port) 156 | }() 157 | } 158 | group.Wait() 159 | } 160 | 161 | func (a *App) Status() *data.Status { 162 | a.lock.Lock() 163 | defer a.lock.Unlock() 164 | status := data.Status{ 165 | Running: a.box != nil, 166 | GamePeer: a.gamePeer, 167 | HttpPeer: a.httpPeer, 168 | } 169 | 170 | counters, _ := netutils.IOCounters(true) 171 | for _, counter := range counters { 172 | if counter.Name == "utun225" { 173 | status.Up = counter.BytesSent 174 | status.Down = counter.BytesRecv 175 | } 176 | } 177 | return &status 178 | } 179 | 180 | func (a *App) List() []*config.Peer { 181 | list := a.conf.PeerList 182 | sort.Slice(list, func(i, j int) bool { return list[i].Ping < list[j].Ping }) 183 | return list 184 | } 185 | func (a *App) Add(token string) string { 186 | if a.conf.PeerList == nil { 187 | a.conf.PeerList = make([]*config.Peer, 0) 188 | } 189 | if strings.HasPrefix(token, "http") { 190 | _, err := http.Get(token) 191 | if err != nil { 192 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 193 | Type: runtime.ErrorDialog, 194 | Title: "订阅错误", 195 | Message: err.Error(), 196 | }) 197 | return err.Error() 198 | } 199 | a.conf.SubAddr = token 200 | } else { 201 | err, peer := config.ParsePeer(token) 202 | if err != nil { 203 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 204 | Type: runtime.ErrorDialog, 205 | Title: "导入错误", 206 | Message: err.Error(), 207 | }) 208 | return err.Error() 209 | } 210 | for _, p := range a.conf.PeerList { 211 | if p.Name == peer.Name { 212 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 213 | Type: runtime.ErrorDialog, 214 | Title: "导入错误", 215 | Message: fmt.Sprintf("节点 %s 已存在", peer.Name), 216 | }) 217 | return fmt.Sprintf("peer %s already exists", peer.Name) 218 | } 219 | } 220 | a.conf.PeerList = append(a.conf.PeerList, peer) 221 | } 222 | err := config.SaveConfig(a.conf) 223 | if err != nil { 224 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 225 | Type: runtime.ErrorDialog, 226 | Title: "导入错误", 227 | Message: err.Error(), 228 | }) 229 | return err.Error() 230 | } 231 | return "ok" 232 | } 233 | func (a *App) Del(Name string) string { 234 | for i, peer := range a.conf.PeerList { 235 | if peer.Name == Name { 236 | a.conf.PeerList = append(a.conf.PeerList[:i], a.conf.PeerList[i+1:]...) 237 | break 238 | } 239 | } 240 | err := config.SaveConfig(a.conf) 241 | if err != nil { 242 | return err.Error() 243 | } 244 | return "ok" 245 | } 246 | func (a *App) SetPeer(game, http string) string { 247 | for _, peer := range a.conf.PeerList { 248 | if peer.Name == game { 249 | a.gamePeer = peer 250 | a.conf.GamePeer = peer.Name 251 | break 252 | } 253 | } 254 | for _, peer := range a.conf.PeerList { 255 | if peer.Name == http { 256 | a.httpPeer = peer 257 | a.conf.HTTPPeer = peer.Name 258 | break 259 | } 260 | } 261 | err := config.SaveConfig(a.conf) 262 | if err != nil { 263 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 264 | Type: runtime.ErrorDialog, 265 | Title: "保存错误", 266 | Message: err.Error(), 267 | }) 268 | return err.Error() 269 | } 270 | return "ok" 271 | } 272 | 273 | // Start 启动加速 274 | func (a *App) Start() string { 275 | a.lock.Lock() 276 | defer a.lock.Unlock() 277 | if a.box != nil { 278 | return "running" 279 | } 280 | var err error 281 | a.box, err = client.Client(a.gamePeer, a.httpPeer, a.conf.ProxyDNS, a.conf.LocalDNS, a.conf.Rules) 282 | if err != nil { 283 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 284 | Type: runtime.ErrorDialog, 285 | Title: "加速失败", 286 | Message: err.Error(), 287 | }) 288 | a.box = nil 289 | return err.Error() 290 | } 291 | err = a.box.Start() 292 | if err != nil { 293 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 294 | Type: runtime.ErrorDialog, 295 | Title: "加速失败", 296 | Message: err.Error(), 297 | }) 298 | a.box = nil 299 | return err.Error() 300 | } 301 | return "ok" 302 | } 303 | 304 | // Stop 停止加速 305 | func (a *App) Stop() string { 306 | a.lock.Lock() 307 | defer a.lock.Unlock() 308 | if a.box == nil { 309 | return "not running" 310 | } 311 | err := a.box.Close() 312 | if err != nil { 313 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 314 | Type: runtime.ErrorDialog, 315 | Title: "停止失败", 316 | Message: err.Error(), 317 | }) 318 | return err.Error() 319 | } 320 | a.box = nil 321 | return "ok" 322 | } 323 | func pingPort(host string, port uint16) uint { 324 | tcPing := ping.NewTCPing() 325 | tcPing.SetTarget(&ping.Target{ 326 | Host: host, 327 | Port: int(port), 328 | Counter: 1, 329 | Interval: time.Millisecond * 200, 330 | Timeout: time.Second * 3, 331 | }) 332 | start := tcPing.Start() 333 | <-start 334 | result := tcPing.Result() 335 | return uint(result.Avg().Milliseconds()) 336 | } 337 | func httpGet(url string) ([]byte, error) { 338 | resp, err := http.Get(url) 339 | if err != nil { 340 | return nil, err 341 | } 342 | defer func() { _ = resp.Body.Close() }() 343 | return io.ReadAll(resp.Body) 344 | } 345 | -------------------------------------------------------------------------------- /backend/client/box.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/danbai225/gpp/backend/config" 8 | "github.com/google/uuid" 9 | box "github.com/sagernet/sing-box" 10 | "github.com/sagernet/sing-box/option" 11 | dns "github.com/sagernet/sing-dns" 12 | "net/netip" 13 | "os" 14 | "time" 15 | ) 16 | 17 | func getOUt(peer *config.Peer) option.Outbound { 18 | var out option.Outbound 19 | switch peer.Protocol { 20 | case "shadowsocks": 21 | out = option.Outbound{ 22 | Type: "shadowsocks", 23 | ShadowsocksOptions: option.ShadowsocksOutboundOptions{ 24 | ServerOptions: option.ServerOptions{ 25 | Server: peer.Addr, 26 | ServerPort: peer.Port, 27 | }, 28 | Method: "aes-256-gcm", 29 | Password: peer.UUID, 30 | UDPOverTCP: &option.UDPOverTCPOptions{ 31 | Enabled: true, 32 | Version: 2, 33 | }, 34 | Multiplex: &option.OutboundMultiplexOptions{ 35 | Enabled: true, 36 | Protocol: "h2mux", 37 | MaxConnections: 16, 38 | MinStreams: 32, 39 | Padding: false, 40 | }, 41 | }, 42 | } 43 | case "socks": 44 | out = option.Outbound{ 45 | Type: "socks", 46 | SocksOptions: option.SocksOutboundOptions{ 47 | ServerOptions: option.ServerOptions{ 48 | Server: peer.Addr, 49 | ServerPort: peer.Port, 50 | }, 51 | Username: "gpp", 52 | Password: peer.UUID, 53 | UDPOverTCP: &option.UDPOverTCPOptions{ 54 | Enabled: true, 55 | Version: 2, 56 | }, 57 | }, 58 | } 59 | case "hysteria2": 60 | out = option.Outbound{ 61 | Type: "hysteria2", 62 | Hysteria2Options: option.Hysteria2OutboundOptions{ 63 | ServerOptions: option.ServerOptions{ 64 | Server: peer.Addr, 65 | ServerPort: peer.Port, 66 | }, 67 | Password: peer.UUID, 68 | OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ 69 | TLS: &option.OutboundTLSOptions{ 70 | Enabled: true, 71 | ServerName: "gpp", 72 | Insecure: true, 73 | ALPN: option.Listable[string]{"h3"}, 74 | }, 75 | }, 76 | BrutalDebug: false, 77 | }, 78 | } 79 | case "direct": 80 | out = option.Outbound{ 81 | Type: "direct", 82 | } 83 | default: 84 | out = option.Outbound{ 85 | Type: "vless", 86 | VLESSOptions: option.VLESSOutboundOptions{ 87 | ServerOptions: option.ServerOptions{ 88 | Server: peer.Addr, 89 | ServerPort: peer.Port, 90 | }, 91 | UUID: peer.UUID, 92 | Multiplex: &option.OutboundMultiplexOptions{ 93 | Enabled: true, 94 | Protocol: "h2mux", 95 | MaxConnections: 16, 96 | MinStreams: 32, 97 | Padding: false, 98 | }, 99 | }, 100 | } 101 | } 102 | out.Tag = uuid.New().String() 103 | return out 104 | } 105 | func Client(gamePeer, httpPeer *config.Peer, proxyDNS, localDNS string, rules []option.Rule) (*box.Box, error) { 106 | home, _ := os.UserHomeDir() 107 | proxyOut := getOUt(gamePeer) 108 | httpOut := proxyOut 109 | if httpPeer != nil { 110 | httpOut = getOUt(httpPeer) 111 | } 112 | httpOut.Tag = "http" 113 | proxyOut.Tag = "proxy" 114 | options := box.Options{ 115 | Context: context.Background(), 116 | Options: option.Options{ 117 | Log: &option.LogOptions{ 118 | Disabled: true, 119 | }, 120 | DNS: &option.DNSOptions{ 121 | Servers: []option.DNSServerOptions{ 122 | { 123 | Tag: "proxyDns", 124 | Address: proxyDNS, 125 | Detour: "proxy", 126 | Strategy: option.DomainStrategy(dns.DomainStrategyUseIPv4), 127 | }, 128 | { 129 | Tag: "localDns", 130 | Address: localDNS, 131 | Detour: "direct", 132 | Strategy: option.DomainStrategy(dns.DomainStrategyUseIPv4), 133 | }, 134 | { 135 | Tag: "block", 136 | Address: "rcode://success", 137 | Strategy: option.DomainStrategy(dns.DomainStrategyUseIPv4), 138 | }, 139 | }, 140 | Rules: []option.DNSRule{ 141 | { 142 | Type: "default", 143 | DefaultOptions: option.DefaultDNSRule{ 144 | Server: "localDns", 145 | Domain: []string{ 146 | gamePeer.Domain(), 147 | httpPeer.Domain(), 148 | }, 149 | }, 150 | }, 151 | { 152 | Type: "default", 153 | DefaultOptions: option.DefaultDNSRule{ 154 | Server: "localDns", 155 | Geosite: []string{ 156 | "cn", 157 | }, 158 | }, 159 | }, 160 | { 161 | Type: "default", 162 | DefaultOptions: option.DefaultDNSRule{ 163 | Server: "proxyDns", 164 | Geosite: []string{ 165 | "geolocation-!cn", 166 | }, 167 | }, 168 | }, 169 | }, 170 | DNSClientOptions: option.DNSClientOptions{ 171 | DisableCache: true, 172 | }, 173 | }, 174 | Inbounds: []option.Inbound{ 175 | { 176 | Type: "tun", 177 | Tag: "tun-in", 178 | TunOptions: option.TunInboundOptions{ 179 | 180 | InterfaceName: "utun225", 181 | MTU: 9000, 182 | Address: option.Listable[netip.Prefix]{ 183 | netip.MustParsePrefix("172.25.0.1/30"), 184 | }, 185 | AutoRoute: true, 186 | StrictRoute: true, 187 | EndpointIndependentNat: true, 188 | UDPTimeout: option.UDPTimeoutCompat(time.Second * 300), 189 | Stack: "system", 190 | InboundOptions: option.InboundOptions{ 191 | SniffEnabled: true, 192 | }, 193 | }, 194 | }, 195 | { 196 | Type: "socks", 197 | Tag: "socks-in", 198 | SocksOptions: option.SocksInboundOptions{ 199 | ListenOptions: option.ListenOptions{ 200 | Listen: option.NewListenAddress(netip.MustParseAddr("0.0.0.0")), 201 | ListenPort: 5123, 202 | InboundOptions: option.InboundOptions{ 203 | SniffEnabled: true, 204 | }, 205 | }, 206 | }, 207 | }, 208 | }, 209 | Route: &option.RouteOptions{ 210 | AutoDetectInterface: true, 211 | GeoIP: &option.GeoIPOptions{ 212 | Path: fmt.Sprintf("%s%c%s%c%s", home, os.PathSeparator, ".gpp", os.PathSeparator, "geoip.db"), 213 | DownloadURL: "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db", 214 | DownloadDetour: "http", 215 | }, 216 | Geosite: &option.GeositeOptions{ 217 | Path: fmt.Sprintf("%s%c%s%c%s", home, os.PathSeparator, ".gpp", os.PathSeparator, "geosite.db"), 218 | DownloadURL: "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db", 219 | DownloadDetour: "http", 220 | }, 221 | Rules: []option.Rule{ 222 | { 223 | Type: "default", 224 | DefaultOptions: option.DefaultRule{ 225 | Protocol: option.Listable[string]{"dns"}, 226 | Outbound: "dns_out", 227 | }, 228 | }, 229 | { 230 | Type: "default", 231 | DefaultOptions: option.DefaultRule{ 232 | Inbound: option.Listable[string]{"dns_in"}, 233 | Outbound: "dns_out", 234 | }, 235 | }, 236 | }, 237 | }, 238 | Outbounds: []option.Outbound{ 239 | proxyOut, 240 | httpOut, 241 | { 242 | Type: "block", 243 | Tag: "block", 244 | }, 245 | { 246 | Type: "direct", 247 | Tag: "direct", 248 | }, { 249 | Type: "dns", 250 | Tag: "dns_out", 251 | }, 252 | }, 253 | }, 254 | } 255 | 256 | options.Options.Route.Rules = append(options.Options.Route.Rules, []option.Rule{ 257 | { 258 | Type: "default", 259 | DefaultOptions: option.DefaultRule{ 260 | Network: option.Listable[string]{"udp"}, 261 | Port: []uint16{443}, 262 | Outbound: "block", 263 | }, 264 | }, 265 | { 266 | Type: "default", 267 | DefaultOptions: option.DefaultRule{ 268 | Geosite: option.Listable[string]{"cn"}, 269 | Outbound: "direct", 270 | }, 271 | }, 272 | { 273 | Type: "default", 274 | DefaultOptions: option.DefaultRule{ 275 | GeoIP: option.Listable[string]{"cn", "private"}, 276 | Outbound: "direct", 277 | }, 278 | }, 279 | { 280 | Type: "default", 281 | DefaultOptions: option.DefaultRule{ 282 | IPCIDR: option.Listable[string]{ 283 | "85.236.96.0/21", 284 | "188.42.95.0/24", 285 | "188.42.147.0/24"}, 286 | Outbound: "direct", 287 | }, 288 | }, { 289 | Type: "default", 290 | DefaultOptions: option.DefaultRule{ 291 | DomainSuffix: option.Listable[string]{ 292 | "vivox.com", 293 | "cm.steampowered.com", 294 | "steamchina.com", 295 | "steamcontent.com", 296 | "steamserver.net", 297 | "steamusercontent.com", 298 | "csgo.wmsj.cn", 299 | "dl.steam.clngaa.com", 300 | "dl.steam.ksyna.com", 301 | "dota2.wmsj.cn", 302 | "st.dl.bscstorage.net", 303 | "st.dl.eccdnx.com", 304 | "st.dl.pinyuncloud.com", 305 | "steampipe.steamcontent.tnkjmec.com", 306 | "steampowered.com.8686c.com", 307 | "steamstatic.com.8686c.com", 308 | "wmsjsteam.com", 309 | "xz.pphimalayanrt.com"}, 310 | Outbound: "direct", 311 | }, 312 | }, 313 | }...) 314 | options.Options.Route.Rules = append(options.Options.Route.Rules, rules...) 315 | // http 316 | if httpPeer != nil && httpPeer.Name != gamePeer.Name { 317 | options.Options.Route.Rules = append(options.Options.Route.Rules, option.Rule{Type: "default", DefaultOptions: option.DefaultRule{Protocol: option.Listable[string]{"http"}, Outbound: httpOut.Tag}}) 318 | options.Options.Route.Rules = append(options.Options.Route.Rules, option.Rule{Type: "default", DefaultOptions: option.DefaultRule{Network: option.Listable[string]{"tcp"}, Port: []uint16{80, 443, 8080, 8443}, Outbound: httpOut.Tag}}) 319 | } 320 | if config.Debug.Load() { 321 | options.Log = &option.LogOptions{ 322 | Disabled: false, 323 | Level: "trace", 324 | Output: "debug.log", 325 | Timestamp: true, 326 | DisableColor: true, 327 | } 328 | indent, _ := json.MarshalIndent(options, "", " ") 329 | _ = os.WriteFile("sing.json", indent, os.ModePerm) 330 | } 331 | var instance, err = box.New(options) 332 | if err != nil { 333 | return nil, err 334 | } 335 | return instance, nil 336 | } 337 | -------------------------------------------------------------------------------- /backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/sagernet/sing-box/option" 9 | "io" 10 | "net/http" 11 | "net/netip" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | "strings" 16 | "sync/atomic" 17 | ) 18 | 19 | var Debug atomic.Bool 20 | 21 | type Peer struct { 22 | Name string `json:"name"` 23 | Protocol string `json:"protocol"` 24 | Port uint16 `json:"port"` 25 | Addr string `json:"addr"` 26 | UUID string `json:"uuid"` 27 | Ping uint `json:"ping"` 28 | } 29 | 30 | func (p *Peer) Domain() string { 31 | host := strings.Split(p.Addr, ":")[0] 32 | _, err := netip.ParseAddr(host) 33 | if err != nil { 34 | return host 35 | } 36 | return "placeholder.com" 37 | } 38 | 39 | type Config struct { 40 | PeerList []*Peer `json:"peer_list"` 41 | SubAddr string `json:"sub_addr"` 42 | Rules []option.Rule `json:"rules"` 43 | GamePeer string `json:"game_peer"` 44 | HTTPPeer string `json:"http_peer"` 45 | ProxyDNS string `json:"proxy_dns"` 46 | LocalDNS string `json:"local_dns"` 47 | Debug bool `json:"debug"` 48 | } 49 | 50 | func InitConfig() { 51 | home, _ := os.UserHomeDir() 52 | _path := "config.json" 53 | _, err := os.Stat(_path) 54 | if err != nil { 55 | _path = fmt.Sprintf("%s%c%s%c%s", home, os.PathSeparator, ".gpp", os.PathSeparator, "config.json") 56 | } 57 | _ = os.MkdirAll(filepath.Dir(_path), 0o755) 58 | _, err = os.Stat(_path) 59 | if err != nil { 60 | file, _ := json.Marshal(Config{PeerList: make([]*Peer, 0)}) 61 | err = os.WriteFile(_path, file, 0o644) 62 | } 63 | } 64 | func LoadConfig() (*Config, error) { 65 | home, _ := os.UserHomeDir() 66 | _path := "config.json" 67 | _, err := os.Stat(_path) 68 | if err != nil { 69 | _path = fmt.Sprintf("%s%c%s%c%s", home, os.PathSeparator, ".gpp", os.PathSeparator, "config.json") 70 | } 71 | file, _ := os.ReadFile(_path) 72 | conf := &Config{PeerList: make([]*Peer, 0)} 73 | err = json.Unmarshal(file, &conf) 74 | var direct bool 75 | for _, peer := range conf.PeerList { 76 | if peer.Name == "直连" { 77 | direct = true 78 | } 79 | } 80 | if !direct { 81 | conf.PeerList = append(conf.PeerList, &Peer{Name: "直连", Protocol: "direct", Port: 0, Addr: "127.0.0.1", UUID: "", Ping: 0}) 82 | } 83 | if conf.ProxyDNS == "" { 84 | conf.ProxyDNS = "https://1.1.1.1/dns-query" 85 | } 86 | if conf.LocalDNS == "" { 87 | conf.LocalDNS = "https://223.5.5.5/dns-query" 88 | } 89 | if conf.SubAddr != "" { 90 | var resp *http.Response 91 | var data []byte 92 | resp, err = http.Get(conf.SubAddr) 93 | if err != nil { 94 | return nil, err 95 | } 96 | defer func() { _ = resp.Body.Close() }() 97 | data, err = io.ReadAll(resp.Body) 98 | if err != nil { 99 | return nil, err 100 | } 101 | peers := make([]*Peer, 0) 102 | err = json.Unmarshal(data, &peers) 103 | if err != nil { 104 | return nil, err 105 | } 106 | set := make(map[string]*Peer) 107 | conf.PeerList = append(conf.PeerList, peers...) 108 | for _, peer := range conf.PeerList { 109 | set[peer.Name] = peer 110 | } 111 | conf.PeerList = make([]*Peer, 0) 112 | for _, peer := range set { 113 | conf.PeerList = append(conf.PeerList, peer) 114 | } 115 | } 116 | if conf.Debug { 117 | Debug.Swap(true) 118 | } 119 | return conf, err 120 | } 121 | func SaveConfig(config *Config) error { 122 | home, _ := os.UserHomeDir() 123 | _path := "config.json" 124 | _, err := os.Stat(_path) 125 | if err != nil { 126 | _path = fmt.Sprintf("%s%c%s%c%s", home, os.PathSeparator, ".gpp", os.PathSeparator, "config.json") 127 | } 128 | file, _ := json.MarshalIndent(config, "", " ") 129 | return os.WriteFile(_path, file, 0o644) 130 | } 131 | func ParsePeer(token string) (error, *Peer) { 132 | split := strings.Split(token, "#") 133 | name := "" 134 | if len(split) == 2 { 135 | token = split[0] 136 | name = split[1] 137 | } 138 | tokenBytes, err := base64.StdEncoding.DecodeString(token) 139 | if err != nil { 140 | return err, nil 141 | } 142 | token = string(tokenBytes) 143 | split = strings.Split(token, "@") 144 | protocol := strings.ReplaceAll(split[0], "gpp://", "") 145 | switch protocol { 146 | case "vless", "shadowsocks", "socks", "hysteria2": 147 | default: 148 | return fmt.Errorf("unknown protocol: %s", protocol), nil 149 | } 150 | if len(split) != 2 { 151 | return fmt.Errorf("invalid token: %s", token), nil 152 | } 153 | split = strings.Split(split[1], "/") 154 | addr := strings.Split(split[0], ":") 155 | if len(addr) != 2 { 156 | return errors.New("invalid addr: " + split[0]), nil 157 | } 158 | if len(split) != 2 { 159 | return fmt.Errorf("invalid token: %s", token), nil 160 | } 161 | uuid := split[1] 162 | if name == "" { 163 | name = fmt.Sprintf("%s:%s", addr[0], addr[1]) 164 | } 165 | port, _ := strconv.ParseInt(addr[1], 10, 64) 166 | return nil, &Peer{ 167 | Name: name, 168 | Protocol: protocol, 169 | Port: uint16(port), 170 | Addr: addr[0], 171 | UUID: uuid, 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /backend/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "testing" 4 | 5 | func TestParsePeer(t *testing.T) { 6 | err, peer := ParsePeer("Z3BwOi8vdmxlc3NAMS4yLjMuNDozNDU1NS8xMjNiMjJlZi0xMjM0LTEyMzQtMTIzNC1lZmViMjI0ZTAzZTc=") 7 | if err != nil { 8 | t.Error(err) 9 | } 10 | if peer == nil { 11 | t.Error("peer is nil") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "github.com/danbai225/gpp/backend/config" 4 | 5 | type Status struct { 6 | Running bool `json:"running"` 7 | GamePeer *config.Peer `json:"game_peer"` 8 | HttpPeer *config.Peer `json:"http_peer"` 9 | Up uint64 `json:"up"` 10 | Down uint64 `json:"down"` 11 | } 12 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | go install github.com/wailsapp/wails/v2/cmd/wails@latest 2 | wails build -m -trimpath -tags webkit2_41,with_quic -------------------------------------------------------------------------------- /build/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbai225/gpp/7880e370c04d4305132488611fec55504a263afd/build/.DS_Store -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Build Directory 2 | 3 | The build directory is used to house all the build files and assets for your application. 4 | 5 | The structure is: 6 | 7 | * bin - Output directory 8 | * darwin - macOS specific files 9 | * windows - Windows specific files 10 | 11 | ## Mac 12 | 13 | The `darwin` directory holds files specific to Mac builds. 14 | These may be customised and used as part of the build. To return these files to the default state, simply delete them 15 | and 16 | build with `wails build`. 17 | 18 | The directory contains the following files: 19 | 20 | - `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. 21 | - `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. 22 | 23 | ## Windows 24 | 25 | The `windows` directory contains the manifest and rc files used when building with `wails build`. 26 | These may be customised for your application. To return these files to the default state, simply delete them and 27 | build with `wails build`. 28 | 29 | - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to 30 | use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file 31 | will be created using the `appicon.png` file in the build directory. 32 | - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. 33 | - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, 34 | as well as the application itself (right click the exe -> properties -> details) 35 | - `wails.exe.manifest` - The main application manifest file. -------------------------------------------------------------------------------- /build/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbai225/gpp/7880e370c04d4305132488611fec55504a263afd/build/appicon.png -------------------------------------------------------------------------------- /build/darwin/Info.dev.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | {{.Info.ProductName}} 8 | CFBundleExecutable 9 | {{.Name}} 10 | CFBundleIdentifier 11 | com.wails.{{.Name}} 12 | CFBundleVersion 13 | {{.Info.ProductVersion}} 14 | CFBundleGetInfoString 15 | {{.Info.Comments}} 16 | CFBundleShortVersionString 17 | {{.Info.ProductVersion}} 18 | CFBundleIconFile 19 | iconfile 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | {{.Info.Copyright}} 26 | {{if .Info.FileAssociations}} 27 | CFBundleDocumentTypes 28 | 29 | {{range .Info.FileAssociations}} 30 | 31 | CFBundleTypeExtensions 32 | 33 | {{.Ext}} 34 | 35 | CFBundleTypeName 36 | {{.Name}} 37 | CFBundleTypeRole 38 | {{.Role}} 39 | CFBundleTypeIconFile 40 | {{.IconName}} 41 | 42 | {{end}} 43 | 44 | {{end}} 45 | {{if .Info.Protocols}} 46 | CFBundleURLTypes 47 | 48 | {{range .Info.Protocols}} 49 | 50 | CFBundleURLName 51 | com.wails.{{.Scheme}} 52 | CFBundleURLSchemes 53 | 54 | {{.Scheme}} 55 | 56 | CFBundleTypeRole 57 | {{.Role}} 58 | 59 | {{end}} 60 | 61 | {{end}} 62 | NSAppTransportSecurity 63 | 64 | NSAllowsLocalNetworking 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /build/darwin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | {{.Info.ProductName}} 8 | CFBundleExecutable 9 | {{.Name}} 10 | CFBundleIdentifier 11 | com.wails.{{.Name}} 12 | CFBundleVersion 13 | {{.Info.ProductVersion}} 14 | CFBundleGetInfoString 15 | {{.Info.Comments}} 16 | CFBundleShortVersionString 17 | {{.Info.ProductVersion}} 18 | CFBundleIconFile 19 | iconfile 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | {{.Info.Copyright}} 26 | {{if .Info.FileAssociations}} 27 | CFBundleDocumentTypes 28 | 29 | {{range .Info.FileAssociations}} 30 | 31 | CFBundleTypeExtensions 32 | 33 | {{.Ext}} 34 | 35 | CFBundleTypeName 36 | {{.Name}} 37 | CFBundleTypeRole 38 | {{.Role}} 39 | CFBundleTypeIconFile 40 | {{.IconName}} 41 | 42 | {{end}} 43 | 44 | {{end}} 45 | {{if .Info.Protocols}} 46 | CFBundleURLTypes 47 | 48 | {{range .Info.Protocols}} 49 | 50 | CFBundleURLName 51 | com.wails.{{.Scheme}} 52 | CFBundleURLSchemes 53 | 54 | {{.Scheme}} 55 | 56 | CFBundleTypeRole 57 | {{.Role}} 58 | 59 | {{end}} 60 | 61 | {{end}} 62 | 63 | 64 | -------------------------------------------------------------------------------- /build/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbai225/gpp/7880e370c04d4305132488611fec55504a263afd/build/windows/icon.ico -------------------------------------------------------------------------------- /build/windows/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixed": { 3 | "file_version": "{{.Info.ProductVersion}}" 4 | }, 5 | "info": { 6 | "0000": { 7 | "ProductVersion": "{{.Info.ProductVersion}}", 8 | "CompanyName": "{{.Info.CompanyName}}", 9 | "FileDescription": "{{.Info.ProductName}}", 10 | "LegalCopyright": "{{.Info.Copyright}}", 11 | "ProductName": "{{.Info.ProductName}}", 12 | "Comments": "{{.Info.Comments}}" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /build/windows/installer/project.nsi: -------------------------------------------------------------------------------- 1 | Unicode true 2 | 3 | #### 4 | ## Please note: Template replacements don't work in this file. They are provided with default defines like 5 | ## mentioned underneath. 6 | ## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. 7 | ## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually 8 | ## from outside of Wails for debugging and development of the installer. 9 | ## 10 | ## For development first make a wails nsis build to populate the "wails_tools.nsh": 11 | ## > wails build --target windows/amd64 --nsis 12 | ## Then you can call makensis on this file with specifying the path to your binary: 13 | ## For a AMD64 only installer: 14 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe 15 | ## For a ARM64 only installer: 16 | ## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe 17 | ## For a installer with both architectures: 18 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe 19 | #### 20 | ## The following information is taken from the ProjectInfo file, but they can be overwritten here. 21 | #### 22 | ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" 23 | ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" 24 | ## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" 25 | ## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" 26 | ## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" 27 | ### 28 | ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" 29 | ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" 30 | #### 31 | ## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html 32 | #### 33 | ## Include the wails tools 34 | #### 35 | !include "wails_tools.nsh" 36 | 37 | # The version information for this two must consist of 4 parts 38 | VIProductVersion "${INFO_PRODUCTVERSION}.0" 39 | VIFileVersion "${INFO_PRODUCTVERSION}.0" 40 | 41 | VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" 42 | VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" 43 | VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" 44 | VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" 45 | VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" 46 | VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" 47 | 48 | # Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware 49 | ManifestDPIAware true 50 | 51 | !include "MUI.nsh" 52 | 53 | !define MUI_ICON "..\icon.ico" 54 | !define MUI_UNICON "..\icon.ico" 55 | # !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 56 | !define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps 57 | !define MUI_ABORTWARNING # This will warn the user if they exit from the installer. 58 | 59 | !insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. 60 | # !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer 61 | !insertmacro MUI_PAGE_DIRECTORY # In which folder install page. 62 | !insertmacro MUI_PAGE_INSTFILES # Installing page. 63 | !insertmacro MUI_PAGE_FINISH # Finished installation page. 64 | 65 | !insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page 66 | 67 | !insertmacro MUI_LANGUAGE "English" # Set the Language of the installer 68 | 69 | ## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 70 | #!uninstfinalize 'signtool --file "%1"' 71 | #!finalize 'signtool --file "%1"' 72 | 73 | Name "${INFO_PRODUCTNAME}" 74 | OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. 75 | InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). 76 | ShowInstDetails show # This will always show the installation details. 77 | 78 | Function .onInit 79 | !insertmacro wails.checkArchitecture 80 | FunctionEnd 81 | 82 | Section 83 | !insertmacro wails.setShellContext 84 | 85 | !insertmacro wails.webview2runtime 86 | 87 | SetOutPath $INSTDIR 88 | 89 | !insertmacro wails.files 90 | 91 | CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" 92 | CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" 93 | 94 | !insertmacro wails.associateFiles 95 | !insertmacro wails.associateCustomProtocols 96 | 97 | !insertmacro wails.writeUninstaller 98 | SectionEnd 99 | 100 | Section "uninstall" 101 | !insertmacro wails.setShellContext 102 | 103 | RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath 104 | 105 | RMDir /r $INSTDIR 106 | 107 | Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" 108 | Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" 109 | 110 | !insertmacro wails.unassociateFiles 111 | !insertmacro wails.unassociateCustomProtocols 112 | 113 | !insertmacro wails.deleteUninstaller 114 | SectionEnd 115 | -------------------------------------------------------------------------------- /build/windows/installer/wails_tools.nsh: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - Generated automatically by `wails build` 2 | 3 | !include "x64.nsh" 4 | !include "WinVer.nsh" 5 | !include "FileFunc.nsh" 6 | 7 | !ifndef INFO_PROJECTNAME 8 | !define INFO_PROJECTNAME "{{.Name}}" 9 | !endif 10 | !ifndef INFO_COMPANYNAME 11 | !define INFO_COMPANYNAME "{{.Info.CompanyName}}" 12 | !endif 13 | !ifndef INFO_PRODUCTNAME 14 | !define INFO_PRODUCTNAME "{{.Info.ProductName}}" 15 | !endif 16 | !ifndef INFO_PRODUCTVERSION 17 | !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" 18 | !endif 19 | !ifndef INFO_COPYRIGHT 20 | !define INFO_COPYRIGHT "{{.Info.Copyright}}" 21 | !endif 22 | !ifndef PRODUCT_EXECUTABLE 23 | !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" 24 | !endif 25 | !ifndef UNINST_KEY_NAME 26 | !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" 27 | !endif 28 | !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" 29 | 30 | !ifndef REQUEST_EXECUTION_LEVEL 31 | !define REQUEST_EXECUTION_LEVEL "admin" 32 | !endif 33 | 34 | RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" 35 | 36 | !ifdef ARG_WAILS_AMD64_BINARY 37 | !define SUPPORTS_AMD64 38 | !endif 39 | 40 | !ifdef ARG_WAILS_ARM64_BINARY 41 | !define SUPPORTS_ARM64 42 | !endif 43 | 44 | !ifdef SUPPORTS_AMD64 45 | !ifdef SUPPORTS_ARM64 46 | !define ARCH "amd64_arm64" 47 | !else 48 | !define ARCH "amd64" 49 | !endif 50 | !else 51 | !ifdef SUPPORTS_ARM64 52 | !define ARCH "arm64" 53 | !else 54 | !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" 55 | !endif 56 | !endif 57 | 58 | !macro wails.checkArchitecture 59 | !ifndef WAILS_WIN10_REQUIRED 60 | !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." 61 | !endif 62 | 63 | !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED 64 | !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" 65 | !endif 66 | 67 | ${If} ${AtLeastWin10} 68 | !ifdef SUPPORTS_AMD64 69 | ${if} ${IsNativeAMD64} 70 | Goto ok 71 | ${EndIf} 72 | !endif 73 | 74 | !ifdef SUPPORTS_ARM64 75 | ${if} ${IsNativeARM64} 76 | Goto ok 77 | ${EndIf} 78 | !endif 79 | 80 | IfSilent silentArch notSilentArch 81 | silentArch: 82 | SetErrorLevel 65 83 | Abort 84 | notSilentArch: 85 | MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" 86 | Quit 87 | ${else} 88 | IfSilent silentWin notSilentWin 89 | silentWin: 90 | SetErrorLevel 64 91 | Abort 92 | notSilentWin: 93 | MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" 94 | Quit 95 | ${EndIf} 96 | 97 | ok: 98 | !macroend 99 | 100 | !macro wails.files 101 | !ifdef SUPPORTS_AMD64 102 | ${if} ${IsNativeAMD64} 103 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" 104 | ${EndIf} 105 | !endif 106 | 107 | !ifdef SUPPORTS_ARM64 108 | ${if} ${IsNativeARM64} 109 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" 110 | ${EndIf} 111 | !endif 112 | !macroend 113 | 114 | !macro wails.writeUninstaller 115 | WriteUninstaller "$INSTDIR\uninstall.exe" 116 | 117 | SetRegView 64 118 | WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" 119 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" 120 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" 121 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" 122 | WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" 123 | WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" 124 | 125 | ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 126 | IntFmt $0 "0x%08X" $0 127 | WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" 128 | !macroend 129 | 130 | !macro wails.deleteUninstaller 131 | Delete "$INSTDIR\uninstall.exe" 132 | 133 | SetRegView 64 134 | DeleteRegKey HKLM "${UNINST_KEY}" 135 | !macroend 136 | 137 | !macro wails.setShellContext 138 | ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" 139 | SetShellVarContext all 140 | ${else} 141 | SetShellVarContext current 142 | ${EndIf} 143 | !macroend 144 | 145 | # Install webview2 by launching the bootstrapper 146 | # See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment 147 | !macro wails.webview2runtime 148 | !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT 149 | !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" 150 | !endif 151 | 152 | SetRegView 64 153 | # If the admin key exists and is not empty then webview2 is already installed 154 | ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" 155 | ${If} $0 != "" 156 | Goto ok 157 | ${EndIf} 158 | 159 | ${If} ${REQUEST_EXECUTION_LEVEL} == "user" 160 | # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed 161 | ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" 162 | ${If} $0 != "" 163 | Goto ok 164 | ${EndIf} 165 | ${EndIf} 166 | 167 | SetDetailsPrint both 168 | DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" 169 | SetDetailsPrint listonly 170 | 171 | InitPluginsDir 172 | CreateDirectory "$pluginsdir\webview2bootstrapper" 173 | SetOutPath "$pluginsdir\webview2bootstrapper" 174 | File "tmp\MicrosoftEdgeWebview2Setup.exe" 175 | ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' 176 | 177 | SetDetailsPrint both 178 | ok: 179 | !macroend 180 | 181 | # Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b 182 | !macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND 183 | ; Backup the previously associated file class 184 | ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" 185 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" 186 | 187 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" 188 | 189 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` 190 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` 191 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" 192 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` 193 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` 194 | !macroend 195 | 196 | !macro APP_UNASSOCIATE EXT FILECLASS 197 | ; Backup the previously associated file class 198 | ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` 199 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" 200 | 201 | DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` 202 | !macroend 203 | 204 | !macro wails.associateFiles 205 | ; Create file associations 206 | {{range .Info.FileAssociations}} 207 | !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" 208 | 209 | File "..\{{.IconName}}.ico" 210 | {{end}} 211 | !macroend 212 | 213 | !macro wails.unassociateFiles 214 | ; Delete app associations 215 | {{range .Info.FileAssociations}} 216 | !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" 217 | 218 | Delete "$INSTDIR\{{.IconName}}.ico" 219 | {{end}} 220 | !macroend 221 | 222 | !macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND 223 | DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" 224 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" 225 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" 226 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" 227 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" 228 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" 229 | WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" 230 | !macroend 231 | 232 | !macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL 233 | DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" 234 | !macroend 235 | 236 | !macro wails.associateCustomProtocols 237 | ; Create custom protocols associations 238 | {{range .Info.Protocols}} 239 | !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" 240 | 241 | {{end}} 242 | !macroend 243 | 244 | !macro wails.unassociateCustomProtocols 245 | ; Delete app custom protocol associations 246 | {{range .Info.Protocols}} 247 | !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" 248 | {{end}} 249 | !macroend 250 | -------------------------------------------------------------------------------- /build/windows/wails.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true/pm 12 | permonitorv2,permonitor 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /cmd/gpp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/danbai225/gpp/server/core" 7 | "github.com/google/uuid" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | func main() { 14 | path := "config.json" 15 | home, _ := os.UserHomeDir() 16 | if len(os.Args) > 1 { 17 | path = os.Args[1] 18 | } else { 19 | _, err := os.Stat(path) 20 | if err != nil { 21 | path = fmt.Sprintf("%s%c%s%c%s", home, os.PathSeparator, ".gpp", os.PathSeparator, "config.json") 22 | } 23 | } 24 | bytes, err := os.ReadFile(path) 25 | if err != nil { 26 | fmt.Println("read config err:", err, path) 27 | return 28 | } 29 | config := core.Peer{} 30 | _ = json.Unmarshal(bytes, &config) 31 | if config.Port == 0 { 32 | config.Port = 34555 33 | } 34 | if config.Addr == "" { 35 | config.Addr = "0.0.0.0" 36 | } 37 | if config.UUID == "" { 38 | config.UUID = uuid.New().String() 39 | } 40 | err = core.Server(config) 41 | if err != nil { 42 | fmt.Println("run err:", err) 43 | } else { 44 | fmt.Println("starting success!!!") 45 | sigCh := make(chan os.Signal, 1) 46 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 47 | s := <-sigCh 48 | fmt.Printf("Received signal: %v\n", s) 49 | fmt.Println("Exiting...") 50 | os.Exit(0) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/READ-THIS.md: -------------------------------------------------------------------------------- 1 | This template uses a work around as the default template does not compile due to this issue: 2 | https://github.com/vuejs/core/issues/1228 3 | 4 | In `tsconfig.json`, `isolatedModules` is set to `false` rather than `true` to work around the issue. -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 4 | 3 ` 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "unplugin-auto-import": "^0.17.5", 13 | "unplugin-icons": "^0.18.5", 14 | "unplugin-vue-components": "^0.26.0", 15 | "vue": "^3.2.37" 16 | }, 17 | "devDependencies": { 18 | "@babel/types": "^7.18.10", 19 | "@vicons/ionicons5": "^0.12.0", 20 | "@vitejs/plugin-vue": "^3.0.3", 21 | "naive-ui": "^2.38.1", 22 | "typescript": "^4.6.4", 23 | "vite": "^3.0.7", 24 | "vue-tsc": "^1.8.27" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | 0224a46745059c12a96d8d69f34dc612 -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nunito-v16-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbai225/gpp/7880e370c04d4305132488611fec55504a263afd/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/images/logo-universal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbai225/gpp/7880e370c04d4305132488611fec55504a263afd/frontend/src/assets/images/logo-universal.png -------------------------------------------------------------------------------- /frontend/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | 75 | -------------------------------------------------------------------------------- /frontend/src/components/Layout.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 88 | 89 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /frontend/src/views/Index.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 261 | 262 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type {DefineComponent} from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": [ 14 | "ESNext", 15 | "DOM" 16 | ], 17 | "skipLibCheck": true 18 | }, 19 | "include": [ 20 | "src/**/*.ts", 21 | "src/**/*.d.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue" 24 | ], 25 | "references": [ 26 | { 27 | "path": "./tsconfig.node.json" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": [ 9 | "vite.config.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import AutoImport from 'unplugin-auto-import/vite' 4 | import Icons from 'unplugin-icons/vite' 5 | 6 | import Components from 'unplugin-vue-components/vite' 7 | import {NaiveUiResolver} from 'unplugin-vue-components/resolvers' 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | AutoImport({ 13 | imports: [ 14 | 'vue', 15 | { 16 | 'naive-ui': [ 17 | 'useDialog', 18 | 'useMessage', 19 | 'useNotification', 20 | 'useLoadingBar' 21 | ] 22 | } 23 | ] 24 | }), 25 | Components({ 26 | resolvers: [NaiveUiResolver()] 27 | }), 28 | Icons(), 29 | ] 30 | }) 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/main/App.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | import {config} from '../models'; 4 | import {data} from '../models'; 5 | 6 | export function Add(arg1:string):Promise; 7 | 8 | export function Del(arg1:string):Promise; 9 | 10 | export function List():Promise>; 11 | 12 | export function PingAll():Promise; 13 | 14 | export function SetPeer(arg1:string,arg2:string):Promise; 15 | 16 | export function Start():Promise; 17 | 18 | export function Status():Promise; 19 | 20 | export function Stop():Promise; 21 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/main/App.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export function Add(arg1) { 6 | return window['go']['main']['App']['Add'](arg1); 7 | } 8 | 9 | export function Del(arg1) { 10 | return window['go']['main']['App']['Del'](arg1); 11 | } 12 | 13 | export function List() { 14 | return window['go']['main']['App']['List'](); 15 | } 16 | 17 | export function PingAll() { 18 | return window['go']['main']['App']['PingAll'](); 19 | } 20 | 21 | export function SetPeer(arg1, arg2) { 22 | return window['go']['main']['App']['SetPeer'](arg1, arg2); 23 | } 24 | 25 | export function Start() { 26 | return window['go']['main']['App']['Start'](); 27 | } 28 | 29 | export function Status() { 30 | return window['go']['main']['App']['Status'](); 31 | } 32 | 33 | export function Stop() { 34 | return window['go']['main']['App']['Stop'](); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/models.ts: -------------------------------------------------------------------------------- 1 | export namespace config { 2 | 3 | export class Peer { 4 | name: string; 5 | protocol: string; 6 | port: number; 7 | addr: string; 8 | uuid: string; 9 | ping: number; 10 | 11 | static createFrom(source: any = {}) { 12 | return new Peer(source); 13 | } 14 | 15 | constructor(source: any = {}) { 16 | if ('string' === typeof source) source = JSON.parse(source); 17 | this.name = source["name"]; 18 | this.protocol = source["protocol"]; 19 | this.port = source["port"]; 20 | this.addr = source["addr"]; 21 | this.uuid = source["uuid"]; 22 | this.ping = source["ping"]; 23 | } 24 | } 25 | 26 | } 27 | 28 | export namespace data { 29 | 30 | export class Status { 31 | running: boolean; 32 | game_peer?: config.Peer; 33 | http_peer?: config.Peer; 34 | up: number; 35 | down: number; 36 | 37 | static createFrom(source: any = {}) { 38 | return new Status(source); 39 | } 40 | 41 | constructor(source: any = {}) { 42 | if ('string' === typeof source) source = JSON.parse(source); 43 | this.running = source["running"]; 44 | this.game_peer = this.convertValues(source["game_peer"], config.Peer); 45 | this.http_peer = this.convertValues(source["http_peer"], config.Peer); 46 | this.up = source["up"]; 47 | this.down = source["down"]; 48 | } 49 | 50 | convertValues(a: any, classs: any, asMap: boolean = false): any { 51 | if (!a) { 52 | return a; 53 | } 54 | if (a.slice && a.map) { 55 | return (a as any[]).map(elem => this.convertValues(elem, classs)); 56 | } else if ("object" === typeof a) { 57 | if (asMap) { 58 | for (const key of Object.keys(a)) { 59 | a[key] = new classs(a[key]); 60 | } 61 | return a; 62 | } 63 | return new classs(a); 64 | } 65 | return a; 66 | } 67 | } 68 | 69 | } 70 | 71 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wailsapp/runtime", 3 | "version": "2.0.0", 4 | "description": "Wails Javascript runtime library", 5 | "main": "runtime.js", 6 | "types": "runtime.d.ts", 7 | "scripts": { 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wailsapp/wails.git" 12 | }, 13 | "keywords": [ 14 | "Wails", 15 | "Javascript", 16 | "Go" 17 | ], 18 | "author": "Lea Anthony ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wailsapp/wails/issues" 22 | }, 23 | "homepage": "https://github.com/wailsapp/wails#readme" 24 | } 25 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/runtime.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | _ __ _ __ 3 | | | / /___ _(_) /____ 4 | | | /| / / __ `/ / / ___/ 5 | | |/ |/ / /_/ / / (__ ) 6 | |__/|__/\__,_/_/_/____/ 7 | The electron alternative for Go 8 | (c) Lea Anthony 2019-present 9 | */ 10 | 11 | export interface Position { 12 | x: number; 13 | y: number; 14 | } 15 | 16 | export interface Size { 17 | w: number; 18 | h: number; 19 | } 20 | 21 | export interface Screen { 22 | isCurrent: boolean; 23 | isPrimary: boolean; 24 | width : number 25 | height : number 26 | } 27 | 28 | // Environment information such as platform, buildtype, ... 29 | export interface EnvironmentInfo { 30 | buildType: string; 31 | platform: string; 32 | arch: string; 33 | } 34 | 35 | // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) 36 | // emits the given event. Optional data may be passed with the event. 37 | // This will trigger any event listeners. 38 | export function EventsEmit(eventName: string, ...data: any): void; 39 | 40 | // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. 41 | export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; 42 | 43 | // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) 44 | // sets up a listener for the given event name, but will only trigger a given number times. 45 | export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; 46 | 47 | // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) 48 | // sets up a listener for the given event name, but will only trigger once. 49 | export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; 50 | 51 | // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) 52 | // unregisters the listener for the given event name. 53 | export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; 54 | 55 | // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) 56 | // unregisters all listeners. 57 | export function EventsOffAll(): void; 58 | 59 | // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) 60 | // logs the given message as a raw message 61 | export function LogPrint(message: string): void; 62 | 63 | // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) 64 | // logs the given message at the `trace` log level. 65 | export function LogTrace(message: string): void; 66 | 67 | // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) 68 | // logs the given message at the `debug` log level. 69 | export function LogDebug(message: string): void; 70 | 71 | // [LogError](https://wails.io/docs/reference/runtime/log#logerror) 72 | // logs the given message at the `error` log level. 73 | export function LogError(message: string): void; 74 | 75 | // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) 76 | // logs the given message at the `fatal` log level. 77 | // The application will quit after calling this method. 78 | export function LogFatal(message: string): void; 79 | 80 | // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) 81 | // logs the given message at the `info` log level. 82 | export function LogInfo(message: string): void; 83 | 84 | // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) 85 | // logs the given message at the `warning` log level. 86 | export function LogWarning(message: string): void; 87 | 88 | // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) 89 | // Forces a reload by the main application as well as connected browsers. 90 | export function WindowReload(): void; 91 | 92 | // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) 93 | // Reloads the application frontend. 94 | export function WindowReloadApp(): void; 95 | 96 | // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) 97 | // Sets the window AlwaysOnTop or not on top. 98 | export function WindowSetAlwaysOnTop(b: boolean): void; 99 | 100 | // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) 101 | // *Windows only* 102 | // Sets window theme to system default (dark/light). 103 | export function WindowSetSystemDefaultTheme(): void; 104 | 105 | // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) 106 | // *Windows only* 107 | // Sets window to light theme. 108 | export function WindowSetLightTheme(): void; 109 | 110 | // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) 111 | // *Windows only* 112 | // Sets window to dark theme. 113 | export function WindowSetDarkTheme(): void; 114 | 115 | // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) 116 | // Centers the window on the monitor the window is currently on. 117 | export function WindowCenter(): void; 118 | 119 | // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) 120 | // Sets the text in the window title bar. 121 | export function WindowSetTitle(title: string): void; 122 | 123 | // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) 124 | // Makes the window full screen. 125 | export function WindowFullscreen(): void; 126 | 127 | // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) 128 | // Restores the previous window dimensions and position prior to full screen. 129 | export function WindowUnfullscreen(): void; 130 | 131 | // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) 132 | // Returns the state of the window, i.e. whether the window is in full screen mode or not. 133 | export function WindowIsFullscreen(): Promise; 134 | 135 | // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) 136 | // Sets the width and height of the window. 137 | export function WindowSetSize(width: number, height: number): void; 138 | 139 | // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) 140 | // Gets the width and height of the window. 141 | export function WindowGetSize(): Promise; 142 | 143 | // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) 144 | // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. 145 | // Setting a size of 0,0 will disable this constraint. 146 | export function WindowSetMaxSize(width: number, height: number): void; 147 | 148 | // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) 149 | // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. 150 | // Setting a size of 0,0 will disable this constraint. 151 | export function WindowSetMinSize(width: number, height: number): void; 152 | 153 | // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) 154 | // Sets the window position relative to the monitor the window is currently on. 155 | export function WindowSetPosition(x: number, y: number): void; 156 | 157 | // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) 158 | // Gets the window position relative to the monitor the window is currently on. 159 | export function WindowGetPosition(): Promise; 160 | 161 | // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) 162 | // Hides the window. 163 | export function WindowHide(): void; 164 | 165 | // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) 166 | // Shows the window, if it is currently hidden. 167 | export function WindowShow(): void; 168 | 169 | // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) 170 | // Maximises the window to fill the screen. 171 | export function WindowMaximise(): void; 172 | 173 | // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) 174 | // Toggles between Maximised and UnMaximised. 175 | export function WindowToggleMaximise(): void; 176 | 177 | // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) 178 | // Restores the window to the dimensions and position prior to maximising. 179 | export function WindowUnmaximise(): void; 180 | 181 | // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) 182 | // Returns the state of the window, i.e. whether the window is maximised or not. 183 | export function WindowIsMaximised(): Promise; 184 | 185 | // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) 186 | // Minimises the window. 187 | export function WindowMinimise(): void; 188 | 189 | // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) 190 | // Restores the window to the dimensions and position prior to minimising. 191 | export function WindowUnminimise(): void; 192 | 193 | // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) 194 | // Returns the state of the window, i.e. whether the window is minimised or not. 195 | export function WindowIsMinimised(): Promise; 196 | 197 | // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) 198 | // Returns the state of the window, i.e. whether the window is normal or not. 199 | export function WindowIsNormal(): Promise; 200 | 201 | // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) 202 | // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. 203 | export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; 204 | 205 | // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) 206 | // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. 207 | export function ScreenGetAll(): Promise; 208 | 209 | // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) 210 | // Opens the given URL in the system browser. 211 | export function BrowserOpenURL(url: string): void; 212 | 213 | // [Environment](https://wails.io/docs/reference/runtime/intro#environment) 214 | // Returns information about the environment 215 | export function Environment(): Promise; 216 | 217 | // [Quit](https://wails.io/docs/reference/runtime/intro#quit) 218 | // Quits the application. 219 | export function Quit(): void; 220 | 221 | // [Hide](https://wails.io/docs/reference/runtime/intro#hide) 222 | // Hides the application. 223 | export function Hide(): void; 224 | 225 | // [Show](https://wails.io/docs/reference/runtime/intro#show) 226 | // Shows the application. 227 | export function Show(): void; 228 | 229 | // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) 230 | // Returns the current text stored on clipboard 231 | export function ClipboardGetText(): Promise; 232 | 233 | // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) 234 | // Sets a text on the clipboard 235 | export function ClipboardSetText(text: string): Promise; 236 | 237 | // [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) 238 | // OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. 239 | export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void 240 | 241 | // [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) 242 | // OnFileDropOff removes the drag and drop listeners and handlers. 243 | export function OnFileDropOff() :void 244 | 245 | // Check if the file path resolver is available 246 | export function CanResolveFilePaths(): boolean; 247 | 248 | // Resolves file paths for an array of files 249 | export function ResolveFilePaths(files: File[]): void -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | _ __ _ __ 3 | | | / /___ _(_) /____ 4 | | | /| / / __ `/ / / ___/ 5 | | |/ |/ / /_/ / / (__ ) 6 | |__/|__/\__,_/_/_/____/ 7 | The electron alternative for Go 8 | (c) Lea Anthony 2019-present 9 | */ 10 | 11 | export function LogPrint(message) { 12 | window.runtime.LogPrint(message); 13 | } 14 | 15 | export function LogTrace(message) { 16 | window.runtime.LogTrace(message); 17 | } 18 | 19 | export function LogDebug(message) { 20 | window.runtime.LogDebug(message); 21 | } 22 | 23 | export function LogInfo(message) { 24 | window.runtime.LogInfo(message); 25 | } 26 | 27 | export function LogWarning(message) { 28 | window.runtime.LogWarning(message); 29 | } 30 | 31 | export function LogError(message) { 32 | window.runtime.LogError(message); 33 | } 34 | 35 | export function LogFatal(message) { 36 | window.runtime.LogFatal(message); 37 | } 38 | 39 | export function EventsOnMultiple(eventName, callback, maxCallbacks) { 40 | return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); 41 | } 42 | 43 | export function EventsOn(eventName, callback) { 44 | return EventsOnMultiple(eventName, callback, -1); 45 | } 46 | 47 | export function EventsOff(eventName, ...additionalEventNames) { 48 | return window.runtime.EventsOff(eventName, ...additionalEventNames); 49 | } 50 | 51 | export function EventsOnce(eventName, callback) { 52 | return EventsOnMultiple(eventName, callback, 1); 53 | } 54 | 55 | export function EventsEmit(eventName) { 56 | let args = [eventName].slice.call(arguments); 57 | return window.runtime.EventsEmit.apply(null, args); 58 | } 59 | 60 | export function WindowReload() { 61 | window.runtime.WindowReload(); 62 | } 63 | 64 | export function WindowReloadApp() { 65 | window.runtime.WindowReloadApp(); 66 | } 67 | 68 | export function WindowSetAlwaysOnTop(b) { 69 | window.runtime.WindowSetAlwaysOnTop(b); 70 | } 71 | 72 | export function WindowSetSystemDefaultTheme() { 73 | window.runtime.WindowSetSystemDefaultTheme(); 74 | } 75 | 76 | export function WindowSetLightTheme() { 77 | window.runtime.WindowSetLightTheme(); 78 | } 79 | 80 | export function WindowSetDarkTheme() { 81 | window.runtime.WindowSetDarkTheme(); 82 | } 83 | 84 | export function WindowCenter() { 85 | window.runtime.WindowCenter(); 86 | } 87 | 88 | export function WindowSetTitle(title) { 89 | window.runtime.WindowSetTitle(title); 90 | } 91 | 92 | export function WindowFullscreen() { 93 | window.runtime.WindowFullscreen(); 94 | } 95 | 96 | export function WindowUnfullscreen() { 97 | window.runtime.WindowUnfullscreen(); 98 | } 99 | 100 | export function WindowIsFullscreen() { 101 | return window.runtime.WindowIsFullscreen(); 102 | } 103 | 104 | export function WindowGetSize() { 105 | return window.runtime.WindowGetSize(); 106 | } 107 | 108 | export function WindowSetSize(width, height) { 109 | window.runtime.WindowSetSize(width, height); 110 | } 111 | 112 | export function WindowSetMaxSize(width, height) { 113 | window.runtime.WindowSetMaxSize(width, height); 114 | } 115 | 116 | export function WindowSetMinSize(width, height) { 117 | window.runtime.WindowSetMinSize(width, height); 118 | } 119 | 120 | export function WindowSetPosition(x, y) { 121 | window.runtime.WindowSetPosition(x, y); 122 | } 123 | 124 | export function WindowGetPosition() { 125 | return window.runtime.WindowGetPosition(); 126 | } 127 | 128 | export function WindowHide() { 129 | window.runtime.WindowHide(); 130 | } 131 | 132 | export function WindowShow() { 133 | window.runtime.WindowShow(); 134 | } 135 | 136 | export function WindowMaximise() { 137 | window.runtime.WindowMaximise(); 138 | } 139 | 140 | export function WindowToggleMaximise() { 141 | window.runtime.WindowToggleMaximise(); 142 | } 143 | 144 | export function WindowUnmaximise() { 145 | window.runtime.WindowUnmaximise(); 146 | } 147 | 148 | export function WindowIsMaximised() { 149 | return window.runtime.WindowIsMaximised(); 150 | } 151 | 152 | export function WindowMinimise() { 153 | window.runtime.WindowMinimise(); 154 | } 155 | 156 | export function WindowUnminimise() { 157 | window.runtime.WindowUnminimise(); 158 | } 159 | 160 | export function WindowSetBackgroundColour(R, G, B, A) { 161 | window.runtime.WindowSetBackgroundColour(R, G, B, A); 162 | } 163 | 164 | export function ScreenGetAll() { 165 | return window.runtime.ScreenGetAll(); 166 | } 167 | 168 | export function WindowIsMinimised() { 169 | return window.runtime.WindowIsMinimised(); 170 | } 171 | 172 | export function WindowIsNormal() { 173 | return window.runtime.WindowIsNormal(); 174 | } 175 | 176 | export function BrowserOpenURL(url) { 177 | window.runtime.BrowserOpenURL(url); 178 | } 179 | 180 | export function Environment() { 181 | return window.runtime.Environment(); 182 | } 183 | 184 | export function Quit() { 185 | window.runtime.Quit(); 186 | } 187 | 188 | export function Hide() { 189 | window.runtime.Hide(); 190 | } 191 | 192 | export function Show() { 193 | window.runtime.Show(); 194 | } 195 | 196 | export function ClipboardGetText() { 197 | return window.runtime.ClipboardGetText(); 198 | } 199 | 200 | export function ClipboardSetText(text) { 201 | return window.runtime.ClipboardSetText(text); 202 | } 203 | 204 | /** 205 | * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. 206 | * 207 | * @export 208 | * @callback OnFileDropCallback 209 | * @param {number} x - x coordinate of the drop 210 | * @param {number} y - y coordinate of the drop 211 | * @param {string[]} paths - A list of file paths. 212 | */ 213 | 214 | /** 215 | * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. 216 | * 217 | * @export 218 | * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. 219 | * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) 220 | */ 221 | export function OnFileDrop(callback, useDropTarget) { 222 | return window.runtime.OnFileDrop(callback, useDropTarget); 223 | } 224 | 225 | /** 226 | * OnFileDropOff removes the drag and drop listeners and handlers. 227 | */ 228 | export function OnFileDropOff() { 229 | return window.runtime.OnFileDropOff(); 230 | } 231 | 232 | export function CanResolveFilePaths() { 233 | return window.runtime.CanResolveFilePaths(); 234 | } 235 | 236 | export function ResolveFilePaths(files) { 237 | return window.runtime.ResolveFilePaths(files); 238 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danbai225/gpp 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/cloverstd/tcping v0.1.1 7 | github.com/godbus/dbus/v5 v5.1.0 8 | github.com/google/uuid v1.6.0 9 | github.com/sagernet/sing v0.5.0-rc.2 10 | github.com/sagernet/sing-box v1.10.1 11 | github.com/sagernet/sing-dns v0.3.0-rc.2 12 | github.com/shirou/gopsutil/v3 v3.24.5 13 | github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c 14 | github.com/wailsapp/wails/v2 v2.10.1 15 | golang.org/x/sys v0.30.0 16 | ) 17 | 18 | require ( 19 | berty.tech/go-libtor v1.0.385 // indirect 20 | github.com/andybalholm/brotli v1.1.1 // indirect 21 | github.com/bep/debounce v1.2.1 // indirect 22 | github.com/caddyserver/certmagic v0.21.4 // indirect 23 | github.com/caddyserver/zerossl v0.1.3 // indirect 24 | github.com/cloudflare/circl v1.5.0 // indirect 25 | github.com/cretz/bine v0.2.0 // indirect 26 | github.com/fsnotify/fsnotify v1.8.0 // indirect 27 | github.com/go-chi/chi/v5 v5.1.0 // indirect 28 | github.com/go-ole/go-ole v1.3.0 // indirect 29 | github.com/gobwas/httphead v0.1.0 // indirect 30 | github.com/gobwas/pool v0.2.1 // indirect 31 | github.com/gofrs/uuid/v5 v5.3.0 // indirect 32 | github.com/google/btree v1.1.3 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/hashicorp/yamux v0.1.2 // indirect 35 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 36 | github.com/josharian/native v1.1.0 // indirect 37 | github.com/klauspost/compress v1.17.11 // indirect 38 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 39 | github.com/labstack/echo/v4 v4.13.3 // indirect 40 | github.com/labstack/gommon v0.4.2 // indirect 41 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 42 | github.com/leaanthony/gosod v1.0.4 // indirect 43 | github.com/leaanthony/slicer v1.6.0 // indirect 44 | github.com/leaanthony/u v1.1.1 // indirect 45 | github.com/libdns/alidns v1.0.3 // indirect 46 | github.com/libdns/cloudflare v0.1.1 // indirect 47 | github.com/libdns/libdns v0.2.2 // indirect 48 | github.com/logrusorgru/aurora v2.0.3+incompatible // indirect 49 | github.com/mattn/go-colorable v0.1.13 // indirect 50 | github.com/mattn/go-isatty v0.0.20 // indirect 51 | github.com/mdlayher/netlink v1.7.2 // indirect 52 | github.com/mdlayher/socket v0.5.1 // indirect 53 | github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect 54 | github.com/mholt/acmez v1.2.0 // indirect 55 | github.com/mholt/acmez/v2 v2.0.3 // indirect 56 | github.com/miekg/dns v1.1.62 // indirect 57 | github.com/ooni/go-libtor v1.1.8 // indirect 58 | github.com/oschwald/maxminddb-golang v1.13.1 // indirect 59 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 60 | github.com/pkg/errors v0.9.1 // indirect 61 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 62 | github.com/quic-go/qpack v0.5.1 // indirect 63 | github.com/quic-go/qtls-go1-20 v0.4.1 // indirect 64 | github.com/rivo/uniseg v0.4.7 // indirect 65 | github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect 66 | github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect 67 | github.com/sagernet/fswatch v0.1.1 // indirect 68 | github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f // indirect 69 | github.com/sagernet/netlink v0.0.0-20240916134442-83396419aa8b // indirect 70 | github.com/sagernet/nftables v0.3.0-beta.4 // indirect 71 | github.com/sagernet/quic-go v0.47.0 // indirect 72 | github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect 73 | github.com/sagernet/sing-mux v0.2.0 // indirect 74 | github.com/sagernet/sing-quic v0.3.0-rc.1 // indirect 75 | github.com/sagernet/sing-shadowsocks v0.2.7 // indirect 76 | github.com/sagernet/sing-shadowsocks2 v0.2.0 // indirect 77 | github.com/sagernet/sing-shadowtls v0.1.4 // indirect 78 | github.com/sagernet/sing-tun v0.4.0-rc.4 // indirect 79 | github.com/sagernet/sing-vmess v0.1.12 // indirect 80 | github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect 81 | github.com/sagernet/utls v1.6.7 // indirect 82 | github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 // indirect 83 | github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect 84 | github.com/samber/lo v1.49.1 // indirect 85 | github.com/tkrajina/go-reflector v0.5.8 // indirect 86 | github.com/valyala/bytebufferpool v1.0.0 // indirect 87 | github.com/valyala/fasttemplate v1.2.2 // indirect 88 | github.com/vishvananda/netns v0.0.4 // indirect 89 | github.com/wailsapp/go-webview2 v1.0.19 // indirect 90 | github.com/wailsapp/mimetype v1.4.1 // indirect 91 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 92 | github.com/zeebo/blake3 v0.2.4 // indirect 93 | go.uber.org/multierr v1.11.0 // indirect 94 | go.uber.org/zap v1.27.0 // indirect 95 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 96 | golang.org/x/crypto v0.33.0 // indirect 97 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 98 | golang.org/x/mod v0.23.0 // indirect 99 | golang.org/x/net v0.35.0 // indirect 100 | golang.org/x/sync v0.11.0 // indirect 101 | golang.org/x/text v0.22.0 // indirect 102 | golang.org/x/time v0.8.0 // indirect 103 | golang.org/x/tools v0.30.0 // indirect 104 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect 105 | google.golang.org/grpc v1.67.1 // indirect 106 | google.golang.org/protobuf v1.35.1 // indirect 107 | lukechampine.com/blake3 v1.3.0 // indirect 108 | ) 109 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "github.com/danbai225/gpp/backend/config" 6 | "github.com/wailsapp/wails/v2" 7 | "github.com/wailsapp/wails/v2/pkg/options" 8 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 9 | "net" 10 | ) 11 | 12 | //go:embed all:frontend/dist 13 | var assets embed.FS 14 | 15 | //go:embed build/windows/icon.ico 16 | var logo []byte 17 | 18 | func main() { 19 | dial, err := net.Dial("tcp", "127.0.0.1:54713") 20 | if err == nil { 21 | _, _ = dial.Write([]byte("SHOW_WINDOW")) 22 | _ = dial.Close() 23 | return 24 | } 25 | config.InitConfig() 26 | // Create an instance of the app structure 27 | app := NewApp() 28 | defer app.Stop() 29 | 30 | // Create application with options 31 | err = wails.Run(&options.App{ 32 | Title: "gpp", 33 | Width: 360, 34 | Height: 520, 35 | DisableResize: true, 36 | HideWindowOnClose: true, 37 | AssetServer: &assetserver.Options{ 38 | Assets: assets, 39 | }, 40 | BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 0}, 41 | OnStartup: app.startup, 42 | Bind: []interface{}{ 43 | app, 44 | }, 45 | }) 46 | if err != nil { 47 | println("Error:", err.Error()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/core/config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Peer struct { 4 | Protocol string `json:"protocol"` 5 | Port uint16 `json:"port"` 6 | Addr string `json:"addr"` 7 | UUID string `json:"uuid"` 8 | } 9 | -------------------------------------------------------------------------------- /server/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | box "github.com/sagernet/sing-box" 12 | "github.com/sagernet/sing-box/option" 13 | "github.com/sagernet/sing/common/auth" 14 | "math/big" 15 | "net/netip" 16 | "time" 17 | ) 18 | 19 | func Server(conf Peer) error { 20 | var in option.Inbound 21 | switch conf.Protocol { 22 | case "shadowsocks": 23 | in = option.Inbound{ 24 | Type: "shadowsocks", 25 | Tag: "ss-in", 26 | ShadowsocksOptions: option.ShadowsocksInboundOptions{ 27 | ListenOptions: option.ListenOptions{ 28 | Listen: option.NewListenAddress(netip.MustParseAddr(conf.Addr)), 29 | ListenPort: conf.Port, 30 | }, 31 | Method: "aes-256-gcm", 32 | Password: conf.UUID, 33 | Multiplex: &option.InboundMultiplexOptions{ 34 | Enabled: true, 35 | }, 36 | }, 37 | } 38 | case "socks": 39 | in = option.Inbound{ 40 | Type: "socks", 41 | Tag: "socks-in", 42 | SocksOptions: option.SocksInboundOptions{ 43 | ListenOptions: option.ListenOptions{ 44 | Listen: option.NewListenAddress(netip.MustParseAddr(conf.Addr)), 45 | ListenPort: conf.Port, 46 | }, 47 | Users: []auth.User{ 48 | { 49 | Username: "gpp", 50 | Password: conf.UUID, 51 | }, 52 | }, 53 | }, 54 | } 55 | case "hysteria2": 56 | c, k := generateKey() 57 | in = option.Inbound{ 58 | Type: "hysteria2", 59 | Tag: "hy2-in", 60 | Hysteria2Options: option.Hysteria2InboundOptions{ 61 | ListenOptions: option.ListenOptions{ 62 | Listen: option.NewListenAddress(netip.MustParseAddr(conf.Addr)), 63 | ListenPort: conf.Port, 64 | }, 65 | Users: []option.Hysteria2User{ 66 | { 67 | Name: "gpp", 68 | Password: conf.UUID, 69 | }, 70 | }, 71 | InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ 72 | TLS: &option.InboundTLSOptions{ 73 | Enabled: true, 74 | ServerName: "gpp", 75 | ALPN: option.Listable[string]{"h3"}, 76 | Certificate: option.Listable[string]{c}, 77 | Key: option.Listable[string]{k}, 78 | }, 79 | }, 80 | }, 81 | } 82 | default: 83 | in = option.Inbound{ 84 | Type: "vless", 85 | Tag: "vless-in", 86 | VLESSOptions: option.VLESSInboundOptions{ 87 | ListenOptions: option.ListenOptions{ 88 | Listen: option.NewListenAddress(netip.MustParseAddr(conf.Addr)), 89 | ListenPort: conf.Port, 90 | }, 91 | Users: []option.VLESSUser{ 92 | { 93 | Name: "gpp", 94 | UUID: conf.UUID, 95 | }, 96 | }, 97 | Multiplex: &option.InboundMultiplexOptions{ 98 | Enabled: true, 99 | }, 100 | }, 101 | } 102 | } 103 | var instance, err = box.New(box.Options{ 104 | Context: context.Background(), 105 | Options: option.Options{ 106 | Log: &option.LogOptions{ 107 | Disabled: false, 108 | Level: "info", 109 | Output: "run.log", 110 | Timestamp: true, 111 | DisableColor: true, 112 | }, 113 | Inbounds: []option.Inbound{in}, 114 | Outbounds: []option.Outbound{ 115 | { 116 | Type: "direct", 117 | Tag: "direct-out", 118 | }, 119 | }, 120 | }, 121 | }) 122 | if err != nil { 123 | return err 124 | } 125 | err = instance.Start() 126 | if err != nil { 127 | return err 128 | } 129 | return nil 130 | } 131 | func generateKey() (string, string) { 132 | // 生成RSA密钥对 133 | pvk, err := rsa.GenerateKey(rand.Reader, 2048) 134 | if err != nil { 135 | return "", "" 136 | } 137 | 138 | // 设置证书信息 139 | template := x509.Certificate{ 140 | SerialNumber: big.NewInt(1), 141 | Subject: pkix.Name{ 142 | Organization: []string{"GPP"}, 143 | CommonName: "gpp", 144 | }, 145 | NotBefore: time.Now(), 146 | NotAfter: time.Now().Add(365 * 24 * time.Hour), 147 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 148 | ExtKeyUsage: []x509.ExtKeyUsage{ 149 | x509.ExtKeyUsageServerAuth, 150 | }, 151 | } 152 | 153 | // 生成证书 154 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &pvk.PublicKey, pvk) 155 | if err != nil { 156 | return "", "" 157 | } 158 | buffer := bytes.NewBuffer([]byte{}) 159 | _ = pem.Encode(buffer, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 160 | buffer2 := bytes.NewBuffer([]byte{}) 161 | pvkBytes, _ := x509.MarshalPKCS8PrivateKey(pvk) 162 | _ = pem.Encode(buffer2, &pem.Block{Type: "PRIVATE KEY", Bytes: pvkBytes}) 163 | return buffer.String(), buffer2.String() 164 | } 165 | -------------------------------------------------------------------------------- /server/export/expoet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/danbai225/gpp/server/core" 7 | "github.com/google/uuid" 8 | "os" 9 | ) 10 | 11 | func init() { 12 | Server() 13 | } 14 | 15 | //export Server 16 | func Server() { 17 | bytes, err := os.ReadFile("config.json") 18 | if err != nil { 19 | fmt.Println("read config err:", err) 20 | } 21 | config := core.Peer{} 22 | _ = json.Unmarshal(bytes, &config) 23 | if config.Port == 0 { 24 | config.Port = 34555 25 | } 26 | if config.Addr == "" { 27 | config.Addr = "0.0.0.0" 28 | } 29 | if config.UUID == "" { 30 | config.UUID = uuid.New().String() 31 | } 32 | _ = core.Server(config) 33 | } 34 | func main() {} 35 | -------------------------------------------------------------------------------- /server/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 检查root权限 3 | if [ "$(id -u)" -ne 0 ]; then 4 | echo "警告: 非root用户运行,某些功能可能受限。建议使用root或sudo运行此脚本。" 5 | fi 6 | 7 | # 检查必要的工具 8 | check_command() { 9 | if ! command -v $1 &> /dev/null; then 10 | echo "错误: $1 未安装。" 11 | echo "尝试安装 $1..." 12 | 13 | # 检测包管理器并安装 14 | if command -v apt &> /dev/null; then 15 | apt update && apt install -y $2 16 | elif command -v dnf &> /dev/null; then 17 | dnf install -y $2 18 | elif command -v yum &> /dev/null; then 19 | yum install -y $2 20 | elif command -v zypper &> /dev/null; then 21 | zypper install -y $2 22 | elif command -v pacman &> /dev/null; then 23 | pacman -S --noconfirm $2 24 | else 25 | echo "错误: 无法确定系统的包管理器。请手动安装 $1 后继续。" 26 | exit 1 27 | fi 28 | 29 | # 再次检查安装是否成功 30 | if ! command -v $1 &> /dev/null; then 31 | echo "错误: 安装 $1 失败,请手动安装后继续。" 32 | exit 1 33 | fi 34 | fi 35 | } 36 | 37 | # 检查必要的命令 38 | check_command "uuidgen" "uuid-runtime" 39 | check_command "curl" "curl" 40 | check_command "tar" "tar" 41 | 42 | echo "欢迎使用 gpp 服务端安装脚本" 43 | read -p "输入安装路径 (默认是 /usr/local/gpp): " INSTALL_PATH 44 | # 设置默认安装路径 45 | INSTALL_PATH=${INSTALL_PATH:-"/usr/local/gpp"} 46 | read -p "请输入监听地址(默认0.0.0.0): " LISTEN_ADDRESS 47 | LISTEN_ADDRESS=${LISTEN_ADDRESS:-"0.0.0.0"} 48 | read -p "请输入监听端口(默认5123): " LISTEN_PORT 49 | LISTEN_PORT=${LISTEN_PORT:-"5123"} 50 | read -p "请输入你的客户端入口地址(有中转就是中转地址不填默认当前服务器ip+端口): " NET_ADDR 51 | NET_ADDR=${NET_ADDR:-""} 52 | # 如果NET_ADDR变量为空,则获取外网IP地址 53 | if [ -z "$NET_ADDR" ]; then 54 | NET_ADDR=$(curl -s ipv4.ip.sb || curl -s ifconfig.me || curl -s icanhazip.com) 55 | if [ -z "$NET_ADDR" ]; then 56 | echo "警告: 无法自动获取外网IP地址,请手动指定。" 57 | read -p "请输入你的客户端入口地址: " NET_ADDR 58 | if [ -z "$NET_ADDR" ]; then 59 | echo "错误: 未提供入口地址,安装终止。" 60 | exit 1 61 | fi 62 | else 63 | NET_ADDR="$NET_ADDR:$LISTEN_PORT" 64 | fi 65 | fi 66 | 67 | echo "请选择一个选项:" 68 | echo "1) shadowsocks" 69 | echo "2) socks" 70 | echo "3) vless" 71 | echo "4) hysteria2" 72 | read -p "输入选项 (1-4): " input 73 | PROTOCOL="vless" 74 | case $input in 75 | 1) 76 | PROTOCOL="shadowsocks" 77 | ;; 78 | 2) 79 | PROTOCOL="socks" 80 | ;; 81 | 3) 82 | PROTOCOL="vless" 83 | ;; 84 | 4) 85 | PROTOCOL="hysteria2" 86 | ;; 87 | *) 88 | echo "无效选项: $input" 89 | exit 0 90 | ;; 91 | esac 92 | echo "您选择的协议为: $PROTOCOL" 93 | echo "您输入的监听地址为: $LISTEN_ADDRESS" 94 | echo "您输入的监听端口为: $LISTEN_PORT" 95 | echo "安装路径为: $INSTALL_PATH" 96 | echo "您的入口地址: $NET_ADDR" 97 | # 检查目录是否存在,如果不存在则创建 98 | if [ ! -d "$INSTALL_PATH" ]; then 99 | mkdir -p "$INSTALL_PATH" 100 | echo "目录 $INSTALL_PATH 已创建" 101 | fi 102 | # 切换到安装目录 103 | cd "$INSTALL_PATH" || exit 104 | echo "已切换到目录: $PWD" 105 | UUID=$(uuidgen) 106 | cat << EOF > config.json 107 | { 108 | "protocol": "$PROTOCOL", 109 | "port": $LISTEN_PORT, 110 | "addr": "$LISTEN_ADDRESS", 111 | "uuid":"$UUID" 112 | } 113 | EOF 114 | 115 | echo "检测系统架构..." 116 | 117 | ARCH=$(uname -m) 118 | 119 | case $ARCH in 120 | x86_64) 121 | ARCH="amd64" 122 | ;; 123 | aarch64|arm64) 124 | ARCH="arm64" 125 | ;; 126 | armv7l|armv7) 127 | ARCH="arm" 128 | ;; 129 | i386|i686) 130 | ARCH="386" 131 | ;; 132 | *) 133 | echo "不支持的架构: $ARCH" 134 | exit 1 135 | ;; 136 | esac 137 | 138 | echo "下载服务端 。。。" 139 | 140 | # 动态地拼接下载URL 141 | latest_release_url=$(curl -s https://api.github.com/repos/danbai225/gpp/releases/latest | grep "browser_download_url.*_linux_$ARCH.tar.gz" | cut -d : -f 2,3 | tr -d \") 142 | 143 | # 检查是否成功获取URL 144 | if [ -z "$latest_release_url" ]; then 145 | echo "错误: 无法获取下载URL,请检查网络连接或手动下载。" 146 | exit 1 147 | fi 148 | 149 | filename=$(basename $latest_release_url) 150 | 151 | echo "下载文件: $filename" 152 | 153 | curl -LO $latest_release_url 154 | 155 | if [ $? -ne 0 ]; then 156 | echo "错误: 下载失败,请检查网络连接或手动下载。" 157 | exit 1 158 | fi 159 | 160 | echo "下载完成" 161 | 162 | echo "解压文件" 163 | 164 | tar -xzf $filename gpp-server 165 | if [ $? -ne 0 ]; then 166 | echo "错误: 解压失败。" 167 | exit 1 168 | fi 169 | 170 | mv gpp-server gpp 171 | echo "解压完成" 172 | 173 | rm $filename 174 | 175 | chmod +x gpp 176 | 177 | # 创建运行脚本 178 | cat << EOF > run.sh 179 | #!/bin/bash 180 | cd ${INSTALL_PATH} 181 | pid_file="${INSTALL_PATH}/gpp.pid" 182 | log_file="${INSTALL_PATH}/gpp.log" 183 | 184 | if [ "\$1" = "start" ]; then 185 | if [ -f "\$pid_file" ]; then 186 | echo "错误: 进程已经在运行中" 187 | exit 1 188 | else 189 | echo "启动 gpp" 190 | nohup ${INSTALL_PATH}/gpp > "\$log_file" 2>&1 & 191 | echo \$! > "\$pid_file" 192 | echo "gpp 已启动,进程ID为 \$!" 193 | exit 0 194 | fi 195 | elif [ "\$1" = "stop" ]; then 196 | if [ -f "\$pid_file" ]; then 197 | pid=\$(cat "\$pid_file") 198 | echo "停止 gpp,进程ID为 \$pid" 199 | kill "\$pid" 200 | rm "\$pid_file" 201 | exit 0 202 | else 203 | echo "错误: 进程未运行" 204 | exit 1 205 | fi 206 | elif [ "\$1" = "restart" ]; then 207 | \$0 stop 208 | sleep 1 209 | \$0 start 210 | elif [ "\$1" = "status" ]; then 211 | if [ -f "\$pid_file" ]; then 212 | pid=\$(cat "\$pid_file") 213 | if ps -p \$pid > /dev/null; then 214 | echo "gpp 正在运行,进程ID为 \$pid" 215 | else 216 | echo "gpp 似乎已崩溃,进程ID \$pid 不存在" 217 | rm "\$pid_file" 218 | fi 219 | else 220 | echo "gpp 未运行" 221 | fi 222 | else 223 | echo "用法: ${INSTALL_PATH}/run.sh [start|stop|restart|status]" 224 | exit 1 225 | fi 226 | EOF 227 | 228 | chmod +x run.sh 229 | 230 | # 检测是否支持systemd 231 | HAS_SYSTEMD=false 232 | if command -v systemctl &> /dev/null ; then 233 | HAS_SYSTEMD=true 234 | fi 235 | 236 | # 如果支持systemd,创建systemd服务文件 237 | if [ "$HAS_SYSTEMD" = true ]; then 238 | echo "检测到系统支持systemd,创建系统服务..." 239 | 240 | cat << EOF > /etc/systemd/system/gpp.service 241 | [Unit] 242 | Description=GPP Proxy Service 243 | After=network.target 244 | 245 | [Service] 246 | Type=simple 247 | User=$(whoami) 248 | WorkingDirectory=${INSTALL_PATH} 249 | ExecStart=${INSTALL_PATH}/gpp 250 | Restart=on-failure 251 | RestartSec=5s 252 | 253 | [Install] 254 | WantedBy=multi-user.target 255 | EOF 256 | 257 | # 重新加载systemd配置 258 | systemctl daemon-reload 259 | 260 | echo "systemd服务已创建。您可以使用以下命令管理服务:" 261 | echo "启动服务: sudo systemctl start gpp 或 sudo service gpp start" 262 | echo "停止服务: sudo systemctl stop gpp 或 sudo service gpp stop" 263 | echo "查看状态: sudo systemctl status gpp 或 sudo service gpp status" 264 | echo "启用开机自启: sudo systemctl enable gpp" 265 | 266 | # 询问是否立即启动服务并设置开机自启 267 | read -p "是否立即启动服务? (y/n): " START_SERVICE 268 | if [ "$START_SERVICE" = "y" ] || [ "$START_SERVICE" = "Y" ]; then 269 | systemctl start gpp 270 | echo "服务已启动" 271 | fi 272 | 273 | read -p "是否设置开机自启? (y/n): " ENABLE_SERVICE 274 | if [ "$ENABLE_SERVICE" = "y" ] || [ "$ENABLE_SERVICE" = "Y" ]; then 275 | systemctl enable gpp 276 | echo "服务已设置为开机自启" 277 | fi 278 | else 279 | echo "使用传统方式管理服务" 280 | echo "安装完成,请执行 ${INSTALL_PATH}/run.sh start 启动服务端,执行 ${INSTALL_PATH}/run.sh stop 停止服务端" 281 | 282 | # 询问是否立即启动服务 283 | read -p "是否立即启动服务? (y/n): " START_SERVICE 284 | if [ "$START_SERVICE" = "y" ] || [ "$START_SERVICE" = "Y" ]; then 285 | ${INSTALL_PATH}/run.sh start 286 | echo "服务已启动" 287 | fi 288 | fi 289 | 290 | read -p "请为您的节点取一个名字: " Name 291 | Name=${Name:-"$NET_ADDR"} 292 | echo "入口地址是: $NET_ADDR" 293 | result="gpp://$PROTOCOL@$NET_ADDR/$UUID" 294 | 295 | # 编码链接(兼容不同系统) 296 | if command -v base64 &> /dev/null; then 297 | encoded_result=$(echo -n $result | base64 | tr -d '\n') 298 | else 299 | # 如果没有base64命令,生成未编码链接 300 | encoded_result=$result 301 | echo "警告: 未找到base64命令,生成未编码链接" 302 | fi 303 | 304 | echo "导入链接:${encoded_result}#$Name" 305 | echo "安装完成!" -------------------------------------------------------------------------------- /systray/internal/DbusMenu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /systray/internal/StatusNotifierItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /systray/internal/generated/menu/dbus_menu.go: -------------------------------------------------------------------------------- 1 | // Code generated by dbus-codegen-go DO NOT EDIT. 2 | package menu 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/godbus/dbus/v5" 10 | "github.com/godbus/dbus/v5/introspect" 11 | ) 12 | 13 | var ( 14 | // Introspection for com.canonical.dbusmenu 15 | IntrospectDataDbusmenu = introspect.Interface{ 16 | Name: "com.canonical.dbusmenu", 17 | Methods: []introspect.Method{{Name: "GetLayout", Args: []introspect.Arg{ 18 | {Name: "parentId", Type: "i", Direction: "in"}, 19 | {Name: "recursionDepth", Type: "i", Direction: "in"}, 20 | {Name: "propertyNames", Type: "as", Direction: "in"}, 21 | {Name: "revision", Type: "u", Direction: "out"}, 22 | {Name: "layout", Type: "(ia{sv}av)", Direction: "out"}, 23 | }}, 24 | {Name: "GetGroupProperties", Args: []introspect.Arg{ 25 | {Name: "ids", Type: "ai", Direction: "in"}, 26 | {Name: "propertyNames", Type: "as", Direction: "in"}, 27 | {Name: "properties", Type: "a(ia{sv})", Direction: "out"}, 28 | }}, 29 | {Name: "GetProperty", Args: []introspect.Arg{ 30 | {Name: "id", Type: "i", Direction: "in"}, 31 | {Name: "name", Type: "s", Direction: "in"}, 32 | {Name: "value", Type: "v", Direction: "out"}, 33 | }}, 34 | {Name: "Event", Args: []introspect.Arg{ 35 | {Name: "id", Type: "i", Direction: "in"}, 36 | {Name: "eventId", Type: "s", Direction: "in"}, 37 | {Name: "data", Type: "v", Direction: "in"}, 38 | {Name: "timestamp", Type: "u", Direction: "in"}, 39 | }}, 40 | {Name: "EventGroup", Args: []introspect.Arg{ 41 | {Name: "events", Type: "a(isvu)", Direction: "in"}, 42 | {Name: "idErrors", Type: "ai", Direction: "out"}, 43 | }}, 44 | {Name: "AboutToShow", Args: []introspect.Arg{ 45 | {Name: "id", Type: "i", Direction: "in"}, 46 | {Name: "needUpdate", Type: "b", Direction: "out"}, 47 | }}, 48 | {Name: "AboutToShowGroup", Args: []introspect.Arg{ 49 | {Name: "ids", Type: "ai", Direction: "in"}, 50 | {Name: "updatesNeeded", Type: "ai", Direction: "out"}, 51 | {Name: "idErrors", Type: "ai", Direction: "out"}, 52 | }}, 53 | }, 54 | Signals: []introspect.Signal{{Name: "ItemsPropertiesUpdated", Args: []introspect.Arg{ 55 | {Name: "updatedProps", Type: "a(ia{sv})", Direction: "out"}, 56 | {Name: "removedProps", Type: "a(ias)", Direction: "out"}, 57 | }}, 58 | {Name: "LayoutUpdated", Args: []introspect.Arg{ 59 | {Name: "revision", Type: "u", Direction: "out"}, 60 | {Name: "parent", Type: "i", Direction: "out"}, 61 | }}, 62 | {Name: "ItemActivationRequested", Args: []introspect.Arg{ 63 | {Name: "id", Type: "i", Direction: "out"}, 64 | {Name: "timestamp", Type: "u", Direction: "out"}, 65 | }}, 66 | }, 67 | Properties: []introspect.Property{{Name: "Version", Type: "u", Access: "read"}, 68 | {Name: "TextDirection", Type: "s", Access: "read"}, 69 | {Name: "Status", Type: "s", Access: "read"}, 70 | {Name: "IconThemePath", Type: "as", Access: "read"}, 71 | }, 72 | Annotations: []introspect.Annotation{}, 73 | } 74 | ) 75 | 76 | // Signal is a common interface for all signals. 77 | type Signal interface { 78 | Name() string 79 | Interface() string 80 | Sender() string 81 | 82 | path() dbus.ObjectPath 83 | values() []interface{} 84 | } 85 | 86 | // Emit sends the given signal to the bus. 87 | func Emit(conn *dbus.Conn, s Signal) error { 88 | return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...) 89 | } 90 | 91 | // ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved. 92 | var ErrUnknownSignal = errors.New("unknown signal") 93 | 94 | // LookupSignal converts the given raw D-Bus signal with variable body 95 | // into one with typed structured body or returns ErrUnknownSignal error. 96 | func LookupSignal(signal *dbus.Signal) (Signal, error) { 97 | switch signal.Name { 98 | case InterfaceDbusmenu + "." + "ItemsPropertiesUpdated": 99 | v0, ok := signal.Body[0].([]struct { 100 | V0 int32 101 | V1 map[string]dbus.Variant 102 | }) 103 | if !ok { 104 | return nil, fmt.Errorf("prop .UpdatedProps is %T, not []struct {V0 int32;V1 map[string]dbus.Variant}", signal.Body[0]) 105 | } 106 | v1, ok := signal.Body[1].([]struct { 107 | V0 int32 108 | V1 []string 109 | }) 110 | if !ok { 111 | return nil, fmt.Errorf("prop .RemovedProps is %T, not []struct {V0 int32;V1 []string}", signal.Body[1]) 112 | } 113 | return &Dbusmenu_ItemsPropertiesUpdatedSignal{ 114 | sender: signal.Sender, 115 | Path: signal.Path, 116 | Body: &Dbusmenu_ItemsPropertiesUpdatedSignalBody{ 117 | UpdatedProps: v0, 118 | RemovedProps: v1, 119 | }, 120 | }, nil 121 | case InterfaceDbusmenu + "." + "LayoutUpdated": 122 | v0, ok := signal.Body[0].(uint32) 123 | if !ok { 124 | return nil, fmt.Errorf("prop .Revision is %T, not uint32", signal.Body[0]) 125 | } 126 | v1, ok := signal.Body[1].(int32) 127 | if !ok { 128 | return nil, fmt.Errorf("prop .Parent is %T, not int32", signal.Body[1]) 129 | } 130 | return &Dbusmenu_LayoutUpdatedSignal{ 131 | sender: signal.Sender, 132 | Path: signal.Path, 133 | Body: &Dbusmenu_LayoutUpdatedSignalBody{ 134 | Revision: v0, 135 | Parent: v1, 136 | }, 137 | }, nil 138 | case InterfaceDbusmenu + "." + "ItemActivationRequested": 139 | v0, ok := signal.Body[0].(int32) 140 | if !ok { 141 | return nil, fmt.Errorf("prop .Id is %T, not int32", signal.Body[0]) 142 | } 143 | v1, ok := signal.Body[1].(uint32) 144 | if !ok { 145 | return nil, fmt.Errorf("prop .Timestamp is %T, not uint32", signal.Body[1]) 146 | } 147 | return &Dbusmenu_ItemActivationRequestedSignal{ 148 | sender: signal.Sender, 149 | Path: signal.Path, 150 | Body: &Dbusmenu_ItemActivationRequestedSignalBody{ 151 | Id: v0, 152 | Timestamp: v1, 153 | }, 154 | }, nil 155 | default: 156 | return nil, ErrUnknownSignal 157 | } 158 | } 159 | 160 | // AddMatchSignal registers a match rule for the given signal, 161 | // opts are appended to the automatically generated signal's rules. 162 | func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { 163 | return conn.AddMatchSignal(append([]dbus.MatchOption{ 164 | dbus.WithMatchInterface(s.Interface()), 165 | dbus.WithMatchMember(s.Name()), 166 | }, opts...)...) 167 | } 168 | 169 | // RemoveMatchSignal unregisters the previously registered subscription. 170 | func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { 171 | return conn.RemoveMatchSignal(append([]dbus.MatchOption{ 172 | dbus.WithMatchInterface(s.Interface()), 173 | dbus.WithMatchMember(s.Name()), 174 | }, opts...)...) 175 | } 176 | 177 | // Interface name constants. 178 | const ( 179 | InterfaceDbusmenu = "com.canonical.dbusmenu" 180 | ) 181 | 182 | // Dbusmenuer is com.canonical.dbusmenu interface. 183 | type Dbusmenuer interface { 184 | // GetLayout is com.canonical.dbusmenu.GetLayout method. 185 | GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { 186 | V0 int32 187 | V1 map[string]dbus.Variant 188 | V2 []dbus.Variant 189 | }, err *dbus.Error) 190 | // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. 191 | GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { 192 | V0 int32 193 | V1 map[string]dbus.Variant 194 | }, err *dbus.Error) 195 | // GetProperty is com.canonical.dbusmenu.GetProperty method. 196 | GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) 197 | // Event is com.canonical.dbusmenu.Event method. 198 | Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) 199 | // EventGroup is com.canonical.dbusmenu.EventGroup method. 200 | EventGroup(events []struct { 201 | V0 int32 202 | V1 string 203 | V2 dbus.Variant 204 | V3 uint32 205 | }) (idErrors []int32, err *dbus.Error) 206 | // AboutToShow is com.canonical.dbusmenu.AboutToShow method. 207 | AboutToShow(id int32) (needUpdate bool, err *dbus.Error) 208 | // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. 209 | AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) 210 | } 211 | 212 | // ExportDbusmenu exports the given object that implements com.canonical.dbusmenu on the bus. 213 | func ExportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath, v Dbusmenuer) error { 214 | return conn.ExportSubtreeMethodTable(map[string]interface{}{ 215 | "GetLayout": v.GetLayout, 216 | "GetGroupProperties": v.GetGroupProperties, 217 | "GetProperty": v.GetProperty, 218 | "Event": v.Event, 219 | "EventGroup": v.EventGroup, 220 | "AboutToShow": v.AboutToShow, 221 | "AboutToShowGroup": v.AboutToShowGroup, 222 | }, path, InterfaceDbusmenu) 223 | } 224 | 225 | // UnexportDbusmenu unexports com.canonical.dbusmenu interface on the named path. 226 | func UnexportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath) error { 227 | return conn.Export(nil, path, InterfaceDbusmenu) 228 | } 229 | 230 | // UnimplementedDbusmenu can be embedded to have forward compatible server implementations. 231 | type UnimplementedDbusmenu struct{} 232 | 233 | func (*UnimplementedDbusmenu) iface() string { 234 | return InterfaceDbusmenu 235 | } 236 | 237 | func (*UnimplementedDbusmenu) GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { 238 | V0 int32 239 | V1 map[string]dbus.Variant 240 | V2 []dbus.Variant 241 | }, err *dbus.Error) { 242 | err = &dbus.ErrMsgUnknownMethod 243 | return 244 | } 245 | 246 | func (*UnimplementedDbusmenu) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { 247 | V0 int32 248 | V1 map[string]dbus.Variant 249 | }, err *dbus.Error) { 250 | err = &dbus.ErrMsgUnknownMethod 251 | return 252 | } 253 | 254 | func (*UnimplementedDbusmenu) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { 255 | err = &dbus.ErrMsgUnknownMethod 256 | return 257 | } 258 | 259 | func (*UnimplementedDbusmenu) Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { 260 | err = &dbus.ErrMsgUnknownMethod 261 | return 262 | } 263 | 264 | func (*UnimplementedDbusmenu) EventGroup(events []struct { 265 | V0 int32 266 | V1 string 267 | V2 dbus.Variant 268 | V3 uint32 269 | }) (idErrors []int32, err *dbus.Error) { 270 | err = &dbus.ErrMsgUnknownMethod 271 | return 272 | } 273 | 274 | func (*UnimplementedDbusmenu) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { 275 | err = &dbus.ErrMsgUnknownMethod 276 | return 277 | } 278 | 279 | func (*UnimplementedDbusmenu) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { 280 | err = &dbus.ErrMsgUnknownMethod 281 | return 282 | } 283 | 284 | // NewDbusmenu creates and allocates com.canonical.dbusmenu. 285 | func NewDbusmenu(object dbus.BusObject) *Dbusmenu { 286 | return &Dbusmenu{object} 287 | } 288 | 289 | // Dbusmenu implements com.canonical.dbusmenu D-Bus interface. 290 | type Dbusmenu struct { 291 | object dbus.BusObject 292 | } 293 | 294 | // GetLayout calls com.canonical.dbusmenu.GetLayout method. 295 | func (o *Dbusmenu) GetLayout(ctx context.Context, parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { 296 | V0 int32 297 | V1 map[string]dbus.Variant 298 | V2 []dbus.Variant 299 | }, err error) { 300 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetLayout", 0, parentId, recursionDepth, propertyNames).Store(&revision, &layout) 301 | return 302 | } 303 | 304 | // GetGroupProperties calls com.canonical.dbusmenu.GetGroupProperties method. 305 | func (o *Dbusmenu) GetGroupProperties(ctx context.Context, ids []int32, propertyNames []string) (properties []struct { 306 | V0 int32 307 | V1 map[string]dbus.Variant 308 | }, err error) { 309 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetGroupProperties", 0, ids, propertyNames).Store(&properties) 310 | return 311 | } 312 | 313 | // GetProperty calls com.canonical.dbusmenu.GetProperty method. 314 | func (o *Dbusmenu) GetProperty(ctx context.Context, id int32, name string) (value dbus.Variant, err error) { 315 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetProperty", 0, id, name).Store(&value) 316 | return 317 | } 318 | 319 | // Event calls com.canonical.dbusmenu.Event method. 320 | func (o *Dbusmenu) Event(ctx context.Context, id int32, eventId string, data dbus.Variant, timestamp uint32) (err error) { 321 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".Event", 0, id, eventId, data, timestamp).Store() 322 | return 323 | } 324 | 325 | // EventGroup calls com.canonical.dbusmenu.EventGroup method. 326 | func (o *Dbusmenu) EventGroup(ctx context.Context, events []struct { 327 | V0 int32 328 | V1 string 329 | V2 dbus.Variant 330 | V3 uint32 331 | }) (idErrors []int32, err error) { 332 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".EventGroup", 0, events).Store(&idErrors) 333 | return 334 | } 335 | 336 | // AboutToShow calls com.canonical.dbusmenu.AboutToShow method. 337 | func (o *Dbusmenu) AboutToShow(ctx context.Context, id int32) (needUpdate bool, err error) { 338 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShow", 0, id).Store(&needUpdate) 339 | return 340 | } 341 | 342 | // AboutToShowGroup calls com.canonical.dbusmenu.AboutToShowGroup method. 343 | func (o *Dbusmenu) AboutToShowGroup(ctx context.Context, ids []int32) (updatesNeeded []int32, idErrors []int32, err error) { 344 | err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShowGroup", 0, ids).Store(&updatesNeeded, &idErrors) 345 | return 346 | } 347 | 348 | // GetVersion gets com.canonical.dbusmenu.Version property. 349 | func (o *Dbusmenu) GetVersion(ctx context.Context) (version uint32, err error) { 350 | err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Version").Store(&version) 351 | return 352 | } 353 | 354 | // GetTextDirection gets com.canonical.dbusmenu.TextDirection property. 355 | func (o *Dbusmenu) GetTextDirection(ctx context.Context) (textDirection string, err error) { 356 | err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "TextDirection").Store(&textDirection) 357 | return 358 | } 359 | 360 | // GetStatus gets com.canonical.dbusmenu.Status property. 361 | func (o *Dbusmenu) GetStatus(ctx context.Context) (status string, err error) { 362 | err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Status").Store(&status) 363 | return 364 | } 365 | 366 | // GetIconThemePath gets com.canonical.dbusmenu.IconThemePath property. 367 | func (o *Dbusmenu) GetIconThemePath(ctx context.Context) (iconThemePath []string, err error) { 368 | err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "IconThemePath").Store(&iconThemePath) 369 | return 370 | } 371 | 372 | // Dbusmenu_ItemsPropertiesUpdatedSignal represents com.canonical.dbusmenu.ItemsPropertiesUpdated signal. 373 | type Dbusmenu_ItemsPropertiesUpdatedSignal struct { 374 | sender string 375 | Path dbus.ObjectPath 376 | Body *Dbusmenu_ItemsPropertiesUpdatedSignalBody 377 | } 378 | 379 | // Name returns the signal's name. 380 | func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Name() string { 381 | return "ItemsPropertiesUpdated" 382 | } 383 | 384 | // Interface returns the signal's interface. 385 | func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Interface() string { 386 | return InterfaceDbusmenu 387 | } 388 | 389 | // Sender returns the signal's sender unique name. 390 | func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Sender() string { 391 | return s.sender 392 | } 393 | 394 | func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) path() dbus.ObjectPath { 395 | return s.Path 396 | } 397 | 398 | func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) values() []interface{} { 399 | return []interface{}{s.Body.UpdatedProps, s.Body.RemovedProps} 400 | } 401 | 402 | // Dbusmenu_ItemsPropertiesUpdatedSignalBody is body container. 403 | type Dbusmenu_ItemsPropertiesUpdatedSignalBody struct { 404 | UpdatedProps []struct { 405 | V0 int32 406 | V1 map[string]dbus.Variant 407 | } 408 | RemovedProps []struct { 409 | V0 int32 410 | V1 []string 411 | } 412 | } 413 | 414 | // Dbusmenu_LayoutUpdatedSignal represents com.canonical.dbusmenu.LayoutUpdated signal. 415 | type Dbusmenu_LayoutUpdatedSignal struct { 416 | sender string 417 | Path dbus.ObjectPath 418 | Body *Dbusmenu_LayoutUpdatedSignalBody 419 | } 420 | 421 | // Name returns the signal's name. 422 | func (s *Dbusmenu_LayoutUpdatedSignal) Name() string { 423 | return "LayoutUpdated" 424 | } 425 | 426 | // Interface returns the signal's interface. 427 | func (s *Dbusmenu_LayoutUpdatedSignal) Interface() string { 428 | return InterfaceDbusmenu 429 | } 430 | 431 | // Sender returns the signal's sender unique name. 432 | func (s *Dbusmenu_LayoutUpdatedSignal) Sender() string { 433 | return s.sender 434 | } 435 | 436 | func (s *Dbusmenu_LayoutUpdatedSignal) path() dbus.ObjectPath { 437 | return s.Path 438 | } 439 | 440 | func (s *Dbusmenu_LayoutUpdatedSignal) values() []interface{} { 441 | return []interface{}{s.Body.Revision, s.Body.Parent} 442 | } 443 | 444 | // Dbusmenu_LayoutUpdatedSignalBody is body container. 445 | type Dbusmenu_LayoutUpdatedSignalBody struct { 446 | Revision uint32 447 | Parent int32 448 | } 449 | 450 | // Dbusmenu_ItemActivationRequestedSignal represents com.canonical.dbusmenu.ItemActivationRequested signal. 451 | type Dbusmenu_ItemActivationRequestedSignal struct { 452 | sender string 453 | Path dbus.ObjectPath 454 | Body *Dbusmenu_ItemActivationRequestedSignalBody 455 | } 456 | 457 | // Name returns the signal's name. 458 | func (s *Dbusmenu_ItemActivationRequestedSignal) Name() string { 459 | return "ItemActivationRequested" 460 | } 461 | 462 | // Interface returns the signal's interface. 463 | func (s *Dbusmenu_ItemActivationRequestedSignal) Interface() string { 464 | return InterfaceDbusmenu 465 | } 466 | 467 | // Sender returns the signal's sender unique name. 468 | func (s *Dbusmenu_ItemActivationRequestedSignal) Sender() string { 469 | return s.sender 470 | } 471 | 472 | func (s *Dbusmenu_ItemActivationRequestedSignal) path() dbus.ObjectPath { 473 | return s.Path 474 | } 475 | 476 | func (s *Dbusmenu_ItemActivationRequestedSignal) values() []interface{} { 477 | return []interface{}{s.Body.Id, s.Body.Timestamp} 478 | } 479 | 480 | // Dbusmenu_ItemActivationRequestedSignalBody is body container. 481 | type Dbusmenu_ItemActivationRequestedSignalBody struct { 482 | Id int32 483 | Timestamp uint32 484 | } 485 | -------------------------------------------------------------------------------- /systray/systray.go: -------------------------------------------------------------------------------- 1 | // Package systray is a cross-platform Go library to place an icon and menu in the notification area. 2 | package systray 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "runtime" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | var ( 13 | systrayReady func() 14 | systrayExit func() 15 | menuItems = make(map[uint32]*MenuItem) 16 | menuItemsLock sync.RWMutex 17 | 18 | currentID = uint32(0) 19 | quitOnce sync.Once 20 | dClickTimeMinInterval int64 = 500 21 | ) 22 | 23 | func init() { 24 | runtime.LockOSThread() 25 | } 26 | 27 | type IMenu interface { 28 | ShowMenu() error 29 | } 30 | 31 | // MenuItem is used to keep track each menu item of systray. 32 | // Don't create it directly, use the one systray.AddMenuItem() returned 33 | type MenuItem struct { 34 | // ClickedCh is the channel which will be notified when the menu item is clicked 35 | click func() 36 | 37 | // id uniquely identify a menu item, not supposed to be modified 38 | id uint32 39 | // title is the text shown on menu item 40 | title string 41 | // tooltip is the text shown when pointing to menu item 42 | tooltip string 43 | // shortcutKey Menu shortcut key 44 | shortcutKey string 45 | // disabled menu item is grayed out and has no effect when clicked 46 | disabled bool 47 | // checked menu item has a tick before the title 48 | checked bool 49 | // has the menu item a checkbox (Linux) 50 | isCheckable bool 51 | // parent item, for sub menus 52 | parent *MenuItem 53 | } 54 | 55 | func (item *MenuItem) Click(fn func()) { 56 | item.click = fn 57 | } 58 | 59 | func (item *MenuItem) String() string { 60 | if item.parent == nil { 61 | return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title) 62 | } 63 | return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title) 64 | } 65 | 66 | // newMenuItem returns a populated MenuItem object 67 | func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { 68 | return &MenuItem{ 69 | id: atomic.AddUint32(¤tID, 1), 70 | title: title, 71 | tooltip: tooltip, 72 | shortcutKey: "", 73 | disabled: false, 74 | checked: false, 75 | isCheckable: false, 76 | parent: parent, 77 | } 78 | } 79 | 80 | // Run initializes GUI and starts the event loop, then invokes the onReady 81 | // callback. It blocks until systray.Quit() is called. 82 | func Run(onReady, onExit func()) { 83 | setInternalLoop(true) 84 | Register(onReady, onExit) 85 | 86 | nativeLoop() 87 | } 88 | 89 | // 设置鼠标左键双击事件的时间间隔 默认500毫秒 90 | func SetDClickTimeMinInterval(value int64) { 91 | dClickTimeMinInterval = value 92 | } 93 | 94 | // 设置托盘鼠标左键点击事件 95 | func SetOnClick(fn func(menu IMenu)) { 96 | setOnClick(fn) 97 | } 98 | 99 | // 设置托盘鼠标左键双击事件 100 | func SetOnDClick(fn func(menu IMenu)) { 101 | setOnDClick(fn) 102 | } 103 | 104 | // 设置托盘鼠标右键事件反馈回调 105 | // 支持windows 和 macosx,不支持linux 106 | // 设置事件,菜单默认将不展示,通过menu.ShowMenu()函数显示 107 | // 未设置事件,默认右键显示托盘菜单 108 | // macosx ShowMenu()只支持OnRClick函数内调用 109 | func SetOnRClick(fn func(menu IMenu)) { 110 | setOnRClick(fn) 111 | } 112 | 113 | // RunWithExternalLoop allows the systemtray module to operate with other tookits. 114 | // The returned start and end functions should be called by the toolkit when the application has started and will end. 115 | func RunWithExternalLoop(onReady, onExit func()) (start, end func()) { 116 | Register(onReady, onExit) 117 | 118 | return nativeStart, func() { 119 | nativeEnd() 120 | Quit() 121 | } 122 | } 123 | 124 | // Register initializes GUI and registers the callbacks but relies on the 125 | // caller to run the event loop somewhere else. It's useful if the program 126 | // needs to show other UI elements, for example, webview. 127 | // To overcome some OS weirdness, On macOS versions before Catalina, calling 128 | // this does exactly the same as Run(). 129 | func Register(onReady func(), onExit func()) { 130 | if onReady == nil { 131 | systrayReady = nil 132 | } else { 133 | var readyCh = make(chan interface{}) 134 | // Run onReady on separate goroutine to avoid blocking event loop 135 | go func() { 136 | <-readyCh 137 | onReady() 138 | }() 139 | systrayReady = func() { 140 | systrayReady = nil 141 | close(readyCh) 142 | } 143 | } 144 | // unlike onReady, onExit runs in the event loop to make sure it has time to 145 | // finish before the process terminates 146 | if onExit == nil { 147 | onExit = func() {} 148 | } 149 | systrayExit = onExit 150 | registerSystray() 151 | } 152 | 153 | // ResetMenu will remove all menu items 154 | func ResetMenu() { 155 | resetMenu() 156 | } 157 | 158 | // Quit the systray 159 | func Quit() { 160 | quitOnce.Do(quit) 161 | } 162 | 163 | // AddMenuItem adds a menu item with the designated title and tooltip. 164 | // It can be safely invoked from different goroutines. 165 | // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox 166 | func AddMenuItem(title string, tooltip string) *MenuItem { 167 | item := newMenuItem(title, tooltip, nil) 168 | item.update() 169 | return item 170 | } 171 | 172 | // AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. 173 | // It can be safely invoked from different goroutines. 174 | // On Windows and OSX this is the same as calling AddMenuItem 175 | func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { 176 | item := newMenuItem(title, tooltip, nil) 177 | item.isCheckable = true 178 | item.checked = checked 179 | item.update() 180 | return item 181 | } 182 | 183 | // AddSeparator adds a separator bar to the menu 184 | func AddSeparator() { 185 | addSeparator(atomic.AddUint32(¤tID, 1)) 186 | } 187 | 188 | // AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. 189 | // It can be safely invoked from different goroutines. 190 | // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox 191 | func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { 192 | child := newMenuItem(title, tooltip, item) 193 | child.update() 194 | return child 195 | } 196 | 197 | // AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux. 198 | // It can be safely invoked from different goroutines. 199 | // On Windows and OSX this is the same as calling AddSubMenuItem 200 | func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { 201 | child := newMenuItem(title, tooltip, item) 202 | child.isCheckable = true 203 | child.checked = checked 204 | child.update() 205 | return child 206 | } 207 | 208 | // SetTitle set the text to display on a menu item 209 | func (item *MenuItem) SetTitle(title string) { 210 | item.title = title 211 | item.update() 212 | } 213 | 214 | // SetTooltip set the tooltip to show when mouse hover 215 | func (item *MenuItem) SetTooltip(tooltip string) { 216 | item.tooltip = tooltip 217 | item.update() 218 | } 219 | 220 | // Disabled checks if the menu item is disabled 221 | func (item *MenuItem) Disabled() bool { 222 | return item.disabled 223 | } 224 | 225 | // Enable a menu item regardless if it's previously enabled or not 226 | func (item *MenuItem) Enable() { 227 | item.disabled = false 228 | item.update() 229 | } 230 | 231 | // Disable a menu item regardless if it's previously disabled or not 232 | func (item *MenuItem) Disable() { 233 | item.disabled = true 234 | item.update() 235 | } 236 | 237 | // Hide hides a menu item 238 | func (item *MenuItem) Hide() { 239 | hideMenuItem(item) 240 | } 241 | 242 | // Show shows a previously hidden menu item 243 | func (item *MenuItem) Show() { 244 | showMenuItem(item) 245 | } 246 | 247 | // Checked returns if the menu item has a check mark 248 | func (item *MenuItem) Checked() bool { 249 | return item.checked 250 | } 251 | 252 | // Check a menu item regardless if it's previously checked or not 253 | func (item *MenuItem) Check() { 254 | item.checked = true 255 | item.update() 256 | } 257 | 258 | // Uncheck a menu item regardless if it's previously unchecked or not 259 | func (item *MenuItem) Uncheck() { 260 | item.checked = false 261 | item.update() 262 | } 263 | 264 | // update propagates changes on a menu item to systray 265 | func (item *MenuItem) update() { 266 | menuItemsLock.Lock() 267 | menuItems[item.id] = item 268 | menuItemsLock.Unlock() 269 | addOrUpdateMenuItem(item) 270 | } 271 | 272 | func systrayMenuItemSelected(id uint32) { 273 | menuItemsLock.RLock() 274 | item, ok := menuItems[id] 275 | menuItemsLock.RUnlock() 276 | if !ok { 277 | log.Printf("systray error: no menu item with ID %d\n", id) 278 | return 279 | } 280 | if item.click != nil { 281 | item.click() 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /systray/systray.h: -------------------------------------------------------------------------------- 1 | #include "stdbool.h" 2 | 3 | extern void systray_ready(); 4 | extern void systray_on_exit(); 5 | extern void systray_menu_item_selected(int menu_id); 6 | extern void systray_on_click(); 7 | extern void systray_on_rclick(); 8 | 9 | void registerSystray(void); 10 | void nativeEnd(void); 11 | int nativeLoop(void); 12 | void nativeStart(void); 13 | 14 | void setIcon(const char* iconBytes, int length, bool template); 15 | void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template); 16 | void setTitle(char* title); 17 | void setTooltip(char* tooltip); 18 | void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, char* shortcutKey, short disabled, short checked, short isCheckable); 19 | void add_separator(int menuId); 20 | void hide_menu_item(int menuId); 21 | void show_menu_item(int menuId); 22 | void reset_menu(); 23 | void create_menu(); 24 | void show_menu(); 25 | void set_menu_nil(); 26 | void quit(); 27 | void enable_on_click(); 28 | -------------------------------------------------------------------------------- /systray/systray_darwin.go: -------------------------------------------------------------------------------- 1 | package systray 2 | 3 | /* 4 | #cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc 5 | #cgo darwin LDFLAGS: -framework Cocoa 6 | 7 | #include 8 | #include "systray.h" 9 | 10 | void setInternalLoop(bool); 11 | */ 12 | import "C" 13 | 14 | import ( 15 | "time" 16 | "unsafe" 17 | ) 18 | 19 | var st = &systray{} 20 | 21 | type systray struct { 22 | } 23 | 24 | func (m *systray) ShowMenu() error { 25 | C.show_menu() 26 | return nil 27 | } 28 | 29 | // SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back 30 | // to a regular icon on other platforms. 31 | // templateIconBytes and regularIconBytes should be the content of .ico for windows and 32 | // .ico/.jpg/.png for other platforms. 33 | func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { 34 | cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) 35 | C.setIcon(cstr, (C.int)(len(templateIconBytes)), true) 36 | } 37 | 38 | // SetIcon sets the icon of a menu item. Only works on macOS and Windows. 39 | // iconBytes should be the content of .ico/.jpg/.png 40 | func (item *MenuItem) SetIcon(iconBytes []byte) { 41 | cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) 42 | C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id), false) 43 | } 44 | 45 | // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it 46 | // falls back to the regular icon bytes and on Linux it does nothing. 47 | // templateIconBytes and regularIconBytes should be the content of .ico for windows and 48 | // .ico/.jpg/.png for other platforms. 49 | func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { 50 | cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) 51 | C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true) 52 | } 53 | 54 | func registerSystray() { 55 | C.registerSystray() 56 | } 57 | 58 | func nativeLoop() { 59 | C.nativeLoop() 60 | } 61 | 62 | func nativeEnd() { 63 | C.nativeEnd() 64 | } 65 | 66 | func nativeStart() { 67 | C.nativeStart() 68 | } 69 | 70 | func quit() { 71 | C.quit() 72 | } 73 | 74 | func setInternalLoop(internal bool) { 75 | C.setInternalLoop(C.bool(internal)) 76 | } 77 | 78 | var ( 79 | onClick func(menu IMenu) 80 | onDClick func(menu IMenu) 81 | onRClick func(menu IMenu) 82 | dClickTime int64 83 | isEnableOnClick = false 84 | ) 85 | 86 | func setOnClick(fn func(menu IMenu)) { 87 | enableOnClick() 88 | onClick = fn 89 | } 90 | 91 | func setOnDClick(fn func(menu IMenu)) { 92 | enableOnClick() 93 | onDClick = fn 94 | } 95 | 96 | func setOnRClick(fn func(menu IMenu)) { 97 | enableOnClick() 98 | onRClick = fn 99 | } 100 | 101 | // CreateMenu 创建托盘菜单, 如果托盘菜单是空, 把菜单项添加到托盘 102 | // 该方法主动调用后 如果托盘菜单已创建则添加进去, 之后鼠标事件失效 103 | // 104 | // 仅MacOSX平台 105 | func CreateMenu() { 106 | createMenu() 107 | } 108 | 109 | // SetMenuNil 托盘菜单设置为nil, 如果托盘菜单不是空, 把菜单项设置为nil 110 | // 该方法主动调用后 将移除托盘菜单, 之后鼠标事件生效 111 | // 112 | // 仅MacOSX平台 113 | func SetMenuNil() { 114 | setMenuNil() 115 | } 116 | 117 | // SetIcon sets the systray icon. 118 | // iconBytes should be the content of .ico for windows and .ico/.jpg/.png 119 | // for other platforms. 120 | func SetIcon(iconBytes []byte) { 121 | cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) 122 | C.setIcon(cstr, (C.int)(len(iconBytes)), false) 123 | } 124 | 125 | // SetTitle sets the systray title, only available on Mac and Linux. 126 | func SetTitle(title string) { 127 | C.setTitle(C.CString(title)) 128 | } 129 | 130 | // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, 131 | // only available on Mac and Windows. 132 | func SetTooltip(tooltip string) { 133 | C.setTooltip(C.CString(tooltip)) 134 | } 135 | 136 | func addOrUpdateMenuItem(item *MenuItem) { 137 | var disabled C.short 138 | if item.disabled { 139 | disabled = 1 140 | } 141 | var checked C.short 142 | if item.checked { 143 | checked = 1 144 | } 145 | var isCheckable C.short 146 | if item.isCheckable { 147 | isCheckable = 1 148 | } 149 | var parentID uint32 = 0 150 | if item.parent != nil { 151 | parentID = item.parent.id 152 | } 153 | C.add_or_update_menu_item( 154 | C.int(item.id), 155 | C.int(parentID), 156 | C.CString(item.title), 157 | C.CString(item.tooltip), 158 | C.CString(item.shortcutKey), 159 | disabled, 160 | checked, 161 | isCheckable, 162 | ) 163 | } 164 | 165 | func addSeparator(id uint32) { 166 | C.add_separator(C.int(id)) 167 | } 168 | 169 | func hideMenuItem(item *MenuItem) { 170 | C.hide_menu_item( 171 | C.int(item.id), 172 | ) 173 | } 174 | 175 | func showMenuItem(item *MenuItem) { 176 | C.show_menu_item( 177 | C.int(item.id), 178 | ) 179 | } 180 | 181 | func resetMenu() { 182 | C.reset_menu() 183 | } 184 | 185 | func createMenu() { 186 | C.create_menu() 187 | } 188 | 189 | func setMenuNil() { 190 | C.set_menu_nil() 191 | } 192 | func enableOnClick() { 193 | if !isEnableOnClick { 194 | isEnableOnClick = true 195 | C.enable_on_click() 196 | } 197 | } 198 | 199 | //export systray_ready 200 | func systray_ready() { 201 | if systrayReady != nil { 202 | systrayReady() 203 | } 204 | } 205 | 206 | //export systray_on_exit 207 | func systray_on_exit() { 208 | systrayExit() 209 | } 210 | 211 | //export systray_menu_item_selected 212 | func systray_menu_item_selected(cID C.int) { 213 | systrayMenuItemSelected(uint32(cID)) 214 | } 215 | 216 | //export systray_on_click 217 | func systray_on_click() { 218 | if dClickTime == 0 { 219 | dClickTime = time.Now().UnixMilli() 220 | } else { 221 | nowMilli := time.Now().UnixMilli() 222 | if nowMilli-dClickTime < dClickTimeMinInterval { 223 | dClickTime = dClickTimeMinInterval 224 | if onDClick != nil { 225 | onDClick(st) 226 | return 227 | } 228 | } else { 229 | dClickTime = nowMilli 230 | } 231 | } 232 | if onClick != nil { 233 | onClick(st) 234 | } 235 | } 236 | 237 | //export systray_on_rclick 238 | func systray_on_rclick() { 239 | if onRClick != nil { 240 | onRClick(st) 241 | } else { 242 | C.show_menu() 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /systray/systray_darwin.m: -------------------------------------------------------------------------------- 1 | #import 2 | #include "systray.h" 3 | 4 | #if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400 5 | 6 | #ifndef NSControlStateValueOff 7 | #define NSControlStateValueOff NSOffState 8 | #endif 9 | 10 | #ifndef NSControlStateValueOn 11 | #define NSControlStateValueOn NSOnState 12 | #endif 13 | 14 | #endif 15 | 16 | @interface MenuItem : NSObject { 17 | @public 18 | NSNumber* menuId; 19 | NSNumber* parentMenuId; 20 | NSString* title; 21 | NSString* tooltip; 22 | NSString* shortcutKey; 23 | short disabled; 24 | short checked; 25 | } 26 | 27 | -(id) initWithId: (int)theMenuId 28 | withParentMenuId: (int)theParentMenuId 29 | withTitle: (const char*)theTitle 30 | withTooltip: (const char*)theTooltip 31 | withShortcutKey: (const char*)theShortcutKey 32 | withDisabled: (short)theDisabled 33 | withChecked: (short)theChecked; 34 | @end 35 | @implementation MenuItem 36 | -(id) initWithId: (int)theMenuId 37 | withParentMenuId: (int)theParentMenuId 38 | withTitle: (const char*)theTitle 39 | withTooltip: (const char*)theTooltip 40 | withShortcutKey: (const char*)theShortcutKey 41 | withDisabled: (short)theDisabled 42 | withChecked: (short)theChecked 43 | { 44 | menuId = [NSNumber numberWithInt:theMenuId]; 45 | parentMenuId = [NSNumber numberWithInt:theParentMenuId]; 46 | title = [[NSString alloc] initWithCString:theTitle 47 | encoding:NSUTF8StringEncoding]; 48 | tooltip = [[NSString alloc] initWithCString:theTooltip 49 | encoding:NSUTF8StringEncoding]; 50 | disabled = theDisabled; 51 | checked = theChecked; 52 | return self; 53 | } 54 | @end 55 | 56 | @interface AppDelegate_: NSObject 57 | - (void) add_or_update_menu_item:(MenuItem*) item; 58 | - (IBAction)menuHandler:(id)sender; 59 | - (void)statusOnClick:(NSButton *)btn; 60 | @property (assign) IBOutlet NSWindow *window; 61 | @end 62 | 63 | @implementation AppDelegate_ { 64 | NSStatusItem *statusItem; 65 | NSMenu *menu; 66 | NSCondition* cond; 67 | } 68 | 69 | @synthesize window = _window; 70 | 71 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 72 | self->statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; 73 | self->menu = [[NSMenu alloc] init]; 74 | [self->menu setAutoenablesItems: FALSE]; 75 | //[self->statusItem.button setTarget:self]; 76 | //[self->menu setDelegate:(AppDelegate *)self]; 77 | //[self->statusItem.button setAction:@selector(statusOnClick:)]; 78 | //[self->statusItem setMenu:self->menu]; //注释掉,不然不设置菜单事件也不启作用 79 | systray_ready(); 80 | } 81 | 82 | - (void)applicationWillTerminate:(NSNotification *)aNotification { 83 | systray_on_exit(); 84 | } 85 | 86 | - (void)setIcon:(NSImage *)image { 87 | statusItem.button.image = image; 88 | [self updateTitleButtonStyle]; 89 | } 90 | 91 | - (void)setTitle:(NSString *)title { 92 | statusItem.button.title = title; 93 | [self updateTitleButtonStyle]; 94 | } 95 | 96 | -(void)updateTitleButtonStyle { 97 | if (statusItem.button.image != nil) { 98 | if ([statusItem.button.title length] == 0) { 99 | statusItem.button.imagePosition = NSImageOnly; 100 | } else { 101 | statusItem.button.imagePosition = NSImageLeft; 102 | } 103 | } else { 104 | statusItem.button.imagePosition = NSNoImage; 105 | } 106 | } 107 | 108 | 109 | - (void)setTooltip:(NSString *)tooltip { 110 | statusItem.button.toolTip = tooltip; 111 | } 112 | 113 | - (IBAction)menuHandler:(id)sender { 114 | NSNumber* menuId = [sender representedObject]; 115 | systray_menu_item_selected(menuId.intValue); 116 | } 117 | 118 | - (void)add_or_update_menu_item:(MenuItem *)item { 119 | NSMenu *theMenu = self->menu; 120 | NSMenuItem *parentItem; 121 | //create_menu(); 122 | if ([item->parentMenuId integerValue] > 0) { 123 | parentItem = find_menu_item(menu, item->parentMenuId); 124 | if (parentItem.hasSubmenu) { 125 | theMenu = parentItem.submenu; 126 | } else { 127 | theMenu = [[NSMenu alloc] init]; 128 | [theMenu setAutoenablesItems:NO]; 129 | [parentItem setSubmenu:theMenu]; 130 | } 131 | } 132 | 133 | NSMenuItem *menuItem; 134 | menuItem = find_menu_item(theMenu, item->menuId); 135 | //item->shortcutKey 136 | if (menuItem == NULL) { 137 | menuItem = [theMenu addItemWithTitle:item->title action:@selector(menuHandler:) keyEquivalent:@""]; 138 | [menuItem setRepresentedObject:item->menuId]; 139 | } 140 | [menuItem setTitle:item->title]; 141 | [menuItem setTag:[item->menuId integerValue]]; 142 | [menuItem setTarget:self]; 143 | [menuItem setToolTip:item->tooltip]; 144 | if (item->disabled == 1) { 145 | menuItem.enabled = FALSE; 146 | } else { 147 | menuItem.enabled = TRUE; 148 | } 149 | if (item->checked == 1) { 150 | menuItem.state = NSControlStateValueOn; 151 | } else { 152 | menuItem.state = NSControlStateValueOff; 153 | } 154 | } 155 | 156 | NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { 157 | NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]]; 158 | if (foundItem != NULL) { 159 | return foundItem; 160 | } 161 | NSArray *menu_items = ourMenu.itemArray; 162 | int i; 163 | for (i = 0; i < [menu_items count]; i++) { 164 | NSMenuItem *i_item = [menu_items objectAtIndex:i]; 165 | if (i_item.hasSubmenu) { 166 | foundItem = find_menu_item(i_item.submenu, menuId); 167 | if (foundItem != NULL) { 168 | return foundItem; 169 | } 170 | } 171 | } 172 | 173 | return NULL; 174 | }; 175 | 176 | - (void) add_separator:(NSNumber*) menuId { 177 | [menu addItem: [NSMenuItem separatorItem]]; 178 | } 179 | 180 | - (void) hide_menu_item:(NSNumber*) menuId { 181 | NSMenuItem* menuItem = find_menu_item(menu, menuId); 182 | if (menuItem != NULL) { 183 | [menuItem setHidden:TRUE]; 184 | } 185 | } 186 | 187 | - (void) setMenuItemIcon:(NSArray*)imageAndMenuId { 188 | NSImage* image = [imageAndMenuId objectAtIndex:0]; 189 | NSNumber* menuId = [imageAndMenuId objectAtIndex:1]; 190 | 191 | NSMenuItem* menuItem; 192 | menuItem = find_menu_item(menu, menuId); 193 | if (menuItem == NULL) { 194 | return; 195 | } 196 | menuItem.image = image; 197 | } 198 | 199 | - (void) show_menu_item:(NSNumber*) menuId { 200 | NSMenuItem* menuItem = find_menu_item(menu, menuId); 201 | if (menuItem != NULL) { 202 | [menuItem setHidden:FALSE]; 203 | } 204 | } 205 | 206 | - (void) create_menu { 207 | if(statusItem.menu == NULL){ 208 | [statusItem setMenu:menu]; 209 | } 210 | } 211 | 212 | - (void) set_menu_nil { 213 | if(statusItem.menu != NULL){ 214 | [statusItem setMenu:NULL]; 215 | } 216 | } 217 | 218 | - (void) reset_menu { 219 | [self->menu removeAllItems]; 220 | } 221 | 222 | - (void) quit { 223 | [NSApp terminate:self]; 224 | } 225 | 226 | - (void) statusOnClick:(NSButton *)btn { 227 | NSEvent *event = [NSApp currentEvent]; 228 | if(event.type == NSEventTypeLeftMouseUp){ 229 | systray_on_click(); 230 | }else if(event.type == NSEventTypeRightMouseUp){ 231 | systray_on_rclick(); 232 | } 233 | } 234 | 235 | - (void) show_menu { 236 | create_menu(); 237 | [statusItem.button performClick:nil]; 238 | set_menu_nil(); 239 | } 240 | 241 | - (void) enable_on_click { 242 | [statusItem.button setAction:@selector(statusOnClick:)]; 243 | [statusItem.button sendActionOn:(NSEventMaskLeftMouseUp|NSEventMaskRightMouseUp)]; 244 | } 245 | 246 | @end 247 | 248 | bool internalLoop = false; 249 | AppDelegate_ *owner; 250 | 251 | void setInternalLoop(bool i) { 252 | internalLoop = i; 253 | } 254 | 255 | void registerSystray(void) { 256 | if (!internalLoop) { // with an external loop we don't take ownership of the app 257 | return; 258 | } 259 | owner = [[AppDelegate_ alloc] init]; 260 | [[NSApplication sharedApplication] setDelegate:owner]; 261 | 262 | // A workaround to avoid crashing on macOS versions before Catalina. Somehow 263 | // SIGSEGV would happen inside AppKit if [NSApp run] is called from a 264 | // different function, even if that function is called right after this. 265 | if (floor(NSAppKitVersionNumber) <= /*NSAppKitVersionNumber10_14*/ 1671){ 266 | [NSApp run]; 267 | } 268 | } 269 | 270 | void nativeEnd(void) { 271 | systray_on_exit(); 272 | } 273 | 274 | int nativeLoop(void) { 275 | if (floor(NSAppKitVersionNumber) > /*NSAppKitVersionNumber10_14*/ 1671){ 276 | [NSApp run]; 277 | } 278 | return EXIT_SUCCESS; 279 | } 280 | 281 | void nativeStart(void) { 282 | owner = [[AppDelegate_ alloc] init]; 283 | NSNotification *launched = [NSNotification 284 | notificationWithName: NSApplicationDidFinishLaunchingNotification 285 | object: [NSApplication sharedApplication]]; 286 | [[NSApplication sharedApplication] setDelegate:owner]; 287 | [owner applicationDidFinishLaunching:launched]; 288 | } 289 | 290 | void runInMainThread(SEL method, id object) { 291 | [owner 292 | performSelectorOnMainThread:method 293 | withObject:object 294 | waitUntilDone: YES]; 295 | } 296 | 297 | void setIcon(const char* iconBytes, int length, bool template) { 298 | NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; 299 | NSImage *image = [[NSImage alloc] initWithData:buffer]; 300 | [image setSize:NSMakeSize(16, 16)]; 301 | image.template = template; 302 | runInMainThread(@selector(setIcon:), (id)image); 303 | } 304 | 305 | void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) { 306 | NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; 307 | NSImage *image = [[NSImage alloc] initWithData:buffer]; 308 | [image setSize:NSMakeSize(16, 16)]; 309 | image.template = template; 310 | NSNumber *mId = [NSNumber numberWithInt:menuId]; 311 | runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]); 312 | } 313 | 314 | void setTitle(char* ctitle) { 315 | NSString* title = [[NSString alloc] initWithCString:ctitle 316 | encoding:NSUTF8StringEncoding]; 317 | free(ctitle); 318 | runInMainThread(@selector(setTitle:), (id)title); 319 | } 320 | 321 | void setTooltip(char* ctooltip) { 322 | NSString* tooltip = [[NSString alloc] initWithCString:ctooltip 323 | encoding:NSUTF8StringEncoding]; 324 | free(ctooltip); 325 | runInMainThread(@selector(setTooltip:), (id)tooltip); 326 | } 327 | 328 | void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, char* shortcutKey, short disabled, short checked, short isCheckable) { 329 | MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withShortcutKey: shortcutKey withDisabled: disabled withChecked: checked]; 330 | free(title); 331 | free(tooltip); 332 | runInMainThread(@selector(add_or_update_menu_item:), (id)item); 333 | } 334 | 335 | void add_separator(int menuId) { 336 | NSNumber *mId = [NSNumber numberWithInt:menuId]; 337 | runInMainThread(@selector(add_separator:), (id)mId); 338 | } 339 | 340 | void hide_menu_item(int menuId) { 341 | NSNumber *mId = [NSNumber numberWithInt:menuId]; 342 | runInMainThread(@selector(hide_menu_item:), (id)mId); 343 | } 344 | 345 | void show_menu_item(int menuId) { 346 | NSNumber *mId = [NSNumber numberWithInt:menuId]; 347 | runInMainThread(@selector(show_menu_item:), (id)mId); 348 | } 349 | 350 | void reset_menu() { 351 | runInMainThread(@selector(reset_menu), nil); 352 | } 353 | 354 | void create_menu() { 355 | runInMainThread(@selector(create_menu), nil); 356 | } 357 | 358 | void set_menu_nil() { 359 | runInMainThread(@selector(set_menu_nil), nil); 360 | } 361 | 362 | void show_menu(){ 363 | runInMainThread(@selector(show_menu), nil); 364 | } 365 | 366 | void enable_on_click(void) { 367 | runInMainThread(@selector(enable_on_click), nil); 368 | } 369 | 370 | void quit() { 371 | runInMainThread(@selector(quit), nil); 372 | } 373 | -------------------------------------------------------------------------------- /systray/systray_menu_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || freebsd || openbsd || netbsd 2 | // +build linux freebsd openbsd netbsd 3 | 4 | package systray 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/godbus/dbus/v5" 10 | "github.com/godbus/dbus/v5/prop" 11 | 12 | "github.com/danbai225/gpp/systray/internal/generated/menu" 13 | ) 14 | 15 | // SetIcon sets the icon of a menu item. 16 | // iconBytes should be the content of .ico/.jpg/.png 17 | func (item *MenuItem) SetIcon(iconBytes []byte) { 18 | instance.menuLock.Lock() 19 | defer instance.menuLock.Unlock() 20 | m, exists := findLayout(int32(item.id)) 21 | if exists { 22 | m.V1["icon-data"] = dbus.MakeVariant(iconBytes) 23 | refresh() 24 | } 25 | } 26 | 27 | // copyLayout makes full copy of layout 28 | func copyLayout(in *menuLayout, depth int32) *menuLayout { 29 | out := menuLayout{ 30 | V0: in.V0, 31 | V1: make(map[string]dbus.Variant, len(in.V1)), 32 | } 33 | for k, v := range in.V1 { 34 | out.V1[k] = v 35 | } 36 | if depth != 0 { 37 | depth-- 38 | out.V2 = make([]dbus.Variant, len(in.V2)) 39 | for i, v := range in.V2 { 40 | out.V2[i] = dbus.MakeVariant(copyLayout(v.Value().(*menuLayout), depth)) 41 | } 42 | } else { 43 | out.V2 = []dbus.Variant{} 44 | } 45 | return &out 46 | } 47 | 48 | // GetLayout is com.canonical.dbusmenu.GetLayout method. 49 | func (t *tray) GetLayout(parentID int32, recursionDepth int32, propertyNames []string) (revision uint32, layout menuLayout, err *dbus.Error) { 50 | instance.menuLock.Lock() 51 | defer instance.menuLock.Unlock() 52 | if m, ok := findLayout(parentID); ok { 53 | // return copy of menu layout to prevent panic from cuncurrent access to layout 54 | return instance.menuVersion, *copyLayout(m, recursionDepth), nil 55 | } 56 | return 57 | } 58 | 59 | // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. 60 | func (t *tray) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { 61 | V0 int32 62 | V1 map[string]dbus.Variant 63 | }, err *dbus.Error) { 64 | instance.menuLock.Lock() 65 | defer instance.menuLock.Unlock() 66 | for _, id := range ids { 67 | if m, ok := findLayout(id); ok { 68 | p := struct { 69 | V0 int32 70 | V1 map[string]dbus.Variant 71 | }{ 72 | V0: m.V0, 73 | V1: make(map[string]dbus.Variant, len(m.V1)), 74 | } 75 | for k, v := range m.V1 { 76 | p.V1[k] = v 77 | } 78 | properties = append(properties, p) 79 | } 80 | } 81 | return 82 | } 83 | 84 | // GetProperty is com.canonical.dbusmenu.GetProperty method. 85 | func (t *tray) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { 86 | instance.menuLock.Lock() 87 | defer instance.menuLock.Unlock() 88 | if m, ok := findLayout(id); ok { 89 | if p, ok := m.V1[name]; ok { 90 | return p, nil 91 | } 92 | } 93 | return 94 | } 95 | 96 | // Event is com.canonical.dbusmenu.Event method. 97 | func (t *tray) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { 98 | if eventID == "clicked" { 99 | systrayMenuItemSelected(uint32(id)) 100 | } 101 | return 102 | } 103 | 104 | // EventGroup is com.canonical.dbusmenu.EventGroup method. 105 | func (t *tray) EventGroup(events []struct { 106 | V0 int32 107 | V1 string 108 | V2 dbus.Variant 109 | V3 uint32 110 | }) (idErrors []int32, err *dbus.Error) { 111 | for _, event := range events { 112 | if event.V1 == "clicked" { 113 | systrayMenuItemSelected(uint32(event.V0)) 114 | } 115 | } 116 | return 117 | } 118 | 119 | // AboutToShow is com.canonical.dbusmenu.AboutToShow method. 120 | func (t *tray) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { 121 | return 122 | } 123 | 124 | // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. 125 | func (t *tray) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { 126 | return 127 | } 128 | 129 | func createMenuPropSpec() map[string]map[string]*prop.Prop { 130 | instance.menuLock.Lock() 131 | defer instance.menuLock.Unlock() 132 | return map[string]map[string]*prop.Prop{ 133 | "com.canonical.dbusmenu": { 134 | "Version": { 135 | Value: instance.menuVersion, 136 | Writable: true, 137 | Emit: prop.EmitTrue, 138 | Callback: nil, 139 | }, 140 | "TextDirection": { 141 | Value: "ltr", 142 | Writable: false, 143 | Emit: prop.EmitTrue, 144 | Callback: nil, 145 | }, 146 | "Status": { 147 | Value: "normal", 148 | Writable: false, 149 | Emit: prop.EmitTrue, 150 | Callback: nil, 151 | }, 152 | "IconThemePath": { 153 | Value: []string{}, 154 | Writable: false, 155 | Emit: prop.EmitTrue, 156 | Callback: nil, 157 | }, 158 | }, 159 | } 160 | } 161 | 162 | // menuLayout is a named struct to map into generated bindings. It represents the layout of a menu item 163 | type menuLayout = struct { 164 | V0 int32 // the unique ID of this item 165 | V1 map[string]dbus.Variant // properties for this menu item layout 166 | V2 []dbus.Variant // child menu item layouts 167 | } 168 | 169 | func addOrUpdateMenuItem(item *MenuItem) { 170 | var layout *menuLayout 171 | instance.menuLock.Lock() 172 | defer instance.menuLock.Unlock() 173 | m, exists := findLayout(int32(item.id)) 174 | if exists { 175 | layout = m 176 | } else { 177 | layout = &menuLayout{ 178 | V0: int32(item.id), 179 | V1: map[string]dbus.Variant{}, 180 | V2: []dbus.Variant{}, 181 | } 182 | 183 | parent := instance.menu 184 | if item.parent != nil { 185 | m, ok := findLayout(int32(item.parent.id)) 186 | if ok { 187 | parent = m 188 | parent.V1["children-display"] = dbus.MakeVariant("submenu") 189 | } 190 | } 191 | parent.V2 = append(parent.V2, dbus.MakeVariant(layout)) 192 | } 193 | 194 | applyItemToLayout(item, layout) 195 | if exists { 196 | refresh() 197 | } 198 | } 199 | 200 | func addSeparator(id uint32) { 201 | instance.menuLock.Lock() 202 | defer instance.menuLock.Unlock() 203 | layout := &menuLayout{ 204 | V0: int32(id), 205 | V1: map[string]dbus.Variant{ 206 | "type": dbus.MakeVariant("separator"), 207 | }, 208 | V2: []dbus.Variant{}, 209 | } 210 | instance.menu.V2 = append(instance.menu.V2, dbus.MakeVariant(layout)) 211 | refresh() 212 | } 213 | 214 | func applyItemToLayout(in *MenuItem, out *menuLayout) { 215 | out.V1["enabled"] = dbus.MakeVariant(!in.disabled) 216 | out.V1["label"] = dbus.MakeVariant(in.title) 217 | 218 | if in.isCheckable { 219 | out.V1["toggle-type"] = dbus.MakeVariant("checkmark") 220 | if in.checked { 221 | out.V1["toggle-state"] = dbus.MakeVariant(1) 222 | } else { 223 | out.V1["toggle-state"] = dbus.MakeVariant(0) 224 | } 225 | } else { 226 | out.V1["toggle-type"] = dbus.MakeVariant("") 227 | out.V1["toggle-state"] = dbus.MakeVariant(0) 228 | } 229 | } 230 | 231 | func findLayout(id int32) (*menuLayout, bool) { 232 | if id == 0 { 233 | return instance.menu, true 234 | } 235 | return findSubLayout(id, instance.menu.V2) 236 | } 237 | 238 | func findSubLayout(id int32, vals []dbus.Variant) (*menuLayout, bool) { 239 | for _, i := range vals { 240 | item := i.Value().(*menuLayout) 241 | if item.V0 == id { 242 | return item, true 243 | } 244 | 245 | if len(item.V2) > 0 { 246 | child, ok := findSubLayout(id, item.V2) 247 | if ok { 248 | return child, true 249 | } 250 | } 251 | } 252 | 253 | return nil, false 254 | } 255 | 256 | func hideMenuItem(item *MenuItem) { 257 | instance.menuLock.Lock() 258 | defer instance.menuLock.Unlock() 259 | m, exists := findLayout(int32(item.id)) 260 | if exists { 261 | m.V1["visible"] = dbus.MakeVariant(false) 262 | refresh() 263 | } 264 | } 265 | 266 | func showMenuItem(item *MenuItem) { 267 | instance.menuLock.Lock() 268 | defer instance.menuLock.Unlock() 269 | m, exists := findLayout(int32(item.id)) 270 | if exists { 271 | m.V1["visible"] = dbus.MakeVariant(true) 272 | refresh() 273 | } 274 | } 275 | 276 | func refresh() { 277 | if instance.conn == nil || instance.menuProps == nil { 278 | return 279 | } 280 | instance.menuVersion++ 281 | dbusErr := instance.menuProps.Set("com.canonical.dbusmenu", "Version", 282 | dbus.MakeVariant(instance.menuVersion)) 283 | if dbusErr != nil { 284 | log.Printf("systray error: failed to update menu version: %s\n", dbusErr) 285 | return 286 | } 287 | err := menu.Emit(instance.conn, &menu.Dbusmenu_LayoutUpdatedSignal{ 288 | Path: menuPath, 289 | Body: &menu.Dbusmenu_LayoutUpdatedSignalBody{ 290 | Revision: instance.menuVersion, 291 | }, 292 | }) 293 | if err != nil { 294 | log.Printf("systray error: failed to emit layout updated signal: %s\n", err) 295 | } 296 | 297 | } 298 | 299 | func resetMenu() { 300 | instance.menuLock.Lock() 301 | defer instance.menuLock.Unlock() 302 | instance.menu = &menuLayout{} 303 | instance.menuVersion++ 304 | refresh() 305 | } 306 | -------------------------------------------------------------------------------- /systray/systray_other.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin 2 | // +build !darwin 3 | 4 | package systray 5 | 6 | // CreateMenu 创建托盘菜单, 如果托盘菜单是空, 把菜单项添加到托盘 7 | // 该方法主动调用后 如果托盘菜单已创建则添加进去, 之后鼠标事件失效 8 | // 9 | // 仅MacOSX平台 10 | func CreateMenu() { 11 | } 12 | 13 | // SetMenuNil 托盘菜单设置为nil, 如果托盘菜单不是空, 把菜单项设置为nil 14 | // 该方法主动调用后 将移除托盘菜单, 之后鼠标事件生效 15 | // 16 | // 仅MacOSX平台 17 | func SetMenuNil() { 18 | } 19 | -------------------------------------------------------------------------------- /systray/systray_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || freebsd || openbsd || netbsd 2 | // +build linux freebsd openbsd netbsd 3 | 4 | //Note that you need to have github.com/knightpp/dbus-codegen-go installed from "custom" branch 5 | //go:generate dbus-codegen-go -prefix org.kde -package notifier -output internal/generated/notifier/status_notifier_item.go internal/StatusNotifierItem.xml 6 | //go:generate dbus-codegen-go -prefix com.canonical -package menu -output internal/generated/menu/dbus_menu.go internal/DbusMenu.xml 7 | 8 | package systray 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "image" 14 | _ "image/png" // used only here 15 | "log" 16 | "os" 17 | "sync" 18 | "time" 19 | 20 | "github.com/godbus/dbus/v5/introspect" 21 | "github.com/godbus/dbus/v5/prop" 22 | 23 | "github.com/danbai225/gpp/systray/internal/generated/menu" 24 | "github.com/danbai225/gpp/systray/internal/generated/notifier" 25 | dbus "github.com/godbus/dbus/v5" 26 | ) 27 | 28 | const ( 29 | path = "/StatusNotifierItem" 30 | menuPath = "/StatusNotifierMenu" 31 | ) 32 | 33 | var ( 34 | // to signal quitting the internal main loop 35 | quitChan = make(chan struct{}) 36 | 37 | // instance is the current instance of our DBus tray server 38 | instance = &tray{menu: &menuLayout{}, menuVersion: 1} 39 | ) 40 | 41 | // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back 42 | // to a regular icon on other platforms. 43 | // templateIconBytes and iconBytes should be the content of .ico for windows and 44 | // .ico/.jpg/.png for other platforms. 45 | func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { 46 | // TODO handle the templateIconBytes? 47 | SetIcon(regularIconBytes) 48 | } 49 | 50 | // SetIcon sets the systray icon. 51 | // iconBytes should be the content of .ico for windows and .ico/.jpg/.png 52 | // for other platforms. 53 | func SetIcon(iconBytes []byte) { 54 | instance.lock.Lock() 55 | instance.iconData = iconBytes 56 | props := instance.props 57 | conn := instance.conn 58 | defer instance.lock.Unlock() 59 | 60 | if props == nil { 61 | return 62 | } 63 | 64 | props.SetMust("org.kde.StatusNotifierItem", "IconPixmap", []PX{convertToPixels(iconBytes)}) 65 | 66 | if conn == nil { 67 | return 68 | } 69 | 70 | err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewIconSignal{ 71 | Path: path, 72 | Body: ¬ifier.StatusNotifierItem_NewIconSignalBody{}, 73 | }) 74 | if err != nil { 75 | log.Printf("systray error: failed to emit new icon signal: %s\n", err) 76 | return 77 | } 78 | } 79 | 80 | // SetTitle sets the systray title, only available on Mac and Linux. 81 | func SetTitle(t string) { 82 | instance.lock.Lock() 83 | instance.title = t 84 | props := instance.props 85 | conn := instance.conn 86 | defer instance.lock.Unlock() 87 | 88 | if props == nil { 89 | return 90 | } 91 | dbusErr := props.Set("org.kde.StatusNotifierItem", "Title", 92 | dbus.MakeVariant(t)) 93 | if dbusErr != nil { 94 | log.Printf("systray error: failed to set Title prop: %s\n", dbusErr) 95 | return 96 | } 97 | 98 | if conn == nil { 99 | return 100 | } 101 | 102 | err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewTitleSignal{ 103 | Path: path, 104 | Body: ¬ifier.StatusNotifierItem_NewTitleSignalBody{}, 105 | }) 106 | if err != nil { 107 | log.Printf("systray error: failed to emit new title signal: %s\n", err) 108 | return 109 | } 110 | } 111 | 112 | // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, 113 | // only available on Mac and Windows. 114 | func SetTooltip(tooltipTitle string) { 115 | instance.lock.Lock() 116 | instance.tooltipTitle = tooltipTitle 117 | props := instance.props 118 | defer instance.lock.Unlock() 119 | 120 | if props == nil { 121 | return 122 | } 123 | dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip", 124 | dbus.MakeVariant(tooltip{V2: tooltipTitle})) 125 | if dbusErr != nil { 126 | log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr) 127 | return 128 | } 129 | } 130 | 131 | // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and 132 | // Linux, it falls back to the regular icon bytes. 133 | // templateIconBytes and regularIconBytes should be the content of .ico for windows and 134 | // .ico/.jpg/.png for other platforms. 135 | func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { 136 | item.SetIcon(regularIconBytes) 137 | } 138 | 139 | func setInternalLoop(_ bool) { 140 | // nothing to action on Linux 141 | } 142 | 143 | func registerSystray() { 144 | } 145 | 146 | func nativeLoop() int { 147 | nativeStart() 148 | <-quitChan 149 | nativeEnd() 150 | return 0 151 | } 152 | 153 | func nativeEnd() { 154 | systrayExit() 155 | instance.conn.Close() 156 | } 157 | 158 | func quit() { 159 | close(quitChan) 160 | } 161 | 162 | var usni = &UnimplementedStatusNotifierItem{} 163 | 164 | type UnimplementedStatusNotifierItem struct { 165 | contextMenu func(x int32, y int32) 166 | activate func(x int32, y int32) 167 | dActivate func(x int32, y int32) 168 | secondaryActivate func(x int32, y int32) 169 | scroll func(delta int32, orientation string) 170 | dActivateTime int64 171 | } 172 | 173 | func (*UnimplementedStatusNotifierItem) iface() string { 174 | return notifier.InterfaceStatusNotifierItem 175 | } 176 | 177 | func (m *UnimplementedStatusNotifierItem) ContextMenu(x int32, y int32) (err *dbus.Error) { 178 | if m.contextMenu != nil { 179 | m.contextMenu(x, y) 180 | } else { 181 | err = &dbus.ErrMsgUnknownMethod 182 | } 183 | return 184 | } 185 | 186 | func (m *UnimplementedStatusNotifierItem) Activate(x int32, y int32) (err *dbus.Error) { 187 | if m.dActivateTime == 0 { 188 | m.dActivateTime = time.Now().UnixMilli() 189 | } else { 190 | nowMilli := time.Now().UnixMilli() 191 | if nowMilli-m.dActivateTime < dClickTimeMinInterval { 192 | m.dActivateTime = dClickTimeMinInterval 193 | if m.dActivate != nil { 194 | m.dActivate(x, y) 195 | return 196 | } 197 | } else { 198 | m.dActivateTime = nowMilli 199 | } 200 | } 201 | 202 | if m.activate != nil { 203 | m.activate(x, y) 204 | } else { 205 | err = &dbus.ErrMsgUnknownMethod 206 | } 207 | return 208 | } 209 | 210 | func (m *UnimplementedStatusNotifierItem) SecondaryActivate(x int32, y int32) (err *dbus.Error) { 211 | if m.secondaryActivate != nil { 212 | m.secondaryActivate(x, y) 213 | } else { 214 | err = &dbus.ErrMsgUnknownMethod 215 | } 216 | return 217 | } 218 | 219 | func (m *UnimplementedStatusNotifierItem) Scroll(delta int32, orientation string) (err *dbus.Error) { 220 | if m.scroll != nil { 221 | m.scroll(delta, orientation) 222 | } else { 223 | err = &dbus.ErrMsgUnknownMethod 224 | } 225 | return 226 | } 227 | 228 | func setOnClick(fn func(menu IMenu)) { 229 | usni.activate = func(x int32, y int32) { 230 | fn(nil) 231 | } 232 | } 233 | 234 | func setOnDClick(fn func(menu IMenu)) { 235 | usni.dActivate = func(x int32, y int32) { 236 | fn(nil) 237 | } 238 | } 239 | 240 | func setOnRClick(dClick func(IMenu)) { 241 | } 242 | 243 | func nativeStart() { 244 | if systrayReady != nil { 245 | systrayReady() 246 | } 247 | conn, _ := dbus.ConnectSessionBus() 248 | err := notifier.ExportStatusNotifierItem(conn, path, usni) 249 | if err != nil { 250 | log.Printf("systray error: failed to export status notifier item: %s\n", err) 251 | } 252 | err = menu.ExportDbusmenu(conn, menuPath, instance) 253 | if err != nil { 254 | log.Printf("systray error: failed to export status notifier item: %s\n", err) 255 | } 256 | 257 | name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process 258 | _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue) 259 | if err != nil { 260 | log.Printf("systray error: failed to request name: %s\n", err) 261 | // it's not critical error: continue 262 | } 263 | props, err := prop.Export(conn, path, instance.createPropSpec()) 264 | if err != nil { 265 | log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err) 266 | return 267 | } 268 | menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec()) 269 | if err != nil { 270 | log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err) 271 | return 272 | } 273 | 274 | node := introspect.Node{ 275 | Name: path, 276 | Interfaces: []introspect.Interface{ 277 | introspect.IntrospectData, 278 | prop.IntrospectData, 279 | notifier.IntrospectDataStatusNotifierItem, 280 | }, 281 | } 282 | err = conn.Export(introspect.NewIntrospectable(&node), path, 283 | "org.freedesktop.DBus.Introspectable") 284 | if err != nil { 285 | log.Printf("systray error: failed to export node introspection: %s\n", err) 286 | return 287 | } 288 | 289 | menuNode := introspect.Node{ 290 | Name: menuPath, 291 | Interfaces: []introspect.Interface{ 292 | introspect.IntrospectData, 293 | prop.IntrospectData, 294 | menu.IntrospectDataDbusmenu, 295 | }, 296 | } 297 | err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath, 298 | "org.freedesktop.DBus.Introspectable") 299 | if err != nil { 300 | log.Printf("systray error: failed to export menu node introspection: %s\n", err) 301 | return 302 | } 303 | 304 | instance.lock.Lock() 305 | instance.conn = conn 306 | instance.props = props 307 | instance.menuProps = menuProps 308 | instance.lock.Unlock() 309 | 310 | var ( 311 | obj dbus.BusObject 312 | call *dbus.Call 313 | ) 314 | obj = conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher") 315 | call = obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path) 316 | if call.Err != nil { 317 | log.Printf("systray error: failed to register our icon with the notifier watcher (maybe no tray is running?): %s\n", call.Err) 318 | } 319 | } 320 | 321 | // tray is a basic type that handles the dbus functionality 322 | type tray struct { 323 | // the DBus connection that we will use 324 | conn *dbus.Conn 325 | 326 | // icon data for the main systray icon 327 | iconData []byte 328 | // title and tooltip state 329 | title, tooltipTitle string 330 | 331 | lock sync.Mutex 332 | menu *menuLayout 333 | menuLock sync.RWMutex 334 | props, menuProps *prop.Properties 335 | menuVersion uint32 336 | } 337 | 338 | func (*tray) iface() string { 339 | return notifier.InterfaceStatusNotifierItem 340 | } 341 | 342 | func (t *tray) createPropSpec() map[string]map[string]*prop.Prop { 343 | t.lock.Lock() 344 | t.lock.Unlock() 345 | return map[string]map[string]*prop.Prop{ 346 | "org.kde.StatusNotifierItem": { 347 | "Status": { 348 | Value: "Active", // Passive, Active or NeedsAttention 349 | Writable: false, 350 | Emit: prop.EmitTrue, 351 | Callback: nil, 352 | }, 353 | "Title": { 354 | Value: t.title, 355 | Writable: true, 356 | Emit: prop.EmitTrue, 357 | Callback: nil, 358 | }, 359 | "Id": { 360 | Value: "1", 361 | Writable: false, 362 | Emit: prop.EmitTrue, 363 | Callback: nil, 364 | }, 365 | "Category": { 366 | Value: "ApplicationStatus", 367 | Writable: false, 368 | Emit: prop.EmitTrue, 369 | Callback: nil, 370 | }, 371 | "IconName": { 372 | Value: "", 373 | Writable: false, 374 | Emit: prop.EmitTrue, 375 | Callback: nil, 376 | }, 377 | "IconPixmap": { 378 | Value: []PX{convertToPixels(t.iconData)}, 379 | Writable: true, 380 | Emit: prop.EmitTrue, 381 | Callback: nil, 382 | }, 383 | "IconThemePath": { 384 | Value: "", 385 | Writable: false, 386 | Emit: prop.EmitTrue, 387 | Callback: nil, 388 | }, 389 | "ItemIsMenu": { 390 | Value: true, 391 | Writable: false, 392 | Emit: prop.EmitTrue, 393 | Callback: nil, 394 | }, 395 | "Menu": { 396 | Value: dbus.ObjectPath(menuPath), 397 | Writable: true, 398 | Emit: prop.EmitTrue, 399 | Callback: nil, 400 | }, 401 | "ToolTip": { 402 | Value: tooltip{V2: t.tooltipTitle}, 403 | Writable: true, 404 | Emit: prop.EmitTrue, 405 | Callback: nil, 406 | }, 407 | }} 408 | } 409 | 410 | // PX is picture pix map structure with width and high 411 | type PX struct { 412 | W, H int 413 | Pix []byte 414 | } 415 | 416 | // tooltip is our data for a tooltip property. 417 | // Param names need to match the generated code... 418 | type tooltip = struct { 419 | V0 string // name 420 | V1 []PX // icons 421 | V2 string // title 422 | V3 string // description 423 | } 424 | 425 | func convertToPixels(data []byte) PX { 426 | if len(data) == 0 { 427 | return PX{} 428 | } 429 | 430 | img, _, err := image.Decode(bytes.NewReader(data)) 431 | if err != nil { 432 | log.Printf("Failed to read icon format %v", err) 433 | return PX{} 434 | } 435 | 436 | return PX{ 437 | img.Bounds().Dx(), img.Bounds().Dy(), 438 | argbForImage(img), 439 | } 440 | } 441 | 442 | func argbForImage(img image.Image) []byte { 443 | w, h := img.Bounds().Dx(), img.Bounds().Dy() 444 | data := make([]byte, w*h*4) 445 | i := 0 446 | for y := 0; y < h; y++ { 447 | for x := 0; x < w; x++ { 448 | r, g, b, a := img.At(x, y).RGBA() 449 | data[i] = byte(a) 450 | data[i+1] = byte(r) 451 | data[i+2] = byte(g) 452 | data[i+3] = byte(b) 453 | i += 4 454 | } 455 | } 456 | return data 457 | } 458 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "gpp", 4 | "outputfilename": "gpp", 5 | "frontend:install": "npm install", 6 | "frontend:build": "npm run build", 7 | "frontend:dev:watcher": "npm run dev", 8 | "frontend:dev:serverUrl": "auto", 9 | "author": { 10 | "name": "danbai", 11 | "email": "danbai@88.com" 12 | }, 13 | "obfuscated": false 14 | } 15 | --------------------------------------------------------------------------------