include systray

This commit is contained in:
Nazar Kanaev 2021-03-17 00:27:18 +00:00
parent 514ed02693
commit e79abb69eb
18 changed files with 2547 additions and 22 deletions

2
go.mod
View File

@ -4,10 +4,10 @@ go 1.16
require (
github.com/PuerkitoBio/goquery v1.5.1
github.com/getlantern/systray v1.0.4
github.com/mattn/go-sqlite3 v1.14.0
github.com/mmcdole/gofeed v1.0.0
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13
)
replace github.com/mmcdole/gofeed => ./src/gofeed

22
go.sum
View File

@ -7,22 +7,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.0.4 h1:qJ/bOlYhn5nsj2FejutWWVFMbhOkYhsChoy26OjgZgU=
github.com/getlantern/systray v1.0.4/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -34,8 +18,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -52,8 +34,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View File

@ -3,7 +3,7 @@
package platform
import (
"github.com/getlantern/systray"
"github.com/nkanaev/yarr/src/systray"
"github.com/nkanaev/yarr/src/server"
)

12
src/systray/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
example/example
webview_example/webview_example
*~
*.swp
**/*.exe
Release
Debug
*.sdf
dll/systray_unsigned.dll
out.txt
.vs
on_exit*.txt

125
src/systray/CHANGELOG.md Normal file
View File

@ -0,0 +1,125 @@
# Changelog
## [v1.1.0](https://github.com/getlantern/systray/tree/v1.1.0) (2020-11-18)
[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.5...v1.1.0)
**Merged pull requests:**
- Add submenu support for Linux [\#183](https://github.com/getlantern/systray/pull/183) ([fbrinker](https://github.com/fbrinker))
- Add checkbox support for Linux [\#181](https://github.com/getlantern/systray/pull/181) ([fbrinker](https://github.com/fbrinker))
- fix SetTitle documentation [\#179](https://github.com/getlantern/systray/pull/179) ([delthas](https://github.com/delthas))
## [v1.0.5](https://github.com/getlantern/systray/tree/v1.0.5) (2020-10-19)
[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.4...v1.0.5)
**Merged pull requests:**
- start menu ID with positive, and change the type to uint32 [\#173](https://github.com/getlantern/systray/pull/173) ([joesis](https://github.com/joesis))
- Allows disabling items in submenu on macOS [\#172](https://github.com/getlantern/systray/pull/172) ([joesis](https://github.com/joesis))
- Does not use the template icon for regular icons [\#171](https://github.com/getlantern/systray/pull/171) ([sithembiso](https://github.com/sithembiso))
## [v1.0.4](https://github.com/getlantern/systray/tree/v1.0.4) (2020-07-21)
[Full Changelog](https://github.com/getlantern/systray/compare/1.0.3...v1.0.4)
**Merged pull requests:**
- protect shared data structures with proper mutexes [\#162](https://github.com/getlantern/systray/pull/162) ([joesis](https://github.com/joesis))
## [1.0.3](https://github.com/getlantern/systray/tree/1.0.3) (2020-06-11)
[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.3...1.0.3)
## [v1.0.3](https://github.com/getlantern/systray/tree/v1.0.3) (2020-06-11)
[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.2...v1.0.3)
**Merged pull requests:**
- have a workaround to avoid crash on old macOS versions [\#153](https://github.com/getlantern/systray/pull/153) ([joesis](https://github.com/joesis))
- Fix bug on darwin of setting icon for menu [\#147](https://github.com/getlantern/systray/pull/147) ([mangalaman93](https://github.com/mangalaman93))
## [v1.0.2](https://github.com/getlantern/systray/tree/v1.0.2) (2020-05-19)
[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.1...v1.0.2)
**Merged pull requests:**
- remove unused dependencies [\#145](https://github.com/getlantern/systray/pull/145) ([joesis](https://github.com/joesis))
## [v1.0.1](https://github.com/getlantern/systray/tree/v1.0.1) (2020-05-18)
[Full Changelog](https://github.com/getlantern/systray/compare/1.0.1...v1.0.1)
## [1.0.1](https://github.com/getlantern/systray/tree/1.0.1) (2020-05-18)
[Full Changelog](https://github.com/getlantern/systray/compare/1.0.0...1.0.1)
**Merged pull requests:**
- Unlock menuItemsLock before changing UI [\#144](https://github.com/getlantern/systray/pull/144) ([joesis](https://github.com/joesis))
## [1.0.0](https://github.com/getlantern/systray/tree/1.0.0) (2020-05-18)
[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.0...1.0.0)
## [v1.0.0](https://github.com/getlantern/systray/tree/v1.0.0) (2020-05-18)
[Full Changelog](https://github.com/getlantern/systray/compare/0.9.0...v1.0.0)
**Merged pull requests:**
- Check if the menu item is nil [\#137](https://github.com/getlantern/systray/pull/137) ([myleshorton](https://github.com/myleshorton))
## [0.9.0](https://github.com/getlantern/systray/tree/0.9.0) (2020-03-24)
[Full Changelog](https://github.com/getlantern/systray/compare/v0.9.0...0.9.0)
## [v0.9.0](https://github.com/getlantern/systray/tree/v0.9.0) (2020-03-24)
[Full Changelog](https://github.com/getlantern/systray/compare/8e63b37ef27d94f6db79c4ffb941608e8f0dc2f9...v0.9.0)
**Merged pull requests:**
- Backport all features and fixes from master [\#140](https://github.com/getlantern/systray/pull/140) ([joesis](https://github.com/joesis))
- Nested menu windows [\#132](https://github.com/getlantern/systray/pull/132) ([joesis](https://github.com/joesis))
- Support for nested sub-menus on OS X [\#131](https://github.com/getlantern/systray/pull/131) ([oxtoacart](https://github.com/oxtoacart))
- Use temp directory for walk resource manager [\#129](https://github.com/getlantern/systray/pull/129) ([max-b](https://github.com/max-b))
- Added support for template icons on macOS [\#119](https://github.com/getlantern/systray/pull/119) ([oxtoacart](https://github.com/oxtoacart))
- When launching app window on macOS, make application a foreground app… [\#118](https://github.com/getlantern/systray/pull/118) ([oxtoacart](https://github.com/oxtoacart))
- Include stdlib.h in systray\_browser\_linux to explicitly declare funct… [\#114](https://github.com/getlantern/systray/pull/114) ([oxtoacart](https://github.com/oxtoacart))
- Fix panic when resources root path is not the working directory [\#112](https://github.com/getlantern/systray/pull/112) ([ksubileau](https://github.com/ksubileau))
- Don't print close reason to console [\#111](https://github.com/getlantern/systray/pull/111) ([ksubileau](https://github.com/ksubileau))
- Systray icon could not be changed dynamically [\#110](https://github.com/getlantern/systray/pull/110) ([ksubileau](https://github.com/ksubileau))
- Preventing deadlock on menu item ClickeCh when no one is listening, c… [\#105](https://github.com/getlantern/systray/pull/105) ([oxtoacart](https://github.com/oxtoacart))
- Reverted deadlock fix \(Affected other receivers\) [\#104](https://github.com/getlantern/systray/pull/104) ([ldstein](https://github.com/ldstein))
- Fix Deadlock and item ordering in Windows [\#103](https://github.com/getlantern/systray/pull/103) ([ldstein](https://github.com/ldstein))
- Minor README improvements \(go modules, example app, screenshot\) [\#98](https://github.com/getlantern/systray/pull/98) ([tstromberg](https://github.com/tstromberg))
- Add support for app window [\#97](https://github.com/getlantern/systray/pull/97) ([oxtoacart](https://github.com/oxtoacart))
- systray\_darwin.m: Compare Mac OS min version with value instead of macro [\#94](https://github.com/getlantern/systray/pull/94) ([teddywing](https://github.com/teddywing))
- Attempt to fix https://github.com/getlantern/systray/issues/75 [\#92](https://github.com/getlantern/systray/pull/92) ([mikeschinkel](https://github.com/mikeschinkel))
- Fix application path for MacOS in README [\#91](https://github.com/getlantern/systray/pull/91) ([zereraz](https://github.com/zereraz))
- Document cross-platform console window details [\#81](https://github.com/getlantern/systray/pull/81) ([michaelsanford](https://github.com/michaelsanford))
- Fix bad-looking system tray icon in Windows [\#78](https://github.com/getlantern/systray/pull/78) ([juja256](https://github.com/juja256))
- Add the separator to the visible items [\#76](https://github.com/getlantern/systray/pull/76) ([meskio](https://github.com/meskio))
- keep track of hidden items [\#74](https://github.com/getlantern/systray/pull/74) ([kalikaneko](https://github.com/kalikaneko))
- Support macOS older than 10.13 [\#73](https://github.com/getlantern/systray/pull/73) ([swznd](https://github.com/swznd))
- define ERROR\_SUCCESS as syscall.Errno [\#69](https://github.com/getlantern/systray/pull/69) ([joesis](https://github.com/joesis))
- Bug/fix broken menuitem show [\#68](https://github.com/getlantern/systray/pull/68) ([kalikaneko](https://github.com/kalikaneko))
- Fix mac deprecations [\#66](https://github.com/getlantern/systray/pull/66) ([jefvel](https://github.com/jefvel))
- Made it possible to add icons to menu items on Mac [\#65](https://github.com/getlantern/systray/pull/65) ([jefvel](https://github.com/jefvel))
- linux: delete temp files as soon as they are not needed [\#63](https://github.com/getlantern/systray/pull/63) ([meskio](https://github.com/meskio))
- Merge changes from amkulikov to remove DLL for windows [\#56](https://github.com/getlantern/systray/pull/56) ([oxtoacart](https://github.com/oxtoacart))
- Revert "Use templated icons for the menu bar in macOS" [\#51](https://github.com/getlantern/systray/pull/51) ([stoggi](https://github.com/stoggi))
- Use templated icons for the menu bar in macOS [\#46](https://github.com/getlantern/systray/pull/46) ([stoggi](https://github.com/stoggi))
- Syscalls instead of custom DLLs [\#44](https://github.com/getlantern/systray/pull/44) ([amkulikov](https://github.com/amkulikov))
- On quit exit main loop on linux [\#41](https://github.com/getlantern/systray/pull/41) ([meskio](https://github.com/meskio))
- Fixed hide show in linux \(\#37\) [\#39](https://github.com/getlantern/systray/pull/39) ([meskio](https://github.com/meskio))
- fix: linux compilation warning [\#36](https://github.com/getlantern/systray/pull/36) ([novln](https://github.com/novln))
- Added separator functionality [\#32](https://github.com/getlantern/systray/pull/32) ([oxtoacart](https://github.com/oxtoacart))
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*

202
src/systray/LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 Brave New Software Project, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

18
src/systray/Makefile Normal file
View File

@ -0,0 +1,18 @@
tag-changelog: require-version require-gh-token
echo "Tagging..." && \
git tag -a "$$VERSION" -f --annotate -m"Tagged $$VERSION" && \
git push --tags -f && \
git checkout master && \
git pull && \
github_changelog_generator --no-issues --max-issues 100 --token "${GH_TOKEN}" --user getlantern --project systray && \
git add CHANGELOG.md && \
git commit -m "Updated changelog for $$VERSION" && \
git push origin HEAD && \
git checkout -
guard-%:
@ if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi
require-version: guard-VERSION
require-gh-token: guard-GH_TOKEN

11
src/systray/NOTES.txt Normal file
View File

@ -0,0 +1,11 @@
taken from:
repo:
https://github.com/getlantern/systray
hash:
2c0986dda9aea361e925f90e848d9036be7b5367
changes:
-removed `getlantern/golog` dependency

120
src/systray/README.md Normal file
View File

@ -0,0 +1,120 @@
systray is a cross-platform Go library to place an icon and menu in the notification area.
## Features
* Supported on Windows, macOS, and Linux
* Menu items can be checked and/or disabled
* Methods may be called from any Goroutine
## API
```go
func main() {
systray.Run(onReady, onExit)
}
func onReady() {
systray.SetIcon(icon.Data)
systray.SetTitle("Awesome App")
systray.SetTooltip("Pretty awesome超级棒")
mQuit := systray.AddMenuItem("Quit", "Quit the whole app")
// Sets the icon of a menu item. Only available on Mac and Windows.
mQuit.SetIcon(icon.Data)
}
func onExit() {
// clean up here
}
```
See [full API](https://pkg.go.dev/github.com/getlantern/systray?tab=doc) as well as [CHANGELOG](https://github.com/getlantern/systray/tree/master/CHANGELOG.md).
## Try the example app!
Have go v1.12+ or higher installed? Here's an example to get started on macOS:
```sh
git clone https://github.com/getlantern/systray
cd example
env GO111MODULE=on go build
./example
```
On Windows, you should build like this:
```
env GO111MODULE=on go build -ldflags "-H=windowsgui"
```
The following text will then appear on the console:
```sh
go: finding github.com/skratchdot/open-golang latest
go: finding github.com/getlantern/systray latest
go: finding github.com/getlantern/golog latest
```
Now look for *Awesome App* in your menu bar!
![Awesome App screenshot](example/screenshot.png)
## The Webview example
The code under `webview_example` is to demostrate how it can co-exist with other UI elements. Note that the example doesn't work on macOS versions older than 10.15 Catalina.
## Platform notes
### Linux
* Building apps requires gcc as well as the `gtk3` and `libappindicator3` development headers to be installed. For Debian or Ubuntu, you may install these using:
```sh
sudo apt-get install gcc libgtk-3-dev libappindicator3-dev
```
On Linux Mint, `libxapp-dev` is also required .
To build `webview_example`, you also need to install `libwebkit2gtk-4.0-dev` and remove `webview_example/rsrc.syso` which is required on Windows.
### Windows
* To avoid opening a console at application startup, use these compile flags:
```sh
go build -ldflags -H=windowsgui
```
### macOS
On macOS, you will need to create an application bundle to wrap the binary; simply folders with the following minimal structure and assets:
```
SystrayApp.app/
Contents/
Info.plist
MacOS/
go-executable
Resources/
SystrayApp.icns
```
When running as an app bundle, you may want to add one or both of the following to your Info.plist:
```xml
<!-- avoid having a blurry icon and text -->
<key>NSHighResolutionCapable</key>
<string>True</string>
<!-- avoid showing the app on the Dock -->
<key>LSUIElement</key>
<string>1</string>
```
Consult the [Official Apple Documentation here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1).
## Credits
- https://github.com/xilp/systray
- https://github.com/cratonica/trayhost

233
src/systray/systray.go Normal file
View File

@ -0,0 +1,233 @@
/*
Package systray is a cross-platform Go library to place an icon and menu in the notification area.
*/
package systray
import (
"fmt"
"log"
"runtime"
"sync"
"sync/atomic"
)
var (
systrayReady func()
systrayExit func()
menuItems = make(map[uint32]*MenuItem)
menuItemsLock sync.RWMutex
currentID = uint32(0)
quitOnce sync.Once
)
func init() {
runtime.LockOSThread()
}
// MenuItem is used to keep track each menu item of systray.
// Don't create it directly, use the one systray.AddMenuItem() returned
type MenuItem struct {
// ClickedCh is the channel which will be notified when the menu item is clicked
ClickedCh chan struct{}
// id uniquely identify a menu item, not supposed to be modified
id uint32
// title is the text shown on menu item
title string
// tooltip is the text shown when pointing to menu item
tooltip string
// disabled menu item is grayed out and has no effect when clicked
disabled bool
// checked menu item has a tick before the title
checked bool
// has the menu item a checkbox (Linux)
isCheckable bool
// parent item, for sub menus
parent *MenuItem
}
func (item *MenuItem) String() string {
if item.parent == nil {
return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title)
}
return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title)
}
// newMenuItem returns a populated MenuItem object
func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem {
return &MenuItem{
ClickedCh: make(chan struct{}),
id: atomic.AddUint32(&currentID, 1),
title: title,
tooltip: tooltip,
disabled: false,
checked: false,
isCheckable: false,
parent: parent,
}
}
// Run initializes GUI and starts the event loop, then invokes the onReady
// callback. It blocks until systray.Quit() is called.
func Run(onReady func(), onExit func()) {
Register(onReady, onExit)
nativeLoop()
}
// Register initializes GUI and registers the callbacks but relies on the
// caller to run the event loop somewhere else. It's useful if the program
// needs to show other UI elements, for example, webview.
// To overcome some OS weirdness, On macOS versions before Catalina, calling
// this does exactly the same as Run().
func Register(onReady func(), onExit func()) {
if onReady == nil {
systrayReady = func() {}
} else {
// Run onReady on separate goroutine to avoid blocking event loop
readyCh := make(chan interface{})
go func() {
<-readyCh
onReady()
}()
systrayReady = func() {
close(readyCh)
}
}
// unlike onReady, onExit runs in the event loop to make sure it has time to
// finish before the process terminates
if onExit == nil {
onExit = func() {}
}
systrayExit = onExit
registerSystray()
}
// Quit the systray
func Quit() {
quitOnce.Do(quit)
}
// AddMenuItem adds a menu item with the designated title and tooltip.
// It can be safely invoked from different goroutines.
// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox
func AddMenuItem(title string, tooltip string) *MenuItem {
item := newMenuItem(title, tooltip, nil)
item.update()
return item
}
// AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux.
// It can be safely invoked from different goroutines.
// On Windows and OSX this is the same as calling AddMenuItem
func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem {
item := newMenuItem(title, tooltip, nil)
item.isCheckable = true
item.checked = checked
item.update()
return item
}
// AddSeparator adds a separator bar to the menu
func AddSeparator() {
addSeparator(atomic.AddUint32(&currentID, 1))
}
// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip.
// It can be safely invoked from different goroutines.
// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox
func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem {
child := newMenuItem(title, tooltip, item)
child.update()
return child
}
// AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux.
// It can be safely invoked from different goroutines.
// On Windows and OSX this is the same as calling AddSubMenuItem
func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem {
child := newMenuItem(title, tooltip, item)
child.isCheckable = true
child.checked = checked
child.update()
return child
}
// SetTitle set the text to display on a menu item
func (item *MenuItem) SetTitle(title string) {
item.title = title
item.update()
}
// SetTooltip set the tooltip to show when mouse hover
func (item *MenuItem) SetTooltip(tooltip string) {
item.tooltip = tooltip
item.update()
}
// Disabled checks if the menu item is disabled
func (item *MenuItem) Disabled() bool {
return item.disabled
}
// Enable a menu item regardless if it's previously enabled or not
func (item *MenuItem) Enable() {
item.disabled = false
item.update()
}
// Disable a menu item regardless if it's previously disabled or not
func (item *MenuItem) Disable() {
item.disabled = true
item.update()
}
// Hide hides a menu item
func (item *MenuItem) Hide() {
hideMenuItem(item)
}
// Show shows a previously hidden menu item
func (item *MenuItem) Show() {
showMenuItem(item)
}
// Checked returns if the menu item has a check mark
func (item *MenuItem) Checked() bool {
return item.checked
}
// Check a menu item regardless if it's previously checked or not
func (item *MenuItem) Check() {
item.checked = true
item.update()
}
// Uncheck a menu item regardless if it's previously unchecked or not
func (item *MenuItem) Uncheck() {
item.checked = false
item.update()
}
// update propagates changes on a menu item to systray
func (item *MenuItem) update() {
menuItemsLock.Lock()
menuItems[item.id] = item
menuItemsLock.Unlock()
addOrUpdateMenuItem(item)
}
func systrayMenuItemSelected(id uint32) {
menuItemsLock.RLock()
item, ok := menuItems[id]
menuItemsLock.RUnlock()
if !ok {
log.Printf("No menu item with ID %v", id)
return
}
select {
case item.ClickedCh <- struct{}{}:
// in case no one waiting for the channel
default:
}
}

