├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── appveyor.yml ├── ci └── before_install.sh ├── examples └── systray-example.rs ├── resources └── rust.ico └── src ├── api ├── cocoa │ └── mod.rs ├── linux │ └── mod.rs ├── mod.rs └── win32 │ └── mod.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 2 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 3 | Cargo.lock 4 | /target/ 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | 8 | matrix: 9 | allow_failures: 10 | - rust: nightly 11 | 12 | addons: 13 | apt: 14 | sources: 15 | - ubuntu-toolchain-r-test 16 | packages: 17 | - build-essential 18 | - libgtk-3-dev 19 | - libappindicator3-dev 20 | - gcc-5 21 | 22 | before_install: . ./ci/before_install.sh 23 | 24 | script: 25 | - RUST_BACKTRACE=1 PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH cargo build --verbose 26 | - RUST_BACKTRACE=1 PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH cargo test --verbose 27 | 28 | global_env: 29 | secure: O40C4FadE2C8yApgCbQNYmeWQuytrhu4W3a2HKRvGgB39LP0ysMU2UKXQIyZqlZUS9mP9qi5HYN+GTt83aE3Ac0eAwRqq+9zMjC2qMaiZ1JBSfCJI5wiiIXP0HpbsxXipG2Z21aqupVfu0HjNP4RVkaZ7ONKAeLAieI06+7VHbMPw6mcJd4Drv8VTyKn89VvB4lxKexLcURfagoic3fzeFKaIIVBSqGHiXrURbpD5tffOnzc5YFWxeGKTVFl8WqQVrRk2gnl/39UhSsOHGuSExw5GSxh+OaNHTiAkvOaSQLa05Y5mkNlHAsMyqg1mW3mI2xuzCQaFFT5G5JF7uxvZsa4GfROaEG8r1CZvpWxG2NtpupXvIC25nN+QQeeMZv5PHaxlk9OkG0k+2+z1Tu0Yd05x/o3+52YFo3geVwDmI3zx4Zgg9u9nIwGhdtzqbKV2fQNnKbNWVQH6D5M1DlBMYyY25jpkehcazqUbLsJXJFIoMkXhdkjTIpZg4w+CQ617WCnoDhXh6+Iqkw+iBBJJugaf2D6qBpXNiLZNJwbv2M5fj8uDsDtsUvjg56qBw+g+TeHJDKjzpEId/zFrAe4lmuFjN4/SlDk3n5xjZ5eY4PGRp1K8DGgeBQI5gyvHR3H7lm4GE2NCEvNILYFjpANZsiWwDepb2/rHvYNiLK+jhc= 30 | 31 | env: 32 | - LLVM_VERSION=3.9 CLANG_VERSION=clang_3_9 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.0 (2020-02-15) 2 | 3 | ## Features 4 | 5 | - Brought up to Rust 2018 (thanks to https://github.com/udoprog) 6 | 7 | ## Bugfixes 8 | 9 | - Updated libappindicator to compile on modern rust 10 | 11 | # 0.3.0 (2018-04-28) 12 | 13 | ## Bugfixes 14 | 15 | - Update gtk so linux version will run again 16 | 17 | # 0.2.0 (2017-05-04) 18 | 19 | ## Features 20 | 21 | - Add Linux Support 22 | 23 | # 0.1.1 (2017-02-28) 24 | 25 | ## Bugfixes 26 | 27 | - Some cleanup and CI work 28 | 29 | # 0.1.0 (2017-02-22) 30 | 31 | ## Features 32 | 33 | - Basic Win32 systray support 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "systray" 3 | version = "0.4.0" 4 | authors = ["Kyle Machulis "] 5 | description = "Rust library for making minimal cross-platform systray GUIs" 6 | license = "BSD-3-Clause" 7 | homepage = "http://github.com/qdot/systray-rs" 8 | repository = "https://github.com/qdot/systray-rs.git" 9 | readme = "README.md" 10 | keywords = ["gui"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | log= "0.4.8" 15 | 16 | [target.'cfg(target_os = "windows")'.dependencies] 17 | winapi= { version = "0.3.8", features = ["shellapi", "libloaderapi", "errhandlingapi", "impl-default"] } 18 | libc= "0.2.66" 19 | 20 | [target.'cfg(target_os = "linux")'.dependencies] 21 | gtk= "0.8.1" 22 | glib= "0.9.3" 23 | libappindicator= "0.5.1" 24 | 25 | # [target.'cfg(target_os = "macos")'.dependencies] 26 | # objc="*" 27 | # cocoa="*" 28 | # core-foundation="*" 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Kyle Machulis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the project nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SYSTRAY-RS IS DEPRECATED AND ARCHIVED 2 | 3 | systray-rs is now deprecated. I had some hopes about working on this, but I really am just not finding any intersection between this project and other projects I'm maintaining, so I think it's best to call it done then having everyone hold out for my updates that will most likely never arrive. 4 | 5 | To anyone who wants to fork: You can, but I would really recommend against it. Use this repo as reference if you want to, but most of this code was written in 2016, when I was very new to Rust. It doesn't handle cross-platform GUI needs well at all. 6 | 7 | If you're going to build your own version of this: Great! If/when you finish it, get in touch and I'll add a link here. But a word of advice: You really do not want to tackle this from the perspective of "Oh I'll do it on [insert prefered OS here] then other people will contribute other platforms". You need to plan for Win/macOS/Linux at the same time or you will end up with an unworkable mess. There are systray implementations in other languages (some of which are listed in the issues here), I'd definitely recommend cribbing from those, much as I tried to crib this one from Go. 8 | 9 | I'm going to leave the issues and PRs up since they may also contain relevant reference info for whoever decides to take this on next. 10 | 11 | Good luck. 12 | 13 | # systray-rs 14 | 15 | [![Crates.io](https://img.shields.io/crates/v/systray)](https://crates.io/crates/systray) [![Crates.io](https://img.shields.io/crates/d/systray)](https://crates.io/crates/systray) 16 | 17 | [![Build Status](https://travis-ci.org/qdot/systray-rs.svg?branch=master)](https://travis-ci.org/qdot/systray-rs) [![Build status](https://ci.appveyor.com/api/projects/status/lhqm3lucb5w5559b?svg=true)](https://ci.appveyor.com/project/qdot/systray-rs) 18 | 19 | systray-rs is a Rust library that makes it easy for applications to 20 | have minimal UI in a platform specific way. It wraps the platform 21 | specific calls required to show an icon in the system tray, as well as 22 | add menu entries. 23 | 24 | systray-rs is heavily influenced by 25 | [the systray library for the Go Language](https://github.com/getlantern/systray). 26 | 27 | systray-rs currently supports: 28 | 29 | - Linux GTK 30 | - Win32 31 | 32 | Cocoa core still needed! 33 | 34 | # License 35 | 36 | systray-rs includes some code 37 | from [winapi-rs, by retep998](https://github.com/retep998/winapi-rs). 38 | This code is covered under the MIT license. This code will be removed 39 | once winapi-rs has a 0.3 crate available. 40 | 41 | systray-rs is BSD licensed. 42 | 43 | Copyright (c) 2016-2020, Nonpolynomial Labs, LLC 44 | All rights reserved. 45 | 46 | Redistribution and use in source and binary forms, with or without 47 | modification, are permitted provided that the following conditions are met: 48 | 49 | * Redistributions of source code must retain the above copyright notice, this 50 | list of conditions and the following disclaimer. 51 | 52 | * Redistributions in binary form must reproduce the above copyright notice, 53 | this list of conditions and the following disclaimer in the documentation 54 | and/or other materials provided with the distribution. 55 | 56 | * Neither the name of the project nor the names of its 57 | contributors may be used to endorse or promote products derived from 58 | this software without specific prior written permission. 59 | 60 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 61 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 62 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 63 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 64 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 65 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 66 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 67 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 68 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 69 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 70 | 71 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "0.1.0.{build}" 2 | environment: 3 | matrix: 4 | - TARGET: nightly-x86_64-pc-windows-msvc 5 | - TARGET: nightly-i686-pc-windows-msvc 6 | - TARGET: nightly-x86_64-pc-windows-gnu 7 | - TARGET: nightly-i686-pc-windows-gnu 8 | install: 9 | - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:TARGET}.exe" -FileName "rust-install.exe" 10 | - ps: .\rust-install.exe /VERYSILENT /NORESTART /DIR="C:\rust" | Out-Null 11 | - ps: $env:PATH="$env:PATH;C:\rust\bin" 12 | - rustc -vV 13 | - cargo -vV 14 | - erase rust-install.exe 15 | build_script: 16 | - cargo build 17 | # Skip packaging step while we're running off a local winapi build 18 | #- cargo package 19 | skip_commits: 20 | files: 21 | - README.md 22 | - .travis.yml 23 | -------------------------------------------------------------------------------- /ci/before_install.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | pushd ~ 3 | 4 | # Workaround for Travis CI macOS bug (https://github.com/travis-ci/travis-ci/issues/6307) 5 | if [ "${TRAVIS_OS_NAME}" == "osx" ]; then 6 | rvm get head || true 7 | fi 8 | 9 | function llvm_version_triple() { 10 | if [ "$1" == "3.8" ]; then 11 | echo "3.8.0" 12 | elif [ "$1" == "3.9" ]; then 13 | echo "3.9.0" 14 | fi 15 | } 16 | 17 | function llvm_download() { 18 | export LLVM_VERSION_TRIPLE=`llvm_version_triple ${LLVM_VERSION}` 19 | export LLVM=clang+llvm-${LLVM_VERSION_TRIPLE}-x86_64-$1 20 | 21 | wget http://llvm.org/releases/${LLVM_VERSION_TRIPLE}/${LLVM}.tar.xz 22 | mkdir llvm 23 | tar -xf ${LLVM}.tar.xz -C llvm --strip-components=1 24 | 25 | export LLVM_CONFIG_PATH=`pwd`/llvm/bin/llvm-config 26 | if [ "${TRAVIS_OS_NAME}" == "osx" ]; then 27 | cp llvm/lib/libclang.dylib /usr/local/lib/libclang.dylib 28 | fi 29 | } 30 | 31 | 32 | if [ "${TRAVIS_OS_NAME}" == "linux" ]; then 33 | llvm_download linux-gnu-ubuntu-14.04 34 | else 35 | llvm_download apple-darwin 36 | fi 37 | 38 | popd 39 | set +e 40 | -------------------------------------------------------------------------------- /examples/systray-example.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | 3 | //#[cfg(target_os = "windows")] 4 | fn main() -> Result<(), systray::Error> { 5 | let mut app; 6 | match systray::Application::new() { 7 | Ok(w) => app = w, 8 | Err(_) => panic!("Can't create window!"), 9 | } 10 | // w.set_icon_from_file(&"C:\\Users\\qdot\\code\\git-projects\\systray-rs\\resources\\rust.ico".to_string()); 11 | // w.set_tooltip(&"Whatever".to_string()); 12 | app.set_icon_from_file("/usr/share/gxkb/flags/ua.png")?; 13 | 14 | app.add_menu_item("Print a thing", |_| { 15 | println!("Printing a thing!"); 16 | Ok::<_, systray::Error>(()) 17 | })?; 18 | 19 | app.add_menu_item("Add Menu Item", |window| { 20 | window.add_menu_item("Interior item", |_| { 21 | println!("what"); 22 | Ok::<_, systray::Error>(()) 23 | })?; 24 | window.add_menu_separator()?; 25 | Ok::<_, systray::Error>(()) 26 | })?; 27 | 28 | app.add_menu_separator()?; 29 | 30 | app.add_menu_item("Quit", |window| { 31 | window.quit(); 32 | Ok::<_, systray::Error>(()) 33 | })?; 34 | 35 | println!("Waiting on message!"); 36 | app.wait_for_message()?; 37 | Ok(()) 38 | } 39 | 40 | // #[cfg(not(target_os = "windows"))] 41 | // fn main() { 42 | // panic!("Not implemented on this platform!"); 43 | // } 44 | -------------------------------------------------------------------------------- /resources/rust.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qdot/systray-rs/6d2d0a0b0a48d74fb9adeb35c638b0a949df79dd/resources/rust.ico -------------------------------------------------------------------------------- /src/api/cocoa/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use std; 3 | 4 | pub struct Window {} 5 | 6 | impl Window { 7 | pub fn new() -> Result { 8 | Err(Error::NotImplementedError) 9 | } 10 | pub fn quit(&self) { 11 | unimplemented!() 12 | } 13 | pub fn set_tooltip(&self, _: &str) -> Result<(), Error> { 14 | unimplemented!() 15 | } 16 | pub fn add_menu_item(&self, _: &str, _: F) -> Result 17 | where 18 | F: std::ops::Fn(&Window) -> () + 'static, 19 | { 20 | unimplemented!() 21 | } 22 | pub fn wait_for_message(&mut self) { 23 | unimplemented!() 24 | } 25 | pub fn set_icon_from_buffer(&self, _: &[u8], _: u32, _: u32) -> Result<(), Error> { 26 | unimplemented!() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/api/linux/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, SystrayEvent}; 2 | use glib; 3 | use gtk::{ 4 | self, MenuShellExt, GtkMenuItemExt, WidgetExt 5 | }; 6 | use libappindicator::{AppIndicator, AppIndicatorStatus}; 7 | use std::{ 8 | self, 9 | cell::RefCell, 10 | collections::HashMap, 11 | sync::mpsc::{channel, Sender}, 12 | thread, 13 | }; 14 | 15 | // Gtk specific struct that will live only in the Gtk thread, since a lot of the 16 | // base types involved don't implement Send (for good reason). 17 | pub struct GtkSystrayApp { 18 | menu: gtk::Menu, 19 | ai: RefCell, 20 | menu_items: RefCell>, 21 | event_tx: Sender, 22 | } 23 | 24 | thread_local!(static GTK_STASH: RefCell> = RefCell::new(None)); 25 | 26 | pub struct MenuItemInfo { 27 | mid: u32, 28 | title: String, 29 | tooltip: String, 30 | disabled: bool, 31 | checked: bool, 32 | } 33 | 34 | type Callback = Box<(Fn(&GtkSystrayApp) -> () + 'static)>; 35 | 36 | // Convenience function to clean up thread local unwrapping 37 | fn run_on_gtk_thread(f: F) 38 | where 39 | F: std::ops::Fn(&GtkSystrayApp) -> () + Send + 'static, 40 | { 41 | // Note this is glib, not gtk. Calling gtk::idle_add will panic us due to 42 | // being on different threads. glib::idle_add can run across threads. 43 | glib::idle_add(move || { 44 | GTK_STASH.with(|stash| { 45 | let stash = stash.borrow(); 46 | let stash = stash.as_ref(); 47 | if let Some(stash) = stash { 48 | f(stash); 49 | } 50 | }); 51 | gtk::prelude::Continue(false) 52 | }); 53 | } 54 | 55 | impl GtkSystrayApp { 56 | pub fn new(event_tx: Sender) -> Result { 57 | if let Err(e) = gtk::init() { 58 | return Err(Error::OsError(format!("{}", "Gtk init error!"))); 59 | } 60 | let mut m = gtk::Menu::new(); 61 | let mut ai = AppIndicator::new("", ""); 62 | ai.set_status(AppIndicatorStatus::Active); 63 | ai.set_menu(&mut m); 64 | Ok(GtkSystrayApp { 65 | menu: m, 66 | ai: RefCell::new(ai), 67 | menu_items: RefCell::new(HashMap::new()), 68 | event_tx: event_tx, 69 | }) 70 | } 71 | 72 | pub fn systray_menu_selected(&self, menu_id: u32) { 73 | self.event_tx 74 | .send(SystrayEvent { 75 | menu_index: menu_id as u32, 76 | }) 77 | .ok(); 78 | } 79 | 80 | pub fn add_menu_separator(&self, item_idx: u32) { 81 | //let mut menu_items = self.menu_items.borrow_mut(); 82 | let m = gtk::SeparatorMenuItem::new(); 83 | self.menu.append(&m); 84 | //menu_items.insert(item_idx, m); 85 | self.menu.show_all(); 86 | } 87 | 88 | pub fn add_menu_entry(&self, item_idx: u32, item_name: &str) { 89 | let mut menu_items = self.menu_items.borrow_mut(); 90 | if menu_items.contains_key(&item_idx) { 91 | let m: >k::MenuItem = menu_items.get(&item_idx).unwrap(); 92 | m.set_label(item_name); 93 | self.menu.show_all(); 94 | return; 95 | } 96 | let m = gtk::MenuItem::new_with_label(item_name); 97 | self.menu.append(&m); 98 | m.connect_activate(move |_| { 99 | run_on_gtk_thread(move |stash: &GtkSystrayApp| { 100 | stash.systray_menu_selected(item_idx); 101 | }); 102 | }); 103 | menu_items.insert(item_idx, m); 104 | self.menu.show_all(); 105 | } 106 | 107 | pub fn set_icon_from_file(&self, file: &str) { 108 | let mut ai = self.ai.borrow_mut(); 109 | ai.set_icon_full(file, "icon"); 110 | } 111 | } 112 | 113 | pub struct Window { 114 | gtk_loop: Option>, 115 | } 116 | 117 | impl Window { 118 | pub fn new(event_tx: Sender) -> Result { 119 | let (tx, rx) = channel(); 120 | let gtk_loop = thread::spawn(move || { 121 | GTK_STASH.with(|stash| match GtkSystrayApp::new(event_tx) { 122 | Ok(data) => { 123 | (*stash.borrow_mut()) = Some(data); 124 | tx.send(Ok(())); 125 | } 126 | Err(e) => { 127 | tx.send(Err(e)); 128 | return; 129 | } 130 | }); 131 | gtk::main(); 132 | }); 133 | match rx.recv().unwrap() { 134 | Ok(()) => Ok(Window { 135 | gtk_loop: Some(gtk_loop), 136 | }), 137 | Err(e) => Err(e), 138 | } 139 | } 140 | 141 | pub fn add_menu_entry(&self, item_idx: u32, item_name: &str) -> Result<(), Error> { 142 | let n = item_name.to_owned().clone(); 143 | run_on_gtk_thread(move |stash: &GtkSystrayApp| { 144 | stash.add_menu_entry(item_idx, &n); 145 | }); 146 | Ok(()) 147 | } 148 | 149 | pub fn add_menu_separator(&self, item_idx: u32) -> Result<(), Error> { 150 | run_on_gtk_thread(move |stash: &GtkSystrayApp| { 151 | stash.add_menu_separator(item_idx); 152 | }); 153 | Ok(()) 154 | } 155 | 156 | pub fn set_icon_from_file(&self, file: &str) -> Result<(), Error> { 157 | let n = file.to_owned().clone(); 158 | run_on_gtk_thread(move |stash: &GtkSystrayApp| { 159 | stash.set_icon_from_file(&n); 160 | }); 161 | Ok(()) 162 | } 163 | 164 | pub fn set_icon_from_resource(&self, resource: &str) -> Result<(), Error> { 165 | panic!("Not implemented on this platform!"); 166 | } 167 | 168 | pub fn shutdown(&self) -> Result<(), Error> { 169 | Ok(()) 170 | } 171 | 172 | pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { 173 | panic!("Not implemented on this platform!"); 174 | } 175 | 176 | pub fn quit(&self) { 177 | glib::idle_add(|| { 178 | gtk::main_quit(); 179 | glib::Continue(false) 180 | }); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "windows")] 2 | #[path = "win32/mod.rs"] 3 | pub mod api; 4 | 5 | #[cfg(target_os = "linux")] 6 | #[path = "linux/mod.rs"] 7 | pub mod api; 8 | 9 | #[cfg(target_os = "macos")] 10 | #[path = "cocoa/mod.rs"] 11 | pub mod api; 12 | -------------------------------------------------------------------------------- /src/api/win32/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, SystrayEvent}; 2 | use std; 3 | use std::cell::RefCell; 4 | use std::ffi::OsStr; 5 | use std::os::windows::ffi::OsStrExt; 6 | use std::sync::mpsc::{channel, Sender}; 7 | use std::thread; 8 | use winapi::{ 9 | ctypes::{c_ulong, c_ushort}, 10 | shared::{ 11 | basetsd::ULONG_PTR, 12 | guiddef::GUID, 13 | minwindef::{DWORD, HINSTANCE, LPARAM, LRESULT, PBYTE, TRUE, UINT, WPARAM}, 14 | ntdef::LPCWSTR, 15 | windef::{HBITMAP, HBRUSH, HICON, HMENU, HWND, POINT}, 16 | }, 17 | um::{ 18 | errhandlingapi, libloaderapi, 19 | shellapi::{ 20 | self, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, 21 | }, 22 | winuser::{ 23 | self, CW_USEDEFAULT, IMAGE_ICON, LR_DEFAULTCOLOR, LR_LOADFROMFILE, MENUINFO, 24 | MENUITEMINFOW, MFT_SEPARATOR, MFT_STRING, MIIM_FTYPE, MIIM_ID, MIIM_STATE, MIIM_STRING, 25 | MIM_APPLYTOSUBMENUS, MIM_STYLE, MNS_NOTIFYBYPOS, WM_DESTROY, WM_USER, WNDCLASSW, 26 | WS_OVERLAPPEDWINDOW, 27 | }, 28 | }, 29 | }; 30 | 31 | // Got this idea from glutin. Yay open source! Boo stupid winproc! Even more boo 32 | // doing SetLongPtr tho. 33 | thread_local!(static WININFO_STASH: RefCell> = RefCell::new(None)); 34 | 35 | fn to_wstring(str: &str) -> Vec { 36 | OsStr::new(str) 37 | .encode_wide() 38 | .chain(Some(0).into_iter()) 39 | .collect::>() 40 | } 41 | 42 | #[derive(Clone)] 43 | struct WindowInfo { 44 | pub hwnd: HWND, 45 | pub hinstance: HINSTANCE, 46 | pub hmenu: HMENU, 47 | } 48 | 49 | unsafe impl Send for WindowInfo {} 50 | unsafe impl Sync for WindowInfo {} 51 | 52 | #[derive(Clone)] 53 | struct WindowsLoopData { 54 | pub info: WindowInfo, 55 | pub tx: Sender, 56 | } 57 | 58 | unsafe fn get_win_os_error(msg: &str) -> Error { 59 | Error::OsError(format!("{}: {}", &msg, errhandlingapi::GetLastError())) 60 | } 61 | 62 | unsafe extern "system" fn window_proc( 63 | h_wnd: HWND, 64 | msg: UINT, 65 | w_param: WPARAM, 66 | l_param: LPARAM, 67 | ) -> LRESULT { 68 | if msg == winuser::WM_MENUCOMMAND { 69 | WININFO_STASH.with(|stash| { 70 | let stash = stash.borrow(); 71 | let stash = stash.as_ref(); 72 | if let Some(stash) = stash { 73 | let menu_id = winuser::GetMenuItemID(stash.info.hmenu, w_param as i32) as i32; 74 | if menu_id != -1 { 75 | stash 76 | .tx 77 | .send(SystrayEvent { 78 | menu_index: menu_id as u32, 79 | }) 80 | .ok(); 81 | } 82 | } 83 | }); 84 | } 85 | 86 | if msg == WM_USER + 1 { 87 | if l_param as UINT == winuser::WM_LBUTTONUP || l_param as UINT == winuser::WM_RBUTTONUP { 88 | let mut p = POINT { x: 0, y: 0 }; 89 | if winuser::GetCursorPos(&mut p as *mut POINT) == 0 { 90 | return 1; 91 | } 92 | winuser::SetForegroundWindow(h_wnd); 93 | WININFO_STASH.with(|stash| { 94 | let stash = stash.borrow(); 95 | let stash = stash.as_ref(); 96 | if let Some(stash) = stash { 97 | winuser::TrackPopupMenu( 98 | stash.info.hmenu, 99 | 0, 100 | p.x, 101 | p.y, 102 | (winuser::TPM_BOTTOMALIGN | winuser::TPM_LEFTALIGN) as i32, 103 | h_wnd, 104 | std::ptr::null_mut(), 105 | ); 106 | } 107 | }); 108 | } 109 | } 110 | if msg == winuser::WM_DESTROY { 111 | winuser::PostQuitMessage(0); 112 | } 113 | return winuser::DefWindowProcW(h_wnd, msg, w_param, l_param); 114 | } 115 | 116 | fn get_nid_struct(hwnd: &HWND) -> NOTIFYICONDATAW { 117 | NOTIFYICONDATAW { 118 | cbSize: std::mem::size_of::() as DWORD, 119 | hWnd: *hwnd, 120 | uID: 0x1 as UINT, 121 | uFlags: 0 as UINT, 122 | uCallbackMessage: 0 as UINT, 123 | hIcon: 0 as HICON, 124 | szTip: [0 as u16; 128], 125 | dwState: 0 as DWORD, 126 | dwStateMask: 0 as DWORD, 127 | szInfo: [0 as u16; 256], 128 | u: Default::default(), 129 | szInfoTitle: [0 as u16; 64], 130 | dwInfoFlags: 0 as UINT, 131 | guidItem: GUID { 132 | Data1: 0 as c_ulong, 133 | Data2: 0 as c_ushort, 134 | Data3: 0 as c_ushort, 135 | Data4: [0; 8], 136 | }, 137 | hBalloonIcon: 0 as HICON, 138 | } 139 | } 140 | 141 | fn get_menu_item_struct() -> MENUITEMINFOW { 142 | MENUITEMINFOW { 143 | cbSize: std::mem::size_of::() as UINT, 144 | fMask: 0 as UINT, 145 | fType: 0 as UINT, 146 | fState: 0 as UINT, 147 | wID: 0 as UINT, 148 | hSubMenu: 0 as HMENU, 149 | hbmpChecked: 0 as HBITMAP, 150 | hbmpUnchecked: 0 as HBITMAP, 151 | dwItemData: 0 as ULONG_PTR, 152 | dwTypeData: std::ptr::null_mut(), 153 | cch: 0 as u32, 154 | hbmpItem: 0 as HBITMAP, 155 | } 156 | } 157 | 158 | unsafe fn init_window() -> Result { 159 | let class_name = to_wstring("my_window"); 160 | let hinstance: HINSTANCE = libloaderapi::GetModuleHandleA(std::ptr::null_mut()); 161 | let wnd = WNDCLASSW { 162 | style: 0, 163 | lpfnWndProc: Some(window_proc), 164 | cbClsExtra: 0, 165 | cbWndExtra: 0, 166 | hInstance: 0 as HINSTANCE, 167 | hIcon: winuser::LoadIconW(0 as HINSTANCE, winuser::IDI_APPLICATION), 168 | hCursor: winuser::LoadCursorW(0 as HINSTANCE, winuser::IDI_APPLICATION), 169 | hbrBackground: 16 as HBRUSH, 170 | lpszMenuName: 0 as LPCWSTR, 171 | lpszClassName: class_name.as_ptr(), 172 | }; 173 | if winuser::RegisterClassW(&wnd) == 0 { 174 | return Err(get_win_os_error("Error creating window class")); 175 | } 176 | let hwnd = winuser::CreateWindowExW( 177 | 0, 178 | class_name.as_ptr(), 179 | to_wstring("rust_systray_window").as_ptr(), 180 | WS_OVERLAPPEDWINDOW, 181 | CW_USEDEFAULT, 182 | 0, 183 | CW_USEDEFAULT, 184 | 0, 185 | 0 as HWND, 186 | 0 as HMENU, 187 | 0 as HINSTANCE, 188 | std::ptr::null_mut(), 189 | ); 190 | if hwnd == std::ptr::null_mut() { 191 | return Err(get_win_os_error("Error creating window")); 192 | } 193 | let mut nid = get_nid_struct(&hwnd); 194 | nid.uID = 0x1; 195 | nid.uFlags = NIF_MESSAGE; 196 | nid.uCallbackMessage = WM_USER + 1; 197 | if shellapi::Shell_NotifyIconW(NIM_ADD, &mut nid as *mut NOTIFYICONDATAW) == 0 { 198 | return Err(get_win_os_error("Error adding menu icon")); 199 | } 200 | // Setup menu 201 | let hmenu = winuser::CreatePopupMenu(); 202 | let m = MENUINFO { 203 | cbSize: std::mem::size_of::() as DWORD, 204 | fMask: MIM_APPLYTOSUBMENUS | MIM_STYLE, 205 | dwStyle: MNS_NOTIFYBYPOS, 206 | cyMax: 0 as UINT, 207 | hbrBack: 0 as HBRUSH, 208 | dwContextHelpID: 0 as DWORD, 209 | dwMenuData: 0 as ULONG_PTR, 210 | }; 211 | if winuser::SetMenuInfo(hmenu, &m as *const MENUINFO) == 0 { 212 | return Err(get_win_os_error("Error setting up menu")); 213 | } 214 | 215 | Ok(WindowInfo { 216 | hwnd: hwnd, 217 | hmenu: hmenu, 218 | hinstance: hinstance, 219 | }) 220 | } 221 | 222 | unsafe fn run_loop() { 223 | log::debug!("Running windows loop"); 224 | // Run message loop 225 | let mut msg = winuser::MSG { 226 | hwnd: 0 as HWND, 227 | message: 0 as UINT, 228 | wParam: 0 as WPARAM, 229 | lParam: 0 as LPARAM, 230 | time: 0 as DWORD, 231 | pt: POINT { x: 0, y: 0 }, 232 | }; 233 | loop { 234 | winuser::GetMessageW(&mut msg, 0 as HWND, 0, 0); 235 | if msg.message == winuser::WM_QUIT { 236 | break; 237 | } 238 | winuser::TranslateMessage(&mut msg); 239 | winuser::DispatchMessageW(&mut msg); 240 | } 241 | log::debug!("Leaving windows run loop"); 242 | } 243 | 244 | pub struct Window { 245 | info: WindowInfo, 246 | windows_loop: Option>, 247 | } 248 | 249 | impl Window { 250 | pub fn new(event_tx: Sender) -> Result { 251 | let (tx, rx) = channel(); 252 | let windows_loop = thread::spawn(move || { 253 | unsafe { 254 | let i = init_window(); 255 | let k; 256 | match i { 257 | Ok(j) => { 258 | tx.send(Ok(j.clone())).ok(); 259 | k = j; 260 | } 261 | Err(e) => { 262 | // If creation didn't work, return out of the thread. 263 | tx.send(Err(e)).ok(); 264 | return; 265 | } 266 | }; 267 | WININFO_STASH.with(|stash| { 268 | let data = WindowsLoopData { 269 | info: k, 270 | tx: event_tx, 271 | }; 272 | (*stash.borrow_mut()) = Some(data); 273 | }); 274 | run_loop(); 275 | } 276 | }); 277 | let info = match rx.recv().unwrap() { 278 | Ok(i) => i, 279 | Err(e) => { 280 | return Err(e); 281 | } 282 | }; 283 | let w = Window { 284 | info: info, 285 | windows_loop: Some(windows_loop), 286 | }; 287 | Ok(w) 288 | } 289 | 290 | pub fn quit(&mut self) { 291 | unsafe { 292 | winuser::PostMessageW(self.info.hwnd, WM_DESTROY, 0 as WPARAM, 0 as LPARAM); 293 | } 294 | if let Some(t) = self.windows_loop.take() { 295 | t.join().ok(); 296 | } 297 | } 298 | 299 | pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { 300 | // Add Tooltip 301 | log::debug!("Setting tooltip to {}", tooltip); 302 | // Gross way to convert String to [i8; 128] 303 | // TODO: Clean up conversion, test for length so we don't panic at runtime 304 | let tt = tooltip.as_bytes().clone(); 305 | let mut nid = get_nid_struct(&self.info.hwnd); 306 | for i in 0..tt.len() { 307 | nid.szTip[i] = tt[i] as u16; 308 | } 309 | nid.uFlags = NIF_TIP; 310 | unsafe { 311 | if shellapi::Shell_NotifyIconW(NIM_MODIFY, &mut nid as *mut NOTIFYICONDATAW) == 0 { 312 | return Err(get_win_os_error("Error setting tooltip")); 313 | } 314 | } 315 | Ok(()) 316 | } 317 | 318 | pub fn add_menu_entry(&self, item_idx: u32, item_name: &str) -> Result<(), Error> { 319 | let mut st = to_wstring(item_name); 320 | let mut item = get_menu_item_struct(); 321 | item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE; 322 | item.fType = MFT_STRING; 323 | item.wID = item_idx; 324 | item.dwTypeData = st.as_mut_ptr(); 325 | item.cch = (item_name.len() * 2) as u32; 326 | unsafe { 327 | if winuser::InsertMenuItemW(self.info.hmenu, item_idx, 1, &item as *const MENUITEMINFOW) 328 | == 0 329 | { 330 | return Err(get_win_os_error("Error inserting menu item")); 331 | } 332 | } 333 | Ok(()) 334 | } 335 | 336 | pub fn add_menu_separator(&self, item_idx: u32) -> Result<(), Error> { 337 | let mut item = get_menu_item_struct(); 338 | item.fMask = MIIM_FTYPE; 339 | item.fType = MFT_SEPARATOR; 340 | item.wID = item_idx; 341 | unsafe { 342 | if winuser::InsertMenuItemW(self.info.hmenu, item_idx, 1, &item as *const MENUITEMINFOW) 343 | == 0 344 | { 345 | return Err(get_win_os_error("Error inserting separator")); 346 | } 347 | } 348 | Ok(()) 349 | } 350 | 351 | fn set_icon(&self, icon: HICON) -> Result<(), Error> { 352 | unsafe { 353 | let mut nid = get_nid_struct(&self.info.hwnd); 354 | nid.uFlags = NIF_ICON; 355 | nid.hIcon = icon; 356 | if shellapi::Shell_NotifyIconW(NIM_MODIFY, &mut nid as *mut NOTIFYICONDATAW) == 0 { 357 | return Err(get_win_os_error("Error setting icon")); 358 | } 359 | } 360 | Ok(()) 361 | } 362 | 363 | pub fn set_icon_from_resource(&self, resource_name: &str) -> Result<(), Error> { 364 | let icon; 365 | unsafe { 366 | icon = winuser::LoadImageW( 367 | self.info.hinstance, 368 | to_wstring(&resource_name).as_ptr(), 369 | IMAGE_ICON, 370 | 64, 371 | 64, 372 | 0, 373 | ) as HICON; 374 | if icon == std::ptr::null_mut() as HICON { 375 | return Err(get_win_os_error("Error setting icon from resource")); 376 | } 377 | } 378 | self.set_icon(icon) 379 | } 380 | 381 | pub fn set_icon_from_file(&self, icon_file: &str) -> Result<(), Error> { 382 | let wstr_icon_file = to_wstring(&icon_file); 383 | let hicon; 384 | unsafe { 385 | hicon = winuser::LoadImageW( 386 | std::ptr::null_mut() as HINSTANCE, 387 | wstr_icon_file.as_ptr(), 388 | IMAGE_ICON, 389 | 64, 390 | 64, 391 | LR_LOADFROMFILE, 392 | ) as HICON; 393 | if hicon == std::ptr::null_mut() as HICON { 394 | return Err(get_win_os_error("Error setting icon from file")); 395 | } 396 | } 397 | self.set_icon(hicon) 398 | } 399 | 400 | pub fn set_icon_from_buffer( 401 | &self, 402 | buffer: &[u8], 403 | width: u32, 404 | height: u32, 405 | ) -> Result<(), Error> { 406 | let offset = unsafe { 407 | winuser::LookupIconIdFromDirectoryEx( 408 | buffer.as_ptr() as PBYTE, 409 | TRUE, 410 | width as i32, 411 | height as i32, 412 | LR_DEFAULTCOLOR, 413 | ) 414 | }; 415 | 416 | if offset != 0 { 417 | let icon_data = &buffer[offset as usize..]; 418 | let hicon = unsafe { 419 | winuser::CreateIconFromResourceEx( 420 | icon_data.as_ptr() as PBYTE, 421 | 0, 422 | TRUE, 423 | 0x30000, 424 | width as i32, 425 | height as i32, 426 | LR_DEFAULTCOLOR, 427 | ) 428 | }; 429 | 430 | if hicon == std::ptr::null_mut() as HICON { 431 | return Err(unsafe { get_win_os_error("Cannot load icon from the buffer") }); 432 | } 433 | 434 | self.set_icon(hicon) 435 | } else { 436 | Err(unsafe { get_win_os_error("Error setting icon from buffer") }) 437 | } 438 | } 439 | 440 | pub fn shutdown(&self) -> Result<(), Error> { 441 | unsafe { 442 | let mut nid = get_nid_struct(&self.info.hwnd); 443 | nid.uFlags = NIF_ICON; 444 | if shellapi::Shell_NotifyIconW(NIM_DELETE, &mut nid as *mut NOTIFYICONDATAW) == 0 { 445 | return Err(get_win_os_error("Error deleting icon from menu")); 446 | } 447 | } 448 | Ok(()) 449 | } 450 | } 451 | 452 | impl Drop for Window { 453 | fn drop(&mut self) { 454 | self.shutdown().ok(); 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Systray Lib 2 | pub mod api; 3 | 4 | use std::{ 5 | collections::HashMap, 6 | error, fmt, 7 | sync::mpsc::{channel, Receiver}, 8 | }; 9 | 10 | type BoxedError = Box; 11 | 12 | #[derive(Debug)] 13 | pub enum Error { 14 | OsError(String), 15 | NotImplementedError, 16 | UnknownError, 17 | Error(BoxedError), 18 | } 19 | 20 | impl From for Error { 21 | fn from(value: BoxedError) -> Self { 22 | Error::Error(value) 23 | } 24 | } 25 | 26 | pub struct SystrayEvent { 27 | menu_index: u32, 28 | } 29 | 30 | impl error::Error for Error {} 31 | 32 | impl fmt::Display for Error { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 34 | use self::Error::*; 35 | 36 | match *self { 37 | OsError(ref err_str) => write!(f, "OsError: {}", err_str), 38 | NotImplementedError => write!(f, "Functionality is not implemented yet"), 39 | UnknownError => write!(f, "Unknown error occurrred"), 40 | Error(ref e) => write!(f, "Error: {}", e), 41 | } 42 | } 43 | } 44 | 45 | pub struct Application { 46 | window: api::api::Window, 47 | menu_idx: u32, 48 | callback: HashMap, 49 | // Each platform-specific window module will set up its own thread for 50 | // dealing with the OS main loop. Use this channel for receiving events from 51 | // that thread. 52 | rx: Receiver, 53 | } 54 | 55 | type Callback = 56 | Box<(dyn FnMut(&mut Application) -> Result<(), BoxedError> + Send + Sync + 'static)>; 57 | 58 | fn make_callback(mut f: F) -> Callback 59 | where 60 | F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, 61 | E: error::Error + Send + Sync + 'static, 62 | { 63 | Box::new(move |a: &mut Application| match f(a) { 64 | Ok(()) => Ok(()), 65 | Err(e) => Err(Box::new(e) as BoxedError), 66 | }) as Callback 67 | } 68 | 69 | impl Application { 70 | pub fn new() -> Result { 71 | let (event_tx, event_rx) = channel(); 72 | match api::api::Window::new(event_tx) { 73 | Ok(w) => Ok(Application { 74 | window: w, 75 | menu_idx: 0, 76 | callback: HashMap::new(), 77 | rx: event_rx, 78 | }), 79 | Err(e) => Err(e), 80 | } 81 | } 82 | 83 | pub fn add_menu_item(&mut self, item_name: &str, f: F) -> Result 84 | where 85 | F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, 86 | E: error::Error + Send + Sync + 'static, 87 | { 88 | let idx = self.menu_idx; 89 | if let Err(e) = self.window.add_menu_entry(idx, item_name) { 90 | return Err(e); 91 | } 92 | self.callback.insert(idx, make_callback(f)); 93 | self.menu_idx += 1; 94 | Ok(idx) 95 | } 96 | 97 | pub fn add_menu_separator(&mut self) -> Result { 98 | let idx = self.menu_idx; 99 | if let Err(e) = self.window.add_menu_separator(idx) { 100 | return Err(e); 101 | } 102 | self.menu_idx += 1; 103 | Ok(idx) 104 | } 105 | 106 | pub fn set_icon_from_file(&self, file: &str) -> Result<(), Error> { 107 | self.window.set_icon_from_file(file) 108 | } 109 | 110 | pub fn set_icon_from_resource(&self, resource: &str) -> Result<(), Error> { 111 | self.window.set_icon_from_resource(resource) 112 | } 113 | 114 | #[cfg(target_os = "windows")] 115 | pub fn set_icon_from_buffer( 116 | &self, 117 | buffer: &[u8], 118 | width: u32, 119 | height: u32, 120 | ) -> Result<(), Error> { 121 | self.window.set_icon_from_buffer(buffer, width, height) 122 | } 123 | 124 | pub fn shutdown(&self) -> Result<(), Error> { 125 | self.window.shutdown() 126 | } 127 | 128 | pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { 129 | self.window.set_tooltip(tooltip) 130 | } 131 | 132 | pub fn quit(&mut self) { 133 | self.window.quit() 134 | } 135 | 136 | pub fn wait_for_message(&mut self) -> Result<(), Error> { 137 | loop { 138 | let msg; 139 | match self.rx.recv() { 140 | Ok(m) => msg = m, 141 | Err(_) => { 142 | self.quit(); 143 | break; 144 | } 145 | } 146 | if self.callback.contains_key(&msg.menu_index) { 147 | if let Some(mut f) = self.callback.remove(&msg.menu_index) { 148 | f(self)?; 149 | self.callback.insert(msg.menu_index, f); 150 | } 151 | } 152 | } 153 | 154 | Ok(()) 155 | } 156 | } 157 | 158 | impl Drop for Application { 159 | fn drop(&mut self) { 160 | self.shutdown().ok(); 161 | } 162 | } 163 | --------------------------------------------------------------------------------