├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── docs.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets ├── icon.ico ├── icon.png ├── image1.png ├── image2.png ├── image3.png ├── image4.png ├── image5.png ├── image6.png ├── menu.ico └── test.ico ├── c ├── linux │ ├── tray.c │ ├── tray.h │ ├── utils.c │ └── utils.h ├── macos │ └── tray.m ├── vtray.h └── windows │ ├── tray.c │ ├── tray.h │ ├── utils.c │ └── utils.h ├── docs.vsh ├── example ├── .editorconfig ├── .gitattributes ├── .gitignore ├── assets │ ├── icon.ico │ └── icon.png ├── basic_usage.v └── v.mod ├── src ├── lib.c.v └── lib.v └── v.mod /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.v] 8 | indent_style = tab 9 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bat eol=crlf 3 | 4 | **/*.v linguist-language=V 5 | **/*.vv linguist-language=V 6 | **/*.vsh linguist-language=V 7 | **/v.mod linguist-language=V -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # 2 | # All credit for this workflow goes to Turiiya (AKA ttytm) it was taken from his Webview project and slightly modified. 3 | # https://github.com/ttytm/webview 4 | # 5 | 6 | # Simple workflow for deploying static content to GitHub Pages 7 | name: Deploy docs to GitHub Pages 8 | 9 | on: 10 | # Runs on pushes targeting the default branch and docs path 11 | push: 12 | branches: ['master'] 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 17 | permissions: 18 | contents: read 19 | pages: write 20 | id-token: write 21 | 22 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 23 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 24 | concurrency: 25 | group: 'pages' 26 | cancel-in-progress: false 27 | 28 | jobs: 29 | # Single deploy job since we're just deploying 30 | deploy: 31 | environment: 32 | name: github-pages 33 | url: ${{ steps.deployment.outputs.page_url }} 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v3 38 | - name: Install V 39 | uses: vlang/setup-v@v1.3 40 | with: 41 | check-latest: true 42 | - name: Checkout 43 | uses: actions/checkout@v3 44 | with: 45 | path: VTray 46 | - name: Build docs 47 | run: | 48 | cd VTray 49 | v run ./docs.vsh 50 | - name: Upload artifact 51 | uses: actions/upload-pages-artifact@v2 52 | with: 53 | path: 'VTray/_docs' 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v2 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | untitled19 4 | *.exe 5 | *.exe~ 6 | *.so 7 | *.dylib 8 | *.dll 9 | # Ignore binary output folders 10 | bin/ 11 | _docs 12 | 13 | # Ignore common editor/system specific metadata 14 | .DS_Store 15 | .idea/ 16 | .vscode/ 17 | *.iml 18 | src/main.v -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sylvester Stephenson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = v 2 | ARGS = -use-os-system-to-run -cg -cc gcc 3 | 4 | all: 5 | $(MAKE) fmt 6 | $(CC) $(ARGS) run . 7 | 8 | 9 | fmt: 10 | $(CC) fmt -w . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VTray 2 | 3 | > **NOTE** 4 | > This library is still in development and is not ready for production use. 5 | 6 | VTray is a cross-platform V library to place an icon and menu in the notification area. 7 | 8 | ## Features 9 | 10 | - [x] Create a tray 11 | - [x] Custom icon for tray 12 | - [x] Add tooltip. 13 | - [x] Add an on click event listener 14 | - [x] Supported on Windows 15 | - [x] Supported on Linux 16 | - [x] Supported on MacOS 17 | - [x] Menu items can be checked/unchecked 18 | - [x] Menu items can be enabled/disabled 19 | - [x] Dynamically update icon 20 | - [x] Dynamically update tooltip 21 | - [ ] Allow menus to have their own icons 22 | - [ ] Allow submenus within menus 23 | 24 | > **NOTE** 25 | > Struct definitions and functions can be found [here](https://ouri028.github.io/VTray/vtray.html) 26 | 27 | ## Requirements 28 | 29 | For Linux you will need the following packages installed: 30 | 31 | ```bash 32 | sudo apt-get install libayatana-appindicator3-dev 33 | sudo apt-get install libgtk-3-dev 34 | sudo apt-get install pkg-config 35 | ``` 36 | 37 | For MacOS the tray icon size must be 22x22 pixels in order for it to render correctly. 38 | 39 | I have only tested this using GCC, so I am not sure if it will work with other compilers. 40 | 41 | ## Example 42 | 43 | ```v 44 | module main 45 | 46 | import ouri028.vtray 47 | 48 | fn main() { 49 | icon := $if macos { 50 | '${@VMODROOT}/assets/icon.png' 51 | } $else { 52 | '${@VMODROOT}/assets/icon.ico' 53 | } 54 | mut tray := vtray.create(icon, tooltip: 'VTray Demo!') 55 | tray.add_item('Edit', 56 | checkable: true 57 | on_click: fn [tray] (item &vtray.MenuItem) { 58 | println(item) 59 | if item.checked { 60 | tray.set_icon('${@VMODROOT}/assets/test.ico') 61 | } else { 62 | tray.set_icon('${@VMODROOT}/assets/icon.ico') 63 | } 64 | } 65 | ) 66 | tray.add_item('Copy', 67 | on_click: fn [tray] () { 68 | tray.set_tooltip('Copied!') 69 | } 70 | ) 71 | tray.add_item('Quit', on_click: tray.destroy) 72 | tray.run() 73 | tray.destroy() 74 | } 75 | ``` 76 | 77 | ### Windows 11 78 | 79 | ![image1.png](assets%2Fimage1.png) 80 | 81 | ![image2.png](assets%2Fimage2.png) 82 | 83 | ![image3.png](assets%2Fimage3.png) 84 | 85 | ![image6.png](assets%2Fimage6.png) 86 | 87 | ### Linux 88 | 89 | ![image4.png](assets%2Fimage4.png) 90 | 91 | ### MacOS 92 | 93 | ![image5.png](assets%2Fimage5.png) 94 | 95 | ## License 96 | 97 | ```text 98 | MIT License 99 | 100 | Copyright (c) 2023 Sylvester Stephenson 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy 103 | of this software and associated documentation files (the "Software"), to deal 104 | in the Software without restriction, including without limitation the rights 105 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 106 | copies of the Software, and to permit persons to whom the Software is 107 | furnished to do so, subject to the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be included in all 110 | copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 113 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 114 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 115 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 116 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 117 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 118 | SOFTWARE. 119 | 120 | ``` 121 | -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/icon.png -------------------------------------------------------------------------------- /assets/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/image1.png -------------------------------------------------------------------------------- /assets/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/image2.png -------------------------------------------------------------------------------- /assets/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/image3.png -------------------------------------------------------------------------------- /assets/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/image4.png -------------------------------------------------------------------------------- /assets/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/image5.png -------------------------------------------------------------------------------- /assets/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/image6.png -------------------------------------------------------------------------------- /assets/menu.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/menu.ico -------------------------------------------------------------------------------- /assets/test.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/assets/test.ico -------------------------------------------------------------------------------- /c/linux/tray.c: -------------------------------------------------------------------------------- 1 | #ifdef __linux__ 2 | #include "tray.h" 3 | 4 | void *GLOBAL_TRAY = NULL; 5 | 6 | static void on_menu_item_clicked(GtkMenuItem *menu_item, gpointer user_data) 7 | { 8 | struct MenuItem *item = (struct MenuItem *)user_data; 9 | struct VTray *tray = (struct VTray *)get_global_vtray(); 10 | if (tray != NULL) 11 | { 12 | vtray_update_menu_item(tray, item->id); 13 | } 14 | else 15 | { 16 | printf("Global pointer is NULL\n"); 17 | } 18 | } 19 | 20 | static void on_toggle_menu_item_clicked(GtkCheckMenuItem *menu_item, gpointer user_data) 21 | { 22 | struct MenuItem *item = (struct MenuItem *)user_data; 23 | struct VTray *tray = (struct VTray *)get_global_vtray(); 24 | if (tray != NULL) 25 | { 26 | vtray_update_menu_item(tray, item->id); 27 | } 28 | else 29 | { 30 | printf("Global pointer is NULL\n"); 31 | } 32 | } 33 | 34 | struct VTray *vtray_init(VTrayParams *params, size_t num_items, struct MenuItem *items[]) 35 | { 36 | struct VTray *tray = (struct VTray *)malloc(sizeof(struct VTray)); 37 | if (!tray) 38 | { 39 | // Handle allocation failure 40 | fprintf(stderr, "Failed to allocate VTray!\n"); 41 | return NULL; 42 | } 43 | 44 | gtk_init(NULL, NULL); 45 | tray->menu = gtk_menu_new(); 46 | tray->indicator = app_indicator_new(string_to_char(params->identifier), string_to_char(params->icon), APP_INDICATOR_CATEGORY_APPLICATION_STATUS); 47 | 48 | // Set the tooltip text for the indicator 49 | app_indicator_set_title(tray->indicator, string_to_char(params->tooltip)); 50 | app_indicator_set_label(tray->indicator, string_to_char(params->tooltip), ""); 51 | app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE); 52 | app_indicator_set_attention_icon_full(tray->indicator, string_to_char(params->identifier), "New messages"); 53 | if (!GTK_IS_MENU(tray->menu)) 54 | { 55 | fprintf(stderr, "Invalid menu instance!\n"); 56 | return NULL; 57 | } 58 | 59 | app_indicator_set_menu(tray->indicator, GTK_MENU(tray->menu)); 60 | tray->on_click = params->on_click; 61 | tray->items = items; 62 | tray->num_items = num_items; 63 | vtray_construct(tray); 64 | // Create the menu 65 | 66 | return tray; 67 | } 68 | 69 | void set_global_vtray(void *ptr) 70 | { 71 | GLOBAL_TRAY = ptr; 72 | } 73 | 74 | void *get_global_vtray() 75 | { 76 | return GLOBAL_TRAY; 77 | } 78 | 79 | void vtray_construct(struct VTray *parent) 80 | { 81 | if (parent->menu) 82 | { 83 | parent->num_menus = 0; 84 | 85 | for (size_t i = 0; i < parent->num_items; i++) 86 | { 87 | struct MenuItem *item = parent->items[i]; 88 | GtkMenuItem *menu_item; 89 | 90 | if (item->checkable) 91 | { 92 | size_t len = parent->num_menus; 93 | menu_item = gtk_check_menu_item_new_with_label(string_to_char(item->text)); 94 | gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_item), item->checked); // Set initial state to checked 95 | g_signal_connect(menu_item, "toggled", G_CALLBACK(on_toggle_menu_item_clicked), item); // Connect to the "toggled" signal 96 | parent->menus[len] = menu_item; 97 | parent->num_menus++; 98 | } 99 | else 100 | { 101 | size_t len = parent->num_menus; 102 | menu_item = gtk_menu_item_new_with_label(string_to_char(item->text)); 103 | g_signal_connect(menu_item, "activate", G_CALLBACK(on_menu_item_clicked), item); 104 | parent->menus[len] = menu_item; 105 | parent->num_menus++; 106 | } 107 | if (item->disabled) 108 | { 109 | gtk_widget_set_sensitive(GTK_WIDGET(menu_item), FALSE); 110 | } 111 | gtk_menu_shell_append(GTK_MENU_SHELL(parent->menu), menu_item); 112 | gtk_widget_show(menu_item); 113 | } 114 | gtk_widget_show_all(parent->menu); 115 | app_indicator_set_menu(parent->indicator, GTK_MENU(parent->menu)); 116 | set_global_vtray(parent); 117 | } 118 | } 119 | 120 | void vtray_update_menu_item(struct VTray *tray, int menu_id) 121 | { 122 | MenuItem *item = get_vmenu_item_by_id(menu_id, tray); 123 | GtkMenuItem *menu_item = get_menu_item_by_label(tray, string_to_char(item->text)); 124 | if (item == NULL) 125 | { 126 | fprintf(stderr, "Failed to find menu item with ID %d\n", menu_id); 127 | return; 128 | } 129 | 130 | if (!item->checkable) 131 | { 132 | tray->on_click(item); 133 | return; 134 | } 135 | if (gtk_check_menu_item_get_active(menu_item)) 136 | { 137 | item->checked = true; 138 | } 139 | else 140 | { 141 | item->checked = false; 142 | } 143 | 144 | gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_item), item->checked); 145 | 146 | tray->on_click(item); 147 | } 148 | 149 | GtkMenuItem *get_menu_item_by_label(struct VTray *tray, char *label) 150 | { 151 | for (size_t i = 0; i < tray->num_menus; i++) 152 | { 153 | GtkMenuItem *menu_item = GTK_MENU_ITEM(tray->menus[i]); 154 | if (strcmp(gtk_menu_item_get_label(menu_item), label) == 0) 155 | { 156 | return menu_item; 157 | } 158 | } 159 | return NULL; 160 | } 161 | 162 | MenuItem *get_vmenu_item_by_id(int menu_id, struct VTray *tray) 163 | { 164 | for (size_t i = 0; i < tray->num_items; i++) 165 | { 166 | if (tray->items[i]->id == menu_id) 167 | { 168 | return tray->items[i]; 169 | } 170 | } 171 | return (MenuItem *){0}; 172 | } 173 | 174 | void vtray_set_icon(char *icon, struct VTray *tray) 175 | { 176 | // Update the icon 177 | app_indicator_set_icon_full(tray->indicator, icon, ""); 178 | app_indicator_set_attention_icon_full(tray->indicator, icon, ""); 179 | } 180 | // Set the tooltip for the system tray icon 181 | void vtray_set_tooltip(char *tooltip, struct VTray *tray) 182 | { 183 | app_indicator_set_title(tray->indicator, tooltip); 184 | } 185 | 186 | void vtray_exit(struct VTray *tray) 187 | { 188 | // Deallocate memory, destroy the indicator, etc. 189 | if (tray) 190 | { 191 | if (tray->menus) 192 | { 193 | // Deallocate memory for individual menu items in the flexible array 194 | for (int i = 0; i < tray->num_menus; i++) 195 | { 196 | if (tray->menus[i] != NULL) 197 | { 198 | gtk_widget_destroy(tray->menus[i]); 199 | } 200 | } 201 | } 202 | if (tray->indicator) 203 | g_object_unref(tray->indicator); 204 | free(tray); 205 | gtk_main_quit(); 206 | exit(1); 207 | } 208 | } 209 | 210 | void vtray_run(struct VTray *tray) 211 | { 212 | // Run the GTK main loop 213 | gtk_main(); 214 | } 215 | 216 | void vtray_update(struct VTray *tray) 217 | { 218 | // Update the system tray icon and menu as needed 219 | app_indicator_set_status(tray->indicator, APP_INDICATOR_STATUS_ACTIVE); 220 | } 221 | 222 | #endif -------------------------------------------------------------------------------- /c/linux/tray.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef __linux__ 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "utils.h" 9 | 10 | typedef struct VTrayParams VTrayParams; 11 | typedef struct MenuItem MenuItem; 12 | typedef void (*CallbackFunction)(MenuItem *menu_item); 13 | 14 | struct VTray 15 | { 16 | AppIndicator *indicator; 17 | GtkWidget *menu; 18 | CallbackFunction on_click; 19 | struct MenuItem **items; 20 | size_t num_items; 21 | size_t num_menus; 22 | GtkMenuItem **menus; 23 | }; 24 | 25 | struct VTrayParams 26 | { 27 | String identifier; 28 | String tooltip; 29 | String icon; 30 | CallbackFunction on_click; 31 | }; 32 | 33 | struct MenuItem 34 | { 35 | int id; 36 | String text; 37 | bool checked; 38 | bool checkable; 39 | bool disabled; 40 | }; 41 | 42 | struct VTray *vtray_init(VTrayParams *params, size_t num_items, struct MenuItem *items[]); 43 | 44 | void vtray_run(struct VTray *tray); 45 | 46 | void vtray_exit(struct VTray *tray); 47 | 48 | void vtray_update(struct VTray *tray); 49 | 50 | void vtray_construct(struct VTray *parent); 51 | 52 | void vtray_set_icon(char *icon, struct VTray *tray); 53 | 54 | void vtray_set_tooltip(char *tooltip, struct VTray *tray); 55 | 56 | void vtray_update_menu_item(struct VTray *tray, int menu_id); 57 | 58 | GtkMenuItem *get_menu_item_by_label(struct VTray *tray, char *label); 59 | 60 | MenuItem *get_vmenu_item_by_id(int menu_id, struct VTray *tray); 61 | 62 | void set_global_vtray(void *ptr); 63 | 64 | void *get_global_vtray(); 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /c/linux/utils.c: -------------------------------------------------------------------------------- 1 | #ifdef __linux__ 2 | #include "utils.h" 3 | char *string_to_char(String string) 4 | { 5 | if (string.str == NULL) 6 | { 7 | perror("string_to_char: string is NULL"); 8 | return NULL; 9 | } 10 | return string.str; 11 | } 12 | 13 | size_t len(String string) 14 | { 15 | return string.len; 16 | } 17 | #endif -------------------------------------------------------------------------------- /c/linux/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef __linux__ 3 | #include 4 | 5 | typedef struct String String; 6 | 7 | struct String 8 | { 9 | char *str; 10 | size_t len; 11 | }; 12 | 13 | char *string_to_char(String string); 14 | size_t len(String string); 15 | #endif -------------------------------------------------------------------------------- /c/macos/tray.m: -------------------------------------------------------------------------------- 1 | #ifdef __APPLE__ 2 | 3 | /** 4 | * This code was based off of https://github.com/vlang/v/blob/master/examples/macos_tray/tray.m 5 | * All credit goes to the legend who created this. 6 | */ 7 | 8 | static char *string_to_char(string str) 9 | { 10 | if (str.str == NULL) 11 | { 12 | perror("string_to_char: string is NULL"); 13 | return NULL; 14 | } 15 | return str.str; 16 | } 17 | 18 | static size_t len(string str) 19 | { 20 | return str.len; 21 | } 22 | 23 | static char *char_to_nsstring(char *str) 24 | { 25 | if (str == NULL) 26 | { 27 | perror("char_to_nsstring: string is NULL"); 28 | return NULL; 29 | } 30 | return [NSString stringWithUTF8String:str]; 31 | } 32 | 33 | static NSString *string_to_nsstring(string str) 34 | { 35 | if (str.str == NULL) 36 | { 37 | perror("string_to_nsstring: string is NULL"); 38 | return NULL; 39 | } 40 | return [NSString stringWithUTF8String:str.str]; 41 | } 42 | 43 | // Manages the app lifecycle. 44 | @interface AppDelegate : NSObject { 45 | NSAutoreleasePool *pool; 46 | NSApplication *app; 47 | NSStatusItem *statusItem; // our button 48 | int num_items; 49 | vtray__VTrayParams *trayParams; // VTrayParams is defined in tray.v 50 | vtray__MenuItem **menuItems; 51 | } 52 | @end 53 | 54 | @implementation AppDelegate 55 | - (AppDelegate *)initWithParams:(vtray__VTrayParams *)params 56 | itemsArray:(vtray__MenuItem *[])itemsArray 57 | numItems:(int)numItems { 58 | if (self = [super init]) { 59 | trayParams = params; 60 | num_items = numItems; 61 | menuItems = itemsArray; 62 | } 63 | return self; 64 | } 65 | 66 | 67 | // Called when NSMenuItem is clicked. 68 | - (void)onAction:(id)sender { 69 | NSMenuItem *menuItem = (NSMenuItem *)sender; 70 | struct vtray__MenuItem *item = 71 | (struct vtray__MenuItem *)[[sender representedObject] pointerValue]; 72 | 73 | if (item && item->checkable) { 74 | item->checked = !item->checked; // Toggle the checked state 75 | 76 | if (item->checked) { 77 | [menuItem setState:NSOnState]; 78 | } else { 79 | [menuItem setState:NSOffState]; 80 | } 81 | 82 | } 83 | trayParams->on_click(item); 84 | } 85 | 86 | 87 | - (NSMenu *)buildMenu { 88 | NSMenu *menu = [NSMenu new]; 89 | [menu autorelease]; 90 | [menu setAutoenablesItems:NO]; 91 | 92 | for (int i = 0; i < num_items; i++) { 93 | NSString *title = string_to_nsstring(menuItems[i]->text); 94 | NSMenuItem *item = [menu addItemWithTitle:title 95 | action:@selector(onAction:) 96 | keyEquivalent:@""]; 97 | NSValue *representedObject = [NSValue valueWithPointer:(menuItems[i])]; 98 | 99 | if(menuItems[i]->disabled) { 100 | [item setEnabled:NO]; 101 | } else { 102 | [item setEnabled:YES]; 103 | } 104 | 105 | if(menuItems[i]->checkable) { 106 | [item setTarget:self]; 107 | if(menuItems[i]->checked) { 108 | [item setState: NSOnState]; 109 | } 110 | } 111 | [item setRepresentedObject:representedObject]; 112 | [item setTarget:self]; 113 | [item autorelease]; 114 | } 115 | 116 | return menu; 117 | } 118 | 119 | - (void)initTrayMenuItem { 120 | NSStatusBar *statusBar = [NSStatusBar systemStatusBar]; 121 | statusItem = [statusBar statusItemWithLength:NSSquareStatusItemLength]; 122 | [statusItem retain]; 123 | [statusItem setVisible:YES]; 124 | [statusItem setToolTip:string_to_nsstring(trayParams->tooltip)]; 125 | NSStatusBarButton *statusBarButton = [statusItem button]; 126 | 127 | // Height must be 22px. 128 | NSImage *img = [[NSImage alloc] initWithContentsOfFile:string_to_nsstring(trayParams->icon)]; 129 | if (img == nil) { 130 | NSLog(@"Error loading image: %@", string_to_nsstring(trayParams->icon)); 131 | // Check the image path and file as well as other potential issues. 132 | } else { 133 | [statusBarButton setImage:img]; 134 | NSMenu *menu = [self buildMenu]; 135 | [statusItem setMenu:menu]; 136 | } 137 | } 138 | 139 | - (NSApplicationTerminateReply)applicationShouldTerminate: 140 | (NSApplication *)sender { 141 | return NSTerminateNow; 142 | } 143 | 144 | - (NSStatusItem *)getStatusItem { 145 | return self->statusItem; 146 | } 147 | 148 | @end 149 | 150 | // Initializes NSApplication and NSStatusItem, aka system tray menu item. 151 | vtray__VTray *vtray_init(vtray__VTrayParams *params, int numItems, vtray__MenuItem *itemsArray[]) { 152 | NSApplication *app = [NSApplication sharedApplication]; 153 | AppDelegate *appDelegate = [[AppDelegate alloc] initWithParams:params itemsArray:itemsArray numItems:numItems]; 154 | 155 | [app setActivationPolicy:NSApplicationActivationPolicyRegular]; 156 | 157 | [app setDelegate:appDelegate]; 158 | 159 | [appDelegate initTrayMenuItem]; 160 | 161 | vtray__VTray *tray = malloc(sizeof(vtray__VTray)); 162 | tray->ptr = app; 163 | tray->ptr_delegate = appDelegate; 164 | return tray; 165 | } 166 | 167 | // Blocks and runs the application. 168 | void vtray_run(vtray__VTray *tray) { 169 | NSApplication *app = (NSApplication *)(tray->ptr); 170 | [app run]; 171 | } 172 | 173 | // Terminates the app. 174 | void vtray_exit(vtray__VTray *tray) { 175 | NSApplication *app = (NSApplication *)(tray->ptr); 176 | [app terminate:app]; 177 | } 178 | 179 | void vtray_set_icon(char* icon, vtray__VTray *tray) { 180 | NSImage *image = [[NSImage alloc] initWithContentsOfFile:[NSString stringWithUTF8String:icon]]; 181 | NSStatusItem *statusItem = [tray->ptr_delegate getStatusItem]; 182 | statusItem.button.image = image; 183 | } 184 | 185 | void vtray_set_tooltip(char* tooltip, vtray__VTray *tray) { 186 | NSStatusItem *statusItem = [tray->ptr_delegate getStatusItem]; 187 | [statusItem setToolTip:char_to_nsstring(tooltip)]; 188 | } 189 | 190 | #endif 191 | -------------------------------------------------------------------------------- /c/vtray.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef _WIN32 3 | #include "windows/tray.h" 4 | #elif __linux__ 5 | #include "linux/tray.h" 6 | #endif 7 | -------------------------------------------------------------------------------- /c/windows/tray.c: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | 3 | #include "tray.h" 4 | 5 | LRESULT CALLBACK vtray_wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 6 | { 7 | struct VTray *tray = (struct VTray *)GetWindowLongPtr(hwnd, GWLP_USERDATA); 8 | 9 | switch (msg) 10 | { 11 | case WM_TRAYICON: 12 | // Handle tray icon messages here 13 | if (wParam == ID_TRAYICON) 14 | { 15 | if (lParam == WM_RBUTTONUP) 16 | { 17 | POINT cursor; 18 | GetCursorPos(&cursor); 19 | SetForegroundWindow(hwnd); 20 | TrackPopupMenu(tray->menu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_HORPOSANIMATION, cursor.x, cursor.y, 21 | 0, tray->hwnd, NULL); 22 | PostMessage(hwnd, WM_NULL, 0, 0); 23 | } 24 | } 25 | break; 26 | 27 | case WM_COMMAND: 28 | // Handle menu item selection 29 | if (HIWORD(wParam) == 0) // Menu item clicked 30 | { 31 | int menuId = LOWORD(wParam); 32 | BOOL checked = is_menu_item_checked(tray->menu, menuId); 33 | vtray_update_menu_item(tray, menuId, !checked); 34 | } 35 | break; 36 | 37 | case WM_CLOSE: 38 | // Handle window close event 39 | ShowWindow(hwnd, SW_HIDE); 40 | break; 41 | 42 | default: 43 | return DefWindowProc(hwnd, msg, wParam, lParam); 44 | } 45 | return 0; 46 | } 47 | 48 | struct VTray *vtray_init(VTrayParams *params, size_t num_items, struct MenuItem *items[]) 49 | { 50 | struct VTray *tray = (struct VTray *)malloc(sizeof(struct VTray)); 51 | if (!tray) 52 | { 53 | // Handle allocation failure 54 | return NULL; 55 | } 56 | 57 | // Initialize other members of the struct 58 | memset(tray, 0, sizeof(struct VTray)); 59 | 60 | strncpy(tray->identifier, string_to_char(params->identifier), sizeof(tray->identifier)); 61 | // Initialize window class 62 | memset(&tray->windowClass, 0, sizeof(WNDCLASSEX)); 63 | tray->tooltip = string_to_wchar_t(params->tooltip); 64 | tray->windowClass.cbSize = sizeof(WNDCLASSEX); 65 | tray->windowClass.lpfnWndProc = vtray_wndProc; 66 | tray->windowClass.hInstance = tray->hInstance; 67 | tray->windowClass.lpszClassName = tray->identifier; 68 | tray->items = items; 69 | tray->num_items = num_items; 70 | 71 | if (!RegisterClassEx(&tray->windowClass)) 72 | { 73 | // Handle class registration failure 74 | 75 | free(tray); 76 | fprintf(stderr, "Failed to register class\n"); 77 | 78 | return NULL; 79 | } 80 | 81 | // Create a hidden window 82 | tray->hwnd = CreateWindow(tray->identifier, NULL, 0, 0, 0, 0, 0, NULL, NULL, tray->windowClass.hInstance, 83 | NULL); 84 | 85 | if (!tray->hwnd) 86 | { 87 | // Handle window creation failure 88 | UnregisterClass(tray->identifier, tray->hInstance); 89 | free(tray); 90 | fprintf(stderr, "Failed to create window\n"); 91 | return NULL; 92 | } 93 | 94 | // Initialize the NOTIFYICONDATA structure 95 | tray->notifyData.cbSize = sizeof(NOTIFYICONDATA); 96 | wchar_t *tooltip = string_to_wchar_t(params->tooltip); 97 | wcscpy((wchar_t *)tray->notifyData.szTip, tooltip); 98 | tray->notifyData.hWnd = tray->hwnd; 99 | tray->notifyData.uID = ID_TRAYICON; 100 | tray->notifyData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; 101 | tray->notifyData.uCallbackMessage = WM_TRAYICON; 102 | tray->hInstance = GetModuleHandle(NULL); 103 | tray->notifyData.hIcon = LoadImageA(tray->hInstance, string_to_char(params->icon), IMAGE_ICON, 0, 0, 104 | LR_LOADFROMFILE | LR_DEFAULTSIZE); 105 | vtray_construct(tray); 106 | tray->on_click = params->on_click; 107 | SetWindowLongPtr(tray->notifyData.hWnd, GWLP_USERDATA, (LONG_PTR)tray); 108 | if (Shell_NotifyIcon(NIM_ADD, &tray->notifyData) == FALSE) 109 | { 110 | fprintf(stderr, "Failed to register tray icon\n"); 111 | return NULL; 112 | } 113 | return tray; 114 | } 115 | 116 | void vtray_exit(struct VTray *tray) 117 | { 118 | // Deallocate memory, destroy windows, etc. 119 | 120 | if (tray) 121 | { 122 | if (tray->hwnd) 123 | DestroyWindow(tray->hwnd); 124 | if (tray->menu) 125 | DestroyMenu(tray->menu); 126 | if (tray->notifyData.hIcon) 127 | DestroyIcon(tray->notifyData.hIcon); 128 | UnregisterClass(tray->identifier, tray->hInstance); 129 | free(tray); 130 | exit(1); 131 | } 132 | } 133 | 134 | void vtray_update(struct VTray *tray) 135 | { 136 | // Update the system tray icon and menu as needed. 137 | tray->notifyData.hWnd = tray->hwnd; 138 | Shell_NotifyIcon(NIM_MODIFY, &tray->notifyData); 139 | } 140 | 141 | void vtray_construct(struct VTray *parent) 142 | { 143 | parent->menu = CreatePopupMenu(); 144 | if (parent->menu) 145 | { 146 | for (size_t i = 0; i < parent->num_items; i++) 147 | { 148 | struct MenuItem *item = parent->items[i]; 149 | MENUITEMINFO menuItem; 150 | memset(&menuItem, 0, sizeof(MENUITEMINFO)); 151 | menuItem.cbSize = sizeof(MENUITEMINFO); 152 | menuItem.fMask = MIIM_ID | MIIM_TYPE | MIIM_STATE; 153 | menuItem.wID = item->id; 154 | menuItem.fMask |= MIIM_BITMAP; 155 | UINT flags = MF_STRING; 156 | 157 | // Set the checkable state based on struct properties 158 | if (item->checkable) 159 | { 160 | if (item->checked) 161 | { 162 | flags |= MFS_CHECKED; 163 | } 164 | } 165 | 166 | if (item->disabled) 167 | { 168 | flags |= MFS_DISABLED; 169 | } 170 | 171 | if (!AppendMenu(parent->menu, flags, item->id, (LPCSTR)string_to_wchar_t(item->text))) 172 | { 173 | fprintf(stderr, "Failed to add menu item\n"); 174 | exit(1); 175 | } 176 | } 177 | } 178 | } 179 | 180 | BOOL is_menu_item_checked(HMENU menu, UINT menuId) 181 | { 182 | MENUITEMINFO menuItemInfo; 183 | memset(&menuItemInfo, 0, sizeof(MENUITEMINFO)); 184 | menuItemInfo.cbSize = sizeof(MENUITEMINFO); 185 | menuItemInfo.fMask = MIIM_STATE; 186 | 187 | if (GetMenuItemInfo(menu, menuId, FALSE, &menuItemInfo)) 188 | { 189 | return (menuItemInfo.fState & MFS_CHECKED) != 0; 190 | } 191 | 192 | return FALSE; 193 | } 194 | 195 | void vtray_update_menu_item(struct VTray *tray, int menu_id, bool checked) 196 | { 197 | MENUITEMINFO menuItemInfo = get_menu_item_by_id(tray->menu, menu_id); 198 | if (menuItemInfo.wID == 0) 199 | { 200 | fprintf(stderr, "Failed to find menu item with ID %d\n", menu_id); 201 | return; 202 | } 203 | menuItemInfo.fMask = MIIM_STATE; 204 | MenuItem *item = get_vmenu_item_by_id(menu_id, tray); 205 | if (item == NULL) 206 | { 207 | fprintf(stderr, "Failed to find menu item with ID %d\n", menu_id); 208 | return; 209 | } 210 | 211 | if (item->checkable) 212 | { 213 | menuItemInfo.fState = (checked ? MFS_CHECKED : MFS_UNCHECKED); 214 | item->checked = checked ? 1 : 0; 215 | } 216 | 217 | SetMenuItemInfo(tray->menu, menu_id, FALSE, &menuItemInfo); 218 | tray->on_click(item); 219 | } 220 | 221 | MENUITEMINFO get_menu_item_by_id(HMENU menu, UINT menu_id) 222 | { 223 | MENUITEMINFO menuItemInfo; 224 | memset(&menuItemInfo, 0, sizeof(MENUITEMINFO)); 225 | menuItemInfo.cbSize = sizeof(MENUITEMINFO); 226 | menuItemInfo.fMask = MIIM_ID | MIIM_STATE | MIIM_FTYPE; 227 | 228 | int itemCount = GetMenuItemCount(menu); 229 | for (int i = 0; i < itemCount; ++i) 230 | { 231 | menuItemInfo.wID = 0; // Reset to 0 for each iteration 232 | if (GetMenuItemInfo(menu, i, TRUE, &menuItemInfo) && menuItemInfo.wID == menu_id) 233 | { 234 | // Found the menu item with the specified ID 235 | return menuItemInfo; 236 | } 237 | } 238 | memset(&menuItemInfo, 0, sizeof(MENUITEMINFO)); 239 | menuItemInfo.cbSize = sizeof(MENUITEMINFO); 240 | return menuItemInfo; 241 | } 242 | 243 | MenuItem *get_vmenu_item_by_id(int menu_id, struct VTray *tray) 244 | { 245 | for (size_t i = 0; i < tray->num_items; i++) 246 | { 247 | if (tray->items[i]->id == menu_id) 248 | { 249 | return tray->items[i]; 250 | } 251 | } 252 | return (MenuItem *){0}; 253 | } 254 | 255 | void vtray_set_icon(char *icon, struct VTray *tray) 256 | { 257 | tray->notifyData.hIcon = LoadImageA(tray->hInstance, icon, IMAGE_ICON, 0, 0, 258 | LR_LOADFROMFILE | LR_DEFAULTSIZE); 259 | Shell_NotifyIcon(NIM_MODIFY, &tray->notifyData); 260 | } 261 | 262 | void vtray_set_tooltip(char *tooltip, struct VTray *tray) 263 | { 264 | tray->notifyData.cbSize = sizeof(NOTIFYICONDATA); 265 | wcscpy((wchar_t *)tray->notifyData.szTip, char_to_wchar_t(tooltip)); 266 | Shell_NotifyIcon(NIM_MODIFY, &tray->notifyData); 267 | } 268 | 269 | void vtray_run(struct VTray *tray) 270 | { 271 | // Show and run your Windows application loop here. 272 | ShowWindow(tray->hwnd, SW_HIDE); 273 | 274 | // Update the system tray icon 275 | vtray_update(tray); 276 | 277 | MSG msg; 278 | while (GetMessage(&msg, NULL, 0, 0)) 279 | { 280 | TranslateMessage(&msg); 281 | DispatchMessage(&msg); 282 | } 283 | } 284 | 285 | #endif -------------------------------------------------------------------------------- /c/windows/tray.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef _WIN32 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "utils.h" 10 | 11 | #define WM_TRAYICON (WM_USER + 1) 12 | #define ID_TRAYICON 100 13 | 14 | typedef struct VTrayParams VTrayParams; 15 | typedef struct MenuItem MenuItem; 16 | 17 | typedef void (*CallbackFunction)(MenuItem *menu_item); 18 | 19 | struct VTrayParams 20 | { 21 | String identifier; 22 | String tooltip; 23 | String icon; 24 | CallbackFunction on_click; 25 | }; 26 | 27 | struct MenuItem 28 | { 29 | int id; 30 | String text; 31 | bool checked; 32 | bool checkable; 33 | bool disabled; 34 | }; 35 | 36 | struct VTray 37 | { 38 | char identifier[256]; 39 | NOTIFYICONDATA notifyData; 40 | HMENU menu; 41 | WNDCLASSEX windowClass; 42 | HINSTANCE hInstance; 43 | HWND hwnd; 44 | wchar_t *tooltip; 45 | CallbackFunction on_click; 46 | struct MenuItem **items; 47 | size_t num_items; 48 | }; 49 | 50 | struct VTray *vtray_init(VTrayParams *params, size_t num_items, struct MenuItem *items[]); 51 | 52 | void vtray_exit(struct VTray *tray); 53 | 54 | void vtray_update(struct VTray *tray); 55 | 56 | void vtray_construct(struct VTray *parent); 57 | 58 | void vtray_update_menu_item(struct VTray *tray, int menu_id, bool checked); 59 | 60 | void vtray_set_icon(char *icon, struct VTray *tray); 61 | 62 | void vtray_set_tooltip(char *tooltip, struct VTray *tray); 63 | 64 | BOOL is_menu_item_checked(HMENU menu, UINT menuId); 65 | 66 | MENUITEMINFO get_menu_item_by_id(HMENU menu, UINT menu_id); 67 | 68 | MenuItem *get_vmenu_item_by_id(int menu_id, struct VTray *tray); 69 | 70 | void vtray_run(struct VTray *tray); 71 | 72 | #endif -------------------------------------------------------------------------------- /c/windows/utils.c: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #include "utils.h" 3 | char *string_to_char(String string) 4 | { 5 | if (string.str == NULL) 6 | { 7 | perror("string_to_char: string is NULL"); 8 | return NULL; 9 | } 10 | return string.str; 11 | } 12 | 13 | size_t len(String string) 14 | { 15 | return string.len; 16 | } 17 | 18 | wchar_t *char_to_wchar_t(char *c) 19 | { 20 | if (c == NULL) 21 | { 22 | perror("char_to_wchar_t: string is NULL"); 23 | return NULL; 24 | } 25 | size_t len = mbstowcs(NULL, c, 0) + 1; 26 | wchar_t *w = malloc(len * sizeof(wchar_t)); 27 | if (w == NULL) 28 | { 29 | perror("char_to_wchar_t: failed to allocate memory"); 30 | return NULL; 31 | } 32 | mbstowcs(w, c, len); 33 | return w; 34 | } 35 | 36 | wchar_t *string_to_wchar_t(String string) 37 | { 38 | char *c = string_to_char(string); 39 | return char_to_wchar_t(c); 40 | } 41 | #endif -------------------------------------------------------------------------------- /c/windows/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef _WIN32 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct String String; 9 | 10 | struct String 11 | { 12 | char *str; 13 | size_t len; 14 | }; 15 | 16 | char *string_to_char(String string); 17 | size_t len(String string); 18 | wchar_t *char_to_wchar_t(char *c); 19 | wchar_t *string_to_wchar_t(String string); 20 | #endif -------------------------------------------------------------------------------- /docs.vsh: -------------------------------------------------------------------------------- 1 | /** 2 | * All credit for this script goes to Turiiya (AKA ttytm) as its part of his build script, it was taken from his Webview project and slightly modified. 3 | * https://github.com/ttytm/webview 4 | **/ 5 | import cli 6 | import os 7 | import regex 8 | 9 | // Remove redundant readme section from module page. 10 | fn rm_readme_section(html string) string { 11 | mut r := regex.regex_opt(r'
') or { panic(err) } 12 | sec_start, sec_end := r.find(html) 13 | return '${html[..sec_start]}
${html[sec_end..]}' 14 | .replace('
  • README
  • ', '') 15 | } 16 | 17 | fn build_docs() ! { 18 | // Cleanup old docs. 19 | rmdir_all('_docs') or {} 20 | 21 | // Build docs. 22 | mut p := new_process(@VEXE) 23 | p.set_args(['doc', '-readme', '-m', '-f', 'html', '.']) 24 | p.wait() 25 | 26 | // Prepare html. 27 | mut vtray_html := read_file('_docs/vtray.html')! 28 | vtray_html = rm_readme_section(vtray_html) 29 | write_file('_docs/vtray.html', vtray_html)! 30 | os.cp_all('assets', '_docs/assets', true)! 31 | } 32 | 33 | mut cmd := cli.Command{ 34 | name: 'build.vsh' 35 | posix_mode: true 36 | required_args: 0 37 | pre_execute: fn (cmd cli.Command) ! { 38 | if cmd.args.len > cmd.required_args { 39 | eprintln('Unknown commands ${cmd.args}.\n') 40 | cmd.execute_help() 41 | exit(0) 42 | } 43 | } 44 | execute: fn (_cmd cli.Command) ! { 45 | build_docs()! 46 | } 47 | } 48 | cmd.parse(os.args) 49 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.v] 8 | indent_style = tab 9 | -------------------------------------------------------------------------------- /example/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bat eol=crlf 3 | 4 | **/*.v linguist-language=V 5 | **/*.vv linguist-language=V 6 | **/*.vsh linguist-language=V 7 | **/v.mod linguist-language=V 8 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | example 4 | *.exe 5 | *.exe~ 6 | *.so 7 | *.dylib 8 | *.dll 9 | 10 | # Ignore binary output folders 11 | bin/ 12 | 13 | # Ignore common editor/system specific metadata 14 | .DS_Store 15 | .idea/ 16 | .vscode/ 17 | *.iml 18 | 19 | # ENV 20 | .env 21 | 22 | # vweb and database 23 | *.db 24 | *.js 25 | -------------------------------------------------------------------------------- /example/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/example/assets/icon.ico -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ouri028/VTray/0d4860b9a4c1f520ad7a5d602b9a2cfe9d9d0417/example/assets/icon.png -------------------------------------------------------------------------------- /example/basic_usage.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import ouri028.vtray 4 | 5 | fn main() { 6 | icon := $if macos { 7 | '${@VMODROOT}/assets/icon.png' 8 | } $else { 9 | '${@VMODROOT}/assets/icon.ico' 10 | } 11 | mut tray := vtray.create(icon, tooltip: 'VTray Demo!') 12 | tray.add_item('Edit', 13 | checkable: true 14 | on_click: fn [tray] (item &vtray.MenuItem) { 15 | if item.checked { 16 | tray.set_icon('${@VMODROOT}/assets/test.ico') 17 | } else { 18 | tray.set_icon('${@VMODROOT}/assets/icon.ico') 19 | } 20 | } 21 | ) 22 | tray.add_item('Copy', 23 | on_click: fn [tray] () { 24 | tray.set_tooltip('Copied!') 25 | } 26 | ) 27 | tray.add_item('Quit', on_click: tray.destroy) 28 | tray.run() 29 | tray.destroy() 30 | } 31 | -------------------------------------------------------------------------------- /example/v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'example' 3 | description: '' 4 | version: '' 5 | license: '' 6 | dependencies: ['Ouri028.VTray'] 7 | } 8 | -------------------------------------------------------------------------------- /src/lib.c.v: -------------------------------------------------------------------------------- 1 | module vtray 2 | 3 | $if windows { 4 | #flag -I @VMODROOT/c/windows/utils.h 5 | #flag -I @VMODROOT/c/vtray.h 6 | #flag @VMODROOT/c/windows/utils.c 7 | #flag @VMODROOT/c/windows/tray.c 8 | #include "@VMODROOT/c/windows/tray.h" 9 | } $else $if linux { 10 | #flag -I @VMODROOT/c/linux/utils.h 11 | #flag -I @VMODROOT/c/vtray.h 12 | #flag @VMODROOT/c/linux/utils.c 13 | #flag @VMODROOT/c/linux/tray.c 14 | #include "@VMODROOT/c/linux/tray.h" 15 | } $else $if macos { 16 | #include 17 | #flag -framework Cocoa 18 | #include "@VMODROOT/c/macos/tray.m" 19 | } 20 | 21 | $if linux { 22 | #pkgconfig gtk+-3.0 23 | #pkgconfig --cflags --libs ayatana-appindicator3-0.1 24 | } 25 | 26 | struct VTray { 27 | // Pointer to VTray instance; 28 | ptr voidptr 29 | // Pointer to delegate the App (only for MacOS) 30 | ptr_delegate voidptr 31 | } 32 | 33 | // Parameters to configure the tray button. 34 | struct VTrayParams { 35 | identifier string 36 | tooltip string 37 | icon string 38 | on_click fn (menu_item &MenuItem) = unsafe { nil } 39 | } 40 | 41 | fn C.vtray_init(params &VTrayParams, num_items usize, items []&MenuItem) &VTray 42 | fn C.vtray_run(tray &VTray) 43 | fn C.vtray_exit(tray &VTray) 44 | fn C.vtray_set_icon(icon &char, tray &VTray) 45 | fn C.vtray_set_tooltip(tooltip &char, tray &VTray) 46 | -------------------------------------------------------------------------------- /src/lib.v: -------------------------------------------------------------------------------- 1 | module vtray 2 | 3 | // Tray is the main struct that represents the tray app. 4 | [heap; noinit] 5 | pub struct Tray { 6 | mut: 7 | instance &VTray = unsafe { nil } 8 | icon string 9 | identifier string 10 | tooltip string 11 | items []&MenuItem 12 | callbacks map[int]ItemCallback 13 | last_id int = 1 14 | } 15 | 16 | pub struct MenuItem { 17 | pub: 18 | id int 19 | text string 20 | checked bool 21 | checkable bool 22 | disabled bool 23 | } 24 | 25 | [params] 26 | pub struct CreatOptions { 27 | identifier string = 'VTray' 28 | tooltip string 29 | } 30 | 31 | // MenuItem is a menu item that can be added to the tray. 32 | [params] 33 | pub struct MenuItemOptions { 34 | checked bool 35 | checkable bool 36 | disabled bool 37 | on_click ?ItemCallback 38 | } 39 | 40 | type ItemCallback = fn () | fn (menu_item &MenuItem) 41 | 42 | // create creates the tray. 43 | // On macOS, the tray icon size must be 22x22 pixels to be rendered correctly. 44 | pub fn create(icon_path string, opts CreatOptions) &Tray { 45 | return &Tray{ 46 | icon: icon_path 47 | identifier: opts.identifier 48 | tooltip: opts.tooltip 49 | } 50 | } 51 | 52 | // add_item adds an item to the tray. 53 | pub fn (mut t Tray) add_item(text string, opts MenuItemOptions) { 54 | id := t.last_id++ 55 | t.items << &MenuItem{ 56 | id: id 57 | text: text 58 | checked: opts.checked 59 | checkable: opts.checkable 60 | disabled: opts.disabled 61 | } 62 | if cb := opts.on_click { 63 | t.callbacks[id] = cb 64 | } 65 | } 66 | 67 | // set_icon sets the tray icon. 68 | pub fn (t &Tray) set_icon(icon string) { 69 | C.vtray_set_icon(icon.str, t.instance) 70 | } 71 | 72 | // set_tooltip sets the tray tooltip. 73 | pub fn (t &Tray) set_tooltip(tooltip string) { 74 | C.vtray_set_tooltip(tooltip.str, t.instance) 75 | } 76 | 77 | // get_item returns the menu item with the given text. 78 | pub fn (t &Tray) get_item(item string) ?&MenuItem { 79 | return t.items.filter(it.text == item)[0] or { return none } 80 | } 81 | 82 | // run runs the tray app. 83 | pub fn (mut t Tray) run() { 84 | t.instance = C.vtray_init(&VTrayParams{ 85 | identifier: t.identifier 86 | tooltip: t.tooltip 87 | icon: t.icon 88 | on_click: fn [t] (menu_item &MenuItem) { 89 | cb := t.callbacks[menu_item.id] or { return } 90 | match cb { 91 | fn (menu_item &MenuItem) { 92 | cb(menu_item) 93 | } 94 | fn () { 95 | cb() 96 | } 97 | } 98 | } 99 | }, usize(t.items.len), t.items.data) 100 | C.vtray_run(t.instance) 101 | } 102 | 103 | // destroy destroys the tray app and frees allocated memory. 104 | pub fn (t &Tray) destroy() { 105 | C.vtray_exit(t.instance) 106 | } 107 | -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'vtray' 3 | description: 'VTray is a cross-platform V library to place an icon and menu in the notification area.' 4 | version: '0.1.0' 5 | license: 'MIT' 6 | dependencies: [] 7 | } --------------------------------------------------------------------------------