17
src/systray/systray.h Normal file
View File

@ -0,0 +1,17 @@
#include "stdbool.h"
extern void systray_ready();
extern void systray_on_exit();
extern void systray_menu_item_selected(int menu_id);
void registerSystray(void);
int nativeLoop(void);
void setIcon(const char* iconBytes, int length, bool template);
void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template);
void setTitle(char* title);
void setTooltip(char* tooltip);
void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable);
void add_separator(int menuId);
void hide_menu_item(int menuId);
void show_menu_item(int menuId);
void quit();

View File

@ -0,0 +1,38 @@
package systray
/*
#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc
#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit
#include "systray.h"
*/
import "C"
import (
"unsafe"
)
// SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back
// to a regular icon on other platforms.
// templateIconBytes and regularIconBytes should be the content of .ico for windows and
// .ico/.jpg/.png for other platforms.
func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0]))
C.setIcon(cstr, (C.int)(len(templateIconBytes)), true)
}
// SetIcon sets the icon of a menu item. Only works on macOS and Windows.
// iconBytes should be the content of .ico/.jpg/.png
func (item *MenuItem) SetIcon(iconBytes []byte) {
cstr := (*C.char)(unsafe.Pointer(&iconBytes[0]))
C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id), false)
}
// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
// falls back to the regular icon bytes and on Linux it does nothing.
// templateIconBytes and regularIconBytes should be the content of .ico for windows and
// .ico/.jpg/.png for other platforms.
func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0]))
C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true)
}

View File

@ -0,0 +1,293 @@
#import <Cocoa/Cocoa.h>
#include "systray.h"
#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400
#ifndef NSControlStateValueOff
#define NSControlStateValueOff NSOffState
#endif
#ifndef NSControlStateValueOn
#define NSControlStateValueOn NSOnState
#endif
#endif
@interface MenuItem : NSObject
{
@public
NSNumber* menuId;
NSNumber* parentMenuId;
NSString* title;
NSString* tooltip;
short disabled;
short checked;
}
-(id) initWithId: (int)theMenuId
withParentMenuId: (int)theParentMenuId
withTitle: (const char*)theTitle
withTooltip: (const char*)theTooltip
withDisabled: (short)theDisabled
withChecked: (short)theChecked;
@end
@implementation MenuItem
-(id) initWithId: (int)theMenuId
withParentMenuId: (int)theParentMenuId
withTitle: (const char*)theTitle
withTooltip: (const char*)theTooltip
withDisabled: (short)theDisabled
withChecked: (short)theChecked
{
menuId = [NSNumber numberWithInt:theMenuId];
parentMenuId = [NSNumber numberWithInt:theParentMenuId];
title = [[NSString alloc] initWithCString:theTitle
encoding:NSUTF8StringEncoding];
tooltip = [[NSString alloc] initWithCString:theTooltip
encoding:NSUTF8StringEncoding];
disabled = theDisabled;
checked = theChecked;
return self;
}
@end
@interface AppDelegate: NSObject <NSApplicationDelegate>
- (void) add_or_update_menu_item:(MenuItem*) item;
- (IBAction)menuHandler:(id)sender;
@property (assign) IBOutlet NSWindow *window;
@end
@implementation AppDelegate
{
NSStatusItem *statusItem;
NSMenu *menu;
NSCondition* cond;
}
@synthesize window = _window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self->statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
self->menu = [[NSMenu alloc] init];
[self->menu setAutoenablesItems: FALSE];
[self->statusItem setMenu:self->menu];
systray_ready();
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
systray_on_exit();
}
- (void)setIcon:(NSImage *)image {
statusItem.button.image = image;
[self updateTitleButtonStyle];
}
- (void)setTitle:(NSString *)title {
statusItem.button.title = title;
[self updateTitleButtonStyle];
}
-(void)updateTitleButtonStyle {
if (statusItem.button.image != nil) {
if ([statusItem.button.title length] == 0) {
statusItem.button.imagePosition = NSImageOnly;
} else {
statusItem.button.imagePosition = NSImageLeft;
}
} else {
statusItem.button.imagePosition = NSNoImage;
}
}
- (void)setTooltip:(NSString *)tooltip {
statusItem.button.toolTip = tooltip;
}
- (IBAction)menuHandler:(id)sender
{
NSNumber* menuId = [sender representedObject];
systray_menu_item_selected(menuId.intValue);
}
- (void)add_or_update_menu_item:(MenuItem *)item {
NSMenu *theMenu = self->menu;
NSMenuItem *parentItem;
if ([item->parentMenuId integerValue] > 0) {
parentItem = find_menu_item(menu, item->parentMenuId);
if (parentItem.hasSubmenu) {
theMenu = parentItem.submenu;
} else {
theMenu = [[NSMenu alloc] init];
[theMenu setAutoenablesItems:NO];
[parentItem setSubmenu:theMenu];
}
}
NSMenuItem *menuItem;
menuItem = find_menu_item(theMenu, item->menuId);
if (menuItem == NULL) {
menuItem = [theMenu addItemWithTitle:item->title
action:@selector(menuHandler:)
keyEquivalent:@""];
[menuItem setRepresentedObject:item->menuId];
}
[menuItem setTitle:item->title];
[menuItem setTag:[item->menuId integerValue]];
[menuItem setTarget:self];
[menuItem setToolTip:item->tooltip];
if (item->disabled == 1) {
menuItem.enabled = FALSE;
} else {
menuItem.enabled = TRUE;
}
if (item->checked == 1) {
menuItem.state = NSControlStateValueOn;
} else {
menuItem.state = NSControlStateValueOff;
}
}
NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) {
NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]];
if (foundItem != NULL) {
return foundItem;
}
NSArray *menu_items = ourMenu.itemArray;
int i;
for (i = 0; i < [menu_items count]; i++) {
NSMenuItem *i_item = [menu_items objectAtIndex:i];
if (i_item.hasSubmenu) {
foundItem = find_menu_item(i_item.submenu, menuId);
if (foundItem != NULL) {
return foundItem;
}
}
}
return NULL;
};
- (void) add_separator:(NSNumber*) menuId
{
[menu addItem: [NSMenuItem separatorItem]];
}
- (void) hide_menu_item:(NSNumber*) menuId
{
NSMenuItem* menuItem = find_menu_item(menu, menuId);
if (menuItem != NULL) {
[menuItem setHidden:TRUE];
}
}
- (void) setMenuItemIcon:(NSArray*)imageAndMenuId {
NSImage* image = [imageAndMenuId objectAtIndex:0];
NSNumber* menuId = [imageAndMenuId objectAtIndex:1];
NSMenuItem* menuItem;
menuItem = find_menu_item(menu, menuId);
if (menuItem == NULL) {
return;
}
menuItem.image = image;
}
- (void) show_menu_item:(NSNumber*) menuId
{
NSMenuItem* menuItem = find_menu_item(menu, menuId);
if (menuItem != NULL) {
[menuItem setHidden:FALSE];
}
}
- (void) quit
{
[NSApp terminate:self];
}
@end
void registerSystray(void) {
AppDelegate *delegate = [[AppDelegate alloc] init];
[[NSApplication sharedApplication] setDelegate:delegate];
// A workaround to avoid crashing on macOS versions before Catalina. Somehow
// SIGSEGV would happen inside AppKit if [NSApp run] is called from a
// different function, even if that function is called right after this.
if (floor(NSAppKitVersionNumber) <= /*NSAppKitVersionNumber10_14*/ 1671){
[NSApp run];
}
}
int nativeLoop(void) {
if (floor(NSAppKitVersionNumber) > /*NSAppKitVersionNumber10_14*/ 1671){
[NSApp run];
}
return EXIT_SUCCESS;
}
void runInMainThread(SEL method, id object) {
[(AppDelegate*)[NSApp delegate]
performSelectorOnMainThread:method
withObject:object
waitUntilDone: YES];
}
void setIcon(const char* iconBytes, int length, bool template) {
NSData* buffer = [NSData dataWithBytes: iconBytes length:length];
NSImage *image = [[NSImage alloc] initWithData:buffer];
[image setSize:NSMakeSize(16, 16)];
image.template = template;
runInMainThread(@selector(setIcon:), (id)image);
}
void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) {
NSData* buffer = [NSData dataWithBytes: iconBytes length:length];
NSImage *image = [[NSImage alloc] initWithData:buffer];
[image setSize:NSMakeSize(16, 16)];
image.template = template;
NSNumber *mId = [NSNumber numberWithInt:menuId];
runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]);
}
void setTitle(char* ctitle) {
NSString* title = [[NSString alloc] initWithCString:ctitle
encoding:NSUTF8StringEncoding];
free(ctitle);
runInMainThread(@selector(setTitle:), (id)title);
}
void setTooltip(char* ctooltip) {
NSString* tooltip = [[NSString alloc] initWithCString:ctooltip
encoding:NSUTF8StringEncoding];
free(ctooltip);
runInMainThread(@selector(setTooltip:), (id)tooltip);
}
void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable) {
MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked];
free(title);
free(tooltip);
runInMainThread(@selector(add_or_update_menu_item:), (id)item);
}
void add_separator(int menuId) {
NSNumber *mId = [NSNumber numberWithInt:menuId];
runInMainThread(@selector(add_separator:), (id)mId);
}
void hide_menu_item(int menuId) {
NSNumber *mId = [NSNumber numberWithInt:menuId];
runInMainThread(@selector(hide_menu_item:), (id)mId);
}
void show_menu_item(int menuId) {
NSNumber *mId = [NSNumber numberWithInt:menuId];
runInMainThread(@selector(show_menu_item:), (id)mId);
}
void quit() {
runInMainThread(@selector(quit), nil);
}

268
src/systray/systray_linux.c Normal file
View File

@ -0,0 +1,268 @@
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <libappindicator/app-indicator.h>
#include "systray.h"
static AppIndicator *global_app_indicator;
static GtkWidget *global_tray_menu = NULL;
static GList *global_menu_items = NULL;
static char temp_file_name[PATH_MAX] = "";
typedef struct {
GtkWidget *menu_item;
int menu_id;
long signalHandlerId;
} MenuItemNode;
typedef struct {
int menu_id;
int parent_menu_id;
char* title;
char* tooltip;
short disabled;
short checked;
short isCheckable;
} MenuItemInfo;
void registerSystray(void) {
gtk_init(0, NULL);
global_app_indicator = app_indicator_new("systray", "", APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
app_indicator_set_status(global_app_indicator, APP_INDICATOR_STATUS_ACTIVE);
global_tray_menu = gtk_menu_new();
app_indicator_set_menu(global_app_indicator, GTK_MENU(global_tray_menu));
systray_ready();
}
int nativeLoop(void) {
gtk_main();
systray_on_exit();
return 0;
}
void _unlink_temp_file() {
if (strlen(temp_file_name) != 0) {
int ret = unlink(temp_file_name);
if (ret == -1) {
printf("failed to remove temp icon file %s: %s\n", temp_file_name, strerror(errno));
}
temp_file_name[0] = '\0';
}
}
// runs in main thread, should always return FALSE to prevent gtk to execute it again
gboolean do_set_icon(gpointer data) {
_unlink_temp_file();
char *tmpdir = getenv("TMPDIR");
if (NULL == tmpdir) {
tmpdir = "/tmp";
}
strncpy(temp_file_name, tmpdir, PATH_MAX-1);
strncat(temp_file_name, "/systray_XXXXXX", PATH_MAX-1);
temp_file_name[PATH_MAX-1] = '\0';
GBytes* bytes = (GBytes*)data;
int fd = mkstemp(temp_file_name);
if (fd == -1) {
printf("failed to create temp icon file %s: %s\n", temp_file_name, strerror(errno));
return FALSE;
}
gsize size = 0;
gconstpointer icon_data = g_bytes_get_data(bytes, &size);
ssize_t written = write(fd, icon_data, size);
close(fd);
if(written != size) {
printf("failed to write temp icon file %s: %s\n", temp_file_name, strerror(errno));
return FALSE;
}
app_indicator_set_icon_full(global_app_indicator, temp_file_name, "");
app_indicator_set_attention_icon_full(global_app_indicator, temp_file_name, "");
g_bytes_unref(bytes);
return FALSE;
}
void _systray_menu_item_selected(int *id) {
systray_menu_item_selected(*id);
}
GtkMenuItem* find_menu_by_id(int id) {
GList* it;
for(it = global_menu_items; it != NULL; it = it->next) {
MenuItemNode* item = (MenuItemNode*)(it->data);
if(item->menu_id == id) {
return GTK_MENU_ITEM(item->menu_item);
}
}
return NULL;
}
// runs in main thread, should always return FALSE to prevent gtk to execute it again
gboolean do_add_or_update_menu_item(gpointer data) {
MenuItemInfo *mii = (MenuItemInfo*)data;
GList* it;
for(it = global_menu_items; it != NULL; it = it->next) {
MenuItemNode* item = (MenuItemNode*)(it->data);
if(item->menu_id == mii->menu_id) {
gtk_menu_item_set_label(GTK_MENU_ITEM(item->menu_item), mii->title);
if (mii->isCheckable) {
// We need to block the "activate" event, to emulate the same behaviour as in the windows version
// A Check/Uncheck does change the checkbox, but does not trigger the checkbox menuItem channel
g_signal_handler_block(GTK_CHECK_MENU_ITEM(item->menu_item), item->signalHandlerId);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item->menu_item), mii->checked == 1);
g_signal_handler_unblock(GTK_CHECK_MENU_ITEM(item->menu_item), item->signalHandlerId);
}
break;
}
}
// menu id doesn't exist, add new item
if(it == NULL) {
GtkWidget *menu_item;
if (mii->isCheckable) {
menu_item = gtk_check_menu_item_new_with_label(mii->title);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_item), mii->checked == 1);
} else {
menu_item = gtk_menu_item_new_with_label(mii->title);
}
int *id = malloc(sizeof(int));
*id = mii->menu_id;
long signalHandlerId = g_signal_connect_swapped(
G_OBJECT(menu_item),
"activate",
G_CALLBACK(_systray_menu_item_selected),
id
);
if (mii->parent_menu_id == 0) {
gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), menu_item);
} else {
GtkMenuItem* parentMenuItem = find_menu_by_id(mii->parent_menu_id);
GtkWidget* parentMenu = gtk_menu_item_get_submenu(parentMenuItem);
if(parentMenu == NULL) {
parentMenu = gtk_menu_new();
gtk_menu_item_set_submenu(parentMenuItem, parentMenu);
}
gtk_menu_shell_append(GTK_MENU_SHELL(parentMenu), menu_item);
}
MenuItemNode* new_item = malloc(sizeof(MenuItemNode));
new_item->menu_id = mii->menu_id;
new_item->signalHandlerId = signalHandlerId;
new_item->menu_item = menu_item;
GList* new_node = malloc(sizeof(GList));
new_node->data = new_item;
new_node->next = global_menu_items;
if(global_menu_items != NULL) {
global_menu_items->prev = new_node;
}
global_menu_items = new_node;
it = new_node;
}
GtkWidget* menu_item = GTK_WIDGET(((MenuItemNode*)(it->data))->menu_item);
gtk_widget_set_sensitive(menu_item, mii->disabled != 1);
gtk_widget_show(menu_item);
free(mii->title);
free(mii->tooltip);
free(mii);
return FALSE;
}
gboolean do_add_separator(gpointer data) {
GtkWidget *separator = gtk_separator_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), separator);
gtk_widget_show(separator);
return FALSE;
}
// runs in main thread, should always return FALSE to prevent gtk to execute it again
gboolean do_hide_menu_item(gpointer data) {
MenuItemInfo *mii = (MenuItemInfo*)data;
GList* it;
for(it = global_menu_items; it != NULL; it = it->next) {
MenuItemNode* item = (MenuItemNode*)(it->data);
if(item->menu_id == mii->menu_id){
gtk_widget_hide(GTK_WIDGET(item->menu_item));
break;
}
}
return FALSE;
}
// runs in main thread, should always return FALSE to prevent gtk to execute it again
gboolean do_show_menu_item(gpointer data) {
MenuItemInfo *mii = (MenuItemInfo*)data;
GList* it;
for(it = global_menu_items; it != NULL; it = it->next) {
MenuItemNode* item = (MenuItemNode*)(it->data);
if(item->menu_id == mii->menu_id){
gtk_widget_show(GTK_WIDGET(item->menu_item));
break;
}
}
return FALSE;
}
// runs in main thread, should always return FALSE to prevent gtk to execute it again
gboolean do_quit(gpointer data) {
_unlink_temp_file();
// app indicator doesn't provide a way to remove it, hide it as a workaround
app_indicator_set_status(global_app_indicator, APP_INDICATOR_STATUS_PASSIVE);
gtk_main_quit();
return FALSE;
}
void setIcon(const char* iconBytes, int length, bool template) {
GBytes* bytes = g_bytes_new_static(iconBytes, length);
g_idle_add(do_set_icon, bytes);
}
void setTitle(char* ctitle) {
app_indicator_set_label(global_app_indicator, ctitle, "");
free(ctitle);
}
void setTooltip(char* ctooltip) {
free(ctooltip);
}
void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) {
}
void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* tooltip, short disabled, short checked, short isCheckable) {
MenuItemInfo *mii = malloc(sizeof(MenuItemInfo));
mii->menu_id = menu_id;
mii->parent_menu_id = parent_menu_id;
mii->title = title;
mii->tooltip = tooltip;
mii->disabled = disabled;
mii->checked = checked;
mii->isCheckable = isCheckable;
g_idle_add(do_add_or_update_menu_item, mii);
}
void add_separator(int menu_id) {
MenuItemInfo *mii = malloc(sizeof(MenuItemInfo));
mii->menu_id = menu_id;
g_idle_add(do_add_separator, mii);
}
void hide_menu_item(int menu_id) {
MenuItemInfo *mii = malloc(sizeof(MenuItemInfo));
mii->menu_id = menu_id;
g_idle_add(do_hide_menu_item, mii);
}
void show_menu_item(int menu_id) {
MenuItemInfo *mii = malloc(sizeof(MenuItemInfo));
mii->menu_id = menu_id;
g_idle_add(do_show_menu_item, mii);
}
void quit() {
g_idle_add(do_quit, NULL);
}

View File

@ -0,0 +1,29 @@
package systray
/*
#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc
#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit
#include "systray.h"
*/
import "C"
// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
// to a regular icon on other platforms.
// templateIconBytes and iconBytes should be the content of .ico for windows and
// .ico/.jpg/.png for other platforms.
func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
SetIcon(regularIconBytes)
}
// SetIcon sets the icon of a menu item. Only works on macOS and Windows.
// iconBytes should be the content of .ico/.jpg/.png
func (item *MenuItem) SetIcon(iconBytes []byte) {
}
// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
// falls back to the regular icon bytes and on Linux it does nothing.
// templateIconBytes and regularIconBytes should be the content of .ico for windows and
// .ico/.jpg/.png for other platforms.
func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
}

View File

@ -0,0 +1,106 @@
// +build !windows
package systray
/*
#cgo linux pkg-config: gtk+-3.0 appindicator3-0.1
#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc
#cgo darwin LDFLAGS: -framework Cocoa
#include "systray.h"
*/
import "C"
import (
"unsafe"
)
func registerSystray() {
C.registerSystray()
}
func nativeLoop() {
C.nativeLoop()
}
func quit() {
C.quit()
}
// SetIcon sets the systray icon.
// iconBytes should be the content of .ico for windows and .ico/.jpg/.png
// for other platforms.
func SetIcon(iconBytes []byte) {
cstr := (*C.char)(unsafe.Pointer(&iconBytes[0]))
C.setIcon(cstr, (C.int)(len(iconBytes)), false)
}
// SetTitle sets the systray title, only available on Mac and Linux.
func SetTitle(title string) {
C.setTitle(C.CString(title))
}
// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
// only available on Mac and Windows.
func SetTooltip(tooltip string) {
C.setTooltip(C.CString(tooltip))
}
func addOrUpdateMenuItem(item *MenuItem) {
var disabled C.short
if item.disabled {
disabled = 1
}
var checked C.short
if item.checked {
checked = 1
}
var isCheckable C.short
if item.isCheckable {
isCheckable = 1
}
var parentID uint32 = 0
if item.parent != nil {
parentID = item.parent.id
}
C.add_or_update_menu_item(
C.int(item.id),
C.int(parentID),
C.CString(item.title),
C.CString(item.tooltip),
disabled,
checked,
isCheckable,
)
}
func addSeparator(id uint32) {
C.add_separator(C.int(id))
}
func hideMenuItem(item *MenuItem) {
C.hide_menu_item(
C.int(item.id),
)
}
func showMenuItem(item *MenuItem) {
C.show_menu_item(
C.int(item.id),
)
}
//export systray_ready
func systray_ready() {
systrayReady()
}
//export systray_on_exit
func systray_on_exit() {
systrayExit()
}
//export systray_menu_item_selected
func systray_menu_item_selected(cID C.int) {
systrayMenuItemSelected(uint32(cID))
}

View File

@ -0,0 +1,939 @@
// +build windows
package systray
import (
"crypto/md5"
"encoding/hex"
"io/ioutil"
"os"
"path/filepath"
"sort"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32
var (
g32 = windows.NewLazySystemDLL("Gdi32.dll")
pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap")
pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC")
pDeleteDC = g32.NewProc("DeleteDC")
pSelectObject = g32.NewProc("SelectObject")
k32 = windows.NewLazySystemDLL("Kernel32.dll")
pGetModuleHandle = k32.NewProc("GetModuleHandleW")
s32 = windows.NewLazySystemDLL("Shell32.dll")
pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW")
u32 = windows.NewLazySystemDLL("User32.dll")
pCreateMenu = u32.NewProc("CreateMenu")
pCreatePopupMenu = u32.NewProc("CreatePopupMenu")
pCreateWindowEx = u32.NewProc("CreateWindowExW")
pDefWindowProc = u32.NewProc("DefWindowProcW")
pDeleteMenu = u32.NewProc("DeleteMenu")
pDestroyWindow = u32.NewProc("DestroyWindow")
pDispatchMessage = u32.NewProc("DispatchMessageW")
pDrawIconEx = u32.NewProc("DrawIconEx")
pGetCursorPos = u32.NewProc("GetCursorPos")
pGetDC = u32.NewProc("GetDC")
pGetMenuItemID = u32.NewProc("GetMenuItemID")
pGetMessage = u32.NewProc("GetMessageW")
pGetSystemMetrics = u32.NewProc("GetSystemMetrics")
pInsertMenuItem = u32.NewProc("InsertMenuItemW")
pLoadCursor = u32.NewProc("LoadCursorW")
pLoadIcon = u32.NewProc("LoadIconW")
pLoadImage = u32.NewProc("LoadImageW")
pPostMessage = u32.NewProc("PostMessageW")
pPostQuitMessage = u32.NewProc("PostQuitMessage")
pRegisterClass = u32.NewProc("RegisterClassExW")
pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW")
pReleaseDC = u32.NewProc("ReleaseDC")
pSetForegroundWindow = u32.NewProc("SetForegroundWindow")
pSetMenuInfo = u32.NewProc("SetMenuInfo")
pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW")
pShowWindow = u32.NewProc("ShowWindow")
pTrackPopupMenu = u32.NewProc("TrackPopupMenu")
pTranslateMessage = u32.NewProc("TranslateMessage")
pUnregisterClass = u32.NewProc("UnregisterClassW")
pUpdateWindow = u32.NewProc("UpdateWindow")
)
// Contains window class information.
// It is used with the RegisterClassEx and GetClassInfoEx functions.
// https://msdn.microsoft.com/en-us/library/ms633577.aspx
type wndClassEx struct {
Size, Style uint32
WndProc uintptr
ClsExtra, WndExtra int32
Instance, Icon, Cursor, Background windows.Handle
MenuName, ClassName *uint16
IconSm windows.Handle
}
// Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function.
// https://msdn.microsoft.com/en-us/library/ms633587.aspx
func (w *wndClassEx) register() error {
w.Size = uint32(unsafe.Sizeof(*w))
res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w)))
if res == 0 {
return err
}
return nil
}
// Unregisters a window class, freeing the memory required for the class.
// https://msdn.microsoft.com/en-us/library/ms644899.aspx
func (w *wndClassEx) unregister() error {
res, _, err := pUnregisterClass.Call(
uintptr(unsafe.Pointer(w.ClassName)),
uintptr(w.Instance),
)
if res == 0 {
return err
}
return nil
}
// Contains information that the system needs to display notifications in the notification area.
// Used by Shell_NotifyIcon.
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159
type notifyIconData struct {
Size uint32
Wnd windows.Handle
ID, Flags, CallbackMessage uint32
Icon windows.Handle
Tip [128]uint16
State, StateMask uint32
Info [256]uint16
Timeout, Version uint32
InfoTitle [64]uint16
InfoFlags uint32
GuidItem windows.GUID
BalloonIcon windows.Handle
}
func (nid *notifyIconData) add() error {
const NIM_ADD = 0x00000000
res, _, err := pShellNotifyIcon.Call(
uintptr(NIM_ADD),
uintptr(unsafe.Pointer(nid)),
)
if res == 0 {
return err
}
return nil
}
func (nid *notifyIconData) modify() error {
const NIM_MODIFY = 0x00000001
res, _, err := pShellNotifyIcon.Call(
uintptr(NIM_MODIFY),
uintptr(unsafe.Pointer(nid)),
)
if res == 0 {
return err
}
return nil
}
func (nid *notifyIconData) delete() error {
const NIM_DELETE = 0x00000002
res, _, err := pShellNotifyIcon.Call(
uintptr(NIM_DELETE),
uintptr(unsafe.Pointer(nid)),
)
if res == 0 {
return err
}
return nil
}
// Contains information about a menu item.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
type menuItemInfo struct {
Size, Mask, Type, State uint32
ID uint32
SubMenu, Checked, Unchecked windows.Handle
ItemData uintptr
TypeData *uint16
Cch uint32
BMPItem windows.Handle
}
// The POINT structure defines the x- and y- coordinates of a point.
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx
type point struct {
X, Y int32
}
// Contains information about loaded resources
type winTray struct {
instance,
icon,
cursor,
window windows.Handle
loadedImages map[string]windows.Handle
muLoadedImages sync.RWMutex
// menus keeps track of the submenus keyed by the menu item ID, plus 0
// which corresponds to the main popup menu.
menus map[uint32]windows.Handle
muMenus sync.RWMutex
// menuOf keeps track of the menu each menu item belongs to.
menuOf map[uint32]windows.Handle
muMenuOf sync.RWMutex
// menuItemIcons maintains the bitmap of each menu item (if applies). It's
// needed to show the icon correctly when showing a previously hidden menu
// item again.
menuItemIcons map[uint32]windows.Handle
muMenuItemIcons sync.RWMutex
visibleItems map[uint32][]uint32
muVisibleItems sync.RWMutex
nid *notifyIconData
muNID sync.RWMutex
wcex *wndClassEx
wmSystrayMessage,
wmTaskbarCreated uint32
}
// Loads an image from file and shows it in tray.
// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
func (t *winTray) setIcon(src string) error {
const NIF_ICON = 0x00000002
h, err := t.loadIconFrom(src)
if err != nil {
return err
}
t.muNID.Lock()
defer t.muNID.Unlock()
t.nid.Icon = h
t.nid.Flags |= NIF_ICON
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
return t.nid.modify()
}
// Sets tooltip on icon.
// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
func (t *winTray) setTooltip(src string) error {
const NIF_TIP = 0x00000004
b, err := windows.UTF16FromString(src)
if err != nil {
return err
}
t.muNID.Lock()
defer t.muNID.Unlock()
copy(t.nid.Tip[:], b[:])
t.nid.Flags |= NIF_TIP
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
return t.nid.modify()
}
var wt winTray
// WindowProc callback function that processes messages sent to a window.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
const (
WM_RBUTTONUP = 0x0205
WM_LBUTTONUP = 0x0202
WM_COMMAND = 0x0111
WM_ENDSESSION = 0x0016
WM_CLOSE = 0x0010
WM_DESTROY = 0x0002
WM_CREATE = 0x0001
)
switch message {
case WM_CREATE:
systrayReady()
case WM_COMMAND:
menuItemId := int32(wParam)
// https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
if menuItemId != -1 {
systrayMenuItemSelected(uint32(wParam))
}
case WM_CLOSE:
pDestroyWindow.Call(uintptr(t.window))
t.wcex.unregister()
case WM_DESTROY:
// same as WM_ENDSESSION, but throws 0 exit code after all
defer pPostQuitMessage.Call(uintptr(int32(0)))
fallthrough
case WM_ENDSESSION:
t.muNID.Lock()
if t.nid != nil {
t.nid.delete()
}
t.muNID.Unlock()
systrayExit()
case t.wmSystrayMessage:
switch lParam {
case WM_RBUTTONUP, WM_LBUTTONUP:
t.showMenu()
}
case t.wmTaskbarCreated: // on explorer.exe restarts
t.muNID.Lock()
t.nid.add()
t.muNID.Unlock()
default:
// Calls the default window procedure to provide default processing for any window messages that an application does not process.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
lResult, _, _ = pDefWindowProc.Call(
uintptr(hWnd),
uintptr(message),
uintptr(wParam),
uintptr(lParam),
)
}
return
}
func (t *winTray) initInstance() error {
const IDI_APPLICATION = 32512
const IDC_ARROW = 32512 // Standard arrow
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx
const SW_HIDE = 0
const CW_USEDEFAULT = 0x80000000
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
const (
WS_CAPTION = 0x00C00000
WS_MAXIMIZEBOX = 0x00010000
WS_MINIMIZEBOX = 0x00020000
WS_OVERLAPPED = 0x00000000
WS_SYSMENU = 0x00080000
WS_THICKFRAME = 0x00040000
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
)
// https://msdn.microsoft.com/en-us/library/windows/desktop/ff729176
const (
CS_HREDRAW = 0x0002
CS_VREDRAW = 0x0001
)
const NIF_MESSAGE = 0x00000001
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx
const WM_USER = 0x0400
const (
className = "SystrayClass"
windowName = ""
)
t.wmSystrayMessage = WM_USER + 1
t.visibleItems = make(map[uint32][]uint32)
t.menus = make(map[uint32]windows.Handle)
t.menuOf = make(map[uint32]windows.Handle)
t.menuItemIcons = make(map[uint32]windows.Handle)
taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated")
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947
res, _, err := pRegisterWindowMessage.Call(
uintptr(unsafe.Pointer(taskbarEventNamePtr)),
)
t.wmTaskbarCreated = uint32(res)
t.loadedImages = make(map[string]windows.Handle)
instanceHandle, _, err := pGetModuleHandle.Call(0)
if instanceHandle == 0 {
return err
}
t.instance = windows.Handle(instanceHandle)
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx
iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION))
if iconHandle == 0 {
return err
}
t.icon = windows.Handle(iconHandle)
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx
cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW))
if cursorHandle == 0 {
return err
}
t.cursor = windows.Handle(cursorHandle)
classNamePtr, err := windows.UTF16PtrFromString(className)
if err != nil {
return err
}
windowNamePtr, err := windows.UTF16PtrFromString(windowName)
if err != nil {
return err
}
t.wcex = &wndClassEx{
Style: CS_HREDRAW | CS_VREDRAW,
WndProc: windows.NewCallback(t.wndProc),
Instance: t.instance,
Icon: t.icon,
Cursor: t.cursor,
Background: windows.Handle(6), // (COLOR_WINDOW + 1)
ClassName: classNamePtr,
IconSm: t.icon,
}
if err := t.wcex.register(); err != nil {
return err
}
windowHandle, _, err := pCreateWindowEx.Call(
uintptr(0),
uintptr(unsafe.Pointer(classNamePtr)),
uintptr(unsafe.Pointer(windowNamePtr)),
uintptr(WS_OVERLAPPEDWINDOW),
uintptr(CW_USEDEFAULT),
uintptr(CW_USEDEFAULT),
uintptr(CW_USEDEFAULT),
uintptr(CW_USEDEFAULT),
uintptr(0),
uintptr(0),
uintptr(t.instance),
uintptr(0),
)
if windowHandle == 0 {
return err
}
t.window = windows.Handle(windowHandle)
pShowWindow.Call(
uintptr(t.window),
uintptr(SW_HIDE),
)
pUpdateWindow.Call(
uintptr(t.window),
)
t.muNID.Lock()
defer t.muNID.Unlock()
t.nid = &notifyIconData{
Wnd: windows.Handle(t.window),
ID: 100,
Flags: NIF_MESSAGE,
CallbackMessage: t.wmSystrayMessage,
}
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
return t.nid.add()
}
func (t *winTray) createMenu() error {
const MIM_APPLYTOSUBMENUS = 0x80000000 // Settings apply to the menu and all of its submenus
menuHandle, _, err := pCreatePopupMenu.Call()
if menuHandle == 0 {
return err
}
t.menus[0] = windows.Handle(menuHandle)
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx
mi := struct {
Size, Mask, Style, Max uint32
Background windows.Handle
ContextHelpID uint32
MenuData uintptr
}{
Mask: MIM_APPLYTOSUBMENUS,
}
mi.Size = uint32(unsafe.Sizeof(mi))
res, _, err := pSetMenuInfo.Call(
uintptr(t.menus[0]),
uintptr(unsafe.Pointer(&mi)),
)
if res == 0 {
return err
}
return nil
}
func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) {
const MIIM_SUBMENU = 0x00000004
res, _, err := pCreateMenu.Call()
if res == 0 {
return 0, err
}
menu := windows.Handle(res)
mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu}
mi.Size = uint32(unsafe.Sizeof(mi))
t.muMenuOf.RLock()
hMenu := t.menuOf[menuItemId]
t.muMenuOf.RUnlock()
res, _, err = pSetMenuItemInfo.Call(
uintptr(hMenu),
uintptr(menuItemId),
0,
uintptr(unsafe.Pointer(&mi)),
)
if res == 0 {
return 0, err
}
t.muMenus.Lock()
t.menus[menuItemId] = menu
t.muMenus.Unlock()
return menu, nil
}
func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
const (
MIIM_FTYPE = 0x00000100
MIIM_BITMAP = 0x00000080
MIIM_STRING = 0x00000040
MIIM_SUBMENU = 0x00000004
MIIM_ID = 0x00000002
MIIM_STATE = 0x00000001
)
const MFT_STRING = 0x00000000
const (
MFS_CHECKED = 0x00000008
MFS_DISABLED = 0x00000003
)
titlePtr, err := windows.UTF16PtrFromString(title)
if err != nil {
return err
}
mi := menuItemInfo{
Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE,
Type: MFT_STRING,
ID: uint32(menuItemId),
TypeData: titlePtr,
Cch: uint32(len(title)),
}
mi.Size = uint32(unsafe.Sizeof(mi))
if disabled {
mi.State |= MFS_DISABLED
}
if checked {
mi.State |= MFS_CHECKED
}
t.muMenuItemIcons.RLock()
hIcon := t.menuItemIcons[menuItemId]
t.muMenuItemIcons.RUnlock()
if hIcon > 0 {
mi.Mask |= MIIM_BITMAP
mi.BMPItem = hIcon
}
var res uintptr
t.muMenus.RLock()
menu, exists := t.menus[parentId]
t.muMenus.RUnlock()
if !exists {
menu, err = t.convertToSubMenu(parentId)
if err != nil {
return err
}
t.muMenus.Lock()
t.menus[parentId] = menu
t.muMenus.Unlock()
} else if t.getVisibleItemIndex(parentId, menuItemId) != -1 {
// We set the menu item info based on the menuID
res, _, err = pSetMenuItemInfo.Call(
uintptr(menu),
uintptr(menuItemId),
0,
uintptr(unsafe.Pointer(&mi)),
)
}
if res == 0 {
t.addToVisibleItems(parentId, menuItemId)
position := t.getVisibleItemIndex(parentId, menuItemId)
res, _, err = pInsertMenuItem.Call(
uintptr(menu),
uintptr(position),
1,
uintptr(unsafe.Pointer(&mi)),
)
if res == 0 {
t.delFromVisibleItems(parentId, menuItemId)
return err
}
t.muMenuOf.Lock()
t.menuOf[menuItemId] = menu
t.muMenuOf.Unlock()
}
return nil
}
func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
const (
MIIM_FTYPE = 0x00000100
MIIM_ID = 0x00000002
MIIM_STATE = 0x00000001
)
const MFT_SEPARATOR = 0x00000800
mi := menuItemInfo{
Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE,
Type: MFT_SEPARATOR,
ID: uint32(menuItemId),
}
mi.Size = uint32(unsafe.Sizeof(mi))
t.addToVisibleItems(parentId, menuItemId)
position := t.getVisibleItemIndex(parentId, menuItemId)
t.muMenus.RLock()
menu := uintptr(t.menus[parentId])
t.muMenus.RUnlock()
res, _, err := pInsertMenuItem.Call(
menu,
uintptr(position),
1,
uintptr(unsafe.Pointer(&mi)),
)
if res == 0 {
return err
}
return nil
}
func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647629(v=vs.85).aspx
const MF_BYCOMMAND = 0x00000000
const ERROR_SUCCESS syscall.Errno = 0
t.muMenus.RLock()
menu := uintptr(t.menus[parentId])
t.muMenus.RUnlock()
res, _, err := pDeleteMenu.Call(
menu,
uintptr(menuItemId),
MF_BYCOMMAND,
)
if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS {
return err
}
t.delFromVisibleItems(parentId, menuItemId)
return nil
}
func (t *winTray) showMenu() error {
const (
TPM_BOTTOMALIGN = 0x0020
TPM_LEFTALIGN = 0x0000
)
p := point{}
res, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p)))
if res == 0 {
return err
}
pSetForegroundWindow.Call(uintptr(t.window))
res, _, err = pTrackPopupMenu.Call(
uintptr(t.menus[0]),
TPM_BOTTOMALIGN|TPM_LEFTALIGN,
uintptr(p.X),
uintptr(p.Y),
0,
uintptr(t.window),
0,
)
if res == 0 {
return err
}
return nil
}
func (t *winTray) delFromVisibleItems(parent, val uint32) {
t.muVisibleItems.Lock()
defer t.muVisibleItems.Unlock()
visibleItems := t.visibleItems[parent]
for i, itemval := range visibleItems {
if val == itemval {
visibleItems = append(visibleItems[:i], visibleItems[i+1:]...)
break
}
}
}
func (t *winTray) addToVisibleItems(parent, val uint32) {
t.muVisibleItems.Lock()
defer t.muVisibleItems.Unlock()
if visibleItems, exists := t.visibleItems[parent]; !exists {
t.visibleItems[parent] = []uint32{val}
} else {
newvisible := append(visibleItems, val)
sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
t.visibleItems[parent] = newvisible
}
}
func (t *winTray) getVisibleItemIndex(parent, val uint32) int {
t.muVisibleItems.RLock()
defer t.muVisibleItems.RUnlock()
for i, itemval := range t.visibleItems[parent] {
if val == itemval {
return i
}
}
return -1
}
// Loads an image from file to be shown in tray or menu item.
// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
func (t *winTray) loadIconFrom(src string) (windows.Handle, error) {
const IMAGE_ICON = 1 // Loads an icon
const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file
const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero
// Save and reuse handles of loaded images
t.muLoadedImages.RLock()
h, ok := t.loadedImages[src]
t.muLoadedImages.RUnlock()
if !ok {
srcPtr, err := windows.UTF16PtrFromString(src)
if err != nil {
return 0, err
}
res, _, err := pLoadImage.Call(
0,
uintptr(unsafe.Pointer(srcPtr)),
IMAGE_ICON,
0,
0,
LR_LOADFROMFILE|LR_DEFAULTSIZE,
)
if res == 0 {
return 0, err
}
h = windows.Handle(res)
t.muLoadedImages.Lock()
t.loadedImages[src] = h
t.muLoadedImages.Unlock()
}
return h, nil
}
func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) {
const SM_CXSMICON = 49
const SM_CYSMICON = 50
const DI_NORMAL = 0x3
hDC, _, err := pGetDC.Call(uintptr(0))
if hDC == 0 {
return 0, err
}
defer pReleaseDC.Call(uintptr(0), hDC)
hMemDC, _, err := pCreateCompatibleDC.Call(hDC)
if hMemDC == 0 {
return 0, err
}
defer pDeleteDC.Call(hMemDC)
cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON)
cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON)
hMemBmp, _, err := pCreateCompatibleBitmap.Call(hDC, cx, cy)
if hMemBmp == 0 {
return 0, err
}
hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp)
defer pSelectObject.Call(hMemDC, hOriginalBmp)
res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL)
if res == 0 {
return 0, err
}
return windows.Handle(hMemBmp), nil
}
func registerSystray() {
if err := wt.initInstance(); err != nil {
log.Errorf("Unable to init instance: %v", err)
return
}
if err := wt.createMenu(); err != nil {
log.Errorf("Unable to create menu: %v", err)
return
}
}
func nativeLoop() {
// Main message pump.
m := &struct {
WindowHandle windows.Handle
Message uint32
Wparam uintptr
Lparam uintptr
Time uint32
Pt point
}{}
for {
ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
// If the function retrieves a message other than WM_QUIT, the return value is nonzero.
// If the function retrieves the WM_QUIT message, the return value is zero.
// If there is an error, the return value is -1
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
switch int32(ret) {
case -1:
log.Errorf("Error at message loop: %v", err)
return
case 0:
return
default:
pTranslateMessage.Call(uintptr(unsafe.Pointer(m)))
pDispatchMessage.Call(uintptr(unsafe.Pointer(m)))
}
}
}
func quit() {
const WM_CLOSE = 0x0010
pPostMessage.Call(
uintptr(wt.window),
WM_CLOSE,
0,
0,
)
}
func iconBytesToFilePath(iconBytes []byte) (string, error) {
bh := md5.Sum(iconBytes)
dataHash := hex.EncodeToString(bh[:])
iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash)
if _, err := os.Stat(iconFilePath); os.IsNotExist(err) {
if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil {
return "", err
}
}
return iconFilePath, nil
}
// SetIcon sets the systray icon.
// iconBytes should be the content of .ico for windows and .ico/.jpg/.png
// for other platforms.
func SetIcon(iconBytes []byte) {
iconFilePath, err := iconBytesToFilePath(iconBytes)
if err != nil {
log.Errorf("Unable to write icon data to temp file: %v", err)
return
}
if err := wt.setIcon(iconFilePath); err != nil {
log.Errorf("Unable to set icon: %v", err)
return
}
}
// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
// to a regular icon on other platforms.
// templateIconBytes and iconBytes should be the content of .ico for windows and
// .ico/.jpg/.png for other platforms.
func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
SetIcon(regularIconBytes)
}
// SetTitle sets the systray title, only available on Mac and Linux.
func SetTitle(title string) {
// do nothing
}
func (item *MenuItem) parentId() uint32 {
if item.parent != nil {
return uint32(item.parent.id)
}
return 0
}
// SetIcon sets the icon of a menu item. Only works on macOS and Windows.
// iconBytes should be the content of .ico/.jpg/.png
func (item *MenuItem) SetIcon(iconBytes []byte) {
iconFilePath, err := iconBytesToFilePath(iconBytes)
if err != nil {
log.Errorf("Unable to write icon data to temp file: %v", err)
return
}
h, err := wt.loadIconFrom(iconFilePath)
if err != nil {
log.Errorf("Unable to load icon from temp file: %v", err)
return
}
h, err = wt.iconToBitmap(h)
if err != nil {
log.Errorf("Unable to convert icon to bitmap: %v", err)
return
}
wt.muMenuItemIcons.Lock()
wt.menuItemIcons[uint32(item.id)] = h
wt.muMenuItemIcons.Unlock()
err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked)
if err != nil {
log.Errorf("Unable to addOrUpdateMenuItem: %v", err)
return
}
}
// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
// only available on Mac and Windows.
func SetTooltip(tooltip string) {
if err := wt.setTooltip(tooltip); err != nil {
log.Errorf("Unable to set tooltip: %v", err)
return
}
}
func addOrUpdateMenuItem(item *MenuItem) {
err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked)
if err != nil {
log.Errorf("Unable to addOrUpdateMenuItem: %v", err)
return
}
}
// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
// falls back to the regular icon bytes and on Linux it does nothing.
// templateIconBytes and regularIconBytes should be the content of .ico for windows and
// .ico/.jpg/.png for other platforms.
func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
item.SetIcon(regularIconBytes)
}
func addSeparator(id uint32) {
err := wt.addSeparatorMenuItem(id, 0)
if err != nil {
log.Errorf("Unable to addSeparator: %v", err)
return
}
}
func hideMenuItem(item *MenuItem) {
err := wt.hideMenuItem(uint32(item.id), item.parentId())
if err != nil {
log.Errorf("Unable to hideMenuItem: %v", err)
return
}
}
func showMenuItem(item *MenuItem) {
addOrUpdateMenuItem(item)
}

View File

@ -0,0 +1,132 @@
// +build windows
package systray
import (
"io/ioutil"
"runtime"
"sync/atomic"
"testing"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
const iconFilePath = "example/icon/iconwin.ico"
func TestBaseWindowsTray(t *testing.T) {
systrayReady = func() {}
systrayExit = func() {}
runtime.LockOSThread()
if err := wt.initInstance(); err != nil {
t.Fatalf("initInstance failed: %s", err)
}
if err := wt.createMenu(); err != nil {
t.Fatalf("createMenu failed: %s", err)
}
defer func() {
pDestroyWindow.Call(uintptr(wt.window))
wt.wcex.unregister()
}()
if err := wt.setIcon(iconFilePath); err != nil {
t.Errorf("SetIcon failed: %s", err)
}
if err := wt.setTooltip("Cyrillic tooltip тест:)"); err != nil {
t.Errorf("SetIcon failed: %s", err)
}
var id int32 = 0
err := wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple enabled", false, false)
if err != nil {
t.Errorf("mergeMenuItem failed: %s", err)
}
err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple disabled", true, false)
if err != nil {
t.Errorf("mergeMenuItem failed: %s", err)
}
err = wt.addSeparatorMenuItem(atomic.AddInt32(&id, 1))
if err != nil {
t.Errorf("addSeparatorMenuItem failed: %s", err)
}
err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple checked enabled", false, true)
if err != nil {
t.Errorf("mergeMenuItem failed: %s", err)
}
err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple checked disabled", true, true)
if err != nil {
t.Errorf("mergeMenuItem failed: %s", err)
}
err = wt.hideMenuItem(1)
if err != nil {
t.Errorf("hideMenuItem failed: %s", err)
}
err = wt.hideMenuItem(100)
if err == nil {
t.Error("hideMenuItem failed: must return error on invalid item id")
}
err = wt.addOrUpdateMenuItem(2, "Simple disabled update", true, false)
if err != nil {
t.Errorf("mergeMenuItem failed: %s", err)
}
time.AfterFunc(1*time.Second, quit)
m := struct {
WindowHandle windows.Handle
Message uint32
Wparam uintptr
Lparam uintptr
Time uint32
Pt point
}{}
for {
ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
res := int32(ret)
if res == -1 {
t.Errorf("win32 GetMessage failed: %v", err)
return
} else if res == 0 {
break
}
pTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
pDispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
}
}
func TestWindowsRun(t *testing.T) {
onReady := func() {
b, err := ioutil.ReadFile(iconFilePath)
if err != nil {
t.Fatalf("Can't load icon file: %v", err)
}
SetIcon(b)
SetTitle("Test title с кириллицей")
bSomeBtn := AddMenuItem("Йа кнопко", "")
bSomeBtn.Check()
AddSeparator()
bQuit := AddMenuItem("Quit", "Quit the whole app")
go func() {
<-bQuit.ClickedCh
t.Log("Quit reqested")
Quit()
}()
time.AfterFunc(1*time.Second, Quit)
}
onExit := func() {
t.Log("Exit success")
}
Run(onReady, onExit)
}